第6章 6.1 net 与 socket 编程
上一章我们做完了一个完整的博客系统,从数据库到前端页面全都跑通了。做完之后你有没有想过一个问题:你的浏览器是怎么跟服务器「打电话」的?打开博客、登录后台,这些数据是怎么嗖一下就过去了的?
这一章我们要揭开这个底牌——学习 socket 编程,也就是计算机和网络「通话」的基本功。学会了它,你就能写出聊天室、文件传输工具、甚至自己造一个迷你版的 QQ。
🎯 开场 3 分钟:为什么要学这个?
先问自己一个问题:你上网的时候,数据是怎么从你家电脑跑到千里之外的服务器,又跑回来的?
举个好理解的例子。你点开一篇博客文章:
- 你的电脑发了个「请求」给服务器:「喂,我要看这篇文章」
- 服务器收到,肚子里翻出文章内容
- 服务器把内容「塞进信封」寄回来
- 你的浏览器收到,拆开信封,展示给你看
这个「发请求 → 收响应」的过程,背后就是 socket 在干活。
生活类比:socket 就像电话机。你不需要懂电话线怎么走、信号怎么调制,拿起电话拨号码,对方接起来,你们就能说话。socket 就是程序里的「电话机」——帮你搞定网络通信的底层破事儿,让你专心写业务逻辑。
学完这章你能:
- 理解 TCP/IP 网络的基本工作原理
- 用 Python 写一个能接收和发送数据的「服务器」
- 做一个两人或多人的简易聊天室
- 知道怎么处理字节流,不会乱码
🧱 基础 25 分钟:核心概念
什么是 socket?
Socket(套接字)是计算机网络通信的基本抽象。你可以把它理解成插头和插座:
- IP 地址 = 你家地址(比如「北京市朝阳区 XX 路 1 号」)
- 端口号 = 房间号(比如「502」)
- Socket = 插头 + 插座,没有它电就没法通
你的电脑要跟另一台电脑聊天,首先得知道对方的 IP(找谁聊)和端口(聊什么服务)。就好比你打电话,得知道对方电话号码和分机号。
什么是 TCP?
TCP(传输控制协议)是 socket 编程最常用的「语言」。它有三大特点:
- 面向连接:打电话前得先拨号、对方接起来,才能说话
- 可靠传输:说过的话对方没听清,会要求重说,不会丢数据
- 有顺序:先说的话先到,不会乱序
对比一下 UDP(另一种协议):就像对讲机,按住就说话,不确定对方收没收到,不管顺序。UDP 更快但不保证可靠,适合视频通话等场景。
我们这章主要学 TCP,因为它更适合做「聊天」这种需要可靠性的场景。
第一个小例子:echo 服务器
先别想太远,来个最简单的——echo 服务器。你发什么过去,它原封不动发回来,像回音壁一样。
import socket
# 创建一个 socket 对象
# AF_INET = 用 IPv4 地址(比如 192.168.1.1)
# SOCK_STREAM = 用 TCP 协议(保证可靠)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定到本地地址和端口
# 127.0.0.1 是「本机自己」,端口 5555 随便选(记住就行)
server.bind(('127.0.0.1', 5555))
# 开始监听,最大同时等待 5 个连接
server.listen(5)
print("服务器启动了,等人来找我聊天...")
while True:
# 等待客户端连接,返回(连接对象, 客户端地址)
conn, addr = server.accept()
print(f"有人来了,地址是 {addr}")
# 接收客户端发来的数据,最多 1024 字节
data = conn.recv(1024)
print(f"收到数据: {data}")
# 把数据原样发回去
conn.send(data)
# 关闭这个连接
conn.close()
保存成 server.py,运行它,然后另开一个终端窗口,连上去测试:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 5555))
# 发一句话给服务器
client.send(b"Hello, server!")
print("发送了: Hello, server!")
# 收到服务器回的消息
response = client.recv(1024)
print(f"收到回复: {response}")
client.close()
保存成 client.py,运行 client.py,你会在 server 窗口看到「收到数据: b'Hello, server!'」,client 窗口收到「收到回复: b'Hello, server!'」。
恭喜你,刚刚完成了一次正经的网络通信!

