大家好,我是飞哥!
在后端相关岗位的入职面试中,三次握手的出场频率非常的高,甚至说它是必考题也不为过。一般的答案都是说客户端如何发起 SYN 握手进入 SYN_SENT 状态,服务器响应 SYN 并回复 SYNACK,然后进入 SYN_RECV,...... , 吧啦吧啦诸如此类。
但我今天想给出一份不一样的答案。其实三次握手在内核的实现中,并不只是简单的状态的流转,还包括半连接队列、syncookie、全连接队列、重传计时器等关键操作。如果能深刻理解这些,你对线上把握和理解将更进一步。如果有面试官问起你三次握手,相信这份答案一定能帮你在面试官面前赢得非常多的加分。
在基于 TCP 的服务开发中,三次握手的主要流程图如下。
服务器中的核心代码是创建 socket,绑定端口,listen 监听,最后 accept 接收客户端的请求。
//服务端核心代码
int main(int argc, char const *argv[])
{
int fd = socket(AF_INET, SOCK_STREAM,
0);
bind(fd, ...);
listen(fd,
128);
accept(fd, ...);
...
}
客户端的相关代码是创建 socket,然后调用 connect 连接 server。
//客户端核心代码
int main(){
fd = socket(AF_INET,SOCK_STREAM,
0);
connect(fd, ...);
...
}
围绕这个三次握手图,以及客户端,服务端的核心代码,我们来深度探索一下三次握手过程中的内部操作。我们从和三次握手过程关系比较大的 listen 讲起!
友情提示:本文中内核源码会比较多。如果你能理解的了更好,如果觉得理解起来有困难,那直接重点看本文中的描述性的文字,尤其是加粗部分的即可。另外文章最后有一张总结图归纳和整理了全文内容。
一、服务器的 listen
我们都知道,服务器在开始提供服务之前都需要先 listen 一下。但 listen 内部究竟干了啥,我们平时很少去琢磨。
今天就让我们详细来看看,直接上一段 listen 时执行到的内核代码。
//file: net/core/request_sock.c
int reqsk_queue_alloc(struct request_sock_queue *queue,
unsigned int nr_table_entries)
{
size_t lopt_size =
sizeof(struct listen_sock);
struct listen_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 > PAGE_SIZE)
lopt = vzalloc(lopt_size);
else
lopt = kzalloc(lopt_size, GFP_KERNEL);
//全连接队列头初始化
queue->rskq_accept_head =
NULL;
//半连接队列设置
lopt->nr_table_entries = nr_table_entries;
queue->listen_opt = lopt;
......
}
在这段代码里,内核计算了半连接队列的长度。然后据此算出半连接队列所需要的实际内存大小,开始申请用于管理半连接队列对象的内存(半连接队列需要快速查找,所以内核是用哈希表来管理半连接队列的,具体在 listen_sock 下的 syn_table 下)。最后将半连接队列挂到了接收队列 queue 上。
另外 queue->rskq_accept_head 代表的是全连接队列,它是一个链表的形式。在 listen 这里因为还没有连接,所以将全连接队列头 queue->rskq_accept_head 设置成 NULL。
当全连接队列和半连接队列中有元素的时候,他们在内核中的结构图大致如下。
在服务器 listen 的时候,主要是进行了全/半连接队列的长度限制计算,以及相关的内存申请和初始化。全/连接队列初始化了以后才可以相应来自客户端的握手请求。
二、客户端 connect
客户端通过调用 connect 来发起连接。在 connect 系统调用中会进入到内核源码的 tcp_v4_connect。
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
//设置 socket 状态为 TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
//动态选择一个端口
err = inet_hash_connect(
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。