深入理解TCP协议及其源代码

Posted maizedu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解TCP协议及其源代码相关的知识,希望对你有一定的参考价值。

一.TCP三次握手建立连接

技术图片

 TCP是面向连接的,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。

三次握手的目的是同步连接双方的序列号和确认号并交换 TCP窗口大小信息。

三次握手的过程

1.第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;

2.第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;

3.第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。

三种状态

SYN_SENT
     在服务端监听后,客户端 socket 执行 connect 连接时,客户端发送 SYN 报文,此时客户端就进入 SYN_SENT 状态,等待服务端的确认。
SYN_RCVD
     表示服务端接受到了 SYN 报文,在正常情况下,这个状态是服务器端的 socket 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,因为一般来说会立即回复一个 ACK ,当收到客户端的 ACK 报文后,它会进入到 ESTABLISHED 状态。
ESTABLISHED
    表示连接已经建立

 

二.TCP握手过程中相关的内核函数

TCP的三次握手过程中,客户端connect和服务端accept建立起连接时的过程,在内核socket接口层对应着sock->opt->connect和sock->opt->accept两个函数指针,

在TCP协议中这两个函数指针对应着tcp_v4_connect函数和inet_csk_accept函数。

tcp_v4_connect函数

140/* This will initiate an outgoing connection. */
141int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
142{
...
   rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
                 RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                 IPPROTO_TCP,
                 orig_sport, orig_dport, sk);
...
   /* Socket identity is still unknown (sport may be zero).
    * However we set state to SYN-SENT and not releasing socket
    * lock select source port, enter ourselves into the hash tables and
    * complete initialization after this.
    */
   tcp_set_state(sk, TCP_SYN_SENT);//设置TCP_SYN_SENT
...
   rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
                  inet->inet_sport, inet->inet_dport, sk);
...
   err = tcp_connect(sk);//实际构造SYN报文段,并发送SYN报文段
...
264}
265EXPORT_SYMBOL(tcp_v4_connect);

inet_stream_connect源码         

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
            int addr_len, int flags)
{
    int err;
 
    lock_sock(sock->sk);
    err = __inet_stream_connect(sock, uaddr, addr_len, flags);
    release_sock(sock->sk);
    return err;
}
 
/*
 *    Connect to a remote host. There is regrettably still a little
 *    TCP ‘magic‘ in here.
 */
 
//1. 检查socket地址长度和使用的协议族。
//2. 检查socket的状态,必须是SS_UNCONNECTED或SS_CONNECTING。
//3. 调用tcp_v4_connect()来发送SYN包。
//4. 等待后续握手的完成:
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
              int addr_len, int flags)
{
    struct sock *sk = sock->sk;
    int err;
    long timeo;
 
    if (addr_len < sizeof(uaddr->sa_family))
        return -EINVAL;
 
    //检查协议族
    if (uaddr->sa_family == AF_UNSPEC) {
        err = sk->sk_prot->disconnect(sk, flags);
        sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
        goto out;
    }
 
    switch (sock->state) {
    default:
        err = -EINVAL;
        goto out;
    case SS_CONNECTED:
        //已经是连接状态
        err = -EISCONN;
        goto out;
    case SS_CONNECTING:
        //正在连接
        err = -EALREADY;
        /* Fall out of switch with err, set for this state */
        break;
    case SS_UNCONNECTED:
        err = -EISCONN;
        if (sk->sk_state != TCP_CLOSE)
            goto out;
 
        //对于流式套接字,sock->ops为 inet_stream_ops -->         inet_stream_connect  --> tcp_prot  --> tcp_v4_connect
 
        //对于数据报套接字,sock->ops为 inet_dgram_ops --> inet_dgram_connect           --> udp_prot  --> ip4_datagram_connect
        err = sk->sk_prot->connect(sk, uaddr, addr_len);
        if (err < 0)
            goto out;
 
        //协议方面的工作已经处理完成了,但是自己的一切工作还没有完成,所以切换至正在连接中
        sock->state = SS_CONNECTING;
 
        /* Just entered SS_CONNECTING state; the only
         * difference is that return value in non-blocking
         * case is EINPROGRESS, rather than EALREADY.
         */
        err = -EINPROGRESS;
        break;
    }
 
    //获取阻塞时间timeo。如果socket是非阻塞的,则timeo是0
    //connect()的超时时间为sk->sk_sndtimeo,在sock_init_data()中初始化为MAX_SCHEDULE_TIMEOUT,表示无限等待,可以通过SO_SNDTIMEO选项来修改
    timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
 
    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
        int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
                tcp_sk(sk)->fastopen_req &&
                tcp_sk(sk)->fastopen_req->data ? 1 : 0;
 
        /* Error code is set above */
 
    //如果socket是非阻塞的,那么就直接返回错误码-EINPROGRESS。
    //如果socket为阻塞的,就调用inet_wait_for_connect(),通过睡眠来等待。在以下三种情况下会被唤醒:
    //(1) 使用SO_SNDTIMEO选项时,睡眠时间超过设定值,返回0。connect()返回错误码-EINPROGRESS。
    //(2) 收到信号,返回剩余的等待时间。connect()返回错误码-ERESTARTSYS或-EINTR。
    //(3) 三次握手成功,sock的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,
    if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
            goto out;
 
        err = sock_intr_errno(timeo);
        //进程收到信号,如果err为-ERESTARTSYS,接下来库函数会重新调用connect()
        if (signal_pending(current))
            goto out;
    }
 
    /* Connection was closed by RST, timeout, ICMP error
     * or another process disconnected us.
     */
    if (sk->sk_state == TCP_CLOSE)
        goto sock_error;
 
    /* sk->sk_err may be not zero now, if RECVERR was ordered by user
     * and error was received after socket entered established state.
     * Hence, it is handled normally after connect() return successfully.
     */
 
    //更新socket状态为连接已建立
    sock->state = SS_CONNECTED;
 
    //清除错误码
    err = 0;
out:
    return err;
 
sock_error:
    err = sock_error(sk) ? : -ECONNABORTED;
    sock->state = SS_UNCONNECTED;
 
    //如果使用的是TCP,则sk_prot为tcp_prot,disconnect为tcp_disconnect()
    if (sk->sk_prot->disconnect(sk, flags))
        //如果失败
        sock->state = SS_DISCONNECTING;
    goto out;
}

 

三.跟踪调试握手过程

通过给tcp_v4_connect inet_csk_accept __sys_socket,__sys_bind,__sys_listen,__sys_connect打上断点,追踪代码执行过程

技术图片

 追踪可以发现:

服务器端先执行__sys_socket函数创建套接字,接着调用__sys_bind函数绑定套接字和服务器的地址,然后调用__sys_connect监听。

接着会调用__sys_accept4,进入inet_csk_accept函数,此时还未有客户端请求连接,所以队列为空,进入inet_csk_wait_for_connect的for循环,直到客户端请求连接。

技术图片

 技术图片

客户端先调用__sys_socket创建套接字,然后调用__sys_connect请求建立连接,其中tcp_v4_connect是真正实现连接的函数,此函数设置构造SYN,并发出去。

技术图片

参考资料:

https://github.com/mengning/net

以上是关于深入理解TCP协议及其源代码的主要内容,如果未能解决你的问题,请参考以下文章

深入理解TCP协议及其源代码

深入理解TCP协议及其源代码

深入理解TCP协议及其源代码

深入理解TCP协议及其源代码

深入理解TCP协议及其源代码

深入理解TCP协议及其源代码