Socket基础回顾:类型、API与TCP握手挥手
说实话,很多做网络编程的朋友,上来就撸代码,结果遇到奇奇怪怪的连接问题,查半天才发现是Socket类型选错了。我个人习惯,每次开始一个新项目,都会先花几分钟把Socket基础过一遍。这就像盖房子打地基,省不了。
这一章,我们就把Socket的类型、核心API,以及TCP的三次握手和四次挥手彻底理清楚。嗯,这些概念你肯定听过,但咱们得从实战角度重新理解一遍。
1. Socket类型:选对“门”才能通信
Socket说白了就是网络通信的“门”。不同的门,有不同的用法。我见过不少新手,用TCP的Socket去发广播数据,结果死活发不出去——这就是门选错了。
常见的Socket类型就三种:
| 类型 | 名称 | 特点 | 典型场景 |
|---|---|---|---|
| SOCK_STREAM | 流式Socket | 面向连接、可靠、有序、无边界 | HTTP、FTP、SSH |
| SOCK_DGRAM | 数据报Socket | 无连接、不可靠、有边界、速度快 | DNS查询、视频直播、游戏UDP |
| SOCK_RAW | 原始Socket | 直接操作IP层,可自定义协议头 | ping、traceroute、网络嗅探 |
SOCK_STREAM(TCP)。因为复用场景通常需要可靠连接,比如HTTP反向代理、SSH隧道。UDP的端口复用虽然也能做,但处理丢包和乱序会让你怀疑人生。
2. 核心API:五步走,建立连接
TCP服务端的Socket编程,说白了就是五步走:socket() → bind() → listen() → accept() → 收发数据。客户端更简单:socket() → connect() → 收发数据。
咱们一个一个看,每个函数都有坑。
2.1 socket() — 创建“门”
这个函数就是告诉系统:我要一个什么类型的门。
# Python示例
import socket
# 创建一个IPv4的TCP Socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
参数很简单:AF_INET 表示IPv4,SOCK_STREAM 表示TCP。如果你用IPv6,就换成 AF_INET6。
SO_REUSEADDR,程序崩溃重启时会报“Address already in use”。尤其是做端口复用,这个选项几乎是必设的。后面章节我会详细讲。
2.2 bind() — 给门挂上门牌号
创建了门,得告诉系统这个门在哪个地址和端口上。这就是 bind() 干的事。
# 绑定到本机所有IP的8080端口
server_address = ('0.0.0.0', 8080)
sock.bind(server_address)
0.0.0.0 表示监听所有网络接口。如果你只想让内网访问,可以绑定 127.0.0.1 或者具体的局域网IP。
2.3 listen() — 开门迎客,但得排队
listen() 把Socket从“主动”模式切换到“被动”模式。说白了,就是告诉系统:我开始监听这个端口了,有人来连接就让他们排队。
# 开始监听,最大等待队列长度为5
sock.listen(5)
这个参数 5 是“已完成连接但还未被 accept 取走”的队列长度。不是最大连接数!我见过有人设成1,结果并发一上来就丢连接。
2.4 accept() — 从队列里接客人
当有客户端连接进来,系统会把它放在队列里。accept() 就是从队列里取出一个连接,并返回一个新的Socket专门和这个客户端通信。
# 阻塞等待客户端连接
client_sock, client_addr = sock.accept()
print(f"收到来自 {client_addr} 的连接")
注意:accept() 返回的是 新的Socket,原来的监听Socket还在继续监听。这就是并发的基础。
2.5 connect() — 客户端主动敲门
客户端这边,用 connect() 去连接服务端。
# 客户端连接服务端
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.1.100', 8080))
如果服务端没在监听,或者端口不对,connect() 会抛 ConnectionRefusedError。嗯,这个错误你应该不陌生。
3. TCP三次握手:为什么是三次?
你想想看,TCP要保证可靠传输,首先得确认双方都能收发数据。三次握手就是为了达成这个共识。
我用一个生活化的例子:
- 第一次握手(SYN): 客户端喊:“喂,你在吗?”(我想和你建立连接)
- 第二次握手(SYN+ACK): 服务端回:“在呢,我听到你了。你能听到我吗?”(我准备好了,你那边呢?)
- 第三次握手(ACK): 客户端回:“我也能听到你。咱们开始聊吧!”(确认双方都在)
为什么不是两次?因为如果只有两次,服务端无法确认客户端是否收到了自己的回应。万一客户端没收到,服务端还傻等着,就浪费资源了。
4. TCP四次挥手:好聚好散
断开连接比建立连接复杂一点,因为TCP是“全双工”的,两边可以同时收发数据。所以断开时,每一端都要单独关闭自己的发送通道。
流程是这样的:
- 第一次挥手(FIN): 客户端说:“我这边没话说了,准备关了。”
- 第二次挥手(ACK): 服务端回:“收到,我知道你关了。但我还有话没说完。”
- 第三次挥手(FIN): 服务端也说:“我也说完了,关吧。”
- 第四次挥手(ACK): 客户端回:“收到,确认关闭。”
这里有个关键点:从第一次挥手到第四次挥手之间,服务端可能还在发送数据。这就是“半关闭”状态(FIN_WAIT_2)。
FIN,连接就一直挂着。这就是“孤儿连接”。如果不设置 TCP_KEEPALIVE,这些连接会越积越多,最终耗尽系统资源。做端口复用时,一定要考虑心跳机制。
5. 核心流程总览
下面这张图,把整个Socket生命周期串起来了。我建议你多看几遍,把每个阶段和上面的API对应起来。
6. 状态变化:从CLOSED到CLOSE_WAIT
每次握手和挥手,Socket都会经历一系列状态变化。理解这些状态,对排查网络问题特别有帮助。
| 状态 | 含义 | 出现时机 |
|---|---|---|
| CLOSED | 初始状态,没有连接 | Socket刚创建时 |
| LISTEN | 服务端正在监听 | 调用 listen() 后 |
| SYN_SENT | 客户端已发送SYN | 调用 connect() 后 |
| SYN_RCVD | 服务端收到SYN | 收到第一次握手后 |
| ESTABLISHED | 连接已建立 | 三次握手完成后 |
| FIN_WAIT_1 | 主动关闭方发送FIN | 调用 close() 后 |
| FIN_WAIT_2 | 主动关闭方收到ACK | 收到第二次挥手后 |
| CLOSE_WAIT | 被动关闭方收到FIN | 收到第一次挥手后 |
| LAST_ACK | 被动关闭方发送FIN | 发送第三次挥手后 |
| TIME_WAIT | 主动关闭方等待2MSL | 发送第四次挥手后 |
netstat -tpan | grep 8080 或者 ss -tpan | grep 8080 可以实时查看Socket状态。如果发现大量 TIME_WAIT 或 CLOSE_WAIT,说明你的程序在连接管理上有问题。端口复用场景下,TIME_WAIT 尤其常见,后面我会专门讲怎么处理。
7. 总结:这些基础决定了你能走多远
Socket类型、API、三次握手、四次挥手,这些看起来是基础中的基础。但说实话,我见过太多高级工程师,在排查线上问题时,就是因为对这些细节理解不透,浪费了大量时间。
比如,端口复用本质上就是利用 SO_REUSEADDR 和 SO_REUSEPORT 这两个Socket选项,让多个Socket绑定到同一个端口。但如果你不理解 bind() 和 listen() 的底层逻辑,就很难用好它们。
嗯,这一章就到这里。把这些基础打牢,后面讲端口复用时,你才能游刃有余。