1. 首页 > 快讯

服务端程序运作原理:监听端口的重要性

大家好,感谢邀请,今天来为大家分享一下服务端程序运作原理:监听端口的重要性的问题,以及和的一些困惑,大家要是还不太明白的话,也没有关系,因为接下来将为大家分享,希望可以帮助到大家,解决大家的问题,下面就开始吧!

[[409687]]

大家好,我是飞哥。飞哥在北京工作9年多了,最近他终于拿到了北京的电动车牌。其中的艰辛过程大概需要一万字才能写完。不管怎么说,新能源也是车,你终于可以开它了。这些天买卖汽车(外国品牌)非常忙碌。但再忙,也不能停止写硬核文章!

众所周知,在创建服务器程序时,需要先监听,然后才能接收客户端的请求。例如,下面的代码对我们来说太熟悉了。

intmain(intargc,charconst*argv[]){intfd=socket(AF_INET,SOCK_STREAM,0);bind(fd,);listen(fd,128);accept(fd,);然后我们今天我们来思考一个问题,为什么我们需要监听接收连接?或者说,listen在内部执行时到底做了什么?

如果你也想了解听里面的这些秘密,那么就请关注我吧!

一、创建 socket

服务器所做的第一件事是创建套接字。具体来说,就是通过调用socket函数。当socket函数执行时,从用户层角度我们看到返回了一个文件描述符fd。但在内核中,它实际上是一组内核对象组合,大致结构如下。

这里简单理解一下这个结构就够了。稍后当我们在源代码中看到函数指针调用时,我们需要回过头来看看。

二、内核执行 listen

2.1 listen 系统调用我在net/socket.c下找到了listen系统调用的源代码。

//file:net/socket.cSYSCALL_DEFINE2(listen,int,fd,int,backlog){//根据fd查找socket内核对象sock=sockfd_lookup_light(fd,err,fput_needed);if(sock){//获取内核参数网。 core.somaxconnsomaxconn=sock_net(sock-sk)-core.sysctl_somaxconn;if((unsignedint)backlogsomaxconn)backlog=somaxconn;//调用协议栈注册的listen函数err=sock-ops-listen(sock,backlog);} 用户态的socket文件描述符只是一个整数,内核不能直接使用它。所以这个函数中的第一行代码就是根据用户传入的文件描述符找到对应的socket内核对象。

然后获取系统中net.core.somaxconn内核参数的值,与用户传入的backlog进行比较,取一个最小值,传递给下一步。

因此,虽然listen允许我们传入backlog(这个值与半连接队列和全连接队列都有关)。但如果用户传入的值大于net.core.somaxconn,则不起作用。

然后通过调用sock-ops-listen进入协议栈的listen函数。

2.2 协议栈 listen这里我们需要用到第一节中的socket内核对象结构图。通过它我们可以看到sock-ops-listen实际上执行的是inet_listen。

//file:net/ipv4/af_inet.cintinet_listen(structsocket*sock,intbacklog){//尚未处于listen状态(尚未监听) if(old_state!=TCP_LISTEN){//开始监听err=inet_csk_listen_start(sk,backlog); } //设置全连接队列长度sk-sk_max_ack_backlog=backlog;} 这里我们先看最后一行。 sk-sk_max_ack_backlog 是全连接队列的最大长度。那么这里我们就知道了一个关键的技术点。服务器的全连接队列长度是监听时传入的backlog与net.core.somaxconn之间较小的值。

如果线上遇到全连接队列溢出的问题,想要增加队列长度,可能需要同时考虑backlog和监听时传入的net.core.somaxconn。

让我们回顾一下inet_csk_listen_start 函数。

//file:net/ipv4/inet_connection_sock.cintinet_csk_listen_start(structsock*sk,constintnr_table_entries){structinet_connection_sock*icsk=inet_csk(sk);//icsk-icsk_accept_queue为接收队列,具体参见2.3节//接收队列的申请和初始化内核对象,具体参见2.4节intrc=reqsk_queue_alloc(icsk-icsk_accept_queue,nr_table_entries);} 函数开头强制将struct sock对象转换为inet_connection_sock,命名为icsk。

下面简要解释一下为什么可以进行这种强制转换。这是因为inet_connection_sock包含sock。 tcp_sock、inet_connection_sock、inet_sock、sock是一层层嵌套的,类似于面向对象中继承的概念。

对于TCP套接字来说,sock对象实际上是一个tcp_sock。因此,TCP中的sock对象可以随时强制类型转换为tcp_sock、inet_connection_sock或inet_sock来使用。

在下一行中,reqsk_queue_alloc 实际上包含两个重要的内容。一是接收队列数据结构的定义。二是接收队列的申请并初始化。这两部分比较重要,我们分别在2.3节和2.4节中介绍。

2.3 接收队列定义icsk-icsk_accept_queue 定义在inet_connection_sock 下,是request_sock_queue 类型的对象。它是内核用来接收客户端请求的主要数据结构。我们平时所说的全连接队列、半连接队列都是在这个数据结构中实现的。

我们来看一下具体的代码。

//file:include/net/inet_connection_sock.hstructinet_connection_sock{/*inet_sockhastobethefirstmember!*/structinet_sockicsk_inet;structrequest_sock_queueicsk_accept_queue;} 我们来找到request_sock_queue的定义,如下。

//file:include/net/request_sock.hstructrequest_sock_queue{//全连接队列structrequest_sock*rskq_accept_head;structrequest_sock*rskq_accept_tail;//半连接队列structlisten_sock*listen_opt;};对于全连接队列,就可以了无需执行复杂的搜索工作。受理时,按照先进先出的原则受理即可。因此,通过rskq_accept_head和rskq_accept_tail以链表的形式管理全连接队列。

与半连接队列相关的数据对象是listen_opt,其类型为listen_sock。

//file:structlisten_sock{u8max_qlen_log;u32nr_table_entries;structrequest_sock*syn_table[0];};因为服务器在第三次握手时需要快速找到第一次握手时保留的request_sock对象,所以实际上是用一个哈希表来管理的,即struct request_sock *syn_table[0]。 max_qlen_log和nr_table_entries与半连接队列的长度有关。

2.4 接收队列申请和初始化了解完全/半连接队列数据结构后,让我们回到inet_csk_listen_start函数。它调用reqsk_queue_alloc来申请并初始化重要对象icsk_accept_queue。

//file:net/ipv4/inet_connection_sock.cintinet_csk_listen_start(structsock*sk,constintnr_table_entries){.intrc=reqsk_queue_alloc(icsk-icsk_accept_queue,nr_table_entries);} 在reqsk_queue_alloc函数中,创建并创建了request_sock_queue内核对象接收队列完成。初始化。其中包括内存申请、半连接队列长度计算、全连接队列头初始化等。

我们来看看它的源码:

//file:net/core/request_sock.cintreqsk_queue_alloc(structrequest_sock_queue*queue,unsignedintnr_table_entries){size_tlopt_size=sizeof(structlisten_sock);structlisten_sock*lopt;//计算半连接队列的长度nr_table_entries=min_t(u32,nr_table_entries,sysctl _max_syn_backlog) ;nr_table_entries=.//为listen_sock对象申请内存,该对象包含半连接队列lopt_size+=nr_table_entries*sizeof(structrequest_sock*);if(lopt_sizePAGE_SIZE)lopt=vzalloc(lopt_size);elselopt=kzalloc(lopt_size,GFP_KERNEL) ;//全连接队列头初始化queue-rskq_accept_head=NULL;//半连接队列设置lopt-nr_table_entries=nr_table_entries;queue-listen_opt=lopt;}开头定义了一个structlisten_sock指针。这个listen_sock就是我们通常所说的半连接队列。

接下来计算半连接队列的长度。计算出实际大小后,开始申请内存。最后将全连接队列头queue-rskq_accept_head设置为NULL,半连接队列挂接到接收队列队列。

这里需要注意的一个细节是,半连接队列上的每个元素都分配了一个指针大小(sizeof(struct request_sock *))。这实际上是一个哈希表。真正的半连接使用的request_sock对象是在握手过程中分配的,计算Hash值后挂在Hash表上。

2.5 半连接队列长度计算上一节我们提到reqsk_queue_alloc函数计算半连接队列的长度。由于这有点复杂,我们将在单独的部分中讨论它。

//file:net/core/request_sock.cintreqsk_queue_alloc(structrequest_sock_queue*queue,unsignedintnr_table_entries){//计算半连接队列的长度nr_table_entries=min_t(u32,nr_table_entries,sysctl_max_syn_backlog);nr_table_entries=max_t(u32,nr_table _条目,8) ;nr_table_entries=roundup_pow_of_two (nr_table_entries+1);//为了效率,不记录nr_table_entries //但是记录2的几次方等于nr_table_entriesfor(lopt-max_qlen_log=3;(1lopt-max_qlen_log)nr_table_entries;lopt-max_qlen_log++);} 传入的nr_table_entries可以在最初调用reqsk_queue_alloc的地方看到。是内核参数net.core.somaxconn和用户调用listen时传入的backlog之间较小的值。

在这个reqsk_queue_alloc函数中,还会完成三个比较和计算。

min_t(u32, nr_table_entries, sysctl_max_syn_backlog) 这又是sysctl_max_syn_backlog 内核对象的最小值。 max_t(u32, nr_table_entries, 8) 这句话保证nr_table_entries不能小于8。这个是用来防止新手用户传入太小的值而无法建立连接的。 roundup_pow_of_two(nr_table_entries + 1) 用于对齐到2 的整数幂。此时,您可能已经开始头痛了。确实,这个描述有点抽象。我们换个方法,通过两个实际案例来计算一下。

假设:某服务器上的内核参数net.core.somaxconn为128,net.ipv4.tcp_max_syn_backlog为8192,那么当用户backlog传入5时,半连接队列有多长?

与代码一样,我们也将计算分为四步,最终结果为16。

min(backlog, somaxconn)=min(5, 128)=5min(5, tcp_max_syn_backlog)=min(5, 8192)=8max(5, 8)=8roundup_pow_of_two(8 + 1)=16somaxconn 和tcp_max_syn_backlog 保持不变,监听当时的backlog增加到512,重复计算,结果是256。

min(backlog, somaxconn)=min(512, 128)=128min(128, tcp_max_syn_backlog)=min(128, 8192)=128max(128, 8)=8roundup_pow_of_two(128 + 1)=256 这里我算了一半的计算连接队列长度可以用一句话来概括。半连接队列的长度为min(backlog, somaxconn, tcp_max_syn_backlog) + 1然后向上取整到2的幂,但最小值不能小于16。我使用的内核源码是3.10。您拥有的内核版本可能与此略有不同。

如果线上遇到半连接队列溢出的问题,想要增加队列长度,需要同时考虑somaxconn、backlog、tcp_max_syn_backlog这三个内核参数。

最后,为了提高比较性能,内核并不直接记录半连接队列的长度。而是采用了一种晦涩的方法,只记录它的力量。如果队列长度为16,则max_qlen_log记录为4(2的4次方等于16)。假设队列长度为256,则max_qlen_log记录为8(2 of 2)。 8 的16 次方)。只要大家都知道这个东西只是为了提高性能。

