Socket(套接字)是计算机网络通信的基石。在 Python 中,socket 模块提供了访问 BSD 套接字接口的途径。无论是构建简单的聊天室,还是复杂的 Web 服务器,理解 Socket 都是必修课。
简单来说,Socket 是网络上运行的两个程序之间双向通信链路的一个端点。
所有网络通信的第一步都是创建一个 socket 对象。
Python
import socket
# 创建一个 TCP/IP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
在 socket.socket(family, type) 中,你通常会用到以下参数:
地址家族 (Family): | 常量 | 说明 | 适用场景 | | :--- | :--- | :--- | | socket.AF_INET | IPv4 (默认) | 互联网通信 (如 192.168.1.1) | | socket.AF_INET6 | IPv6 | 下一代互联网通信 | | socket.AF_UNIX | Unix Domain Socket | 同一台机器进程间通信 (Linux/Mac) |
套接字类型 (Type): | 常量 | 说明 | 协议 | 特点 | | :--- | :--- | :--- | :--- | | socket.SOCK_STREAM | 流式套接字 (默认) | TCP | 面向连接、可靠、顺序数据流 (HTTP, FTP) | | socket.SOCK_DGRAM | 数据报套接字 | UDP | 无连接、不可靠、速度快 (视频流, DNS) |
这里详细列出了服务端和客户端常用的方法及其扩展用法。
s.bind(address)将套接字绑定到地址。
address 取决于地址家族。对于 AF_INET,它是一个元组 (host, port)。host 为 '' 表示绑定所有可用接口;端口 0 表示随机分配一个空闲端口。s.listen(backlog)开始监听传入连接。
backlog (int) 指定在拒绝新连接之前,系统允许挂起的未接受连接的最大数量(排队长度)。通常设为 5 或更大。s.accept()接受一个连接。
(conn, address)。conn: 一个新的 socket 对象,用于通过该连接发送/接收数据。address: 客户端的地址 (IP, Port)。s.connect(address)连接到远程套接字。
(host, port) 元组。ConnectionRefusedError 等异常。s.connect_ex(address)connect() 的扩展版本,出错时不抛出异常,而是返回错误码(返回 0 表示成功)。常用于端口扫描器。
s.send(bytes)发送数据。
send() 并不保证发送所有数据,可能只发送了一部分,需要应用程序自己处理循环发送。s.sendall(bytes)推荐使用。 持续发送数据,直到所有数据发送完毕或发生错误。成功返回 None。
s.recv(bufsize)接收数据。
bufsize 指定一次最多接收的数据量(通常是 1024 或 4096)。bytes 对象。如果返回空字节 b'',表示对方已关闭连接。s.sendto(bytes, address)发送 UDP 数据。不需要建立连接。
s.recvfrom(bufsize)接收 UDP 数据。
(bytes, address),包含数据和发送方地址。s.setsockopt(level, optname, value)设置套接字选项。最常用的一个是解决 "Address already in use" 错误:
Python
# 允许端口复用 (在服务器重启时非常有用)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.settimeout(value)设置阻塞套接字操作的超时时间(秒)。如果超时,抛出 socket.timeout 异常。
s.setblocking(bool)True (默认): 阻塞模式。False: 非阻塞模式。如果操作不能立即完成(如 recv 没有数据),会抛出 BlockingIOError。s.close()关闭套接字,释放资源。建议放在 finally 块中或使用 with 语句。
下面我们将编写一个健壮的 TCP Echo 系统。服务端接收客户端发送的消息,加上时间戳后原样返回。
这个服务端示例包含了:
Python
import socket
import time
# 配置信息
HOST = '127.0.0.1' # 监听本地回环地址
PORT = 65432 # 监听的非特权端口 (> 1023)
def start_server():
# 1. 创建 Socket 对象 (IPv4, TCP)
# 使用 with 语句可以确保 socket 最后会被正确关闭
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# [扩展写法] 设置端口复用,避免服务端重启时报 "Address already in use"
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定地址和端口
try:
s.bind((HOST, PORT))
print(f"[*] 服务端启动,监听 {HOST}:{PORT}")
except OSError as e:
print(f"[!] 绑定失败: {e}")
return
# 3. 开始监听,backlog 设为 5
s.listen(5)
print("[*] 等待客户端连接...")
while True:
try:
# 4. 接受连接 (阻塞式)
# conn 是新的 socket 对象,用于和该特定客户端通信
# addr 是客户端的 (IP, Port)
conn, addr = s.accept()
# 使用 with 管理连接 socket
with conn:
print(f"[+] 已连接: {addr}")
while True:
# 5. 接收数据 (一次最多 1024 字节)
data = conn.recv(1024)
# 如果 data 为空,说明客户端断开了连接
if not data:
print(f"[-] 客户端 {addr} 断开连接")
break
# 处理数据 (解码 -> 处理 -> 编码)
msg_str = data.decode('utf-8')
print(f" 收到消息: {msg_str}")
# 构造响应:加上时间戳
timestamp = time.strftime("%H:%M:%S", time.localtime())
response = f"[{timestamp}] 服务器收到: {msg_str}"
# 6. 发送数据 (sendall 保证数据发完)
conn.sendall(response.encode('utf-8'))
except KeyboardInterrupt:
print("\n[!] 服务器正在停止...")
break
except Exception as e:
print(f"[!] 发生错误: {e}")
break
if __name__ == "__main__":
start_server()
这个客户端示例包含了:
Python
import socket
# 必须与服务端一致
HOST = '127.0.0.1'
PORT = 65432
def start_client():
print(f"[*] 正在尝试连接 {HOST}:{PORT} ...")
# 1. 创建 Socket
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 2. 连接服务端
s.connect((HOST, PORT))
print("[+] 连接成功!输入 'exit' 退出。")
while True:
# 获取用户输入
msg = input(">>> 请输入消息: ")
if not msg: continue
if msg.lower() == 'exit':
break
# 3. 发送数据 (记得 encode 转为 bytes)
s.sendall(msg.encode('utf-8'))
# 4. 接收响应
# 注意:TCP 是流式协议,但在这种简单交互下,recv 通常能收到完整包
data = s.recv(1024)
if not data:
print("[!] 服务器关闭了连接")
break
print(f" {data.decode('utf-8')}")
except ConnectionRefusedError:
print("[!] 连接失败:目标计算机拒绝连接 (请检查服务端是否开启)")
except Exception as e:
print(f"[!] 发生错误: {e}")
if __name__ == "__main__":
start_client()
bytes)。
- 发送前必须编码:str.encode('utf-8')
- 接收后必须解码:bytes.decode('utf-8')recv(1024) 并不意味着一定能收到 1024 字节,也不意味着只收到对方发送的一次 send。TCP 是流(stream),没有边界。
- 解决方案: 在实际工程中,通常会在消息头部加上由固定字节(如 4 字节)表示的消息长度,接收方先读长度,再根据长度读取消息体。accept() 和 recv() 都会卡住程序,直到有事件发生。如果需要同时处理多个客户端,通常需要使用多线程 (threading) 或 IO 多路复用 (select/selectors 模块)。