Socket三次握手及四次挥手 关键代码流程

三次握手过程 流程向

套接字处理 核心函数

  • 几乎所有状态的套接字,在收到数据报时都在这里完成处理。充当路由功能。
[/net/ipv4/tcp_input/int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)]
//服务端处理第一次握手
switch (sk->sk_state) {
    case TCP_LISTEN:
        if (th->syn) {
            acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
        }
}
//客户端/服务器第二次握手处理
switch (sk->sk_state) {
    case TCP_SYN_SENT:
        //处理SYN_SENT状态下接收到的TCP段
        queued = tcp_rcv_synsent_state_process(sk, skb, th);
}

//服务器端的第三次握手
switch (sk->sk_state) {
	case TCP_SYN_RECV:
        //正常的第三次握手,设置连接状态为TCP_ESTABLISHED 
        tcp_set_state(sk, TCP_ESTABLISHED);

客户端发出第一次握手

  1. 首先客户端调用connect主动发起连接:
[/net/socket.c/int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)]
sock = sockfd_lookup_light(fd, &err, &fput_needed);//根据文件描述符找到指定的Socket对象

  1. 客户端使用流式套接字,故调用实现该协议的connect函数tcp_v4_connect()来发送SYN包:
[/net/ipv4/af_inet.c/int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags)]
//调用connect函数,对于流式套接字,sock->ops为 inet_stream_ops --> inet_stream_connect  --> tcp_prot  --> tcp_v4_connect
err = sk->sk_prot->connect(sk, uaddr, addr_len);
//更新socket状态为连接已建立
sock->state = SS_CONNECTED;
  1. 客户端发出第一次握手
[/net/ipv4/tcp_ipv4/int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)]
//套接字状态被置为 TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
//为 TCP报文计算一个初始id
inet->inet_id = prandom_u32();
//函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去。
err = tcp_connect(sk);
  • 三次握手中的第一次握手在客户端的层面完成.报文到达服务端,由服务端处理完毕后,第一次握手完成.客户端socket状态变为TCP_SYN_SENT。

**服务器开启第二次握手 **

  1. 服务器开启第二次握手,向客户端发送SYN+ACK报文
[/net/ipv4/tcp_ipv4/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)]
//根据路由、传输控制块、连接请求块中的构建SYN+ACK段
skb = tcp_make_synack(sk, dst, req, foc, synack_type);
//生成IP数据报并发送出去
err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
                        ireq->ir_rmt_addr,
                        ireq->opt);
  1. 至此,第二次握手完成。客户端socket状态变为TCP_ESTABLISHED,此时服务端socket的状态为TCP_NEW_SYN_RECV。

客户端发出第三次握手

  1. 调用tcp_v4_rcv进行第三次握手。
  2. 收到第三次握手最后一个ack后,服务端会找到TCP_NEW_SYN_RECV状态的req,然后创建一个新的sock进入TCP_SYN_RECV状态,最终进入TCP_ESTABLISHED状态。
  3. 放入accept队列通知select/epoll
[/net/ipv4/tcp_ipv4/int tcp_v4_rcv(struct sk_buff *skb)]
//创建新的sock进入TCP_SYN_RECV state
nsk = tcp_check_req(sk, skb, req, false);
  1. 随后在tcp_check_req()中创建在第三次握手后的新的Socket
  2. 在tcp_check_req()中通过调用链tcp_v4_syn_recv_sock --> tcp_create_openreq_child --> inet_csk_clone_lock 生成新sock,状态设置为TCP_SYN_RECV;
  3. 且tcp_v4_syn_recv_sock通过调用inet_ehash_nolisten将新sock加入ESTABLISHED状态的哈希表中;
  4. 通过调用inet_csk_complete_hashdance,将新sock插入accept队列.
  5. 至此我们得到一个代表本次连接的新sock,状态为TCP_SYN_RECV
  6. 并回到tcp_rcv_state_process,也就是上面说的路由函数。即由以下代码处理。
