Day 14:网络编程
🎯 学习目标:理解网络通信原理,掌握 UDP/TCP/HTTP 编程
1. 加锁解决线程安全问题(续 Day 13)
import threading
# 除了 Lock,还有 RLock(可重入锁)
rlock = threading.RLock()
def recursive_function(n):
with rlock: # 同一线程可以多次获取 RLock
if n > 0:
recursive_function(n - 1) # 递归时不会死锁
# 死锁演示(两个锁互相等待)
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread1():
with lock_a:
time.sleep(0.1)
with lock_b: # 等待 lock_b(被 thread2 持有)
pass
def thread2():
with lock_b:
time.sleep(0.1)
with lock_a: # 等待 lock_a(被 thread1 持有)
pass
# thread1 和 thread2 互相等待 → 死锁!
2. 进程和线程对比
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源 | 独立内存空间 | 共享进程内存 |
| 创建开销 | 大(复制整个进程) | 小(共享内存) |
| 通信 | IPC(Queue、Pipe) | 共享变量(需加锁) |
| GIL 影响 | 无(各自有独立 GIL) | IO 型不受影响 |
| 适用场景 | CPU 密集型 | IO 密集型 |
| 稳定性 | 一个崩了不影响其他 | 一个崩了全崩 |
3. 网络相关概念
3.1 IP 与端口
# 49152-65535:动态/私有端口
3.2 协议
graph TB APP[应用层:HTTP FTP DNS SMTP] --> TCP_UDP[传输层:TCP UDP] TCP_UDP --> IP[网络层:IP 地址与路由] IP --> LINK[链路层:网卡 网线 WiFi]
3.3 TCP vs UDP
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 需要建立连接(三次握手) | 无连接 |
| 可靠性 | 可靠,有确认重传机制 | 不可靠,可能丢包 |
| 顺序 | 保证有序 | 不保证 |
| 速度 | 相对慢 | 快 |
| 适用 | 网页、文件、邮件 | 视频、直播、游戏 |
| 类比 | 打电话(确认对方收到) | 寄信(不知道是否收到) |
4. UDP 通信
4.1 服务端
import socket
# 创建 UDP socket
udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.bind(("127.0.0.1", 8888))
print("UDP 服务器启动,等待消息...")
while True:
data, addr = udp_server.recvfrom(1024) # 接收(阻塞)
text = data.decode("utf-8")
print(f"收到来自 {addr} 的消息:{text}")
if text == "quit":
break
# 回复
reply = f"收到:{text}"
udp_server.sendto(reply.encode("utf-8"), addr)
udp_server.close()
4.2 客户端
import socket
# 创建 UDP socket
udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = input("输入消息(quit 退出):")
udp_client.sendto(msg.encode("utf-8"), ("127.0.0.1", 8888))
if msg == "quit":
break
data, addr = udp_client.recvfrom(1024)
print(f"服务器回复:{data.decode('utf-8')}")
udp_client.close()
5. TCP 通信
5.1 服务端
import socket
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 端口复用
tcp_server.bind(("127.0.0.1", 9999))
tcp_server.listen(5) # 最大等待连接数
print("TCP 服务器启动,等待连接...")
while True:
client_socket, client_addr = tcp_server.accept()
print(f"客户端 {client_addr} 已连接")
# 接收数据
data = client_socket.recv(1024)
print(f"收到:{data.decode('utf-8')}")
# 回复
client_socket.send(f"服务器收到:{data.decode('utf-8')}".encode("utf-8"))
client_socket.close()
tcp_server.close()
5.2 客户端
import socket
tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_client.connect(("127.0.0.1", 9999))
tcp_client.send("Hello TCP!".encode("utf-8"))
response = tcp_client.recv(1024)
print(f"服务器回复:{response.decode('utf-8')}")
tcp_client.close()
5.3 多线程 TCP 服务端(优化)
import socket
import threading
def handle_client(client_socket, client_addr):
"""处理单个客户端(运行在线程中)"""
print(f"新连接:{client_addr}")
try:
while True:
data = client_socket.recv(1024)
if not data:
break
print(f"{client_addr}:{data.decode('utf-8')}")
client_socket.send(f"Echo:{data.decode('utf-8')}".encode())
except ConnectionResetError:
print(f"{client_addr} 断开连接")
finally:
client_socket.close()
# 主循环
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_server.bind(("127.0.0.1", 9999))
tcp_server.listen(10)
print("多线程 TCP 服务器启动...")
while True:
client, addr = tcp_server.accept()
thread = threading.Thread(target=handle_client, args=(client, addr), daemon=True)
thread.start()
6. HTTP 请求
6.1 requests 库
import requests
# GET 请求
resp = requests.get("https://api.github.com")
print(f"状态码:{resp.status_code}")
print(f"Content-Type:{resp.headers['Content-Type']}")
data = resp.json()
# POST 请求
resp = requests.post(
"https://httpbin.org/post",
json={"name": "张三", "age": 20},
headers={"X-Custom": "value"}
)
print(resp.json())
# 带参数的 GET
resp = requests.get(
"https://api.example.com/search",
params={"q": "python", "page": 1}
)
# 超时和异常处理
try:
resp = requests.get("https://api.example.com", timeout=5)
resp.raise_for_status() # 非 200 状态码抛出异常
except requests.Timeout:
print("请求超时")
except requests.HTTPError as e:
print(f"HTTP 错误:{e}")
except requests.ConnectionError:
print("连接失败")
6.2 一言网案例
import requests
# 获取随机句子
def get_hitokoto():
"""从一言网获取一句随机名言"""
try:
resp = requests.get("https://v1.hitokoto.cn/", timeout=5)
data = resp.json()
return f"{data['hitokoto']}\n—— {data['from']}"
except Exception as e:
return f"获取失败:{e}"
print(get_hitokoto())
7. starlette 部署 Web 服务
from starlette.applications import Starlette
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Route
import uvicorn
# 路由处理函数
async def hello(request):
return JSONResponse({"message": "Hello World!"})
async def greet(request):
name = request.path_params.get("name", "World")
return PlainTextResponse(f"Hello, {name}!")
# 创建应用
app = Starlette(debug=True, routes=[
Route("/", hello),
Route("/greet/{name}", greet),
])
# 启动:uvicorn app:app --reload
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
后续学习
在大模型项目中,你会使用 FastAPI(基于 starlette)构建 API,用 asyncio 实现异步高并发。
速记卡(面试闪卡)
Q1:一句话讲清「Day 14:网络编程」到底是什么?
A:网络编程这块讲清三件事:进程线程怎么选、TCP/UDP 有啥区别、以及用 socket 和 requests 写通信。
Q2:进程和线程怎么对比? —— 怎么理解?
A:像公司和部门——进程是独立公司(独立内存、开销大、一个崩不影响其他、适合 CPU 密集);线程是部门(共享内存、开销小、一个崩全崩、适合 IO 密集)。进程间靠 IPC 通信,线程间共享变量要加锁。
Q3:TCP 和 UDP 有啥区别? —— 怎么理解?
A:TCP 像打电话——先三次握手建连接、可靠有序、慢,适合网页文件邮件;UDP 像寄信——无连接、可能丢包不保序、快,适合视频直播游戏。Python 里 socket.SOCK_STREAM 是 TCP,SOCK_DGRAM 是 UDP。
Q4:怎么写 UDP 和 TCP 通信? —— 怎么理解?
A:像寄信 vs 打电话的前台——UDP 用 socket + SOCK_DGRAM,bind 后 recvfrom/sendto 不建连直接收发;TCP 用 SOCK_STREAM,服务端 bind+listen+accept、客户端 connect,再 send/recv 可靠传,多线程服务端每连接开一线程。
Q5:requests 和 starlette 怎么用? —— 怎么理解?
A:requests 像派快递员——get/post 一行发 HTTP 请求,带 params/timeout/异常处理;starlette 像搭服务台——定义 Route 路由和 async 处理函数返回 JSONResponse,用 uvicorn 启动,是 FastAPI 的底层。
Q6:核心速记主线有哪些?
-
进程线程对比:进程独立内存开销大、线程共享内存开销小;进程适合CPU密集、线程适合IO密集
-
TCP vs UDP:TCP 可靠有序慢(打电话)、UDP 无连接快可能丢(寄信)
-
UDP 用 SOCK_DGRAM recvfrom/sendto;TCP 用 SOCK_STREAM 需 connect/accept
-
应用层:requests 发 HTTP 请求;starlette 搭异步 Web 服务(FastAPI 的底层)
口诀
A:进程独立线程共享,CPU密集进程扛。
TCP打电话UDP寄信,可靠快选你要的。
UDP不连直接发,TCP握手再传话。
requests派件starlette台,Web服务异步开。