我选择的问题是:connect及bind、listen、accept背后的三次握手
1.TCP建立连接的三次握手过程
- 第一次握手:客户端尝试连接服务器,向服务器发送syn(全称是同步序列编号)报文,syn=i,客户端进入SYN_SEND状态等待服务器确认
- 第二次握手:服务器接收客户端syn报文并确认(ack=i+1),同时向客户端发送一个新的SYN报文(syn=j),即SYN+ACK报文,此时服务器进入SYN_RECV状态
- 第三次握手:客户端收到服务器的SYN+ACK报文,向服务器发送确认报文ACK(ack=j+1),此报文发送并被客户端接收后,客户端和服务器进入ESTABLISHED状态,完成三次握手
2.探究使用Linux Socket api建立TCP连接的过程
从创建socket,到建立连接接收数据,最后关闭socket的过程如上图所示。其中,和建立连接有关系的socket api主要是:connect、bind、listen和accept
为了探究建立连接时发生了什么,和TCP三次握手有什么关系,我们使用之前实验所写的hello/hi程序,用gdb为这四个函数打上断点,并使用wireshark监视相应端口,抓取数据包
当服务端运行bind,listen后,并没有捕获到任何数据包
直到客户端运行connect后,才捕获到TCP三次握手发送的数据包,如下图所示
可以通过抓取的数据包信息看到Socket是如何建立TCP连接的
- 由客户端(44434端口)发送SYN数据报给服务端(65432端口),其中seq=0(这里和后面的seq,都是显示的相对seq,实际并不是0)
- 服务端返回SYN+ACK数据报给客户端,其中ack=1,seq=0
- 客户端返回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报文。受限于篇幅,不再对这些函数的源码进行分析