第6章 6.1 net 与 socket 编程

上一章我们做完了一个完整的博客系统,从数据库到前端页面全都跑通了。做完之后你有没有想过一个问题:你的浏览器是怎么跟服务器「打电话」的?打开博客、登录后台,这些数据是怎么嗖一下就过去了的?

这一章我们要揭开这个底牌——学习 socket 编程,也就是计算机和网络「通话」的基本功。学会了它,你就能写出聊天室、文件传输工具、甚至自己造一个迷你版的 QQ。


🎯 开场 3 分钟:为什么要学这个?

先问自己一个问题:你上网的时候,数据是怎么从你家电脑跑到千里之外的服务器,又跑回来的?

举个好理解的例子。你点开一篇博客文章:

  1. 你的电脑发了个「请求」给服务器:「喂,我要看这篇文章」
  2. 服务器收到,肚子里翻出文章内容
  3. 服务器把内容「塞进信封」寄回来
  4. 你的浏览器收到,拆开信封,展示给你看

这个「发请求 → 收响应」的过程,背后就是 socket 在干活

生活类比:socket 就像电话机。你不需要懂电话线怎么走、信号怎么调制,拿起电话拨号码,对方接起来,你们就能说话。socket 就是程序里的「电话机」——帮你搞定网络通信的底层破事儿,让你专心写业务逻辑。

学完这章你能
- 理解 TCP/IP 网络的基本工作原理
- 用 Python 写一个能接收和发送数据的「服务器」
- 做一个两人或多人的简易聊天室
- 知道怎么处理字节流,不会乱码


🧱 基础 25 分钟:核心概念

什么是 socket?

Socket(套接字)是计算机网络通信的基本抽象。你可以把它理解成插头和插座

  • IP 地址 = 你家地址(比如「北京市朝阳区 XX 路 1 号」)
  • 端口号 = 房间号(比如「502」)
  • Socket = 插头 + 插座,没有它电就没法通

你的电脑要跟另一台电脑聊天,首先得知道对方的 IP(找谁聊)和端口(聊什么服务)。就好比你打电话,得知道对方电话号码和分机号。

什么是 TCP?

TCP(传输控制协议)是 socket 编程最常用的「语言」。它有三大特点:

  1. 面向连接:打电话前得先拨号、对方接起来,才能说话
  2. 可靠传输:说过的话对方没听清,会要求重说,不会丢数据
  3. 有顺序:先说的话先到,不会乱序

对比一下 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!'」。

恭喜你,刚刚完成了一次正经的网络通信!

配图1 - 配图1

为什么要用 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] 你好
>

配图2 - 配图2

项目 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_clientwhile 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 个核心点:

  1. Socket 是网络通信的「电话机」,用 socket() 创建、bind() 绑定地址、listen() 监听、accept() 接电话
  2. TCP 是可靠的面向连接的协议,保证数据不丢不乱序,适合聊天室、文件传输等场景
  3. 多线程让服务器能同时服务多个人,每个客户端分配一个线程处理,主线程继续接待新人

延伸学习资源

  • Python 官方 socket 文档 —— 完整的 API 参考
  • 《计算机网络:自顶向下方法》—— 想深入理解网络原理可以看这本
  • 《Unix 网络编程》—— 网络编程的经典大部头,有深度

互动钩子:你在实际项目里用过 socket 编程吗?做过聊天室、文件传输、还是其他好玩的东西?评论区聊聊,老粉优先回复!


📌 下章预告:学会了 socket 编程,你有没有想过——你跟服务器说的「悄悄话」,在网络上裸奔,谁都能偷看?下一章「HTTPS 与 TLS/SSL」教你怎么给通信加密,让内容只有你和服务器知道。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。