本文从TCP的基本概念和TCP三次握手的过程入手,结合socket API中的connect及bind、listen、accept函数对TCP协议进行深入理解。
一、TCP的基本概念
TCP协议:TCP协议提供提供一种面向连接的、可靠的字节流服务。TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。
SYN:同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。
ACK: 确认字符(Acknowledge character)。表示发来的数据已确认接收无误。
三次握手:TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
TCP三次握手过程: 1.客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。 2.服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。 3.客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。二、源码阅读
TCP协议相关的代码主要集中在linux-5.0.1/net/ipv4/目录下,由之前的分析可知在linux5.0.1/net/ipv4文件中定义的结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数,我们分析出socket API中的sock->ops->bind函数的传输层接口函数为inet_csk_get_port函数。同理,不难分析出sock->opt->connect和sock->opt->accept函数对应的传输层接口函数分别为tcp_v4_connect和inet_csk_accept函数。
我们接下来继续对tcp_v4_connect函数和inet_csk_accept函数进行分析:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) { struct inet_sock *inet = inet_sk(sk); struct tcp_sock *tp = tcp_sk(sk); struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; //指向高速缓冲区的路由 struct rtable *rt; __be32 daddr, nexthop; int tmp; int err; //地址长度检查 if (addr_len < sizeof(struct sockaddr_in)) return -EINVAL; //协议族检查 if (usin->sin_family != AF_INET) return -EAFNOSUPPORT; //是否设置源路由选项 nexthop = daddr = usin->sin_addr.s_addr; if (inet->opt && inet->opt->srr) { if (!daddr) return -EINVAL; nexthop = inet->opt->faddr; } //选路由,路由保存在rt->rt_dst中 tmp = ip_route_connect(&rt, nexthop, inet->inet_saddr, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, IPPROTO_TCP, inet->inet_sport, usin->sin_port, sk, 1); if (tmp < 0) { if (tmp == -ENETUNREACH) IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); return tmp; } //组传送地址、广播地址则返回错误 if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) { ip_rt_put(rt); return -ENETUNREACH; } //如果没有设置源路由ip选项,就使用路由表寻址的路由 if (!inet->opt || !inet->opt->srr) daddr = rt->rt_dst; if (!inet->inet_saddr) inet->inet_saddr = rt->rt_src; inet->inet_rcv_saddr = inet->inet_saddr; if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) { /* Reset inherited state */ tp->rx_opt.ts_recent = 0; tp->rx_opt.ts_recent_stamp = 0; tp->write_seq = 0; } //获取套接字最近使用的时间 if (tcp_death_row.sysctl_tw_recycle && !tp->rx_opt.ts_recent_stamp && rt->rt_dst == daddr) { struct inet_peer *peer = rt_get_peer(rt); /* * VJ's idea. We save last timestamp seen from * the destination in peer table, when entering state * TIME-WAIT * and initialize rx_opt.ts_recent from it, * when trying new connection. */ if (peer != NULL && (u32)get_seconds() - peer->tcp_ts_stamp <= TCP_PAWS_MSL) { tp->rx_opt.ts_recent_stamp = peer->tcp_ts_stamp; tp->rx_opt.ts_recent = peer->tcp_ts; } } inet->inet_dport = usin->sin_port; inet->inet_daddr = daddr; inet_csk(sk)->icsk_ext_hdr_len = 0; if (inet->opt) inet_csk(sk)->icsk_ext_hdr_len = inet->opt->optlen; tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT; /* 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_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; /* OK, now commit destination to socket. */ sk->sk_gso_type = SKB_GSO_TCPV4; sk_setup_caps(sk, &rt->u.dst); if (!tp->write_seq) //初始化TCP数据段序列号 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包调用tcp_transmit_skb发送到IP层 err = tcp_connect(sk); rt = NULL; if (err) goto failure; return 0; failure: /* * This unhashes the socket and releases the local port, * if necessary. */ //失败设置套接字状态为CLOSED tcp_set_state(sk, TCP_CLOSE); ip_rt_put(rt); sk->sk_route_caps = 0; inet->inet_dport = 0; return err; }
其中参数sk为套接字指针,uaddr为sockaddr类型的地址,addr_len为套接字地址长度。
tcp_v4_connect函数的具体工作:
1.初始化
检查目的IP长度、协议、如果设置了源路由选项而且数据包目的地址不为空,则从用户给定的源路由列表中取一个IP地址赋给网关地址。
2.选择路由
根据目的ip、目的端口、网络设备接口调用ip_route_connect选路由,路由结构保存到rt->rt_dst中,实际调用的函数是ip_route_output_flow,如果是广播地址、组地址就返回。
3.设置连接状态
调用tcp_set_state设置套接字状态为TCP_SYN_SENT,本把套接字sk加入到连接管理哈希链表中,为连接分配一个临时端口。
4.发送连接请求
初始化第一个序列号,调用tcp_connect函数完成建立连接,包括发送SYN,tcp_connect将创建号的SYN数据段加入到套接字发送队列,最后调用tcp_transmit_skb数据包发送到IP层。
5.连接建立失败
如果连接建立失败,就将TCP状态切换回CLOSE,将套接字从连接管理hash表中移除,释放本地端口。
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern) { struct inet_connection_sock *icsk = inet_csk(sk); struct request_sock_queue *queue = &icsk->icsk_accept_queue; struct request_sock *req; struct sock *newsk; int error; //获取sock锁将sk->sk_lock.owned设置为1 //此锁用于进程上下文和中断上下文 lock_sock(sk); /* We need to make sure that this socket is listening, * and that it has something pending. */ //用于accept的sock必须处于监听状态 error = -EINVAL; if (sk->sk_state != TCP_LISTEN) goto out_err; /* Find already established connection */ //在监听套接字上的连接队列如果为空 if (reqsk_queue_empty(queue)) { //设置接收超时时间,若调用accept的时候设置了O_NONBLOCK,表示马上返回不阻塞 long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); /* If this is a non blocking socket don't sleep */ error = -EAGAIN; if (!timeo)//如果是非阻塞模式timeo为0,则马上返回 goto out_err; //将进程阻塞,等待连接的完成 error = inet_csk_wait_for_connect(sk, timeo); if (error)//返回值为0说明监听套接字的完全建立连接队列不为空 goto out_err; } //在监听套接字建立连接的队列中删除此request_sock连接项,并返回建立连接的sock //三次握手的完成是在tcp_v4_rcv中完成的 req = reqsk_queue_remove(queue, sk); newsk = req->sk; //此时sock的状态应为TCP_ESTABLISHED if (sk->sk_protocol == IPPROTO_TCP && tcp_rsk(req)->tfo_listener) { spin_lock_bh(&queue->fastopenq.lock); if (tcp_rsk(req)->tfo_listener) { /* We are still waiting for the final ACK from 3WHS * so can't free req now. Instead, we set req->sk to * NULL to signify that the child socket is taken * so reqsk_fastopen_remove() will free the req * when 3WHS finishes (or is aborted). */ req->sk = NULL; req = NULL; } spin_unlock_bh(&queue->fastopenq.lock); } out: release_sock(sk); if (req) reqsk_put(req); return newsk; out_err: newsk = NULL; req = NULL; *err = error; goto out; } EXPORT_SYMBOL(inet_csk_accept);
其中参数sk为套接字指针,flags为文件标志(例如:O_NONBLOCK),err用于接收错误。
inet_csk_accept函数的具体工作:
1.初始化
从队列取带建立连接的套接字。其中icsk_accept_queue在listen时初始化,存放于SYN_RECV状态等待建立连接的套接字。
2.设置监听状态
当前状态必须为TCP_LISTEN,否则出错。
3.设置定时器
若设置了O_NONBLOCK非阻塞,队列没有数据直接返回;否则inet_csk_wait_for_connect一直阻塞,等待新的连接,直至timeo超时。
4.处理请求队列
三、运行跟踪
使用gdb在分析出的函数处设置断点