最后,总结一下

计算机专业的学生背服务器端socket程序流程就像背八足文一样:先bind,再listen,再accept。至于为什么要先听再接受,我们似乎很少关注。

今天通过简单浏览一下listen源码,我们发现listen的主要工作就是申请并初始化接收队列,包括全连接队列和半连接队列。全连接队列是一个链表,而半连接队列则使用哈希表,因为它需要快速搜索(实际上,半连接队列更准确地称为半连接哈希表)。

full/half两个队列是三向握手中两个非常重要的数据结构。有了它们,服务器就可以正常响应客户端的三向握手。所以服务器端需要监听。

除此之外,我们还有一个额外的好处。我们还知道内核如何确定全/半连接队列的长度。

1.全连接队列的长度对于全连接队列,其最大长度为监听时传入的backlog与net.core.somaxconn之间较小的值。如果需要增加全连接队列长度,则调整backlog和somaxconn。

2.半连接队列的长度监听过程中,内核还看到,对于半连接队列,最大长度为min(backlog, somaxconn, tcp_max_syn_backlog) + 1然后向上取整到2的幂,但最小值不能小于超过16 。如果需要增加半连接队列的长度,则需要一起考虑backlog、somaxconn和tcp_max_syn_backlog这三个参数。互联网上任何告诉您更改某个参数可以增加半连接队列长度的文章都是错误的。

