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 无连接的数据报

⚠️ 常见错误

  1. TCP 用 SOCK_STREAM,UDP 用 SOCK_DGRAM,记反则行为完全不同。
  2. 忘了 import socket,直接 socket.socket(...)NameError
  3. 以为 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
  • 默认 bindacceptrecv 都是阻塞的:没数据就一直等。

想接多个客户端,把 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()

线程的作用是让"等这位客人说话"和"接待下一位客人"互不耽误——这是多客户端服务端的基本套路。

⚠️ 常见错误

  1. 顺序错:必须 bind → listen → accept,跳过 bind 直接 listen 会报 OSError
  2. accept() 一次只能接一个客户端:想接多个,外层要加 while True 循环。
  3. recv 返回空字节串表示对方已断开b""),要据此判断关闭连接,否则死循环。
  4. 端口被占用:别的进程用了 9000,bindOSError: [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,操作系统已经替你做完了。理解这一点,代码和协议就串起来了。

⚠️ 常见错误

  1. 服务端没启动就 connect:报 ConnectionRefusedError(WinError 10061)。
  2. IP/端口写错:连不上,且不报明确的错(可能一直卡在 connect)。
  3. send 的是字符串send 必须收 bytes,字符串要 .encode("utf-8");接收回来是 bytes,要 .decode()
  4. 客户端不需要 bind:本地端口由操作系统自动分配。

9.3 UDP:无需连接的"飞鸽传书"

UDP 像飞鸽传书:不管鸽子到没到、对方在不在,你把信绑上去就放。没有连接不保证到达不保证顺序,但也因此更快、开销更小。

UDP 服务端不用 listenaccept,只有一个核心动作:绑好端口后,循环 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)

⚠️ 常见错误

  1. UDP 也用 listen/accept:不需要,UDP 没有连接。
  2. recvfrom 返回值结构:是 (data, addr) 两个值,只接一个会 ValueError
  3. UDP 不保证送达:对方没启动你也收不到错误提示,数据就"丢"了。
  4. 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"" 判断结束"。

⚠️ 常见错误

  1. 请求格式错:少写 \r\n 或漏空行,服务器直接报 400。
  2. 忘了 encodes.send(request) 传字符串会报错,必须是 bytes。
  3. 明文 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 连接"。

⚠️ 常见错误

  1. requests 需要安装:标准库没有,pip install requestsimport requests 才不报 ModuleNotFoundError
  2. 忘加 timeout:请求可能一直挂着,程序"卡死"。
  3. params(GET 用)与 data/json(POST 用)混用:GET 参数拼 URL,POST 数据放 body。
  4. 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"

解决粘包,本质是自己约定边界,三种常见方案:

  1. 定长消息:每条固定 N 字节,不够补位。
  2. 分隔符:每条结尾加 \n 等特殊字符,按分隔符切分。
  3. 长度前缀:先发 4 字节说明"这条多长",再发正文。

💡 记忆:UDP 是"一个个信封"(有边界),TCP 是"一条水管"(无边界,所以会粘包)。

⚠️ 常见错误

  1. 以为粘包是"bug 可以修复":是 TCP 流式传输的固有特性,只能通过自定义边界规避。
  2. SO_REUSEADDR 放在 bind 之后设置:没作用,必须 bind 前。
  3. 端口占用和连接拒绝混淆:占用是"我想 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 库

⭐ 本章考点清单

  1. TCP 服务端流程与客户端流程(每步调用的函数)。
  2. TCP vs UDP 的 socket 接口差异(listen/accept vs sendto/recvfrom)。
  3. accept() 的返回值、recv 的阻塞特性、空字节串表示断开。
  4. send 必须传 bytes,字符串 .encode() / .decode()
  5. 端口占用、连接拒绝、地址被占用的报错与排查。
  6. 粘包的原因(TCP 无边界)与三种解决思路。
  7. 用 socket 手动构造 HTTP GET 请求。
  8. urllib 与 requests 的 GET/POST、params/datatimeout