网络编程(六):端口那些事儿

网络编程(六):端口那些事儿

auxtenauxten

TCP和UDP协议都存在一个叫做端口的东西,但端口却不是IP协议的一部分。

端口被设计出来主要是为了给协议栈和应用对应:

  • 协议栈用端口号将数据分配给不同的应用层程序
  • 应用层程序用端口号去区分不同的连接,参见之前提到过的“四元组”

TCP和UDP协议都使用了端口号(Port number)的概念来标识发送方和接收方的应用层。 对每个TCP连接的一端都有一个相关的16位的无符号端口号分配给它们。 即使是UDP这种没有连接的协议,依旧有一个16位的无符号端口号。 可能的、被正式承认的端口号有 2^16 -1 = 65535 个。

三类端口

端口被分为三类:著名端口、监听端口和动态端口。

  • 著名端口是由因特网赋号管理局(IANA)来分配的,并且通常被用于系统进程。 IANA对于端口号的分配见这里 Service Name and Transport Protocol Port Number Registry 。 系统的/etc/services也有相应端口和服务名的对应,主要是用来给netstat、nmap 等系统命令做端口名反解用。

    著名的应用程序作为服务器程序来运行,并侦听经常使用这些端口的连接。 这些端口的一个显著特征就是限定在0~1023,并且在Linux、UNIX平台均需要 Root权限才能监听这些端口。

    在UNIX刚刚兴起的年代,服务器资源是十分稀缺的, 通常一台服务器上会有很多的用户,同时这台服务器往往还兼任一个学院、公司的邮件、 网站等服务。为了保证这些服务的端口不被普通用户占用, 当时UNIX的设计者就把使用这些端口的权限限制在系统管理员(Root)手里。

    常见的`著名端口`有:FTP:21、SSH:22、SMTP:25、HTTP:80、HTTPS:443等。
    
  • 监听端口通常被用来运行各种用户自己写的服务,服务监听在这些端口下不需要特别的权限。

    • BSD使用的监听端口范围是1024到4999。
    • IANA建议49152至65535作为“监听端口”。
    • 许多Linux内核使用32768至61000范围。 配置文件 /proc/sys/net/ipv4/ip_local_port_range 有当前系统设定。

  • 动态端口通常被用来在主动发起连接时随机分配使用,在任何特定的TCP连接外不具有任何意义。 这是由于TCP等协议是通过四元组来区分不同的网络连接。 当本机主动发起TCP连接的时候如果目的IP、目的端口、本地IP都是一样的, 只能通过占用不同的本地端口来区分不同的连接。

    0~65535除去上述著名端口、监听端口两种端口号,剩下的端口都是备用的动态端口。 所以在某些特殊用途的需要主动发起大量连接的服务器上(例如:爬虫、代理), 需要调整 /proc/sys/net/ipv4/ip_local_port_range 的数值,来保留更多的 动态端口以供使用。

0号端口

端口号里有一个极为特殊的端口,各种文档书籍中都鲜有记载,就是0号端口。

在IANA官方的标准里0号端口是保留端口。

也就是说无论是TCP还是UDP网络通信,0号端口都是不能使用的。

然而,标准归标准,在UNIX/Linux网络编程中0号端口被赋予了特殊的涵义:

如果在bind绑定的时候指定端口0,意味着由系统随机选择一个可用端口来绑定。

用Python实现一个获取可用监听端口的示例:

def findFreePort():
  """
  函数返回值是当前可用来监听的一个随机端口。
  """
  import socket
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.bind(('localhost', 0))
  # 用getsockname来获取我们实际绑定的端口号
  addr, port = s.getsockname()
  # 释放端口
  s.close()
  return port

网络地址转换NAT

既然说到了端口,不得不提一下NAT。

NAT是"Network Address Translation"的缩写,直译就是网络地址转换。 1990年代中期,为了应对IPv4地址短缺,NAT技术流行起来。

WikiPedia的解释为:

