Python Socket编程基础:怎么实现TCP与UDP通信
详细讲解Python中Socket网络编程的基础知识与具体实现方法,包含TCP与UDP客户端服务端的完整代码步骤、通信时序、关键机制与核心差异对比。
Socket(套接字)是计算机网络通信的底层基石,它为上层软件提供了访问操作系统网络协议栈的统一编程接口。在 Python 中,通过内置的标准库 socket,开发者无需安装任何第三方依赖,即可直接实现主机间的数据传输。本文将直接从底层通信流程展开,分别讲解如何在 Python 中完整实现基于面向连接的 TCP 通信 与基于无连接的 UDP 通信,剖析两者的具体操作步骤、可运行示例与核心差异,帮助你在实际项目中准确选用并编写稳定的网络交互程序。
一、Socket 编程的核心概念与协议选择
在网络体系中,一台主机通过 IP 地址标识身份,而主机内部运行的各种网络程序则通过“端口号”(Port)进行区分。一个 Socket 本质上就是由IP 地址与端口号组成的逻辑端点。应用程序向 Socket 写入字节流或数据报,操作系统内核负责把数据打包并发送到网络线缆中;反向接收时,操作系统再依据目的端口把数据推送到对应的 Socket 缓冲区中供程序读取。
在 Python 中,我们使用 socket.socket(family, type) 函数来创建一个套接字对象。这个函数最常用的两个参数决定了通信的寻址协议与传输行为:
- 地址族(family):最常用的是
socket.AF_INET,代表 IPv4 地址寻址(如127.0.0.1);若使用 IPv6 地址,则指定为socket.AF_INET6。 - 套接字类型(type):
socket.SOCK_STREAM:对应 TCP 协议(流式套接字)。它提供顺序到达、无丢失、无重复且面向连接的可靠字节流服务。socket.SOCK_DGRAM:对应 UDP 协议(数据报套接字)。它提供无连接、独立数据报文传输服务,不保证顺序也不保证送达,但首部极小、转发高效。
如图 1 所示,Socket API 位于应用层与内核传输层之间。无论是构建高可靠的 Web 数据交互,还是追求极低延迟的音视频传输,程序内部的调用起点都是这个套接字对象。下面我们分别进入 TCP 和 UDP 的具体实现步骤与完整代码。
二、实现基于 TCP 的面向连接通信
TCP(Transmission Control Protocol)是面向连接的传输协议。在两个通信实体传输数据之前,必须先通过底层“三次握手”建立一条端到端的虚电路;通信结束后,再通过“四次挥手”拆除连接。这意味着 TCP 通信严格区分服务端(Server)与客户端(Client)角色。
1. TCP 服务端实现步骤
- 创建套接字:指定
AF_INET与SOCK_STREAM。 - 设置地址复用(推荐):调用
setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。这样当服务端重启时,即使原端口处于 TIME_WAIT 状态也能立即重新绑定,避免抛出“Address already in use”异常。 - 绑定地址与端口:调用
bind((host, port)),参数为一个元组。 - 开启监听:调用
listen(backlog),其中backlog指定系统允许暂存的未完成连接队列的最大长度。 - 接受连接:调用
accept()。该方法会一直阻塞,直到有客户端连入。成功连接后,它返回一个由两个元素组成的元组:(conn, client_address)。其中conn是专门代表该客户端连接的全新套接字,随后的数据收发全部通过它进行;原监听套接字继续负责等待其它客户端。 - 收发数据:调用
conn.recv(bufsize)接收字节数据,调用conn.sendall(bytes_data)发送数据。 - 关闭连接:处理完毕后依次调用
conn.close()和server_socket.close()释放系统文件描述符。
2. TCP 通信完整代码示例
我们编写一个完整的回显(Echo)服务端与客户端程序。客户端向服务端发送一段文本,服务端收到后转为大写并回传给客户端。
import socket
def run_tcp_server(host='127.0.0.1', port=8888):
# 1. 创建 TCP 套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 允许端口快速重用
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 3. 绑定 IP 与 端口
server_socket.bind((host, port))
# 4. 开启监听,排队队列上限设为 5
server_socket.listen(5)
print(f"[服务端已启动] 正在监听 {host}:{port},等待客户端连接...")
try:
while True:
# 5. 阻塞等待客户端连接
conn, client_addr = server_socket.accept()
print(f"[连接建立] 客户端来自: {client_addr}")
with conn:
while True:
# 6. 接收客户端数据(单次最多读取 1024 字节)
data = conn.recv(1024)
if not data:
# 客户端断开连接时,recv 会返回空字节 b''
print(f"[连接断开] 客户端 {client_addr} 已下线")
break
msg = data.decode('utf-8')
print(f"[收到数据] 内容: {msg}")
# 加工处理:转为大写回传
reply = f"ECHO: {msg.upper()}".encode('utf-8')
conn.sendall(reply)
except KeyboardInterrupt:
print("
[服务端停止] 收到终止信号,正在关闭服务...")
finally:
server_socket.close()
if __name__ == '__main__':
run_tcp_server()
import socket
def run_tcp_client(host='127.0.0.1', port=8888):
# 1. 创建 TCP 套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 2. 连接服务端(触发三次握手)
print(f"[正在连接] 目标服务器: {host}:{port}...")
client_socket.connect((host, port))
print("[连接成功] 已与服务器建立 TCP 会话")
# 3. 发送数据
message = "Hello, Python Socket TCP!"
print(f"[准备发送] {message}")
client_socket.sendall(message.encode('utf-8'))
# 4. 接收服务端响应
response = client_socket.recv(1024)
print(f"[收到应答] {response.decode('utf-8')}")
except ConnectionRefusedError:
print(f"[连接失败] 无法连入 {host}:{port},请确认服务端已启动")
finally:
# 5. 关闭套接字(触发四次挥手)
client_socket.close()
print("[连接关闭] 本地套接字已安全释放")
if __name__ == '__main__':
run_tcp_client()
3. 运行验证与结果示例
在两个独立的终端窗口中分别运行服务端与客户端程序:
# 终端 1:先启动服务端
python3 tcp_server.py
# 终端 2:随后启动客户端
python3 tcp_client.py
TCP 是纯粹的字节流协议,在传输层眼中只有无界限的连续二进制字节,并没有“一条消息”的边界概念。如果发送端连续快速调用两次 sendall(),接收端单次 recv() 可能会把两次内容一次性读出;或者单次发送的长数据被拆分成多次 recv() 接收。
在实际生产环境中,若要在 TCP 上传输多条独立业务指令,必须在应用层自定义协议格式,常见做法有两种:固定长度消息头(声明后随数据体长度)或特定分隔符(如 )。
三、实现基于 UDP 的无连接数据报通信
UDP(User Datagram Protocol)是无连接的轻量级传输协议。它不进行三次握手,也不维持连接状态。发送端只需把数据封装进数据报,指定目的 IP 和端口,便直接抛入网络;接收端也只需监听本地端口,等待数据报到达。
1. UDP 服务端与客户端实现步骤
- UDP 服务端:
- 创建套接字:指定
AF_INET和SOCK_DGRAM。 - 绑定端口:调用
bind((host, port))。由于 UDP 没有连接,不需要调用 listen() 也不需要 accept()。 - 接收数据:调用
data, client_addr = s.recvfrom(bufsize)。该方法同时返回数据内容与发信方的地址元组。 - 应答数据:调用
s.sendto(reply_bytes, client_addr),把应答直接发回目标地址。
- 创建套接字:指定
- UDP 客户端:
- 创建套接字:同样指定
SOCK_DGRAM。 - 无需调用 connect(),直接调用
s.sendto(data_bytes, (server_host, server_port))即可发包。操作系统会在此时自动为客户端分配一个临时本地端口。 - 接收应答:调用
data, addr = s.recvfrom(bufsize)接收服务端的回复。 - 关闭套接字:调用
s.close()。
- 创建套接字:同样指定
2. UDP 通信完整代码示例
import socket
def run_udp_server(host='127.0.0.1', port=9999):
# 1. 创建 UDP 套接字 (SOCK_DGRAM)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2. 绑定端口
server_socket.bind((host, port))
print(f"[UDP 服务端就绪] 监听端口 {host}:{port},随时等待接收数据报...")
try:
while True:
# 3. 阻塞接收数据报,同时获取发送方的 IP 和端口
data, client_addr = server_socket.recvfrom(1024)
msg = data.decode('utf-8')
print(f"[收到数据报] 来自 {client_addr}: {msg}")
# 4. 原路回传应答数据报
reply = f"ACK: Received '{msg}'".encode('utf-8')
server_socket.sendto(reply, client_addr)
except KeyboardInterrupt:
print("
[服务端停止] 正在关闭 UDP 套接字...")
finally:
server_socket.close()
if __name__ == '__main__':
run_udp_server()
import socket
def run_udp_client(host='127.0.0.1', port=9999):
# 1. 创建 UDP 套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置 3 秒超时,防止网络丢包时程序无限期卡住
client_socket.settimeout(3.0)
target_addr = (host, port)
message = "Hello, Python Socket UDP!"
try:
# 2. 直接发送数据报到指定地址,无需 connect
print(f"[发送数据报] 目标: {target_addr},内容: {message}")
client_socket.sendto(message.encode('utf-8'), target_addr)
# 3. 接收服务端响应
data, server_addr = client_socket.recvfrom(1024)
print(f"[收到应答] 来自 {server_addr}: {data.decode('utf-8')}")
except socket.timeout:
print(f"[请求超时] 未在 3 秒内收到来自 {target_addr} 的数据报响应(可能丢包)")
finally:
# 4. 释放套接字
client_socket.close()
print("[客户端退出] 套接字已关闭")
if __name__ == '__main__':
run_udp_client()
3. 运行验证与结果示例
# 终端 1:启动 UDP 服务端
python3 udp_server.py
# 终端 2:启动 UDP 客户端
python3 udp_client.py
与 TCP 的字节流不同,UDP 天生完整保留报文边界。发送端调用一次 sendto() 发送一个数据报,接收端的一次 recvfrom() 就严格对应读取这一个数据报,绝不会出现多包混杂粘连的情况。
但需要注意,底层以太网链路层的最大传输单元(MTU)通常为 1500 字节。去除 20 字节 IP 首部和 8 字节 UDP 首部后,建议单次发送的 UDP 净荷(Payload)控制在 1472 字节以内。若超过此限制,IP 层会强行分片传输,一旦其中一个分片在网络中丢失,整个 UDP 数据报都会被接收端内核直接丢弃。
四、TCP 与 UDP 的核心差异与技术选型
理解了两种协议的具体代码实现后,关键问题在于:在实际业务开发中,究竟应当如何选择?它们的核心差异不在于谁好谁坏,而在于它们解决的侧重目标不同。
我们把影响实际开发设计的关键差异总结如下:
| 特性维度 | TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) | 差异带来的实际开发影响 |
|---|---|---|---|
| 连接模型 | 面向连接(需握手和维护连接状态) | 无连接(随时发送,无状态维护) | TCP 单机并发受限于连接句柄与内存;UDP 服务端可用单个套接字响应海量客户端。 |
| 可靠性机制 | 确认应答、超时重传、保序去重 | 尽力交付,不保证送达,无重传 | TCP 开发者无需操心丢包;UDP 若需要可靠性必须在应用层自行设计 ACK 与重传。 |
| 数据传输形态 | 连续字节流(Byte Stream) | 独立数据报(Datagram) | TCP 必须自行处理粘包与分包协议;UDP 报文天然独立,天然具备明确的数据边界。 |
| 协议开销与延迟 | 首部 20~60 字节,有握手和拥塞控制 | 首部固定 8 字节,无握手,即刻发送 | UDP 数据传输初始延迟几乎为零,传输开销小,网络利用率极高。 |
| 通信模式支持 | 仅支持一对一单播 | 支持单播、广播与组播 | 局域网设备自动发现、一对多通知等场景必须依赖 UDP。 |
选型依据简明总结:
- 必须选 TCP 的场景:对数据的准确性、完整性有绝对要求。例如用户登录凭证、金融交易指令、数据库 SQL 查询、文件上传下载、网页 HTTP/RESTful 接口等。即使网络发生波动引发重传,也绝不能丢失任何一个字节。
- 优先选 UDP 的场景:对实时性要求极高,并且能够容忍个别数据帧丢失的场景。例如实时音视频会议(丢失一帧画面比画面卡住数秒重传更能接受)、多人竞技游戏的角色实时坐标同步、物联网传感器的周期性遥测上报,以及 DNS 查询(单次问答、超时直接重发即可)。
五、总结与高并发进阶指引
本文详细梳理了 Python 底层 Socket 通信的完整实现机制:
- TCP 依靠
socket()->bind()->listen()->accept()完成服务端搭建,客户端通过connect()建立可靠连接;传输时需要警惕字节流粘包。 - UDP 依靠
socket(AF_INET, SOCK_DGRAM)与recvfrom()/sendto()实现直接收发;无需握手且自带报文边界,但单包应注意 MTU 限制。
需要特别说明的是,本文展示的基础代码均属于同步阻塞模式(单线程一次只能处理一个客户端)。如果要在生产环境下支撑成千上万个并发连接,可以从以下两种演进路径继续深入:
- 多线程/多进程模型:在 TCP 服务端通过
threading.Thread为每个接入的conn分配独立线程,实现多客户端并发处理。 - I/O 多路复用与异步编程:使用 Python 标准库的
selectors(底层封装 epoll / kqueue)或现代异步框架asyncio,以单线程事件循环驱动非阻塞套接字,实现单机数十万级高并发网络通信。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