为什么要用 while True 循环?
因为服务器得一直开着,等下一个客户端来。就像餐厅前台,不能接待完一桌就关门。
recv(1024) 里的 1024 是什么?
这是缓冲区大小,类似快递柜的格子数。每次最多收 1024 个字节,如果数据超过这个长度,得多次接收。
什么是字节流?
网络传输中,数据是以字节流的形式一位一位传的,不是整块发的。
生活类比:就像你发一条很长的微信消息,对方可能先收到前半句,后收到后半句,不是同时到达。你得自己判断「这条消息完整了吗」。
所以 recv() 可能一次返回不完整的数据。后面实战里会教你处理这个问题。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):带多人响应的 echo 服务器
上面那个服务器只能服务一个客户端,接完一个就「卡住」等下一个。有没有办法同时服务多个人?
有!用多线程。每个客户端来,就开一个新线程去服务它,主线程继续等着接待新人。
import socket
import threading
def handle_client(conn, addr):
"""处理单个客户端的函数"""
print(f"[新连接] 来自 {addr}")
while True:
data = conn.recv(1024)
if not data: # 客户端断开了
print(f"[断开] {addr} 离开了")
break
print(f"[收到] {addr}: {data.decode()}")
conn.send(data) # echo 回去
conn.close()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 5555))
server.listen(10)
print("多用户 echo 服务器启动,端口 5555")
while True:
conn, addr = server.accept()
# 每来一个客户端,开一个新线程处理
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.start()
运行这个服务器,然后开多个客户端窗口去连接,你会发现服务器能同时回应所有人,互不干扰。
预期输出(服务端):
多用户 echo 服务器启动,端口 5555
[新连接] 来自 ('127.0.0.1', 12345)
[收到] ('127.0.0.1', 12345): b'你好'
[新连接] 来自 ('127.0.0.1', 12346)
[收到] ('127.0.0.1', 12346): b'Hello'
项目 2(15 分钟):简易聊天室
把 echo 改成「广播」——一个人说的话,所有人都能看到。
import socket
import threading
# 存储所有活跃的连接
clients = []
def broadcast(message, sender_conn):
"""把消息发给所有客户端,除了发送者"""
for conn in clients:
if conn != sender_conn:
try:
conn.send(message)
except:
conn.close()
clients.remove(conn)
def handle_client(conn, addr):
"""处理客户端消息"""
clients.append(conn)
print(f"[加入] {addr},当前在线 {len(clients)} 人")
while True:
try:
data = conn.recv(1024)
if not data:
break
msg = f"[{addr[0]}:{addr[1]}] {data.decode()}"
print(msg)
broadcast(msg.encode(), conn)
except:
break
clients.remove(conn)
conn.close()
print(f"[离开] {addr},当前在线 {len(clients)} 人")
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 5555))
server.listen(20)
print("聊天室服务器启动,端口 5555")
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.start()
客户端代码跟之前差不多,但加个循环,能一直发消息:
import socket
import threading
def receive_messages(sock):
"""接收服务器发来的广播消息"""
while True:
try:
data = sock.recv(1024)
if not data:
print("服务器断开了")
break
print("\r" + data.decode() + "\n> ", end="")
except:
break
server_ip = input("服务器 IP(直接回车用本机): ") or '127.0.0.1'
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((server_ip, 5555))
# 启动一个线程专门收消息
thread = threading.Thread(target=receive_messages, args=(sock,))
thread.start()
print("已连接,开始聊天吧(输入 exit 退出)")
while True:
msg = input("> ")
if msg.lower() == 'exit':
break
sock.send(msg.encode())
sock.close()
怎么玩:
1. 运行服务器
2. 开两个客户端窗口,分别连接
3. 在一个窗口发消息,另一个窗口立刻能看到
预期输出:
# 客户端 A
> 你好
[127.0.0.1:54321] 你好
# 客户端 B(收到)
[127.0.0.1:54320] 你好
>

