关于网络连接
约 3041 字大约 10 分钟
2025-05-16
关于端口
关于端口的知识点,还是挺多可以讲的,比如还可以牵扯到这几个问题:
- 多个 TCP 服务进程可以同时绑定同一个端口吗?
- 客户端的端口可以重复使用吗?
- 客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?
TCP 和 UDP 可以监听相同的端口吗?
其实这个问题「TCP 和 UDP 可以同时监听相同的端口吗?」表述有问题,这个问题应该表述成「TCP 和 UDP 可以 同时绑定
相同的端口吗?」
因为「监听」这个动作是在 TCP 服务端网络编程中才具有的,而 UDP 服务端网络编程中是没有「监听」这个动作的。
TCP 和 UDP 服务端网络相似的一个地方,就是会调用 bind 绑定端口。
给大家贴一下 TCP 和 UDP 网络编程的区别就知道了。
TCP 网络编程如下,服务端执行 listen() 系统调用就是监听端口的动作。

UDP 网络编程如下,服务端是没有监听这个动作的,只有执行 bind() 系统调用来绑定端口的动作。

那么, TCP与UDP可以同时绑定相同的端口吗?
在数据链路层中,通过 MAC 地址来寻找局域网中的主机。在网际层中,通过 IP 地址来寻找网络中互连的主机或路由器。在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序。
所以,传输层的 「端口号」
的作用,是为了区分同一个主机上不同应用程序的数据包。
传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块。
当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。

