1. Socket基础:理解TCP/IP协议栈、Socket是什么、Socket类型与工作流程
说实话,很多人学网络编程一上来就撸代码,结果连数据怎么从A到B的都没搞明白。我当年也是这样,写了个聊天程序,局域网跑得挺好,一上公网就各种掉线、乱序。后来老老实实把TCP/IP协议栈啃了一遍,才明白问题出在哪。
这一节,咱们就把地基打牢。你想想看,Socket说白了就是操作系统给应用层开的一扇门,数据从这扇门进去,经过协议栈层层打包,最后通过网络发出去。反过来也一样。
1.1 TCP/IP协议栈:数据是怎么旅行的
TCP/IP协议栈不是单个协议,而是一组协议的集合。我习惯把它理解成「快递分拣系统」——每一层只干自己那点事,干完就交给下一层。
核心认知:TCP/IP协议栈分为四层,从上到下依次是:
- 应用层 — HTTP、FTP、SMTP,你写的程序就在这层
- 传输层 — TCP、UDP,负责端到端的可靠或不可靠传输
- 网络层 — IP协议,负责寻址和路由
- 网络接口层 — 以太网、Wi-Fi,负责物理介质上的数据收发
每一层都会给数据「加个信封」。应用层的数据到了传输层,会被加上TCP头部(源端口、目的端口、序号等);到了网络层,再加上IP头部(源IP、目的IP);到了网络接口层,再加上MAC头部。这个过程叫封装。
接收端反过来,一层层拆信封,直到拿到原始数据。这叫解封装。
我在项目中遇到过一个问题:客户端发了个大文件,服务端收不全。查了半天,发现是应用层没处理TCP的粘包和拆包。说白了,TCP是流式协议,它不关心你发了几次write,只关心字节流。你得自己在应用层定义消息边界。
1.2 Socket到底是什么
Socket,中文叫「套接字」。名字挺拗口,但概念很简单——它是应用层与传输层之间的编程接口。
我个人的理解:Socket就是IP地址 + 端口号的组合。IP地址找到哪台机器,端口号找到机器上的哪个进程。两者一组合,就能唯一定位网络上的一个通信端点。
我的习惯:把Socket想象成「电话插座」。IP地址是区号+电话号码,端口号是分机号。你拨通总机(IP),再转分机(端口),就能找到具体的人(进程)。
在Python里,创建一个Socket只需要一行代码:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP Socket
AF_INET 表示使用IPv4地址族,SOCK_STREAM 表示使用TCP协议。就这么简单。
1.3 Socket类型:TCP vs UDP
这是面试常考题,也是实际开发中第一个要做的选择。我直接给你对比表:
| 特性 | TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) |
|---|---|---|
| 连接方式 | 面向连接(先建立连接再通信) | 无连接(直接发数据) |
| 可靠性 | 可靠传输(有确认、重传机制) | 不可靠(发出去就不管了) |
| 数据顺序 | 保证顺序 | 不保证顺序 |
| 传输模式 | 流式(无边界) | 报文式(有边界) |
| 速度 | 相对较慢 | 快 |
| 典型应用 | HTTP、FTP、SMTP | DNS、视频直播、在线游戏 |
选哪个?我有个简单的判断标准:
- 如果数据必须完整、有序到达(比如网页、文件传输),选TCP。
- 如果实时性优先,丢几个包无所谓(比如视频通话、游戏位置同步),选UDP。
我曾经踩过的坑:做一个实时数据采集系统,用了UDP。结果局域网测试一切正常,部署到公网后,数据包丢失率高达15%。后来加了应用层确认重传机制,才勉强能用。所以,别迷信UDP「快」——在不可靠网络上,快但丢包等于没用。
1.4 Socket工作流程:从创建到关闭
TCP Socket的工作流程,我习惯用「打电话」来类比:
- 创建Socket — 相当于装一部电话
- 绑定地址和端口 (bind) — 相当于给电话分配一个号码
- 监听 (listen) — 相当于把电话设为待机状态,等别人打进来
- 接受连接 (accept) — 相当于有人打电话进来,你接起来
- 收发数据 (send/recv) — 相当于通话
- 关闭连接 (close) — 相当于挂电话
客户端那边更简单:
- 创建Socket
- 连接服务器 (connect) — 相当于拨号
- 收发数据 (send/recv)
- 关闭连接 (close)
看个最简单的例子,服务端:
import socket
# 1. 创建Socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址和端口
server.bind(('0.0.0.0', 8080))
# 3. 监听,最大等待队列为5
server.listen(5)
print('服务器启动,等待连接...')
# 4. 接受连接
conn, addr = server.accept()
print(f'客户端 {addr} 已连接')
# 5. 收发数据
data = conn.recv(1024)
print(f'收到: {data.decode()}')
conn.send(b'Hello, Client!')
# 6. 关闭
conn.close()
server.close()
客户端:
import socket
# 1. 创建Socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务器
client.connect(('127.0.0.1', 8080))
# 3. 收发数据
client.send(b'Hello, Server!')
data = client.recv(1024)
print(f'收到: {data.decode()}')
# 4. 关闭
client.close()
注意:上面的代码是同步阻塞模式。recv() 会一直等,直到收到数据。在实际的HTTP服务器中,我们会用多线程或异步IO来处理多个客户端。这个后面会详细讲。
UDP的工作流程更简单,不需要连接:
# UDP服务端
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', 8080))
data, addr = s.recvfrom(1024) # 接收数据,同时获取客户端地址
s.sendto(b'OK', addr) # 直接发,不需要连接
# UDP客户端
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'Hello', ('127.0.0.1', 8080))
data, addr = s.recvfrom(1024)
看到区别了吗?UDP没有connect、listen、accept这些步骤。发数据时直接指定目标地址,收数据时recvfrom会同时返回数据和对端地址。说白了,UDP就是「发出去就不管了」。
1.5 本章小结
这一节我们搞清楚了四件事:
- TCP/IP协议栈是分层封装的,每层各司其职
- Socket是IP+端口的组合,是应用层与传输层的接口
- TCP可靠但慢,UDP快但不可靠,选哪个看场景
- TCP Socket工作流程:创建→绑定→监听→接受→收发→关闭