TCP连接
概念基础
客户端的核心逻辑:
(1)创建socket。
(2)调用connect连接服务端地址和端口。
服务端核心逻辑:
(1)创建socket,服务端需要创建一个 socket 对象,用于监听传入的连接请求。
(2)bind()绑定本地的IP/Port二元组用以定位,将创建的 socket 绑定到一个具体的 IP 地址和端口上,便于客户端寻找。
(3)listen监听,监听绑定的地址和端口。
(4)最后accept接收客户端的的请求,客户端发起连接请求时,服务端调用 accept() 函数接受连接请求,并建立一个新的 socket 来处理与客户端之间的通信。
内核中三次握手转换图:
过程:
- client端发起主动连接,将自身状态置为TCP_SYN_SENT(连接请求SYN包),向服务器端发送一个SYN被置1的报文表示请求连接。
- server端在listen之后处于LISTEN状态,收到client发送的SYN之后,socket加入半连接队列,并设置其状态为TCP_NEW_SYN_RECV(等待客户端请求),向client发送ACK和SYN均置为1的数据包,表示收到请求并同意建立连接。
- client收到后,将自身状态置为ESTABLISHED,并向server端发送ACK置为1的数据包,表示接收到了该数据包。server端收到后查询半连接的表,拿出来创建新的socket连接,并设置其状态为TCP_SYN_RECV,将其加入全连接队列,然后将状态置为TCP_SYN_RECV,三次握手完毕,连接建立成功,最后再将状态切换为TCP_FIN_WAIT(已经完成了数据传输并请求关闭连接,但是另一端还没有发送确认)等待连接关闭。
总体查看linux内核协议栈中三次握手关键源码函数关系图:
服务端:
listen
利用bind()
将套接字和ip/端口号进行绑定之后,为什么要在服务端listen之后,服务端才会接收到客户端发送的连接请求呢?剖析源码与connect系统调用过程一致,只是进入的函数不同SYSCALL_DEFINE2()
。
int inet_listen(struct socket *sock, int backlog)
{
...
old_state = sk->sk_state; // 保存当前的套接字状态
// 检查套接字的状态是否处于CLOSE或LISTEN,否则不允许监听
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
WRITE_ONCE(sk->sk_max_ack_backlog, backlog); // 设置允许排队的最大半连接数
// 如果套接字不是首次监听,允许调整backlog
if (old_state != TCP_LISTEN) {
// 读取系统配置的TCP快速打开设置
tcp_fastopen = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_fastopen);
// 判断是否启用TCP快速打开而无需显式套接字选项
if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
(tcp_fastopen & TFO_SERVER_ENABLE) &&
!inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
fastopen_queue_tune(sk, backlog); // 调整快速打开队列长度
tcp_fastopen_init_key_once(sock_net(sk)); // 初始化快速打开密钥
}
// 开始监听,并设置套接字为监听状态
err = inet_csk_listen_start(sk);
if (err)
goto out;
// 调用BPF程序回调,以处理任何用户定义的套接字选项
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
}
err = 0; // 设置错误代码为0,表示成功
...
}
listen()完成的核心:申请初始化全连接队列(链表)、半连接队列(哈希表)。
全连接队列长度如何确定?
设置全连接队列长度sk->sk_max_ack_backlog
,服务端全连接队列长度执行listen函数的时候传入的是backlog
和net.core.somaxconn
之间较小的值。
半连接队列长度如何确定?(过程比较复杂,后续分析)
客户端:
connect
connect()
函数通常由客户端发起,是三次握手的开始,服务端收到了SYN
之后回复ACK + SYN
并将该连接加入半连接队列,进入SYN_RCVD
状态,第三次握手收到ACK
后从半连接队列取出,加入全连接队列,此时的 socket 处于 ESTABLISHED
状态。客户端发起连接会创建一个socket调用connect函数进入系统调用,socket系统调用函数都在net/socket.c目录下,其中socket接口函数都定义在SYSCALL_DEFINE接口里,找到主要的相关SYSCALL_DEFINE定义如下:
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
int, addrlen)
{
return __sys_connect(fd, uservaddr, addrlen);
}
根据用户态传入的文件描述符fd查询socket内核对象,实际调用函数__sys_connect()
。
客户端发送SYN请求
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
int ret = -EBADF;
struct fd f;
f = fdget(fd);
if (f.file) {
struct sockaddr_storage address;
ret = move_addr_to_kernel(uservaddr, addrlen, &address);
if (!ret)
ret = __sys_connect_file(f.file, &address, addrlen, 0);
fdput(f);
}
return ret;
}
用户空间发起TCP连接调用sys_connect()
,通过判断fd是否存在将用户态传入的地址信息复制到内核空间,由于TCP连接属于字节流式套接字所以会调用inet_stream_connect
,而对于数据报类型套接字会调用inet_dgram_connect
发起连接,初始创建的socket状态就是SS_CONNECTED,根据switch判断去调用sk->sk_prot->connect套接字是否连接,此时未连接调用的是tcp_v4_connect
。
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags, int is_sendmsg)
{
struct sock *sk = sock->sk;
switch (sock->state) {
default:
err = -EINVAL;
goto out;
case SS_CONNECTED://此套接口已经和对端的套接口相连接了,即连接已经建立
err = -EISCONN;
goto out;
case SS_CONNECTING://此套接口正在尝试连接对端的套接口,即连接正在建立中
if (inet_sk(sk)->defer_connect)
err = is_sendmsg ? -EINPROGRESS : -EISCONN;
else
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;
if (BPF_CGROUP_PRE_CONNECT_ENABLED(sk)) {
err = sk->sk_prot->pre_connect(sk, uaddr, addr_len);
if (err)
goto out;
}
err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
goto out;
sock->state = SS_CONNECTING;
if (!err && inet_sk(sk)->defer_connect)
goto out;
/* Just entered SS_CONNECTING state; the only
* difference is that return value in non-blocking
* case is EINPROGRESS, rather than EALREADY.
*/
err = -EINPROGRESS;
break;
}
......
}
tcp_v4_connect
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_timewait_death_row *tcp_death_row;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct ip_options_rcu *inet_opt;
struct net *net = sock_net(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
struct flowi4 *fl4;
struct rtable *rt;
int err;
......
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
if (err)
goto failure;
sk_set_txhash(sk);
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet->inet_sport, inet->inet_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
rt = NULL;
goto failure;
}
......
}
将socket的状态设置为TCP_SYN_SENT,调用inet_hash_connect()
函数动态选择一个端口。
int inet_hash_connect(struct inet_timewait_death_row *death_row,
struct sock *sk)
{
u64 port_offset = 0;
if (!inet_sk(sk)->inet_num)
port_offset = inet_sk_port_offset(sk);//计算端口偏移量
return __inet_hash_connect(death_row, sk, port_offset,
__inet_check_established);
}
返回函数__inet_hash_connect()
,参数为port_offset随机数、__inet_check_established检查是否与现有ESTABLISH状态连接的连接冲突时用的函数。
int __inet_hash_connect(struct inet_timewait_death_row *death_row,
struct sock *sk, u64 port_offset,
int (*check_established)(struct inet_timewait_death_row *,
struct sock *, __u16, struct inet_timewait_sock **))
{
if (port) {
local_bh_disable();
ret = check_established(death_row, sk, port, NULL);
local_bh_enable();
return ret;
}
l3mdev = inet_sk_bound_l3mdev(sk);
inet_sk_get_local_port_range(sk, &low, &high);
high++; /* [32768, 60999] -> [32768, 61000[ */
remaining = high - low;
if (likely(remaining > 1))
remaining &= ~1U;
get_random_sleepable_once(table_perturb,
INET_TABLE_PERTURB_SIZE * sizeof(*table_perturb));
index = port_offset & (INET_TABLE_PERTURB_SIZE - 1);
offset = READ_ONCE(table_perturb[index]) + (port_offset >> 32);
offset %= remaining;
/* In first pass we try ports of @low parity.
* inet_csk_get_port() does the opposite choice.
*/
offset &= ~1U;
other_parity_scan:
port = low + offset;
for (i = 0; i < remaining; i += 2, port += 2) {
if (unlikely(port >= high))
port -= remaining;
if (inet_is_local_reserved_port(net, port))
continue;
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock_bh(&head->lock);
/* Does not bother with rcv_saddr checks, because
* the established check is already unique enough.
*/
inet_bind_bucket_for_each(tb, &head->chain) {
if (inet_bind_bucket_match(tb, net, port, l3mdev)) {
if (tb->fastreuse >= 0 ||
tb->fastreuseport >= 0)
goto next_port;
WARN_ON(hlist_empty(&tb->owners));
if (!check_established(death_row, sk,
port, &tw))
goto ok;
goto next_port;
}
}
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port, l3mdev);
if (!tb) {
spin_unlock_bh(&head->lock);
return -ENOMEM;
}
tb_created = true;
tb->fastreuse = -1;
tb->fastreuseport = -1;
goto ok;
next_port:
spin_unlock_bh(&head->lock);
cond_resched();
}
offset++;
if ((offset & 1) && remaining > 1)
goto other_parity_scan;
return -EADDRNOTAVAIL;
......
}
检查是否绑定端口号,如果已经设置过端口号直接调用check_established()
函数检查连接,将socket绑定到特定网络接口l3(网络层设备),获取本地端口范围,计算端口剩余数量存储于remaining,该参数的默认值是32768-61000,意味着端口与总可用量是61000-32768=28232个。如果觉得这个数字不够用,那么可以通过修改net.ipve4.ip_local_port_range内参参数来重新设置,计算随机数offset。
遍历剩余的端口范围,步长为2,因为之前已经确保了remaining为偶数,一直遍历查找到可用的端口。
具体实现细节:
-
如果端口号超出了上限,就从低位端口重新开始
-
检查端口是否是本地保留端口(判断端口是否在net.ipv4.ip_local_reserved_ports中),如果是,则跳过
-
根据端口号获取对应的哈希表
-
遍历哈希表中判断端口是否被使用,如果没有找到则说明可以使用,已使用过则调用
check_established
,如果check_established返回值0则说明端口号可以继续使用。 -
如果找到了匹配的端口则调用
inet_bind_bucket_create
申请一个inet_bind_bucket
来记录端口已经使用了,并用哈希表的形式管理起来。 -
找不到返回-EADDRNOTAVAIL
但是我们在确定可用端口的时候,需要从一个随机数开始一直遍历哈希表,如果端口充足那么循环很快结束,如果可用端口不足,一直循环遍历哈希表,直到我们找到可用端口停止遍历,这无疑会导致connect系统调用CPU开销增长,当我们确定好端口号之后就可以进行三次握手了,首先会通过
tcp_v4_connect
调用tcp_connect
函数根据sk中的信息构建SYN报文进行发送。
tcp_connect
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);
if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk))
return -EHOSTUNREACH; /* Routing failure or similar. */
tcp_connect_init(sk);
if (unlikely(tp->repair)) {
tcp_finish_connect(sk, NULL);
return 0;
}
buff = tcp_stream_alloc_skb(sk, sk->sk_allocation, true);
if (unlikely(!buff))
return -ENOBUFS;
tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
tcp_mstamp_refresh(tp);
tp->retrans_stamp = tcp_time_stamp(tp);
tcp_connect_queue_skb(sk, buff);
tcp_ecn_send_syn(sk, buff);
tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);
/* Send off SYN; include data in Fast Open. */
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
if (err == -ECONNREFUSED)
return err;
/* We change tp->snd_nxt after the tcp_transmit_skb() call
* in order to make this packet get counted in tcpOutSegs.
*/
WRITE_ONCE(tp->snd_nxt, tp->write_seq);
tp->pushed_seq = tp->write_seq;
buff = tcp_send_head(sk);
if (unlikely(buff)) {
WRITE_ONCE(tp->snd_nxt, TCP_SKB_CB(buff)->seq);
tp->pushed_seq = TCP_SKB_CB(buff)->seq;
}
TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);
/* Timer for repeating the SYN until an answer. */
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}
确定好可用端口后调用tcp_connect
,根据sk中的信息构建一个syn报文发送出去。
核心操作:
申请一个skb,并将其设置为SYN包
tcp_init_nondata_skb
初始化SYN数据包并将其添加到发送队列中- 调用
tcp_transmit_skb
函数发送数据包到网络层,传入参数:套接字指针sk、发送数据包的结构体指针sk_buff、ACK延迟是否存在、发送数据包分配内存 - 统计活跃连接次数
inet_csk_reset_xmit_timer
设置重传定时器timer,若超时则进行重发
三次握手中的第一次握手在客户端的层面完成,报文到达服务端,由服务端处理完毕后,第一次握手完成,客户端socket状态变为TCP_SYN_SENT。
服务端响应SYN
tcp_v4_do_rcv
服务端所有的TCP包(包括客户端发来的SYN握手请求)都经过网卡、软中断进入tcp_v4_rcv
。在该函数中根据网络包skb的TCP头信息中的目的IP信息查找当前处于listen状态的socket,然后继续进入tcp_v4_do_rcv
处理握手过程,因为listen状态的socket不会收到的进入预处理队列。
/*
* 函数名称:tcp_v4_do_rcv
* 功能描述:TCP 协议栈中用于处理接收到的 TCP 数据包的函数
* 输入参数:
* - sk: 指向当前 TCP 套接字的指针
* - skb: 指向要处理的数据包的 sk_buff 结构体指针
* 返回值:
* - 返回值为 0 表示处理成功
* - 返回值为负数表示处理失败
*/
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
enum skb_drop_reason reason; // 定义丢弃数据包的原因
struct sock *rsk; // 定义套接字指针用于存储要发送 Reset 数据包的套接字
// 套接字处于已建立连接的状态
if (sk->sk_state == TCP_ESTABLISHED) {
struct dst_entry *dst;
// 获取套接字关联的目的地
dst = rcu_dereference_protected(sk->sk_rx_dst,
lockdep_sock_is_held(sk));
// 保存接收数据包的哈希值和 NAPI ID
sock_rps_save_rxhash(sk, skb);
sk_mark_napi_id(sk, skb);
// 如果存在目的地,检查目的地的接口索引是否与数据包接收接口索引匹配
if (dst) {
if (sk->sk_rx_dst_ifindex != skb->skb_iif ||
!INDIRECT_CALL_1(dst->ops->check, ipv4_dst_check,
dst, 0)) {
RCU_INIT_POINTER(sk->sk_rx_dst, NULL);
dst_release(dst);
}
}
// 调用 tcp_rcv_established 函数进行处理
tcp_rcv_established(sk, skb);
return 0;
}
reason = SKB_DROP_REASON_NOT_SPECIFIED; // 初始化丢弃数据包的原因为未指定
// 如果校验和验证失败,跳转到处理 TCP 校验和错误的代码段
if (tcp_checksum_complete(skb))
goto csum_err;
// 如果套接字处于监听状态,进行监听状态处理路径
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_cookie_check(sk, skb);
// 如果 SYN 数据包不包含合法的 Cookie,丢弃数据包
if (!nsk)
goto discard;
// 如果 SYN 数据包包含合法的 Cookie,处理子套接字
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
} else
// 保存接收数据包的哈希值
sock_rps_save_rxhash(sk, skb);
// 处理 TCP 接收状态
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
reset:
// 发送 TCP Reset 数据包
tcp_v4_send_reset(rsk, skb);
discard:
// 释放数据包
kfree_skb_reason(skb, reason);
/* Be careful here. If this function gets more complicated and
* gcc suffers from register pressure on the x86, sk (in %ebx)
* might be destroyed here. This current version compiles correctly,
* but you have been warned.
*/
return 0;
csum_err:
// 处理 TCP 校验和错误
reason = SKB_DROP_REASON_TCP_CSUM;
trace_tcp_bad_csum(skb);
TCP_INC_STATS(sock_net(sk), TCP_MIB_CSUMERRORS);
TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
tcp_v4_do_rcv
中判断当前socket是TCP_LISTEN
状态后,首先会到tcp_v4_cookie_check
查看半连接队列。如果在半连接队列中没有找到对应的半连接对象,则会返回listen的socket(连接尚未创建);如果找到了就将该半连接socket返回。服务端第一次响应SYN的时候,半连接队列自然没有对应的半连接对象,所以返回的是原listen的sock。
tcp_rcv_state_process
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk); // 将传入的套接字结构体转换为TCP套接字结构体
struct inet_connection_sock *icsk = inet_csk(sk); // 将传入的套接字结构体转换为INET连接套接字结构体
const struct tcphdr *th = tcp_hdr(skb); // 获取TCP头部结构体
struct request_sock *req; // 请求套接字结构体指针
int queued = 0; // 用于标记是否有数据包被排队
bool acceptable; // 用于标记连接请求是否被接受
SKB_DR(reason); // 定义一个数据包丢弃的原因
switch (sk->sk_state) { // 根据当前套接字状态执行相应的操作
case TCP_CLOSE: // 当前状态为TCP关闭状态
SKB_DR_SET(reason, TCP_CLOSE); // 设置丢弃原因为TCP关闭
goto discard; // 丢弃数据包
case TCP_LISTEN: // 当前状态为TCP监听状态
if (th->ack) // 如果接收到的数据包是一个ACK包
return 1; // 直接返回,不做处理
if (th->rst) { // 如果接收到的数据包是一个RST包
SKB_DR_SET(reason, TCP_RESET); // 设置丢弃原因为TCP重置
goto discard; // 丢弃数据包
}
if (th->syn) { // 如果接收到的数据包是一个SYN包
if (th->fin) { // 如果同时还是一个FIN包
SKB_DR_SET(reason, TCP_FLAGS); // 设置丢弃原因为TCP标志错误
goto discard; // 丢弃数据包
}
/* It is possible that we process SYN packets from backlog,
* so we need to make sure to disable BH and RCU right there.
*/
rcu_read_lock(); // 读取RCU锁
local_bh_disable(); // 禁用本地BH
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0; // 尝试处理连接请求
local_bh_enable(); // 启用本地BH
rcu_read_unlock(); // 解锁RCU
if (!acceptable) // 如果连接请求未被接受
return 1; // 返回1,表示处理失败
consume_skb(skb); // 消耗数据包
return 0; // 返回0,表示处理成功
}
// 其他情况的处理...
}
}
TCP建立连接的关键函数,几乎所有状态的套接字,在收到报文时都会在这里完成处理。客户端传递SYN包,对于服务端来说,收到第一次握手报文时的状态为TCP_LISTEN,该函数根据socket状态进行不同的处理,其中conn_request是一个函数指针,指向tcp_v4_conn_request
。
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.mtu_reduced = tcp_v4_mtu_reduced,
};
tcp_v4_conn_request
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))//检查接收到的数据包是否发送到广播或多播地址
goto drop;
return tcp_conn_request(&tcp_request_sock_ops,
&tcp_request_sock_ipv4_ops, sk, skb);
drop:
tcp_listendrop(sk); // 丢弃套接字的监听状态
return 0;
}
检查接收到的数据包是否发送到广播或多播地址,是则丢弃数据包,否则调用tcp_conn_request()
处理TCP连接请求。
tcp_conn_request()
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
... ...
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie); // 分配请求 sock
if (!req)
goto drop; // 如果分配失败,则丢弃请求
req->syncookie = want_cookie; // 设置是否为 SYN cookie 请求
... ...
tcp_rsk(req)->tfo_listener = false; // 设置是否为 TFO 监听者
if (!want_cookie) {
req->timeout = tcp_timeout_init((struct sock *)req); // 初始化超时时间
inet_csk_reqsk_queue_hash_add(sk, req, req->timeout); // 添加请求到 hash 表
}
af_ops->send_synack(sk, dst, &fl, req, &foc,
!want_cookie ? TCP_SYNACK_NORMAL :
TCP_SYNACK_COOKIE,
skb); // 发送 SYN-ACK
if (want_cookie) { // 如果需要 SYN cookie,则释放请求并返回
reqsk_free(req);
return 0;
}
}
}
核心:处理TCP连接请求,判断接收队列是否已满(drop),新建连接请求控制块并将sk添加至半连接队列中。
请求控制块的操作指向rsk_ops
,新建连接请求控制块的时连接状态会更新为TCP_NEW_SYN_RECV
,此时状态为TCP_LISTEN
,调用inet_reqsk_alloc
分配sock值,inet_csk_reqsk_queue_hash_add
将sk保存在半连接队列syn_table去填充客户端信息,af_ops->send_synack()
发送一个 SYN-ACK 响应报文作为对 TCP 连接请求的响应。该函数将SYN传输到ip层。
服务端将SYN+ACK发送到IP
tcp_v4_send_synack
static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
struct flowi *fl,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
enum tcp_synack_type synack_type,
struct sk_buff *syn_skb)
{
// 从请求套接字结构中获取本地和远程地址
const struct inet_request_sock *ireq = inet_rsk(req);
// IPv4 特定的流结构
struct flowi4 fl4;
// 错误代码初始化为 -1
int err = -1;
// 用于构造 SYN+ACK 报文的 SKB 结构指针
struct sk_buff *skb;
// 获取路由
if (!dst && (dst = inet_csk_route_req(sk, &fl4, req)) == NULL)
return -1; // 如果无法获取路由,则返回错误
// 根据监听套接字、连接请求块和路由构造SYN+ACK数据包
skb = tcp_make_synack(sk, dst, req, foc, synack_type, syn_skb);
// 检查报文创建是否成功
if (skb) {
// 执行 TCP 校验和计算和验证
__tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr);
// 确定报文的 TOS(服务类型)值
u8 tos = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_reflect_tos) ?
(tcp_rsk(req)->syn_tos & ~INET_ECN_MASK) |
(inet_sk(sk)->tos & INET_ECN_MASK) :
inet_sk(sk)->tos;
// 如果未启用 ECN(显式拥塞通知),则将 ECN 位设置为 ECT(0)
if (!INET_ECN_is_capable(tos) &&
tcp_bpf_ca_needs_ecn((struct sock *)req))
tos |= INET_ECN_ECT_0;
// 锁定并对路由缓存进行读取
rcu_read_lock();
// 构建并发送报文
err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
ireq->ir_rmt_addr,
rcu_dereference(ireq->ireq_opt),
tos);
// 解锁路由缓存
rcu_read_unlock();
// 评估发送结果
err = net_xmit_eval(err);
}
// 返回发送报文的结果
return err;
}
核心:负责在接收到 SYN 报文后,调用ip_build_and_send_pkt()
函数TCP会构造出SYN+ACK 报文并直接发送给IP层,此时状态状态为TCP_NEW_SYN_RECV
。其中参数synack_type:要发送的 SYN+ACK 报文类型(普通 SYN+ACK 或带 Cookie 的 SYN+ACK;syn_skb:指向表示从客户端接收到的 SYN 报文的 SKB(套接字缓冲区)结构的指针。
客户端响应SYN+ACK
tcp_rcv_synsent_state_process
之前分析tcp_rcv_state_process()
函数的时候,
客户端状态为TCP_SYN_SENT,调用函数为tcp_rcv_synsent_state_process()
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th)
{
......
tcp_ack(sk, skb, FLAG_SLOWPATH);//FLAG_SLOWPATH:指示是否使用慢路径进行 ACK 处理的标志
tcp_finish_connect(sk, skb);
tcp_send_ack(sk);
......
}
核心:调用tcp_ack()
函数检查ACK的有效性,当ACK有效的时候调用tcp_finish_connect()
函数完成TCP连接,此时状态由TCP_SYN_SENT
转变为TCP_ESTABLISHED
;调用 tcp_send_ack()
函数发送ACK。
客户端发送ACK
tcp_send_ack
void tcp_send_ack(struct sock *sk)
{
__tcp_send_ack(sk, tcp_sk(sk)->rcv_nxt);
}
实际封装的函数是__tcp_send_ack()
。
__tcp_send_ack
void __tcp_send_ack(struct sock *sk, u32 rcv_nxt)
{
struct sk_buff *buff;
/* 如果套接字状态为 TCP_CLOSE,则不执行发送操作。 */
if (sk->sk_state == TCP_CLOSE)
return;
/* 分配一个大小为 MAX_TCP_HEADER 的套接字缓冲区(SKB)。 */
buff = alloc_skb(MAX_TCP_HEADER,
sk_gfp_mask(sk, GFP_ATOMIC | __GFP_NOWARN));
if (unlikely(!buff)) {
/* 处理内存不足的情况,进行重传计时器的设置和调度。 */
struct inet_connection_sock *icsk = inet_csk(sk);
unsigned long delay;
delay = TCP_DELACK_MAX << icsk->icsk_ack.retry;
if (delay < TCP_RTO_MAX)
icsk->icsk_ack.retry++;
inet_csk_schedule_ack(sk);
icsk->icsk_ack.ato = TCP_ATO_MIN;
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, delay, TCP_RTO_MAX);
return;
}
/* 为头部预留空间并准备控制位。 */
skb_reserve(buff, MAX_TCP_HEADER);
tcp_init_nondata_skb(buff, tcp_acceptable_seq(sk), TCPHDR_ACK);
/* 设置 ACK 报文的标志位,用于不影响 TCP Small Queues 或 fq/pacing。 */
skb_set_tcp_pure_ack(buff);
/* 发送 ACK 报文。 */
__tcp_transmit_skb(sk, buff, 0, (__force gfp_t)0, rcv_nxt);
}
核心:用于在 TCP 接收到数据后发送 ACK 报文,检查TCP状态、内存分配情况最终发送ACK的实际调用函数为__tcp_transmit_skb()
,这个过程与TCP第一次握手发送SYN过程一致。
服务端收到ACK
tcp_v4_rcv()
函数调用tcp_v4_syn_recv_sock()
tcp_v4_syn_recv_sock
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)
{
......
newsk = tcp_create_openreq_child(sk, req, skb);
if (!newsk)
goto exit_nonewsk;
*own_req = inet_ehash_nolisten(newsk, req_to_sk(req_unhash), &found_dup_sk);//req_unhash:要处理的连接请求的指针found_dup_sk:是否找到重复的套接字
......
}
核心:调用tcp_create_openreq_child()
创建一个新的 TCP 连接请求的套接字,设置其状态为SYN_RECV
,调用inet_ehash_nolisten()
函数将新创建的套接字newsk插入到队列中,并判断全连接队列是否已满。
我们在tcp_check_req->inet_csk_complete_hashdance->inet_csk_reqsk_queue_add函数嵌套下将当前半连接对象删除并将握手成功的request_sock对象成功添加到全连接队列链表尾部,完成连接状态的改变ESTABLISHED
。
inet_csk_reqsk_queue_add
struct sock *inet_csk_reqsk_queue_add(struct sock *sk,
struct request_sock *req,
struct sock *child)
{
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
spin_lock(&queue->rskq_lock);
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_child_forget(sk, req, child);
child = NULL;
} else {
req->sk = child;
req->dl_next = NULL;
if (queue->rskq_accept_head == NULL)
WRITE_ONCE(queue->rskq_accept_head, req);
else
queue->rskq_accept_tail->dl_next = req;
queue->rskq_accept_tail = req;
sk_acceptq_added(sk);
}
spin_unlock(&queue->rskq_lock);
return child;
}
核心:检查当前套接字状态是否为监听状态 TCP_LISTEN
,将已经完成三次握手请求的request_sock对象添加至全连接队列链表尾部,队列为空则添加至队列头部.
服务端accept
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;
lock_sock(sk);
/* We need to make sure that this socket is listening,
* and that it has something pending.
*/
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out_err;
/* Find already established connection */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
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);
}
完成TCP三次握手,为了确保服务端可以正确接收到来组客户端的数据传输,引入inet_csk_accept
,核心是在套接字处于TCP_LISTEN
状态下调用reqsk_queue_remove
函数从全连接队列queue中移除一个套接字sk并返回给用户进程。
至此,linux内核源码三次握手理论部分内容较浅分析完成,后续追踪内核函数实现实验功能部分。