当前位置: 财声传媒网 -> 财经

能将三次握手理解到这个深度,面试官拍案叫绝

发布时间:2022年10月05日 08:46   来源:IT之家   关键词:   阅读量:13377   
导读:大家好,我是飞哥! 在后端的求职面试中,三次握手的出现频率非常高,甚至可以不夸张地说是必答题一般的回答是客户端如何发起SYN握手进入SYN_SENT状态,服务器响应SYN并回复SYNACK,然后进入SYN_RECV,...诸如此类 ...

大家好,我是飞哥!

能将三次握手理解到这个深度,面试官拍案叫绝

在后端的求职面试中,三次握手的出现频率非常高,甚至可以不夸张地说是必答题一般的回答是客户端如何发起SYN握手进入SYN_SENT状态,服务器响应SYN并回复SYNACK,然后进入SYN_RECV,...诸如此类

但是今天我想给出一个不一样的答案实际上,在内核的实现中,三次握手不仅仅是简单的状态转移,还包括半连接队列,syncookie,全连接队列,重传定时器等关键操作如果你能深刻理解这些,你会进一步在线把握和理解如果面试官问你三次握手的问题,我相信这个回答一定会帮你在面试官面前赢得很多加分

在基于TCP的服务开发中,三次握手的主要流程图如下。

服务器端的核心代码是创建socket,绑定端口,监听并最终接受客户端的请求。

//服务器核心代码intmain) intfd = socket (af _ inet,sock _ stream,0),绑定(fd,...),听(fd,128),接受(fd,...),...

客户端的相关代码是创建一个socket,然后调用connect连接服务器。

//客户端核心代码int mainfd = socket (AF _ inet,sock _ stream,0),连接(fd,),

围绕这张三次握手图,以及客户端和服务器端的核心代码,我们来深入探讨一下三次握手过程中的内部操作。先说listen,和三次握手过程有很大关系!

友情提示:本文会有更多内核源代码如果你能更好地理解它,如果你觉得很难理解它,就专注于本文中的描述性词语,尤其是粗体字此外,在文章的最后还有一个总结图表,对全文进行总结和梳理

首先,服务器的监听

众所周知,服务器在开始提供服务之前需要监听但是听里面做了什么,我们很少去想

今天我们来详细看看直接监听时执行的内核代码。

//file:net/core/request _ sock . cintreqsk _ queue _ allocsize _ tlopt _ size = sizeof(struct listen _ sock),structlisten _ sock * lopt//计算半连通队列的长度NR _ table _ entries = min _ t (U32,NR _ table _ entries,sysctl _ max _ syn _ backlog),Nr_table_entries=......//半连接队列请求内存lopt _ size+= NR _ table _ entries * sizeof(struct Request _ sock *),if(lopt _ size gt,PAGE _ SIZE)lopt = vzalloc(lopt _ SIZE),elselopt=kzalloc(lopt_size,GFP _ KERNEL),//用全连接队列头初始化queue—gt,rskq _ accept _ head = NULL//为半连接队列设置lopt—gt,NR _表_条目= nr _表_条目,queue—gt,listen _ opt = lopt......

在这段代码中,内核计算半连接队列的长度然后,计算半连接队列所需的实际内存大小,并启动用于管理半连接队列对象的内存的应用程序最后一个半连接队列挂在接收队列上

另外,queue—gt,Rsq _ accept _ head代表全连接队列,采用链表的形式在listen中,因为还没有连接,所以队列头queue—gt,Rsq _ accept _ head设置为NULL

当全连接队列和半连接队列中都有元素时,它们在内核中的结构图大致如下。

服务器侦听时,主要计算全/半连接队列的长度限制,以及相关的内存应用和初始化在可以响应来自客户端的握手请求之前,已满/连接队列被初始化

如果想进一步了解listen的内部操作细节,可以阅读之前的文章《为什么服务器端程序需要先监听。》

第二,客户端连接

客户端通过调用connect来启动连接在connect系统调用中,您将输入内核源代码的tcp_v4_connect

//file:net/IP v4/TCP _ IP v4 . CIN TCP _ v4 _ connect//将套接字状态设置为TCP _ syn _ sent TCP _ set _ state (SK,TCP _ syn _ sent),//动态选择一个端口err = inet _ hash _ connect(amp,tcp_death_row,sk),//该函数用于根据sk中的信息构造一条完整的syn报文并发送出去。err = TCP _ connect(sk),

将套接字状态设置为TCP_SYN_SENT将在此完成然后通过inet_hash_connect动态选择一个可用端口,进入tcp_connect

