目录
前言
TCP客户端想通信要先connect一下,但我们大多数人只停留在“成功建连、失败报错”的表层认知,connect阻塞和非阻塞怎么实现的?连接队列是怎么工作的?排查网络故障时只会抓包,却不知道对应内核的哪种行为,导致排查效率极低。三次握手过程涉及到网络包的收发,本文暂时忽略收发过程,后续文章会详细分享。
一、用户态connect基础
函数原型
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解析:
- sockfd:通信的 socket 文件描述符,由 socket() 系统调用创建
- addr:存储服务器IP与端口信息的地址结构体指针
- addrlen:addr指针指向的内存的大小,IPv4为 sizeof(struct sockaddr_in)
返回值:成功返回0,失败返回-1
业务层简单示例
// 1. 创建客户端socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket create failed");
return 1;
}
// 2. 配置服务端地址 127.0.0.1:8080
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// 3. 发起连接
int ret = connect(fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
二、三次握手过程
2.1 三次握手过程示意图

2.2 服务端listen
前文《深入Linux网络子系统03:调用 listen 之后,内核发生了哪些事?》中详细介绍过listen过程中内核做了哪些事情,这里简单总结一下:
- 将监听 socket 从 TCP_CLOSE 切换为 TCP_LISTEN ,使其能够接收客户端连接请求
- 初始化半连接队列和全连接队列,根据backlog和somaxconn设置队列长度上限
2.3 客户端connect
客户端调用connect会执行内核__inet_stream_connect方法,关键行为:
- 调用tcp_v4_connect,这里面会完成:
- 路由查找
- sock状态设置为
TCP_SYN_SENT - 源端口选择,将sock加入ehash,此时四元组信息齐全。hashinfo介绍参考前文《深入Linux网络子系统02:bind()到底做了什么?》
// net/ipv4/tcp_ipv4.c
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
// 路由查找
ip_route_connect(......);
// 状态改为 SYN_SENT,此时源端口可能还是 0
tcp_set_state(sk, TCP_SYN_SENT);
// 分配源端口并加入连接哈希表(如果没 bind,这里自动选一个)
inet_hash_connect(......);
// 构造并发送第一个 SYN 包
tcp_connect(sk);
}
- tcp_v4_connect会调用tcp_connect,构造并发送第一个 SYN 包,同时启动重传定时器。至此,三次握手正式启动
// net/ipv4/tcp_output.c
int tcp_connect(struct sock *sk)
{
// 把 SYN 包放入发送队列
tcp_connect_queue_skb(sk, buff);
// 发送 SYN
tcp_transmit_skb(......);
// 启动重传定时器
// /proc/sys/net/ipv4/tcp_syn_retries 重传次数
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, ......);
}
- 计算超时时间,判断是否是非阻塞模式。阻塞模式下,内核将当前进程休眠挂起,不占用CPU资源,持续等待服务端响应;非阻塞模式下,内核发送SYN报文后,不会等待握手结果,直接返回EINPROGRESS
2.4 服务端收到SYN
客户端发送的第一个SYN会由内核中的tcp_v4_rcv接收,关键行为:
- 根据SYN包中的四元组信息在全局hashinfo中查找监听sock,此时其状态为 TCP_LISTEN
- 调用tcp_v4_do_rcv处理后续握手过程
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
// 用目的地址+端口查找监听 sock
sk = __inet_lookup_skb(&tcp_hashinfo, ......);
tcp_v4_do_rcv(sk, skb);
}
- tcp_rcv_state_process是TCP 状态机收包入口——处理除了 TCP_ESTABLISHED 和 TCP_TIME_WAIT之外所有状态的入站报文,服务端收到SYN包时,会调用tcp_v4_conn_request -> tcp_conn_request
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
switch (sk->sk_state)
{
// 服务端接收客户端连接请求,第一个 SYN 包
case TCP_LISTEN:
// 确定是 SYN 包
if (th->syn)
// 实际调用 tcp_v4_conn_request
icsk->icsk_af_ops->conn_request(sk, skb);
}
}
- tcp_conn_request方法中:
- 首先会判断半连接队列和全连接队列是否已满,全连接队列满的话,直接丢弃SYN包,半连接队列情况分析见下表。
- 创建半连接并设置其状态为 TCP_NEW_SYN_RECV
- 没有 syncookies,且半连接队列已用 3/4,且对端未被验证过,直接丢包,防止 SYN Flood 填满队列
- 添加到半连接队列中,并且插入ehash中
- 发送 synack ,调用 tcp_v4_send_synack
| 场景 | 行为 |
|---|---|
| 普通 SYN + 队列没满 | 正常存 req |
| 普通 SYN + 队列满 + syncookies 开 | 发 SYN Cookie,不存 req |
| 普通 SYN + 队列满 + syncookies 关 | 直接丢包 |
| 普通 SYN + sysctl_tcp_syncookies == 2 | 强制走 SYN Cookie(不管队列满没满) |
// net/ipv4/tcp_input.c
int tcp_conn_request(......)
{
// 判断半连接队列是否已满,最大值是 sk_max_ack_backlog
if (net->ipv4.sysctl_tcp_syncookies == 2 || inet_csk_reqsk_queue_is_full(sk) ...)
{
......
}
// 判断全连接队列是否已满,最大值是 sk_max_ack_backlog
if (sk_acceptq_is_full(sk))
goto drop;
// 创建半连接并设置状态为 TCP_NEW_SYN_RECV
req = inet_reqsk_alloc(rsk_ops, sk,...);
// 添加到半连接队列中,并且插入ehash中
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
// 发送 SYN + ACK ,调用 tcp_v4_send_synack
af_ops->send_synack(......);
}
2.5 客户端收到SYNACK
客户端接收synack包同样会走到tcp_rcv_state_process方法中,客户端的sock状态为TCP_SYN_SENT
// // net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
// 客户端收到服务端 SYNACK 包
case TCP_SYN_SENT:
tcp_rcv_synsent_state_process(sk, skb, th);
}
tcp_rcv_synsent_state_process方法中:
- 完成建立连接,将状态设置为
TCP_ESTABLISHED - 唤醒阻塞的 connect()
- 发送ACK
// net/ipv4/tcp_input.c
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,...)
{
// 完成连接,状态设置为 TCP_ESTABLISHED
tcp_finish_connect(sk, skb);
// 唤醒等待握手完成线程,实际调用 sock_def_wakeup
sk->sk_state_change(sk);
// 唤醒异步等待(epoll 等)
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
// 发送ACK
tcp_send_ack(sk);
}
2.6 服务端收到ACK
服务端收到ack包进入tcp_v4_rcv方法中:
- 根据ack中的四元组信息可以找到之前创建的半连接请求,然后调用tcp_check_req:
创建子 socket,其状态设为 TCP_SYN_RECV,这个状态极为短暂,马上在tcp_rcv_state_process设置成TCP_ESTABLISHED- 将连接请求从半连接队列中删除,并添加到全连接队列中
- 创建子 socket 实际调用 tcp_v4_syn_recv_sock
// net/ipv4/tcp_minisocks.c
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
bool fastopen)
{
// 创建子 socket
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
// 将连接请求从半连接队列中删除,并添加到全连接队列中
return inet_csk_complete_hashdance(sk, child, req, own_req);
}
// net/ipv4/tcp_ipv4.c
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst,
struct request_sock *req_unhash,
bool *own_req)
{
// 创建新的sock用于和客户端通信,状态设为 TCP_SYN_RECV
newsk = tcp_create_openreq_child(sk, req, skb);
// 加入 ehash
inet_ehash_nolisten(newsk, req_to_sk(req_unhash));
}
- 调用tcp_rcv_state_process将连接状态设为
TCP_ESTABLISHED - 唤醒阻塞中的accept
// // net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
// 服务端收到客户端ACK
case TCP_SYN_RECV:
// 状态设为TCP_ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
}
三、总结
三次握手过程总结如下:
- 服务端首先调用listen打开监听,初始化连接队列
- 客户端调用connect,sock状态设置为
TCP_SYN_SENT,选择本地端口,构造并发送第一个 syn 包,同时启动重传定时器。至此,三次握手正式启动 - 服务端收到syn包,判断半连接队列和全连接队列是否已满,创建半连接请求,添加到半连接队列中,发送synack
- 客户端收到synack,完成建立连接,sock状态设置为
TCP_ESTABLISHED,唤醒阻塞的 connect,并发送ack - 服务端收到ack,创建子 socket,将连接请求从半连接队列中删除,添加到全连接队列中,子 socket 状态设置为
TCP_ESTABLISHED,唤醒阻塞中的accept。至此三次握手已完成
266



被折叠的 条评论
为什么被折叠?