//服务器端的第三次握手
switch (sk->sk_state) {
	case TCP_SYN_RECV:
        //正常的第三次握手,设置连接状态为TCP_ESTABLISHED 
        tcp_set_state(sk, TCP_ESTABLISHED);
  1. 最后将sock的状态设置为TCP_ESTABLISHED,至此三次握手完成
  2. 等待用户调用accept调用,取出套接字使用。

参考文献 :

  1. TCP三次握手Linux源码详解
  2. openEuler操作系统源代码,Linux内核4.19

四次挥手 流程向(客户端视角)

  • 触发时间:当调用close()函数时,执行四次挥手流程。(默认在正常连接状态时发生)
[/net/ipv4/tcp.c/static void tcp_close(struct sock *sk, int timeout)]
//改变状态
sk->sk_shutdown = SHUTDOWN_MASK;
//发送结束信号
if (tcp_close_state(sk)) {
		tcp_send_fin(sk);
}
  • if语句中在tcp_close_state中根据当前Socket状态进行判断,本例中执行如下:
[/net/ipv4/tcp.c/static int tcp_close_state(struct sock *sk)]
//根据当前状态确定下一状态
int next = (int)new_state[sk->sk_state];
int ns = next & TCP_STATE_MASK;
//设置下一状态
tcp_set_state(sk, ns);
//指导是否发出fin
return next & TCP_ACTION_FIN;

第一次挥手

  • 发送一个fin包。(在调用者处已经上锁,此处不需要上锁了)
//查看是否有没有发送的数据
struct sk_buff *skb, *tskb = tcp_write_queue_tail(sk);
if (tskb) {//有尾部剩余数据就对其进行发送,同时假设在发送中也包含了Fin
		if (tcp_write_queue_empty(sk)) {
				//设置tp->snd_nxt为发送Fin后状态
				tp->snd_nxt++;
			return;
		}
} else {
		//分配空间并重发数据
		skb = alloc_skb_fclone(MAX_TCP_HEADER, sk->sk_allocation);
		//重发数据
		...
}

第二次握手

  • P.S.openEuler较为繁琐,使用实例代码

  • 在如下的tcp_ack函数中处理第二次挥手,接收服务发送的ack。

  • 最后客户端把状态改成TCP_FIN_WAIT2,此时服务端的状态是close_wait。服务端等待自己发送fin包。

if (sk->state == TCP_FIN_WAIT1) {
//本端的数据发送完毕
		if (sk->rcv_ack_seq == sk->write_seq) 
		{
			sk->shutdown |= SEND_SHUTDOWN;
			tcp_set_state(sk, TCP_FIN_WAIT2);
		}
	}

第三次挥手

  • 本端收到服务端的fin包,在tcp_fin中处理。
  • 客户端在TCP_FIN_WAIT2的时候遇到fin包,则把状态置为TCP_TIME_WAIT
[/net/ipv4/tcp_input.c/void tcp_fin(struct sock *sk)]
switch (sk->sk_state) {
	case TCP_FIN_WAIT2:
		//返回ack信号
		tcp_send_ack(sk);
		tcp_time_wait(sk, TCP_TIME_WAIT, 0);
		break;
}

第四次挥手

  • 在接收到服务端fin信号后,向服务器发送ack信号。

四次挥手 流程向(服务端视角)

第一次,第二次挥手

  • 服务器接收到客户端的fin包后进入TCP_CLOSE_WAIT状态,完成第一次挥手。
  • 服务端发送ack给客户端,完成第二次挥手。
[/net/ipv4/tcp_input.c/void tcp_fin(struct sock *sk)]
switch (sk->sk_state) {
  case TCP_ESTABLISHED:
      tcp_set_state(sk, TCP_CLOSE_WAIT);
      inet_csk(sk)->icsk_ack.pingpong = 1;
      break;
} 

第三次挥手

  • 服务端调用close函数关闭本端,和客户端发出close类似。
[/net/ipv4/tcp.c/static void tcp_close(struct sock *sk, int timeout)]
//改变状态
sk->sk_shutdown = SHUTDOWN_MASK;
//发送结束信号
if (tcp_close_state(sk)) {
		tcp_send_fin(sk);
}
  • 此时处理端是TCP_CLOSE_WAIT状态,经过如下函数tcp_close_states处理后为LAST_ACK状态
[/net/ipv4/tcp.c/static int tcp_close_state(struct sock *sk)]
//根据当前状态确定下一状态
int next = (int)new_state[sk->sk_state];
int ns = next & TCP_STATE_MASK;
//设置下一状态
tcp_set_state(sk, ns);
//指导是否发出fin
return next & TCP_ACTION_FIN;
  • 随后发送fin包,完成第三次挥手。

第四次挥手

  • 服务器socket的状态是LAST_ACK,客户端发送的最后ack包的时候即完成了第四次挥手。示例代码如下:
if (sk->state == TCP_LAST_ACK) {
		if (sk->rcv_ack_seq == sk->write_seq ) 
		{
			flag |= 1;
			tcp_set_state(sk,TCP_CLOSE);
			//关闭连接
			sk->shutdown = SHUTDOWN_MASK;
		}
	}

参考文献:

  1. 客户端角度
  2. 服务端角度
  3. openEuler操作系统源代码 linux4.19

你可能感兴趣的:(openEuler)