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

我选择的问题是:connect及bind、listen、accept背后的三次握手

1.TCP建立连接的三次握手过程

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

  1. 第一次握手:客户端尝试连接服务器,向服务器发送syn(全称是同步序列编号)报文,syn=i,客户端进入SYN_SEND状态等待服务器确认
  2. 第二次握手:服务器接收客户端syn报文并确认(ack=i+1),同时向客户端发送一个新的SYN报文(syn=j),即SYN+ACK报文,此时服务器进入SYN_RECV状态
  3. 第三次握手:客户端收到服务器的SYN+ACK报文,向服务器发送确认报文ACK(ack=j+1),此报文发送并被客户端接收后,客户端和服务器进入ESTABLISHED状态,完成三次握手

2.探究使用Linux Socket api建立TCP连接的过程

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

 从创建socket,到建立连接接收数据,最后关闭socket的过程如上图所示。其中,和建立连接有关系的socket api主要是:connect、bind、listen和accept

为了探究建立连接时发生了什么,和TCP三次握手有什么关系,我们使用之前实验所写的hello/hi程序,用gdb为这四个函数打上断点,并使用wireshark监视相应端口,抓取数据包

当服务端运行bind,listen后,并没有捕获到任何数据包

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

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

直到客户端运行connect后,才捕获到TCP三次握手发送的数据包,如下图所示

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

可以通过抓取的数据包信息看到Socket是如何建立TCP连接的

  1. 由客户端(44434端口)发送SYN数据报给服务端(65432端口),其中seq=0(这里和后面的seq,都是显示的相对seq,实际并不是0)
  2. 服务端返回SYN+ACK数据报给客户端,其中ack=1,seq=0
  3. 客户端返回ACK数据报,其中ack=1

通过这个实践可以推测,TCP的三次握手是在connect和accept之间完成的,bind和listen只是完成绑定和监听的功能

3.从源码角度分析TCP三次握手的过程

在上一个实验探究Socket底层是如何实现多态机制的时候,我们发现socket结构体中有一个名为ops的结构体指针,结构体中又通过函数指针绑定了具体的底层函数,完成了connect、accept的实现。在struct proto tcp_prot的初始化中我们可以找到对应的绑定函数。

struct proto tcp_prot = {
    .name            = "TCP",
    .owner            = THIS_MODULE,
    .close            = tcp_close,
    .pre_connect    = tcp_v4_pre_connect,
    .connect        = tcp_v4_connect,
    .disconnect        = tcp_disconnect,
    .accept            = inet_csk_accept,

    ...
};

可以看到,socket->ops->connect绑定了函数tcp_v4_connect,socket->ops->accept绑定了inet_csk_accept

对tcp_v4_connect的部分源码分析

...
 
    //设置套接字状态,从CLOSE变为TCP_SYN_SENT,对应客户端从CLOSED->SYN_SENT这一过程
    tcp_set_state(sk, TCP_SYN_SENT);
    //将套接字sk放入TCP连接管理哈希链表中
    err = inet_hash_connect(&tcp_death_row, sk);
    if (err)
        goto failure;
   //为连接分配一个随机的空闲端口
    err = ip_route_newports(&rt, IPPROTO_TCP,
                inet->inet_sport, inet->inet_dport, sk);
    if (err)
        goto failure;
 
...
    
...
 
if (!tp->write_seq)
        //初始化报文内容
        tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
                               inet->inet_daddr,
                               inet->inet_sport,
                               usin->sin_port);
 
    inet->inet_id = tp->write_seq ^ jiffies;
    //构建并发送SYN数据报
    err = tcp_connect(sk);
    rt = NULL;
    if (err)
        goto failure;
 
...

对inet_csk_accept的部分源码分析

在分析代码前我们需要了解,套接字有监听套接字和具体通信的套接字(accept返回的那个)。监听套接字的扩展结构inet_connection_sock中存在icsk_accept_queue成员,此成员中有两个队列,一个用于完全建立连接(完成三次握手)的队列,此队列项中会包含新建的
用于通信的sock结构,在进程不在阻塞获得此sock结构后会把此队列项从完全建立连接的队列删除.此队列的最大长度即是listen(int s, int backlog)中第二个参数指定的;另一个队列是半连接队列,即还没有完成三次握手的队列项会加入到此队列,此队列项中的sock完成三次握手后会从此队列中移除,添加到完全建立连接的队列中

...
//检查套接字是否处于监听状态(应该是在调用listen时设置的)
    error = -EINVAL;
    if (sk->sk_state != TCP_LISTEN)
        goto out_err;
 
    //在监听套接字上的连接队列如果为空(没有任何连接完成)
    if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
 
        //设置接收超时时间,若调用accept的时候设置了O_NONBLOCK,表示马上返回不阻塞进程
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
 
        error = -EAGAIN;
        if (!timeo)//如果是非阻塞模式timeo为0 则马上返回
            goto out_err;
 
        //将进程阻塞,等待连接的完成,inet_csk_wait_for_connect核心是一个循环,等待三次握手中,客户端发来的最后一个ACK报文
        error = inet_csk_wait_for_connect(sk, timeo);
        if (error)
            goto out_err;
    }
 
    //在监听套接字建立连接的队列中删除此request_sock连接项 并返回建立连接的sock
    newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
 
    //套接字状态变为TCP_SYN_RECV,对应连接建立完成,服务端进入ESTABLISHED状态
    WARN_ON(newsk->sk_state == TCP_SYN_RECV)

分析这两段代码后,我们对TCP连接的建立已经有了一部分认知,tcp_v4_connect()会发送SYN报文开始三次握手,而inet_csk_accept接收来自客户端的ACK报文,标志着TCP连接建立完成。

三次握手的分析还并不完整,服务器端是如何接收第一次握手发来的SYN数据报,并返回SYN+ACK数据报的?实际上服务器端接收到SYN报文后,最终会调用tcp_v4_do_rcv()进行处理, 和tcp_send_ack()一起返回第二次握手中的SYN+ACK报文,客户端则是使用tcp_send_ack() 返回最后的ACK报文。受限于篇幅,不再对这些函数的源码进行分析