深入理解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,并发出去。