4、Python Socket编程入门:socket模块、创建Socket、绑定地址、监听连接、接受连接

好,咱们进入实战环节了。前面几章聊了理论,现在该动手写代码了。

Python 的 socket 模块,说白了就是操作系统网络接口的封装。你不需要关心底层是 TCP 还是 UDP,调用几个函数就能让两台机器聊起来。我个人习惯把 Socket 编程比作「打电话」——你得先有部电话(创建 Socket),然后插上电话线(绑定地址),等着铃响(监听),最后接起来(接受连接)。

核心要点:Socket 是应用层与传输层之间的抽象接口。Python 的 socket 模块让你用几行代码就能完成网络通信的底层操作。

4.1 socket模块:你的网络工具箱

Python 标准库里的 socket 模块,我用了十几年了。它提供了创建网络端点所需的一切函数和常量。你不需要安装任何第三方包,直接 import socket 就能用。

这个模块主要包含三类东西:

  • 工厂函数:比如 socket() 用来创建套接字对象
  • 地址族常量:AF_INET(IPv4)、AF_INET6(IPv6)
  • 套接字类型常量:SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)

嗯,这里要注意:AF_INET 和 SOCK_STREAM 是最常用的组合,也就是 TCP over IPv4。我在项目中遇到过有人用了 SOCK_DGRAM 却期待可靠传输,结果丢包丢到怀疑人生——UDP 不保证送达,别搞混了。

常量 值 说明
AF_INET 2 IPv4 地址族
AF_INET6 23 IPv6 地址族
SOCK_STREAM 1 面向连接的 TCP
SOCK_DGRAM 2 无连接的 UDP

4.2 创建Socket:拿起你的电话

创建 Socket 就一行代码:

import socket

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

就这么简单。socket.socket() 接受两个参数:地址族和套接字类型。返回的 server_socket 对象就是你的通信端点。

你想想看,这就像从货架上拿了一部电话机。还没接线,还不能用。接下来要给它分配一个号码和线路。

小技巧:我习惯用 with 语句管理 Socket 生命周期,这样能自动关闭,避免资源泄漏。不过初学阶段先别急,咱们一步步来。

4.3 绑定地址:插上电话线

创建完 Socket,下一步就是绑定地址和端口。用 bind() 方法:

# 绑定到本地 127.0.0.1 的 8888 端口
server_address = ('127.0.0.1', 8888)
server_socket.bind(server_address)

bind() 接受一个元组,格式是 (IP地址, 端口号)。IP 地址用字符串,端口号用整数。

这里有几个坑,我曾经踩过:

  • 端口被占用:如果端口已被其他程序使用,bind() 会抛出 OSError。可以用 netstat -an | grep 8888 检查端口状态
  • 权限问题:Unix 系统上绑定 1024 以下端口需要 root 权限。开发时建议用 8000-9999 之间的端口
  • 地址格式:IPv4 地址必须是点分十进制字符串,比如 '192.168.1.1',不能传整数

避坑指南:我曾经在生产环境上忘记检查端口是否被占用,结果服务启动失败,线上挂了 10 分钟。从那以后,我每次绑定前都会先 try-except 捕获 OSError,并打印清晰的错误信息。

4.4 监听连接:等着铃响

绑定完地址,Socket 就进入「待机」状态。调用 listen() 让它开始监听:

# 开始监听,最大等待队列长度为 5
server_socket.listen(5)
print(f"服务器正在监听 {server_address}")

listen() 的参数是 backlog,表示内核为这个套接字排队的最大连接数。什么意思呢?

假设 backlog=5,当有 6 个客户端同时发起连接,第 6 个会被拒绝。这个值不是越大越好——太大了会浪费内核内存,太小了在高并发下容易丢连接。我个人习惯在开发环境设 5,生产环境根据压测结果调整,一般 128 到 1024 之间。

嗯,这里要注意:listen() 只是让 Socket 进入监听状态,它不会阻塞程序。真正的阻塞发生在下一步。

4.5 接受连接:接起电话

最后一步,用 accept() 接受客户端的连接请求:

print("等待客户端连接...")
client_socket, client_address = server_socket.accept()
print(f"收到来自 {client_address} 的连接")

accept() 会 阻塞 程序执行,直到有客户端连接进来。它返回两个值:

  • client_socket:一个新的 Socket 对象,专门用于和这个客户端通信
  • client_address:客户端的 IP 和端口元组

你想想看,server_socket 就像总机接线员,它只负责接电话,不负责通话。真正的对话要用 client_socket 这个分机。

关键区别:server_socket 只用来监听和接受连接,client_socket 才用来收发数据。别搞混了,我见过新手用 server_socket 发数据,结果报错一脸懵。

4.6 完整示例:一个简单的TCP服务器

把上面几步串起来,就是一个能跑的最小 TCP 服务器:

import socket

def start_server(host='127.0.0.1', port=8888):
    # 1. 创建 Socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 2. 绑定地址
    server_address = (host, port)
    server_socket.bind(server_address)
    
    # 3. 监听连接
    server_socket.listen(5)
    print(f"[*] 服务器启动,监听 {host}:{port}")
    
    # 4. 接受连接
    print("[*] 等待客户端连接...")
    client_socket, client_address = server_socket.accept()
    print(f"[+] 收到连接:{client_address}")
    
    # 5. 接收数据(下一章详细讲)
    data = client_socket.recv(1024)
    print(f"[<] 收到数据:{data.decode()}")
    
    # 6. 发送响应
    response = "Hello, client!"
    client_socket.send(response.encode())
    print(f"[>] 发送响应:{response}")
    
    # 7. 清理
    client_socket.close()
    server_socket.close()

if __name__ == '__main__':
    start_server()

这段代码虽然简单,但包含了 Socket 编程的完整生命周期。你可以先跑起来,感受一下「服务器等着,客户端连进来」这个过程。

4.7 核心流程总结

咱们用一张图把整个流程串起来:

TCP Socket 服务器核心流程 1. 创建 Socket socket.socket() 2. 绑定地址 bind(ip, port) 3. 监听连接 listen(backlog) 4. 接受连接 accept() → 阻塞等待 5. 收发数据 recv() / send() 6. 关闭连接 ⚠️ 关键点 server_socket 只负责 监听和接受连接 client_socket 负责通信

这张图把整个流程拆成了 6 步。你会发现,真正「干活」的是第 5 步——收发数据。前面 4 步都是在做准备工作。我刚开始学的时候,总觉得 accept() 之后就能直接通信了,其实 recv() 和 send() 才是重头戏,下一章咱们详细拆解。

4.8 常见错误与排查

写 Socket 程序,报错是家常便饭。我列几个最常见的:

错误 原因 解决方法
OSError: [Errno 98] Address already in use 端口被占用 换端口,或等一会儿再试(TIME_WAIT 状态)
OSError: [Errno 99] Cannot assign requested address 绑定了不存在的 IP 检查 IP 地址是否正确,本地测试用 127.0.0.1
ConnectionRefusedError 客户端连了没在监听的端口 确认服务器已启动,端口号一致
TimeoutError 连接超时 检查网络连通性,防火墙是否放行

调试小技巧:我习惯在 bind() 之前加一行 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。这样即使端口处于 TIME_WAIT 状态,也能立即重用,开发时特别方便。