linux网络协议栈——TCP连接

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函数的时候传入的是backlognet.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内核源码三次握手理论部分内容较浅分析完成,后续追踪内核函数实现实验功能部分。

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
下面是一个简单的示例代码,可以在 Linux 上使用 TCP 连接向客户端发送数据: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define PORT 8080 int main() { int sockfd, new_sockfd, valread; struct sockaddr_in serv_addr, cli_addr; char buffer[1024] = {0}; char *hello = "Hello from server"; int addrlen = sizeof(serv_addr); // 创建 socket if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置地址重用 int opt = 1; if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt failed"); exit(EXIT_FAILURE); } // 设置服务器地址 serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); // 绑定 socket 到指定地址 if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听连接 if (listen(sockfd, 3) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } // 等待客户端连接 if ((new_sockfd = accept(sockfd, (struct sockaddr *)&cli_addr, (socklen_t*)&addrlen)) < 0) { perror("accept failed"); exit(EXIT_FAILURE); } // 发送数据到客户端 send(new_sockfd, hello, strlen(hello), 0); printf("Hello message sent\n"); return 0; } ``` 这个代码会监听本地的 8080 端口,并等待客户端连接。一旦客户端连接上来,服务器就会发送一个 “Hello from server” 的消息到客户端。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值