学网络编程的时候,很多人一开始会觉得自己懂了——IP 地址、端口号、TCP/UDP,这些概念背得滚瓜烂熟。但真到了写代码的时候,才发现到处都是坑:为什么端口被占用?为什么 recv 收不到完整数据?为什么服务只能连一个客户?
这篇文章不会重复教科书式的概念 ,而是在系统梳理原笔记全部知识点的基础上,用大白话把那些"教程一带而过但实际开发必踩的坑"讲透。
网络编程,就是让在不同的电脑上的软件能够进行数据传递,即进程之间的通信。
打个比方:
网络编程的本质:就是写代码让两个进程通过网络"聊天"。
网络编程有三个核心要素,缺一不可:
| IP 地址 | ||
| 端口 | ||
| 协议 |
类比理解:
IP 地址是一串数字,用来标识一台电脑在网络中的位置。
当你的电脑连接网络时,运营商会给你分配一个 IP 地址。通过这个地址,网络上的其他设备才能找到你。
IP 地址的两个核心功能:
查看本机 IP:
# Windows 用户
ipconfig
# Linux / macOS 用户
ifconfig
# 或者
ip addr
IP 地址虽然能标识设备,但当网络规模变大时,管理起来就很麻烦。比如一个公司有 1000 台电脑,如果都在一个网络里,广播消息会让整个网络瘫痪。
解决办法:把大网络切成小网络(子网),每个子网独立管理。
子网掩码(Subnet Mask)的作用是:把 IP 地址分成两部分——网络前缀和主机编号。

IP 地址: 192.168.10.2
子网掩码: 255.255.255.0
解读:
255.255.255.0 的前 24 位都是 1,表示 IP 地址的前 24 位是网络部分CIDR(Classless Inter-Domain Routing,无类别域间路由)是一种更简洁的表示方法:
192.168.10.2/24
/24 表示 IP 地址的前 24 位是网络部分,等价于子网掩码 255.255.255.0。
为什么 CIDR 更好?
192.168.10.2/24 比写 192.168.10.2 255.255.255.0 方便| 网络号 | 192.168.10.0/24 | |
| 广播地址 | 192.168.10.255/24 |
IPv4 地址根据网络规模分为 5 类:

