ChannelOption.SO_BACKLOG的作用

在使用 netty 开发网络应用时,通常会设置 ChannelOption.SO_BACKLOG,不知你有没想过这个参数的作用是什么。

源码分析

一个常见的配置方式如下所示,通过 ServerBootstrapoption() 方法配置 ChannelOption.SO_BACKLOG 的值

1
2
3
4
5
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
...

要搞清楚它的作用,我们可以跟踪找到它的应用位置。一个简单的方式是 debug,跟着 ServerBootstrapbind 方法一路点进去,最终可以定位到 NioServerSocketChanneldoBind(SocketAddress) 方法,它的实现如下

1
2
3
4
5
6
7
8
9
10
// NioServerSocketChannel#doBind
@SuppressJava6Requirement(reason = "Usage guarded by java version check")
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}

可以看到是在 jdk ServerSocketChannelbind 方法中使用的。

查看它的方法签名,一共有两个参数。第一个是 SocketAddress 指定了要绑定的地址,第二个就是 backlog

1
2
public abstract ServerSocketChannel bind(SocketAddress local, int backlog)
throws IOException;

bind 方法将 channel 的 socket 绑定到一个本地地址,并且配置 socket 以监听远端的连接请求。它将 socket 和本地地址进行关联。关于 backlog 参数的含义,其注释说明如下:

1
2
3
4
5
6
The {@code backlog} parameter is the maximum number of pending
connections on the socket. Its exact semantics are implementation specific.
In particular, an implementation may impose a maximum length or may choose
to ignore the parameter altogther. If the {@code backlog} parameter has
the value {@code 0}, or a negative value, then an implementation specific
default is used.

简单来说,backlog 用于指定 socket 的 pending connection 的最大数量。当然,它的具体语义与底层实现相关,比如可以实现为强制设置一个默认最大值而忽略 backlog 参数。另外,如果将 backlog 设置为 0 或一个负数,那么系统将会使用默认值。

我们来查看下 jdk 中的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ServerSocketChannelImpl#bind
public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException {
synchronized (lock) {
if (!isOpen())
throw new ClosedChannelException();
if (isBound())
throw new AlreadyBoundException();
InetSocketAddress isa = (local == null) ? new InetSocketAddress(0) :
Net.checkAddress(local);
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkListen(isa.getPort());
NetHooks.beforeTcpBind(fd, isa.getAddress(), isa.getPort());
// 将socket绑定到本地地址和端口
Net.bind(fd, isa.getAddress(), isa.getPort());
// 监听连接请求,并设置backlog的值
Net.listen(fd, backlog < 1 ? 50 : backlog);
synchronized (stateLock) {
localAddress = Net.localAddress(fd);
}
}
return this;
}

从中可以看到,如果 backlog 的值设置为 0 或负数,那么将会使用默认值 50

Net.listen 底层使用的是 listen 系统调用,可通过 man 2 listen 查看 listen 的说明

1
2
3
4
5
6
7
Creation of socket-based connections requires several operations.  First, a socket is created with socket(2).  Next, a willingness to
accept incoming connections and a queue limit for incoming connections are specified with listen(). Finally, the connections are
accepted with accept(2). The listen() call applies only to sockets of type SOCK_STREAM.

The backlog parameter defines the maximum length for the queue of pending connections. If a connection request arrives with the
queue full, the client may receive an error with an indication of ECONNREFUSED. Alternatively, if the underlying protocol supports
retransmission, the request may be ignored so that retries may succeed.

创建一个基于 socket 的连接需要以下几个步骤:

  1. 通过 socket 系统调用创建一个 socket
  2. 通过 listen 系统调用监听连接请求,并且设置请求队列的大小限制。listen 系统调用只适用于 SOCK_STREAM 类型的 socket

backlog 参数定义了待接受连接的最大队列长度,如果一个连接到来后发现队列是满的,发起连接请求的 client 将会受到一个 ECONNREFUSED 类型的错误。另外,如果底层协议支持重传,这个请求将被忽略,重传将有可能成功。

与3次握手的关系

tcp 建立连接需要 3 次握手:

  1. client 发起 SYN 请求:Flags [S.]
  2. server 返回 ACK,同时带上 SYN 标志:Flags [S.]
  3. client 返回 ACK:Flags [.]

第二步之后,server 端收到了请求意愿,此时连接还未完全建立,只能算是半连接,还需等待对方的 ACK。

第三步之后,server 端收到了最后一个 ACK,连接已建立。

连接建立后,应用就可以通过 accept 系统调用获取到已建立的连接

1
2
3
4
accept() extracts the first connection request on the queue of pending connections, creates a new socket
with the same properties of socket, and allocates a new file descriptor for the socket. If no pending connections are present on the
queue, and the socket is not marked as non-blocking, accept() blocks the caller until a connection is present. If the socket is
marked non-blocking and no pending connections are present on the queue, accept() returns an error as described below.

accept() 从待接受连接队列中获取第一个连接,使用与原 socket 相同的属性创建一个新的 socket,并且为这个 socket 分配对应的文件描述符 FD。

如果队列中没有待接受的连接,并且 socket 没有设置非阻塞标记,accept() 将会阻塞调用者,直到有新的连接出现。如果 socket 标记为非阻塞,并且队列中没有待接受的连接,accept() 将会返回一个错误码

未设置时的默认值是多少

如果不手动设置,netty 会根据系统情况设置一个默认值

1
2
// DefaultServerSocketChannelConfig#backlog
private volatile int backlog = NetUtil.SOMAXCONN;

该默认值来自于当前系统下的 somaxconn 取值,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Determine the default somaxconn (server socket backlog) value of the platform.
// The known defaults:
// - Windows NT Server 4.0+: 200
// - Linux and Mac OS X: 128
int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
File file = new File("/proc/sys/net/core/somaxconn");
BufferedReader in = null;
try {
// file.exists() may throw a SecurityException if a SecurityManager is used, so execute it in the
// try / catch block.
// See https://github.com/netty/netty/issues/4936
if (file.exists()) {
in = new BufferedReader(new FileReader(file));
somaxconn = Integer.parseInt(in.readLine());
if (logger.isDebugEnabled()) {
logger.debug("{}: {}", file, somaxconn);
}
} else {
// Try to get from sysctl
Integer tmp = null;
if (SystemPropertyUtil.getBoolean("io.netty.net.somaxconn.trySysctl", false)) {
tmp = sysctlGetInt("kern.ipc.somaxconn");
if (tmp == null) {
tmp = sysctlGetInt("kern.ipc.soacceptqueue");
if (tmp != null) {
somaxconn = tmp;
}
} else {
somaxconn = tmp;
}
}

if (tmp == null) {
logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file,
somaxconn);
}
}
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}",
file, somaxconn, e);
}
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
// Ignored.
}
}
}
return somaxconn;
  1. 尝试读取 /proc/sys/net/core/somaxconn 文件中的值(适用于Linux),若文件不存在则继续下一步
  2. 尝试从 sysctl 命令中获取(针对kqueue,适用于BSD或MacOS),前提是设置了 io.netty.net.somaxconn.trySysctl 属性
  3. 前两步都失败,则使用默认值。windows 下的默认值是 200,其他系统下的默认值是 128

结论

backlog 参数用于设置待接受连接队列的长度,该队列用于放置 3 次握手后建立的连接,应用通过 accept 系统调用从该队列获取已建立的连接,用于后续的处理。

如果你对待接受连接队列的信息感兴趣,可以了解下这篇文章的实验和分析:backlog参数对TCP连接建立的影响