项目 3(15 分钟):文件传输小工具
用 socket 传文件。把客户端改成能上传文件给服务器,服务器保存到本地。
服务器端加个指令系统,能识别「上传」「下载」等命令:
import socket
import threading
import os
def handle_client(conn, addr):
"""处理客户端请求"""
print(f"[连接] {addr}")
while True:
try:
data = conn.recv(1024)
if not data:
break
msg = data.decode().strip()
if msg.startswith("UPLOAD:"):
# 客户端要上传文件
filename = msg.split(":", 1)[1]
conn.send(b"READY") # 告诉客户端可以发了
# 接收文件内容
filesize = int.from_bytes(conn.recv(8), 'big')
conn.send(b"ACK")
received = 0
with open(f"uploaded_{filename}", "wb") as f:
while received < filesize:
chunk = conn.recv(4096)
if not chunk:
break
f.write(chunk)
received += len(chunk)
print(f"[收到文件] {filename},大小 {filesize} 字节")
conn.send(b"UPLOAD_OK")
elif msg == "LIST":
# 返回服务器上的文件列表
files = os.listdir(".")
file_list = "\n".join([f for f in files if f.startswith("uploaded_")])
conn.send(file_list.encode() or b"(空)")
else:
conn.send(b"UNKNOWN_COMMAND")
except Exception as e:
print(f"[错误] {e}")
break
conn.close()
print(f"[断开] {addr}")
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 6666))
server.listen(10)
print("文件服务器启动,端口 6666")
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.start()
客户端上传文件:
import socket
import os
def upload_file(filename):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 6666))
filesize = os.path.getsize(filename)
# 发送上传命令
sock.send(f"UPLOAD:{os.path.basename(filename)}".encode())
# 等待服务器确认
resp = sock.recv(1024)
if resp != b"READY":
print("服务器拒绝了上传请求")
sock.close()
return
# 发送文件大小
sock.send(filesize.to_bytes(8, 'big'))
# 等待服务器准备接收
sock.recv(1024)
# 发送文件内容
with open(filename, "rb") as f:
while True:
chunk = f.read(4096)
if not chunk:
break
sock.send(chunk)
# 等待上传结果
result = sock.recv(1024)
print(result.decode())
sock.close()
# 上传当前目录下的 test.txt
if os.path.exists("test.txt"):
upload_file("test.txt")
else:
# 创建一个测试文件
with open("test.txt", "w") as f:
f.write("这是测试文件内容。\n第二行。")
upload_file("test.txt")
预期输出(客户端):
文件上传成功: uploaded_test.txt,大小 29 字节
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:编码问题——中文变乱码
# ❌ 错误:bytes 和 str 混用
conn.send("你好") # TypeError: a bytes-like object is required
# ✅ 正确:编码后再发送
conn.send("你好".encode('utf-8'))
# ❌ 错误:收数据时假设一定是字符串
data = conn.recv(1024)
print(data.decode()) # 如果 data 是空数据,会报错
# ✅ 正确:先判断是否为空
data = conn.recv(1024)
if data:
print(data.decode('utf-8'))
坑 2:端口被占用
# ❌ 错误:程序异常退出后立即重启
server.bind(('127.0.0.1', 5555)) # OSError: Address already in use
# ✅ 正确:添加这行, reuse 地址
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 5555))
坑 3:粘包问题——数据「粘」在一起了
TCP 是字节流协议,不保证你发的一条消息就是收的一条。可能两条消息粘成一包,也可能一条消息分成两包收。
# ❌ 错误:假设一次 recv 就能收完整条消息
data = conn.recv(1024)
# 假设客户端发 "Hello" + "World",可能一次收到 "HelloWorld"
# ✅ 正确:自己定义消息边界(简单方案:加换行或固定长度)
# 发送时加换行,收的时候按行收
conn.send(b"Hello\n")
data = b""
while not data.endswith(b"\n"):
data += conn.recv(1) # 逐字节收,直到收到换行
坑 4:忘记关闭连接
# ❌ 错误:打开连接用完不关
conn = socket.socket(...)
conn.connect(...)
# 用完了,函数结束,conn 对象被 GC 回收,但连接没优雅关闭
# ✅ 正确:用 try-finally 确保一定关闭
conn = socket.socket(...)
try:
conn.connect(...)
conn.send(b"data")
finally:
conn.close()
坑 5:阻塞导致程序卡死
conn.recv() 会一直等数据来,程序就卡在那不动了。
# ❌ 错误:主线程里 recv,可能卡死整个程序
def handle_client(conn):
data = conn.recv(1024) # 如果客户端永远不发,主线程就卡在这
# ✅ 正确:用 settimeout 设置超时,或者用非阻塞模式
conn.settimeout(30) # 等 30 秒还没数据就抛异常
try:
data = conn.recv(1024)
except socket.timeout:
print("对方太慢了,超时了")
性能小贴士:合理设置缓冲区
# 默认的 socket 缓冲区可能很小,高并发时性能差
# 可以调大(但不是越大越好)
# 服务器端
server.listen(128) # 连接队列调大
# 收发数据时,用合适的缓冲区大小
# 传文件用 8KB-64KB 比较合适,太小要多次系统调用,太大占内存
while True:
chunk = f.read(65536) # 每次读 64KB
if not chunk:
break
conn.sendall(chunk) # sendall 会确保全部发出去
调试技巧:加日志
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def handle_client(conn, addr):
logging.info(f"新连接: {addr}")
try:
while True:
data = conn.recv(1024)
logging.debug(f"收到数据: {data}")
if not data:
break
conn.send(data)
except Exception as e:
logging.error(f"处理出错: {e}")
finally:
conn.close()
logging.info(f"连接关闭: {addr}")
✏️ 练习题 + 作业题
练习题(5 道,10 分钟)
练习 1(2 分钟):改端口
- 输入:在项目 1 的代码中,把端口从 5555 改成 8888
- 预期输出:客户端连接 127.0.0.1:8888 能正常通信
- 提示:改一个数字就行
练习 2(2 分钟):加个条件判断
- 输入:在 echo 服务器里,加个判断,如果客户端发的是 "bye",就断开连接
- 预期输出:客户端发 "bye" 后服务器主动关闭连接
- 提示:在 handle_client 的 while True 循环里加个 if 判断
练习 3(3 分钟):换个广播内容
- 输入:在聊天室项目里,把广播消息格式从 [IP:端口] 内容 改成 [在线人数] IP:端口 说: 内容
- 预期输出:客户端收到类似 [3] 127.0.0.1:54321 说: 你好 的消息
- 提示:用 len(clients) 获取当前人数
练习 4(3 分钟):处理文件上传响应
- 输入:在文件传输客户端里,打印服务器返回的 UPLOAD_OK 或错误信息
- 预期输出:上传成功后看到「文件上传成功」提示
- 提示:看 result = sock.recv(1024) 之后怎么 print
练习 5(5 分钟):分析这个报错
- 输入:运行下面代码,观察报错,然后修复
import socket
s = socket.socket()
s.connect(('127.0.0.1', 5555))
s.send("hello") # 报错了
print(s.recv(1024))
- 预期输出:不报错,能正常发送和接收
- 提示:报错信息是
TypeError: a bytes-like object is required
作业题(30 分钟 - 2 小时)
作业:做一个「多人在线待办清单服务器」
- 需求描述:做一个 TCP 服务器,多个客户端可以连接,每个客户端可以添加、查看、删除自己的待办事项。数据存在服务器内存里(不用数据库),每个客户端只能看到自己的清单。
- 功能点:
1. 客户端连接后,输入「注册 zhangsan」来注册用户名
2. 输入「添加 买牛奶」来添加待办
3. 输入「列表」查看自己的待办
4. 输入「删除 1」删除第 1 条待办
5. 输入「退出」断开连接 - 加分项:
1. 待办按添加时间排序,显示序号
2. 关机时把所有待办保存到文件,重启后自动恢复 - 验收标准:
- 两个客户端同时连接,分别添加待办,互相看不到对方的清单
- 列表、删除功能正常工作
- 代码有适当的注释
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
这一章我们学了 3 个核心点:
- Socket 是网络通信的「电话机」,用
socket()创建、bind()绑定地址、listen()监听、accept()接电话 - TCP 是可靠的面向连接的协议,保证数据不丢不乱序,适合聊天室、文件传输等场景
- 多线程让服务器能同时服务多个人,每个客户端分配一个线程处理,主线程继续接待新人
延伸学习资源:
- Python 官方 socket 文档 —— 完整的 API 参考
- 《计算机网络:自顶向下方法》—— 想深入理解网络原理可以看这本
- 《Unix 网络编程》—— 网络编程的经典大部头,有深度
互动钩子:你在实际项目里用过 socket 编程吗?做过聊天室、文件传输、还是其他好玩的东西?评论区聊聊,老粉优先回复!
📌 下章预告:学会了 socket 编程,你有没有想过——你跟服务器说的「悄悄话」,在网络上裸奔,谁都能偷看?下一章「HTTPS 与 TLS/SSL」教你怎么给通信加密,让内容只有你和服务器知道。敬请期待!

评论(0)