//file:net/IP v4/TCP _ output . cinttcp _ connect TCP _ connect _ init(sk),//申请skb,构造成SYN包......//添加到发送队列sk_write_queue上的tcp_connect_queue_skb(sk,buff),//实际发出syne RR = TP—gt,fastopen_req。tcp_send_syn_data(sk,buff):tcp_transmit_skb(sk,buff,1,sk—gt,sk _ allocation),//启动重传定时器inet _ csk _ reset _ xmit _ timer (sk,icsk _ time _ retries,inet _ csk(sk)—gt,icsk_rto,TCP _ RTO _ MAX),

在tcp_connect上申请并构造SYN包,然后发送出去同时启动重传定时器,用于在一定时间后没有收到服务器的反馈时开始重传在3.10版本中,第一次超时是1 s,在一些旧版本中,是3 s

综上所述,客户端连接时,将本地socket状态设置为TCP_SYN_SENT,选择一个可用端口,然后发出SYN握手请求,启动重传定时器。

第三,服务器响应SYN

在服务器端,所有的TCP包通过网卡和软中断进入tcp_v4_rcv在这个函数中,根据SKB的TCP报头信息中的目的IP信息找到当前监听的套接字然后继续进入tcp_v4_do_rcv握手流程

//file:net/IP v4/TCP _ IP v4 . CIN TCP _ v4 _ do _ rcv//当服务器收到第一个握手SYN或者第三个ACK时,如果(sk—gt,sk _ state TCP _ LISTEN)struct sock * NSK = TCP _ v4 _ hnd _ req(sk,skb),if(tcp_rcv_state_process(sk,skb,tcp_hdr(skb),sk b—gt,len))rsk = sk,gotoreset

在tcp_v4_do_rcv中判断出当前套接字处于listen状态后,首先会检查tcp_v4_hnd_req中的半连接队列服务器第一次响应SYN时,半连接队列必须为空,所以相当于什么都不做就返回

在tcp_rcv_state_process中,根据不同的套接字状态执行不同的进程。

//file:net/IP v4/TCP _ input . cinttcp _ rcv _ state _ process switch(sk—gt,Sk_state)//第一次握手案例TCP _ LISTEN:if(th—gt,Syn)//判断是Syn握手包...if(icsk—gt,icsk _ af _ ops—gt,conn_request(sk,skb)lt,0)返回1,......

其中conn_request是指向tcp_v4_conn_request的函数指针响应服务器SYN的主要处理逻辑在这个tcp_v4_conn_request中

//file:net/IP v4/TCP _ IP v4 . CIN TCP _ v4 _ conn _ request//查看半连通队列是否已满if(inet _ csk _ reqsk _ queue _ is _ full(sk)amp,amp!isn)want _ cookie = TCP _ syn _ flood _ action(sk,skb, " TCP "),如果(!want _ cookie)gotodrop,//在全连接队列已满的情况下,如果有young_ack,那么直接抛出if(sk _ acceptq _ is _ full(sk)amp,ampinet _ csk _ reqsk _ queue _ young(sk)gt,1)NET_INC_STATS_BH(sock_net(sk),LINUX _ MIB _ LISTENOVERFLOWSgotodrop//allocate request_sock内核对象req = inet _ reqsk _ alloc(amp,TCP _ request _ sock _ ops),//构造syn+ack包skb _ synack = TCP _ make _ synack (sk,dst,req,fast open _ cookie _ present(amp,valid_foc)。ampvalid _ foc:NULL),如果(有可能(!do _ fast open))//Send syn+ack response ERR = IP _ Build _ and _ Send _ PKT(SKB _ SYNACK,SK,IREQ—gt,loc_addr,ireq—gt,rmt_addr,ireq—gt,opt),//加入半连接队列,启动定时器inet _ csk _ reqsk _ queue _ hash _ Add(SK,REQ,TCP _ time out _ init),其他

这里先判断半连接队列是否已满,如果是,输入tcp_syn_flood_action判断tcp_syncookies内核参数是否开启。如果队列满了,没有开启tcp_syncookies,握手包会直接丢弃!!

那么就要判断全连接队列是否满了因为满连接队列也会导致握手异常,所以在第一次握手时简单判断如果全连接队列已满且有young_ack,也直接丢弃

Young_ack是保存在半连接队列中的计数器它记录了刚到达SYN,还没有被SYN_ACK重传定时器重传,还没有完成三次握手的sock的数量

下一步是构造synack包,然后通过ip_build_and_send_pkt发送出去。

最后,将当前握手信息添加到半连接队列中,并启动计时器定时器的作用是,如果在一定时间内没有收到客户端的三次握手,服务器会重新发送synack包

综上所述,服务器响应ack的主要工作是确定接收队列是否已满如果它已满,该请求可能会被丢弃,否则,将发送一个synack将request_sock添加到半连接队列中,并启动定时器