| A 类 | ||||
| B 类 | ||||
| C 类 | ||||
| D 类 | 组播地址 | |||
| E 类 | 保留地址 |
记忆口诀:A 大 B 中 C 小,D 组播 E 保留。
💡 实战提示:
127.0.0.1是个特殊地址,属于 A 类但用于回路测试(Loopback),永远指向本机,即使网卡没配 IP 也能 ping 通。
| 公网 IP | ||
| 私网 IP |
10.0.0.0 ~ 10.255.255.255 (A 类私网,超大型内网)
172.16.0.0 ~ 172.31.255.255 (B 类私网,中型内网)
192.168.0.0 ~ 192.168.255.255 (C 类私网,家庭/办公室)
127.0.0.1 ~ 127.255.255.255:回路测试专用,127.0.0.1 就是 localhost。
问题:IPv4 地址只有约 42 亿个,不够全球每台设备分一个。
解决办法:NAT 技术。
NAT 的原理:
123.45.67.89)192.168.1.2、192.168.1.3)123.45.67.89好处:
💡 为什么 NAT 流行? 1990 年代中期 IPv4 地址开始枯竭,NAT 让多台主机共享一个公网 IP,既解决了地址短缺,又成了家庭路由器的标准配置。
172.16.254.1 | 2001:db8:0:1234:0:567:8:1 | |
IPv6 有多大? 可以给地球上每一粒沙子都分配一个 IP 地址。
💡 现状:虽然 IPv6 推广多年,但 IPv4 + NAT 仍然是主流。写代码时建议同时兼容两者。
很多新手用 socket.gethostbyname(socket.gethostname()) 获取本机 IP,这在某些系统上会返回 127.0.0.1。靠谱的做法:
import socket
def get_local_ip():
"""获取本机真实 IP(不是 127.0.0.1)"""
try:
# 创建一个 UDP 套接字,不需要真正连接
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80)) # Google 的 DNS
ip = s.getsockname()[0]
return ip
except Exception:
return"127.0.0.1"
print(f"本机 IP: {get_local_ip()}")
原理:创建一个 UDP 包发往 8.8.8.8(Google DNS),操作系统会选出正确的网卡和 IP,然后我们通过 getsockname() 获取这个 IP。
端口(Port)是计算机中的逻辑概念,用于标识进程(应用软件)。
类比:
一台电脑有多个软件同时运行(微信、浏览器、QQ),数据包到了电脑之后,怎么知道该发给谁?靠的就是端口。
端口号范围:0 ~ 65535,共 65536 个。
查看端口占用:
# Windows
netstat -ano
# Linux / macOS
netstat -tlnp
# 或
ss -tlnp
| 公认端口 | ||
| 注册端口 | ||
| 动态端口 |
⚠️ 注意:端口 0 是保留端口,不可使用。1~1023 系统保留,只能由 root 用户使用。
💡 实战建议:写服务程序时,别用 8080,这年头比公共厕所还容易被占用。用 8000 以上,或者从配置文件读取。
import socket
def is_port_in_use(port, host='0.0.0.0'):
"""检测端口是否被占用"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind((host, port))
return False
except OSError:
return True
def find_free_port(start_port=8000):
"""找一个可用端口"""
port = start_port
while is_port_in_use(port):
port += 1
return port
print(f"8080 是否可用: {not is_port_in_use(8080)}")
print(f"推荐端口: {find_free_port()}")
通信协议是一组规则,规定了不同设备之间如何进行数据交换。
类比:
协议定义了通信的各个方面:
问题:网络通信很复杂,涉及硬件、驱动、路由、应用等多个环节。如果所有功能都混在一起,改一个地方就可能影响全局。
解决办法:分层。
类比:公司组织架构
分层的好处:
OSI(Open System Interconnection,开放式系统互联)七层网络模型由国际标准化组织制定。
类比:一个公司,从高到低有 7 个层级:

7. 应用层 —— 直接为用户提供服务(HTTP、FTP、SMTP)
类比:前台接待员,直接和客户打交道
6. 表示层 —— 数据格式转换、加密、压缩
类比:翻译官,把数据翻译成接收方能懂的语言
5. 会话层 —— 建立、管理、终止会话
类比:秘书,负责安排会议、记录会议内容
4. 传输层 —— 端到端的数据传输(TCP、UDP)
类比:快递公司,负责把数据从一端送到另一端
3. 网络层 —— 路由和寻址(IP、ICMP)
类比:导航系统,决定数据包走哪条路
2. 数据链路层 —— 帧的传输(以太网、WiFi)
类比:卡车司机,负责在一段路上运输数据
1. 物理层 —— 物理介质(电缆、光纤、无线电波)
类比:公路、铁路、航线,实际的物理传输介质
问题:OSI 模型虽然理论完美,但实现过于复杂,且制定周期过长。
在 OSI 标准推出之前,TCP/IP 模型已经在全球广泛使用。它把 7 层精简为 4 层:
4. 应用层 —— 对应 OSI 的 5~7 层(前台+翻译官+秘书的活一起干)
3. 传输层 —— 对应 OSI 的第 4 层(快递公司)
2. 网络层 —— 对应 OSI 的第 3 层(导航系统)
1. 网络接口层 —— 对应 OSI 的 1~2 层(司机+公路一起管)
由于 TCP/IP 模型没有定义网络接口层的具体内容,实际学习和开发中,通常将其替换为 OSI 的数据链路层和物理层,这就是五层网络模型:
5. 应用层
4. 传输层
3. 网络层
2. 数据链路层
1. 物理层
TCP/IP 协议族得名于两个核心协议:

类比:
常见网络协议(按层级):
Socket(套接字)是同一或不同电脑的进程间通信的工具。
类比:
为什么需要 Socket?
特点:
Socket 主要分两种:
| 流式套接字 | |||
| 数据报套接字 |
流式套接字(TCP):
数据报套接字(UDP):
Python 中提供了 socket 模块用于创建套接字。
import socket
# TCP 套接字:面向连接、可靠传输
tcp_socket = socket.socket(
family=socket.AF_INET, # IPv4
type=socket.SOCK_STREAM # 流式套接字 = TCP
)
# UDP 套接字:无连接、不可靠但快速
udp_socket = socket.socket(
family=socket.AF_INET, # IPv4
type=socket.SOCK_DGRAM # 数据报套接字 = UDP
)
常用参数解释:
family | AF_INET | |
AF_INET6 | ||
AF_UNIX | ||
type | SOCK_STREAM | |
SOCK_DGRAM | ||
SOCK_RAW |
Socket 地址 = IP 地址 + 端口号
类比:
在代码中,Socket 地址表示为元组(IP, 端口):
server_address = ("127.0.0.1", 8000) # 本地 IP,端口 8000
这部分是实战中最容易踩坑的地方。
问题场景:服务重启时经常遇到 Address already in use 错误。
原因:TCP 连接断开后,端口会进入 TIME_WAIT 状态(默认 60 秒),期间无法重新绑定。
类比:
SO_REUSEADDR 就是告诉系统:"冷却期不用等,直接复用"解决办法:
import socket
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 必须在 bind 之前设置
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_socket.bind(('0.0.0.0', 8000))
问题场景:发送小数据包时,TCP 会缓冲一下再发送,导致延迟。
原因:Nagle 算法——TCP 会把多个小数据包合并成一个大包发送,减少网络开销。
类比:
TCP_NODELAY 就是告诉 TCP:"别攒了,直接发"tcp_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
默认缓冲区大小可能不够,高吞吐场景需要手动调整:
# 设置发送缓冲区 64KB
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)
# 设置接收缓冲区 64KB
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
UDP(User Datagram Protocol,用户数据报协议)是一个简单的面向数据报的通信协议。

核心特性:
| 无连接 | ||
| 不可靠 | ||
| 快速 |
UDP 适合什么场景?
服务端 客户端
| |
|--- 1. 创建 UDP 套接字 ---------->|
|--- 2. 绑定 IP 和端口 ---------->|
| |
|<---------- 3. 发送数据 -----------|
|--- 4. 接收数据 ----------------->|
| |
|--- 5. 发送响应 ----------------->|
|<---------- 6. 接收响应 -----------|
| |
|--- 7. 关闭套接字 --------------->|
和 TCP 的区别:
listen() 和 accept()connect()sendto() 发送,用 recvfrom() 接收"""udp_server.py"""
import socket
def udp_server(host='0.0.0.0', port=8000):
# 创建 UDP 套接字
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# 设置端口复用
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定 IP 和端口
# 注意:UDP 不需要 listen(),直接绑定就能收数据
udp_socket.bind((host, port))
print(f"UDP 服务端启动,监听 {host}:{port}")
try:
while True:
# 接收数据(最大 1024 字节)
# recvfrom 返回数据和发送方的地址
recv_data, client_addr = udp_socket.recvfrom(1024)
client_ip = client_addr[0]
client_port = client_addr[1]
print(f"{client_ip}:{client_port} >> {recv_data.decode('utf-8')}")
# 发送响应
# sendto 需要指定对方的地址
udp_socket.sendto("你好".encode('utf-8'), client_addr)
except KeyboardInterrupt:
print("\n服务端关闭")
finally:
# 关闭套接字
udp_socket.close()
if __name__ == '__main__':
udp_server()
"""udp_client.py"""
import socket
def udp_client(server_ip='127.0.0.1', server_port=8000):
# 创建 UDP 套接字
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# 设置超时,避免无限等待(UDP 不保证到达,所以要设超时)
udp_socket.settimeout(5.0)
try:
while True:
try:
# 发送数据
msg = input(f"{server_ip}:{server_port} << ")
udp_socket.sendto(msg.encode('utf-8'), (server_ip, server_port))
# 接收响应
recv_data, client_addr = udp_socket.recvfrom(1024)
client_ip = client_addr[0]
client_port = client_addr[1]
print(f"{client_ip}:{client_port} >> {recv_data.decode('utf-8')}")
except socket.timeout:
print("请求超时,服务端未响应")
except KeyboardInterrupt:
print("\n客户端退出")
finally:
# 关闭套接字
udp_socket.close()
if __name__ == '__main__':
udp_client()
坑 1:UDP 数据包大小限制
UDP 数据包最大理论值是 65535 字节(含头部),但实际受 MTU(通常 1500 字节)限制。超过 MTU 的数据包会被分片,丢一个分片整个包就废了。
💡 实战建议:单个 UDP 数据包控制在 1400 字节以内,避免分片。
坑 2:UDP 不保证顺序
先发的包可能后到,后发的包可能先到。如果你的应用需要保证顺序,要么加序号,要么换 TCP。
坑 3:UDP 没有拥塞控制
疯狂发数据包可能导致网络拥塞。高吞吐场景需要自己做流量控制。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

核心特性:
| 面向连接 | ||
| 可靠 | ||
| 基于字节流 |
TCP 的三个阶段:
保证可靠性的机制:
目的:确认双方都能正常收发数据。
为什么不能两次?
过程:
客户端 服务器
| |
|------- SYN=1, seq=x ------->| ← 第一次握手:客户端说"你好,能听到吗?"
| |
|<---- SYN=1, ACK=1, | ← 第二次握手:服务器说"能听到,你能听到我吗?"
| seq=y, ack=x+1 ---------|
| |
|------- ACK=1, | ← 第三次握手:客户端说"我也能听到"
| seq=x+1, ack=y+1 -----> |
| |
|====== 连接已建立 ============|
目的:双方都确认没有数据要发送了,才能断开。
为什么不是三次?
过程:
客户端 服务器
| |
|------- FIN=1, seq=u ------->| ← 第一次挥手:客户端说"我说完了"
| |
|<------ ACK=1, | ← 第二次挥手:服务器说"好的,我知道了"
| seq=v, ack=u+1 ---------|
| |
|<------ FIN=1, | ← 第三次挥手:服务器说"我也说完了"
| seq=w, ack=u+1 ---------|
| |
|------- ACK=1, | ← 第四次挥手:客户端说"好的,再见"
| seq=u+1, ack=w+1 -----> |
| |
|====== 连接已断开 ============|
服务端 客户端
| |
|--- 1. 创建 TCP 套接字 ---------->|
|--- 2. 绑定 IP 和端口 ---------->|
|--- 3. 设置监听(listen)------->| ← 服务器进入"等待连接"状态
| |
|<-------- 4. 连接请求 ------------| ← 客户端发起连接(三次握手)
|--- 5. 接受连接(accept)------>| ← 服务器接受连接
| |
|<-------- 6. 发送数据 ------------|
|--- 7. 接收数据 ----------------->|
| |
|--- 8. 关闭连接 ---------------->|
|--- 9. 关闭套接字 --------------->|
关键区别:
listen() 进入监听状态connect() 发起连接accept() 接受连接,返回一个新的 Socket 用于和该客户端通信send() 和 recv() 传输数据,不需要指定地址"""tcp_server.py"""
import socket
import threading
def handle_client(client_socket, client_addr):
"""处理单个客户端连接"""
print(f"新连接: {client_addr[0]}:{client_addr[1]}")
try:
while True:
# 接收数据
# recv 返回空数据表示客户端断开连接
recv_data = client_socket.recv(1024)
if not recv_data:
print(f"客户端 {client_addr[0]}:{client_addr[1]} 断开")
break
print(f"{client_addr[0]}:{client_addr[1]} >> {recv_data.decode('utf-8')}")
# 发送响应
client_socket.send("你好".encode('utf-8'))
except ConnectionResetError:
print(f"客户端 {client_addr[0]}:{client_addr[1]} 异常断开")
except Exception as e:
print(f"处理客户端 {client_addr[0]}:{client_addr[1]} 时出错: {e}")
finally:
client_socket.close()
def tcp_server(host='0.0.0.0', port=8000, max_connections=5):
# 创建 TCP 套接字
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 设置端口复用
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定 IP 和端口
tcp_socket.bind((host, port))
# 设置监听(最大等待连接数)
# listen 让服务器进入"等待连接"状态
tcp_socket.listen(max_connections)
print(f"TCP 服务端启动,监听 {host}:{port}")
try:
while True:
# 等待客户端连接
# accept 返回一个新的 Socket 和客户端地址
client_sock, client_addr = tcp_socket.accept()
# 为每个客户端创建独立线程
thread = threading.Thread(
target=handle_client,
args=(client_sock, client_addr),
daemon=True
)
thread.start()
print(f"活跃连接数: {threading.active_count() - 1}")
except KeyboardInterrupt:
print("\n服务端关闭")
finally:
# 关闭套接字
tcp_socket.close()
if __name__ == '__main__':
tcp_server()
"""tcp_client.py"""
import socket
def tcp_client(server_ip='127.0.0.1', server_port=8000):
# 创建 TCP 套接字
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_socket.settimeout(10.0)
try:
# 连接服务器(三次握手在这里发生)
tcp_socket.connect((server_ip, server_port))
print(f"已连接到 {server_ip}:{server_port}")
while True:
try:
# 发送数据
msg = input(f"{server_ip}:{server_port} << ")
tcp_socket.send(msg.encode('utf-8'))
# 接收数据
recv_data = tcp_socket.recv(1024)
if not recv_data:
print("服务器断开连接")
break
print(f"{server_ip}:{server_port} >> {recv_data.decode('utf-8')}")
except socket.timeout:
print("接收超时")
except ConnectionRefusedError:
print(f"无法连接到 {server_ip}:{server_port},请确认服务是否启动")
except KeyboardInterrupt:
print("\n客户端退出")
finally:
# 关闭套接字(四次挥手在这里发生)
tcp_socket.close()
if __name__ == '__main__':
tcp_client()
问题根源:TCP 是面向字节流的协议,没有消息边界。
类比:
发送方: [消息A][消息B][消息C]
接收方可能收到: [消息A的前半部分][消息A的后半部分+消息B][消息C]
原理:先发 4 字节表示消息长度,再发消息内容。接收方先读 4 字节知道长度,再读对应长度的数据。
import struct
def send_msg(sock, msg):
"""发送带长度前缀的消息"""
encoded = msg.encode('utf-8')
# 4 字节长度前缀(网络字节序)
length_prefix = struct.pack('!I', len(encoded))
sock.sendall(length_prefix + encoded)
def recv_msg(sock):
"""接收带长度前缀的消息"""
length_data = recv_exact(sock, 4)
if not length_data:
return None
msg_length = struct.unpack('!I', length_data)[0]
msg_data = recv_exact(sock, msg_length)
return msg_data.decode('utf-8')
def recv_exact(sock, n):
"""精确读取 n 字节"""
data = bytearray()
while len(data) < n:
packet = sock.recv(n - len(data))
if not packet:
return None
data.extend(packet)
return bytes(data)
这是工业界最常用的方案,Redis、HTTP/2 等协议都采用类似机制。
def send_fixed(sock, msg, size=64):
"""发送固定长度消息"""
encoded = msg.encode('utf-8')[:size].ljust(size, b'\0')
sock.sendall(encoded)
def recv_fixed(sock, size=64):
"""接收固定长度消息"""
data = recv_exact(sock, size)
return data.rstrip(b'\0').decode('utf-8') if data else None
缺点:浪费带宽,消息长度受限。
def send_delimited(sock, msg, delimiter=b'\r\n'):
"""发送带分隔符的消息"""
sock.sendall(msg.encode('utf-8') + delimiter)
def recv_delimited(sock, delimiter=b'\r\n', buffer_size=4096):
"""接收带分隔符的消息"""
buffer = bytearray()
while True:
data = sock.recv(buffer_size)
if not data:
return None
buffer.extend(data)
if delimiter in buffer:
line, _, buffer = buffer.partition(delimiter)
return line.decode('utf-8')
HTTP 和 FTP 用 \r\n 作为分隔符。
上面的示例用了多线程,但实际项目中还有更好的方案:
asyncio 示例:
import asyncio
async def handle_client(reader, writer):
addr = writer.get_extra_info('peername')
print(f"新连接: {addr}")
try:
while True:
data = await reader.read(1024)
if not data:
break
msg = data.decode('utf-8')
print(f"收到 {addr}: {msg}")
writer.write(f"已收到: {msg}".encode('utf-8'))
await writer.drain()
except Exception as e:
print(f"处理 {addr} 时出错: {e}")
finally:
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle_client, '0.0.0.0', 8000)
async with server:
await server.serve_forever()
asyncio.run(main())
💡 决策树:需要可靠传输?→ TCP。需要实时性能,能容忍丢包?→ UDP。需要广播/组播?→ UDP。不确定?→ 默认选 TCP。
HTTP(HyperText Transfer Protocol,超文本传输协议)是万维网数据通信的基础。
类比:
设计初衷:提供一种发布和接收 HTML 页面的方法。
工作流程:
HTTP/1.1 200 OK)和内容HTTP 请求由四个部分组成:请求行、请求头、空行、请求体。

GET /index.html HTTP/1.1 ← 请求行:我要用什么方法,访问什么资源
Host: example.com ← 请求头:补充信息(访问哪个网站)
User-Agent: Python/3.11 (客户端是什么)
Accept: application/json (支持什么格式)
← 空行:请求头结束
{"name": "test"} ← 请求体:POST/PUT 等带的数据
请求行包含:
常见请求头:

Host | Host: example.com | |
User-Agent | User-Agent: Chrome/120 | |
Accept | Accept: application/json | |
Content-Length | Content-Length: 42 | |
Content-Type | Content-Type: application/json |
HTTP 响应由四个部分组成:状态行、消息报头、空行、响应正文。

HTTP/1.1 200 OK ← 状态行:请求成功了吗?
Content-Type: application/json ← 响应头:补充信息(返回什么格式)
Content-Length: 42 (返回数据多大)
← 空行:响应头结束
{"id": 123, "name": "张三"} ← 响应正文:实际返回的数据

HTTP/1.1 定义了八种方法,HTTP 服务器至少应该实现 GET 和 HEAD 方法,其他方法都是可选的。
| GET | ||||
| HEAD | ||||
| POST | ||||
| PUT | ||||
| DELETE | ||||
| TRACE | ||||
| OPTIONS | ||||
| CONNECT |
关键概念:
⚠️ 注意:GET 方法只应用于读取,不应产生"副作用"。因为 GET 可能被网络爬虫随意访问,如果 GET 请求会修改数据,后果很严重。
状态码分为五类:
表示请求正在处理。
表示请求正常处理完毕。
表示需要后续操作才能完成请求。
表示请求包含语法错误或无法完成。
表示服务器处理请求时发生错误。
💡 记忆技巧:2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务器错。
HTTPS = HTTP + TLS/SSL。
类比:
核心概念:
requests 是 Python 中最常用的 HTTP 库。
import requests
# 一言网的 API 地址
url = 'https://v1.hitokoto.cn/'
# 请求参数
params = {
'c': 'a', # 类型:a 动画、b 漫画、c 游戏...
'encode': 'json'
}
try:
print(f"正在发送 GET 请求到: {url},参数: {params}")
response = requests.get(url, params=params)
status_code = response.status_code
if status_code == 200:
print(f"请求成功!状态码: {status_code}")
data = response.json()
hitokoto = data['hitokoto']
from_who = data['from_who'] if data['from_who'] else'未知'
print(f"随机名言: {hitokoto} - {from_who}")
elif status_code == 404:
print(f"请求的资源未找到!状态码: {status_code}")
elif status_code == 500:
print(f"服务器内部错误!状态码: {status_code}")
else:
print(f"发生未知错误,状态码: {status_code}")
except requests.RequestException as e:
print(f"请求过程中出现错误: {e}")
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session(
retries=3,
backoff_factor=0.3,
status_forcelist=(500, 502, 503, 504),
timeout=10
):
"""创建带重试机制的 requests Session"""
session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
allowed_methods=["GET", "POST", "PUT", "DELETE"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 设置默认请求头
session.headers.update({
'User-Agent': 'MyApp/1.0',
'Accept': 'application/json'
})
return session
# 使用示例
session = create_session()
try:
response = session.get(
'https://v1.hitokoto.cn/',
params={'c': 'a', 'encode': 'json'},
timeout=10
)
response.raise_for_status()
data = response.json()
print(f"名言: {data['hitokoto']} —— {data.get('from_who', '未知')}")
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
import requests
from typing import Optional, Dict
class HitokotoClient:
"""一言网 API 客户端"""
BASE_URL = 'https://v1.hitokoto.cn/'
TYPES = {
'a': '动画', 'b': '漫画', 'c': '游戏',
'd': '文学', 'e': '原创', 'f': '来自网络',
'g': '其他', 'h': '影视', 'i': '诗词',
'j': '网易云', 'k': '哲学', 'l': '抖机灵'
}
def __init__(self, timeout: int = 10):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'HitokotoPythonClient/1.0',
'Accept': 'application/json'
})
self.timeout = timeout
def get_hitokoto(self, type_code: str = 'a') -> Optional[Dict]:
"""获取随机一言"""
params = {'c': type_code, 'encode': 'json'}
try:
response = self.session.get(
self.BASE_URL,
params=params,
timeout=self.timeout
)
response.raise_for_status()
data = response.json()
return {
'id': data['id'],
'content': data['hitokoto'],
'author': data.get('from_who') or'未知',
'type': self.TYPES.get(type_code, '未知'),
'source': data.get('from', '')
}
except requests.RequestException as e:
print(f"获取一言失败: {e}")
return None
def close(self):
self.session.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# 使用示例
with HitokotoClient() as client:
result = client.get_hitokoto('i')
if result:
print(f"「{result['content']}」")
print(f"—— {result['author']}({result['type']})")
Starlette 是一个轻量级的 Python 异步 Web 框架,专为高性能异步应用设计。
类比:
特点:
💡 关系:FastAPI 就是基于 Starlette 构建的,额外加入了数据验证(Pydantic)和自动文档功能。
| Uvicorn | ||
| Starlette | ||
| 协作方式 |
pip install starlette uvicorn requests
"""hitokoto_api.py"""
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
import requests
import uvicorn
HITOKOTO_URL = 'https://v1.hitokoto.cn/'
async def get_hitokoto():
"""获取随机名言"""
try:
params = {'c': 'a', 'encode': 'json'}
response = requests.get(HITOKOTO_URL, params=params)
status_code = response.status_code
if status_code == 200:
data = response.json()
hitokoto = data['hitokoto']
from_who = data['from_who'] if data['from_who'] else'未知'
return {'hitokoto': hitokoto, 'from_who': from_who}
else:
return {'error': f'请求一言网 API 失败,状态码: {status_code}'}
except requests.RequestException as e:
return {'error': f'请求过程中出现错误: {str(e)}'}
async def homepage(request):
"""处理根路径请求"""
result = await get_hitokoto()
return JSONResponse(result)
app = Starlette(debug=True, routes=[
Route('/', homepage),
])
if __name__ == "__main__":
uvicorn.run(app, host='0.0.0.0', port=8000)
"""hitokoto_api_advanced.py"""
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.middleware.cors import CORSMiddleware
import httpx
import uvicorn
HITOKOTO_URL = 'https://v1.hitokoto.cn/'
TYPES = {
'a': '动画', 'b': '漫画', 'c': '游戏',
'd': '文学', 'e': '原创', 'f': '来自网络',
'g': '其他', 'h': '影视', 'i': '诗词',
'j': '网易云', 'k': '哲学', 'l': '抖机灵'
}
async def get_hitokoto(type_code: str = 'a'):
"""异步获取随机名言"""
async with httpx.AsyncClient(timeout=10.0) as client:
try:
response = await client.get(HITOKOTO_URL, params={
'c': type_code,
'encode': 'json'
})
response.raise_for_status()
data = response.json()
return {
'success': True,
'data': {
'id': data['id'],
'content': data['hitokoto'],
'author': data.get('from_who') or'未知',
'type': TYPES.get(type_code, '未知'),
'source': data.get('from', '')
}
}
except httpx.HTTPError as e:
return {'success': False, 'error': f'请求失败: {str(e)}'}
async def homepage(request):
"""根路径:返回 API 说明"""
return JSONResponse({
'message': '一言 API 服务',
'endpoints': {
'/': 'API 说明',
'/hitokoto': '获取随机一言(默认动画类)',
'/hitokoto/{type}': '获取指定类型的一言',
},
'types': TYPES
})
async def random_hitokoto(request):
"""获取随机一言"""
return JSONResponse(await get_hitokoto())
async def typed_hitokoto(request):
"""获取指定类型的一言"""
type_code = request.path_params.get('type', 'a')
return JSONResponse(await get_hitokoto(type_code))
app = Starlette(
debug=True,
routes=[
Route('/', homepage),
Route('/hitokoto', random_hitokoto),
Route('/hitokoto/{type}', typed_hitokoto),
]
)
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_methods=['*'],
allow_headers=['*'],
)
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8000)
pip install starlette uvicorn httpx
python hitokoto_api_advanced.py
curl http://localhost:8000/
curl http://localhost:8000/hitokoto
curl http://localhost:8000/hitokoto/i
get_hitokoto() | |
homepage() | |
random_hitokoto() | get_hitokoto() 返回 JSON 响应 |
typed_hitokoto() | |
Starlette | |
CORSMiddleware |
⚠️ 注意:虽然 Starlette 支持异步,但
requests库是同步的。在异步环境中使用requests会阻塞事件循环,丧失异步优势。应该用httpx.AsyncClient或aiohttp。
写网络程序不会调试等于盲人摸象。这些工具你必须掌握:
curl 是一个命令行工具,用来发送 HTTP/HTTPS 请求。它可以:
类比:curl 就像"命令行版的浏览器"——你输入网址,它帮你访问并显示结果。
# 发送 GET 请求(-v 显示详细信息,包括请求头和响应头)
curl -v https://v1.hitokoto.cn/?c=a
# 发送 POST 请求(-X 指定方法,-H 添加请求头,-d 添加请求体)
curl -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d '{"name": "Python"}'
# 只查看响应头(-I)
curl -I https://example.com
# 带超时和重试(--connect-timeout 连接超时,--max-time 总超时,--retry 重试次数)
curl --connect-timeout 10 --max-time 30 --retry 3 https://example.com
# 保存响应到文件(-o)
curl -o output.html https://example.com
# 跟随重定向(-L)
curl -L https://example.com
# 添加请求头(-H)
curl -H "Authorization: Bearer token123" https://api.example.com
# 上传文件(-F)
curl -F "file=@/path/to/file.txt" https://example.com/upload
.zip 包,解压后添加到系统 PATHbrew install curlsudo apt install curl 或 sudo yum install curlnetstat 和 ss 用来查看网络连接状态、端口占用、路由表等信息。
区别:
netstat:老牌工具,所有系统都支持ss:新一代工具,速度更快,推荐 Linux 使用类比:netstat/ss 就像"网络监控摄像头"——可以看到谁在连接、哪个端口被占用。
# 查看所有监听端口(-t TCP,-l 监听,-n 数字显示,-p 显示进程)
netstat -tlnp
# 查看所有连接状态
netstat -an
# 查看 ESTABLISHED 状态的连接
netstat -an | grep ESTABLISHED
# 查看端口 8000 的占用情况
netstat -tlnp | grep 8000
# 查看路由表
netstat -r
# 查看网络统计信息
netstat -s
ss 命令(Linux 推荐):
# 查看所有监听端口
ss -tlnp
# 查看所有连接
ss -an
# 查看端口 8000 的占用
ss -tlnp | grep 8000
# 查看连接统计
ss -s
# 查看 TIME_WAIT 状态的连接数
ss -o state time-wait | wc -l
iproute2 包的一部分,Linux 自带man netstat 或 man ssWireshark 是一个图形化抓包工具,可以捕获和分析网络数据包。
功能:
类比:Wireshark 就像"网络显微镜"——可以看到网络中传输的每一个数据包。
启动和抓包:
常用过滤表达式:
# 过滤特定端口
tcp.port == 8000
# 只看 HTTP 协议
http
# 过滤特定 IP
ip.addr == 192.168.1.100
# 过滤源 IP
ip.src == 192.168.1.100
# 只看 SYN 包(TCP 握手)
tcp.flags.syn == 1
# 只看 HTTP GET 请求
http.request.method == "GET"
# 过滤特定域名
http.host == "example.com"
# 组合过滤
tcp.port == 8000 && http
查看 TCP 三次握手:
tcp.flags.syn == 1sudo apt install wireshark 或 sudo yum install wiresharktcpdump 是命令行抓包工具,功能类似 Wireshark,但没有图形界面。
适用场景:
类比:tcpdump 就像"命令行版的 Wireshark"——功能相同,但用命令行操作。
# 抓取 8000 端口的流量,保存到文件
tcpdump -i any port 8000 -w capture.pcap
# 查看实时流量(ASCII 显示)
tcpdump -i any port 8000 -A
# 抓取 HTTP 请求和响应(显示内容)
tcpdump -i any port 8000 -A -s 0 'tcp'
# 抓取 100 个数据包后停止
tcpdump -i any -c 100
# 过滤特定主机
tcpdump -i any host 192.168.1.100
# 过滤特定端口范围
tcpdump -i any portrange 8000-9000
# 读取 pcap 文件
tcpdump -r capture.pcap
# 显示更详细的信息(-vvv 最详细)
tcpdump -i any port 8000 -vvv
常用参数:
-i any:监听所有接口-w file.pcap:保存到文件-r file.pcap:读取文件-A:ASCII 显示-s 0:捕获完整数据包(不截断)-c N:抓 N 个包后停止-n:不解析域名(直接显示 IP)sudo apt install tcpdump 或 sudo yum install tcpdumpPython 提供了内置的调试工具,可以在代码中打印网络相关的调试信息。
import socket
import logging
# 开启 socket 调试日志
logging.basicConfig(level=logging.DEBUG)
# 或者设置环境变量(在命令行中)
# PYTHONASYNCIODEBUG=1 python your_script.py
# 查看 socket 的详细信息
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(f"Socket family: {sock.family}") # 地址族
print(f"Socket type: {sock.type}") # 类型
print(f"Socket timeout: {sock.gettimeout()}") # 超时时间
print(f"Socket blocking: {sock.getblocking()}") # 是否阻塞
# 查看本地地址
sock.bind(('0.0.0.0', 8000))
print(f"Bound to: {sock.getsockname()}")
# 查看 socket 选项
print(f"SO_REUSEADDR: {sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)}")
print(f"SO_SNDBUF: {sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)}")
print(f"SO_RCVBUF: {sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)}")
import socket
# 1. 打印所有可用的网络接口
print(socket.getaddrinfo('localhost', 80))
# 2. 查看主机名对应的 IP
print(socket.gethostbyname('localhost'))
# 3. 查看本机主机名
print(socket.gethostname())
# 4. 获取本机所有 IP 地址
print(socket.gethostbyname_ex(socket.gethostname()))
# 5. 测试端口是否连通
def test_connection(host, port, timeout=5):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
s.connect((host, port))
print(f"✅ {host}:{port} 连通")
return True
except Exception as e:
print(f"❌ {host}:{port} 不通: {e}")
return False
test_connection('www.baidu.com', 80)
test_connection('127.0.0.1', 8000)
不同 CPU 架构存储多字节数据的方式不同:
网络传输统一使用大端序(网络字节序),所以跨平台通信时需要转换:
import struct
import socket
num = 12345
network_order = socket.htons(num)
network_order_32 = socket.htonl(num)
host_order = socket.ntohs(network_order)
host_order_32 = socket.ntohl(network_order_32)
data = struct.pack('!I', 12345)
value = struct.unpack('!I', data)[0]
💡 什么时候会踩坑? 用 C 写服务端、Python 写客户端,或者反过来,如果没统一字节序,解析出来的数字会完全错乱。
DNS(Domain Name System)是互联网的"电话簿"。
类比:
import socket
ip = socket.gethostbyname('www.example.com')
print(f"www.example.com → {ip}")
infos = socket.getaddrinfo('www.example.com', 80, socket.AF_INET, socket.SOCK_STREAM)
for info in infos:
print(f"地址族: {info[0]}, 类型: {info[1]}, 地址: {info[4]}")
try:
hostname = socket.gethostbyaddr('8.8.8.8')
print(f"8.8.8.8 → {hostname[0]}")
except socket.herror:
print("无反向解析记录")
Address already in use原因:端口处于 TIME_WAIT 状态(默认 60 秒)。解决:设置 SO_REUSEADDR。
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
recv 一次收不到完整数据原因:TCP 是字节流,没有消息边界。解决:用长度前缀、固定长度或分隔符界定消息(见第 8 章)。
原因:线程/进程开销太大,或文件描述符限制。解决:
ulimit -n 65535
用 asyncio + epoll 替代多线程。
原因:编码不一致。解决:统一用 UTF-8,encode('utf-8') 和 decode('utf-8') 配对使用。
原因:网络不通、防火墙拦截、服务端未启动。解决:
tcp_socket.settimeout(10.0)
原因:在 asyncio 里调用 requests.get() 会阻塞事件循环。解决:用 httpx.AsyncClient 或 aiohttp。
# ❌ 错误
async def bad_example():
response = requests.get('https://api.example.com')
return response.json()
# ✅ 正确
async def good_example():
async with httpx.AsyncClient() as client:
response = await client.get('https://api.example.com')
return response.json()
原因:x86 是小端序,网络传输是大端序。解决:用 struct.pack('!...') 或 socket.htons/htonl。
原因:超过 MTU(1500 字节)导致分片丢失。解决:单个 UDP 数据包控制在 1400 字节以内。
网络编程的知识体系从底层到上层大致是:
物理层/数据链路层 → 网络层(IP) → 传输层(TCP/UDP) → 应用层(HTTP)
实际开发中,你不需要从 Socket 开始写所有东西。Python 生态提供了丰富的抽象:
socket | ||
requestshttpx | ||
StarletteFastAPI、Django | ||
asynciosocket |
理解底层原理很重要——当你的服务出现连接超时、数据错乱、性能瓶颈时,只有理解 TCP/IP 才能快速定位问题。
网络编程是一个"入门容易精通难"的领域。希望这篇文章能帮你少踩几个坑。