NET-09 网络编程socket
09 · 网络编程:Socket 实战(Socket Programming in Python)
📅 预计 100 分钟 | ⭐ 本章主题:用 Python 实现 TCP/UDP 通信与 HTTP 请求
9.0 Socket 是什么
把两台电脑的通信比作两个房间里的人隔着墙喊话。你(进程)要跟对面(进程)说话,必须先各自在"墙"上开一个插座(socket),然后约定"我说中文、你说中文"(协议),再顺着插座把话送出去。socket 就是网络通信的端点——一个程序要联网,就得先创建自己的 socket。
一个 socket 由 五元组 唯一标识:
(源 IP, 源端口, 目的 IP, 目的端口, 协议类型)
Python 里创建 socket 只需要一行,两个关键参数:
import socket
# AF_INET:使用 IPv4;SOCK_STREAM:流式字节流 → TCP
s_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SOCK_DGRAM:数据报 → UDP
s_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
| 参数 | 含义 | TCP 用 | UDP 用 |
|---|---|---|---|
AF_INET |
IPv4 地址族 | ✅ | ✅ |
SOCK_STREAM |
面向连接的字节流 | ✅ | ❌ |
SOCK_DGRAM |
无连接的数据报 | ❌ | ✅ |
⚠️ 常见错误
- TCP 用
SOCK_STREAM,UDP 用SOCK_DGRAM,记反则行为完全不同。 - 忘了
import socket,直接socket.socket(...)会NameError。 - 以为 socket 是"接口硬件":它是软件抽象,操作系统提供的通信端点。
9.1 TCP 服务端:先占坑、再等人
TCP 服务端的标准流程是四步,像餐厅在门口放号等客:
① socket() 创建 socket(开插座)
② bind() 绑定 IP 和端口(占好门牌号)
③ listen() 开始监听(门口放号,等客人)
④ accept() 接收一个客户端连接(来一位,领一位)
然后 recv()/send() 收发数据
最后 close() 关闭
import socket
# 1. 创建 TCP socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址:IP 用 0.0.0.0 表示"本机所有网卡",端口选 9000
server.bind(("0.0.0.0", 9000))
# 3. 开始监听,listen(backlog) 里的数字是"排队上限"
server.listen(5)
print("服务端已启动,等待连接...")
# 4. 接受连接:accept() 会阻塞,直到有客户端连上来
conn, addr = server.accept() # conn 是新的连接 socket,addr 是客户端地址
print(f"客户端 {addr} 已连接")
data = conn.recv(1024) # 接收最多 1024 字节
print(f"收到:{data.decode('utf-8')}")
conn.send(b"Hello, Client!") # 发送(注意是 bytes)
conn.close() # 关闭本次连接
server.close() # 关闭监听 socket
# 运行示例(配合 9.2 的客户端):
# 服务端已启动,等待连接...
# 客户端 ('127.0.0.1', 54321) 已连接
# 收到:Hello, Server!
几个要点:
accept()返回两个值:conn是"这一位客人"专用的连接,addr是客人地址;原来的server继续监听,接下一个客人。recv(1024)里的 1024 是"最多读多少字节",返回的是 bytes。- 默认
bind、accept、recv都是阻塞的:没数据就一直等。
想接多个客户端,把 accept() 之后的部分放进 while True 循环,并为每个连接开一个线程处理——否则一个客户端占住 recv,其他客户端全都排队等着:
import socket
import threading
def handle(conn, addr):
data = conn.recv(1024) # 这个客户端自己的连接
conn.send(b"echo: " + data)
conn.close()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(5)
while True:
conn, addr = server.accept() # 每次 accept 领一位客人
threading.Thread(target=handle, args=(conn, addr)).start()
线程的作用是让"等这位客人说话"和"接待下一位客人"互不耽误——这是多客户端服务端的基本套路。
⚠️ 常见错误
- 顺序错:必须
bind → listen → accept,跳过bind直接listen会报OSError。 accept()一次只能接一个客户端:想接多个,外层要加while True循环。recv返回空字节串表示对方已断开(b""),要据此判断关闭连接,否则死循环。- 端口被占用:别的进程用了 9000,
bind报OSError: [Errno 98](Linux)或WinError 10048(Windows)。
9.2 TCP 客户端:主动敲门
TCP 客户端更简单,三步,像客人主动上门:
① socket() 创建 socket
② connect() 连接服务器(敲门)
③ send()/recv() 收发数据
最后 close()
import socket
# 1. 创建 TCP socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务器(IP、端口要和服务端一致)
client.connect(("127.0.0.1", 9000))
# 如果服务器没开:ConnectionRefusedError
# 3. 发送:send 的参数必须是 bytes
client.send("Hello, Server!".encode("utf-8"))
data = client.recv(1024)
print(f"收到:{data.decode('utf-8')}")
client.close()
# 运行示例:
# 收到:Hello, Client!
客户端与服务端流程对照:
| 步骤 | 服务端 | 客户端 |
|---|---|---|
| 创建 | socket() |
socket() |
| 绑定 | bind() 自己地址 |
不需要(系统自动分配本地端口) |
| 就绪 | listen() |
connect() 主动连 |
| 对接 | accept() 接受 |
(已连上) |
| 通信 | recv() / send() |
send() / recv() |
| 结束 | close() |
close() |
你也许已经注意到:客户端 connect() 和服务端 accept() 一旦返回,连接就"成了"。这正是第 05 讲学过的 TCP 三次握手(SYN → SYN+ACK → ACK)被这两个函数在幕后自动完成——你不需要手动发 SYN,操作系统已经替你做完了。理解这一点,代码和协议就串起来了。
⚠️ 常见错误
- 服务端没启动就
connect:报ConnectionRefusedError(WinError 10061)。 - IP/端口写错:连不上,且不报明确的错(可能一直卡在 connect)。
send的是字符串:send必须收 bytes,字符串要.encode("utf-8");接收回来是 bytes,要.decode()。- 客户端不需要 bind:本地端口由操作系统自动分配。
9.3 UDP:无需连接的"飞鸽传书"
UDP 像飞鸽传书:不管鸽子到没到、对方在不在,你把信绑上去就放。没有连接、不保证到达、不保证顺序,但也因此更快、开销更小。
UDP 服务端不用 listen、accept,只有一个核心动作:绑好端口后,循环 recvfrom 收、sendto 发:
import socket
# UDP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9001)) # 服务端要绑端口
print("UDP 服务端就绪...")
while True:
data, addr = s.recvfrom(1024) # 返回:(数据, 对方地址)
print(f"{addr} 发来:{data.decode('utf-8')}")
s.sendto(b"Got it!", addr) # 回给这个地址
UDP 客户端甚至不用 bind,直接 sendto 发、recvfrom 收:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello udp", ("127.0.0.1", 9001)) # 指定目标,不需要 connect
data, addr = s.recvfrom(1024)
print(data.decode("utf-8")) # 输出: Got it!
TCP 与 UDP 编程接口对照:
| 动作 | TCP | UDP |
|---|---|---|
| 建立连接 | connect() |
无 |
| 接受连接 | listen() + accept() |
无 |
| 发送 | send(data) |
sendto(data, addr) |
| 接收 | recv(1024) |
recvfrom(1024) 返回 (data, addr) |
⚠️ 常见错误
- UDP 也用
listen/accept:不需要,UDP 没有连接。 recvfrom返回值结构:是(data, addr)两个值,只接一个会ValueError。- UDP 不保证送达:对方没启动你也收不到错误提示,数据就"丢"了。
sendto必须指定目标地址:没有连接,不说地址发给谁。
9.4 用 socket 发 HTTP 请求
学完 TCP 客户端,其实你已经能"手动"访问网页了——HTTP 就是跑在 TCP 之上的一层"格式规则"。下面用裸 socket 模拟一个最简 HTTP GET 请求:
import socket
# 1. 连上服务器(www.baidu.com 的 80 端口)
s = socket.create_connection(("www.baidu.com", 80))
# 2. 按 HTTP 格式拼请求:请求行 + Host 头 + 空行
request = "GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n"
s.send(request.encode("utf-8"))
# 3. 循环收响应,直到连接关闭
resp = b""
while True:
part = s.recv(4096)
if not part:
break
resp += part
s.close()
print(resp.decode("utf-8", errors="ignore")[:300]) # 输出: HTTP/1.1 200 OK ...
注意这里 Connection: close 告诉服务器"传完就断开",这样客户端才能靠"收到 b"" 判断结束"。
⚠️ 常见错误
- 请求格式错:少写
\r\n或漏空行,服务器直接报 400。 - 忘了 encode:
s.send(request)传字符串会报错,必须是 bytes。 - 明文 HTTP 别传敏感信息:这正是第 07 章 HTTPS 存在的意义。
9.5 用标准库与 requests 发 HTTP 请求
手写 socket 太底层,实际开发用现成库。Python 自带 urllib,社区最常用 requests(第三方库,pip install requests)。
urllib(标准库,无需安装)
import urllib.request
import json
resp = urllib.request.urlopen("https://httpbin.org/get", timeout=5)
data = json.loads(resp.read().decode("utf-8"))
print(data["url"]) # 输出: https://httpbin.org/get
requests(更简洁,日常开发常用)
import requests
# GET:参数自动拼接进 URL
r = requests.get("https://httpbin.org/get", params={"q": "python"}, timeout=5)
print(r.status_code) # 200
print(r.json()["args"]) # {'q': 'python'}
# POST:数据放请求体,自动编码
r2 = requests.post("https://httpbin.org/post",
data={"username": "guorui"},
timeout=5)
print(r2.json()["form"]) # {'username': 'guorui'}
# 常见属性
print(r.headers) # 响应头(dict)
print(r.text) # 响应体(文本)
print(r.encoding) # 编码
requests 帮你自动处理:URL 编码、请求头、状态码判断、JSON 解析——背后仍然是我们学过的"HTTP 报文 + TCP 连接"。
⚠️ 常见错误
requests需要安装:标准库没有,pip install requests后import requests才不报ModuleNotFoundError。- 忘加
timeout:请求可能一直挂着,程序"卡死"。 params(GET 用)与data/json(POST 用)混用:GET 参数拼 URL,POST 数据放 body。r.json()前提是响应真的是 JSON,否则解析报错;先用r.status_code判断请求是否成功。
9.6 常见坑:端口占用、连接拒绝、粘包
端口占用(Address already in use)
进程没退出、端口还占着,新程序 bind 同一个端口就报错:
OSError: [Errno 98] Address already in use (Linux)
OSError: [WinError 10048] 端口被占用 (Windows)
排查:Windows 用 netstat -ano | findstr 9000,Linux 用 ss -tlnp | grep 9000,找到占用进程后关闭,或用 SO_REUSEADDR 允许复用:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 要在 bind 之前设置
连接拒绝(Connection refused)
对端 IP 可达但没有程序在监听该端口。解决:确认服务端启动、IP/端口一致、防火墙放行。
粘包问题(概念)
粘包(sticky packet)是 TCP 特有的现象:TCP 是字节流,没有"消息边界"——你 send("abc") 和 send("def"),对方一次 recv 可能收到 "abcdef" 两个粘在一起;反之,一个大消息也可能被拆成多次 recv。这不叫"错误",是流的本性。
# 客户端连续发两次,服务端一次 recv 可能全收到 → "abcdef"
client.send(b"abc")
client.send(b"def")
# 服务端 data = conn.recv(1024) # 可能是 b"abcdef"
解决粘包,本质是自己约定边界,三种常见方案:
- 定长消息:每条固定 N 字节,不够补位。
- 分隔符:每条结尾加
\n等特殊字符,按分隔符切分。 - 长度前缀:先发 4 字节说明"这条多长",再发正文。
💡 记忆:UDP 是"一个个信封"(有边界),TCP 是"一条水管"(无边界,所以会粘包)。
⚠️ 常见错误
- 以为粘包是"bug 可以修复":是 TCP 流式传输的固有特性,只能通过自定义边界规避。
SO_REUSEADDR放在bind之后设置:没作用,必须 bind 前。- 端口占用和连接拒绝混淆:占用是"我想 bind 被占",拒绝是"我想 connect 没人接"。
9.7 本讲小结
网络编程的核心是分清 TCP(有连接、可靠、字节流) 和 UDP(无连接、不可靠、数据报),再套模板:服务端 socket→bind→listen→accept→recv/send,客户端 socket→connect→send/recv。HTTP 请求就是 TCP + 报文格式的组合,日常用 requests 就够了。遇到异常,先看五元组和端口,再查协议层。
🧠 记忆口诀
- 服务端四步曲:socket 开门、bind 占号、listen 放号、accept 接客。
- 客户端三步曲:socket 开门、connect 敲门、send 递话。
- TCP 是水管(无边界、粘包),UDP 是信封(有边界、可能丢)。
- send 收 bytes,字符串先 encode;recv 回 bytes,显示先 decode。
- requests 三件套:
get查、post交、timeout保命。
📌 中英术语表(本讲)
| 中文 | English | 说明 |
|---|---|---|
| 套接字 | socket | 网络通信的端点 |
| 五元组 | 5-tuple | 源/目的 IP、源/目的端口、协议 |
| IPv4 地址族 | AF_INET | 使用 IPv4 的地址族 |
| 流式套接字 | SOCK_STREAM | TCP 用的字节流 |
| 数据报套接字 | SOCK_DGRAM | UDP 用的数据报 |
| 绑定 | bind | 绑定 IP 与端口 |
| 监听 | listen | 服务端进入监听状态 |
| 接受连接 | accept | 服务端接下一个客户端 |
| 连接 | connect | 客户端发起连接 |
| 发送 / 接收 | send / recv | 已连接 socket 收发 |
| 发往 / 收自 | sendto / recvfrom | UDP 指定地址收发 |
| 阻塞 | blocking | 没数据就一直等 |
| 端口占用 | address already in use | bind 时端口被占用 |
| 连接被拒绝 | connection refused | 端口无程序监听 |
| 粘包 | sticky packet | TCP 字节流无消息边界 |
| 超时 | timeout | 请求等待的最长时间 |
| 请求库 | requests | Python 第三方 HTTP 库 |
⭐ 本章考点清单
- TCP 服务端流程与客户端流程(每步调用的函数)。
- TCP vs UDP 的 socket 接口差异(listen/accept vs sendto/recvfrom)。
accept()的返回值、recv的阻塞特性、空字节串表示断开。send必须传 bytes,字符串.encode()/.decode()。- 端口占用、连接拒绝、地址被占用的报错与排查。
- 粘包的原因(TCP 无边界)与三种解决思路。
- 用 socket 手动构造 HTTP GET 请求。
- urllib 与 requests 的 GET/POST、
params/data、timeout。