一、网络编程基础:打开网络通信的大门
1.1 网络编程核心概念
网络编程是指编写运行在网络环境中的程序,实现不同设备、不同进程间的数据交换与通信。在 Python 中,网络编程的核心是socket(套接字)技术 ------ 它是网络通信的 "端点",屏蔽了底层 TCP/IP 协议栈的复杂细节,为应用层提供了统一的编程接口。
网络通信的三大核心要素:
- IP 地址 :网络设备的唯一标识,相当于 "门牌号",用于定位网络中的目标设备。常用 IPv4 格式(如
127.0.0.1、192.168.1.100),IPv6 则解决了地址枯竭问题。 - 端口:设备上应用程序的通信标识,范围 0~65535。0~1023 为知名端口(如 HTTP 的 80 端口、HTTPS 的 443 端口),1024~65535 为动态端口,用于程序临时通信。
- 协议 :通信双方的 "语言规则",决定数据传输的方式、可靠性和效率。核心传输层协议包括TCP 和UDP,是网络编程的基础。
1.2 五层网络模型
理解网络模型是掌握网络编程的前提,OSI 七层模型过于复杂,实际开发中常用TCP/IP 五层模型:
- 应用层:定义数据的具体格式,如 HTTP、FTP、SMTP 等协议,对应我们编写的应用程序。
- 传输层:负责端到端的数据传输,核心协议为 TCP、UDP,决定数据传输的可靠性和效率。
- 网络层:负责路由寻址,将数据包从源设备传输到目标设备,核心协议为 IP。
- 数据链路层:负责物理链路的数据传输,如以太网、WiFi 协议。
- 物理层:负责数据的物理传输,如网线、光纤、无线电波等硬件。
1.3 Socket 核心原理
Socket 是对 BSD 套接字 API 的封装,是应用层与传输层之间的接口。我们可以将 Socket 理解为 "网络通信的插座":服务端创建 Socket 并绑定 IP、端口,监听连接请求;客户端创建 Socket,主动连接服务端,建立通信通道。
Python 中socket模块提供了 Socket API 的完整实现,核心参数如下:
family:地址族,常用AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地进程通信)。type:套接字类型,SOCK_STREAM(TCP 套接字,面向连接)、SOCK_DGRAM(UDP 套接字,无连接)。proto:协议类型,默认 0,一般无需指定。
二、TCP 编程:可靠的面向连接通信
2.1 TCP 协议核心特性
TCP(Transmission Control Protocol,传输控制协议)是面向连接、可靠、有序的字节流协议,其核心特性如下:
- 面向连接:通信前必须通过 "三次握手" 建立连接,通信结束后通过 "四次挥手" 断开连接,类似打电话前的拨号和通话结束后的挂断。
- 可靠传输 :通过确认应答、超时重传、滑动窗口、拥塞控制等机制,保证数据无丢失、无重复、按序到达。
- 字节流模式 :数据无明确边界,发送方的多次
send可能被接收方一次recv接收,需手动处理消息边界。 - 全双工通信:通信双方可同时发送和接收数据,无单向限制。
TCP 三次握手与四次挥手
- 三次握手 :建立可靠连接的过程,核心是同步序列号(SEQ)和确认号(ACK):
- 客户端发送
SYN报文(同步序列号),请求建立连接; - 服务端回复
SYN+ACK报文,确认客户端请求并同步自身序列号; - 客户端发送
ACK报文,确认服务端响应,连接建立。
- 客户端发送
- 四次挥手 :断开连接的过程,因 TCP 连接是全双工的,需双方分别关闭:
- 主动方发送
FIN报文,请求关闭连接; - 被动方回复
ACK报文,确认收到关闭请求; - 被动方发送
FIN报文,请求关闭自身连接; - 主动方回复
ACK报文,确认关闭,连接断开。
- 主动方发送
2.2 TCP 编程核心流程
2.2.1 TCP 服务端流程
- 创建 Socket 对象:
socket.socket(AF_INET, SOCK_STREAM); - 绑定地址:
bind((host, port)),绑定本机 IP 和端口; - 监听连接:
listen(backlog),设置最大等待连接数; - 接受连接:
accept(),阻塞等待客户端连接,返回新的 Socket 对象(用于通信)和客户端地址; - 收发数据:
recv(bufsize)接收数据,send(data)/sendall(data)发送数据; - 关闭连接:关闭通信 Socket 和监听 Socket。
2.2.2 TCP 客户端流程
- 创建 Socket 对象:
socket.socket(AF_INET, SOCK_STREAM); - 连接服务端:
connect((host, port)),主动连接服务端地址; - 收发数据:
send(data)发送数据,recv(bufsize)接收响应; - 关闭连接:关闭 Socket。
2.3 TCP 编程实战代码
2.3.1 基础 TCP 回显服务(服务端 + 客户端)
服务端代码(tcp_server.py):实现监听客户端连接,接收数据并原样返回(回显),处理端口复用避免重启报错。
# -*- coding: utf-8 -*-
"""
TCP服务端:基础回显服务
功能:监听客户端连接,接收数据并回显,处理多连接基础逻辑
"""
import socket
import sys
def tcp_echo_server(host: str = "0.0.0.0", port: int = 8888, buffer_size: int = 1024):
"""
TCP回显服务端
:param host: 监听地址,0.0.0.0表示监听所有网卡
:param port: 监听端口
:param buffer_size: 接收数据缓冲区大小
"""
# 1. 创建TCP套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 设置端口复用,避免重启时端口占用报错
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 3. 绑定地址和端口
try:
server_socket.bind((host, port))
except socket.error as e:
print(f"绑定地址失败:{e}")
sys.exit(1)
# 4. 监听连接,backlog=5表示最大等待连接数
server_socket.listen(5)
print(f"TCP回显服务启动,监听地址:{host}:{port}")
print(f"缓冲区大小:{buffer_size}字节,等待客户端连接...")
try:
while True:
# 5. 阻塞等待客户端连接,返回新的通信套接字和客户端地址
client_socket, client_addr = server_socket.accept()
print(f"\n新客户端连接:{client_addr[0]}:{client_addr[1]}")
with client_socket: # 使用with自动关闭客户端套接字
while True:
# 6. 接收客户端数据,缓冲区大小为buffer_size
recv_data = client_socket.recv(buffer_size)
if not recv_data: # 客户端断开连接时,recv返回空字节
print(f"客户端{client_addr}断开连接")
break
# 解码数据(默认utf-8),处理编码异常
try:
recv_text = recv_data.decode("utf-8")
except UnicodeDecodeError:
print(f"接收数据编码错误,原始数据:{recv_data}")
recv_text = str(recv_data)
print(f"收到{client_addr}的数据:{recv_text}")
# 7. 回显数据:编码后发送
send_data = f"服务端回显:{recv_text}".encode("utf-8")
client_socket.sendall(send_data)
except KeyboardInterrupt:
print("\n服务端手动停止,关闭套接字...")
finally:
# 8. 关闭监听套接字
server_socket.close()
print("服务端套接字已关闭")
if __name__ == "__main__":
# 可通过命令行参数指定端口:python tcp_server.py 9999
if len(sys.argv) > 1:
tcp_echo_server(port=int(sys.argv[1]))
else:
tcp_echo_server()
客户端代码(tcp_client.py):主动连接服务端,发送用户输入的消息,接收回显数据。
# -*- coding: utf-8 -*-
"""
TCP客户端:基础回显客户端
功能:连接TCP服务端,发送消息并接收回显
"""
import socket
import sys
def tcp_echo_client(host: str = "127.0.0.1", port: int = 8888, buffer_size: int = 1024):
"""
TCP回显客户端
:param host: 服务端IP地址
:param port: 服务端端口
:param buffer_size: 接收缓冲区大小
"""
# 1. 创建TCP套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 2. 连接服务端
client_socket.connect((host, port))
print(f"已连接到TCP服务端:{host}:{port}")
print("输入消息发送(输入exit退出):")
while True:
# 3. 获取用户输入
send_text = input("你说:")
if send_text.lower() == "exit":
print("客户端退出连接...")
break
if not send_text:
print("请输入有效内容")
continue
# 4. 编码后发送数据
send_data = send_text.encode("utf-8")
client_socket.sendall(send_data)
# 5. 接收服务端回显数据
recv_data = client_socket.recv(buffer_size)
if not recv_data:
print("服务端断开连接")
break
# 解码并打印回显内容
recv_text = recv_data.decode("utf-8")
print(f"服务端说:{recv_text}")
except socket.error as e:
print(f"连接服务端失败:{e}")
except KeyboardInterrupt:
print("\n客户端手动退出...")
finally:
# 6. 关闭套接字
client_socket.close()
print("客户端套接字已关闭")
if __name__ == "__main__":
if len(sys.argv) > 1:
tcp_echo_client(port=int(sys.argv[1]))
else:
tcp_echo_client()
2.3.2 进阶:处理粘包问题
TCP 是字节流协议,无消息边界,连续发送的小数据会被合并,导致粘包问题 。例如:客户端连续发送"Hello"和"World",服务端可能一次接收"HelloWorld",无法区分两条消息。
解决方案:
- 固定长度:每条消息固定长度,不足补空格 / 0;
- 特殊分隔符 :用特殊字符(如
\r\n\r\n)分隔消息,需保证消息体不含分隔符; - 长度前缀法:在消息前添加 4 字节的长度标识,指定消息体长度(最常用)。
长度前缀法实战代码:
# -*- coding: utf-8 -*-
"""
TCP粘包处理:长度前缀法
核心:发送前添加4字节消息长度,接收时先读长度,再读对应长度数据
"""
import socket
import struct
# 发送函数:处理粘包
def send_msg(sock: socket.socket, data: bytes):
"""
发送带长度前缀的消息
:param sock: 通信套接字
:param data: 要发送的字节数据
"""
# 1. 计算消息长度,打包为4字节大端整数
data_len = len(data)
len_pack = struct.pack("!I", data_len) # !I表示大端无符号整数
# 2. 先发送长度,再发送数据
sock.sendall(len_pack + data)
# 接收函数:处理粘包
def recv_msg(sock: socket.socket, buffer_size: int = 1024) -> bytes | None:
"""
接收带长度前缀的消息
:param sock: 通信套接字
:param buffer_size: 缓冲区大小
:return: 接收的字节数据,None表示连接断开
"""
# 1. 先接收4字节长度
len_pack = sock.recv(4)
if not len_pack:
return None
# 2. 解包获取消息长度
data_len = struct.unpack("!I", len_pack)[0]
# 3. 循环接收指定长度的数据
recv_data = b""
while len(recv_data) < data_len:
chunk = sock.recv(min(buffer_size, data_len - len(recv_data)))
if not chunk:
return None
recv_data += chunk
return recv_data
# 改进后的服务端(处理粘包)
def tcp_sticky_server(host: str = "0.0.0.0", port: int = 8889):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((host, port))
server_socket.listen(5)
print(f"粘包处理服务端启动:{host}:{port}")
try:
while True:
client_socket, client_addr = server_socket.accept()
print(f"\n新客户端连接:{client_addr}")
with client_socket:
while True:
# 接收粘包处理后的消息
recv_data = recv_msg(client_socket)
if not recv_data:
print(f"客户端{client_addr}断开连接")
break
recv_text = recv_data.decode("utf-8")
print(f"收到{client_addr}的消息:{recv_text}")
# 回显消息
send_data = f"粘包处理回显:{recv_text}".encode("utf-8")
send_msg(client_socket, send_data)
except KeyboardInterrupt:
print("\n服务端停止")
finally:
server_socket.close()
# 改进后的客户端(处理粘包)
def tcp_sticky_client(host: str = "127.0.0.1", port: int = 8889):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, port))
print(f"已连接到服务端:{host}:{port}")
try:
# 模拟连续发送多条消息,测试粘包处理
messages = ["第一条消息", "第二条稍长一点的消息测试", "第三条消息ABCDEFG"]
for msg in messages:
send_data = msg.encode("utf-8")
send_msg(client_socket, send_data)
print(f"发送消息:{msg}")
# 接收回显
recv_data = recv_msg(client_socket)
if recv_data:
print(f"服务端回显:{recv_data.decode('utf-8')}")
print("\n测试完成,退出连接...")
except socket.error as e:
print(f"通信失败:{e}")
finally:
client_socket.close()
if __name__ == "__main__":
# 启动服务端:python tcp_sticky_server.py
# 启动客户端:python tcp_sticky_client.py
import sys
if sys.argv[0].endswith("tcp_sticky_server.py"):
tcp_sticky_server()
else:
tcp_sticky_client()
2.4 TCP 编程常见问题与解决方案
表格
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 端口占用报错 | Socket 关闭后进入 TIME_WAIT 状态,重启服务端无法绑定端口 | 设置SO_REUSEADDR选项:sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 接收数据为空 | 客户端正常断开连接,或网络异常 | 循环接收时判断if not recv_data,处理断开逻辑 |
| 发送数据失败 |