我想这个问题要从计算机网络通信谈起,学过计算机网络的同学,可能都还记得7层或者4层网络模型,TCP/UDP属于其中的传输层协议,在传输层之下是网络层,网络层主要通过IP协议来进行通信,这也是我们日常程序开发中能够接触到的最底层了,再往下的数据链路层和物理层就不是我们这些普通程序员需要关心的了。
图片
IP
我们先具体看下网络层。在IP网路层,发送者向接收者传输数据的时候,首先需要知道接收者的IP地址,IP地址可以在网络中唯一标识一台计算机,然后数据就可以根据IP协议抵达接收者所在的计算机,但是接收者所在的计算机上运行了几十个程序,计算机应该把这个数据交给哪个程序呢?
端口号
这就像快递员到达了一栋大楼,下一步它怎么把快递送到对应的用户手中呢?聪明的你一定想到了,那就是门牌号。
在计算机中,端口号就是门牌号。计算机的操作系统可以为不同的程序绑定不同的端口号,这样发送者发送数据时不仅要设置接收者的IP,还要加上接收者的端口号,如此接收者所在的计算机就能把数据转发给正确的程序了。
TCP/UDP
那么TCP和UDP能不能使用同一个端口号呢?其实在查找端口号之前还有一个传输层协议的处理过程,操作系统收到数据后,会先查看数据包使用的是TCP协议还是UDP协议,然后再根据协议进行不同的解析处理,提取到数据后,再转发到拥有对应端口的程序。
所以TCP和UDP是可以使用相同的端口号的,这在现实中也是常见的。比如 DNS(域名系统)可能需要同时支持 TCP 和 UDP 查询,这两种查询就都可以通过53这个标准端口来进行接收和响应。
但是在同一个传输协议下,端口号就不能相同了。如果相同,操作系统的协议栈就不知道该把这个数据包转给哪个程序了,这种设计会增加很多麻烦。
有的同学可能会观察到一个现象,那就是同一个计算机上的多个网站可以共享80或者443端口,这其实是应用层的能力,这些网站都寄宿在同一个Web服务器程序上,这个Web服务器程序绑定了80端口,Web服务器收到数据后再根据HTTP协议中的主机头(可以理解成域名)转发给不同的网站程序。
还有,如果你的电脑上有多个IP,那就更没有问题了。不同的IP代表不同的网络接口,即使都使用TCP协议,只要IP不同,端口号一样也完全不会冲突。
“IP+传输层协议+端口号”就是我们常说的套接字,它能确保数据从一个网络程序传递到另一个网络程序。大家如果直接使用TCP和UDP编程,就需要手动为套接字设置这几个参数。
示例
口说无凭,再给大家写个demo,使用go语言,简单易懂:
下边的程序会启动一个TCP服务器和一个UDP服务器,它们绑定相同的IP和端口号。这里为了方便测试,使用了127.0.0.1这个本机IP,你也可以换成局域网或者公网IP。
package main
import (
"fmt"
"net"
"os"
)
func main() {
// 定义监听的端口
port := "127.0.0.1:12345"
// 启动TCP服务器
go startTCPServer(port)
// 启动UDP服务器
startUDPServer(port)
}
func startTCPServer(port string) {
// 通过TCP协议监听端口
l, err := net.Listen("tcp", port)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
defer l.Close()
fmt.Println("TCP Server Listening on " + port)
// 持续接收TCP数据
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
os.Exit(1)
}
fmt.Println("Received TCP connection")
conn.Close()
}
}
func startUDPServer(port string) {
// 通过UDP协议监听端口
addr, err := net.ResolveUDPAddr("udp", port)
if err != nil {
fmt.Println("Error resolving: ", err.Error())
os.Exit(1)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("Error listening: ", err.Error())
os.Exit(1)
}
defer conn.Close()
fmt.Println("UDP Server Listening on " + port)
buffer := make([]byte, 1024)
// 持续接收UDP数据
for {
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("Error reading: ", err.Error())
continue
}
fmt.Printf("Received UDP packet: %s\n", string(buffer[:n]))
}
}
然后再创建两个客户端,一个是TCP客户端:
package main
import (
"fmt"
"net"
"os"
)
func main() {
// 连接到服务器
conn, err := net.Dial("tcp", "localhost:12345")
if err != nil {
fmt.Println("Error connecting:", err.Error())
os.Exit(1)
}
defer conn.Close()
// 发送数据
_, err = conn.Write([]byte("Hello TCP Server!"))
if err != nil {
fmt.Println("Error sending data:", err.Error())
return
}
fmt.Println("Message sent to TCP server")
}
另一个是UDP客户端:
package main
import (
"fmt"
"net"
"os"
)
func main() {
ServerAddr, err := net.ResolveUDPAddr("udp", "localhost:12345")
if err != nil {
fmt.Println("Error resolving: ", err.Error())
os.Exit(1)
}
conn, err := net.DialUDP("udp", nil, ServerAddr)
if err != nil {
fmt.Println("Error dialing: ", err.Error())
os.Exit(1)
}
defer conn.Close()
// 发送数据
_, err = conn.Write([]byte("Hello UDP Server!"))
if err != nil {
fmt.Println("Error sending data:", err.Error())
return
}
fmt.Println("Message sent to UDP server")
}
我们可以看到,客户端发起请求的时候都使用了 localhost:12345 这个目标地址,其中的localhost 实际上是个域名,它会被本地计算机解析为 127.0.0.1。这块不清楚的可以看我之前写的这篇:
实际运行效果如下:
图片
最后总结下:在网络通信中,同一台计算机中,TCP和UDP协议可以使用相同的端口号。每个网络进程中的套接字地址都是唯一的,由三元组(IP地址,传输层协议,端口号)标识。操作系统会根据数据包中的传输层协议(TCP或UDP)以及端口号,将接收到的数据正确地交付给相应的应用程序。