可以看到,数据的传输是 一个五元组(源地址,目标地址,源端口号,目标端口号,数据传输协议),所以说可以同时监听一个端口号
。
因此, TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突。
结论验证
基于golang实现 TCP 与 UDP Server, 同时绑定在 127.0.0.1:9999
package main
import (
"bufio"
"fmt"
"net"
"os"
)
var address = "127.0.0.1:9999"
// TCP Server端测试
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err: ", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到Client端发来的数据:", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}
func tcpServer() {
listen, err := net.Listen("tcp", address)
if err != nil {
fmt.Println("Listen() failed, err: ", err)
return
}
for {
conn, err := listen.Accept() // 监听客户端的连接请求
if err != nil {
fmt.Println("Accept() failed, err: ", err)
continue
}
go process(conn) // 启动一个goroutine来处理客户端的连接请求
}
}
// UDP goroutine 实现并发读取UDP数据
func udpProcess(conn *net.UDPConn) {
// 最大读取数据大小
data := make([]byte, 1024)
n, _, err := conn.ReadFromUDP(data)
if err != nil {
fmt.Println("failed read udp msg, error: " + err.Error())
}
str := string(data[:n])
fmt.Println("receive from client, data:" + str)
<-limitChan
}
// 限制goroutine数量
var limitChan = make(chan bool, 1000)
func udpServer() {
udpAddr, err := net.ResolveUDPAddr("udp", address)
if nil != err {
panic(err.Error())
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
fmt.Println("read from connect failed, err:" + err.Error())
os.Exit(1)
}
defer conn.Close()
for {
limitChan <- true
go udpProcess(conn)
}
}
func main() {
go tcpServer()
udpServer()
}
运行服务后, 查看端口绑定情况, 会看到如下信息
> lsof -i:9999
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
___go_bui 36357 foobar 3u IPv4 0x1b9922b15ce716a8 0t0 UDP localhost:distinct
___go_bui 36357 foobar 4u IPv4 0xf04bc32279291021 0t0 TCP localhost:distinct (LISTEN)
> netstat -na | grep 9999
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 127.0.0.1.9999 *.* LISTEN
udp4 0 0 127.0.0.1.9999 *.*
多个 TCP 服务进程可以绑定同一个端口吗?
还是以上面的tcp实现为例(朱一移除tcp示例), 启动一个server之后, 在启动一个server, 会发现端口被占用, 报错如下:
Listen() failed, err: listen tcp 127.0.0.1:9999: bind: address already in use
上面的测试案例是两个 TCP 服务进程同时绑定地址和端口是:0.0.0.0 地址和9999端口,所以才出现的错误。
如果两个 TCP 服务进程绑定的 IP 地址不同,而端口相同的话,也是可以绑定成功的,如下图:
❯ netstat -na | grep 9999
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 10.33.202.35.9999 *.* LISTEN
tcp4 0 0 127.0.0.1.9999 *.* LISTEN
所以,默认情况下,针对「多个 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 地址都绑定了。
客户端的端口可以重复使用吗?
与服务端概念类似, 客户端在执行 connect 函数
的时候,会在内核里 随机选择一个端口
,然后向服务端发起 SYN 报文,然后与服务端进行 三次握手
, 三次握手流程参见:三次握手。
客户端的端口选择的发生在 connect 函数,内核在选择端口的时候,会从 net.ipv4.ip_local_port_range
这个内核参数指定的范围来选取一个端口作为客户端端口。
该参数的默认值是 32768 61000
,意味着端口总可用的数量是 61000 - 32768 = 28232 个。
当客户端与服务端完成 TCP 连接建立后,我们可以通过 netstat 命令查看 TCP 连接。
依旧以上面的代码为例, 启动一个tcp server, 然后启动一个tcp client, 查看端口绑定情况, 会看到如下信息:
> netstat -na | grep 9999
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 127.0.0.1.9999 127.0.0.1.49651 ESTABLISHED
tcp4 0 0 127.0.0.1.49651 127.0.0.1.9999 ESTABLISHED
tcp4 0 0 127.0.0.1.9999 *.* LISTEN
那问题来了,上面客户端已经用了 49651 端口,那么还可以继续使用该端口发起连接吗?
这个问题,很多人都会说不可以继续使用该端口了,如果按这个理解的话, 默认情况下客户端可以选择的端口是 28232 个,那么意味着客户端只能最多建立 28232 个 TCP 连接,如果真是这样的话,那么这个客户端并发连接也太少了吧,所以这是错误理解。
正确的理解:
TCP 连接是由 四元组(源IP地址,源端口,目的IP地址,目的端口)唯一确认的
,那么只要 四元组中其中一个元素发生了变化,那么就表示不同的 TCP 连接的
。所以如果客户端已使用端口 64992 与服务端 A 建立了连接,那么客户端要与服务端 B 建立连接,还是可以使用端口 64992 的,因为内核是通过四元祖信息来定位一个 TCP 连接的,并不会因为客户端的端口号相同,而导致连接冲突的问题。
关于连接的描述
一个链接的描述应该是五元组决定的, 在上述四元组基础之上在增加一个通信协议, 上面之所以说是四元组, 是因为前提已经限定, 基于 TCP 协议的连接, 协议一定相同.
多个客户端可以 bind 同一个端口吗?
此处原理与服务端原理类似.
bind 函数虽然常用于服务端网络编程中,但是它也是用于客户端的。
前面我们知道,客户端是在调用 connect 函数的时候,由内核随机选取一个端口作为连接的端口。
而如果我们想自己指定连接的端口,就可以用 bind 函数来实现:客户端先通过 bind 函数绑定一个端口,然后调用 connect 函数就会跳过端口选择的过程了,转而使用 bind 时确定的端口。
针对这个问题:多个客户端可以 bind 同一个端口吗?
要看多个客户端绑定的 IP + PORT 是否都相同,如果都是相同的,那么在执行 bind() 时候就会出错,错误是“Address already in use”。
如果一个绑定在 192.168.1.100:6666,一个绑定在 192.168.1.200:6666,因为 IP 不相同,所以执行 bind() 的时候,能正常绑定。
所以, 如果多个客户端同时绑定的 IP 地址和端口都是相同的,那么执行 bind() 时候就会出错,错误是“Address already in use”。
一般而言,客户端不建议使用 bind 函数,应该交由 connect 函数来选择端口会比较好,因为客户端的端口通常都没什么意义。
客户端 TCP 连接 TIME_WAIT 状态过多
客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?
针对这个问题要看,客户端是否都是与同一个服务器(目标地址和目标端口一样)建立连接。
如果客户端都是与同一个服务器(目标地址和目标端口一样)建立连接,那么如果客户端 TIME_WAIT 状态的连接过多,当端口资源被耗尽,就无法与这个服务器再建立连接了。
但是,因为只要客户端连接的服务器不同,端口资源可以重复使用的。
所以,如果客户端都是与不同的服务器建立连接,即使客户端端口资源只有几万个, 客户端发起百万级连接也是没问题的(当然这个过程还会受限于其他资源,比如文件描述符、内存、CPU 等)。
如何解决客户端 TCP 连接 TIME_WAIT 过多
如何解决客户端 TCP 连接 TIME_WAIT 过多,导致无法与同一个服务器建立连接的问题?
前面我们提到,如果客户端都是与同一个服务器(目标地址和目标端口一样)建立连接,那么如果客户端 TIME_WAIT 状态的连接过多,当端口资源被耗尽,就无法与这个服务器再建立连接了。
针对这个问题,也是有解决办法的,那就是打开 net.ipv4.tcp_tw_reuse
这个内核参数。
因为开启了这个内核参数后,客户端调用 connect 函数时,如果选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态,如果该 连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接
,然后就可以正常使用该端口了。
提醒
再次提醒一次,开启了 net.ipv4.tcp_tw_reuse 内核参数,是客户端(连接发起方) 在调用 connect() 函数时才起作用,所以在服务端开启这个参数是没有效果的。
客户端端口选择的流程总结

总结
- 一个连接是有五元组决定的: 协议、源IP地址,源端口,目的IP地址,目的端口
- TIME_WAIT 过多, 未修改内核配置的情况下, 客户端与服务端建立连接会失败
- TIME_WAIT 过多, 已修改内核配置(net.ipv4.tcp_tw_reuse)的情况下, 并且TIME_WAIT超过1s, 客户端会复用连接