所以,不放过任何一个细节,说不定你会有意想不到的收获!

好了,文章到这里就结束啦,如果本次分享的服务端程序运作原理:监听端口的重要性和问题对您有所帮助,还望关注下本站哦!

用户评论

呆檬

服务端程序要听一下才能接收到客户发来的请求是不是挺像人一样啊!

    有14位网友表示赞同!

回忆未来

学习这方面知识真是很有意思,感觉能懂一些网络技术的原理了。

    有8位网友表示赞同!

来自火星球的我

原来这样子啊,我一直以为服务器就一直在那里等着呢。

    有18位网友表示赞同!

非想

听过 "Listen" 这个词在编程里的意思了?以前没怎么想过它其实像人一样等候着啥事儿。

    有20位网友表示赞同!

莫失莫忘

今天学到了新东西!之前不理解为什么服务端程序要先 "Listen",现在明白了!

    有11位网友表示赞同!

野兽之美

服务器的工作模式真是让人佩服,能够高效地处理来自众多客户的请求。

    有11位网友表示赞同!

丢了爱情i

"Listen" 这个词还真是形容得很贴切啊。

    有12位网友表示赞同!

〆mè村姑

感觉这个标题很有吸引力,我一定去看一看文章内容!

    有20位网友表示赞同!

话少情在

学习编程有时候就是需要慢慢琢磨这些原理...

    有19位网友表示赞同!

初阳

以前觉得服务器就只是个工具,现在才知道它还有这么多复杂的运作过程。

    有11位网友表示赞同!

孤败

我对网络技术的探索越来越感兴趣了!

    有13位网友表示赞同!

?亡梦爱人

希望这篇文章能够解释清楚这个问题,我迫切想知道!

    有18位网友表示赞同!

放肆丶小侽人

服务端程序的知识确实很重要,我现在要开动脑筋好好理解一下!

    有19位网友表示赞同!

微信名字

这个标题很有深度啊,让我很想去了解更多关于服务器的信息。

    有17位网友表示赞同!

ー半忧伤

编程语言和网络技术的发展真是日新月异!

    有14位网友表示赞同!

我没有爱人i

学习这方面的知识真的很丰富多彩,可以拓展我的视野。

    有20位网友表示赞同!

君临臣

真希望以后能够自己开发程序,体验一下服务端的运作过程!

    有8位网友表示赞同!

颜洛殇

网络的世界越来越复杂了,但也很让人兴奋!

    有17位网友表示赞同!

暖瞳

这个标题让我对服务器的工作有了新的认识,感觉很有启发性!

    有19位网友表示赞同!

本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/7823.html

联系我们

在线咨询:点击这里给我发消息

微信号:666666