两台电脑之间究竟可以建立多少个 TCP 连接?

最近参加面试,有一道面试题是这样的,两台普通 PC 之间可以建立多少个 TCP 连接?这里的普通 PC 是指对外只有一个 IP 地址。我当时回答的是如果不考虑已经被占用的端口和保留端口号,可以建立 65535 个连接。结果面试官说不对,说可以建立 65535 的平方个连接。这也就是说,一个端口可以同时建立 65535 个 TCP 连接。那么,平时我们所说的端口占用到底是什么情况?这个面试题的答案是如面试官所说的吗?面试归来后,我经过一番搜索,终于搞明白了这个问题。

我们把其中一台 PC 叫做 Client(主动建立 TCP 连接的一方),另一台 PC 叫做 Server(被动建立 TCP 连接的一方)。如果要建立多于 65535 个连接,就得考虑一个端口建立多个连接,我们考虑四种情况:

  1. Client 同一个端口与 Server 同一个端口同时建立多个连接
  2. Client 多个端口与 Server 多个端口建立连接,一个端口只建立一个连接
  3. Client 多个端口与 Server 同一个端口同时建立连接
  4. Client 同一个端口与 Server 多个端口同时建立连接

下面分别讲讲这四种情况的可行性。以下测试都是在 Mac OS X 上面进行,一台机器同时做 Client 和 Server。

Client 同一个端口与 Server 同一个端口同时建立多个连接

Client 与 Server 通过 socket 建立 TCP 连接,而一个 socket 是被 {源地址, 源端口, 目的地址, 目的端口, 协议} 这个五元组唯一确定的,一个连接对应于 Client 上的一个 socket 和 Server 上的一个 socket,这两个 socket 的区别就是把源地址/源端口与目的地址/目的端口调换一下。如果 Client 与 Server 的端口都不变的情况下建立多个连接,而源地址和目的地址也不变(每个 PC 对外只有一个 IP 地址),协议保持为 TCP 不变,这多个 socket 的五元组都相同,那么如何去区分这些连接呢?所以第一种情况显然是不可行的。

Client 多个端口与 Server 多个端口建立连接,一个端口只建立一个连接

在这种情况下,Client 的每个 socket 的五元组中的源端口与目的端口都不相同(Server 的 socket 也是这样),显然,第二种情况是可行的。

Client 多个端口与 Server 同一个端口同时建立连接

这种情况下与我们浏览器访问网站服务器是一样的,如果我们开了两个浏览器标签页,两个标签页同时访问一个网站的 80 端口,两个标签页使用的源端口不一样,但目的端口都是 80 端口。为了模拟这种情况,下面给出一个 Server 的示例:

一个 Client 的示例:

编译后分别运行 java TCPServer 12345 , java TCPClient 12346 12345 , java TCPClient 12347 12345 。Server 端的输出:

可以看到,第三种情况也是可行的。其实到这里,在 Server 上,一个端口已经可以同时与 Client 的不同端口建立连接,但是,如果要建立多于 65535 个连接,还要求 Client 同一个端口与 Server 多个端口同时建立连接。

Client 同一个端口与 Server 多个端口同时建立连接

这里我们用 Python 写一个脚本 TCPClient.py:

运行  java TCPServer 12345 , java TCPServer 12347 , Python TCPClient.py 12346 12345 和  Python TCPClient.py 12346 12347 。两个 Server 的输出分别为 :

可以看出,第四种情况也是可行的。你可能要问,博主怎么那么装逼,前面都用 Java,怎么又换用 Python。 别急,用 Python 的原因,下文会有说明。

socket 有两个 option,分别为 SO_REUSEADDR 和 SO_REUSEPORT,正是这两个 option 控制着 socket 对地址和端口的复用。下面来说说这两个 option。以下 A 指某个特定的 IP 地址,P 指某个特定的端口。

SO_REUSEADDR

从 [1] 中可以找到这个 option 的作用,总结如下:

  • 如果 socket 绑定 0.0.0.0,表示绑定任何本地所有 IP 地址。
  • 如果没有使用 SO_REUSEADDR,在有一个 socket 绑定了 0.0.0.0:P 的情况下,其他 socket 都不能绑定 AnyIP:P(AnyIP 指 0.0.0.0 或者一个特定 IP 地址),反过来也不行。
  • 如果使用了 SO_REUSEADDR,有一个 socket 绑定了 0.0.0.0:P 的情况下,其他 socket 可以绑定 A:P,A 不能为 0.0.0.0,反过来也成立。另外,如果有一个 socket 绑定了 A:P 并且连接状态为 TIME-WAIT,另一个 socket 想要绑定 A:P,在使用了 SO_REUSEADDR 的情况下是可行的。
  • SO_REUSEADDR 只需要想要绑定的 socket 使用这个选项就可以,不要求已经绑定的 socket 或者处在 TIME-WAIT 的 socket 使用这个选项。

