注:机翻,未校。
Difference Between SO_REUSEADDR and SO_REUSEPORT
Last Updated : 05 Feb, 2023
Processes use sockets as endpoints of a two-way channel to transfer data. The socket options SO_REUSEADDR and SO_REUSEPORT have different man pages and programmer documentation for various operating systems, which can be very confusing. The option SO_REUSEPORT is not even available on some operating systems. To perform socket operations, such as connecting to a socket address or listening for a new connection, processes use a variety of socket-layer functions.
进程使用套接字作为双向通道的端点来传输数据。套接字选项 SO_REUSEADDR 和 SO_REUSEPORT 具有不同的手册页和适用于各种操作系统的程序员文档,这可能非常令人困惑。选项 SO_REUSEPORT 在某些操作系统上甚至不可用。为了执行套接字操作,例如连接到套接字地址或侦听新连接,进程使用各种套接字层函数。
Understanding Socket Implementation 了解套接字实现
Pipes and sockets are comparable. To the programs that use them, both appear to be filed. Both aid in the communication between processes. Sockets interact with a remote program; pipes interact with a local program. As you mentioned, sockets also provide bidirectional communication (much like a pair of properly connected pipes could). The values of a socket are the protocol, source IP address, source port, destination IP address, and port. A socket is a five-tuple. In order to maintain a connection between the two endpoints, no two sockets can have the same five values. A call to the socket() function is used to initially create a socket. A socket’s unique identifier, or socket descriptor, is what this function returns. We can give the socket a source IP address and a source port with the bind() function. The connect() function sets the destination IP address and destination port. At last, it is normal for programs on a solitary machine to convey utilizing standard organization conventions, like TCP; it would be inefficient to go the whole way to the organization equipment (if any!) and compute checksums.
管道和套接字具有可比性。对于使用它们的程序来说,两者似乎都已归档。两者都有助于进程之间的通信。套接字与远程程序交互;管道与本地程序交互。正如提到的,套接字还提供双向通信(就像一对正确连接的管道一样)。套接字的值包括协议、源 IP 地址、源端口、目标 IP 地址和端口。套接字是一个五元组。为了保持两个端点之间的连接,任何两个套接字都不能具有相同的五个值。对 socket() 函数的调用用于初始创建套接字。套接字的唯一标识符或套接字描述符是此函数返回的内容。可以使用 bind() 函数为套接字提供源 IP 地址和源端口。connect() 函数设置目标 IP 地址和目标端口。最后,独立机器上的程序使用标准组织约定(如 TCP)进行传输是正常的;全程到组织设备(如果有的话!)并计算校验和是低效的。
The protocol, source IP address, source port, destination IP address, and port are the values of a socket, which is a five-tuple. In order to maintain a connection between the two endpoints, no two sockets can have the same five values. Connecting to www.geeksforgeeks.org in our web browser will allow us to verify this.
协议、源 IP 地址、源端口、目标 IP 地址和端口是套接字的值,套接字是一个五元组。为了保持两个端点之间的连接,任何两个套接字都不能具有相同的五个值。在网络浏览器中连接到 www.geeksforgeeks.org 将能够验证这一点。
nslookup www.geeksforgeeks.org
A collection of IPV4 and IPV6 addresses are retrieved. Then, when we use the ss command, we’ll search for any of these IP addresses. This command aids in our ongoing investigation of the socket:
检索 IPV4 和 IPV6 地址的集合。然后,当使用 ss 命令时,将搜索这些 IP 地址中的任何一个。此命令有助于对套接字的持续调查:
ss -t
We can see that the socket we have has a working connection. The source IP address in this instance is 180.149.59.201, and the source port is 52984. Additionally, the destination IP address (180.149.59.203) and port (192 for HTTPS) are both those of the youtube server.
可以看到拥有的套接字有一个工作连接。本实例中的源 IP 地址为 180.149.59.201,源端口为 52984。此外,目标 IP 地址 (180.149.59.203) 和端口(HTTPS 为 192)都是 youtube 服务器的地址。
What Are Socket Options? 什么是套接字选项?
A network socket’s identifier is a socket file descriptor. It is essential to keep in mind that while all file descriptors are sockets, not all sockets are file descriptors. This is due to the fact that file descriptors can serve as identifiers for pipes, sockets, and files. The option name, such as SO_BROADCAST, indicates that the property is set. There are various options for various protocol levels. The protocol level is a necessary parameter because of this. When examining the socket level, the protocol level to use is SOL SOCKET. By looking at the option name’s prefix, we can tell the options for a level apart. For example, we can perceive that SO_DEBUG is on the attachment level just from the initial two letters of the chchosenoice name. IP_DONTFRAG operates at the IP protocol level, whereas TCP_NODELAY operates at the TCP protocol level. Socket management must be possible for processes. For instance, a process might need to enable the recording of debugging information or broadcast messages. The values of SO BROADCAST and SO DEBUG would change in this situation. This is accomplished by a process using the setsockopt() function. The setsockopt function needs the following five inputs: Name of the socket, file descriptor, Protocol level, value, and length.
网络套接字的标识符是套接字文件描述符。必须记住,虽然所有文件描述符都是套接字,但并非所有套接字都是文件描述符。这是因为文件描述符可以用作管道、套接字和文件的标识符。选项名称(如 SO_BROADCAST)表示已设置属性。对于不同的协议级别,有各种选项。因此,协议级别是一个必要的参数。检查套接字级别时,要使用的协议级别是 SOL SOCKET。通过查看选项名称的前缀,可以分辨出不同级别的选项。例如,可以从 chchosenoice 名称的前两个字母中感知到 SO_DEBUG 处于依恋级别。IP_DONTFRAG 在 IP 协议级别运行,而 TCP_NODELAY 在 TCP 协议级别运行。对于进程,套接字管理必须是可行的。例如,进程可能需要启用调试信息的记录或广播消息。在这种情况下,SO BROADCAST 和 SO DEBUG 的值将更改。这是通过使用 setsockopt() 函数的进程实现的。setsockopt 函数需要以下五个输入:套接字名称、文件描述符、协议级别、值和长度。
What Is SO_REUSEADDR? 什么是 SO_REUSEADDR?
Local addresses and ports can be reused with the SO_REUSEADDR socket option. Your server can bind to an address that is in the TIME-WAIT state using SO REUSEADDR. It prevents multiple servers from binding to the same address. The fact that another server can bind to the same port by binding to a specific address rather than INADDR_ANY poses a security risk when this flag is used. Starting with the Linux kernel version 2.4 and later, SO REUSEADDR is used. Different operating systems have different ways of implementing this socket option.
本地地址和端口可以通过 SO_REUSEADDR 套接字选项重复使用。服务器可以使用 SO REUSEADDR 绑定到处于 TIME-WAIT 状态的地址。它可以防止多个服务器绑定到同一地址。使用此标志时,另一台服务器可以通过绑定到特定地址而不是 INADDR_ANY 地址来绑定到同一端口,这一事实会带来安全风险。从 Linux 内核版本 2.4 及更高版本开始,将使用 SO REUSEADDR。不同的操作系统有不同的方式来实现此套接字选项。
The same address/port combination will be used every time the process stops and starts over. We would need to explicitly request this behavior by activating the SO_REUSEADDR socket option with setsockopt() in order for this to take place. Before calling the bind() function, the setsockopt() function needs to be called. Additionally, the restarted process will fail if the SO_REUSEADDR socket option is not enabled. The way wildcard addresses are handled changes when the SO REUSEADDR socket option is set. Because more than one socket needs to bind to the UDP port, SO_REUSEADDR is required. This guarantees that the source IP will send a message to each socket that is connected to the UDP port.
每次进程停止和重新开始时,都将使用相同的地址 / 端口组合。需要通过使用 setsockopt() 激活 SO_REUSEADDR 套接字选项来明确请求此行为,以便发生此操作。在调用 bind() 函数之前,需要调用 setsockopt() 函数。此外,如果未启用 SO_REUSEADDR 套接字选项,重新启动的过程将失败。设置 SO REUSEADDR 套接字选项时,通配符地址的处理方式会发生变化。 由于需要将多个套接字绑定到 UDP 端口,因此需要 SO_REUSEADDR 套接字。这保证了源 IP 将向连接到 UDP 端口的每个套接字发送消息。
Additionally, wildcard addresses can bind to the same port with this socket option. A socket binding to 0.0.0.0:80 and another socket b attempting to bind to 10.1.0.1:40 will fail without SO_REUSEADDR. Since 0.0.0.0 also includes 10.1.0.1, there would be a conflict because it lists all possible local addresses. This is interpreted by the kernel as a pair of sockets sharing the same local address and port. The way wildcard addresses are handled changes when the SO REUSEADDR socket option is set. There won’t be a conflict between a socket bound to 0.0.0.0:40 and a socket bound to 10.1.0.1:40 if SO REUSEADDR is enabled. This is due to the fact that the IP address 0.0.0.0:40 is treated as a wildcard address and isn’t the same as the precise local address of 10.1.0.1.
此外,通配符地址可以使用此套接字选项绑定到同一端口。绑定到 0.0.0.0:80 的套接字和尝试绑定到 10.1.0.1:40 的另一个套接字 b 将失败,且不 SO_REUSEADDR。由于 0.0.0.0 还包括 10.1.0.1,因此会存在冲突,因为它列出了所有可能的本地地址。这被内核解释为一对共享相同本地地址和端口的套接字。设置 SO REUSEADDR 套接字选项时,通配符地址的处理方式会发生变化。如果启用了 SO REUSEADDR,则绑定到 0.0.0.0:40 的套接字和绑定到 10.1.0.1:40 的套接字之间不会发生冲突。这是因为 IP 地址 0.0.0.0:40 被视为通配符地址,与精确的本地地址 10.1.0.1 不同。
What Is SO_REUSEPORT? 什么是 SO_REUSEPORT?
Multiple sockets can bind to the same address and port combination when SO REUSEPORT is enabled, just like SO REUSEADDR does. If all of the processes use the SO_REUSEPORT option, the SO_REUSEPORT flag allows them to bind to the same address. The rule stipulates that the socket option SO_REUSEPORT must be enabled for each socket binding to the address and port. Before binding to a specific local IP and port combination, for instance, no socket can bind to socket A if SO_REUSEPORT is not enabled.
当启用 SO REUSEPORT 时,多个套接字可以绑定到相同的地址和端口组合,就像 SO REUSEADDR 一样。如果所有进程都使用 SO_REUSEPORT 选项,则 SO_REUSEPORT 标志允许它们绑定到同一地址。该规则规定,必须为绑定到地址和端口的每个套接字启用套接字选项 SO_REUSEPORT。例如,在绑定到特定的本地 IP 和端口组合之前,如果未启用 SO_REUSEPORT 套接字,则任何套接字都无法绑定到套接字 A。
As previously mentioned, a socket enters the synchronized state known as TIME WAIT when it closes. Unless both sockets have the SO REUSEPORT option, another socket won’t be able to use the IP address and port combination of the socket in the TIME-WAIT state. The SO_REUSEPORT socket option behaves similarly to SO_REUSEADDR when it comes to multicasting. The user’s restriction is what differentiates SO_REUSEPORT from the others. With SO_REUSEPORT, one compelling userID ought to achieve all attachments that share a similar IP and port. In point of fact, this holds true for both TCP and UDP.
如前所述,套接字在关闭时会进入称为 TIME WAIT 的同步状态。除非两个套接字都有 SO REUSEPORT 选项,否则另一个套接字将无法在 TIME-WAIT 状态下使用套接字的 IP 地址和端口组合。SO_REUSEPORT 套接字选项的行为类似于 SO_REUSEADDR 在组播方面。用户的限制是 SO_REUSEPORT 与其他人的区别。借助 SO_REUSEPORT,一个引人注目的 userID 应该实现共享相似 IP 和端口的所有附件。事实上,TCP 和 UDP 都是如此。
Difference Between SO_REUSEADDR and SO_REUSEPORT
SO_REUSEADDR | SO_REUSEPORT |
---|---|
Local addresses and ports may be reused with the SO REUSEADDR socket option. | Multiple sockets may bind to the same address and port combination with SO REUSEPORT enabled. |
Starting with Linux kernel version 2.4 and up, SO_REUSEADDR can be used. | This socket option was only implemented in Linux kernel version 3.9, making it relatively recent. |
Different operating systems have different ways of implementing this socket option. | Different operating systems have the same ways of implementing this socket option. |
In a multicast, packets are sent in a group communication to multiple destination IPs at once. | The SO REUSEPORT socket option functions similarly to SO REUSEADDR when multicasting. |
Using setsockopt(), set the SO_REUSEADDR socket option. | unless both sockets have the SO_REUSEPORT option, the TIME_WAIT state. |
this holds true for only UDP. | this holds true for both TCP and UDP. |
For example, SO REUSEADDR doesn’t examine whether any of the different sockets tying to the IP/port combination have a particular socket option set. | For example, Before binding to a particular local IP and port combination, no socket can do so if socket A does not have SO_REUSEPORT enabled. |
via:
-
Difference Between SO_REUSEADDR and SO_REUSEPORT - GeeksforGeeks
https://www.geeksforgeeks.org/difference-between-so_reuseaddr-and-so_reuseport/
Linux TCP SO_REUSEPORT — 使用和实现
Linux TCP SO_REUSEPORT — Usage and implementation
Krishna Kumar
Aug 19, 2019
Improve your server performance using a relatively new feature of the Linux networking stack — the SO_REUSEPORT socket option.
使用 Linux 网络堆栈的一个相对较新的功能(SO_REUSEPORT套接字选项)提高服务器性能。
Figure 1: Server on top uses parallel listeners to avoid bottlenecks, while the server at the bottom uses a single listener to accept incoming connection.
Summary 总结
HAProxy and NGINX are some of the few applications that use the TCP’s SO_REUSEPORT socket option [1] of the Linux networking stack. This option, initially introduced in 4.4 BSD, is used to implement high performance servers that help better utilize today’s large multicore systems. The first few sections of this article explains some essential concepts of TCP/IP sockets, and the remaining sections uses that knowledge to describe the rationale, usage and implementation of the SO_REUSEPORT socket option.
HAProxy 和 NGINX 是少数几个使用 Linux 网络堆栈的 TCP SO_REUSEPORT套接字选项 [1] 的应用程序。此选项最初在 4.4 BSD 中引入,用于实现高性能服务器,以帮助更好地利用当今的大型多核系统。本文的前几节介绍了 TCP/IP 套接字的一些基本概念,其余几节使用这些知识来描述 SO_REUSEPORT 套接字选项的基本原理、用法和实现。
Problem statement 问题陈述
The conventional method that a high performance server employs, when running on a multiprocessor system, is to have a single listener process that accepts incoming connections and passes these connections to worker processes for processing. However under heavy connection load, the listening process becomes a bottleneck. The other method often used by servers is to open a single listening socket, and fork multiple processes each of which invokes accept() to handle incoming connections on that socket, while performing the work themselves. The problem with this approach is that the process that starts picking up connections tends to get a high skew of connections. We discuss an alternate third approach in this article — opening multiple listen sockets to process incoming connections using SO_REUSEPORT, which solves both the problem of a single process bottleneck, as well as connection skew between processes.
在多处理器系统上运行时,高性能服务器采用的传统方法是具有单个侦听器进程,该进程接受传入连接并将这些连接传递给工作进程进行处理。然而,在重连接负载下,监听过程成为瓶颈。服务器经常使用的另一种方法是打开单个侦听套接字,并分叉多个进程,每个进程都调用 accept() 来处理该套接字上的传入连接,同时自己执行工作。这种方法的问题在于,开始获取连接的过程往往会获得高偏斜的连接。在本文中,我们将讨论另一种替代的第三种方法 — 打开多个侦听套接字以使用 SO_REUSEPORT 处理传入连接,这既解决了单个进程瓶颈的问题,也解决了进程之间的连接倾斜问题。
TCP connection basics
TCP 连接基础知识
A TCP connection is defined by a unique 5-tuple [2]:
TCP 连接由唯一的 5 元组 [2] 定义:
[ Protocol, Source IP address, Source Port, Destination IP address, Destination Port ]
Individual tuple elements are specified in different ways by clients and servers. Let’s understand how each tuple element is initialized by applications.
客户端和服务器以不同的方式指定各个元组元素。让我们了解应用程序如何初始化每个元组元素。
Client application 客户端应用程序
- Protocol: This field is initialized when the socket is created based on parameters provided by the application. Protocol is always TCP for purposes of this article. For example,
socket(AF_INET, SOCK_STREAM, 0); /* create a TCP socket /
协议:根据应用程序提供的参数创建套接字时,将初始化此字段。就本文而言,协议始终是 TCP。例如 socket(AF_INET, SOCK_STREAM, 0);/ 创建 TCP 套接字 */ - Source IP address and Port: These are usually set by the kernel when the application calls connect() without a prior invocation to bind(). The kernel picks a suitable IP address for communicating with the destination server, and a source port from the ephemeral port range (sysctl net.ipv4.ip_local_port_range).
源 IP 地址和端口:这些通常是在应用程序调用 connect() 而没有事先调用 bind() 时由内核设置的。内核选择一个合适的 IP 地址与目标服务器通信,并从临时端口范围 (sysctl net.ipv4.ip_local_port_range) 中选择一个源端口。 - Destination IP address and Port: These are set by the application by invoking connect(). For example:
目标 IP 地址和端口:这些是应用程序通过调用 connect() 来设置的。例如:
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);
bcopy(server_ent->h_addr, &server.sin_addr.s_addr, server_ent->h_length);
/* Connect to server, and set the socket's destination IP address and port#
* based on above parameters. Also, request the kernel to automatically set
* the Source IP and port# if the application did not call bind() prior to connect().
*/
connect(fd, (struct sockaddr *)&server, sizeof server);
Server application 服务器应用程序
- Protocol: Initialized in the same way as described for a client application.
协议:以与客户端应用程序相同的方式进行初始化。 - Source IP address and Port: Set by the application when it invokes bind(), for example:
源 IP 地址和端口:由应用程序在调用 bind() 时设置,例如:
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = INADDR_ANY;
srv_addr.sin_port = htons(SERVER_PORT);
bind(fd, &srv_addr, sizeof srv_addr);
Destination IP address and Port: A client connects to a server by completing the TCP 3-way handshake [3]. The server’s TCP/IP stack creates a new socket to track the client connection, and sets it’s Source IP:Port and Destination IP:Port from the incoming client connection parameters. The new socket is transitioned to the ESTABLISHED state while the server’s LISTEN socket is left unmodified. At this time, the server application’s call to accept() on the LISTEN socket returns with a reference to the newly ESTABLISHED socket. See the source code listing at the end of this article for an example implementation of client and server applications.
目标 IP 地址和端口:客户端通过完成 TCP 3 向握手 [3] 连接到服务器。服务器的 TCP/IP 堆栈创建一个新套接字来跟踪客户端连接,并从传入的客户端连接参数中设置其源 IP:Port 和目标 IP:Port。新套接字将转换为 ESTABLISHED 状态,而服务器的 LISTEN 套接字保持不变。此时,服务器应用程序在 LISTEN 套接字上对 accept() 的调用将返回对新 ESTABLISHED 套接字的引用。有关客户端和服务器应用程序的示例实现,请参阅本文末尾的源代码列表。
TIME-WAIT sockets TIME-WAIT 套接字
A TIME-WAIT [4] socket is created when an application closes it’s end of a TCP connection first. This results in the initiation of the TCP 4-way handshake, during which the socket state changes from ESTABLISHED to FIN-WAIT1 to FIN-WAIT2 to TIME-WAIT, before the socket is closed. The TIME-WAIT state is a lingering state for protocol reasons. An application can instruct the TCP/IP stack to not linger a connection by sending a TCP RST packet. In doing so, the connection gets instantly terminated without going through the TCP 4-way handshake. The following code fragment implements the reset of a connection by specifying a socket linger time of zero seconds:
当应用程序首先关闭其 TCP 连接结束时,将创建 time-wait [4] 套接字。这会导致启动 TCP 4 次握手,在此期间,套接字状态从 ESTABLISHED 变为 FIN-WAIT1 再到 FIN-WAIT2 变为 TIME-WAIT,然后套接字才会关闭。由于协议原因,TIME-WAIT 状态是一种挥之不去的状态。应用程序可以通过发送 TCP RST 数据包来指示 TCP/IP 堆栈不要在连接中停留。这样一来,连接就会立即终止,而无需经过 TCP 4 次握手。以下代码片段通过指定零秒的套接字停留时间来实现连接的重置:
const struct linger opt = { .l_onoff = 1, .l_linger = 0 };
setsockopt(fd, SOL_SOCKET, SO_LINGER, &opt, sizeof opt);
close(fd);
Understanding different states of a server socket
了解服务器套接字的不同状态
A server typically executes the following system calls at start up:
服务器通常在启动时执行以下系统调用:
1. Create a socket:
server_fd = socket(...);
2. Bind to a well known IP address and port#:
ret = bind(server_fd, ...);
3. Mark the socket as passive by changing it's state to LISTEN:
ret = listen(server_fd, ...);
4. Wait for a client to connect, and get a reference file descriptor:
client_fd = accept(server_fd, ...);
Any new socket, created via socket() or accept() system calls, is tracked in the kernel using a “struct sock” structure [5]. In the code fragment above, a socket is created in step #1, and given a well known address in step #2. This socket is transitioned to the LISTEN state in step #3. Step #4 calls accept(), which blocks till a client connects to this IP:port. After the client completes the TCP 3-way handshake, the kernel creates a 2nd socket and returns a reference to this socket. The state of the new socket is set to ESTABLISHED, while the server_fd socket remains in LISTEN state.
任何通过 socket() 或 accept() 系统调用创建的新套接字,都会在内核中使用 “struct sock” 结构 [5] 进行跟踪。在上面的代码片段中,在步骤 #1 中创建一个套接字,并在步骤 #2 中给出一个众所周知的地址。在步骤 #3 中,此套接字转换为 LISTEN 状态。步骤 #4 调用 accept(),它会阻塞直到客户端连接到此 IP:端口。客户端完成 TCP 3 次握手后,内核会创建第二个套接字并返回对此套接字的引用。新套接字的状态设置为 ESTABLISHED,而server_fd套接字保持 LISTEN 状态。
SO_REUSEADDR socket option
SO_REUSEADDR socket 选项
The SO_REUSEADDR option for TCP sockets can be better understood from the following two use cases:
从以下两个用例中可以更好地理解 TCP 套接字的SO_REUSEADDR选项:
Use case #1. A server application restarts in two steps — an exit followed by start up. During exit, the server’s LISTEN socket is closed immediately. Let’s take a look at two situations that can arise due to the presence of existing connections to the server.
用例#1。服务器应用程序通过两个步骤重新启动 — 退出,然后启动。在退出过程中,服务器的 LISTEN 套接字将立即关闭。让我们看一下由于存在与服务器的现有连接而可能出现的两种情况。
- All established connections that were being handled by this dying server process are closed, and those sockets transitions to the TIME-WAIT state.
这个垂死的服务器进程正在处理的所有已建立的连接都将关闭,并且这些套接字将转换为 TIME-WAIT 状态。 - All established connections which were handed off to a child process continue to remain in ESTABLISHED state.
移交给子进程的所有已建立连接将继续保持 ESTABLISHED 状态。
When the server is subsequently started up, it’s attempt to bind to it’s LISTEN port fails with EADDRINUSE because some sockets on the system are already bound to this IP:port combination (for example, a socket in either TIME-WAIT or ESTABLISHED state). A demonstration of this problem is shown below:
当服务器随后启动时,它尝试绑定到其 LISTEN 端口失败,并使用 EADDRINUSE 失败,因为系统上的某些套接字已绑定到此 IP:端口组合(例如,处于 TIME-WAIT 或 ESTABLISHED 状态的套接字)。此问题的演示如下所示:
# Server is listening on port #45000
$ ss -tan | grep :45000
LISTEN 0 1 10.20.1.1:45000 *:*
# A client connects to the server using it's source port 54762. A new
# socket is created and is seen in ESTABLISHED state, along with the
# earlier LISTEN socket.
$ ss -tan | grep :45000
LISTEN 0 1 10.20.1.1:45000 *:*
ESTAB 0 0 10.20.1.1:45000 10.20.1.100:54762
# Kill the server application.
$ pkill -9 my_server
# Restart the server application.
$ ./my_server 45000
bind: Address already in use
# Find out why
$ ss -tan | grep :45000
TIME-WAIT 0 0 10.20.1.1:45000 10.20.1.100:54762
This listing shows that the earlier ESTABLISHED socket is the same one that is now seen in the TIME-WAIT state. The presence of this socket bound to the local address — 10.20.1.1:45000 — prevented the server from being able to subsequently bind() to the same IP:port combination for it’s LISTEN socket.
此列表显示,早期的 ESTABLISHED 套接字与现在在 TIME-WAIT 状态中看到的套接字相同。此套接字绑定到本地地址(10.20.1.1:45000)的存在阻止了服务器随后将 bind() 绑定到其 LISTEN 套接字的同一 IP:端口组合。
Use case #2. If two processes attempt to bind() to the same IP:port combination, the process that executes bind() first succeeds while the latter fails with EADDRINUSE. Another instance of this use case involves an application binding to a specific IP:port (for example, 192.168.100.1:80), and another application attempting to bind to the wild card IP address with the same port number (for example, 0.0.0.0:80); or vice versa. The latter bind() invocation fails as it attempts to bind to all addresses with the same port number that was used by the first process. If both processes sets the SO_REUSEADDR option on their sockets, both sockets can be bound successfully. However, note this caveat — if the first process calls bind() and listen(), the second process would still be unable to bind() successfully since the first socket is in LISTEN state. Hence this use case is usually meant for clients that want to bind to a specific IP:port before connecting to different services.
用例#2。如果两个进程尝试将 bind() 绑定到同一 IP:端口组合,则执行 bind() 的进程首先成功,而后者因 EADDRINUSE 而失败。此用例的另一个实例涉及绑定到特定 IP:port 的应用程序(例如,192.168.100.1:80),以及尝试绑定到具有相同端口号的通配符 IP 地址的另一个应用程序(例如,0.0.0.0:80);反之亦然。后一个 bind() 调用失败,因为它尝试绑定到第一个进程使用的相同端口号的所有地址。如果两个进程都在其套接字上设置了 SO_REUSEADDR 选项,则两个套接字都可以成功绑定。但是,请注意以下注意事项 — 如果第一个进程调用 bind() 和 listen(),则第二个进程仍然无法成功绑定(),因为第一个套接字处于 LISTEN 状态。因此,此用例通常适用于希望在连接到不同服务之前绑定到特定 IP:port 的客户端。
How does SO_REUSEADDR help solve this problem? When the server is restarted and invokes bind() on a socket with SO_REUSEADDR set, the kernel ignores all non-LISTEN sockets bound to the same IP:port combination. Richard Stevens, in his Unix Network Programming book [6], describes this feature as: “SO_REUSEADDR allows a listening server to start and bind its well-known port, even if previously established connections exist that use this port as their local port”.
SO_REUSEADDR如何帮助解决这个问题?当服务器重新启动并在设置了 SO_REUSEADDR 套接字的套接字上调用 bind() 时,内核会忽略绑定到同一 IP:端口组合的所有非 LISTEN 套接字。理查德·史蒂文斯(Richard Stevens)在他的《Unix网络编程》一书[6]中将这一特性描述为:“SO_REUSEADDR允许监听服务器启动并绑定其众所周知的端口,即使先前建立的连接存在使用此端口作为其本地端口的连接”。
However we need the SO_REUSEPORT option to allow two or more processes to successfully invoke listen() on the same port. This option is described in more detail in the remaining sections.
但是,我们需要 SO_REUSEPORT 选项来允许两个或多个进程在同一端口上成功调用 listen()。此选项将在其余部分中更详细地介绍。
SO_REUSEPORT socket option
SO_REUSEPORT socket 选项
While SO_REUSEADDR allows sockets to bind() to the same IP:port combination when existing ESTABLISHED or TIME-WAIT sockets may be present, SO_REUSEPORT allows binding to the same IP:port when existing LISTEN sockets may also be present. The kernel ignores all sockets, including sockets in LISTEN state, when an application invokes bind() or listen() on a socket with SO_REUSEPORT enabled. This permits a server process to be invoked multiple times, allowing many processes to listen for connections. The next section examines the kernel implementation SO_REUSEPORT.
当现有的 ESTABLISHED 或 TIME-WAIT 套接字可能存在时,SO_REUSEADDR允许套接字将 bind() 绑定到相同的 IP:port 组合,而当现有的 LISTEN 套接字也可能存在时,SO_REUSEPORT允许绑定到同一 IP:端口。当应用程序在启用了 SO_REUSEPORT 的套接字上调用 bind() 或 listen() 时,内核会忽略所有套接字,包括处于 LISTEN 状态的套接字。这允许多次调用服务器进程,从而允许许多进程侦听连接。下一节将介绍内核实现SO_REUSEPORT。
How are connections distributed among multiple listeners?
连接如何在多个侦听器之间分布?
When multiple sockets are in LISTEN state, how does the kernel decide which socket — and thus which application process — receives an incoming connection? Is this determined using a round-robin, least-connection, random, or some other method? Let’s take a deeper look into the TCP/IP code to understand how socket selection is performed.
当多个套接字处于 LISTEN 状态时,内核如何确定哪个套接字(从而确定哪个应用程序进程)接收传入连接?这是使用循环、最小连接、随机还是其他方法确定的?让我们更深入地了解 TCP/IP 代码,以了解如何执行套接字选择。
Notes: :
- Data structures and code snippets in this section are heavily simplified for the sake of clarity — removing some structure elements, function arguments, variables and unnecessary code — but without losing correctness. Some part of the listing is also in pseudo-code for better ease of understanding.
为了清晰起见,本节中的数据结构和代码片段进行了大量简化 - 删除了一些结构元素、函数参数、变量和不必要的代码 - 但不会失去正确性。为了便于理解,列表的某些部分也是伪代码。 - sk represents a kernel socket data structure of type “struct sock”.
sk 表示“struct sock”类型的内核套接字数据结构。 - skb, or the socket buffer, represents a network packet of type “struct sk_buff”.
SKB 或套接字缓冲区表示“struct sk_buff”类型的网络数据包。 - src_addr, src_port and dst_addr, dst_port refers to source IP:port and destination IP:port respectively.
src_addr、src_port和dst_addr,dst_port分别指源IP:端口和目的IP:端口。 - Readers can correlate the code snippets with the actual source code [5], if desired.
如果需要,读者可以将代码片段与实际源代码 [5] 相关联。
As an incoming packet, skb, moves up the TCP/IP stack, the IP subsystem calls into the TCP packet receive handler, tcp_v4_rcv(), providing skb as argument. tcp_v4_rcv() attempts to locate a socket associated with this skb:
当传入数据包 skb 在 TCP/IP 堆栈中向上移动时,IP 子系统将调用 TCP 数据包接收处理程序 tcp_v4_rcv(),并提供 skb 作为参数。tcp_v4_rcv() 尝试找到与此 SKB 关联的套接字:
sk = __inet_lookup_skb(&tcp_hashinfo, skb, src_port, dst_port);
tcp_hashinfo is a global variable of type “struct inet_hashinfo”, containing, among others, two hash tables of ESTABLISHED and LISTEN sockets respectively. The LISTEN hash table is sized to 32 buckets, as seen below:
tcp_hashinfo 是一个类型的全局变量 “struct inet_hashinfo”,其中包含两个哈希表,分别是 ESTABLISHED 和 LISTEN 套接字。LISTEN 哈希表的大小为 32 个存储桶,如下所示:
#define LHTABLE_SIZE 32 /* Yes, really, this is all you need */
struct inet_hashinfo {
/* Hash table for fully established sockets */
struct inet_ehash_bucket *ehash;
/* Hash table for LISTEN sockets */
struct inet_listen_hashbucket listening_hash[LHTABLE_SIZE];
};
struct inet_hashinfo tcp_hashinfo;
__inet_lookup_skb() extracts the source and destination IP addresses from the incoming skb, and passes these along with the source and destination ports to __inet_lookup() to find the associated ESTABLISHED or LISTEN socket, as shown below:
__inet_lookup_skb() 从传入的 skb 中提取源 IP 地址和目标 IP 地址,并将这些地址与源端口和目标端口一起传递给 __inet_lookup() 以查找关联的 ESTABLISHED 或 LISTEN 套接字,如下所示:
struct sock *__inet_lookup_skb(tcp_hashinfo, skb, src_port, dst_port)
{
/* Get the IPv4 header to know the source and destination IP's */
const struct iphdr *iph = ip_hdr(skb);
/*
* Look up the incoming skb in tcp_hashinfo using the
* [ Source-IP:Port, Destination-IP:Port ] tuple.
*/
return __inet_lookup(tcp_hashinfo, skb, iph->saddr, src_port, iph->daddr, dst_port);
}
__inet_lookup() looks in tcp_hashinfo->ehash hash-table for an already established socket matching the client 4-tuple parameters. In the absence of an established socket, it looks in tcp_hashinfo->listening_hash hash-table for a LISTEN socket.
__inet_lookup() 在 tcp_hashinfo->ehash 哈希表中查找已建立的套接字,该套接字与客户端 4 元组参数匹配。在没有已建立的套接字的情况下,它会在tcp_hashinfo>listening_hash的哈希表中查找 LISTEN 套接字。
struct sock *__inet_lookup(tcp_hashinfo, skb, src_addr, src_port, dst_addr, dst_port)
{
/* Convert dest_port# from network to host byte order */
u16 hnum = ntohs(dst_port);
/* First look for an established socket ... */
sk = __inet_lookup_established(tcp_hashinfo, src_addr, src_port, dst_addr, hnum);
if (sk)
return sk;
/* failing which, look for a LISTEN socket */
return __inet_lookup_listener(tcp_hashinfo, skb, src_addr, src_port, dst_addr, hnum);
}
The __inet_lookup_listener() function implements the selection of a LISTEN socket:
__inet_lookup_listener() 函数实现了 LISTEN 套接字的选择:
struct sock *__inet_lookup_listener(tcp_hashinfo, skb, src_addr, src_port, dst_addr, dst_port)
{
/*
* Use the destination port# to calculate a hash table slot# of the listen socket.
* inet_lhashfn() returns a number between 0 and INET_LHTABLE_SIZE-1 (both
* inclusive).
*/
unsigned int hash = inet_lhashfn(dst_port);
/* Use this hash slot# to index the global LISTEN hash table */
struct inet_listen_hashbucket *ilb = tcp_hashinfo->listening_hash[hash];
/* Keep track of the best matching LISTEN socket thus far, and it’s “score” */
struct sock *result = NULL, *sk;
int hi_score = 0;
for each socket, ‘sk’, in the selected hash bucket, ‘ilb’ {
/*
* Calculate the “score” of this LISTEN socket (sk) against the incoming skb.
* Score is computed on some parameters, such as, exact destination port#,
* destination IP address closest match (as against matching INADDR_ANY,
* for example), with each criteria getting a different weight.
*/
score = compute_score(sk, dst_port, dst_addr);
if (score > hi_score) {
/* Highest score - best matched socket till now */
if (sk->sk_reuseport) {
/*
* sk has SO_REUSEPORT feature enabled. Call inet_ehashfn()
* with dest_addr, dest_port, src_addr and src_port to compute a
* 2nd hash - phash.
*/
phash = inet_ehashfn(dst_addr, dst_port, src_addr, src_port);
/* Select socket from sk’s SO_REUSEPORT group using phash.*/
result = reuseport_select_sock(sk, phash);
if (result)
return result;
}
/* Update new best socket and it’s score */
result = sk;
hi_score = score;
}
}
return result;
}
Selecting a socket from the SO_REUSEPORT group is done by reuseport_select_sock():
从SO_REUSEPORT组中选择一个套接字是通过 reuseport_select_sock() 完成的:
struct sock *reuseport_select_sock(struct sock *sk, unsigned int phash)
{
/* Get control block of sockets in this SO_REUSEPORT group */
struct sock_reuseport *reuse = sk->sk_reuseport_cb;
/* Get count of sockets in the group */
int num_socks = reuse->num_socks;
/* Calculate value between 0 and 'num_socks-1' (both inclusive) */
unsigned int index = reciprocal_scale(phash, num_socks);
/* Index into the SO_REUSEPORT group using this index, and return that socket */
return reuse->socks[index];
}
We need to step back a little to understand how this works. When the first process invoked listen() on a socket with SO_REUSEPORT enabled, a pointer in it’s “struct sock” structure — sk_reuseport_cb, is allocated. This structure is defined as:
我们需要退后一步来理解它是如何工作的。当第一个进程在启用了 SO_REUSEPORT 的套接字上调用 listen() 时,会在其“struct sock”结构 — sk_reuseport_cb 中分配一个指针。此结构定义为:
struct sock_reuseport {
u16 max_socks; /* Allocated size of socks[] array */
u16 num_socks; /* #Elements in socks[] */
struct sock *socks[0]; /* All sockets added to this group */
};
The last element of this structure is a “flexible array member” [7]. The entire structure is allocated such that the socks[] array has 128 elements of type “*struct sock **”. Note that as the number of listeners increase beyond 128, this structure is reallocated such that the socks[] array size is doubled.
这个结构的最后一个元素是“灵活的数组成员”[7]。整个结构的分配使得 socks[] 数组有 128 个类型为“struct sock *”的元素。请注意,当侦听器数量增加到 128 以上时,此结构将重新分配,以便 socks[] 数组大小加倍。
The first socket, sk1, that invoked listen() is cached in the first slot of it’s own socks[] array, for example: sk1->sk_reuseport_cb->socks[0] = sk1;
调用 listen() 的第一个套接字 sk1 缓存在其自己的 socks[] 数组的第一个插槽中,例如:sk1->sk_reuseport_cb->socks[0] = sk1;
When listen() is subsequently invoked on other sockets (sk2, …) bound to the same IP:port, two operations are performed:
当随后在绑定到同一 IP:port 的其他套接字(sk2 等)上调用 listen() 时,将执行两个操作:
- The address of the new socket (sk2, …) is appended to the sk_reuseport_cb->socks[] of the first socket (sk1).
新套接字 (sk2, …) 的地址将追加到第一个套接字 (sk1) 的 sk_reuseport_cb->socks[] 中。 - The new socket’s sk_reuseport_cb pointer is made to point to the first socket’s sk_reuseport_cb pointer. This ensures that all LISTEN sockets of the same group reference the same sk_reuseport_cb pointer.
新套接字的sk_reuseport_cb指针指向第一个套接字的sk_reuseport_cb指针。这可确保同一组的所有 LISTEN 套接字都引用相同的 sk_reuseport_cb 指针。
The result of these two steps is seen in Figure 2
这两个步骤的结果如图 2 所示
Figure 2: Representation of SO_REUSEPORT group of LISTEN sockets
In this figure, sk1 is the first LISTEN socket, while sk2 and sk3 are sockets that invoked listen() subsequently. The two steps described above are performed in the following code snippet, and executed via the listen() call chain:
在此图中,sk1 是第一个 LISTEN 套接字,而 sk2 和 sk3 是随后调用 listen() 的套接字。上述两个步骤在以下代码片段中执行,并通过 listen() 调用链执行:
static int inet_reuseport_add_sock(struct sock *new_sk)
{
/* First check if another identical LISTEN socket, prev_sk,
* exists. ... Then do the following:
*/
if (prev_sk) {
/*
* Not the first listener - do the following:
* - Grow prev_sk->sk_reuseport_cb structure if required.
* - Save new_sk socket pointer in prev_sk's socks[].
* - prev_sk->sk_reuseport_cb->socks[num_socks] = new_sk;
* - prev_sk->sk_reuseport_cb->num_socks++;
* - Pointer assignment of the control block:
* new_sk->sk_reuseport_cb = prev_sk->sk_reuseport_cb;
*/
return reuseport_add_sock(new_sk, prev_sk);
}
/*
* First listener - do the following:
* - allocate new_sk->sk_reuseport_cb to contain 128 socks[]
* - new_sk->sk_reuseport_cb->max_socks = 128;
* - new_sk->sk_reuseport_cb->socks[0] = new_sk;
* - new_sk->sk_reuseport_cb->numsocks = 1;
*/
return reuseport_alloc(new_sk);
}
Now let’s understand how reuseport_select_sock() selects a LISTEN socket. reuseport_select_sock() simply indexes into the ‘socks[]’ array via a call to reciprocal_scale() as follows:
现在让我们了解 reuseport_select_sock() 如何选择 LISTEN 套接字。reuseport_select_sock() 只是通过调用 reciprocal_scale() 索引到“socks[]”数组中,如下所示:
unsigned int index = reciprocal_scale(phash, num_socks);
return reuse->socks[index];
reciprocal_scale() [8] is an optimized function that implements a pseudo-modulo operation using multiply and shift operations.
reciprocal_scale() [8] 是一个优化函数,它使用乘法和移位运算实现伪模运算。
As we saw earlier, ‘phash’ was calculated in __inet_lookup_listener(),
正如我们之前看到的,“phash”是在 __inet_lookup_listener() 中计算的,
phash = inet_ehashfn(dst_addr, dst_port, src_addr, src_port);
and ‘num_socks’ is the number of sockets in the socks[] array. The function reciprocal_scale(phash, num_socks) calculates an index, 0 <= index < num_socks. This index is used to retrieve a socket from the SO_REUSEPORT socket group. Hence we see that the kernel selects a socket by hashing the client IP:port and server IP:port values. This method provides a good distribution of connections among different LISTEN sockets.
“num_socks”是 socks[] 数组中的套接字数。函数 reciprocal_scale(phash, num_socks) 计算索引 0 <= 索引 < num_socks。此索引用于从 SO_REUSEPORT 套接字组中检索套接字。因此,我们看到内核通过对客户端 IP:port 和服务器 IP:port 值进行哈希处理来选择套接字。此方法在不同的 LISTEN 套接字之间提供了良好的连接分布。
Seeing SO_REUSEPORT in action
看到 SO_REUSEPORT 在行动
Let’s see the effect of SO_REUSEPORT on the command line through two tests.
让我们通过两个测试来看看 SO_REUSEPORT 对命令行的影响。
- An application opens a socket for listen, and creates two processes. Application code path: socket(); bind(); listen(); fork();
一个应用程序打开一个套接字进行侦听,并创建两个进程。应用程序代码路径:socket();绑定();听();叉子(); - An application creates two processes, and each creates a LISTEN socket after setting SO_REUSEPORT. Application code path: fork(); socket(); setsockopt(SO_REUSEPORT); bind(); listen()
应用程序创建两个进程,每个进程在设置SO_REUSEPORT后创建一个 LISTEN 套接字。应用程序代码路径:fork();socket();setsockopt(SO_REUSEPORT);绑定();听()
Let’s see the socket state without SO_REUSEPORT:
让我们看看没有SO_REUSEPORT套接字的状态:
$ ss -tlnpe | grep :45000
LISTEN 0 128 *:45000 *:* users:(("my_server",pid=3020,fd=3),("my_server",pid=3019,fd=3)) ino:3854904087 sk:37d5a0
The string “ino:3854904087 sk:37d5a0” describes a single kernel socket.
字符串“ino:3854904087 sk:37d5a0”描述了单个内核套接字。
Let’s see the socket state with SO_REUSEPORT:
让我们看看套接字的状态SO_REUSEPORT:
$ ss -tlnpe | grep :45000
LISTEN 0 128 *:45000 *:* users:(("my_server",pid=1975,fd=3)) ino:3854935788 sk:37d59c
LISTEN 0 128 *:45000 *:* users:(("my_server",pid=1974,fd=3)) ino:3854935786 sk:37d59d
Now we see two different kernel sockets — notice the different inode numbers.
现在我们看到两个不同的内核套接字 — 请注意不同的 inode 编号。
Applications using multiple processes to accept connections on a single LISTEN socket may experience significant performance issues since each process contends for the same socket lock in accept(), as shown in the following simplified pseudo-code:
使用多个进程在单个 LISTEN 套接字上接受连接的应用程序可能会遇到严重的性能问题,因为每个进程都在 accept() 中争用相同的套接字锁,如以下简化的伪代码所示:
struct sock *inet_csk_accept(struct sock *sk)
{
struct sock *newsk = NULL; /* client socket */
/* Make sure that this socket is listening, and that it has something pending. */
lock_sock(sk);
if (sk->sk_state == TCP_LISTEN)
if ("there are completed connections waiting to be accepted")
newsk = get_first_connection(sk);
release_sock(sk);
return newsk;
}
Both lock_sock() and release_sock() internally acquires and releases a spinlock embedded in ‘sk’. See Figure 4 later in the article to observe the overhead due to the spinlock contention.
lock_sock() 和 release_sock() 都在内部获取并释放嵌入在 ‘sk’ 中的自旋锁。请参阅本文后面的图 4,以观察由于旋转锁争用而产生的开销。
Benchmarking SO_REUSEPORT
基准测试SO_REUSEPORT
The following setup is used to measure SO_REUSEPORT performance:
以下设置用于衡量SO_REUSEPORT性能:
- Kernel version: 4.17.13.
内核版本:4.17.13。 - Client and server systems both have 48 hyper-threaded cores, and are connected to each other using a 40g NIC over a switch.
客户端和服务器系统都有 48 个超线程内核,并通过交换机上的 40g NIC 相互连接。 - Server is started in one of two ways:
服务器以以下两种方式之一启动: - Create a single LISTEN socket and fork 48 times; or
创建单个 LISTEN 套接孔和分叉 48 次;或 - Fork 48 times and each child process creates a LISTEN socket after enabling SO_REUSEPORT.
Fork 48 次,每个子进程在启用 SO_REUSEPORT后创建一个 LISTEN 套接字。 - Client creates 48 processes. Each process connects and disconnects to the server a million times sequentially.
客户端创建 48 个进程。每个进程按顺序与服务器连接和断开一百万次。 - Source code of client and server applications are at the end of this article.
客户端和服务器应用程序的源代码位于本文末尾。
With fork of the LISTEN socket
server-system-$ ./my_server 45000 48 0 # 0 indicates fork() of a LISTEN socket
client-system-$ time ./my_client 45000 48 1000000real 4m45.471s
With SO_REUSEPORT
server-system-$ ./my_server 45000 48 1 # 1 indicates SO_REUSEPORT
client-system-$ time ./my_client 45000 48 1000000real 1m36.766s
Performance analysis of SO_REUSEPORT
SO_REUSEPORT性能分析
Let’s take a look at the performance profile for the above two tests using the perf [9] tool. Figure 3 and 4 shows hardware performance counter statistics and kernel profile for the above test without using SO_REUSEPORT.
让我们看一下使用 perf [9] 工具的上述两个测试的性能配置文件。图 3 和图 4 显示了未使用 SO_REUSEPORT 的情况下进行的上述测试的硬件性能计数器统计信息和内核配置文件。
Figure 3. Performance counter statistics without SO_REUSEPORT
Figure 4. Performance profile of the top 25 functions without SO_REUSEPORT
Figure 5 and 6 shows hardware performance counter statistics and kernel profile for the above test with the use of SO_REUSEPORT.
图 5 和图 6 显示了使用 SO_REUSEPORT 进行上述测试的硬件性能计数器统计信息和内核配置文件。
Figure 5. Performance counter statistics with SO_REUSEPORT
Figure 6. Performance profile of the top 25 functions with SO_REUSEPORT
Source code for client and server applications
客户端和服务器应用程序的源代码
The listing below implements a server and client application that were used for SO_REUSEPORT performance testing.
下面的清单实现了用于SO_REUSEPORT性能测试的服务器和客户端应用程序。
Server program:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <netdb.h>
void create_children(int nprocs, int parent_pid)
{
while (nprocs-- > 0) {
if (getpid() == parent_pid && fork() < 0)
exit(1);
}
}
int main(int argc, char *argv[])
{
int reuse_port, fd, cfd, nprocs, opt = 1, parent_pid = getpid();
struct sockaddr_in server;
if (argc != 4) {
fprintf(stderr, "Port# #Procs {0->fork, or 1->SO_REUSEPORT}\n");
return 1;
}
nprocs = atoi(argv[2]);
reuse_port = atoi(argv[3]);
if (reuse_port) /* proper SO_REUSEPORT */
create_children(nprocs, parent_pid);
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return 1;
}
if (reuse_port)
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, (char *)&opt, sizeof opt);
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(atoi(argv[1]));
if (bind(fd, (struct sockaddr *)&server, sizeof server) < 0) {
perror("bind");
return 1;
}
if (!reuse_port) /* simple fork instead of SO_REUSEPORT */
create_children(nprocs, parent_pid);
if (parent_pid == getpid()) {
while (wait(NULL) != -1); /* wait for all children */
} else {
listen(fd, SOMAXCONN);
while (1) {
if ((cfd = accept(fd, NULL, NULL)) < 0) {
perror("accept");
return 1;
}
close(cfd);
}
}
return 0;
}
Client program::
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <sys/wait.h>
#include <netdb.h>
void create_children(int nprocs, int parent_pid)
{
while (nprocs-- > 0) {
if (getpid() == parent_pid && fork() < 0)
exit(1);
}
}
int main(int argc, char *argv[])
{
int fd, count, nprocs, parent_pid = getpid();
struct sockaddr_in server;
struct hostent *server_ent;
const struct linger nolinger = { .l_onoff = 1, .l_linger = 0 };
if (argc != 5) {
fprintf(stderr, "Server-IP Port# #Processes #Conns_per_Proc\n");
return 1;
}
nprocs = atoi(argv[3]);
count = atoi(argv[4]);
if ((server_ent = gethostbyname(argv[1])) == NULL) {
perror("gethostbyname");
return 1;
}
bzero((char *)&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
bcopy((char *)server_ent->h_addr, (char *)&server.sin_addr.s_addr,
server_ent->h_length);
create_children(nprocs, parent_pid);
if (getpid() == parent_pid) {
/* Parent does nothing other than wait for children */
while (wait(NULL) != -1);
} else {
/* while the children connect() ‘count’ times to the server */
while (count-- > 0) {
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return 1;
}
if (connect(fd, (struct sockaddr *)&server, sizeof server) < 0) {
perror("connect");
return 1;
}
/* Reset connection to avoid TIME-WAIT state */
setsockopt(fd, SOL_SOCKET, SO_LINGER, &nolinger, sizeof nolinger);
close(fd);
}
}
return 0;
}
References
[1] https://lwn.net/Articles/542629/
[2] https://en.wikipedia.org/wiki/Network_socket
[3] https://en.wikipedia.org/wiki/Transmission_Control_Protocol
[4] TCP State Transition diagram: https://en.wikipedia.org/wiki/File:Tcp_state_diagram.png
[5] <ernel source code: https://elixir.bootlin.com/linux/v4.17.13/source>
[6] https://www.amazon.com/Unix-Network-Programming-Sockets-Networking/dp/0131411551/
[7] https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html
[8] http://homepage.cs.uiowa.edu/~jones/bcd/divide.html
[9] https://perf.wiki.kernel.org/index.php/Tutorial
via:
-
Linux TCP SO_REUSEPORT — Usage and implementation | by Krishna Kumar | Flipkart Tech Blog
https://blog.flipkart.tech/linux-tcp-so-reuseport-usage-and-implementation-6bfbf642885a
SO_REUSEADDR 与 SO_REUSEPORT 平台差异性与测试
前些天,与另外一个项目组的同事聊天的时候,谈到他遇到的一个有意思的 BUG。在 window 上启动服务器,然后客户端连接的时候收到一些奇怪的消息,查证了,原来是他自己的另一个工具也在相同的地址上监听,客户端连接到了后面这个工具程序上。我问他,是相同的 IP 和端口?他说是的,因为服务器代码和工具程序都设置了 SO_REUSEADDR 这个 socket 选项,所以可以在同样的地址上监听。
可是,在我的认知里面,SO_REUSEADDR 这个选项并不是说让两个程序在相同地址(相同的 IP 和 端口)上监听,而是说可以让处于 time_wait 状态的 socket 可以快速复用,搜了一下,看到的 这篇文章,也是这么说的:
SO_REUSEADDR allows your server to bind to an address which is in a TIME_WAIT state. It does not allow more than one server to bind to the same address.
看了一下 Linux manual,关于这个选项是这么描述的:
SO_REUSEADDR Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a specific port then it is not possible to bind to this port for any local address. Argument is an integer boolean flag.
manual 并没有提到 time_wait 的事情,但是明确指出,如果一个 socket 处于 listen 状态,那么同样的端口(port)是不能再次被绑定的(binding),不能 binding,自然也不能再次 listen,因此是不可能两个程序在相同的地址(IP PORT)上监听的。
于是自己用 python 在写了一个小的测试程序:
服务端代码:
tcp_server.py
客户端代码:
tcp_client.py
服务端代码设置了 SO_REUSEADDR,在 Linux 下, 确实不能在相同的地址(IP, Port)上监听, 但是在 windows 上,却又是可以的。于是想到,这个选项可能与平台相关。
平台差异性
网上搜了一下,发现了这篇文章《SO_REUSEADDR 和 SO_REUSEPORT 异同》(链接已沉寂),该文章翻译自 stackoverflow 上的这个问答《socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t》,关于 SO_REUSEADDR 和 SO_REUSEPORT 这两个选项在不同平台上的表现介绍得很清楚。
本文记录一下这个问答的要点,并用上面的小程序在各个平台(Linux, Mac, Windows)上进行测试。注意,本文只关注 TCP、单播,事实上原问答还包括 UDP、多播知识,感兴趣的读者可以自行阅读。
第零:一条 tcp 连接是一个五元祖: {<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
第一:SO_REUSEPORT 和 SO_REUSEADDR 在不同的操作系统上行为是不一样的
第二:默认情况下,任意两个 socket 都无法绑定到相同的源 IP 地址和源端口, 0.0.0.0 (即INADDR_ANY)和所有其他地址冲突
第三:BSD 系统下
SO_REUSEADDR 使得 0.0.0.0 与 其他地址不冲突
SO_REUSEPORT 允许你将多个 socket 绑定到相同的地址和端口, 但第一个启动的 socket 必须设置 SO_REUSEPORT
第四:MacOS IOS 表现同 BSD
第五:Linux
SO_REUSEADDR 只要有 socket 处于 listen 状态, 就不能在同样的地址和端口上 listen, 0.0.0.0 与其他所有地址冲突
只要监听前设置了 SO_REUSEPORT(在 Linux3.9 版本之后可用) ,就可以在相同的(ip port) 上监听
对于 SO_REUSEPORT:为了阻止 “port 劫持”(Port hijacking) 有一个特别的限制,所有希望共享源地址和端口的 socket 都必须拥有相同的有效用户 id(effective user ID);对于 TCP 监听 socket,内核尝试将新的客户连接请求(由 accept 返回) 平均的交给共享同一地址和端口的 socket(监听 socket)
第六:Android 同 Linux
第七:Windows
只有 SO_REUSEADDR 选项,没有 SO_REUSEPORT。
设置 SO_REUSEADDR 等价于 BSD 上设定了 SO_REUSEPORT 和 SO_REUSEADDR,而且不管之前的端口是否设定了 SO_REUSEADDR(存疑)
上述选项存在风险:因为允许一个应用程序从别的应用程序上 “偷取” 已连接的端口。因此在 windows 上加入了另一个 socket 选项: SO_EXECLUSIVEADDRUSE。设置了 SO_EXECLUSIVEADDRUSE 的 socket 确保一旦绑定成功,那么被绑定的源端口和地址就只属于这一个 socket,其它的 socket 不能绑定,甚至他们使用了 SO_REUSEADDR 也没用。
测试
在后文涉及到的三个平台(Linux 、MacOS、Windows),都涉及到三个 IP:127.0.0.1,0.0.0.0,10.0.0.x(局域网 IP)。使用的脚本如上(tcp_server.py, tcp_client.py),运行的时候需要简单修改 tcp_server.py 中第 9、10 行的注释,以便测试不同选项下的效果。
MAC
由于没有 BSD 系统,而且前文提到 MacOS 和 BSD 系统的表现是一样的,因此在这里实在 MAC 上测试
在不使用 SO_REUSEADDR (此时未使用 SO_REUSEPORT)时:
注意:first 指第一条监听的 socket,second 指第二条希望在同样的端口(port)上监听的连接。兼容指第二条连接可以成功监听,不兼容则指第二条连接不能成功监听。下同
在使用 SO_REUSEADDR(此时未使用 SO_REUSEPORT)时:
在使用 SO_REUSEADDR 情况下,如果第一个 scoket 在 0.0.0.0 上监听,第二个 scoket 在 127.0.0.1 上监听。那么客户端使用 127.0.0.1 连接的时候会连接到第二个 socket;使用 10.0.0.x 则会连接到第一个 socket
使用 SO_REUSEPORT(同时使用了 SO_REUSEADDR):
如果两个 socket 都在 127.0.0.1 上监听,客户端也通过 127.0.0.1 去连接,那么客户端连接都会发被第二个 socket accept, 笔者并发实验了几十次都是这样, 但并没有找到明确的官方文档说明是否是这样。
Linux
在不使用 SO_REUSEADDR (此时未使用 SO_REUSEPORT)时:
在使用 SO_REUSEADDR(此时未使用 SO_REUSEPORT)时:
从上面两个测试可以看到,在 linux 下,是否使用 SO_REUSEADDR 并不影响两个 socket 的监听
使用 SO_REUSEPORT(同时使用了 SO_REUSEADDR):
如果两个 socket 都在 127.0.0.1 上监听,客户端也通过 127.0.0.1 去连接, 那么客户端连接会被操作系统分发到两个 socket 上,具体如下
客户端并发 10 次连接: for((a=1;a<=10;a++)) ; do(python tcp_client.py 127.0.0.1 &); done
第一个 socket accept 了六次, 第二个 socket accept 了 10 次。
Windows
前面已经提到,windows 下面只有 SO_REUSEADDR 选项,但其功能类似 bsd 系统下的 SO_REUSEADDR 与 SO_REUSEPORT
在不使用 SO_REUSEADDR 时:
比如都在 127.0.0.1 上监听时,第二个 socket 会报错: socket.error: [Errno 10048] 通常每个套接字地址(协议 / 网络地址 / 端口)
使用 SO_REUSEADDR 时:
此时,如果两个 socket 都在 127.0.0.1 上监听,客户端也通过 127.0.0.1 去连接,那么多次实验的结果都是第一个 socket accept。
在上面提到,windows 第一个 socket 可以不使用 SO_REUSEADDR,只要第二个 socket 使用了 SO_REUSEADDR,就可以在相同的地址(IP:PORT)上监听。但是我自己试验了一把,并不成功:socket.error: [Errno 10013]
上面也提到,如果第一个 socket 使用了 SO_EXECLUSIVEADDRUSE 选项,那么第二个连接即使使用了 SO_REUSEADDR 也无济于事,那么是否 SO_EXECLUSIVEADDRUSE 是默认开启的呢?但是在 Python2.7 中,socket 并没有这个属性
查了一下 MSDN,有附图清晰了说明了在 window 下 SO_REUSEADDR 与 SO_EXECLUSIVEADDRUSE 的关系,如下:
但为什么使用 Python 的时候 效果不一样呢,这个就没细究了
总结
本文测试了一下 socket 中 SO_REUSEADDR 与 SO_REUSEPORT 在各个平台下的差异性,一些结论只是实验结果,并没有查到官方权威定论,如果有差错,还请指正!
references
http://www.unixguide.net/network/socketfaq/4.11.shtml
http://man7.org/linux/man-pages/man7/socket.7.html
http://blog.chinaunix.net/uid-28587158-id-4006500.html
https://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t
https://msdn.microsoft.com/en-us/library/windows/desktop/cc150667(v=vs.85).aspx
via:
-
SO_REUSEADDR 与 SO_REUSEPORT 平台差异性与测试 - xybaby - 博客园 posted @ 2017-08-14 10:42
https://www.cnblogs.com/xybaby/p/7341579.html
socket 常见选项之 SO_REUSEADDR,SO_REUSEPORT
SO_REUSEADDR
一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR 是让端口释放后立即就可以被再次使用
SO_REUSEADDR
用于对 TCP 套接字处于 TIME_WAIT 状态下的 socket,才可以重复绑定使用
server 程序总是应该在调用 bind() 之前设置 SO_REUSEADDR 套接字选项
TCP,先调用 close() 的一方会进入 TIME_WAIT 状态
SO_REUSEADDR 提供如下四个功能:
- 允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则 bind 时将出错
- 允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地 IP 地址即可。对于 TCP,我们根本不可能启动捆绑相同 IP 地址和相同端口号的多个服务器。
- 允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地 IP 地址即可。这一般不用于 TCP 服务器。
- SO_REUSEADDR 允许完全重复的捆绑:
当一个 IP 地址和端口绑定到某个套接口上时,还允许此 IP 地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对 UDP 套接口而言(TCP 不支持多播)。
SO_REUSEPORT 选项有如下语义:
此选项允许完全重复捆绑,但仅在想捆绑相同 IP 地址和端口的套接口都指定了此套接口选项才行。
如果被捆绑的 IP 地址是一个多播地址,则 SO_REUSEADDR 和 SO_REUSEPORT 等效。
使用这两个套接口选项的建议:
- 在所有 TCP 服务器中,在调用 bind 之前设置 SO_REUSEADDR 套接口选项;
- 当编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置 SO_REUSEADDR 选项,并将本组的多播地址作为本地 IP 地址捆绑
time-wait
TIME_WAIT 状态有两个存在的理由:
- (1) 可靠地实现 TCP 全双工连接的终止
- (2) 允许老的重复分节在网络中消逝
如图所示,在主机 A 的 4 次挥手过程中,如果最后的数据丢失,则主机 B 会认为 A 未能收到自己发送的 FIN 消息,因此重传。这时,收到 FIN 消息的主机 A 将重启 time-wait 计时器。因此,如果网络状态不佳,time-wait 状态将持续
(1) 如果服务器最后发送的 ACK 因为某种原因丢失了,那么客户一定会重新发送 FIN,这样因为有 TIME_WAIT 的存在,服务器会重新发送 ACK 给客户,如果没有 TIME_WAIT,那么无论客户有没有收到 ACK,服务器都已经关掉连接了,此时客户重新发送 FIN,服务器将不会发送 ACK,而是 RST,从而使客户端报错。也就是说,TIME_WAIT 有助于可靠地实现 TCP 全双工连接的终止。
(2) 如果没有 TIME_WAIT,我们可以在最后一个 ACK 还未到达客户的时候,就建立一个新的连接。那么此时,如果客户收到了这个 ACK 的话,就乱套了,必须保证这个 ACK 完全死掉之后,才能建立新的连接。也就是说,TIME_WAIT 允许老的重复分节在网络中消逝。
回到我们的问题,由于我并不是正常地经过四次断开的方式中断连接,所以并不会存在最后一个 ACK 的问题。所以,这样是安全的。不过,最终的服务器版本,还是不要设置为端口可复用的
<?php
$address = '0.0.0.0';
$port = $argv[1] ?? 8071;
$listen = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (false === $listen) errhandle(__LINE__);
//ctrl+c重启时 能立马重启,要在bind,listen之前
if (true !== socket_set_option($listen, SOL_SOCKET, SO_REUSEADDR, 1)) errhandle(__LINE__);;
if (true !== socket_bind($listen, '0.0.0.0', $port)) errhandle(__LINE__);;
if (true !== socket_listen($listen, 5)) errhandle(__LINE__);; //待连接队列长度
//socket_set_nonblock($listen);
echo "Server linsten on:{$address}:$port" . PHP_EOL;
while (true) {
//连接socket,处理连接的接入
$sock_client = socket_accept($listen);
if (false === $sock_client) {
errhandle(__LINE__,false);
continue;
}
processClientConn($sock_client);
}
//处理已经连入的连接
function processClientConn($sock_client)
{
if (socket_getpeername($sock_client, $clinet_addr, $client_port)) {
echo "New client " . intval($sock_client) . " come from {$clinet_addr}:$client_port" . PHP_EOL;
sayWelcome($sock_client);
}
while (true) {
//接收到不少于len
$len = socket_recv($sock_client, $buf, 2048, 0);
if ($len === false) {
echo "no data" . PHP_EOL;
continue;
} elseif ($len === 0) {
errhandle(__LINE__,false);
socket_shutdown($sock_client);
break;
} else {
echo "recv:{" . $buf . "}len=" . $len . PHP_EOL;
if ($buf == 'quit') {
socket_shutdown($sock_client);
break;
}
}
}
}
function errhandle($line_num,$exit=true)
{
echo $line_num.":".socket_last_error() . ":" . socket_strerror(socket_last_error()) . PHP_EOL;
if($exit){
exit();
}
}
function sayWelcome($client)
{
$buf = date("H:i:s") . " welcome to server! you id:" . intval($client) . PHP_EOL;
socket_write($client, $buf, strlen($buf));
}
SO_REUSEPORT
目前常见的网络编程模型就是多进程或多线程,根据 accpet 的位置,分为如下场景
2 种场景
- (1) 单进程或线程创建 socket,并进行 listen 和 accept,接收到连接后创建进程和线程处理连接
- (2) 单进程或线程创建 socket,并进行 listen,预先创建好多个工作进程或线程 accept() 在同一个服务器套接字
这两种模型解充分发挥了多核 CPU 的优势,虽然可以做到线程和 CPU 核绑定,但都会存在:
- 单一 listener 工作进程或线程在高速的连接接入处理时会成为瓶颈
- 多个线程之间竞争获取服务套接字
- 缓存行跳跃
- 很难做到 CPU 之间的负载均衡
- 随着核数的扩展,性能并没有随着提升
SO_REUSEPORT 解决了什么问题
SO_REUSEPORT 支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
- 允许多个套接字 bind()/listen() 同一个 TCP/UDP 端口
- 每一个线程拥有自己的服务器套接字
- 在服务器套接字上没有了锁的竞争
- 内核层面实现负载均衡
- 安全层面,监听同一个端口的套接字只能位于同一个用户下面
其核心的实现主要有三点:
- 扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport
- 修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口
- 修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 和端口的多个 sock 之间均衡选择。
有了 SO_RESUEPORT 后,每个进程可以自己创建 socket、bind、listen、accept 相同的地址和端口,各自是独立平等的
让多进程监听同一个端口,各个进程中 accept socket fd 不一样,有新连接建立时,内核只会唤醒一个进程来 accept,并且保证唤醒的均衡性。
Linux 最新SO_REUSEPORT特性 - Rabbit_Dale - 博客园
https://www.cnblogs.com/Anker/p/7076537.html
SO_REUSEPORT学习笔记 - 聂永的博客 - BlogJava
http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html
没用 reuseport 的
php/advanced/socket_fork_no_reuseport.php · hphper/linux_c - Gitee.com https://gitee.com/hk/process_thread_study/blob/master/php/advanced/socket_fork_no_reuseport.php
用了的
php/advanced/socket_fork_reuseport.php · hphper/linux_c - Gitee.com https://gitee.com/hk/process_thread_study/blob/master/php/advanced/socket_fork_reuseport.php
via:
-
socket 常见选项之 SO_REUSEADDR,SO_REUSEPORT - H&K - 博客园 posted @ 2019-10-20 12:01
https://www.cnblogs.com/HKUI/p/11707170.html
socket 端口复用 SO_REUSEPORT 与 SO_REUSEADDR
背景
在学习 SO_REUSEADDR 地址复用的时候,看到有人提到了 SO_REUSEPORT 。于是也了解了一下。
SO_REUSEPORT 概述
SO_REUSEPOR
这个 socket 选项可以让你将多个 socket 绑定在同一个监听端口,然后让内核给你自动做负载均衡,将请求平均地让多个线程进行处理。
SO_REUSEPORT 解决了什么问题
SO_REUSEPORT 支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
- 允许多个套接字 bind()/listen() 同一个 TCP/UDP 端口
- 每一个线程拥有自己的服务器套接字
- 在服务器套接字上没有了锁的竞争
- 内核层面实现负载均衡
- 安全层面,监听同一个端口的套接字只能位于同一个用户下面
其核心的实现主要有三点:
- 扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport
- 修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口
- 修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 和端口的多个 sock 之间均衡选择。
有了 SO_RESUEPORT 后,每个进程可以自己创建 socket、bind、listen、accept 相同的地址和端口,各自是独立平等的
让多进程监听同一个端口,各个进程中 accept socket fd 不一样,有新连接建立时,内核只会唤醒一个进程来 accept,并且保证唤醒的均衡性。
安全性考虑
- 第一个进程必须 enable 了这个选项之后,后续的进程才可以通过 enable 这个选项将 socket 绑定到同一个端口上。
- 绑定到同一个端口的进程的 effective user id 必须一致。
上述规定是为了避免 hijacking:恶意用户通过监听相同的端口来获取用户信息。
在没有 SO_REUSEPORT 的年代
在 SO_REUSEPORT 没有出现之前,多线程编程一般有两种获取到来的请求。
- 指派一条线程专门进行 accept,获取 socket 后分派给 worker 线程。这种方法使得进行 accept 的线程成为了单点,容易成为性能的瓶颈。
- 多个线程同时进行 accept。这种方法的问题是每一个线程 accept 成功的概率不均匀,导致负载不均衡。
SO_REUSEPORT 的负载均衡算法
使用 (remote_ip, remote_port, local_ip, local_port)
来进行哈希,因此可以保证同一个 client 的包可以路由到同一个进程。但是,当一个 listen 的进程加进来或者 terminate 的时候,由于没有实现 一致性哈希
,结果可能导致有些请求由于路由到另外一个进程上,client-server 的三次握手过程可能会被重置。
SO_REUSEPORT 与 SO_REUSEADDR 的区别
Socket 的基本背景
在讨论这两个选项的区别时,我们需要知道的是 BSD 实现是所有 socket 实现的起源。基本上其他所有的系统某种程度上都参考了 BSD socket 实现(或者至少是其接口),然后开始了它们自己的独立发展进化。显然,BSD 本身也是随着时间在不断发展变化的。所以较晚参考 BSD 的系统比较早参考 BSD 的系统多一些特性。所以理解 BSD socket 实现是理解其他 socket 实现的基石。下面我们就分析一下 BSD socket 实现。
在这之前,我们首先要明白如何唯一识别 TCP/UDP 连接。TCP/UDP 是由以下五元组唯一地识别的:
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
这些数值组成的任何独特的组合可以唯一地确一个连接。
**那么,对于任意连接,这五个值都不能完全相同。**否则的话操作系统就无法区别这些连接了。
一个 socket 的协议是在用 socket() 初始化的时候就设置好的。
源地址(source address)和源端口(source port)在调用 bind() 的时候设置。
目的地址(destination address)和目的端口(destination port)在调用 connect() 的时候设置。
其中 UDP 是无连接的,UDP socket 可以在未与目的端口连接的情况下使用。但 UDP 也可以在某些情况下先与目的地址和端口建立连接后使用。在使用无连接 UDP 发送数据的情况下,如果没有显式地调用 bind(),操作系统会在第一次发送数据时自动将 UDP socket 与本机的地址和某个端口绑定(否则的话程序无法接受任何远程主机回复的数据)。同样的,一个没有绑定地址的 TCP socket 也会在建立连接时被自动绑定一个本机地址和端口。
如果我们手动绑定一个端口,我们可以将 socket 绑定至端口 0,绑定至端口 0 的意思是让系统自己决定使用哪个端口(一般是从一组操作系统特定的提前决定的端口数范围中),所以也就是任何端口的意思。同样的,我们也可以使用一个通配符来让系统决定绑定哪个源地址(ipv4 通配符为 0.0.0.0,ipv6 通配符为::)。而与端口不同的是,一个 socket 可以被绑定到主机上所有接口所对应的地址中的任意一个。基于连接在本 socket 的目的地址和路由表中对应的信息,操作系统将会选择合适的地址来绑定这个 socket,并用这个地址来取代之前的通配符 IP 地址。
在默认情况下,任意两个 socket 不能被绑定在同一个源地址和源端口组合上。比如说我们将 socketA 绑定在 A:X 地址,将 socketB 绑定在 B:Y 地址,其中 A 和 B 是 IP 地址,X 和 Y 是端口。那么在 AB 的情况下 X!=Y 必须满足,在 XY 的情况下 A!=B 必须满足。需要注意的是,如果某一个 socket 被绑定在通配符 IP 地址下,那么事实上本机所有 IP 都会被系统认为与其绑定了。例如一个 socket 绑定了 0.0.0.0:21,在这种情况下,任何其他 socket 不论选择哪一个具体的 IP 地址,其都不能再绑定在 21 端口下。因为通配符 IP0.0.0.0 与所有本地 IP 都冲突。
以上所有内容基本上在主要操作系统中都相同。而各个中 SO_REUSEADDR 会有不同的含义。首先我们来讨论 BSD 实现。因为 BSD 试试其他所有 socket 实现方法的源头。
SO_REUSEADDR
如果在一个 socket 绑定到某一地址和端口之前设置了其 SO_REUSEADDR 的属性,那么除非本 socket 与产生了尝试与另一个 socket 绑定到完全相同的源地址和源端口组合的冲突,否则的话这个 socket 就可以成功的绑定这个地址端口对。这听起来似乎和之前一样。但是其中的关键字是完全。SO_REUSEADDR 主要改变了系统对待通配符 IP 地址冲突的方式。
如果不用 SO_REUSEADDR 的话,如果我们将 socketA 绑定到 0.0.0.0:21,那么任何将本机其他 socket 绑定到端口 21 的举动(如绑定到 192.168.1.1:21)都会导致 EADDRINUSE 错误。因为 0.0.0.0 是一个通配符 IP 地址,意味着任意一个 IP 地址,所以任何其他本机上的 IP 地址都被系统认为已被占用。如果设置了 SO_REUSEADDR 选项,因为 0.0.0.0:21 和 192.168.1.1:21 并不是完全相同的地址端口对(其中一个是通配符 IP 地址,另一个是一个本机的具体 IP 地址),所以这样的绑定是可以成功的。需要注意的是,无论 socketA 和 socketB 初始化的顺序如何,只要设置了 SO_REUSEADDR,绑定都会成功;而只要没有设置 SO_REUSEADDR,绑定都不会成功。
SO_REUSEADDR | socketA | socketB | Result |
---|---|---|---|
ON / OFF | 192.168.1.1:21 | 192.168.1.1:21 | ERROR(EADDRINUSE) |
ON / OFF | 192.168.1.1:21 | 10.0.1.1:21 | OK |
ON / OFF | 10.0.1.1:21 | 192.168.1.1:21 | OK |
OFF | 192.168.1.1:21 | 0.0.0.0:21 | ERROR(EADDRINUSE) |
OFF | 0.0.0.0:21 | 192.168.1.1:21 | ERROR(EADDRINUSE) |
ON | 192.168.1.1:21 | 0.0.0.0:21 | OK |
ON | 0.0.0.0:21 | 192.168.1.1:21 | OK |
ON / OFF | 0.0.0.0:21 | 0.0.0.0:21 | OK |
这个表格假定 socketA 已经成功地绑定了表格中对应的地址,然后 socketB 被初始化了,其 SO_REUSEADDR 设置的情况如表格第一列所示,然后 socketB 试图绑定表格中对应地址。Result 列是其绑定的结果。如果第一列中的值是 ON/OFF,那么 SO_REUSEADDR 设置与否都与结果无关。
上面讨论了 SO_REUSEADDR 对通配符 IP 地址的作用,但其并不只有这一作用。其另一作用也是为什么大家在进行服务器端编程的时候会采用 SO_REUSEADDR 选项的原因。为了理解其另一个作用及其重要应用,我们需要先更深入地讨论一下 TCP 协议的工作原理。
每一个 socket 都有其相应的发送缓冲区(buffer)。当成功调用其 send() 方法的时候,实际上我们所要求发送的数据并不一定被立即发送出去,而是被添加到了发送缓冲区中。对于 UDP socket 来说,即使不是马上被发送,这些数据一般也会被很快发送出去。但对于 TCP socket 来说,在将数据添加到发送缓冲区之后,可能需要等待相对较长的时间之后数据才会被真正发送出去。因此,当我们关闭了一个 TCP socket 之后,其发送缓冲区中可能实际上还仍然有等待发送的数据。但此时因为 send() 返回了成功,我们的代码认为数据已经实际上被成功发送了。如果 TCP socket 在我们调用 close() 之后直接关闭,那么所有这些数据都将会丢失,而我们的代码根本不会知道。但是,TCP 是一个可靠的传输层协议,直接丢弃这些待传输的数据显然是不可取的。实际上,如果在 socket 的发送缓冲区中还有待发送数据的情况下调用了其 close() 方法,其将会进入一个所谓的 TIME_WAIT 状态。在这个状态下,socket 将会持续尝试发送缓冲区的数据直到所有数据都被成功发送或者直到超时,超时被触发的情况下 socket 将会被强制关闭。
操作系统的 kernel 在强制关闭一个 socket 之前的最长等待时间被称为延迟时间(Linger Time)。在大部分系统中延迟时间都已经被全局设置好了,并且相对较长(大部分系统将其设置为 2 分钟)。我们也可以在初始化一个 socket 的时候使用 SO_LINGER 选项来特定地设置每一个 socket 的延迟时间。我们甚至可以完全关闭延迟等待。但是需要注意的是,将延迟时间设置为 0(完全关闭延迟等待)并不是一个好的编程实践。因为优雅地关闭 TCP socket 是一个比较复杂的过程,过程中包括与远程主机交换数个数据包(包括在丢包的情况下的丢失重传),而这个数据包交换的过程所需要的时间也包括在延迟时间中。如果我们停用延迟等待,socket 不止会在关闭的时候直接丢弃所有待发送的数据,而且总是会被强制关闭(由于 TCP 是面向连接的协议,不与远端端口交换关闭数据包将会导致远端端口处于长时间的等待状态)。所以通常我们并不推荐在实际编程中这样做。TCP 断开连接的过程超出了本文讨论的范围,如果对此有兴趣,可以参考这个页面。并且实际上,如果我们禁用了延迟等待,而我们的程序没有显式地关闭 socket 就退出了,BSD(可能包括其他系统)会忽略我们的设置进行延迟等待。例如,如果我们的程序调用了 exit() 方法,或者其进程被使用某个信号终止了(包括进程因为非法内存访问之类的情况而崩溃)。所以我们无法百分之百保证一个 socket 在所有情况下忽略延迟等待时间而终止。
这里的问题在于操作系统如何对待处于 TIME_WAIT 阶段的 socket。如果 SO_REUSEADDR 选项没有被设置,处于 TIME_WAIT 阶段的 socket 任然被认为是绑定在原来那个地址和端口上的。直到该 socket 被完全关闭之前(结束 TIME_WAIT 阶段),任何其他企图将一个新 socket 绑定该该地址端口对的操作都无法成功。这一等待的过程可能和延迟等待的时间一样长。所以我们并不能马上将一个新的 socket 绑定到一个刚刚被关闭的 socket 对应的地址端口对上。在大多数情况下这种操作都会失败。
然而,如果我们在新的 socket 上设置了 SO_REUSEADDR 选项,如果此时有另一个 socket 绑定在当前的地址端口对且处于 TIME_WAIT 阶段,那么这个已存在的绑定关系将会被忽略。事实上处于 TIME_WAIT 阶段的 socket 已经是半关闭的状态,将一个新的 socket 绑定在这个地址端口对上不会有任何问题。这样的话原来绑定在这个端口上的 socket 一般不会对新的 socket 产生影响。但需要注意的是,在某些时候,将一个新的 socket 绑定在一个处于 TIME_WAIT 阶段但仍在工作的 socket 所对应的地址端口对会产生一些我们并不想要的,无法预料的负面影响。但这个问题超过了本文的讨论范围。而且幸运的是这些负面影响在实践中很少见到。
最后,关于 SO_REUSEADDR,我们还要注意的一件事是,以上所有内容只要我们对新的 socket 设置了 SO_REUSEADDR 就成立。至于原有的已经绑定在当前地址端口对上的,处于或不处于 TIME_WAIT 阶段的 socket 是否设置了 SO_REUSEADDR 并无影响。决定 bind 操作是否成功的代码仅仅会检查新的被传递到 bind() 方法的 socket 的 SO_REUSEADDR 选项。其他涉及到的 socket 的 SO_REUSEADDR 选项并不会被检查。
SO_REUSEPORT
许多人将 SO_REUSEADDR 当成了 SO_REUSEPORT。基本上来说,SO_REUSEPORT 允许我们将任意数目的 socket 绑定到完全相同的源地址端口对上,只要所有之前绑定的 socket 都设置了 SO_REUSEPORT 选项。如果第一个绑定在该地址端口对上的 socket 没有设置 SO_REUSEPORT,无论之后的 socket 是否设置 SO_REUSEPORT,其都无法绑定在与这个地址端口完全相同的地址上。除非第一个绑定在这个地址端口对上的 socket 释放了这个绑定关系。与 SO_REUSEADDR 不同的是 ,处理 SO_REUSEPORT 的代码不仅会检查当前尝试绑定的 socket 的 SO_REUSEPORT,而且也会检查之前已绑定了当前尝试绑定的地址端口对的 socket 的 SO_REUSEPORT 选项。
SO_REUSEPORT 并不等于 SO_REUSEADDR。这么说的含义是如果一个已经绑定了地址的 socket 没有设置 SO_REUSEPORT,而另一个新 socket 设置了 SO_REUSEPORT 且尝试绑定到与当前 socket 完全相同的端口地址对,这次绑定尝试将会失败。同时,如果当前 socket 已经处于 TIME_WAIT 阶段,而这个设置了 SO_REUSEPORT 选项的新 socket 尝试绑定到当前地址,这个绑定操作也会失败。为了能够将新的 socket 绑定到一个当前处于 TIME_WAIT 阶段的 socket 对应的地址端口对上,我们要么需要在绑定之前设置这个新 socket 的 SO_REUSEADDR 选项,要么需要在绑定之前给两个 socket 都设置 SO_REUSEPORT 选项。当然,同时给 socket 设置 SO_REUSEADDR 和 SO_REUSEPORT 选项是也是可以的。
SO_REUSEPORT 是在 SO_REUSEADDR 之后被添加到 BSD 系统中的。这也是为什么现在有些系统的 socket 实现里没有 SO_REUSEPORT 选项。因为它们在这个选项被加入 BSD 系统之前参考了 BSD 的 socket 实现。而在这个选项被加入之前,BSD 系统下没有任何办法能够将两个 socket 绑定在完全相同的地址端口对上。
Connect() 返回 EADDRINUSE?
有些时候 bind() 操作会返回 EADDRINUSE 错误。但奇怪的是,在我们调用 connect() 操作时,也有可能得到 EADDRINUSE 错误。这是为什么呢?为何一个我们尝试令当前端口建立连接的远程地址也会被占用呢?难道将多个 socket 连接到同一个远程地址的操作会有什么问题产生吗?
正如本文之前所说,一个连接关系是由一个五元组确定的。对于任意的连接关系而言,这个五元组必须是唯一的。否则的话,系统将无法分辨两个连接。而现在当我们采用了地址复用之后,我们可以将两个采用相同协议的 socket 绑定到同一地址端口对上。这意味着对这两个 socket 而言,五元组里的 {, , } 已经相同了。在这种情况下,如果我们尝试将它们都连接到同一个远程地址端口上,这两个连接关系的五元组将完全相同。也就是说,产生了两个完全相同的连接。在 TCP 协议中这是不被允许的(UDP 是无连接的)。如果这两个完全相同的连接种的某一个接收到了数据,系统将无法分辨这个数据到底属于哪个连接。所以在这种情况下,至少这两个 socket 所尝试连接的远程主机的地址和端口不能相同。只有如此,系统才能继续区分这两个连接关系。
所以当我们将两个采用相同协议的 socket 绑定到同一个本地地址端口对上后,如果我们还尝试让它们和同一个目的地址端口对建立连接,第二个尝试调用 connect() 方法的 socket 将会报 EADDRINUSE 的错误,这说明一个拥有完全相同的五元组的 socket 已经存在了。
Multicast Address
相对于用于一对一通信的 unicast 地址,multicast 地址用于一对多通信。IPv4 和 IPv6 都拥有 multicast 地址。但是 IPv4 中的 multicast 实际上在公共网路上很少被使用。
SO_REUSEADDR 的意义在 multicast 地址的情况下会与之前有所不同。在这种情况下,SO_REUSEADDR 允许我们将多个 socket 绑定至完全相同的源广播地址端口对上。换句话说,对于 multicast 地址而言,SO_REUSEADDR 的作用相当于 unicast 通信中的 SO_REUSEPORT。事实上,在 multicast 情况下,SO_REUSEADDR 和 SO_REUSEPORT 的作用完全相同。
其他 操作系统 的差异性
FreeBSD/OpenBSD/NetBSD
所有这些系统都是参考了较新的原生 BSD 系统代码。所以这三个系统提供与 BSD 完全相同的 socket 选项,这些选项的含义与原生 BSD 完全相同。
MacOS X
MacOS X 的核心代码实现是基于较新版本的原生 BSD 的 BSD 风格的 UNIX,所以 MacOS X 提供与 BSD 完全相同的 socket 选项,并且它们的含义也与 BSD 系统相同。
iOS
iOS 事实上是一个略微改造过的 MacOS X,所以适用于 MacOS X 的也适用于 iOS。
Linux
在 Linux3.9 之前,只有 SO_REUSEADDR 选项存在。这个选项的作用基本上同 BSD 系统下相同。但其仍有两个重要的区别。
第一个区别是如果一个处于监听(服务器)状态下的 TCP socket 已经被绑定到了一个通配符 IP 地址和一个特定端口下,那么不论这两个 socket 有没有设置 SO_REUSEADDR 选项,任何其他 TCP socket 都无法再被绑定到相同的端口下。即使另一个 socket 使用了一个具体 IP 地址(像在 BSD 系统中允许的那样)也不行。而非监听(客户)TCP socket 则无此限制。
第二个区别是对于 UDP socket 来说,SO_REUSEADDR 的作用和 BSD 中 SO_REUSEPORT 完全相同。所以两个 UDP socket 如果都设置了 SO_REUSEADDR 的话,它们就可以被绑定在一组完全相同的地址端口对上。
Linux3.9 加入了 SO_REUSEPORT 选项。只要所有 socket(包括第一个)在绑定地址前设置了这个选项,两个或多个,TCP 或 UDP,监听(服务器)或非监听(客户)socket 就可以被绑定在完全相同的地址端口组合下。同时,为了防止端口劫持(port hijacking),还有一个特别的限制:所有试图绑定在相同的地址端口组合的 socket 必须属于拥有相同用户 ID 的进程。所以一个用户无法从另一个用户那里 “偷窃” 端口。
除此之外,对于设置了 SO_REUSEPORT 选项的 socket,Linux kernel 还会执行一些别的系统所没有的特别的操作:对于绑定于同一地址端口组合上的 UDP socket,kernel 尝试在它们之间平均分配收到的数据包;对于绑定于同一地址端口组合上的 TCP 监听 socket,kernel 尝试在它们之间平均分配收到的连接请求(调用 accept() 方法所得到的请求)。这意味着相比于其他允许地址复用但随机将收到的数据包或者连接请求分配给连接在同一地址端口组合上的 socket 的系统而言,Linux 尝试了进行流量分配上的优化。比如一个简单的服务器进程的几个不同实例可以方便地使用 SO_REUSEPORT 来实现一个简单的负载均衡,而且这个负载均衡有 kernel 负责, 对程序来说完全免费!
Android
Android 的核心部分是略微修改过的 Linux kernel,所以所有适用于 Linux 的操作也适用于 Android。
Windows
Windows 仅有 SO_REUSEADDR 选项。在 Windows 中对一个 socket 设置 SO_REUSEADDR 的效果与在 BSD 下同时对一个 socket 设置 SO_REUSEPORT 和 SO_REUSEADDR 相同。但其区别在于:即使另一个已绑定地址的 socket 并没有设置 SO_REUSEADDR,一个设置了 SO_REUSEADDR 的 socket 总是可以绑定到与另一个已绑定的 socket 完全相同的地址端口组合上。这个行为可以说是有些危险的。因为它允许了一个应用从另一个引用已连接的端口上偷取数据。微软意识到了这个问题,因此添加了另一个 socket 选项:SO_EXCLUSIVEADDRUSE。对一个 socket 设置 SO_EXCLUSIVEADDRUSE 可以确保一旦该 socket 绑定了一个地址端口组合,任何其他 socket,不论设置 SO_REUSEADDR 与否,都无法再绑定当前的地址端口组合。
Solaris
Solaris 是 SunOS 的继任者。SunOS 从某种程度上来说也是一个较早版本的 BSD 的一个支路。因此 Solaris 只提供 SO_REUSEADDR,且其表现和 BSD 系统中基本相同。据我所知,在 Solaris 系统中无法实现与 SO_REUSEPORT 相同的功能。这意味着在 Solaris 中无法将两个 socket 绑定到完全相同的地址端口组合下。
与 Windows 类似的是,Solaris 也为 socket 提供独占绑定的选项 ——SO_EXCLBIND。如果一个 socket 在绑定地址前设置了这个选项,即使其他 socket 设置了 SO_REUSEADDR 也将无法绑定至相同地址。例如:如果 socketA 绑定在了通配符 IP 地址下,而 socketB 设置了 SO_REUSEADDR 且绑定在一个具体 IP 地址和与 socketA 相同的端口的组合下,这个操作在 socketA 没有设置 SO_EXCLBIND 的情况下会成功,否则会失败。
via:
-
socket 端口复用 SO_REUSEPORT 与 SO_REUSEADDR - schips - 博客园 posted @ 2020-03-23 17:05
https://www.cnblogs.com/schips/p/12553321.html