跳到主要内容
预计阅读 34 分钟

网络编程实战 — 从 Socket 到微服务通信

理解了网络协议的原理之后,是时候动手写代码了。本章将从最底层的 Socket 编程出发,一步步上升到 RESTful API 设计和 gRPC 高性能通信框架,让你把网络知识转化为实战能力。

📋 开篇自测:你已经知道多少?

  1. TCP Socket 编程中,服务端的 listen、accept、read/write 分别对应 TCP 状态机的哪些阶段?
  2. RESTful API 的核心设计原则有哪些?资源命名有什么规范?
  3. 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.pyuser_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

六、章节小结

  1. Socket 是操作系统提供的网络编程接口,TCP Socket 需要经历创建、绑定、监听、接受、读写、关闭的完整生命周期。
  2. I/O 多路复用(epoll)是实现高并发网络服务的基石,一个线程可以管理数万个连接。
  3. RESTful API 是 Web 服务最主流的设计风格,核心是资源导向、HTTP 方法语义化、无状态。
  4. gRPC 基于 HTTP/2 和 Protocol Buffers,提供高性能的跨语言 RPC 通信,适合微服务内部调用。
  5. 网络调试工具(tcpdump、curl、Wireshark)是排查网络问题的必备利器。

📝 结尾自测:检验你的收获

  1. TCP Socket 编程中,服务端调用 accept() 时实际发生了什么?它和 listen() 的关系是什么?
  2. 为什么 epoll 的性能比 select 好?它们在连接数很大时的效率差异在哪里?
  3. 设计一个用户管理的 RESTful API,列出 CRUD 操作的 URL、HTTP 方法和状态码。
  4. gRPC 的四种通信模式分别适合什么场景?服务端流式 RPC 和 WebSocket 有什么区别?
  5. 用 curl 命令如何查看一次 HTTPS 请求的各阶段耗时(DNS、TCP、TLS、首字节)?

下一章预告:单台服务器搞定一切的时代已经过去了。下一章我们将进入分布式网络架构的世界——微服务之间如何通信、服务发现如何工作、API 网关扮演什么角色,以及如何在生产环境中排查复杂的网络故障。

购买课程解锁全部内容

网络通信第一课:10 章掌握计算机网络

¥29.90