第四,客户端响应SYNACK

当接收到来自服务器的synack包时,客户端也会进入tcp_rcv_state_process函数但是,由于它自己的套接字的状态是TCP_SYN_SENT,所以它将进入另一个不同的分支

//file:net/ipv4/tcp_input.c//除了ESTABLISHED和TIME_WAIT,状态中的tcp处理都到这里int TCP _ RCV _ state _ process switch(SK—gt,Sk_state)//服务器收到第一个ACK包caseTCP_LISTEN://客户端处理第二个握手caseTCP_SYN_SENT://处理synack包queued = TCP _ RCV _ SYN SENT _ state _ process(SK,SKB,TH,LEN),return0

Tcp_rcv_synsent_state_process是客户端响应synack的主要逻辑。

//file:net/IP v4/TCP _ input . cstaticinttcp _ rcv _ syn sent _ state _ process TCP _ ack(sk,skb,FLAG _ slow path),//连接建立后tcp_finish_connect(sk,skb),if(sk—gt,sk _ write _ pendingicsk—gt,icsk _ accept _ queue . rskq _ defer _ accept icsk—gt,Icsk_ack.pingpong)//延迟确认elsetcp _ send _ ack(sk),

TCP _ ack—gt,tcp _清理_ rtx _队列

//file:net/IP v4/TCP _ input . cstaticitcp _ clean _ RTX _ queue//删除发送队列...//删除定时器TCP _ rearm _ RTO(sk),

//file:net/IP v4/TCP _ input . cvoidtcp _ finish _ connect//修改套接字状态TCP _ set _ state (SK,TCP _ established),//初始化拥塞控制TCP _ init _ congestion _ control(sk),...//keep—alive timer打开if (sock _ flag (SK,sock _ keepOpen)) inet _ CSK _重置_ keepalive _ timer (SK,keepalive _ time _ when(TP)),

客户端将其套接字状态修改为已建立,然后启动TCP保持活动计时器。

//file:net/IP v4/TCP _ output . cvoidtcp _ send _ ack//申请并构造ack包buff = alloc _ skb (Max _ TCP _ header,SK _ GFP _ atomic (SK,GFP _ atomic)),//发出TCP _ transmit _ skb (sk,buff,0,sk _ GFP _ atomic (sk,GFP _ atomic)),

ack包在tcp_send_ack中构造并发送出去。

当客户端响应来自服务器的synack时,它清除连接期间设置的重新传输计时器,将当前套接字状态设置为已建立,启动保活计时器,并发送三次握手的ack确认。

5.服务器响应ACK

当服务器响应三次握手的ack时,它也会进入tcp_v4_do_rcv

//file:net/IP v4/TCP _ IP v4 . cinttcp _ v4 _ do _ RC Vif(sk—sk _ state TCP _ LISTEN)struct sock * NSK = TCP _ v4 _ hnd _ req(sk,skb),if(tcp_rcv_state_process(sk,skb,tcp_hdr(skb),sk b—len))rsk = sk,gotoreset

但是,由于这是第一次三次握手,因此在半连接队列中会有上次第一次握手留下的半连接信息所以tcp_v4_hnd_req的执行逻辑会有所不同

//file:net/IP v4/TCP _ IP v4 . cstaticstructsock * TCP _ v4 _ hnd _ reqstructrequest _ sock * req = inet _ csk _ search _ req(sk,ampprev,th—source,iph—saddr,iph—daddr),if(req)returntcp_check_req(sk,skb,req,prev,false),

Inet_csk_search_req负责在半连通队列中搜索,找到后返回一个半连通的request_sock对象然后输入tcp_check_req

//file:net/IP v4/TCP _ mini socks . cstructsock * TCP _ check _ req//创建一个子socket child = inet _ csk(sk)—gt,icsk _ af _ ops—gt,syn_recv_sock(sk,skb,req,NULL),//清理半连通队列inet _ csk _ reqsk _ queue _ unlink (sk,req,prev),inet _ csk _ reqsk _ queue _ removed(sk,req),//添加全连接队列inet _ csk _ reqsk _ queue _ add (sk,req,child),returnchild5.1创建子套接字

icsk _ af _ ops—gt,Syn_recv_sock对应tcp_v4_syn_recv_sock函数。

//file:net/IP v4/TCP _ IP v4 . cconstructinet _ connection _ sock _ af _ opsipv4 _ specific =conn _请求= tcp _ v4 _ conn _请求,syn _ recv _ sock = TCP _ v4 _ syn _ recv _ sock,//就算三次握手接近,也完了

