2. Socket编程基础与最小可用通信

这一章,我们来亲手搭建一个 TCP 服务端。说白了,就是让一台机器「蹲」在一个端口上,等着别人来找它说话。

我刚开始学网络编程时,总觉得这玩意儿很玄乎。后来做第一个线上项目,服务端一启动就崩,排查了半天才发现是端口被占用了……嗯,从那以后,我对每一步都格外小心。

2.1 整体流程概览

一个标准的 TCP Server,生命周期就这六步:

  1. 创建 Socket —— 向操作系统申请一个网络通信的「门」
  2. 绑定地址 —— 把这个门挂到某个 IP 和端口上
  3. 监听连接 —— 告诉内核:我准备好了,有人敲门就通知我
  4. 接受连接 —— 真正把来访者「领进门」,得到一个专用的通信通道
  5. 收发数据 —— 用这个通道和客户端聊天
  6. 关闭连接 —— 聊完了,礼貌地说再见,释放资源

下面这张图,把这六个步骤串了起来,你可以先看个全貌:

TCP Server 六步流程 TCP Server 核心流程 1. socket() 创建 Socket 2. bind() 绑定地址 3. listen() 监听连接 4. accept() 接受连接 5. recv/send 收发数据 6. close() 关闭连接 💡 关键理解 • socket() 创建的是「监听 Socket」,accept() 返回的是「连接 Socket」 • 一个服务端通常只有一个监听 Socket,但可以有多个连接 Socket • 关闭连接时,要区分「关闭监听」和「关闭单个客户端连接」

2.2 创建 Socket

在 Python 里,创建 Socket 就一行代码:

import socket

# 创建一个 IPv4 的 TCP Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

这里有两个参数:

  • AF_INET —— 指定使用 IPv4 地址族。如果你要玩 IPv6,就用 AF_INET6。
  • SOCK_STREAM —— 指定使用 TCP 协议,面向连接、可靠传输。
我的习惯:创建 Socket 时,我通常会顺手加上 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。这行代码的作用是允许端口重用。为什么?因为服务端崩溃后,端口会进入 TIME_WAIT 状态,不加这个选项,重启时会报「Address already in use」。我在生产环境吃过这个亏,后来就养成了这个习惯。

2.3 绑定地址

绑定地址,就是把 Socket 和某个 IP + 端口拴在一起。

server_address = ('0.0.0.0', 8888)
server_socket.bind(server_address)

关于 IP 地址,有几种常见写法:

IP 地址 含义 适用场景
'0.0.0.0' 监听所有网络接口 服务器有多块网卡时,接受任意 IP 的访问
'127.0.0.1' 仅监听本地回环地址 只允许本机程序连接,外部无法访问
'192.168.1.100' 监听特定网卡 只接受来自某个网段的连接
注意:端口号范围是 0~65535,但 0~1023 是知名端口,需要 root 权限才能绑定。开发调试时,建议用 1024 以上的端口,比如 8888、9999。我曾经图省事绑了 80 端口,结果在非 root 用户下运行直接报错,排查了半天才发现是权限问题。

2.4 监听连接

绑定完地址,Socket 还处于「待机」状态。你需要调用 listen() 告诉内核:我开始接客了。

server_socket.listen(5)
print("服务器已启动,等待客户端连接...")

listen() 的参数叫「backlog」,表示连接请求队列的最大长度。什么意思呢?

当多个客户端同时发起连接,而你的服务端来不及处理时,内核会把它们排成一个队列。backlog 就是这个队列的上限。超过上限的请求,会被内核直接拒绝。

我个人建议:

  • 开发环境设 5 就够了
  • 生产环境根据并发量,一般设 128 或 256
  • 不要设太大,否则队列积压会消耗大量内存

2.5 接受连接

这一步是关键。调用 accept() 后,程序会阻塞在这里,直到有客户端连上来。

client_socket, client_address = server_socket.accept()
print(f"客户端 {client_address} 已连接")

accept() 返回两个东西:

  • client_socket —— 一个全新的 Socket 对象,专门用来和这个客户端通信
  • client_address —— 客户端的 IP 和端口
