2. 端口与Socket:端口号的概念与分类

聊到传输层,有个概念你绕不开——端口号。说白了,端口就是给应用程序用的“门牌号”。

我刚开始学网络时,总觉得IP地址就够了,为什么还要端口?后来在项目中调试一个Web服务器,两台机器明明能ping通,浏览器就是打不开页面。查了半天,发现是防火墙把80端口给封了。嗯,从那以后我彻底记住了:IP地址找机器,端口号找进程

2.1 端口号的概念

端口号是一个16位的整数,范围从0到65535。它用来标识主机上的一个特定进程或服务。

你想想看,一台服务器上同时跑着Web服务、邮件服务、数据库服务。数据包到了服务器,内核怎么知道该交给谁?靠的就是端口号。

核心要点:

  • IP地址 + 端口号 = 唯一的通信端点
  • 一个端口同一时间只能被一个进程占用
  • 端口号是传输层(TCP/UDP)的概念

2.2 端口号的分类

IANA(互联网数字分配机构)把端口号分成了三大类。我习惯用一张表来记:

分类 范围 特点 常见例子
知名端口 0 - 1023 系统预留,需要root权限才能使用 HTTP(80), HTTPS(443), SSH(22)
注册端口 1024 - 49151 IANA登记,但普通用户可用 MySQL(3306), Redis(6379)
动态端口 49152 - 65535 临时分配,客户端常用 浏览器随机分配的端口

2.3 知名端口(Well-Known Ports)

这些端口是“老前辈”,从互联网早期就定下来了。我个人习惯把它们分成几类记:

  • Web服务:80(HTTP)、443(HTTPS)
  • 远程管理:22(SSH)、23(Telnet)
  • 邮件服务:25(SMTP)、110(POP3)、143(IMAP)
  • 文件传输:20/21(FTP)
  • DNS:53(域名解析)

避坑指南:我曾经在部署一个内部工具时,随手用了80端口。结果和系统自带的Apache冲突了,服务死活起不来。后来查日志才发现端口被占用了。所以,生产环境用知名端口前,一定先检查端口是否被占用

2.4 注册端口(Registered Ports)

这些端口是给第三方应用用的。比如:

  • 3306:MySQL数据库
  • 6379:Redis缓存
  • 8080:HTTP备用端口(Tomcat常用)
  • 27017:MongoDB

我建议你在开发时,尽量避开这些端口。万一你用了3306,后来要装MySQL,那就尴尬了。

2.5 动态端口(Dynamic Ports)

这个范围是给客户端用的。你每次打开浏览器访问网站,操作系统就会从49152-65535里随机挑一个端口给你用。

为什么客户端要用动态端口?因为客户端不需要固定端口,用完就释放了。你想想看,如果每个浏览器标签页都用固定端口,那端口早就不够用了。

2.6 Socket编程模型

Socket,中文叫“套接字”。它其实是操作系统提供的一个编程接口,让应用程序能通过网络收发数据。

我刚开始学Socket编程时,觉得它很神秘。后来发现,Socket本质上就是一个文件描述符。你读写Socket,就像读写文件一样。

2.6.1 核心API

以TCP Socket为例,核心函数就这几个:

// 服务端
socket()    // 创建Socket
bind()      // 绑定端口和IP
listen()    // 监听连接
accept()    // 接受客户端连接
recv()      // 接收数据
send()      // 发送数据
close()     // 关闭连接

// 客户端
socket()    // 创建Socket
connect()   // 连接服务器
send()      // 发送数据
recv()      // 接收数据
close()     // 关闭连接

个人经验:我写Socket程序时,习惯在每次调用后检查返回值。很多新手忽略了这个,结果程序莫名其妙崩溃。记住:网络编程,错误处理比业务逻辑更重要

2.7 客户端与服务器通信流程

TCP通信的流程,说白了就是“三次握手、四次挥手”。但落实到代码层面,流程是这样的:

2.7.1 服务端流程

  1. 创建Socket:调用socket(),指定协议族(AF_INET)和类型(SOCK_STREAM)
  2. 绑定地址:调用bind(),绑定IP和端口。IP可以用INADDR_ANY表示监听所有网卡
  3. 开始监听:调用listen(),指定最大连接数(backlog)
  4. 接受连接:调用accept(),阻塞等待客户端连接
  5. 收发数据:用recv()和send()和客户端通信
  6. 关闭连接:调用close()释放资源

2.7.2 客户端流程

  1. 创建Socket:同样调用socket()
  2. 连接服务器:调用connect(),指定服务器的IP和端口
  3. 收发数据:用send()和recv()和服务端通信
  4. 关闭连接:调用close()

一个完整的TCP通信示例(伪代码):

// 服务端
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(server_fd, &addr, sizeof(addr));
listen(server_fd, 5);

while(1) {
    int client_fd = accept(server_fd, NULL, NULL);
    char buf[1024] = {0};
    recv(client_fd, buf, sizeof(buf), 0);
    printf("收到: %s\n", buf);
    send(client_fd, "OK", 2, 0);
    close(client_fd);
}

// 客户端
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
connect(client_fd, &server_addr, sizeof(server_addr));
send(client_fd, "Hello", 5, 0);
char buf[1024] = {0};
recv(client_fd, buf, sizeof(buf), 0);
printf("服务器回复: %s\n", buf);
close(client_fd);

2.8 常见问题与避坑

  • 端口被占用:用netstat -anp | grep 端口号 检查
  • 地址已在使用:设置SO_REUSEADDR选项,允许重用TIME_WAIT状态的端口
  • 连接超时:检查防火墙是否放行了对应端口
  • 端口范围限制:Linux下可以通过/proc/sys/net/ipv4/ip_local_port_range调整动态端口范围

我曾经踩过的坑:有一次写一个高并发服务器,客户端连接后频繁断开重连。结果发现端口全被占满了,新连接连不上。后来查资料才知道,TCP的TIME_WAIT状态会导致端口在一段时间内不可用。解决方案是开启SO_REUSEADDR,或者调整TIME_WAIT的超时时间。

2.9 小结

端口号和Socket是传输层编程的基础。记住三点:

  1. 端口号分三类:知名(0-1023)、注册(1024-49151)、动态(49152-65535)
  2. Socket是网络编程的基石,本质是文件描述符
  3. TCP通信流程:服务端(创建→绑定→监听→接受→收发→关闭),客户端(创建→连接→收发→关闭)

下一章,我们会深入TCP的报文格式和三次握手细节。到时候你会发现,理解了端口和Socket,后面的内容就顺理成章了。