* *注意,这里在三次握手中,继续判断全连接队列是否再次满如果已满,修改计数器并将其丢弃* *如果队列不满足,则申请创建新的sock对象

5.2删除半连接队列

从半连接队列中删除连接请求块。

//file:include/net/inet _ connection _ sock . hstaticinlinevoidinet _ csk _ reqsk _ queue _ unlinkreqsk _ queue _ unlink(amp,inet_csk(sk)—icsk_accept_queue,req,prev),

Reqsk_queue_unlink从半连接队列中删除连接请求块。

5.3添加完整的连接队列

然后将其添加到完整的连接队列中。

//file:net/IP v4/syncookies . cstaticinlinevoidinet _ csk _ reqsk _ queue _ addreqsk _ queue _ add(amp,inet_csk(sk)—icsk_accept_queue,req,sk,child),

在reqsk_queue_add中,握手成功的request_sock对象被插入到全连接队列的链表末尾。

//file:include/net/request _ sock . hstaticinlinevoidreqsk _ queue _ addreq—sk = child,sk _ acceptq _ added(parent),if(queue—rskq _ accept _ head null)queue—rskq _ accept _ head = req,elsequeue—rskq _ accept _ tail—dl _ next = req,queue—rskq _ accept _ tail = req,req—dl _ next = NULL,5.4将连接设置为已建立。

//file:net/IP v4/TCP _ input . CIN TCP _ rcv _ state _ process switch(sk—sk _ state)//服务器端三方握手处理caseTCP_SYN_RECV://并改变状态连接TCP _ set _ state (sk,TCP _ established),

将连接设置为TCP_ESTABLISHED状态。

服务器响应三次握手ack所做的是删除当前的半连接对象,创建一个新的sock并将其添加到全连接队列中,最后将新的连接状态设置为ESTABLISHED。

不及物动词服务器接受

接受最后一步让我们长话短说

//file:net/IP v4/inet _ connection _ sock . cstructsock * inet _ csk _ accept//从全连接队列中获取struct request _ sock _ queue * queue = amp,icsk—gt,icsk _ accept _ queuereq = reqsk _ queue _ remove(queue),newsk = req—gt,sk,returnnewsk

Reqsk_queue_remove这个操作很简单,从全连接队列的链表中获取第一个元素并返回即可。

//file:include/net/request _ sock . hstaticinlinestructrequest _ sock * reqsk _ queue _ removestructrequest _ sock * req = queue—rskq _ accept _ head,queue—rskq _ accept _ head = req—dl _ next,if(queue—rskq _ accept _ head NULL)queue—rskq _ accept _ tail = NULL,returnreq

因此,accept的关键工作是从已建立的全连接队列中取出一个进程并返回给用户。

本文摘要

在后端工作面试中,三次握手的出现频率很高实际上,在三次握手的过程中,不仅仅是发送一个握手包和传输TCP状态它还包括端口选择,连接队列创建和处理等许多关键技术点通过今天的文章,我们对三次握手过程内核中的这些内部操作有了深入的了解

全文足有几万字,其实一张图就能概括。

1.当服务器监听时,计算全/半连接队列的长度,并申请和初始化相关的内存。

2.当客户端连接时,它将本地套接字状态设置为TCP_SYN_SENT,选择一个可用端口,发送一个SYN握手请求,并启动重新传输计时器。

3.当服务器响应ack时,它将确定下一个接收队列是否已满,如果已满,它可能会丢弃该请求否则发出synack,申请将request_sock加入半连接队列,同时启动定时器

4.当客户端响应synack时,它清除连接期间设置的重传计时器,将当前套接字状态设置为已建立,并启动保活计时器发送第一次三次握手的ack确认。

5.当服务器响应ack时,它删除相应的半连接对象,创建一个新的sock,然后将其添加到全连接队列中最后,它将新的连接状态设置为ESTABLISHED

6.accept从已建立的完整连接队列中取出一个,并将其返回给用户进程。

另外,需要注意的是,如果握手过程中发生丢包,内核会等待定时器超时后重试3.10版的重试间隔分别为1s 2s 4s在一些老版本中,比如2.6,第一次重试时间是3秒最大重试次数分别由tcp_syn_retries和tcp_synack_retries控制

如果您的在线界面正常情况下会在几十毫秒内返回,但偶尔会出现1 s,3 s等响应时间的问题变长了,那你就要定位一下,看看有没有握手包超时重传

这是三次握手中一些更详细的内部操作。如果你能在面试官面前说出内核的底层逻辑,我相信面试官一定会对你印象深刻!

郑重声明:此文内容为本网站转载企业宣传资讯,目的在于传播更多信息,与本站立场无关。仅供读者参考,并请自行核实相关内容。

~全文结束~

分享到微信