网络编程实战 — 从 Socket 到微服务通信
理解了网络协议的原理之后,是时候动手写代码了。本章将从最底层的 Socket 编程出发,一步步上升到 RESTful API 设计和 gRPC 高性能通信框架,让你把网络知识转化为实战能力。
📋 开篇自测:你已经知道多少?
- TCP Socket 编程中,服务端的 listen、accept、read/write 分别对应 TCP 状态机的哪些阶段?
- RESTful API 的核心设计原则有哪些?资源命名有什么规范?
- gRPC 相比传统 REST API 有什么优势?它使用什么序列化格式?
一、Socket 编程基础
1.1 Socket 是什么
Socket(套接字)是操作系统提供的网络编程接口,它是应用层与传输层之间的”门”。通过 Socket API,程序可以发送和接收网络数据,而不需要关心底层的 TCP/IP 实现细节。
应用程序的视角:
应用层代码
|
| socket(), bind(), listen(), accept(), read(), write()
|
+---v---+
| Socket | <-- 操作系统提供的抽象接口
+---+---+
|
+---v---+
| TCP/IP | <-- 内核协议栈
+---+---+
|
+---v---+
| 网卡 | <-- 硬件
+-------+
1.2 TCP Socket 通信流程
服务端 客户端
| |
| socket() 创建套接字 |
| bind() 绑定IP和端口 |
| listen() 开始监听 |
| 进入LISTEN状态 |
| | socket() 创建套接字
| | connect() 发起连接
| <---------- 三次握手 ----------> |
| accept() 接受连接,返回新Socket |
| 进入ESTABLISHED状态 | ESTABLISHED状态
| |
| read() <--- 数据传输 ---> write() |
| write() <--- 数据传输 ---> read() |
| |
| close() | close()
| <---------- 四次挥手 ----------> |
1.3 Python TCP 服务端示例
import socket
import threading
def handle_client(client_socket, address):
"""处理单个客户端连接"""
print(f"新连接来自: {address}")
try:
while True:
data = client_socket.recv(1024) # 接收最多1024字节
if not data:
break # 客户端关闭连接
message = data.decode('utf-8')
print(f"收到 [{address}]: {message}")
# 回声服务: 原样返回
client_socket.sendall(f"Echo: {message}".encode('utf-8'))
finally:
client_socket.close()
print(f"连接关闭: {address}")
def main():
# 1. 创建 TCP Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 允许端口复用(避免TIME_WAIT导致无法重启)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 3. 绑定地址和端口
server_socket.bind(('0.0.0.0', 8080))
# 4. 开始监听,backlog=128
server_socket.listen(128)
print("服务器启动,监听端口 8080...")
try:
while True:
# 5. 接受新连接(阻塞等待)
client_socket, address = server_socket.accept()
# 6. 为每个客户端创建一个线程
thread = threading.Thread(
target=handle_client,
args=(client_socket, address)
)
thread.daemon = True # 注意: 仅用于教学示例。生产环境应使用线程池或异步框架,
# 因为 daemon 线程会在主线程退出时被强制终止,可能导致数据丢失。
thread.start()
finally:
server_socket.close()
if __name__ == '__main__':
main()
1.4 Python TCP 客户端示例
import socket
def main():
# 1. 创建 Socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务器
client_socket.connect(('127.0.0.1', 8080))
print("已连接到服务器")
try:
while True:
message = input("输入消息 (q退出): ")
if message == 'q':
break
# 3. 发送数据
client_socket.sendall(message.encode('utf-8'))
# 4. 接收响应
data = client_socket.recv(1024)
print(f"服务器回复: {data.decode('utf-8')}")
finally:
# 5. 关闭连接
client_socket.close()
if __name__ == '__main__':
main()
1.5 UDP Socket 编程
UDP 编程比 TCP 简单——不需要建立连接,直接发送和接收数据报:
# UDP 服务端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM = UDP
server.bind(('0.0.0.0', 8080))
while True:
data, addr = server.recvfrom(1024) # 接收数据和发送方地址
print(f"收到来自 {addr}: {data.decode()}")
server.sendto(f"Echo: {data.decode()}".encode(), addr)
# UDP 客户端
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto("Hello".encode(), ('127.0.0.1', 8080))
data, addr = client.recvfrom(1024)
print(f"回复: {data.decode()}")
🤔 想一想 为什么 TCP 服务端需要 listen + accept 两步,而 UDP 不需要?这和 TCP 面向连接的特性有什么关系?
二、I/O 模型与高并发
2.1 五种 I/O 模型
1. 阻塞I/O (Blocking I/O)
线程调用 read() --> 阻塞等待数据 --> 数据到达 --> 返回
问题: 一个连接占用一个线程
2. 非阻塞I/O (Non-blocking I/O)
线程调用 read() --> 没数据立即返回EAGAIN --> 不断轮询 --> 数据到达
问题: 忙轮询浪费 CPU
3. I/O 多路复用 (I/O Multiplexing)
select/poll/epoll 同时监听多个 Socket
--> 有事件就绪时通知程序 --> 处理就绪的 Socket
优点: 一个线程管理成千上万个连接
4. 信号驱动I/O
注册信号处理函数 --> 数据就绪时内核发信号 --> 处理
5. 异步I/O (Asynchronous I/O)
发起异步read --> 内核完成数据拷贝后通知 --> 处理
真正的"全异步",Linux AIO / io_uring
2.2 epoll:高并发的基石
select vs poll vs epoll:
+----------+------------------+------------------+------------------+
| | select | poll | epoll |
+----------+------------------+------------------+------------------+
| 最大连接 | 默认1024 (FD_SETSIZE)| 无限制 | 无限制 |
| 效率 | O(n) 每次遍历全部 | O(n) 每次遍历 | O(1) 事件通知 |
| 数据拷贝 | 每次调用拷贝fd集合 | 每次调用拷贝 | 内核与用户共享 |
| 触发模式 | 水平触发 | 水平触发 | 水平/边缘触发 |
| 适用场景 | 连接数少 | 中等规模 | 大规模并发 |
+----------+------------------+------------------+------------------+
Nginx, Redis, Node.js 底层都使用 epoll (Linux) 或 kqueue (macOS)
2.3 常见的网络编程模型
模型1: 多进程/多线程
每个连接一个线程 --> 简单但资源消耗大
适用: 连接数少、处理逻辑重的场景
模型2: Reactor (事件驱动)
主线程负责事件监听(epoll) --> 工作线程处理业务
├── 单 Reactor 单线程: Redis
├── 单 Reactor 多线程: 常见方案
└── 多 Reactor 多线程: Netty, Nginx
模型3: Proactor (异步I/O)
完全异步,由OS完成I/O --> 通知应用处理结果
代表: Windows IOCP, Linux io_uring
三、RESTful API 设计
3.1 REST 的核心原则
REST(Representational State Transfer,表述性状态转移)不是协议,而是一种架构风格:
REST 六大原则:
1. 客户端-服务器分离: 前后端独立演化
2. 无状态: 每次请求包含所有必要信息,服务器不保存会话
3. 可缓存: 响应明确标识是否可缓存
4. 统一接口: 资源通过URI标识,操作通过HTTP方法表达
5. 分层系统: 客户端不需要知道是否直连服务器
6. 按需代码(可选): 服务器可以传输可执行代码
3.2 URL 设计规范
良好的 REST URL 设计:
资源命名(使用名词、复数形式):
GET /api/v1/users 获取用户列表
GET /api/v1/users/123 获取单个用户
POST /api/v1/users 创建用户
PUT /api/v1/users/123 完整更新用户
PATCH /api/v1/users/123 部分更新用户
DELETE /api/v1/users/123 删除用户
嵌套资源:
GET /api/v1/users/123/orders 用户123的订单列表
GET /api/v1/users/123/orders/456 用户123的订单456
查询和过滤:
GET /api/v1/users?page=2&limit=20 分页
GET /api/v1/users?sort=created_at:desc 排序
GET /api/v1/users?status=active 过滤
反面教材(不要这样做):
GET /api/getUsers <-- 动词不该出现在URL中
POST /api/deleteUser/123 <-- 应该用 DELETE 方法
GET /api/user <-- 应该用复数 users
3.3 响应设计
// 成功响应 (200 OK)
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"created_at": "2026-03-18T10:30:00Z"
}
}
// 列表响应 (200 OK)
{
"code": 0,
"data": {
"items": [...],
"total": 100,
"page": 1,
"limit": 20
}
}
// 错误响应 (400 Bad Request)
{
"code": 40001,
"message": "Validation failed",
"errors": [
{"field": "email", "message": "Invalid email format"},
{"field": "name", "message": "Name is required"}
]
}
3.4 API 版本控制
三种常见的版本控制方式:
1. URL 路径版本(最常用):
/api/v1/users
/api/v2/users
2. 请求头版本:
Accept: application/vnd.example.v1+json
3. 查询参数版本:
/api/users?version=1
🤔 想一想 RESTful API 要求”无状态”,但实际应用中往往需要用户登录状态。JWT(JSON Web Token)是如何在无状态的前提下实现认证的?
四、gRPC:高性能 RPC 框架
4.1 RPC 的概念
RPC(Remote Procedure Call)让调用远程服务就像调用本地函数一样简单:
传统方式(开发者要关心网络细节):
1. 序列化参数
2. 建立TCP连接
3. 发送数据
4. 接收响应
5. 反序列化结果
RPC方式(网络细节被封装):
result = remote_service.get_user(user_id=123)
# 看起来就像调用本地函数!
4.2 gRPC 概览
gRPC 是 Google 开源的高性能 RPC 框架:
gRPC 技术栈:
+------------------+
| 应用代码 |
+------------------+
| 生成的 Stub 代码 | <-- 由 .proto 文件自动生成
+------------------+
| gRPC 框架 |
+------------------+
| HTTP/2 | <-- 利用多路复用和头部压缩
+------------------+
| Protocol Buffers | <-- 高效的二进制序列化
+------------------+
4.3 Protocol Buffers
gRPC 使用 Protocol Buffers(简称 protobuf)作为接口定义语言和序列化格式:
// user.proto - 接口定义文件
syntax = "proto3";
package user;
// 定义消息类型
message User {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message GetUserRequest {
int64 user_id = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 limit = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
}
// 定义服务
service UserService {
// 一元RPC: 请求-响应
rpc GetUser(GetUserRequest) returns (User);
// 服务端流式RPC: 服务端返回数据流
rpc ListUsers(ListUsersRequest) returns (stream User);
// 客户端流式RPC: 客户端发送数据流
rpc UploadUsers(stream User) returns (ListUsersResponse);
// 双向流式RPC: 双方都发送数据流
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
4.4 gRPC 的四种通信模式
1. 一元 RPC (Unary):
客户端 --[单个请求]--> 服务端 --[单个响应]--> 客户端
类似普通的 HTTP 请求-响应
2. 服务端流式 (Server Streaming):
客户端 --[单个请求]--> 服务端 --[响应1]--> 客户端
--[响应2]-->
--[响应3]-->
--[完成]-->
场景: 订阅实时数据、大量结果分批返回
3. 客户端流式 (Client Streaming):
客户端 --[请求1]--> 服务端
--[请求2]-->
--[请求3]-->
--[完成]--> --[单个响应]--> 客户端
场景: 批量上传数据
4. 双向流式 (Bidirectional Streaming):
客户端 <===[双向数据流]===> 服务端
场景: 聊天、实时协作
4.5 gRPC Python 实战示例
假设已通过 python -m grpc_tools.protoc 从上面的 user.proto 生成了 user_pb2.py 和 user_pb2_grpc.py,以下是服务端和客户端的核心代码:
# server.py - gRPC 服务端
import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc
class UserServicer(user_pb2_grpc.UserServiceServicer):
def GetUser(self, request, context):
# 模拟从数据库查询用户
return user_pb2.User(
id=request.user_id,
name="Alice",
email="alice@example.com",
age=30,
)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserServicer(), server)
server.add_insecure_port("[::]:50051")
server.start()
print("gRPC 服务端启动,监听端口 50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()
# client.py - gRPC 客户端
import grpc
import user_pb2
import user_pb2_grpc
def main():
with grpc.insecure_channel("localhost:50051") as channel:
stub = user_pb2_grpc.UserServiceStub(channel)
response = stub.GetUser(user_pb2.GetUserRequest(user_id=1))
print(f"用户: {response.name}, 邮箱: {response.email}")
if __name__ == "__main__":
main()
4.6 gRPC vs REST 对比
+------------------+------------------+------------------+
| 特性 | REST | gRPC |
+------------------+------------------+------------------+
| 协议 | HTTP/1.1 或 HTTP/2 | HTTP/2 |
| 数据格式 | JSON (文本) | Protobuf (二进制) |
| 序列化速度 | 较慢 | 快 5-10 倍 |
| 数据体积 | 较大 | 小 3-10 倍 |
| 接口定义 | OpenAPI/无 | .proto 强类型 |
| 代码生成 | 可选 | 内置 |
| 流式通信 | 不原生支持 | 4种流式模式 |
| 浏览器支持 | 原生支持 | 需要 gRPC-Web |
| 可读性 | 高(JSON) | 低(二进制) |
| 适用场景 | 对外API、Web前端 | 微服务内部通信 |
+------------------+------------------+------------------+
五、网络调试工具
5.1 常用工具速查
抓包分析:
tcpdump -- 命令行抓包工具
Wireshark -- 图形化协议分析器
Charles -- HTTP/HTTPS 代理调试
网络诊断:
ping -- 测试连通性和延迟
traceroute -- 追踪路由路径
nslookup -- DNS 查询
dig -- DNS 详细查询
netstat -- 网络连接状态
ss -- netstat 的现代替代品
HTTP 调试:
curl -- 命令行 HTTP 客户端
httpie -- 更友好的 HTTP 客户端
Postman -- 图形化 API 测试工具
5.2 tcpdump 实战
# 抓取指定端口的TCP包
tcpdump -i eth0 tcp port 80 -nn
# 抓取与特定主机的通信
tcpdump -i any host 192.168.1.100
# 抓取TCP三次握手
tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0'
# 保存为pcap文件(可用Wireshark打开)
tcpdump -i eth0 -w capture.pcap
# 只看HTTP GET请求
tcpdump -i eth0 -A 'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420'
5.3 curl 常用技巧
# 查看请求和响应头
curl -v https://api.example.com/users
# 发送 JSON POST 请求
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# 查看响应时间分解
curl -w "@-" -o /dev/null -s https://example.com <<'EOF'
DNS解析: %{time_namelookup}s
TCP连接: %{time_connect}s
TLS握手: %{time_appconnect}s
首字节时间: %{time_starttransfer}s
总耗时: %{time_total}s
EOF
六、章节小结
- Socket 是操作系统提供的网络编程接口,TCP Socket 需要经历创建、绑定、监听、接受、读写、关闭的完整生命周期。
- I/O 多路复用(epoll)是实现高并发网络服务的基石,一个线程可以管理数万个连接。
- RESTful API 是 Web 服务最主流的设计风格,核心是资源导向、HTTP 方法语义化、无状态。
- gRPC 基于 HTTP/2 和 Protocol Buffers,提供高性能的跨语言 RPC 通信,适合微服务内部调用。
- 网络调试工具(tcpdump、curl、Wireshark)是排查网络问题的必备利器。
📝 结尾自测:检验你的收获
- TCP Socket 编程中,服务端调用 accept() 时实际发生了什么?它和 listen() 的关系是什么?
- 为什么 epoll 的性能比 select 好?它们在连接数很大时的效率差异在哪里?
- 设计一个用户管理的 RESTful API,列出 CRUD 操作的 URL、HTTP 方法和状态码。
- gRPC 的四种通信模式分别适合什么场景?服务端流式 RPC 和 WebSocket 有什么区别?
- 用 curl 命令如何查看一次 HTTPS 请求的各阶段耗时(DNS、TCP、TLS、首字节)?
下一章预告:单台服务器搞定一切的时代已经过去了。下一章我们将进入分布式网络架构的世界——微服务之间如何通信、服务发现如何工作、API 网关扮演什么角色,以及如何在生产环境中排查复杂的网络故障。
购买课程解锁全部内容
网络通信第一课:10 章掌握计算机网络
¥29.90