在一个典型的配置中,一个本地网络使用一个专有网络的指定子网 (比如192.168.x.x或10.x.x.x)和连在这个网络上的一个路由器。 这个路由器占有这个网络地址空间的一个专有地址(比如192.168.0.1), 同时它还通过一个或多个因特网服务提供商提供的公有的IP地址(叫做“过载”NAT) 连接到因特网上。当信息由本地网络向因特网传递时,源地址被立即从专有地址转换为公用地址。 由路由器跟踪每个连接上的基本数据,主要是目的地址和端口。当有回复返回路由器时, 它通过输出阶段记录的连接跟踪数据来决定该转发给内部网的哪个主机; 如果有多个公用地址可用,当数据包返回时,TCP或UDP客户机的端口号可以用来分解数据包。 对于因特网上的一个系统,路由器本身充当通信的源和目的地址。

这个技术能够被广泛使用还要感谢当时端口号的记录字段是2Bytes而不是1Byte。

NAT技术的广泛应用也给很多应用带来了极大的麻烦: 处于NAT网络环境内的服务器很难被外部的网络程序主动连接,受这一点伤害最大的莫过于: 点对点视频、语音、文件传输类的程序。

当然我们聪明的工程师经过长时间的努力,发明了“NAT打洞”技术,一定程度上解决了此类问题。

如果没有他们的努力,我们现在各种QQ视频、微信实时语音、网络电话都是需要用户连接到 服务商的服务器上进行数据传输。这样对服务商的网络消耗将是十分巨大的, 服务质量也是很难以提高的,具体的技术实现,我们以后再表。

多进程端口监听

我们都有一个计算机网络的常识:不同的进程不能使用同一端口。

如果一个端口正在被使用,无论是TIME_WAIT、CLOSE_WAIT、还是ESTABLISHED状态。 这个端口都不能被复用,这里面自然也是包括不能被用来LISTEN(监听)。

但这件事也不是绝对的,之前跟大家讲进程的创建过程提到过一件事: 当进程调用fork(2)系统调用的时候,会发生一系列资源的复制,其中就包括句柄。 所以,在调用fork(2)之前,打开任何文件,监听端口产生的句柄也将会被复制。

通过这种方式,我们就可以达成"多进程端口监听"。

但,这又有什么用呢?

我们大名鼎鼎的Nginx就是通过这种手法让多个进程同时监听在HTTP的服务端口上的, 这么做的好处就在于,当外部请求到达,Linux内核会保证多个进程只会有一个accept(2) 成功,这种情况下此端口的服务可用性就和单个进程存在与否无关。 Nginx正是利用这一点达成“不停服务reload、restart”的。

SO_REUSEADDR

要说SO_REUSEADDR,我们需要先需要说一段历史: 记得大学的时候面试我们学校的“星辰工作室”,有一个问题就是

为什么有时候重启Apache会失败,报“Address already in use”?

当时答得不太好,不太明白这个问题的关键点在哪里,后来逐渐明白了。

TCP的原理会导致这样的一个结果:

主动close socket的一方会进入TIME_WAIT,这个状况持续的时间取决于三件事:

  • TCP关闭连接的五次挥手包什么时候到达
  • SO_LINGER的设置
  • /proc/sys/net/ipv4/tcp_tw_recycle 和 /proc/sys/net/ipv4/tcp_tw_reuse 的设置

总之默认情况下,处于TIME_WAIT状态的端口是不能用来LISTEN的。 这就导致,Apache重启时产生80端口TIME_WAIT,进而导致Apache再次尝试LISTEN失败。

在很多开源代码里我们会看到如下代码:

int reuseaddr = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(int));

有了上面这段神奇的代码,就不会出现上面的惨剧。但SO_REUSEADDR的作用不仅限于上述

Linux 的 SO_REUSEADDR 设置为 1 有四种效果:

  1. 当端口处在TIME_WAIT时候,可以复用监听。

  2. 可以允许多个进程监听同一端口,但是必须不同IP。

    这里说的比较隐晦,如果进程A监听0.0.0.0:80,B进程可以成功监听127.0.0.1:80, 顺序反过来也是可以的。

  3. 允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的IP地址不同。

  4. 使用UDP时候,可以允许多个实例或者单进程同时监听同个端口同个IP。



服务端网络编程交流群:348904670

公众号:


# 得到书面授权之前,拒绝任何形式的转载

「真诚赞赏,手留余香」
还没有人赞赏,快来当第一个赞赏的人吧!
文章被以下专栏收录
16 条评论