一些基本概念:
概念 | 说明 |
---|---|
IP地址 | 网络中唯一标识一台主机的地址(如 192.168.1.1) |
端口号 | 标识某台主机上的具体服务(如 80 是 HTTP 服务) |
协议 | 通信的规则和格式,常见如 TCP、UDP、HTTP、FTP |
客户端/服务器 | 客户端发起请求,服务器响应请求 |
DNS | 域名解析服务,将域名转为 IP 地址 |
一、网络基础知识
1. 计算机网络的分层结构
计算机网络通信是复杂的系统,为了简化设计,采用分层结构:
OSI 七层模型(理论模型)
层级 | 名称 | 作用简介 |
---|---|---|
7 | 应用层 | 用户软件与网络交互,如HTTP、FTP |
6 | 表示层 | 数据格式转换、加解密、压缩 |
5 | 会话层 | 建立、管理和终止会话 |
4 | 传输层 | 端到端传输控制,TCP/UDP |
3 | 网络层 | IP寻址与路由选择(IP、ICMP) |
2 | 数据链路层 | 点到点传输,MAC地址,ARP协议 |
1 | 物理层 | 物理传输,比特流(如电信号、光信号) |
TCP/IP 四层模型(实际使用)
层级 | 名称 | OSI对应 | 协议举例 |
---|---|---|---|
4 | 应用层 | 7,6,5 | HTTP, HTTPS, FTP, DNS |
3 | 传输层 | 4 | TCP, UDP |
2 | 网络层 | 3 | IP, ICMP |
1 | 网络接口层 | 2,1 | Ethernet, PPP |
2. 常见协议讲解
HTTP / HTTPS
-
HTTP 是超文本传输协议,基于 TCP,明文传输。
-
HTTPS 是加密的 HTTP,使用 TLS/SSL。
HTTP 报文结构:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
...
-
请求方法: GET、POST、PUT、DELETE...
-
状态码: 200 成功、404 未找到、500 服务器错误
TCP 与 UDP(传输层)
特点 | TCP | UDP |
---|---|---|
是否连接 | 面向连接(三次握手) | 无连接 |
可靠性 | 可靠(重传、校验、确认) | 不可靠 |
传输速度 | 慢 | 快 |
应用举例 | HTTP、FTP、SMTP | DNS、视频、语音、游戏 |
三次握手流程:
-
客户端发送 SYN
-
服务器回复 SYN+ACK
-
客户端回复 ACK
四次挥手流程:
-
主动方发送 FIN
-
被动方回复 ACK
-
被动方发送 FIN
-
主动方回复 ACK
3. IP地址、子网与路由
IP 地址分类(IPv4)
类别 | 地址范围 | 特点 |
---|---|---|
A | 1.0.0.0 - 126.255.255.255 | 大型网络 |
B | 128.0.0.0 - 191.255.255.255 | 中型网络 |
C | 192.0.0.0 - 223.255.255.255 | 小型网络 |
私有地址(常见于内网)
-
A类:10.0.0.0/8
-
B类:172.16.0.0/12
-
C类:192.168.0.0/16
子网掩码
用来划分子网,例如:
-
IP: 192.168.1.1
-
子网掩码:255.255.255.0 表示前24位是网络位,后8位是主机位
4. DNS域名解析流程
-
浏览器检查本地缓存
-
向本地 DNS 查询
-
本地 DNS 若无结果则向根服务器查找
-
找到对应的权威 DNS 返回 IP 地址
二、Socket 编程
Socket 是“套接字”的意思,是操作系统提供的一种网络通信接口。它是网络编程中最底层、最关键的部分,用来建立客户端和服务端之间的连接,实现数据的发送与接收。
可以把 Socket 理解为一个通信端口,客户端和服务端都通过各自的 Socket 建立连接,就像电话的两端:
-
客户端主动拨号(connect)
-
服务器监听电话(listen),接听(accept)
-
双方通话(send、recv)
-
最后挂断电话(close)
1. Socket 通信步骤(以 TCP 为例)
服务端流程:
-
socket()
创建 Socket 对象 -
bind()
绑定 IP 和端口 -
listen()
开始监听 -
accept()
等待客户端连接 -
recv()
接收数据 -
send()
发送响应 -
close()
关闭连接
客户端流程:
-
socket()
创建 Socket 对象 -
connect()
连接服务端 -
send()
发送请求 -
recv()
接收服务端返回 -
close()
关闭连接
2. 详细案例:模拟聊天室(单连接版本)
服务端代码(server.py)
import socket
# 创建 socket 对象
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定 IP 和端口
server.bind(('127.0.0.1', 8000))
# 开始监听,最多同时挂起 5 个连接
server.listen(5)
print("服务器启动,等待连接...")
# 等待客户端连接
conn, addr = server.accept()
print("连接来自:", addr)
while True:
# 接收客户端数据
data = conn.recv(1024)
if not data:
break # 断开连接
print("客户端说:", data.decode())
# 发送响应
reply = input("你回复:")
conn.send(reply.encode())
# 关闭连接
conn.close()
server.close()
客户端代码(client.py)
import socket
# 创建 socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
client.connect(('127.0.0.1', 8000))
print("连接服务器成功")
while True:
msg = input("你说:")
if msg.lower() == 'exit':
break
client.send(msg.encode())
# 接收服务端返回
reply = client.recv(1024)
print("服务器说:", reply.decode())
# 关闭连接
client.close()
使用说明:
-
先运行服务端:
python server.py
-
再运行客户端:
python client.py
-
客户端输入消息,服务器可接收并回复
-
双向通信,直到
exit
3. Socket 编程核心参数解释
socket.socket(family=AF_INET, type=SOCK_STREAM)
参数 | 含义 |
---|---|
AF_INET | 使用 IPv4 地址 |
SOCK_STREAM | 使用 TCP 协议(若用 UDP 则写 SOCK_DGRAM) |
4. Socket 编程注意事项
-
每个连接都需要一个 socket 对象处理:服务端 accept 之后要分线程处理
-
传输的数据需是字节串(bytes),不是字符串,注意编码
encode()/decode()
-
recv() 是阻塞的,没有数据时会等待
-
端口被占用或断线要处理异常
5. 基于 Python Socket + 多线程的群聊软件
软件结构
-
server.py
:服务端(监听连接 + 广播消息) -
client.py
:客户端(发送 + 接收消息)
服务端 server.py
import socket
import threading
# 存储所有客户端连接
clients = []
# 广播消息给所有客户端
def broadcast(message, sender_socket):
for client in clients:
if client != sender_socket:
try:
client.send(message)
except:
clients.remove(client)
# 每个客户端的处理线程
def handle_client(client_socket, addr):
print(f"[+] 新连接:{addr}")
clients.append(client_socket)
client_socket.send("欢迎加入群聊!\n".encode())
while True:
try:
msg = client_socket.recv(1024)
if not msg:
break
full_msg = f"[{addr[0]}:{addr[1]}] 说:{msg.decode()}"
print(full_msg.strip())
broadcast(full_msg.encode(), client_socket)
except:
break
print(f"[-] 断开连接:{addr}")
clients.remove(client_socket)
client_socket.close()
def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 9999))
server.listen(10)
print("[*] 群聊服务器已启动,等待客户端连接...")
while True:
client_socket, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(client_socket, addr))
thread.daemon = True
thread.start()
if __name__ == '__main__':
main()
客户端 client.py
import socket
import threading
def receive_messages(sock):
while True:
try:
data = sock.recv(1024)
if not data:
break
print(data.decode())
except:
break
def main():
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9999)) # 改成你服务端IP也可以
print("已连接到群聊服务器。输入内容开始聊天,输入 exit 退出。\n")
# 开启接收消息线程
thread = threading.Thread(target=receive_messages, args=(client,))
thread.daemon = True
thread.start()
while True:
msg = input()
if msg.lower() == 'exit':
break
try:
client.send(msg.encode())
except:
break
client.close()
print("已退出群聊。")
if __name__ == '__main__':
main()
运行说明
打开终端运行服务端:
python server.py
输出:
[*] 群聊服务器已启动,等待客户端连接...
多个终端分别运行客户端(你可以多开几个):
python client.py
输入内容即可发消息,其他客户端会同步收到。输入 exit
退出群聊。
关键点解释
技术 | 说明 |
---|---|
socket.socket() | 创建 TCP 连接 |
threading.Thread | 多线程处理多个客户端 |
broadcast() | 把消息发给除自己以外的所有人 |
recv() 和 send() | 接收和发送字节数据 |
daemon=True | 子线程随着主线程退出而退出 |
三、socket.makefile()
将 Socket 对象包装成类文件对象(类文件接口),从而可以使用 readline()
、read()
、write()
等操作,就像操作文件一样。
Python 的 socket
通常使用的是低级接口,如:
data = sock.recv(1024)
sock.send(b"hello")
这种方式只能收发固定长度的字节串,不太适合处理“按行读取”等文本协议(如 HTTP、SMTP、POP3 等)。
1. makefile()
的作用
sock_file = sock.makefile(mode='rwb')
你可以将一个 socket 包装成“文件流”,用 sock_file.read()
, sock_file.readline()
,更加灵活,特别适合“基于行”的通信协议。
2. 方法签名
socket.makefile(mode='r', buffering=None, encoding=None, errors=None, newline=None)
参数 | 作用 |
---|---|
mode | 模式,如 'r' (读)、'w' (写)、'rb' (二进制读)等 |
buffering | 缓冲策略(通常默认即可) |
encoding | 文本编码,如 'utf-8' |
newline | 设置换行符行为 |
3. 实际例子:模拟 HTTP 服务端
import socket
def handle_client(conn):
# 将 socket 包装为类文件对象,方便按行读取
rfile = conn.makefile('r', encoding='utf-8')
wfile = conn.makefile('w', encoding='utf-8')
# 读取第一行(GET / HTTP/1.1)
line = rfile.readline()
print("请求行:", line.strip())
# 忽略请求头,只读空行分隔
while rfile.readline().strip() != "":
continue
# 写入 HTTP 响应
wfile.write("HTTP/1.1 200 OK\r\n")
wfile.write("Content-Type: text/plain\r\n")
wfile.write("\r\n") # 头部结束
wfile.write("Hello from Python server!\n")
wfile.flush()
# 关闭文件流和 socket
wfile.close()
rfile.close()
conn.close()
def main():
server = socket.socket()
server.bind(('0.0.0.0', 8888))
server.listen(1)
print("服务器已启动:http://127.0.0.1:8888")
while True:
conn, addr = server.accept()
handle_client(conn)
if __name__ == '__main__':
main()
功能:
-
可以用浏览器访问
http://127.0.0.1:8888
-
返回纯文本内容
-
用
makefile()
实现按行读取 HTTP 请求,便于分析请求行和头部
4. 常见用法场景
场景 | 为什么适合用 makefile() |
---|---|
HTTP 协议 | 请求头是按行组织的,需要 readline() |
SMTP/POP3 | 邮件协议以行分隔命令和响应 |
自定义协议 | 若你设计了以\n 结尾的消息格式 |
使用 json , text , base64 编码 | 不需要手动切片二进制流 |
5. 与 recv()
/send()
区别总结
方法 | 特点 |
---|---|
recv() / send() | 底层,二进制收发,需要自己控制协议边界 |
makefile() 文件流 | 高层,支持按行读写,适合文本协议处理 |
四、udp
UDP 是一种无连接、不可靠但速度快的协议。
与 TCP 的区别:
特性 | TCP | UDP |
---|---|---|
是否连接 | 是(连接后通信) | 否(直接发消息) |
是否可靠 | 是(顺序、重传) | 否(可能丢失) |
应用场景 | HTTP、FTP、SSH | 视频、语音、DNS、局域网群聊 |
编程模型 | 服务端需 accept() | 直接 recvfrom()/sendto() |
1. UDP 编程基础结构
服务端(接收消息):
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 9999))
while True:
data, addr = sock.recvfrom(1024)
print(f"收到来自 {addr} 的消息:{data.decode()}")
客户端(发送消息):
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = input("你说:")
sock.sendto(msg.encode(), ("127.0.0.1", 9999))
注意:UDP 不需要 listen()
、accept()
,而是直接收发。
2. UDP 群聊系统实现(支持多客户端)
功能
-
所有人加入就可以发消息
-
所有人能实时收到群消息
-
基于局域网(不跨公网)
服务器端 udp_server.py
import socket
# 用于存储所有客户端的地址(set 自动去重)
clients = set()
def main():
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('0.0.0.0', 9999))
print("[*] UDP 群聊服务器已启动,端口 9999")
while True:
data, addr = server.recvfrom(1024)
message = data.decode()
print(f"[{addr}]:{message}")
# 加入客户端集合
clients.add(addr)
# 广播给其他所有客户端
for client in clients:
if client != addr:
server.sendto(f"[{addr}] 说: {message}".encode(), client)
if __name__ == '__main__':
main()
客户端 udp_client.py
import socket
import threading
SERVER_IP = '127.0.0.1'
SERVER_PORT = 9999
def receive(sock):
while True:
try:
data, _ = sock.recvfrom(1024)
print(data.decode())
except:
break
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 启动接收线程
threading.Thread(target=receive, args=(sock,), daemon=True).start()
print("已加入群聊。输入内容发送消息,输入 exit 退出。")
while True:
msg = input()
if msg.lower() == 'exit':
break
sock.sendto(msg.encode(), (SERVER_IP, SERVER_PORT))
sock.close()
if __name__ == '__main__':
main()
运行方式
启动服务器:
python udp_server.py
多个终端运行客户端(可复制运行):
python udp_client.py
聊天,输入内容自动广播给所有客户端。
关键点解释
关键词 | 含义 |
---|---|
recvfrom() | 接收数据,同时获取客户端地址 |
sendto() | 向指定地址发送数据 |
set() | 存储客户端地址集合,避免重复广播 |
多线程 | 每个客户端用线程接收消息,防止阻塞 |
daemon=True | 守护线程,不阻止主线程退出 |
五、心跳机制
心跳机制(Heartbeat Mechanism)是网络通信中用于判断连接是否仍然有效的机制,常用于 TCP、WebSocket、UDP 等长连接场景,确保客户端/服务端还“活着”。
在长连接中,即使两端掉线,操作系统也可能不知道,这会造成资源浪费或服务异常。
例如:
-
客户端死机、断网、宕机,但服务端还以为它在线
-
服务端宕机,客户端还在等回复
所以需要“定时问候”一下彼此是否还活着,这就是心跳机制。
客户端 → 每隔5秒 → 发送 ping → 服务端
服务端 ← 立即回应 ← pong
服务端定时检查:超过10秒没收到 ping 的客户端 = 掉线
TCP 心跳机制实现
我们以 TCP 的 socket
连接为例,用两个线程:
-
一个线程用来收消息
-
一个线程用来定时发心跳
服务端:tcp_heartbeat_server.py
import socket
import threading
import time
clients = {} # 存储客户端连接及最后心跳时间
def handle_client(conn, addr):
clients[addr] = time.time()
print(f"[{addr}] 连接")
while True:
try:
data = conn.recv(1024)
if not data:
break
msg = data.decode().strip()
if msg == "ping":
clients[addr] = time.time()
conn.sendall(b"pong")
else:
print(f"[{addr}] 消息: {msg}")
conn.sendall(b"ok")
except:
break
print(f"[{addr}] 断开连接")
conn.close()
del clients[addr]
def check_heartbeat():
while True:
now = time.time()
for addr, last in list(clients.items()):
if now - last > 10: # 超过10秒无响应
print(f"[{addr}] 心跳超时,关闭连接")
# 注意:这里只是标记断开,实际关闭应通过 conn.close()
del clients[addr]
time.sleep(5)
def main():
server = socket.socket()
server.bind(("0.0.0.0", 9001))
server.listen(5)
print("服务器启动,监听 9001")
threading.Thread(target=check_heartbeat, daemon=True).start()
while True:
conn, addr = server.accept()
threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()
if __name__ == "__main__":
main()
客户端:tcp_heartbeat_client.py
import socket
import threading
import time
def receive(sock):
while True:
try:
data = sock.recv(1024)
if not data:
break
print("服务器回复:", data.decode())
except:
break
def heartbeat(sock):
while True:
try:
sock.sendall(b"ping")
except:
break
time.sleep(3)
def main():
sock = socket.socket()
sock.connect(("127.0.0.1", 9001))
# 启动收消息线程
threading.Thread(target=receive, args=(sock,), daemon=True).start()
# 启动心跳线程
threading.Thread(target=heartbeat, args=(sock,), daemon=True).start()
# 输入消息
while True:
msg = input()
if msg == "exit":
break
sock.sendall(msg.encode())
sock.close()
if __name__ == "__main__":
main()
技术点 | 说明 |
---|---|
ping /pong | 心跳包格式,可以是任何自定义内容 |
超时检测 | 服务器定期检查客户端最后活跃时间 |
recv() 超时 | 可配合 socket.settimeout() 判断 |
daemon=True | 心跳与接收线程不会阻塞主进程关闭 |
time.sleep() | 控制心跳频率(建议 5~30 秒一次) |
六、socketserver
socketserver
是 Python 标准库中封装好的一个网络服务器框架,基于 socket
封装,自动处理:
-
连接监听
-
多客户端支持
-
数据接收、响应
-
多线程或多进程支持
简单说:用少量代码实现一个并发网络服务器!
手动 socket 写法(很烦):
while True:
conn, addr = server.accept()
# 手动写线程
t = threading.Thread(target=handle, args=(conn,))
t.start()
用 socketserver
(超简单):
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
self.data = self.request.recv(1024)
self.request.sendall(b'hi')
server = socketserver.ThreadingTCPServer(('0.0.0.0', 9001), MyHandler)
server.serve_forever()
它自动处理线程、连接管理,你只要管数据怎么处理。
1. 核心类介绍
类名 | 描述 |
---|---|
BaseRequestHandler | 所有请求处理类的基类 |
TCPServer | 单线程 TCP 服务器 |
ThreadingTCPServer | 多线程 TCP 服务器(常用) |
ForkingTCPServer | 多进程 TCP 服务器(Unix) |
UDPServer | 单线程 UDP 服务器 |
ThreadingUDPServer | 多线程 UDP 服务器 |
2. 最常用结构:多线程 TCP 服务
import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
print(f"连接来自: {self.client_address}")
while True:
data = self.request.recv(1024)
if not data:
break
print("收到:", data.decode())
self.request.sendall(b"我收到了: " + data)
# 创建多线程 TCP 服务器
server = socketserver.ThreadingTCPServer(("0.0.0.0", 9001), MyHandler)
print("服务器已启动")
server.serve_forever()
客户端(可用原始 socket):
import socket
sock = socket.socket()
sock.connect(("127.0.0.1", 9001))
while True:
msg = input("你说:")
sock.send(msg.encode())
data = sock.recv(1024)
print("服务器说:", data.decode())
3. 关键点解释
概念 | 解释 |
---|---|
BaseRequestHandler | 自定义处理逻辑,重写 handle() 方法 |
self.request | 表示客户端连接对象(一个 socket) |
self.client_address | 表示客户端 IP 和端口元组 |
ThreadingTCPServer | 每个客户端连接自动起一个线程 |
serve_forever() | 启动服务器并阻塞运行 |
总结:socketserver
是 Python 提供的一个高层网络服务器封装模块,让你专注写逻辑,不用再操心 socket 和多线程细节。
七、同步和阻塞
同步 vs 异步
- 同步(Synchronous):你做事必须一步一步等结果。
比喻:你排队打饭,前面人不动你就不能动。
- 异步(Asynchronous):你发出请求就继续干别的,结果回来再处理。
比喻:你点了外卖就继续学习,饭到了再吃。
阻塞 vs 非阻塞
- 阻塞(Blocking):程序卡在那等结果,不返回。
比喻:你打电话,电话没人接,你就只能一直等。
- 非阻塞(Non-blocking):程序立刻返回,即使没结果也继续执行。
比喻:你打电话没人接,立刻挂断去做别的事。
类型 | 行为 |
---|---|
同步阻塞 | 一步一步 + 等结果,最常见的方式(比如 recv() ) |
同步非阻塞 | 一步一步 + 不等结果,很少用 |
异步阻塞 | 程序逻辑异步,但中间有阻塞等待(比如异步里 await ) |
异步非阻塞(理想) | 真正并发,不等待结果,比如回调、协程、事件驱动 |
1. 同步 + 阻塞(最常见)
客户端连接服务器后,recv() 会阻塞,直到有数据来。
import socket
sock = socket.socket()
sock.connect(('127.0.0.1', 9001))
sock.send(b'hello')
# 同步阻塞:这里会一直等,直到服务器发数据
data = sock.recv(1024)
print("收到:", data.decode())
运行这个,如果服务器不回复,程序会卡在 recv()
那一行。
2. 非阻塞 socket 示例
将 socket 设置为非阻塞模式,立刻返回,不管有没有数据。
import socket
sock = socket.socket()
sock.connect(('127.0.0.1', 9001))
sock.setblocking(False) # 设置为非阻塞
sock.send(b'hello')
try:
data = sock.recv(1024) # 如果没数据会直接报错
print("收到:", data.decode())
except BlockingIOError:
print("当前没有数据,继续干别的事")
输出:
当前没有数据,继续干别的事
3. 异步 + 非阻塞(使用 asyncio)
import asyncio
async def client():
reader, writer = await asyncio.open_connection('127.0.0.1', 9001)
writer.write(b'hello\n')
await writer.drain()
# 等待数据(不阻塞整个程序,其他任务仍能运行)
data = await reader.read(100)
print("收到:", data.decode())
asyncio.run(client())
这个异步程序在 await
期间不会卡住主线程,可以同时执行多个任务。
总结
模式 | 阻塞? | 异步? | 说明 |
---|---|---|---|
sock.recv() | 阻塞 | 同步 | 一直等数据,主线程被卡住 |
sock.setblocking(False) + recv() | 非阻塞 | 同步 | 立刻返回,没有数据就报错 |
asyncio + await reader.read() | 非阻塞 | 异步 | 数据没来不会卡死主线程,可并发处理 |
八、IO多路复用
I/O 多路复用 是一种机制,允许单个线程/进程同时监听多个 socket 连接的读写状态,从而高效处理多个客户端连接。
原始 socket 写法(同步 + 阻塞)的问题:
-
每个客户端都要一个线程,线程太多浪费资源。
-
有些连接很久才发一次数据,占着资源没事干。
多路复用的优势是:
一个线程监听多个 socket,只在真正有数据时才处理,节省资源,效率高。
模型 | 说明 | Python对应模块 |
---|---|---|
select | 最基础,跨平台,但效率低(有最大连接限制) | select.select() |
poll | 没有连接数量限制,效率高 | select.poll() |
epoll | Linux专用,效率最高(推荐) | select.epoll() |
主线程:
┌────────────────────────────┐
│ select/poll/epoll 监听多个 socket │
└────┬───────────────┬─────┘
│ │
客户端1 客户端2
(有数据) (没数据)
=> 只处理有数据的 socket,节省 CPU 和线程资源
Python 示例:select 多路复用
这是一个使用 select
同时监听多个客户端的服务器示例。
import socket
import select
# 创建 TCP socket
server = socket.socket()
server.bind(('0.0.0.0', 9001))
server.listen()
server.setblocking(False) # 设置非阻塞
inputs = [server] # 要监听的 socket 列表
print("服务器启动")
while True:
readable, _, _ = select.select(inputs, [], [])
for sock in readable:
if sock is server:
conn, addr = server.accept()
print("新连接:", addr)
conn.setblocking(False)
inputs.append(conn) # 把新连接加入监听列表
else:
try:
data = sock.recv(1024)
if data:
print("收到:", data.decode())
sock.send(b"我收到了")
else:
print("客户端断开")
inputs.remove(sock)
sock.close()
except:
inputs.remove(sock)
sock.close()
模型 | 特点 | 建议用途 |
---|---|---|
多线程/进程 | 简单但资源开销大 | 客户端少、简单场景 |
select | 跨平台,但连接数有限 | 学习、简单并发场景 |
epoll | 高性能,适合高并发 | 推荐用于生产环境 |
asyncio | 基于多路复用封装的协程模型 | 写异步代码更优雅 |
I/O 多路复用就是一个线程高效监听多个 socket,只处理“有事的连接”,提高并发性能,是写高性能网络服务的基础。
九、事件驱动
事件驱动是一种编程模型,它的核心思想是:
程序不主动一步步执行,而是等待事件发生,再执行对应的事件处理函数。
就像生活中打电话、收快递:
-
电话响(事件) → 你接电话(事件处理)
-
快递到了(事件) → 你签收(事件处理)
事件驱动模型的流程
整体流程:
事件源(如 socket)→ 等待事件(注册)→ 检测事件发生 → 回调处理函数
举个实际例子(聊天服务器):
-
等待客户端连接
-
客户端发来消息(事件发生)
-
触发“on_message”处理函数,服务器回应
事件驱动 vs 常见模型
模型 | 描述 |
---|---|
同步阻塞模型 | 一步步执行,哪个阻塞就卡哪 |
多线程模型 | 每个连接用一个线程,同时处理多个 |
事件驱动模型 | 一个线程 + 事件循环 + 回调函数,高效处理并发 |
事件驱动的关键组件
组件 | 说明 |
---|---|
事件源 | 比如:socket、用户操作、定时器等 |
事件循环 | 不断检测“是否有事件发生” |
回调函数 | 事件发生后自动调用的处理函数 |
事件注册表 | 记录哪个事件应该调用哪个函数 |
Python 中事件驱动的常用方式
方式 1:asyncio
(推荐)
import asyncio
async def handle_client(reader, writer):
data = await reader.read(100)
print("收到:", data.decode())
# 将字符串转换为字节串
writer.write("我收到了".encode('utf-8'))
await writer.drain()
writer.close()
async def main():
server = await asyncio.start_server(handle_client, '0.0.0.0', 9001)
print("服务器启动")
async with server:
await server.serve_forever()
asyncio.run(main())
核心思想:事件发生时(socket 可读)→ 调用 handler
方式 2:自己实现 select + 回调(底层事件驱动)
import socket, select
def on_recv(sock):
data = sock.recv(1024)
print("收到:", data.decode())
sock.send(b'收到')
sock = socket.socket()
sock.bind(('0.0.0.0', 9001))
sock.listen()
sock.setblocking(False)
inputs = [sock]
handlers = {}
while True:
readable, _, _ = select.select(inputs, [], [])
for s in readable:
if s is sock:
conn, addr = sock.accept()
conn.setblocking(False)
inputs.append(conn)
handlers[conn] = on_recv
else:
handlers[s](s)
事件驱动的优点
-
高性能:一个线程可管理上千个连接(如 nginx、Node.js)
-
资源低:无需频繁创建线程或进程
-
响应快:只处理“有事件”的部分
总结:事件驱动模型让程序“不主动等”,而是“监听 + 回调”,是高并发、高性能服务器的核心思想(如:asyncio
、Node.js、nginx、epoll 等)。
十、WebSocket 通信
WebSocket 是一种用于在客户端与服务器之间建立持久化、双向通信的协议。它基于 TCP 协议,通过 HTTP 协议进行握手,连接建立后,客户端和服务器可以在不重复握手的情况下,随时交换数据,极大地提高了实时性和效率。
1. WebSocket 基本原理
WebSocket 协议在初次建立连接时,使用 HTTP 协议进行握手,之后转换为 WebSocket 协议进行双向通信,并保持该连接。
-
HTTP 握手阶段:
-
客户端向服务器发送 WebSocket 握手请求(通过 HTTP 协议),并指定将要升级到 WebSocket 协议。
-
服务器收到请求后,返回一个响应,表示同意升级协议。
-
握手完成后,通信双方就建立了 WebSocket 连接。
-
-
数据传输阶段:
-
一旦连接建立,客户端和服务器可以随时向对方发送数据。
-
数据传输基于 WebSocket 协议,是二进制数据和文本数据的流,并且在两端都可以异步发送和接收数据。
-
-
连接关闭阶段:
-
连接的关闭是由客户端或服务器发起的,双方进行“关闭握手”后,连接将被关闭。
-
2. WebSocket 的特点
-
全双工:客户端和服务器可以同时双向传输数据,而不需要等待对方完成操作。
-
长连接:WebSocket 协议会保持连接,在连接期间可以频繁交换数据,避免了每次请求都需要重新建立连接。
-
低延迟:避免了传统的轮询或长轮询方式中多次请求的延迟,提高了实时性。
-
节省带宽:相比 HTTP 协议每次请求都要包含完整的头信息,WebSocket 数据包更加轻量。
3. WebSocket 客户端和服务器实现
客户端代码(Python 使用 websockets
库)
首先,安装 websockets
库:
pip install websockets
客户端代码:
import asyncio
import websockets
async def hello():
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
await websocket.send("Hello Server!")
response = await websocket.recv()
print(f"服务器回应: {response}")
asyncio.get_event_loop().run_until_complete(hello())
服务器代码(Python 使用 websockets
库)
import asyncio
import websockets
async def echo(websocket, path):
message = await websocket.recv() # 接收客户端消息
print(f"收到客户端消息: {message}")
await websocket.send(f"服务器回应: {message}") # 向客户端发送回应
start_server = websockets.serve(echo, "localhost", 8765)
# 启动 WebSocket 服务器
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
解释:
-
websockets.connect(uri)
:客户端使用此函数连接到服务器,uri
表示服务器的 WebSocket 地址。 -
websocket.send()
:向服务器发送数据。 -
websocket.recv()
:接收服务器发送的数据。 -
websockets.serve()
:服务器端用于启动 WebSocket 服务,指定了服务器地址和端口。
4. WebSocket 协议握手过程
WebSocket 协议的握手阶段,实际上是一个 HTTP 升级请求,它使用 HTTP 协议发送一个特定的请求头来告知服务器,它希望升级为 WebSocket 连接。
客户端请求示例:
GET / HTTP/1.1
Host: localhost:8765
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
-
Upgrade: 表示客户端希望升级到 WebSocket 协议。
-
Connection: 必须是
Upgrade
,表示连接要被升级。 -
Sec-WebSocket-Key: 随机生成的键,服务器需要使用这个键生成一个响应。
-
Sec-WebSocket-Version: 客户端支持的 WebSocket 协议版本。
服务器回应示例:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: dGhlIHNhbXBsZSBub25jZQ==
-
Sec-WebSocket-Accept: 服务器通过
Sec-WebSocket-Key
和一个固定的 GUID 生成的响应密钥,用来验证客户端的请求。
握手完成后,客户端和服务器就切换到 WebSocket 协议进行数据传输。
5. WebSocket 关闭连接
WebSocket 连接的关闭是由客户端或服务器发起的关闭握手:
客户端发起关闭:
await websocket.close()
服务器发起关闭:
await websocket.close()
关闭后,连接会被切断,任何后续数据都无法再传输。
总结:WebSocket 提供了一个高效的双向通信协议,广泛用于需要实时通信的应用场景(如即时聊天、游戏、金融实时数据等)。相比传统的 HTTP,WebSocket 在性能和实时性上具有显著优势。
十一、requests 库
requests
是一个非常流行且简洁易用的 Python 庑库,主要用于发送 HTTP 请求,获取服务器的响应。它比 Python 标准库中的 urllib
更加简单和直观,常用于与 Web 服务进行交互(例如 REST API 调用、网页爬取等)。
1. 安装 requests
库
首先,如果你的环境中没有安装 requests
,可以通过以下命令安装:
pip install requests
2. 基本概念
requests
库主要用于发起 HTTP 请求,并能够处理 HTTP 的请求头、请求体、响应头、响应体等内容。它支持各种 HTTP 方法(如 GET、POST、PUT、DELETE、HEAD 等)和一些高级特性(如会话维持、身份验证、代理等)。
常见 HTTP 请求方法
-
GET:用于从服务器获取资源。请求数据通过 URL 传递。
-
POST:用于向服务器提交数据(例如表单数据或 JSON 数据)。
-
PUT:用于向服务器更新现有资源。
-
DELETE:用于删除服务器上的资源。
-
HEAD:与 GET 方法类似,但只返回响应头,不返回响应体。
3. 常见功能及用法
GET 请求
GET
请求用于获取资源(如网页内容、API 返回数据等)。通过 requests.get()
发起请求。
import requests
# 发起 GET 请求
response = requests.get("https://jsonplaceholder.typicode.com/posts")
# 获取响应内容
print(response.text) # 返回响应体文本内容
print(response.status_code) # 返回响应的状态码
print(response.json()) # 如果响应内容是 JSON 格式,使用 .json() 转换为 Python 字典
-
response.text
:返回响应体的文本内容。 -
response.status_code
:返回 HTTP 响应的状态码。 -
response.json()
:如果服务器返回的是 JSON 格式数据,可以使用此方法将其转换为 Python 字典。
POST 请求
POST
请求用于向服务器发送数据(如表单数据)。通过 requests.post()
发起请求。
import requests
url = "https://jsonplaceholder.typicode.com/posts"
data = {"title": "foo", "body": "bar", "userId": 1}
# 发起 POST 请求
response = requests.post(url, json=data) # 使用 json 参数自动将数据转换为 JSON 格式
# 获取响应内容
print(response.text)
json=data
:将 Python 字典自动转化为 JSON 格式的请求体并发送。
发送表单数据(POST)
import requests
url = "https://httpbin.org/post"
data = {"username": "admin", "password": "123456"}
# 使用 form 表单提交数据
response = requests.post(url, data=data)
print(response.json()) # 获取响应的 JSON 格式数据
data=data
:将数据以表单格式发送。
发送文件(POST)
import requests
url = "https://httpbin.org/post"
files = {'file': open('example.txt', 'rb')} # 以二进制方式打开文件
# 发送文件
response = requests.post(url, files=files)
print(response.json())
files=files
:用于上传文件。可以上传一个或多个文件。
添加请求头
import requests
url = "https://jsonplaceholder.typicode.com/posts"
headers = {
"User-Agent": "my-app",
"Authorization": "Bearer <your_token>"
}
response = requests.get(url, headers=headers)
print(response.text)
headers
:可以通过传递字典来指定请求头。
URL 参数(Query Parameters)
import requests
url = "https://jsonplaceholder.typicode.com/posts"
params = {'userId': 1}
# 发起 GET 请求,带 URL 参数
response = requests.get(url, params=params)
print(response.text)
params=params
:URL 参数会被自动拼接到 URL 后面。
处理 JSON 响应
import requests
url = "https://jsonplaceholder.typicode.com/posts/1"
response = requests.get(url)
# 获取 JSON 数据并处理
data = response.json()
print(data)
使用 response.json()
直接获取 JSON 响应并转换成 Python 字典。
异常处理
请求可能会因为多种原因失败(如网络问题、服务器不可达等)。因此,在使用 requests
时,可以通过异常处理来捕获错误。
import requests
from requests.exceptions import RequestException
try:
response = requests.get("https://jsonplaceholder.typicode.com/posts")
response.raise_for_status() # 如果响应状态码不是 2xx,会抛出异常
except RequestException as e:
print(f"请求错误:{e}")
else:
print(response.text)
response.raise_for_status()
:检查响应的状态码,如果是 4xx 或 5xx 错误,会抛出异常。
4. 进阶用法
会话管理(Session)
requests
库的 Session
对象可以帮助我们保持某些参数(如 cookies)跨请求持续存在。适用于需要登录的场景。
import requests
# 创建会话对象
session = requests.Session()
# 使用会话对象发起请求
session.post("https://httpbin.org/post", data={"username": "admin", "password": "123456"})
# 发送后续请求时,session 会自动保持之前的 cookies
response = session.get("https://httpbin.org/cookies")
print(response.text)
超时设置
可以通过设置 timeout
来限制请求等待的最大时间,防止程序一直挂起。
import requests
try:
response = requests.get("https://jsonplaceholder.typicode.com/posts", timeout=3) # 设置超时为 3 秒
print(response.text)
except requests.Timeout:
print("请求超时!")
代理设置
通过代理服务器发送请求:
import requests
proxies = {
"http": "http://10.10.1.10:3128",
"https": "https://10.10.1.10:1080"
}
response = requests.get("https://jsonplaceholder.typicode.com/posts", proxies=proxies)
print(response.text)
proxies
:字典中包含了代理服务器的地址,http
和 https
的代理分别设置。
5. 总结
requests
是 Python 中非常方便和常用的网络请求库,它封装了 HTTP 请求的常见操作,提供了简单易用的 API,可以非常方便地进行 GET、POST 请求、表单提交、文件上传、JSON 数据处理等操作。同时,requests
还支持会话管理、异常处理、请求头和代理设置等高级功能。
对于网络编程和数据爬取任务,requests
是一个非常实用的工具,能够更高效地与 Web 服务进行交互。