TCP 和 UDP 可以使用同一个端口吗?( 二 )


文章插图
 
我上面的测试案例是两个 TCP 服务进程同时绑定地址和端口是:0.0.0.0 地址和8888端口,所以才出现的错误 。
如果两个 TCP 服务进程绑定的 IP 地址不同,而端口相同的话,也是可以绑定成功的,如下图:

TCP 和 UDP 可以使用同一个端口吗?

文章插图
 
所以,默认情况下,针对「多个 TCP 服务进程可以绑定同一个端口吗?」这个问题的答案是:如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同,那么执行 bind() 时候就会出错,错误是“Address already in use” 。
注意,如果 TCP 服务进程 A 绑定的地址是 0.0.0.0 和端口 8888,而如果 TCP 服务进程 B 绑定的地址是 192.168.1.100 地址(或者其他地址)和端口 8888,那么执行 bind() 时候也会出错 。
这是因为 0.0.0.0 地址比较特殊,代表任意地址,意味着绑定了 0.0.0.0 地址,相当于把主机上的所有 IP 地址都绑定了 。
 
重启 TCP 服务进程时,为什么会有“Address in use”的报错信息?
 
TCP 服务进程需要绑定一个 IP 地址和一个端口,然后就监听在这个地址和端口上,等待客户端连接的到来 。
然后在实践中,我们可能会经常碰到一个问题,当 TCP 服务进程重启之后,总是碰到“Address in use”的报错信息,TCP 服务进程不能很快地重启,而是要过一会才能重启成功 。
这是为什么呢?
当我们重启 TCP 服务进程的时候,意味着通过服务器端发起了关闭连接操作,于是就会经过四次挥手,而对于主动关闭方,会在 TIME_WAIT 这个状态里停留一段时间,这个时间大约为 2MSL 。
【TCP 和 UDP 可以使用同一个端口吗?】
TCP 和 UDP 可以使用同一个端口吗?

文章插图
 
当 TCP 服务进程重启时,服务端会出现 TIME_WAIT 状态的连接,TIME_WAIT 状态的连接使用的 IP+PORT 仍然被认为是一个有效的 IP+PORT 组合,相同机器上不能够在该 IP+PORT 组合上进行绑定,那么执行 bind() 函数的时候,就会返回了 Address already in use 的错误 。
而等 TIME_WAIT 状态的连接结束后,重启 TCP 服务进程就能成功 。
 
重启 TCP 服务进程时,如何避免“Address in use”的报错信息?
 
我们可以在调用 bind 前,对 socket 设置 SO_REUSEADDR 属性,可以解决这个问题 。
int on = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
因为 SO_REUSEADDR 作用是:如果当前启动进程绑定的 IP+PORT 与处于TIME_WAIT 状态的连接占用的 IP+PORT 存在冲突,但是新启动的进程使用了 SO_REUSEADDR 选项,那么该进程就可以绑定成功 。
举个例子,服务端有个监听 0.0.0.0 地址和 8888 端口的 TCP 服务进程 。‍
有个客户端(IP地址:192.168.1.100)已经和服务端(IP 地址:172.19.11.200)建立了 TCP 连接,那么在 TCP 服务进程重启时,服务端会与客户端经历四次挥手,服务端的 TCP 连接会短暂处于 TIME_WAIT 状态:
客户端地址:端口 服务端地址:端口 TCP 连接状态 192.168.1.100:37272 172.19.11.200:8888 TIME_WAIT
如果 TCP 服务进程没有对 socket 设置 SO_REUSEADDR 属性,那么在重启时,由于存在一个和绑定 IP+PORT 一样的 TIME_WAIT 状态的连接,那么在执行 bind() 函数的时候,就会返回了 Address already in use 的错误 。
如果 TCP 服务进程对 socket 设置 SO_REUSEADDR 属性了,那么在重启时,即使存在一个和绑定 IP+PORT一样的 TIME_WAIT 状态的连接,依然可以正常绑定成功,因此可以正常重启成功 。
因此,在所有 TCP 服务器程序中,调用 bind 之前最好对 socket 设置 SO_REUSEADDR 属性,这不会产生危害,相反,它会帮助我们在很快时间内重启服务端程序 。‍
前面我提到过这个问题:如果 TCP 服务进程 A 绑定的地址是 0.0.0.0 和端口 8888,而如果 TCP 服务进程 B 绑定的地址是 192.168.1.100 地址(或者其他地址)和端口 8888,那么执行 bind() 时候也会出错 。
这个问题也可以由 SO_REUSEADDR 解决,因为它的另外一个作用是:绑定的 IP地址 + 端口时,只要 IP 地址不是正好(exactly)相同,那么允许绑定 。
比如,0.0.0.0:8888 和192.168.1.100:8888,虽然逻辑意义上前者包含了后者,但是 0.0.0.0 泛指所有本地 IP,而 192.168.0.100 特指某一IP,两者并不是完全相同,所以在对 socket 设置 SO_REUSEADDR 属性后,那么执行 bind() 时候就会绑定成功 。


推荐阅读