TCP连接中客户端的端口号是如何确定的?

大家好,我是飞哥! 在 TCP 连接中,客户端在发起连接请求前会先确定一个客户端端口,然后用这个端口去和服务器端进行握手建立连接。那么在 Linux 上,客户端的端口到底是如何被确定下来的呢? 事实上很多我们平时遇到的问题都和这个端口选择过程相关,如果能深度理解这个过程,将有助于我们对这些问题的深刻理解。 Cannot assign requested address 报错是怎么回事? 一个客户端端口可以同时用在两条 TCP 连接上吗? 还是让我们借助一段简单到只有两句的代码,从这个来讲起! int main(){  fd = socket(AF_INET,SOCK_STREAM, 0);  connect(fd, …);  … } 一、创建 socket 客户端在发起连接的时候,需要事先创建一个 socket。在 c 语言中,就是调用 socket 函数,例如 socket(AF_INET,SOCK_STREAM, 0) 这句。 socket 函数执行完毕后,在用户层视角我们是看到返回了一个文件描述符 fd。但在内核中其实是一套内核对象组合,大体结构如下。 从上图我们看到,socket 在内核里并不是一个内核对象。而是包含 file、socket、sock 等多个相关内核对象构成,每个内核对象还定义了 ops 操作函数集合。在后面的内核源码执行过程中,我们需要时不时回头来看这些内核对象,这里先简单了解一下就行。 这些内核对象都是在 socket 系统调用执行过程中创建出来的。为了避免喧宾夺主,这里只列出入口代码,详细过程就不展开介绍了。 //file: net/socket.c SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) {  //创建 socket、sock 等内核对象,并初始化  sock_create(family, type, protocol, &sock);  //创建 file 内核对象,申请 fd  sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));  …… } 二、connect 发起连接… Continue reading TCP连接中客户端的端口号是如何确定的?

Published
Categorized as 网络

为什么服务端程序都需要先 listen 一下?