这里有个重要概念:server_socket 和 client_socket 是两回事。server_socket 只负责「接客」,不负责「聊天」。每来一个客户端,accept() 就生成一个新的 client_socket。所以一个服务端可以同时和多个客户端通信,每个客户端有自己的专属通道。

2.6 收发数据

有了 client_socket,就可以和客户端「聊天」了。TCP 是流式协议,数据像水流一样,没有边界。所以收发数据时,你要自己定义「什么时候算一条完整的消息」。

# 接收数据
data = client_socket.recv(1024)
print(f"收到: {data.decode('utf-8')}")

# 发送数据
response = "Hello, client!"
client_socket.send(response.encode('utf-8'))

recv() 的参数是缓冲区大小,单位是字节。1024 表示一次最多读 1024 字节。如果对方发来的数据超过 1024 字节,剩下的会留在内核缓冲区里,下次 recv() 再读。

这里有个坑:recv() 返回空字节 b'' 时,表示对方已经关闭了连接。你一定要检查这个条件,否则会陷入死循环。

while True:
    data = client_socket.recv(1024)
    if not data:
        print("客户端已断开连接")
        break
    print(f"收到: {data.decode('utf-8')}")
避坑指南:我曾经写过一个服务端,recv() 之后直接 decode(),结果客户端发来的是二进制数据,直接抛 UnicodeDecodeError 崩溃了。后来我养成了习惯:先判断数据类型,或者用 try-except 包一下。生产环境中,消息格式一定要提前约定好。

2.7 关闭连接

聊完了,要记得关门。关闭连接有两个层面:

# 关闭客户端连接
client_socket.close()

# 关闭服务端监听(通常在程序退出时)
server_socket.close()

为什么要分开关?因为 server_socket 是「大门」,client_socket 是「客房」。你关掉客房,不影响大门继续接待新客人。只有程序彻底退出时,才需要关大门。

另外,Python 的 with 语句可以帮你自动管理资源:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('0.0.0.0', 8888))
    s.listen(5)
    # ... 业务逻辑
    # 退出 with 块时,s 会自动 close()

但要注意,with 只关闭 server_socket,不会自动关闭 accept() 返回的 client_socket。你需要自己管理每个 client_socket 的生命周期。

2.8 完整示例:一个能跑起来的 TCP Server

把上面所有步骤串起来,就是一个最简单的 TCP 服务端:

import socket

def main():
    # 1. 创建 Socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定地址
    server_address = ('0.0.0.0', 8888)
    server_socket.bind(server_address)

    # 3. 监听连接
    server_socket.listen(5)
    print(f"服务器启动,监听 {server_address}")

    try:
        while True:
            # 4. 接受连接
            client_socket, client_address = server_socket.accept()
            print(f"客户端 {client_address} 已连接")

            try:
                # 5. 收发数据
                while True:
                    data = client_socket.recv(1024)
                    if not data:
                        break
                    message = data.decode('utf-8')
                    print(f"收到: {message}")
                    response = f"服务器已收到: {message}"
                    client_socket.send(response.encode('utf-8'))
            finally:
                # 6. 关闭客户端连接
                client_socket.close()
                print(f"客户端 {client_address} 连接已关闭")
    except KeyboardInterrupt:
        print("\n服务器正在关闭...")
    finally:
        server_socket.close()
        print("服务器已关闭")

if __name__ == '__main__':
    main()

这个例子虽然简单,但已经包含了 TCP Server 的完整骨架。你可以直接复制运行,然后用 telnet 127.0.0.1 8888 或者 nc 127.0.0.1 8888 来测试。

总结一下这章的核心:
  • 六步流程:socket → bind → listen → accept → recv/send → close
  • 监听 Socket 和连接 Socket 是两回事,别搞混
  • recv() 返回空字节表示对方断开,一定要检查
  • 记得设置 SO_REUSEADDR,避免端口被占用
  • 用 try-finally 或 with 确保资源被释放

这六步,是 TCP 服务端编程的「基本功」。后面的章节,我们会在这个基础上,一步步加上并发处理、消息边界、心跳检测、断线重连……把骨架变成一个有血有肉的可靠通信框架。