1.1 Socket通信核心概念
Socket又称"套接字",是实现网络通信的核心接口,本质是操作系统提供的网络通信端点,负责在不同设备的进程之间建立数据传输通道。无论是本地进程通信还是跨网络的远程通信,Socket都能通过统一的接口完成数据收发,是TCP/IP协议栈的编程实现载体。
在Python中,socket模块是实现Socket通信的核心工具,无需额外安装第三方库,直接导入即可使用。Socket通信遵循"客户端-服务端"架构,服务端负责监听指定端口、接收客户端连接请求并处理数据,客户端负责主动发起连接、与服务端交互数据,两者通过IP地址和端口号定位彼此,确保数据精准传输。
Socket通信主要分为两种类型:TCP Socket和UDP Socket。TCP(传输控制协议)是面向连接的可靠通信协议,通信前需建立三次握手连接,通信过程中保证数据有序、无丢失、无重复,适用于文件传输、登录认证等对数据可靠性要求高的场景;UDP(用户数据报协议)是无连接的不可靠通信协议,通信前无需建立连接,直接发送数据报,传输速度快但可能出现数据丢失、乱序,适用于视频直播、实时语音等对时效性要求高于可靠性的场景。
1.2 Socket通信基础原理
1.2.1 核心通信要素
***Socket通信的实现依赖三个核心要素,缺少任意一个都无法完成正常通信:
- 协议类型:选择TCP或UDP协议,决定通信的可靠性和传输方式;
- IP地址:定位网络中的目标设备,IPv4地址格式为xxx.xxx.xxx.xxx(如127.0.0.1为本地回环地址,用于本机内部通信);
- 端口号:定位设备中的目标进程,范围为0-65535,其中0-1023为系统保留端口(如80端口用于HTTP服务、22端口用于SSH服务),开发时建议使用1024-65535之间的未占用端口。***
1.2.2 TCP Socket通信流程
TCP是面向连接的通信模式,流程严谨且固定,分为服务端和客户端两个维度,核心步骤缺一不可:
服务端流程:创建套接字→绑定IP和端口→监听连接→接收客户端连接→收发数据→关闭连接→关闭套接字;
客户端流程:创建套接字→连接服务端IP和端口→收发数据→关闭连接→关闭套接字。
TCP的三次握手发生在"客户端连接"与"服务端接收连接"阶段,确保服务端和客户端都具备收发数据的能力;四次挥手发生在"关闭连接"阶段,确保双方数据都传输完毕后再断开连接,避免数据丢失。
1.2.3 UDP Socket通信流程
UDP是无连接的通信模式,流程相对简单,无需建立和断开连接,核心步骤如下:
服务端流程:创建套接字→绑定IP和端口→收发数据→关闭套接字;
客户端流程:创建套接字→收发数据→关闭套接字。
UDP通信时,客户端无需确认服务端是否在线,直接发送数据报;服务端绑定端口后即可接收任意客户端的数据,无需建立专属连接,每个数据报都包含目标IP和端口,独立传输。
1.3 Python Socket模块核心函数与参数
Python的socket模块封装了所有Socket通信所需的函数,核心函数的参数和用法直接决定通信是否能正常实现,以下是开发中高频使用的核心函数详解。
1.3.1 套接字创建:socket.socket()
功能:创建一个Socket对象,是所有通信的基础,语法格式如下:
sock = socket.socket(family, type, proto)
参数说明:
family:地址族,指定IP类型,常用值为socket.AF_INET(IPv4地址,开发中最常用)、socket.AF_INET6(IPv6地址);
type:套接字类型,TCP通信使用socket.SOCK_STREAM(流式套接字,面向连接、可靠传输),UDP通信使用socket.SOCK_DGRAM(数据报套接字,无连接、不可靠传输);
proto:协议编号,默认值为0,无需手动指定,由系统根据family和type自动匹配。
示例(创建TCP套接字): tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
示例(创建UDP套接字): udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1.3.2 地址绑定:bind()
功能:将套接字与指定的IP地址和端口号绑定,仅服务端需要调用,客户端无需绑定(由系统自动分配随机端口),语法格式如下:
sock.bind((host, port))
参数说明:
host:IP地址,字符串类型,绑定本机所有网卡使用空字符串""或0.0.0.0,绑定本地回环地址使用127.0.0.1(仅本机内部可访问),绑定具体网卡IP使用设备的实际IP(如192.168.1.100,局域网内其他设备可访问);
port:端口号,整数类型,需在1024-65535之间选择未被占用的端口,避免端口冲突。
注意:bind()的参数必须是元组格式(host, port),缺少括号会报错;绑定已占用的端口时,会抛出OSError异常,需更换端口后重新运行。
1.3.3 监听连接:listen()
功能:仅TCP服务端使用,将套接字设置为监听状态,等待客户端连接请求,语法格式如下:
sock.listen(backlog)
参数说明:
backlog:指定最大等待连接数,即允许同时有多少个客户端处于等待连接的状态,超过该数量的连接请求会被拒绝,常用值为5、10,无需设置过大(Python3.5+后该参数为可选,默认值由系统决定)。
示例: tcp_sock.listen(5) # 允许最多5个客户端等待连接
1.3.4 接收连接:accept()
功能:仅TCP服务端使用,阻塞等待客户端的连接请求,当有客户端连接时,返回一个新的套接字(用于与该客户端单独通信)和客户端的地址信息,语法格式如下:
conn, addr = sock.accept()
返回值说明:
conn:新的套接字对象,专门用于与当前连接的客户端收发数据,每个客户端连接都会生成一个独立的conn对象;
addr:元组类型,包含客户端的IP地址和端口号,格式为(client_ip, client_port)。
注意:accept()是阻塞函数,程序运行到该语句时会暂停,直到有客户端连接才会继续执行,后续需通过conn对象与客户端通信,而非原始的监听套接字。
1.3.5 发起连接:connect()
功能:仅TCP客户端使用,主动向服务端发起连接请求,语法格式如下:
sock.connect((host, port))
参数说明:
host为服务端的IP地址,port为服务端绑定的端口号,参数格式必须是元组;若服务端未启动、IP或端口错误,会抛出ConnectionRefusedError异常。
示例: tcp_sock.connect(("127.0.0.1", 8888)) # 连接本地8888端口的TCP服务端
1.3.6 数据收发:recv()、send()、sendto()、recvfrom()
1. recv():TCP通信专用,用于接收数据,阻塞等待对方发送数据,语法: data = conn.recv(bufsize)
参数bufsize:单次接收的最大字节数,常用值为1024、4096,返回值为bytes类型的二进制数据,需解码为字符串(如data.decode("utf-8"));
2. send():TCP通信专用,用于发送数据,语法: conn.send(data.encode("utf-8"))
注意:发送的数据必须是bytes类型,字符串需通过encode()编码,若发送数据过大,可能需要分多次发送;
3. recvfrom():UDP通信专用,接收数据并获取发送方地址,语法: data, addr = sock.recvfrom(bufsize)
返回值data为bytes类型数据,addr为发送方的(IP, 端口)元组;
4. sendto():UDP通信专用,发送数据到指定目标,语法: sock.sendto(data.encode("utf-8"), (host, port))
无需提前建立连接,直接指定目标IP和端口即可发送。
1.3.7 关闭套接字:close()
功能:关闭套接字,释放占用的端口资源,语法: sock.close() 或 conn.close()
注意:TCP通信中,服务端需关闭两个套接字(与客户端通信的conn对象和监听的sock对象),客户端仅需关闭自身的sock对象;通信结束后必须关闭套接字,否则会导致端口被长期占用。
1.4 TCP Socket完整实战(一对一通信)
TCP是开发中最常用的Socket通信方式,以下实现一对一的TCP通信案例:服务端绑定端口监听,客户端发起连接,双方可互相发送文本消息,输入"exit"即可断开连接。
1.4.1 TCP服务端代码实现
导入socket模块
import socket
def tcp_server():
1. 创建TCP套接字对象(IPv4+流式套接字)
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2. 绑定IP和端口:绑定本机所有网卡,端口8888
server_addr = ("", 8888)
server_sock.bind(server_addr)
3. 开启监听,最大等待连接数5
server_sock.listen(5)
print("TCP服务端已启动,监听端口8888,等待客户端连接...")
4. 阻塞等待客户端连接,获取通信套接字和客户端地址
conn, client_addr = server_sock.accept()
print(f"成功连接客户端:{client_addr[0]}:{client_addr[1]}")
try:
循环收发数据,实现持续通信
while True:
5. 接收客户端数据,单次最多接收1024字节,解码为字符串
recv_data = conn.recv(1024).decode("utf-8")
若客户端发送exit或断开连接,退出循环
if not recv_data or recv_data == "exit":
print(f"客户端{client_addr[0]}:{client_addr[1]}已断开连接")
break
print(f"收到客户端消息:{recv_data}")
6. 向客户端发送回复消息
send_data = input("请输入回复客户端的消息(输入exit断开):")
conn.send(send_data.encode("utf-8"))
if send_data == "exit":
print("服务端主动断开连接")
break
finally:
7. 关闭与客户端的通信套接字
conn.close()
8. 关闭服务端监听套接字,释放端口
server_sock.close()
print("TCP服务端已关闭")
启动服务端
if name == "main":
tcp_server()
1.4.2 TCP客户端代码实现
导入socket模块
import socket
def tcp_client():
1. 创建TCP套接字对象
client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2. 向服务端发起连接请求,指定服务端IP和端口(本地测试用127.0.0.1)
server_addr = ("127.0.0.1", 8888)
try:
client_sock.connect(server_addr)
print("成功连接TCP服务端,可开始发送消息(输入exit断开)")
except ConnectionRefusedError:
print("服务端未启动或IP/端口错误,连接失败")
return
循环收发数据
while True:
3. 向服务端发送消息
send_data = input("请输入发送给服务端的消息(输入exit断开):")
client_sock.send(send_data.encode("utf-8"))
if send_data == "exit":
print("客户端主动断开连接")
break
4. 接收服务端回复的数据
recv_data = client_sock.recv(1024).decode("utf-8")
if not recv_data or recv_data == "exit":
print("服务端已断开连接")
break
print(f"收到服务端回复:{recv_data}")
5. 关闭客户端套接字
client_sock.close()
print("TCP客户端已关闭")
启动客户端
if name == "main":
tcp_client()
1.4.3 运行说明与注意事项
1. 运行顺序:必须先启动服务端,再启动客户端,否则客户端会提示连接失败;
2. 本地测试:服务端绑定""或127.0.0.1,客户端连接127.0.0.1即可实现本机内部通信;若需局域网通信,服务端绑定本机实际IP(如192.168.1.100),客户端连接该IP即可;
3. 编码解码:网络传输的是二进制数据,字符串必须通过encode("utf-8")编码为bytes,接收后通过decode("utf-8")解码为字符串,否则会出现乱码;
4. 异常处理:服务端未启动、端口被占用、网络中断等情况会抛出异常,实际开发中需添加异常捕获,避免程序崩溃。
1.5 UDP Socket完整实战(无连接通信)
UDP无需建立连接,通信流程更简单,以下实现UDP通信案例:服务端绑定端口接收任意客户端的消息并回复,客户端直接发送消息到服务端,无需提前连接。
1.5.1 UDP服务端代码实现
import socket
def udp_server():
1. 创建UDP套接字对象(IPv4+数据报套接字)
server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
2. 绑定IP和端口,UDP服务端必须绑定端口才能接收数据
server_addr = ("", 9999)
server_sock.bind(server_addr)
print("UDP服务端已启动,绑定端口9999,等待接收消息(输入exit退出)")
while True:
3. 接收客户端数据和客户端地址,单次接收1024字节
recv_data, client_addr = server_sock.recvfrom(1024)
recv_msg = recv_data.decode("utf-8")
print(f"收到客户端{client_addr[0]}:{client_addr[1]}的消息:{recv_msg}")
若接收exit,服务端退出
if recv_msg == "exit":
print("UDP服务端主动退出")
break
4. 向客户端回复消息,需指定客户端地址
send_msg = input("请输入回复客户端的消息:")
server_sock.sendto(send_msg.encode("utf-8"), client_addr)
5. 关闭套接字
server_sock.close()
print("UDP服务端已关闭")
if name == "main":
udp_server()
1.5.2 UDP客户端代码实现
import socket
def udp_client():
1. 创建UDP套接字对象
client_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
UDP客户端无需绑定端口,系统自动分配
server_addr = ("127.0.0.1", 9999)
print("UDP客户端已启动,可发送消息(输入exit退出)")
while True:
2. 向服务端发送消息,指定服务端地址
send_msg = input("请输入发送给服务端的消息:")
client_sock.sendto(send_msg.encode("utf-8"), server_addr)
if send_msg == "exit":
print("UDP客户端主动退出")
break
3. 接收服务端回复的数据
recv_data, _ = client_sock.recvfrom(1024)
recv_msg = recv_data.decode("utf-8")
print(f"收到服务端回复:{recv_msg}")
4. 关闭套接字
client_sock.close()
print("UDP客户端已关闭")
if name == "main":
udp_client()
1.5.3 UDP通信核心特点
1. 无连接特性:客户端无需调用connect(),直接通过sendto()发送数据,服务端通过recvfrom()获取客户端地址并回复,无需建立专属连接;
2. 不可靠性:若服务端未启动,客户端发送的数据会直接丢失,无报错提示;数据传输过程中可能出现丢失、乱序,需自行实现重传、排序机制(如需可靠传输);
3. 高效性:无需三次握手和四次挥手,传输速度快,适合小数据量、实时性要求高的场景。
1.6 Socket通信进阶技巧
1.6.1 解决TCP粘包问题
***TCP是流式传输,数据会被合并为字节流发送,导致"粘包":多次发送的小数据被一次性接收(如连续发送"a"和"b",接收方可能收到"ab")。解决粘包的核心思路是明确数据边界,常用方案:
- 固定长度:发送方每次发送固定长度的数据,接收方按固定长度接收,不足则补空字符,超过则截断;
- 分隔符:在数据末尾添加特殊分隔符(如"\n"),接收方按分隔符拆分数据;
- 数据头+数据体:数据头存储数据体的长度,接收方先接收数据头获取长度,再按长度接收数据体,是最常用的方案。
示例(数据头+数据体解决粘包):
发送方:先计算数据长度→编码长度为4字节→发送长度+数据体***
data = "hello world".encode("utf-8")
length = len(data).to_bytes(4, byteorder="big") # 4字节存储长度
conn.send(length + data)
接收方:先接收4字节数据头→解析长度→按长度接收数据体
length_data = conn.recv(4)
length = int.from_bytes(length_data, byteorder="big")
data = conn.recv(length).decode("utf-8")
1.6.2 实现TCP多客户端通信
基础TCP服务端只能与一个客户端通信,要实现多客户端同时连接,需结合多线程:服务端每接收一个客户端连接,就创建一个新线程专门处理该客户端的通信,主线程继续监听新的连接请求。
核心代码片段(多线程TCP服务端)
import socket
import threading
def handle_client(conn, client_addr):
"""处理单个客户端的通信"""
print(f"新客户端连接:{client_addr}")
try:
while True:
recv_data = conn.recv(1024).decode("utf-8")
if not recv_data or recv_data == "exit":
print(f"客户端{client_addr}断开连接")
break
print(f"[{client_addr}]:{recv_data}")
conn.send(f"已收到:{recv_data}".encode("utf-8"))
finally:
conn.close()
def tcp_multi_server():
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.bind(("", 8888))
server_sock.listen(10)
print("多客户端TCP服务端已启动,监听8888端口")
while True:
conn, addr = server_sock.accept()
创建线程处理客户端通信
t = threading.Thread(target=handle_client, args=(conn, addr))
t.daemon = True # 守护线程,主线程退出时子线程也退出
t.start()
if name == "main":
tcp_multi_server()
1.6.3 超时设置
默认情况下,recv()、accept()等函数是阻塞的,程序会一直等待,直到有数据或连接。通过settimeout()设置超时时间,超时后会抛出socket.timeout异常,避免程序长期阻塞。
示例:
sock.settimeout(5) # 设置超时时间为5秒,单位为秒
若5秒内没有数据接收或连接请求,直接抛出异常,可通过try-except捕获并处理。
1.7 Socket通信常见问题与排查
1. 端口被占用:报错OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次,解决方案:更换未被占用的端口,或通过命令杀死占用端口的进程(Windows:netstat -ano | findstr 端口号,taskkill /pid 进程号 /f;Linux:lsof -i:端口号,kill -9 进程号);
2. 连接被拒绝:报错ConnectionRefusedError,原因是服务端未启动、IP地址错误、端口号不匹配,解决方案:先启动服务端,核对IP和端口是否一致;
3. 数据乱码:原因是编码解码格式不一致,解决方案:发送方和接收方统一使用utf-8编码解码;
4. 多客户端连接失败:原因是listen()的backlog值过小,或未使用多线程/多进程处理连接,解决方案:增大backlog值,结合多线程/多进程实现多客户端处理;
5. 防火墙拦截:通信双方的防火墙可能拦截端口,解决方案:关闭防火墙,或在防火墙中放行对应的端口。
1.8 本章总结
Socket通信是Python网络编程的基础,核心是基于客户端-服务端架构,通过socket模块实现TCP或UDP协议的通信。TCP适用于可靠传输场景,流程为创建套接字→绑定→监听→连接→收发数据→关闭;UDP适用于高效传输场景,无需建立连接,直接通过sendto()和recvfrom()收发数据。
开发中需注意端口绑定、编码解码、异常处理等细节,针对TCP粘包、多客户端通信等需求,可通过数据边界标识、多线程等进阶技巧解决。掌握Socket通信后,可进一步实现文件传输、聊天室、远程控制等复杂网络应用,为后续Python网络开发打下基础。