大家好,我是飞哥。飞哥在北京摇号 9 年多,最近终于算是搞下来个北京的电动车牌,其中的艰难过程写个一万字估计都写不完。不管咋说,新能源也是车,总算是有车能开了。这几天买车卖车(外地牌)忙的团团转。不过无论多忙,硬核文章仍然不能停! 大家都知道,在创建一个服务器程序的时候,需要先 listen 一下,然后才能接收客户端的请求。例如下面的这段代码我们再熟悉不过了。 int main(int argc, char const *argv[]) {  int fd = socket(AF_INET, SOCK_STREAM, 0);  bind(fd, …);  listen(fd, 128);  accept(fd, …); 那么我们今天来思考一个问题,为什么需要 listen 一下才能接收连接?或者换句话说,listen 内部执行的时候到底干了啥? 如果你也想搞清楚 listen 内部的这些秘密,那么请跟我来! 一、创建 socket 服务器要做的第一件事就是先创建一个 socket。具体就是通过调用 socket 函数。当 socket 函数执行完毕后,在用户层视角我们是看到返回了一个文件描述符 fd。但在内核中其实是一套内核对象组合,大体结构如下。 这里简单了解这个结构就行,后面我们在源码中看到函数指针调用的时候需要回头再来看它。 二、内核执行 listen 2.1 listen 系统调用 我在 net/socket.c 下找到了 listen 系统调用的源码。 //file: net/socket.c SYSCALL_DEFINE2(listen, int, fd, int, backlog) {  //根据 fd 查找 socket 内核对象  sock = sockfd_lookup_light(fd, &err, &fput_needed);  if (sock) {   //获取内核参数 net.core.somaxconn   somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;   if ((unsigned int)backlog > somaxconn)    backlog = somaxconn;      //调用协议栈注册的 listen 函数   err = sock->ops->listen(sock, backlog);   …… } 用户态的… Continue reading 为什么服务端程序都需要先 listen 一下?

Published
Categorized as 网络

深入解析常见三次握手异常

大家好,我是飞哥! 在后端接口性能指标中一类重要的指标就是接口耗时。具体包括平均响应时间 TP90、TP99 耗时值等。这些值越低越好,一般来说是几毫秒,或者是几十毫秒。如果响应时间一旦过长,比如超过了 1 秒,在用户侧就能感觉到非常明显的卡顿。如果长此以往,用户可能就直接用脚投票,卸载我们的 App 了。 在正常情况下一次 TCP 连接耗时也就大约是一次 RTT 多一点。但事情不一定总是这么美好,总会有意外发生。在某些情况下,可能会导致连接耗时上涨、CPU 处理开销增加、甚至是超时失败。 今天飞哥就来说一下我在线上遇到过的那些 TCP 握手相关的各种异常情况。 一、客户端 connect 异常 端口号和 CPU 消耗这二者听起来感觉没啥太大联系。但我却遭遇过因为端口号不足导致 CPU 消耗大幅上涨的情况。来听飞哥分析分析为啥会出现这种问题! 客户端在发起 connect 系统调用的时候,主要工作就是端口选择(参见TCP连接中客户端的端口号是如何确定的?)。 在选择的过程中,有个大循环,从 ip_local_port_range 的一个随机位置开始把这个范围遍历一遍,找到可用端口则退出循环。如果端口很充足,那么循环只需要执行少数几次就可以退出。但假设说端口消耗掉很多已经不充足,或者干脆就没有可用的了。那么这个循环就得执行很多遍。我们来看下详细的代码。 //file:net/ipv4/inet_hashtables.c int __inet_hash_connect(…) {  inet_get_local_port_range(&low, &high);  remaining = (high – low) + 1;  for (i = 1; i <= remaining; i++) {   // 其中 offset 是一个随机数   port = low + (i + offset) % remaining;   head = &hinfo->bhash[inet_bhashfn(net, port,      hinfo->bhash_size)];   //加锁   spin_lock(&head->lock);    //一大段的选择端口逻辑   //……   //选择成功就 goto ok   //不成功就 goto next_port   next_port:    //解锁    spin_unlock(&head->lock);   } } 在每次的循环内部需要等待锁,以及在哈希表中执行多次的搜索。注意这里的是自旋锁,是一种非阻塞的锁,如果资源被占用,进程并不会被挂起,而是会占用 CPU 去不断尝试获取锁。… Continue reading 深入解析常见三次握手异常

Published
Categorized as 网络

带你深入理解内存对齐最底层原理

相信绝大多数的人都了解内存对齐,对齐后性能高。但是其最最底层的原理是啥呢? 有的人可能会说,因为高速缓存的工作机制。读者你很聪明,这是原因之一。但我今天想挖的是更底层一点的原理,让我们去内存的物理构成里找找答案! 内存物理结构 我们来了解一下内存的物理构造,一般内存的外形图片如下图: 图1 内存外形图 一个内存是由若干个黑色的内存颗粒构成的。每一个内存颗粒叫做一个chip。每个chip内部,是由8个bank组成的。其构造如下图: 图2 chip内部构成 而每一个bank是一个二维平面上的矩阵,前面文章中我们说到过。矩阵中每一个元素中都是保存了1个字节,也就是8个bit。 图3 bank内部构成 内存编址方式 那么对于我们在应用程序中内存中地址连续的8个字节,例如0x0000-0x0007,是从位于bank上的呢?直观感觉,应该是在第一个bank上吗?其实不是的,程序员视角看起来连续的地址0x0000-0x0007,实际上是位于8个bank中的,每一个bank只保存了一个字节。在物理上,他们并不连续。下图很好地阐述了实际情况。 图4 连续8字节在内存中实际分布 你可能想知道这是为什么,原因是电路工作效率。内存中的8个bank是可以并行工作的。如果你想读取地址0x0000-0x0007,每个bank工作一次,拼起来就是你要的数据,IO效率会比较高。但要存在一个bank里,那这个bank只能自己干活。只能串行进行读取,需要读8次,这样速度会慢很多。 结论 所以,内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的。 对于64位数据宽度的内存,假如cpu也是64位的cpu(现在的计算机基本都是这样的),每次内存IO获取数据都是从同行同列的8个bank中各自读取一个字节拼起来的。从内存的0地址开始,0-7字节的数据可以一次IO读取出来,8-15字节的数据也可以一次读取出来。 换个例子,假如你指定要获取的是0x0001-0x0008,也是8字节,但是不是0开头的,内存需要怎么工作呢?没有好办法,内存只好先工作一次把0x0000-0x0007取出来,然后再把0x0008-0x0015取出来,把两次的结果都返回给你。CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。这样你的应用程序就会变慢,算是计算机因为你不懂内存对齐而给你的一点点惩罚。 扩展1:事实上,编译和链接器会自动替开发者对齐内存的,尽量帮你保证一个变量不跨列寻址。但是他不能做到十分完美。 扩展2:其实在内存硬件层上,还有操作系统层。操作系统还管理了CPU的一级、二级、三级缓存。不知道你有没有印象,我们前面的文章说过高速缓存里的Cache Line是64字节,它是内存IO单位的8倍,不会让内存IO浪费。

漫画 | 一台Linux服务器最多能支撑多少个TCP连接?

困惑很多人的并发问题 在网络开发中,我发现有很多同学对一个基础问题始终是没有彻底搞明白。那就是一台服务器最大究竟能支持多少个网络连接?我想我有必要单独发一篇文章来好好说一下这个问题。 很多同学看到这个问题的第一反应是65535。原因是:“听说端口号最多有65535个,那长连接就最多保持65535个了”。是这样的吗?还有的人说:“应该受TCP连接里四元组的空间大小限制,算起来是200多万亿个!” 如果你对这个问题也是理解的不够彻底,那么今天讲个故事讲给你听! 一次关于服务器端并发的聊天 “TCP连接四元组是源IP地址、源端口、目的IP地址和目的端口。任意一个元素发生了改变,那么就代表的是一条完全不同的连接了。拿我的Nginx举例,它的端口是固定使用80。另外我的IP也是固定的,这样目的IP地址、目的端口都是固定的。剩下源IP地址、源端口是可变的。所以理论上我的Nginx上最多可以建立2的32次方(ip数)×2的16次方(port数)个连接。这是两百多万亿的一个大数字!!” “进程每打开一个文件(linux下一切皆文件,包括socket),都会消耗一定的内存资源。如果有不怀好心的人启动一个进程来无限的创建和打开新的文件,会让服务器崩溃。所以linux系统出于安全角度的考虑,在多个位置都限制了可打开的文件描述符的数量,包括系统级、用户级、进程级。这三个限制的含义和修改方式如下:” 系统级:当前系统可打开的最大数量,通过fs.file-max参数可修改 用户级:指定用户可打开的最大数量,修改/etc/security/limits.conf 进程级:单个进程可打开的最大数量,通过fs.nr_open参数可修改 “我的接收缓存区大小是可以配置的,通过sysctl命令就可以查看。” $ sysctl -a | grep rmem net.ipv4.tcp_rmem = 4096 87380 8388608 net.core.rmem_default = 212992 net.core.rmem_max = 8388608 “其中在tcp_rmem”中的第一个值是为你们的TCP连接所需分配的最少字节数。该值默认是4K,最大的话8MB之多。也就是说你们有数据发送的时候我需要至少为对应的socket再分配4K内存,甚至可能更大。” “TCP分配发送缓存区的大小受参数net.ipv4.tcp_wmem配置影响。” $ sysctl -a | grep wmem net.ipv4.tcp_wmem = 4096 65536 8388608 net.core.wmem_default = 212992 net.core.wmem_max = 8388608 “在net.ipv4.tcp_wmem”中的第一个值是发送缓存区的最小值,默认也是4K。当然了如果数据很大的话,该缓存区实际分配的也会比默认值大。” 服务端百万连接达成记 “准备啥呢,还记得前面说过Linux对最大文件对象数量有限制,所以要想完成这个实验,得在用户级、系统级、进程级等位置把这个上限加大。我们实验目的是100W,这里都设置成110W,这个很重要!因为得保证做实验的时候其它基础命令例如ps,vi等是可用的。“ 活动连接数量确实达到了100W: $ ss -n | grep ESTAB | wc -l   1000024 当前机器内存总共是3.9GB,其中内核Slab占用了3.2GB之多。MemFree和Buffers加起来也只剩下100多MB了: $ cat /proc/meminfo MemTotal:        3922956 kB MemFree:           96652 kB MemAvailable:       6448 kB Buffers:           44396 kB …… Slab:          3241244KB kB 通过slabtop命令可以查看到densty、flip、sock_inode_cache、TCP四个内核对象都分别有100W个: 结语 互联网后端的业务特点之一就是高并发. 但是一台服务器最大究竟能支持多少个TCP连接,这个问题似乎却又在困惑着很多同学。希望今天过后,你能够将这个问题踩在脚下摩擦! 学习是一件痛苦的事情,尤其咱们号里很多读者朋友都是工作满一天了再来看我的技术号的文章的。我一直都在琢磨到底怎么样组织技术内容形式,能让大家理解起来更能省一点脑细胞呢。这篇服务器的最大并发数的文章是早就想发的,但是写了两三个版本都不满意。今天终于想出了一种让大家更容易理解的方式,算过了自己这关了。 如果您喜欢我的文章、并觉得它有用,期望您能不吝把它转发到你的朋友圈,技术群。或者哪怕是点个赞,点个再看都可以。触达更多的技术同学并收获大家的反馈将极大地提升彦飞的创作动力! 改天再讲客户端,敬请期待!! 开发内功修炼 飞哥有鹅厂、搜狗 10 年多的开发工作经验。通过本号,我把多年中对于性能的一些深度思考分享给大家。 66篇原创内容 公众号  

Published
Categorized as 网络