SO_REUSEPORT

同样从 [1] 中找到 option 的作用:

  • 使用这个选项后,多个 socket 可以绑定同一个 A:P。
  • 要求这些 socket 都要使用这个选项,这与 SO_REUSEADDR 不同。
  • 设置了 SO_REUSEPORT ,并没有表示 SO_REUSEADDR 也被设置了。
  • 可以同时设置 SO_REUSEPORT 和 SO_REUSEADDR。

以上都是在 BSD 上面的作用,对于其他系统来说,这两个 option 的作用如何呢?

Windows

Windows 只知道 SO_REUSEADDR 选项,不知道 SO_REUSEPORT,在 Windows 上面设置了SO_REUSEADDR 就相当于同时设置了 SO_REUSEADDR 和 SO_REUSEPORT。当然有一些其他的不同,见 [1]。

Linux

Linux 3.9 之前,Linux 只知道 SO_REUSEADDR,SO_REUSEADDR 的作用与 BSD 中的作用是一样的,除了一些特殊情况,见 [1]。Linux 3.9 引入了SO_REUSEPORT,允许多个 socket 绑定同一个 A:P。在引入 SO_REUSEPORT 这一点上,Linux 3.9 还加入了一个黑科技,就是,如果有多个线程或进程同时绑定并监听同一个端口,Client 的连接请求在分配给这多个线程或进程的时候可以实现负载均衡,这在网站服务器上面很有用。以往网站服务器有两种方法分配连接,第一种是上面的 Java 代码所示,主线程监听连接请求,建立连接后就把这个连接让新线程去处理,这时候,主线程就成了一个瓶颈。第二种方法是绑定端口并监听后,让多个线程去 accept()(Apache 就是这样,见 [5]),这些线程都会阻塞在 accept() 函数,但这又引入了一个新的问题,一个新的连接请求来了后,哪个线程被唤醒去处理这个连接请求就是操作系统的线程调度所决定的,这容易造成分配不均衡。而 Linux 3.9 这个新特性就从底层解决了这个问题,而不是由线程调度所决定。

Mac OS X

Mac OS X 的内核包含 BSD 的一些代码,它支持 SO_REUSEADDR 和 SO_REUSEPORT,而且与标准 BSD 中这两个选项的作用一样。

我测试的机器是 Mac OS X 系统,所有上面的 Python 代码可以使用 SO_REUSEPORT 来使多个 socket 来绑定同一个 A:P。那么为什么刚才不使用 Java 呢?由 [3] 和 [4] 可以知道,Java 不支持 SO_REUSEPORT,这是因为 Java 要考虑跨平台,而 Windows 是不支持 SO_REUSEPORT 这个 option 的,所以 Java 目前来说没有支持 SO_REUSEPORT。如果我们运行  java TCPServer 12345 , java TCPServer 12347 ,然后运行  java TCPClient 12346 12345 ,到这一步都是没有问题的,如果继续运行  java TCPClient 12346 12347 ,就会有如下错误:

显示地址已经被占用。由 [4] 可以看到,Java 支持 SO_REUSEADDR,但 SO_REUSEADDR 在不同平台的作用是不一样的,在 Mac OS X 平台上,它就没有提供多个 socket 绑定同一个 A:P 的功能,所以我就使用了 Python。如果 Java 要使用 SO_REUSEPORT,只能借助于 Java Native Interface(JNI)调用 C 代码了,正如 《Java 编程思想》的作者 Bruce Eckel 所说,Java 是个好东西,但也不是那么好。真正跨平台的语言是不存在的,总是有些特性需要特殊处理,Java 也不例外。

现在我们就可以回答文章开头提出来的问题了。

到底什么是端口占用?

在没有使用 SO_REUSEPORT 的情况下(Windows 是 SO_REUSEADDR),已经被占用的端口,新的 socket 是不能绑定这个端口的。

两台电脑之间究竟可以建立多少个 TCP 连接?

在某些操作系统上(Mac OS X,Linux 3.9 和之后的版本,Windows),使用了 SO_REUSEPORT 的情况下(Windows 是 SO_REUSEADDR),使用部分语言,如 Python 或更底层的 C(Windows 上面还可以使用 Java,因为 Java 支持SO_REUSEADDR),一个端口可以建立 65535 个连接,总共可以建立 65535*65535 个 TCP 连接。但在其他情况下,只能建立 65535 个连接。

参考资料:

  1. Stack Overflow: Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ? Do they mean the same across all major operating systems?
  2. The SO_REUSEPORT socket option of Linux 3.9
  3. Java Bug System: No way to access SO_REUSEPORT on sockets
  4. Java docs: StandardSocketOptions
  5. Stack Overflow: Is there a way for multiple processes to share a listening socket?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">