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。

我曾经踩过的坑: 创建Socket后,如果不设置 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对应起来。

Socket通信核心流程:三次握手与四次挥手 Socket通信核心流程:三次握手与四次挥手 客户端 服务端 三次握手(建立连接) ① SYN (seq=x) ② SYN+ACK (seq=y, ack=x+1) ③ ACK (seq=x+1, ack=y+1) 数据通信 read() / write() 或 send() / recv() 四次挥手(断开连接) ① FIN (seq=u) ② ACK (ack=u+1) ③ FIN (seq=v) ④ ACK (ack=v+1)

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 发送第四次挥手后
实战小技巧: 在Linux上,用 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() 的底层逻辑,就很难用好它们。

嗯,这一章就到这里。把这些基础打牢,后面讲端口复用时,你才能游刃有余。