3. TCP/IP协议栈回顾与Socket关联
说实话,很多搞了几年网络开发的朋友,被问到「Socket到底是什么」时,还是会愣一下。我面试过不少人,能背出七层模型的不少,但能把Socket和TCP/IP协议栈真正串起来的,真不多。
这一章,咱们就把这些基础概念彻底捋一遍。不是为了背书,是为了后面用Wireshark抓包时,你能一眼看出每一层在干什么。
3.1 OSI模型与TCP/IP模型:别被七层吓住
OSI七层模型,教科书上的标准答案。但实际互联网用的是TCP/IP四层模型。我个人的习惯是,把OSI当作「理论地图」,把TCP/IP当作「实战工具」。
| OSI七层 | TCP/IP四层 | 核心协议举例 | Wireshark中看到的 |
|---|---|---|---|
| 应用层 | 应用层 | HTTP, FTP, DNS | GET /index.html |
| 表示层 | SSL/TLS | Client Hello | |
| 会话层 | SOCKS, RPC | 会话建立 | |
| 传输层 | 传输层 | TCP, UDP | 源端口、目的端口 |
| 网络层 | 网络层 | IP, ICMP | 源IP、目的IP |
| 数据链路层 | 网络接口层 | Ethernet, PPP | MAC地址 |
| 物理层 | RJ45, 光纤 | 比特流 |
我的经验: 抓包分析时,我只看TCP/IP四层。OSI的会话层和表示层在实际TCP/IP协议栈里被合并了。你非要在Wireshark里找「表示层」,只会把自己绕晕。
3.2 Socket定义:IP + Port = 通信的门牌号
Socket到底是什么?说白了,就是IP地址 + 端口号的组合。一个Socket唯一标识了网络上的一台主机上的一个进程。
举个例子:192.168.1.100:443 这个Socket,代表的是IP为192.168.1.100的机器上,那个监听443端口的HTTPS服务进程。
我记得刚入行时,有个同事把Socket理解成「一根网线」,调试了三天没找到bug。后来我告诉他:Socket不是线,是门牌号。数据包到了你家楼下(IP),还得知道敲哪个门(端口),才能找到你这个人(进程)。
核心公式:
Socket = IP地址 + 端口号
一个TCP连接 = (源Socket, 目的Socket) 的四元组
Socket = IP地址 + 端口号
一个TCP连接 = (源Socket, 目的Socket) 的四元组
3.3 TCP与UDP核心区别:可靠 vs 快速
这两个协议的区别,面试必问。但实战中怎么选?我直接给结论:
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(重传、确认) | 不可靠(尽力而为) |
| 顺序 | 保证顺序 | 不保证顺序 |
| 速度 | 较慢(有开销) | 快(无开销) |
| 典型应用 | HTTP, FTP, SMTP | DNS, 视频直播, 游戏 |
避坑指南: 我曾经在一个物联网项目里,用TCP传传感器数据。结果设备多了,服务器连接数暴涨,内存直接爆了。后来换成UDP + 应用层ACK,问题解决。记住:不是所有场景都需要TCP的可靠性,有时候「丢了就丢了」反而是最优解。
3.4 三次握手与四次挥手:连接的生与死
这是TCP最核心的机制。咱们用Wireshark抓包时,最先看到的就是这三个包。
三次握手(建立连接)
- 客户端 → 服务端: SYN(seq=x)—— 「我要连你」
- 服务端 → 客户端: SYN+ACK(seq=y, ack=x+1)—— 「收到,我也连你」
- 客户端 → 服务端: ACK(seq=x+1, ack=y+1)—— 「好,开始传数据」
为什么是三次?不是两次?我简单解释:两次握手的话,服务端无法确认客户端收到了自己的SYN。万一第一个SYN延迟了,客户端重传,服务端就会建立两个连接,浪费资源。
四次挥手(断开连接)
- 主动方 → 被动方: FIN —— 「我发完了」
- 被动方 → 主动方: ACK —— 「知道了,但我还有数据要发」
- 被动方 → 主动方: FIN —— 「我也发完了」
- 主动方 → 被动方: ACK —— 「好,断开」
实战观察: 用Wireshark抓三次握手时,注意看SEQ和ACK号的变化。第一次握手的SEQ是随机数(不是0!),第二次的ACK是第一次SEQ+1。这个规律能帮你快速定位连接问题。
3.5 TCP状态转换图:一张图看懂连接生命周期
下面这张图,是我自己画的状态转换图。你把它存下来,以后抓包分析时对照着看,比翻书快多了。
这张图里,我特意把客户端和服务端分开画。你注意看:
- 三次握手:CLOSED → SYN_SENT → ESTABLISHED(客户端);LISTEN → SYN_RCVD → ESTABLISHED(服务端)
- 四次挥手:ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT(客户端);ESTABLISHED → CLOSE_WAIT → LAST_ACK(服务端)
避坑指南: 我曾经在生产环境遇到过一个问题:服务器上有大量TIME_WAIT状态的连接。原因是客户端频繁短连接,服务端主动关闭后,客户端进入TIME_WAIT,要等2MSL(约2分钟)才能释放端口。解决方案:要么让客户端复用连接,要么调整内核参数
net.ipv4.tcp_tw_reuse。嗯,这个坑我踩过,你注意。
3.6 Socket与TCP状态的关系
Socket编程里,你调用的每个API,背后都对应着TCP状态的变化:
| Socket API | 对应TCP状态变化 | Wireshark中看到的包 |
|---|---|---|
| socket() | CLOSED | 无 |
| bind() | CLOSED(绑定端口) | 无 |
| listen() | CLOSED → LISTEN | 无 |
| connect() | CLOSED → SYN_SENT → ESTABLISHED | SYN → SYN+ACK → ACK |
| accept() | LISTEN → SYN_RCVD → ESTABLISHED | 收到SYN后返回SYN+ACK |
| close() | ESTABLISHED → FIN_WAIT_1 → ... → CLOSED | FIN → ACK → FIN → ACK |
我的习惯: 调试网络问题时,我第一件事就是看
netstat -an | grep TIME_WAIT。如果TIME_WAIT数量超过几千,基本可以断定是连接管理出了问题。这时候用Wireshark抓一下,看看是谁先发的FIN,问题就清楚了。