3. 网络层与传输层:IP地址与子网掩码、TCP三次握手与四次挥手、UDP与TCP的选型对比、端口与Socket编程基础
各位工程师朋友,咱们今天聊点实在的。网络层和传输层,说白了就是数据在设备之间“怎么找路”和“怎么安全送达”的问题。我刚开始做嵌入式通信时,总觉得这些协议离底层硬件很远,直到被一个IP冲突的问题折腾了三天三夜……嗯,从那以后我再也不敢小看这两层了。
3.1 IP地址与子网掩码:设备的名片与门牌号
IP地址就像你家的门牌号,子网掩码就是告诉你“哪些邻居跟你住一个小区”。我个人习惯把子网掩码理解成一把尺子——它量出IP地址里哪些位是网络号,哪些是主机号。
举个例子:192.168.1.100/24,这里的/24就是子网掩码的简写,表示前24位是网络号。换算成十进制就是255.255.255.0。这意味着192.168.1.x这个网段里,所有设备都在同一个局域网。
核心公式:
- 网络地址 = IP地址 & 子网掩码
- 广播地址 = 网络地址 | (~子网掩码)
- 可用主机数 = 2^(32-掩码位数) - 2
我在项目中遇到过一个问题:两台设备IP分别是192.168.1.10和192.168.1.20,子网掩码都是255.255.255.0,按理说能直接通信。但死活ping不通。查了半天,发现其中一台设备的子网掩码被人改成了255.255.0.0。你想想看,一个认为对方在隔壁,一个认为对方在千里之外,这能通才怪。
避坑指南:我曾经在配置PLC网络时,把子网掩码写成了255.255.255.128,结果导致两个工位之间的设备无法互访。后来才意识到,这个掩码把原本的/24网段切成了两个/25子网,设备被分到了不同的广播域。
3.2 TCP三次握手:建立可靠连接的“三声问候”
TCP三次握手,说白了就是客户端和服务端互相确认“你准备好了吗?”“我准备好了!”的过程。我习惯把它比作打电话:
- 第一次握手(SYN):客户端说:“喂,能听到吗?”(发送SYN包,进入SYN_SENT状态)
- 第二次握手(SYN+ACK):服务端回答:“听到了,你能听到我吗?”(发送SYN+ACK包,进入SYN_RCVD状态)
- 第三次握手(ACK):客户端回应:“我也听到了,咱们开始聊吧。”(发送ACK包,双方进入ESTABLISHED状态)
为什么会这样设计?其实是为了防止“过期连接请求”造成混乱。我记得有一次调试一个工业网关,发现设备每隔几分钟就会自动重连一次。抓包一看,原来是服务端收到了一个旧的SYN包,以为客户端要建立新连接。三次握手就是为了避免这种“历史包袱”。
注意:三次握手的核心是“双方都要确认对方的接收能力”。少一次都不行。我曾经见过有人为了省流量,试图用两次握手建立连接,结果数据发出去对方根本没收到——因为服务端不知道客户端是否收到了自己的确认。
3.3 TCP四次挥手:优雅的告别
四次挥手比握手复杂一点,因为TCP连接是全双工的——两边可以同时收发数据。所以断开连接时,每一端都要单独说再见。
- 第一次挥手(FIN):客户端说:“我说完了,准备关。”(发送FIN包,进入FIN_WAIT_1状态)
- 第二次挥手(ACK):服务端回答:“知道了,但我还有话没说完。”(发送ACK包,进入CLOSE_WAIT状态)
- 第三次挥手(FIN):服务端说:“我也说完了,可以关了。”(发送FIN包,进入LAST_ACK状态)
- 第四次挥手(ACK):客户端回应:“好的,再见。”(发送ACK包,进入TIME_WAIT状态,等待2MSL后关闭)
这里有个坑:TIME_WAIT状态。我曾经在服务器上看到大量TIME_WAIT连接,导致端口被占满,新连接无法建立。原因是客户端频繁短连接,每次断开后都要等2MSL(约1-4分钟)才能释放端口。解决方案?要么用长连接,要么调整tcp_tw_reuse参数。
关键点:四次挥手之所以是四次,是因为服务端可能还有数据要发送。如果服务端在收到FIN时已经没有数据要发了,可以把第二次和第三次合并,变成三次挥手。但标准实现还是四次。
3.4 UDP与TCP的选型对比:什么时候该用谁?
这个问题我几乎每次培训都会被问到。直接上对比表:
| 特性 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接 | 无连接 |
| 可靠性 | 可靠(重传、排序、流量控制) | 不可靠(尽力而为) |
| 速度 | 较慢(有握手和确认开销) | 快(无握手,直接发) |
| 数据边界 | 字节流,无边界 | 报文,有边界 |
| 典型应用 | HTTP、FTP、SSH、MQTT | DNS、DHCP、视频流、游戏 |
我个人习惯这样选型:
- 必须用TCP的场景:文件传输、远程控制、数据库操作。丢一个字节都不行。
- 必须用UDP的场景:实时音视频、传感器数据广播、DNS查询。丢几个包无所谓,但延迟必须低。
- 两可的场景:物联网设备上报数据。如果数据量小、频率低,TCP更稳妥;如果数据量大、实时性要求高,UDP+应用层重传更合适。
我的经验:在工业现场总线中,我见过有人用UDP做Modbus TCP的替代方案。结果因为丢包导致设备状态不同步,差点酿成事故。后来我建议他们改用TCP,虽然延迟高了20ms,但可靠性从99%提升到了99.99%。记住:工业场景,可靠性优先。
3.5 端口与Socket编程基础:应用层的“门牌号”
端口号是传输层用来区分不同应用程序的标识。IP地址找到了设备,端口号找到了设备上的具体应用。就像一栋楼有门牌号,每个房间还有房间号。
端口号范围:
- 0-1023:知名端口,如HTTP(80)、HTTPS(443)、FTP(21)
- 1024-49151:注册端口,如MySQL(3306)、Redis(6379)
- 49152-65535:动态/私有端口,客户端临时使用
Socket编程,说白了就是操作系统提供的一套API,让应用程序能通过IP+端口收发数据。我最早接触Socket是在一个嵌入式Linux项目上,用C语言写了一个简单的TCP服务器:
// 简单的TCP服务器示例(C语言)
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr*)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
char buffer[1024] = {0};
read(client_fd, buffer, 1024);
printf("收到: %s\n", buffer);
send(client_fd, "Hello from server", 17, 0);
close(client_fd);
close(server_fd);
这段代码虽然简单,但包含了Socket编程的核心流程:创建→绑定→监听→接受→读写→关闭。我在实际项目中,经常需要处理非阻塞IO、多线程并发、超时重连等问题。嗯,这些咱们后面章节再细聊。
注意:端口冲突是个常见问题。我曾经在调试时,发现服务端启动失败,报“Address already in use”。原因是上一个进程没有正常关闭,端口还处于TIME_WAIT状态。解决方案是设置SO_REUSEADDR选项,允许重用端口。
3.6 本章知识体系总览
下面这张图是我自己画的,把本章的核心逻辑串起来了。你一看就明白:
这张图把网络层和传输层的关系、TCP连接管理、以及UDP/TCP的选型逻辑都串起来了。你保存下来,以后做方案设计时拿出来对照一下,思路会清晰很多。
好了,这一章的内容就到这里。网络层和传输层是通信协议的“骨架”,理解透了,后面应用层的协议(HTTP、MQTT、Modbus等)学起来就轻松多了。下一章咱们聊聊应用层的那些事儿。