1. 课程导入与网络编程全景

在开始写第一行网络代码之前,我们需要先建立一张"地图":Python 网络编程到底在操作什么对象?数据从一台机器到另一台机器,中间经过了哪些层?一个能长期运行的服务,从开发到上线要经历哪些阶段?把这三个问题想清楚,后面的代码才不会变成"照抄能跑、改了就崩"的碎片。

一、核心对象:套接字是网络编程的"句柄"

Python 网络编程最核心的对象是套接字(socket)。它不是一个抽象概念,而是操作系统提供的一个文件描述符——你可以像读写文件一样读写它,只不过读写的内容会经过网卡发出去。在 Python 中,socket 模块把这个系统调用封装成了对象:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

这里两个参数决定了套接字的"身份"。AF_INET 表示使用 IPv4 地址族,地址形如 127.0.0.1;SOCK_STREAM 表示使用面向连接的字节流,也就是 TCP。如果换成 SOCK_DGRAM,得到的就是 UDP 套接字。除此之外,还有 AF_INET6(IPv6)、AF_UNIX(本机进程间通信)等地址族。理解这两个参数,是理解后续所有网络代码的基础。

套接字对象上最常用的方法有:bind() 绑定本地地址和端口,listen() 进入监听状态,accept() 接受一个连接并返回新的套接字,connect() 主动连接远端,send()/recv() 收发数据,close() 关闭。服务端和客户端的区别,本质上就是这些方法的调用顺序不同。

二、协议栈分层:数据是怎么被"打包"的

网络通信不是一根线直连,而是分层协作。以 TCP 为例,应用层的数据向下经过传输层、网络层、链路层,每一层都会加上自己的头部,这个过程叫封装;到达对端后再逐层剥离头部,叫解封装。

TCP 数据封装与解封装路径 发送端 应用层 HTTP / 自定义协议 传输层 TCP 头 + 端口 网络层 IP 头 + IP 地址 链路层 帧头 + MAC 地址 数据 段(Segment) 包(Packet) 帧(Frame) 接收端 应用层 还原原始数据 传输层 校验、重组 网络层 路由、寻址 链路层 帧接收、校验 封装方向:应用数据 → TCP 段 → IP 包 → 以太网帧(每层加头) 解封装方向:帧 → 包 → 段 → 应用数据(每层剥头,逐层校验)

这张图解释了为什么"端口"和"IP 地址"是两个不同层次的概念:IP 地址在网络层用于定位主机,端口在传输层用于定位主机上的进程。一个 TCP 连接由四元组唯一标识:(源IP, 源端口, 目标IP, 目标端口)。理解这一点,就能理解为什么同一台机器可以同时运行多个 Web 服务——它们监听不同端口。

三、服务工程生命周期

写一个能跑的 echo 服务只需要十几行代码,但要让它在生产环境稳定运行,需要经历完整的生命周期。这个生命周期不是线性的,而是循环迭代的:

  • 需求与协议设计:明确服务做什么、消息格式是什么、是长连接还是短连接、是否需要心跳。
  • 原型实现:用最少的代码验证核心逻辑,比如 echo 服务先验证"收到什么就回什么"。
  • 并发模型选择:单线程阻塞、多线程、多进程、selectors 多路复用,还是 asyncio。这决定了服务能承载多少并发连接。
  • 健壮性处理:超时、断连、半包、异常输入、资源释放。这是原型和产品之间的分水岭。
  • 可观测性:日志、连接数、吞吐量、错误率。没有观测就没有运维。
  • 部署与压测:容器化、端口暴露、用 ab 或 wrk 验证承载能力。

本课程后续章节会沿着这条主线逐步展开。现在,我们先完成第一步:搭建环境并跑通第一个 TCP 回声服务。

四、环境准备与第一个 TCP 回声服务

Python 3.8 以上版本自带 socket 模块,无需额外安装。建议使用虚拟环境隔离依赖:

python3 -m venv venv
source venv/bin/activate        # Windows 使用 venv\Scripts\activate
python -c "import socket; print(socket.AF_INET, socket.SOCK_STREAM)"

如果输出 2 1,说明环境正常。接下来写服务端。echo 服务的语义是:客户端发什么,服务端原样返回。我们使用 127.0.0.1:9000 作为示例地址,这个地址只允许本机访问,适合开发调试。

# echo_server.py
import socket

HOST = "127.0.0.1"
PORT = 9000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, PORT))
    srv.listen(5)
    print(f"listening on {HOST}:{PORT}")

    while True:
        conn, addr = srv.accept()
        with conn:
            print(f"connected by {addr}")
            while True:
                data = conn.recv(1024)
                if not data:
                    break
                conn.sendall(data)

几个关键点值得说明。SO_REUSEADDR 允许服务重启后立即复用端口,避免 Address already in use;listen(5) 中的 5 是等待队列长度,表示最多允许 5 个已完成三次握手但尚未被 accept 的连接排队;recv(1024) 每次最多读 1024 字节,返回空字节串 b"" 表示对端关闭了连接,这是退出内层循环的信号;sendall() 会确保所有数据都发送出去,而 send() 可能只发送一部分。

客户端代码如下:

# echo_client.py
import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("127.0.0.1", 9000))
    s.sendall(b"hello, network")
    resp = s.recv(1024)
    print("received:", resp.decode())

先运行服务端,再运行客户端,客户端应输出 received: hello, network,服务端打印 connected by ('127.0.0.1', xxxxx)。这里的 xxxxx 是客户端随机分配的临时端口,每次运行都可能不同——这正是四元组中"源端口"的体现。

判断服务是否真正工作,不要只看"没报错"。可以用 ss -tlnp | grep 9000(Linux)或 netstat -ano | findstr 9000(Windows)确认端口处于 LISTEN 状态;再用 telnet 127.0.0.1 9000 手动输入字符,观察是否原样回显。这两个动作分别验证了"监听"和"数据通路"两件事。