精华内容
下载资源
问答
  • 一个端口多少tcp连接
    千次阅读
    2021-07-15 15:01:28

    一个端口到底可以建立多少TCP连接?
    这是一个很基础的问题,但是网上对于这个问题的解释千奇百怪。

    有人说,一个端口只能建立一个TCP连接,所以说无论服务还是客户端都最多只能建立65535个TCP连接。还有人说,服务端因为accept之后新建立的socket是重用listen的端口的,所以服务端最多可以建立65535×n个连接,而客户端connect建立的端口不可重用所以客户端只能65535。

    这里有两个误解:

    误解一:一个端口只能建立一个TCP连接。事实上Linux内核对TCP连接的识别是通过四元组来区分,即(源ip,源port,目标ip,目标port)。这个四元组只要任意一个不同,就是完全不同的连接!所以说,只要建立的连接是不同的,一个端口是可以建立多个TCP连接的!

    误解二:客户端connect建立的端口不可重用。在一篇讨论这个问题的知乎专栏中作者说“当Linux作为客户端建立连接的时候,最大连接数量是受内核参数net.ipv4.ip_local_port_range限制 而ip_local_port_range是可配置的,最大理论范围是0-65535”。他的这个结论建立在下面的代码:

    img

     

    我们可以看到作者的方法是:在客户端建立num个socket和同一服务器ip:port建立连接,如果是这样那么从四元组出发目的ip和目的port都固定了,那一个客户端端口当然只能建立一个和同一ip:port的连接,那这样算当然只能建立65535(端口数)个连接了!但是场景应当换到和不同的服务端ip:port建立连接啊!也就是说,对于一个端口,只要它要建立的连接不同是可以建立多个连接的。所以对于客户端也没有所谓”端口不可重用所以客户端只能65535“的无稽之谈!其能建立的最大TCP连接数量也应当是65535×n。n取决于单进程/系统/用户最大能打开的文件描述数和内存情况,其实文件描述符数量也不要紧,可以改内核参数对应的限制,所以真正限制一台主机(无论服务客户)的TCP最大连接数的只有内存!
     

    更多相关内容
  • 文章目录文章1:不太清楚socket一个端口如何建立多个tcp连接,这边转载一篇文章文章2:一台Linux服务器最多能支撑多少TCP连接?结论:socket服务端只用开一个端口监听(listen)socket请求就行,理论上来说,最大...

    文章1:不太清楚socket一个端口如何建立多个tcp连接,这边转载一篇文章

    一、背景

    记得上学期暑假的时候我基于MFC写了一个简单的聊天程序。那个聊天程序,两部分组成,监听客户端请求线程和客户端请求处理线程。

    1.服务器接收到登陆请求,验证登陆信息后,如果通过验证建立新线程与其交互,并通知用户连接到新的端口,并创建好新端口的SOCKET连接。

    2.然后将用户类和新端口传给新建立的客户端请求处理线程。

    当时,可能是没理解好的原因,误以为,一个端口同一时间只能建立起一个TCP连接。所以写这个聊天程序时才会每一个用户分配一个新的端口。

    之前,自己了解HTTP后,接触到web应用开发的时候,就疑惑了,web server接收浏览器的请求,都是从80端口接受请求。当时没仔细去想,就以为web server和我那个聊天程序一样,会去建立新的线程与其进行请求处理。

    二、问题

    最近,写爬虫的时候用到了Smsniff去抓包。发现,一个http请求中。往往是只与服务器的80端口进行通信。这就与我记忆中的SOCKET冲突了。于是今天写了个小代码测试了一下,一个端口,真的能建立多个连接。

    三、代码逻辑流程

    1.server

    ①服务端主线程:负责监听5174端口,如果有请求,accept到系统分配的SOCKET(为unsigned int, recv接受函数就需要这个SOCKET)于是建立一个新线程,将这个SOCKET通过lpParament传递给新线程。

    ②服务器信息接受线程:负责从lpParment从拿到SOCKET并,recv客户端发来的信息。

    2.client

    连接到服务器的5174端口,并发送消息。

    四、执行结果

    一个端口的确能同时建立多条TCP请求。
    在这里插入图片描述
    五、理解

    综合部分网上看到的资料。我的理解是,一个连接的唯一标识是[server ip, server port, client ip, client port]也就是说。操作系统,接收到一个端口发来的数据时,会在该端口,产生的连接中,查找到符合这个唯一标识的并传递信息到对应缓冲区。

    1.一个端口同一时间只能bind给一个SOCKET。就是同一时间一个端口只可能有一个监听线程(监听listen之前要bind)。

    2.为什么一个端口能建立多个TCP连接,同一个端口也就是说 server ip和server port 是不变的。那么只要[client ip 和 client port]不相同就可以了。能保证接唯一标识[server ip, server port, client ip, client port]的唯一性。

    六、疑问解答

    1.如果监听的线程释放掉监听用的SOCKET了,会影响之前通过这个监听SOCKET建立的TCP连接么?

    答案:并不会,SOCKET之间是独立的,不会有影响(我已经自己写了程序验证了,读者可以自己写代码验证)。

    2.一个端口能建立多个UDP连接么?

    答案:UPD本身就是无连接的。所以不存在什么多个UDP连接。只是,服务端接收UDP数据需要bind一个端口。一个SOCKET只能绑定到一个端口。

    参考文章:SOCKET编程之一个端口能建立多个TCP连接?

    文章2:一台Linux服务器最多能支撑多少个TCP连接?

    1、一次关于服务器端并发的聊天
    在这里插入图片描述

    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。当然了如果数据很大的话,该缓存区实际分配的也会比默认值大。

    在这里插入图片描述
    2、服务端百万连接达成记’

    在这里插入图片描述

    准备啥呢,还记得前面说过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个:
    在这里插入图片描述
    在这里插入图片描述

    结论:socket服务端只用开一个端口监听(listen)socket请求就行,理论上来说,最大能支持2的32次方(ip数)×2的16次方(port数)个连接,但是linux对打开文件数有限制(65536个,每个socket连接占用一个文件),如果想支持更多,需要修改系统级/用户级/进程级

    参考文章:一个服务端端口能建立多个TCP连接吗

    文章3:为什么服务端需要产生两个socket(listen_socket_fd和connect_socket_fd)

    为什么服务端需要产生两个socket(listen_socket_fd和connect_socket_fd)
    答:监听socket是服务器作为客户端连接请求的一个对端,只需创建一次即可,它存在于服务器的整个生命周期,可为成千上万的客户端服务,而一旦一个客户端和服务器连接成功,完成了TCP三次握手,操作系统内核就为这个客户端生成一个已连接套接字(connect_socket_fd),让应用服务器使用这个connect_socket_fd和客户端进行通信,如果应用服务器完成了对这个客户端的服务,那么关闭的就是已连接套接字,这样就完成了TCP连接的释放。请注意,这个时候释放的只是这一个客户端连接,其它被服务的客户端连接可能还存在。最重要的是,监听套接字一直都处于“监听”状态,等待新的客户请求到达并服务。若只使用一个listen_socket_fd完成从创建监听到被请求连接,处理请求,关闭socket的整个过程,那么这个socket就会一直被占用,而不能被其它的客户端请求,造成服务端性能低下。使用两个socket,按职责分工,listen_socket_fd专门负责响应客户端的请求,每个新的connect_socket_fd专门负责该次连接的数据交互,分层协作,提高服务端的性能。

    先放着,去看其他的先。。。

    20220512 一个端口当然是可以建立多个tcp连接的,以socket服务端为例,bind一个端口后监听,可以用fork子线程的方法去处理socket_fd;如果不想用子线程处理,也可以用select,或者poll或者epoll,都是可以的

    参考文章:linux C/C++多进程教程(多进程原理以及多进程的应用【以多连接socket服务端为例(fork子进程处理socket_fd),同时介绍了僵尸进程产生原因与解决方法】)(getpid、fork)

    20220512 像上面说维持了100W条长连接,应该绝对没有fork子进程,否则子进程怎么够呢?估计是单线程,用select或poll、epoll方法,一个连接占用一个文件描述符(上文图中称为“文件句柄”)

    在这里插入图片描述

    展开全文
  • Socket编程之一个端口能建立多个TCP连接

    万次阅读 多人点赞 2018-05-14 12:53:34
    记得上学期暑假的时候我基于MFC写了一个简单的聊天程序。那个聊天程序,两部分组成,监听客户端请求线程和客户端请求处理线程。 1.服务器接收到登陆请求,验证登陆信息后,如果通过验证建立新线程与其交互,并通知...

    个人博客:www.saoguang.top

    一、背景

    记得上学期暑假的时候我基于MFC写了一个简单的聊天程序。那个聊天程序,两部分组成,监听客户端请求线程和客户端请求处理线程。

    1.服务器接收到登陆请求,验证登陆信息后,如果通过验证建立新线程与其交互,并通知用户连接到新的端口,并创建好新端口的SOCKET连接。

    2.然后将用户类和新端口传给新建立的客户端请求处理线程。

    当时,可能是没理解好的原因,误以为,一个端口同一时间只能建立起一个TCP连接。所以写这个聊天程序时才会每一个用户分配一个新的端口。

    之前,自己了解HTTP后,接触到web应用开发的时候,就疑惑了,web server接收浏览器的请求,都是从80端口接受请求。当时没仔细去想,就以为web server和我那个聊天程序一样,会去建立新的线程与其进行请求处理。

    二、问题

    最近,写爬虫的时候用到了Smsniff去抓包。发现,一个http请求中。往往是只与服务器的80端口进行通信。这就与我记忆中的SOCKET冲突了。于是今天写了个小代码测试了一下,一个端口,真的能建立多个连接。

    三、代码逻辑流程

    1.server

    ①服务端主线程:负责监听5174端口,如果有请求,accept到系统分配的SOCKET(为unsigned int, recv接受函数就需要这个SOCKET)于是建立一个新线程,将这个SOCKET通过lpParament传递给新线程。

    ②服务器信息接受线程:负责从lpParment从拿到SOCKET并,recv客户端发来的信息。

    2.client

    连接到服务器的5174端口,并发送消息。

    四、执行结果

    一个端口的确能同时建立多条TCP请求。

    五、理解

    综合部分网上看到的资料。我的理解是,一个连接的唯一标识是[server ip, server port, client ip, client port]也就是说。操作系统,接收到一个端口发来的数据时,会在该端口,产生的连接中,查找到符合这个唯一标识的并传递信息到对应缓冲区。

    1.一个端口同一时间只能bind给一个SOCKET。就是同一时间一个端口只可能有一个监听线程(监听listen之前要bind)。

    2.为什么一个端口能建立多个TCP连接,同一个端口也就是说 server ip和server port 是不变的。那么只要[client ip 和 client port]不相同就可以了。能保证接唯一标识[server ip, server port, client ip, client port]的唯一性。

     

    六、疑问解答

    1.如果监听的线程释放掉监听用的SOCKET了,会影响之前通过这个监听SOCKET建立的TCP连接么?

    答案:并不会,SOCKET之间是独立的,不会有影响(我已经自己写了程序验证了,读者可以自己写代码验证)。

    2.一个端口能建立多个UDP连接么?

    答案:UPD本身就是无连接的。所以不存在什么多个UDP连接。只是,服务端接收UDP数据需要bind一个端口。一个SOCKET只能绑定到一个端口。

    七、服务端和客户端代码

    注意:如果用的是VS,记得把 SDL checks(安全开发生命周期检测关闭)

    服务端:

    #include <stdio.h>
    #include <stdlib.h>
    #include <WinSock2.h>
    #pragma comment(lib,"Ws2_32.lib")
    #include <memory.h>
    
    #define BUF_SIZE 4096
    #define QUEUE_SIZE 5
    
    DWORD WINAPI ThreadProcServerConmunicate(
    	_In_ LPVOID lpParameter
    ) {
    	SOCKET * psc = (SOCKET *)lpParameter;
    
    	int receByt = 0;
    	while (1)
    	{
    		char buf[BUF_SIZE];
    		receByt = recv(*psc, buf, BUF_SIZE, 0);
    		buf[receByt] = '\0';
    		if (receByt>0)
    		{
    			printf("%u : 接收的消息是:%s\n", *psc, buf);
    		}
    		else
    		{
    			printf("接收消息结束!");
    			break;
    		}
    
    	}
    	int ic = closesocket(*psc);
    	free(psc);
    	return 0;
    }
    
    
    int main() {
    	
    	WSADATA wsd;
    	WSAStartup(MAKEWORD(2, 0), &wsd);
    
    	SOCKET s = NULL;
    	s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    	struct sockaddr_in ch;
    	memset(&ch, 0, sizeof(ch));
    	ch.sin_family = AF_INET;
    	ch.sin_addr.s_addr = INADDR_ANY;
    	ch.sin_port = htons(5174);
    	int b = bind(s, (struct sockaddr *) &ch, sizeof(ch));
    	
    	int l = listen(s, QUEUE_SIZE);
    	printf("正在监听本机的5174端口\n");
    
    	while (1) {
    		SOCKET * psc = (SOCKET *)malloc(sizeof(SOCKET));
    		*psc = accept(s, 0, 0);
    		printf("一个客户端已经连接到本机的5174端口,SOCKET是 : %u \n", *psc);
    
    		CreateThread(NULL,
    			0,
    			&ThreadProcServerConmunicate,
    			psc,
    			0,
    			NULL
    		);
    	}
    	
    	int is = closesocket(s);
    	WSACleanup();
    	return 0;
    }

    客户端:

    #include <stdio.h>
    #include <stdlib.h>
    #include <WinSock2.h>
    #pragma comment(lib,"Ws2_32.lib")
    #include <memory.h>
    
    #define BUF_SIZE 4096
    
    void main()
    {
    	WSADATA wsd;
    	WSAStartup(MAKEWORD(2, 0), &wsd);
    
    	SOCKET s = NULL;
    	s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    	struct sockaddr_in ch;
    	memset(&ch, 0, sizeof(ch));
    	ch.sin_family = AF_INET;
    	ch.sin_addr.s_addr = inet_addr("127.0.0.1");
    	ch.sin_port = htons(5174);
    
    	int c = connect(s, (struct sockaddr *) &ch, sizeof(ch));
    	printf("已经连接到服务器的5174端口,现在可以向服务器发送消息了!\n");
    
    	char info[1024], buf[BUF_SIZE];
    
    	while (1)
    	{
    		gets(info);
    		if (info[0] == '\0')
    			break;
    		strcpy(buf, info);
    		int nsend = send(s, buf, strlen(buf), 0);
    		Sleep(500);
    	}
    	int ic = closesocket(s);
    	WSACleanup();
    	return 0;
    }

     

    展开全文
  • TCP 连接中,客户端在发起连接请求前会先确定一个客户端的端口,然后用这个端口去和服务器端进行握手建立连接。那么在 Linux 上,客户端的端口到底是如何被确定下来的呢? 事实上,我们平时很多遇到的问题都和这...

    一、前言

    • 在 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 发起连接

    ① connect 调用链展开

    • 当在客户端机上调用 connect 函数的时候,事实上会进入到内核的系统调用源码中进行执行:
    // file: net/socket.c
    SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
      int, addrlen) {
        struct socket *sock;
    
        // 根据用户 fd 查找内核中的 socket 对象
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
    
        // 进行 connect
        err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
          sock->file->f_flags);
        ...
    }
    
    • 从上面的代码可以看出:首先根据用户传入的 fd(文件描述符)来查询对应的 socket 内核对象。了解了上文中的 socket 内核对象结构,据此可以知道接下来 sock->ops->connect 其实调用的是 inet_stream_connect 函数:
    // file: ipv4/af_inet.c
    int inet_stream_connect(struct socket *sock, ...) { 
       ...
       __inet_stream_connect(sock, uaddr, addr_len, flags);
    }
    
    int __inet_stream_connect(struct socket *sock, ...) {
       struct sock *sk = sock->sk;
    
       switch (sock->state) {
          case SS_UNCONNECTED:
          err = sk->sk_prot->connect(sk, uaddr, addr_len);
          sock->state = SS_CONNECTING;
          break;
       }
       ...
    }
    
    • 刚创建完毕的 socket 的状态就是 SS_UNCONNECTED,所以在 __inet_stream_connect 中的 switch 判断会进入到 case SS_UNCONNECTED 的处理逻辑中。
    • 上述代码中 sk 取的是 sock 对象,继续回顾上文中的 socket 的内核数据结构图,可以得知 sk->sk_prot->connect 实际上对应的是 tcp_v4_connect 方法。
    • 现在来看 tcp_v4_connect 的代码,它位于 net/ipv4/tcp_ipv4.c:
    // 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(&tcp_death_row, sk);
    
       // 函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去
       err = tcp_connect(sk);
    }
    
    • 在 tcp_v4_connect 中终于看到了选择端口的函数,那就是 inet_hash_connect。

    ② 选择可用端口

    • 找到 inet_hash_connect 的源码,来看看到底端口是如何选择出来的:
    // file:net/ipv4/inet_hashtables.c
    int inet_hash_connect(struct inet_timewait_death_row *death_row,
            struct sock *sk) {
       return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),
       __inet_check_established, __inet_hash_nolisten);
    }
    
    • 这里需要提一下在调用 __inet_hash_connect 时传入的两个重要参数:
      • inet_sk_port_offset(sk):这个函数是根据要连接的目的 IP 和端口等信息生成一个随机数;
      • __inet_check_established:检查是否和现有 ESTABLISH 的连接是否冲突的时候用的函数。
    • 了解了这两个参数后,继续进入 __inet_hash_connect,这个函数比较长,为了方便理解,先看前面这一段:
    // file:net/ipv4/inet_hashtables.c
    int __inet_hash_connect(...) {
        // 是否绑定过端口
        const unsigned short snum = inet_sk(sk)->inet_num;
    
        // 获取本地端口配置
        inet_get_local_port_range(&low, &high);
        remaining = (high - low) + 1;
    
        if (!snum) {
           // 遍历查找
           for (i = 1; i <= remaining; i++) {
              port = low + (i + offset) % remaining;
              ...
           }
        }
    }
    
    • 在这个函数中首先判断了 inet_sk(sk)->inet_num,如果调用过 bind,那么这个函数会选择好端口并设置在 inet_num 上。这里假设没有调用过 bind,所以 snum 为 0。
    • 接着调用 inet_get_local_port_range,这个函数读取的是 net.ipv4.ip_local_port_range 这个内核参数,来读取管理员配置的可用的端口范围:
    该参数的默认值是 32768 61000,意味着端口总可用的数量是 61000 - 32768 = 28232 个。如果你觉得这个数字不够用,那就修改你的 net.ipv4.ip_local_port_range 内核参数。
    
    • 接下来进入到了 for 循环中,其中 offset 是前面所说的,通过 inet_sk_port_offset(sk) 计算出的随机数,那这段循环的作用就是从某个随机数开始,把整个可用端口范围来遍历一遍,直到找到可用的端口后停止。
    • 那么接着来看,如何来确定一个端口是否可以使用呢?
    // file:net/ipv4/inet_hashtables.c
    int __inet_hash_connect(...) {
      for (i = 1; i <= remaining; i++) {
      port = low + (i + offset) % remaining;
    
      // 查看是否是保留端口,是则跳过
      if (inet_is_reserved_local_port(port))
       continue;
    
      // 查找和遍历已经使用的端口的哈希链表
      head = &hinfo->bhash[inet_bhashfn(net, port,
        hinfo->bhash_size)];
      inet_bind_bucket_for_each(tb, &head->chain) {
    
       // 如果端口已经被使用
       if (net_eq(ib_net(tb), net) &&
           tb->port == port) {
    
    	// 通过 check_established 继续检查是否可用
        if (!check_established(death_row, sk,
           port, &tw))
         goto ok;
       }
      }
    
      // 未使用的话,直接 ok
      goto ok;
     }
    
     return -EADDRNOTAVAIL;
    ok: 
     ...  
    }
    
    • 首先判断的是 inet_is_reserved_local_port,这个很简单,就是判断要选择的端口是否在 net.ipv4.ip_local_reserved_ports 中,在的话就不能用(如果因为某种原因不希望某些端口被内核使用,那么就把它们写到 ip_local_reserved_ports 这个内核参数中就行)。
    • 整个系统中会维护一个所有使用过的端口的哈希表,它就是 hinfo->bhash,接下来的代码就会在这里进行查找。如果在哈希表中没有找到,那么说明这个端口是可用的,至此端口就算是找到了。
    • 遍历完所有端口都没找到合适的,就返回 -EADDRNOTAVAIL,在用户程序上看到的就是 Cannot assign requested address 这个错误,怎么样?是不是很眼熟,有没有?我相信大家都见过它的,对吧?
    /* Cannot assign requested address */
    #define EADDRNOTAVAIL 99 
    
    • 以后当再遇到 Cannot assign requested address 错误,我们应该想到去查一下 net.ipv4.ip_local_port_range 中设置的可用端口的范围是不是太小了。

    ③ 端口被使用过怎么办?

    • 回顾刚才的 __inet_hash_connect, 为了描述简单,我们之前跳过了已经在 bhash 中存在时候的判断,这是由于其一这个过程比较长,其二这段逻辑很有价值:
    // file:net/ipv4/inet_hashtables.c
    int __inet_hash_connect(...) {
     for (i = 1; i <= remaining; i++) {
      port = low + (i + offset) % remaining;
    
      ...
      // 如果端口已经被使用
      if (net_eq(ib_net(tb), net) && tb->port == port) {
       // 通过 check_established 继续检查是否可用
       if (!check_established(death_row, sk, port, &tw))
        goto ok;
      }
     }
    }
    
    • port 已经在 bhash 中如果已经存在,就表示有其它的连接使用过该端口;如果 check_established 返回 0,该端口仍然可以接着使用。可能你会有困惑,一个端口怎么可以被用多次呢?
    • 回忆下四元组的概念,两对儿四元组中只要任意一个元素不同,都算是两条不同的连接。以下的两条 TCP 连接完全可以同时存在(假设 192.168.1.101 是客户端,192.168.1.100 是服务端)
    连接1192.168.1.101 5000 192.168.1.100 8090
    连接2192.168.1.101 5000 192.168.1.100 8091
    
    • check_established 作用就是检测现有的 TCP 连接中是否四元组和要建立的连接四元素完全一致,如果不完全一致,那么该端口仍然可用。这个 check_established 是由调用方传入的,实际上使用的是 __inet_check_established,我们来看它的源码:
    // file: net/ipv4/inet_hashtables.c
    static int __inet_check_established(struct inet_timewait_death_row *death_row,
            struct sock *sk, __u16 lport,
            struct inet_timewait_sock **twp)
    {
     // 找到hash桶
     struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
    
     // 遍历看看有没有四元组一样的,一样的话就报错
     sk_nulls_for_each(sk2, node, &head->chain) {
      if (sk2->sk_hash != hash)
       continue;
      if (likely(INET_MATCH(sk2, net, acookie,
              saddr, daddr, ports, dif)))
       goto not_unique;
     }
    
    unique:
     // 要用了,记录,返回 0 (成功)
     return 0;
    not_unique:
     return -EADDRNOTAVAIL; 
    }
    
    • 该函数首先找到 inet_ehash_bucket,这个和 bhash 类似,只不过是所有 ESTABLISH 状态的 socket 组成的哈希表,然后遍历这个哈希表,使用 INET_MATCH 来判断是否可用。
    • INET_MATCH 源码如下:
    // include/net/inet_hashtables.h
    #define INET_MATCH(__sk, __net, __cookie, __saddr, __daddr, __ports, __dif) \
     ((inet_sk(__sk)->inet_portpair == (__ports)) &&  \
      (inet_sk(__sk)->inet_daddr == (__saddr)) &&  \
      (inet_sk(__sk)->inet_rcv_saddr == (__daddr)) &&  \
      (!(__sk)->sk_bound_dev_if ||    \
        ((__sk)->sk_bound_dev_if == (__dif)))  &&  \
      net_eq(sock_net(__sk), (__net)))
    
    • 在 INET_MATCH 中将 __saddr、__daddr、__ports 都进行了比较,当然除了 ip 和端口,INET_MATCH还比较了其它一些东西,所以 TCP 连接还有五元组、七元组之类的说法。为了统一,这里还是沿用四元组的说法:
      • 如果 MATCH,就是说就四元组完全一致的连接,所以这个端口不可用,也返回 -EADDRNOTAVAIL;
      • 如果不 MATCH,哪怕四元组中有一个元素不一样,例如服务器的端口号不一样,那么就 return 0,表示该端口仍然可用于建立新连接。
    • 所以一台客户端机最大能建立的连接数并不是 65535,只要 server 足够多,单机发出百万条连接没有任何问题。

    ④ 发起 syn 请求

    • 再回到 tcp_v4_connect,这时 inet_hash_connect 已经返回了一个可用端口,接下来就进入到 tcp_connect,如下源码所示:
    // file: net/ipv4/tcp_ipv4.c
    int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) {
     ......
    
     // 动态选择一个端口
     err = inet_hash_connect(&tcp_death_row, sk);
    
     // 函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去
     err = tcp_connect(sk);
    }
    
    • 到这里,其实就和本文要讨论的主题没有太大的关系,所以只是简单看一下:
    // file:net/ipv4/tcp_output.c
    int tcp_connect(struct sock *sk) {
     // 申请并设置 skb
     buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);
     tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
    
     // 添加到发送队列 sk_write_queue 上
     tcp_connect_queue_skb(sk, buff);
    
     // 实际发出 syn
     err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
           tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
    
     // 启动重传定时器
     inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
          inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
    }
    
    • tcp_connect 主要处理了以下逻辑:
      • 申请一个 skb,并将其设置为 SYN 包;
      • 添加到发送队列上;
      • 调用 tcp_transmit_skb 将该包发出;
      • 启动一个重传定时器,超时会重发。

    四、bind 时端口如何选择

    • 在上文中,我们看到 connect 选择端口之前先判断了 inet_sk(sk)->inet_num 有没有值,如果有的话就直接用这个,而会跳过端口选择过程。那么这个值是从哪儿来的呢?其实,它就是在对 socket 使用 bind 时设置的。
    • 不只是服务器端,哪怕是对于客户端,也可以对 socket 使用 bind 来绑定 IP 或者端口,如果使用了 bind,那么在 bind 的时候就会确定好端口,并设置到 inet_num 变量中(一般非常不推荐在客户端角色下使用 bind,因为这会打乱 connect 里的端口选择过程)。
    • bind 的时候,如果传了端口,那么 bind 就会尝试使用该端口,如果端口号传的是 0 ,那么 bind 有一套独立的选择端口号的逻辑:
    // file: net/ipv4/af_inet.c
    int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len) {
     struct sock *sk = sock->sk;
     ...
    
     // 用户传入的端口号
     snum = ntohs(addr->sin_port);
    
     // 不允许绑定 1024 以下的端口
     if (snum && snum < PROT_SOCK &&
         !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
      goto out;
    
     // 尝试确定端口号
     if (sk->sk_prot->get_port(sk, snum)) {
      inet->inet_saddr = inet->inet_rcv_saddr = 0;
      err = -EADDRINUSE;
      goto out_release_sock;
     }
    
    • 根据上文中的 socket 内核对象,能找到 sk->sk_prot->get_port 实际调用的是 inet_csk_get_port,该函数来尝试确定端口号,如果尝试失败,返回 EADDRINUSE,应用程序将会显示一条错误信息 “Address already in use”。
    #define EADDRINUSE 226 /* Address already in use */
    
    • 简单看一下,如果用户没有传入端口(传入的为 0),bind 是怎么选择端口的:
    // file: net/ipv4/inet_connection_sock.c
    int inet_csk_get_port(struct sock *sk, unsigned short snum) {
     ...
    
     if (!snum) {
      inet_get_local_port_range(&low, &high);
      remaining = (high - low) + 1;
      smallest_rover = rover = net_random() % remaining + low;
    
      do {
       if (inet_is_reserved_local_port(rover))
        goto next_nolock;
    
       head = &hashinfo->bhash[inet_bhashfn(net, rover,
          hashinfo->bhash_size)];
       inet_bind_bucket_for_each(tb, &head->chain)
    
        // 冲突检测
        if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
         snum = rover;
         goto tb_found;
        }
    
      } while (--remaining > 0);
     }
    }
    
    • 这段逻辑和 connect 很像,通过 net_random 来从 net.ipv4.ip_local_port_range 指定的端口范围内一个随机位置开始遍历,也会跳开 ip_local_reserved_ports 保留端口配置,通过 inet_csk(sk)->icsk_af_ops->bind_conflict 进行冲突检测。
    • inet_csk_bind_conflict 这个函数整体比较复杂,不过只需要了解一点就好,该函数和 connect 中端口选择逻辑不同的是,并不会到 ESTABLISH 的哈希表进行可用检测,只在 bind 状态的 socket 里查。所以默认情况下,只要端口用过一次就不会再次使用。

    五、结论

    • 客户端建立连接前需要确定一个端口,该端口会在两个位置进行确定。
    • 第一个位置,也是最主要的确定时机是 connect 系统调用执行过程。
      • 在 connect 的时候,会随机地从 ip_local_port_range 选择一个位置开始循环判断;找到可用端口后,发出 syn 握手包,如果端口查找失败,会报错 “Cannot assign requested address”,这时,应该首先想到去检查一下服务器上的 net.ipv4.ip_local_port_range 参数,是不是可以再放的多一些。
      • 如果因为某种原因不希望某些端口被使用到,那么就把它们写到 ip_local_reserved_ports 这个内核参数中就行了,内核在选择的时候会跳过这些端口。
      • 另外注意即使是一个端口是可以被用于多条 TCP 连接的,所以一台客户端机最大能建立的连接数并不是 65535,只要 server 足够多,单机发出百万条连接没有任何问题。
      • 如下所示,在客户机上实验时的实际截图,来实际看一下一个端口号确实是被用在了多条连接上:

    在这里插入图片描述

      • 截图中左边的 192 是客户端,右边的 119 是服务器的 ip,可以看到客户端的 10000 这个端口号是用在了多条连接上了的。
    • 第二个位置,如果在 connect 之前使用了 bind,将会使得 connect 时的端口选择方式无效,转而使用 bind 时确定的端口。
      • bind 时如果传入了端口号,会尝试首先使用该端口号,如果传入了 0 ,也会自动选择一个。但默认情况下一个端口只会被使用一次,所以对于客户端角色的 socket,不建议使用 bind。
      • 上面的选择端口的都是从 ip_local_port_range 范围中的某一个随机位置开始循环的,如果可用端口很充足,则能快一点找到可用端口,那循环很快就能退出。
      • 假设实际中 ip_local_port_range 中的端口快被用光,这时候内核就大概率得把循环多执行很多轮才能找到,这会导致 connect 系统调用的 CPU 开销的上涨。
      • 所以,最好不要等到端口不够用了才加大 ip_local_port_range 的范围,而是事先就应该保持一个充足的范围。
    展开全文
  • Tcp连接端口占用问题

    千次阅读 2022-03-01 15:46:14
    一个套接字不能连接两次,并不代表一个本地地址不能用两次 >>> import socket >>> s = socket.socket() # since Linux 3.9, 见 man 7 socket >>> s.setsockopt(socket.SOL_SOCKET, ...
  • linux查看端口TCP连接情况

    千次阅读 2021-07-18 21:21:12
    1、第一个为服务器的监听套接字,其监听队列中存在两个连接未处理,而整个服务器连接到19833端口连接才3个。 2、其中源主机端口为37534和37532的接收队列中有17303字节的数据未处理。 3、三个连接的发送队列均无...
  • tomcat作为服务端程序,一直在监听80端口,之前一直以为tomcat每接收到一个新的连接,都会创建一个新的socket,然后这个socket又会占用一个端口。但是事实上并不是这样的,肯定是会创建新的socket的,但是这个新创建...
  • 台服务器最多支持多少TCP连接

    千次阅读 2021-06-04 10:41:53
    那Nginx只能接受一个TCP连接?这明显是太荒唐了。因为一个客户端有多个端口,根据四元组,参考下面结论。 服务端理论最大并发数: 2 的 32 次方(ip数)× 2的 16 次方(port数)大约等于两百多万亿。(四元组) ...
  • 这是我在讨论区看到的一个回答,写的很好,让我明白了为什么单个服务器程序可承受最大连接数可以达到几十W 要写网络程序就必须用Socket,这是程序员都知道的。而且,面试的时候,我们也会问对方会不会Socket编程?...
  • TCP端口TCP连接管理

    千次阅读 2018-08-27 12:34:49
    对于这65536个端口号,有以下使用规定。 (1)端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1~1023之间的端口号。这些端口号由Internet端口号分配机构来...
  • 通过一个tcp连接获取客户端ip和端口

    千次阅读 2021-02-17 15:05:49
    在多路实况下出现某几路视频卡顿时,最好的办法是分析流数据,但抓包时同时可能抓了多路视频流,一般会先分析日志,确定哪几路流存在问题,然后通过日志里的客户端ip和端口号去找到要分析的那路流,一般情况流媒体...
  • 对于客户端来说,linux向外TCP最大的连接数是28232, 为什么呢? 因为它只能打开28232个端口,具体看下面! 当你写程序连接端口的时候,达到28231 - 28233左右这个连接数的时候,就会报错 cannot assign ...
  • 一个端口多个连接

    千次阅读 2019-05-08 16:39:42
    服务程序在listen某个端口并accept某个连接请求后,会生成一个新的socket来对该请求进行处理。 如果一个程序创建了一个socket,并让其监听80端口,其实是向TCP/IP协议栈声明了其对80端口的占有。以后,所有目标是80...
  • 这是一个简短的帖子,用于检查来自 Linux 服务器的端口 [TCP/UDP] 连接TCP/IP 网络连接可能被阻止、丢弃、打开或过滤。这些操作通常由系统使用的 IPtables 防火墙控制,并且独立于可能正在侦听网络端口的任何进程...
  • TCP协议中的端口

    千次阅读 2021-09-07 16:41:33
    2. 逻辑意义的端口,一般是指TCP/IP协议中的端口,范围 0到65535,比如用于浏览器网页服务的80端口,用于FTP服务的21端口等等。 分类情况 公认端口 从0到1023,它们紧密绑定(binding)于一些服务。通常这些...
  • 最多能创建TCP连接个

    千次阅读 2022-03-04 14:27:10
    系统用一个四元组来唯一标识一个TCP连接:{local ip, local port,remote ip,remote port} 在local ip, local port固定、不考虑ip地址分类的情况下,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也...
  • 大家好,我是飞哥!在 TCP 连接中,客户端在发起连接请求前会先确定一个客户端端口,然后用这个端口去和服务器端进行握手建立连接。那么在 Linux 上,客户端的端口到底是如何被确定下来的呢...
  • 一个套接字只能建立一个连接,无论对于 server 还是 client。 注意报错消息是: [Errno 106] (EISCONN) Transport endpoint is already connected man 2 connect 说得很清楚了: Generally, co...
  • linux 查看tcp 端口连接IP

    千次阅读 2021-01-05 23:01:03
    netstat -nat|grep ":80"|awk '{print $5}' |awk -F: '{print $1}' | sort| uniq -c|sort -n
  • 使用Nginx实现端口转发TCP代理

    千次阅读 2021-12-23 19:11:02
    最近同事在测试和生产环境中分别部署了一套应用,由于应用只能集成LDAP,而我们公司使用的是AD,于是我搭建了一个OpenLDAP服务,账号先通过lsc从AD同步到OpenLDAP,然后使用saslauthd传递身份验证到AD。在测试环境中...
  • Netty 同一个端口支持Tcp和 websocket

    千次阅读 2018-09-29 11:44:30
    在Netty 实战群里讨论了下能否一个端口支持tcp和websocket . 既然websocket是从http升级到websocket的 Netty能判断http的话 理论上能判断出是http的话,那就应该可以的。服务器监听端口,在最开始添加一个decode ...
  • 想知道一个在互联网中的服务器都开放了哪些端口吗?通过本文你可以知道如何用NetCat命令行工具进行最基础的端口扫描。
  • TCP端口和UDP端口有什么区别

    千次阅读 2021-11-25 19:17:47
    当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。 2)UDP端口---用户数据报协议,是...
  • windows查看指定TCP端口连接

    万次阅读 2018-07-26 08:35:02
    netstat -an -p tcp | find "X.X.X.X:PORT" | find "ESTABLISHED" /c
  • TCP连接的建立与关闭

    千次阅读 2022-03-16 17:12:26
    TCP连接建立 首先要说明的是要明确TCP连接建立的过程需要3次握手,下面举例说明各种状态存在的时刻: 1.首先在服务器A上开启FTP服务,开始侦听来自远端TCP端口的连接请求,这时候查看服务器A的对应端口状态为:...
  • 8080端口建立TCP连接。。。本地机器和远程机器的8080都要开放吗?。。。谢谢!!!!!
  • TCP server 为什么一个端口可以建立多个连接?我一直对这个问题有个疑问,今天看到一个论坛里面的讨论,看到了一些回答,解决了我的疑惑,并且我搜索了一些其他资料,记录在这里。 TCP server 可以,TCP client 也...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 619,061
精华内容 247,624
热门标签
关键字:

一个端口多少tcp连接