第 4 篇 · 兵器谱:网络编程 · Socket 从入门到实战
行走江湖,得有趁手的兵器。
IP 是江湖坐标,端口是暗号,TCP/UDP 是传音入密的内功心法。
本篇为你打造网络编程的「屠龙刀」,客户端与服务端通信,一剑封喉!

1. 网络设备的三要素
- IP地址:网络环境中电脑的唯一标识,通过 IP 可以找到某台计算机
- 端口:一台服务器上会有 N 个应用,通过唯一的端口映射服务器中某个应用(服务)
- 协议:网络编程协议 --- TCP / UDP
1.1 域名解析
输入 www.baidu.com 的时候,会进入 DNS 解析:
- 本地 host 文件 → 本地 DNS 解析 → 远程 DNS 解析 → 获取 IP 地址 → 找到服务器
8.8.8.8 是比较靠谱的 DNS 解析器
绝大部分网站的端口是 80 端口,默认不需要写
1.2 IP 版本
- IPv4:IP 地址量约 2 的 32 次方 ≈ 43 亿,已接近耗尽
- IPv6:IP 地址量约 2 的 128 次方,几乎无穷无尽
1.3 端口可用范围
端口号的可用范围是 0 ~ 65535,作用是帮你找到在计算机上独一无二的服务。
不要去抢占知名端口号:0 ~ 1023。但也有一些不在这个范围内,比如 MySQL 的 3306。
IP 地址帮你锁定网络中的计算机设备,端口号帮你在这个设备上找到对应的服务。
2. TCP 与 UDP 的区别
TCP(传输控制协议)面向连接、可靠 ;UDP(用户数据报协议)无连接、不可靠,都属于传输层协议。
2.1 核心对比
| 对比项 | TCP | UDP |
|---|---|---|
| 连接特性 | 面向连接,三次握手建立连接,四次挥手断开连接 | 无连接,发数据前不需要建立连接,直接发包 |
| 可靠性 | 可靠传输:确认应答、超时重传、丢包重传;不丢包、不乱序 | 不可靠:不保证到达,不保证顺序,可能丢包、乱序 |
| 流量/拥塞控制 | 有流量控制(滑动窗口)、拥塞控制 | 无,只管发送,不感知网络拥堵 |
| 报文 | 面向字节流,数据是流,没有报文边界 | 面向数据包,保留报文边界,一个 UDP 包就是一条消息 |
| 头部开销 | 头部 20~60 字节,开销大 | 头部固定 8 字节,开销小 |
| 速度 | 较慢,握手、重传、确认带来额外开销 | 速度快,延迟低,开销小 |
| 适用场景 | 文件传输、HTTP/HTTPS、SSH、数据库访问,要求数据不能丢 | 直播、视频通话、游戏、DNS,允许少量丢包,追求低延迟 |
2.2 形象比喻
- TCP = 打电话:拨号(三次握手)→ 通话(可靠传输)→ 挂断(四次挥手);说话对方一定能收到,顺序不变,但是流程多
- UDP = 寄明信片:直接扔出去,不用先联系对方;明信片可能丢失、顺序乱掉,但是发得快,成本低
3. TCP 的三次握手
客户端主动发起连接,服务端被动接收,一共 3 次报文交互。
3.1 三次握手流程
- 第一次握手:客户端 → 服务端【SYN】
客户端发送SYN=1,随机初始序列号seq=x,客户端进入 SYN_SENT 状态。
含义:客户端请求建立连接。 - 第二次握手:服务端 → 客户端【SYN+ACK】
服务端收到 SYN,回复SYN=1,ACK=1;ack=x+1(确认客户端),自己随机初始序列号seq=y;服务端进入 SYN_RCVD 状态。
含义:同意连接,同时也向客户端发起连接请求。 - 第三次握手:客户端 → 服务端【ACK】
客户端收到 SYN+ACK,回复ACK=1,ack=y+1;客户端进入 ESTABLISHED(已连接) ;服务端收到这个 ACK 后,也进入 ESTABLISHED,连接正式建立,可以传数据。
3.2 为什么是三次,不是两次?
- 两次握手:服务端收到 SYN 就直接建立连接。如果客户端旧的失效 SYN 延迟到达服务器,服务器会建立无效连接,白白占用资源。
- 三次握手:双方收发能力都得到验证:客户端能发能收、服务端能发能收。第三次 ACK 可以拒绝过期的历史连接。
SYN=1 表示握手连接
客户端发 SYN=1,发 seq=x
服务端回 SYN=1,发 ack=x+1,seq=y
客户端回 ack=y+1
4. TCP 的四次挥手
任何一方都可以主动关闭,下面以客户端主动关闭举例。
4.1 四次挥手流程
- 第一次挥手:客户端 → 服务端【FIN】
客户端发送FIN=1,seq=u,客户端进入 FIN_WAIT_1 。
含义:客户端不再发送数据,但还可以接收数据。 - 第二次挥手:服务端 → 客户端【ACK】
服务端回复ACK=1,ack=u+1,seq=v;服务端进入 CLOSE_WAIT ;客户端收到 ACK 进入 FIN_WAIT_2 。
含义:收到关闭请求;此时服务端还可以继续向客户端发送剩余数据。 - 第三次挥手:服务端 → 客户端【FIN】
服务端数据全部发送完毕,发送FIN=1,ACK=1,seq=w,ack=u+1;服务端进入 LAST_ACK 。
含义:服务端也没有数据要发了,请求关闭。 - 第四次挥手:客户端 → 服务端【ACK】
客户端回复ACK=1,ack=w+1,seq=u+1;客户端进入 TIME_WAIT (等待 2MSL);服务端收到 ACK,立刻进入 CLOSED ,连接关闭。客户端等待 2MSL 之后,也进入 CLOSED。
FIN=1 表示挥手连接
客户端发 FIN=1,seq=u
服务端回 ack=u+1 --- 把剩余的数据传输完成
服务端发 FIN=1,ack=u+1,seq=w
客户端回 ACK=1,ack=w+1
4.2 为什么挥手要 4 次,握手只要 3 次?
握手时 SYN 和 ACK 可以合并成一个报文;挥手时,服务端收到 FIN,可能还有业务数据没发完,不能立刻发 FIN,只能先回复 ACK,等数据发完再发 FIN,所以拆成两次,总共 4 次。
5. Socket 套接字
5.1 Socket 套接字对象介绍
使用 socket 来进行网络通信编程工具。
python
# 引入socket模块
import socket
# 创建一个socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 地址族选用 IPv4,传输类型选用字节流
print(server_socket)
- 客户端(学名 Client):前端 / HTML / CSS / JS / Android / iOS / 小程序 / Flutter / RN
- 服务端(学名 Server):Python / Java / Go / Rust / Node.js / C#
5.2 Socket 客户端和服务端通信流程分析
服务端流程
- 创建一个 socket 对象 --- 大管家(不负责和客户端直接对话)
bind()绑定 IP 和端口号 --- 表示通过这个地址和端口能找到该服务listen(5)--- 设置最大挂起的 socket 数量,默认值为 1,最大为 128,一般设置为 5accept()--- 接收客户端发起连接,生成和客户端对话的 socket 对象,返回客户端信息
accept 是阻塞的:它不走完,下面谁也别走send()--- 发送数据recv()--- 接收数据(发送和接收没有强制顺序要求,但发送的数据一定是字节流)close()--- 释放当前和客户端对话的 socket
5.3 二进制编解码操作
python
str = "rucy,今天是个好日子"
print(str.encode("gbk").decode("gbk"))
print(str.encode("utf-8").decode("utf-8"))
- GBK 编码:一个中文占 2 个字节,英文字符占 1 个字节,支持简体 + 繁体,部分日文
- UTF-8 编码:一个中文占 3 个字节,英文字符占 1 个字节,支持全球所有字符
如果内容中不包含中文,直接在字符串前加
b即表示该字符为 bytes
5.4 Socket 套接字对象创建(服务端)
注意:socket 对象默认是阻塞模式,阻塞的方法有:
accept()(服务端专用)recv(bufsize)(客户端/服务端)connect()(客户端专用)
阻塞:当前代码未执行完毕,绝不可能执行下一行代码
python
import socket
print("1.创建socket服务器对象")
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("2.设置端口号在进程结束立刻释放")
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
print("3.绑定端口号和ip地址")
server_socket.bind(("127.0.0.1", 9999))
print("4.设置监听并设置挂起最大监听量")
server_socket.listen(5)
print("5.等待客户端连接")
client_socket, client_info = server_socket.accept()
print("6.客户端连接成功", client_info)
print("7.给客户端发送数据")
client_socket.send("我是你的服务器对象".encode("utf-8"))
print("8.接收客户端消息")
data = client_socket.recv(1024)
print(f"收到客户端发的消息:{data.decode('utf-8')}")
print("9.关闭和客户端通讯的连接")
client_socket.close()
# server_socket 一般不关
5.5 Socket 套接字对象创建(客户端)
python
import socket
print("1.创建客户端socket对象")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("2.通过ip端口连接服务器")
client_socket.connect(("127.0.0.1", 9999))
print("3.连接成功")
content = client_socket.recv(1024)
print(f"服务器给你说:{content.decode('utf-8')}")
send_content = input("跟服务说:")
client_socket.send(send_content.encode("utf-8"))
print("关闭客户端连接")
client_socket.close()
5.6 实现无限聊天版本 --- 服务端
python
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind(("127.0.0.1", 9999))
server_socket.listen(5)
# 阻塞方法
client_socket, client_info = server_socket.accept()
print(f"和客户端{client_info}连接成功")
client_socket.send("你好,我是rucy,欢迎你来到我的聊天房间".encode("utf-8"))
# accept recv
while True:
data = client_socket.recv(1024)
if not data:
break
print(f"收到客户端:{client_info}的消息:{data.decode('utf-8')}")
client_socket.close()
5.7 实现无限聊天 --- 客户端
python
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 9999))
data = client_socket.recv(1024)
print(f"服务器对你说:{data.decode('utf-8')}")
while True:
send_content = input("对服务器说(quit退出)")
if send_content == 'quit':
break
client_socket.send(send_content.encode('utf-8'))
client_socket.close()
5.8 模拟多任务实现多人版本聊天(服务端)
支持多人聊天的服务端。
python
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
server_socket.bind(("192.168.25.170", 9999))
server_socket.listen(5)
print("完成服务端初始化所有功能")
# 模拟多任务 --- 多进程 > 多线程 > 多协程
while True:
client_socket, client_info = server_socket.accept()
client_socket.send("欢迎你兄弟来连接rucy的电脑".encode("utf-8"))
data = client_socket.recv(1024)
print(f"收到客户端{client_info}消息:{data.decode('utf-8')}")
client_socket.close()
5.9 模拟多任务多人版本聊天(客户端)
python
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 8888))
data = client_socket.recv(1024)
print(f"收到服务端消息:{data.decode('utf-8')}")
content = input("跟rucy说:")
client_socket.send(content.encode("utf-8"))
client_socket.close()
6. 扩展功能
6.1 文件选择器对象
需求:当输入 img 的时候,自动弹出文件选择器,自动过滤文件,只选择图片。
python
import tkinter
from tkinter import filedialog
content = input("对服务器说(img选择图片):")
if content == 'img':
print("选择图片")
root = tkinter.Tk()
root.withdraw() # 防止弹出多余小窗口
file_path = filedialog.askopenfilename(
title="请选择图片",
filetypes=[("图片", "*.png;*.jpg;*.jpeg;*.gif")]
)
print(file_path)
root.destroy()
6.2 解析图片获取图片的字节流及详细信息
python
import os
import tkinter
from tkinter import filedialog
content = input("输入img选择图片")
if content == 'img':
root = tkinter.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="请选择图片",
filetypes=[("图片", "*.jpg *.jpeg *.png")]
)
ext = os.path.splitext(file_path)[1][1:] # 拿到图片的扩展名
with open(file_path, "rb") as f:
img_data = f.read() # 拿到图片的二进制数据
print(f"拿到了图片的扩展名{ext}, 长度大小:{len(img_data)}, 二进制流数据:{img_data}")
# TCP协议 二进制字节流 jpeg|42196|image_data
6.3 选择一张图片存储到 data 目录下
6.3.1 快速创建目录
python
import os
# 在当前目录下创建一个abc目录,如果存在则不创建
os.makedirs("./abc", exist_ok=True)
6.3.2 查询某个文件夹目录下的文件个数
python
import os
os.makedirs("./abc", exist_ok=True)
print(len(os.listdir("./"))) # 返回当前目录下所有的文件列表
6.3.3 完整需求实现
- 当用户输入 img 的时候,弹出选择图片窗口
- 选择完毕图片解析图片,得到图片的后缀、长度、二进制
- 在当前目录下创建一个 data 目录,如果存在则不创建
- 在 data 目录下写入刚刚读取到的图片,保留原来的后缀,图片的命名为 图片_1、图片_2......
python
import os
import tkinter
from tkinter import filedialog
content = input("请输入您的内容(img)")
if content == 'img':
root = tkinter.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="选择图片",
filetypes=[("图片", "*.png *.jpeg *.jpg")]
)
ext = os.path.splitext(file_path)[1][1:] # 得到后缀
with open(file_path, "rb") as f:
img_data = f.read()
size = len(img_data)
os.makedirs("./data", exist_ok=True)
flag = len(os.listdir("./data")) + 1
with open(f"./data/图片_{flag}.{ext}", "wb") as f:
f.write(img_data)
print(f"图片已经存储到对应的位置:路径:./data/图片_{flag}.{ext}")
6.4 选择图片通过 Socket(客户端)
python
import os.path
import socket
import tkinter
from tkinter import filedialog
print("1.创建客户端对象")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 9999))
print("2.客户端连接成功")
content = input("请输入您要的内容(img传图片)")
if content == 'img':
root = tkinter.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="请选择图片",
filetypes=[("图片", "*.jpg *.jpeg *.png")]
)
ext = os.path.splitext(file_path)[1][1:]
with open(file_path, "rb") as f:
img_data = f.read()
size = len(img_data)
# 不但要传字节流,还要传后缀、长度
client_socket.sendall(f"IMG|{ext}|{size}".encode("utf-8") + img_data)
# 等待服务端图片接收完成,给一个图片上传成功的消息
print(client_socket.recv(1024).decode("utf-8"))
client_socket.close()
6.5 选择图片通过 Socket(服务端)
python
import os
import socket
print("1.创建服务端socket对象")
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("2.设置端口释放")
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
print("3.绑定端口")
server_socket.bind(("127.0.0.1", 9999))
print("4.监听")
server_socket.listen(5)
print("5.接受客户端连接")
client_socket, client_info = server_socket.accept()
print("6.客户端连接成功", client_info)
# 阻塞方法: accept recv connect
# 第一次接收,得到一定是图片的元信息: IMG|jpeg|10086|x01
first_data = client_socket.recv(1024)
if first_data.startswith(b"IMG|"):
parts = first_data.split(b"|", maxsplit=3)
ext = parts[1].decode("utf-8")
size = int(parts[2])
img_data = parts[3]
# 循环接收直到收完所有图片数据
while len(img_data) < size:
img_data += client_socket.recv(1024)
os.makedirs("./data", exist_ok=True)
flag = len(os.listdir("./data")) + 1
with open(f"./data/图片_{flag}.{ext}", "wb") as f:
f.write(img_data)
client_socket.send(f"图片上传成功,图片路径:./data/图片_{flag}.{ext}".encode("utf-8"))
client_socket.close()
7. Socket 编程基础模板(必背)
7.1 服务端基础模板
python
plain
import socket
# 1. 创建 socket 对象(IPv4,TCP 字节流)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 设置端口复用(防止程序崩溃后端口被占用)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 3. 绑定 IP 和端口
server_socket.bind(("127.0.0.1", 9999))
# 4. 开启监听,设置最大挂起连接数
server_socket.listen(5)
# 5. 等待客户端连接(阻塞)
client_socket, client_info = server_socket.accept()
# 6. 接收数据(阻塞)
data = client_socket.recv(1024)
print(data.decode("utf-8"))
# 7. 发送数据
client_socket.send("消息内容".encode("utf-8"))
# 8. 关闭连接
client_socket.close()
# server_socket.close() # 服务端一般不关,如需关闭则取消注释
7.2 客户端基础模板
python
plain
import socket
# 1. 创建 socket 对象(IPv4,TCP 字节流)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务器
client_socket.connect(("127.0.0.1", 9999))
# 3. 发送数据
client_socket.send("消息内容".encode("utf-8"))
# 4. 接收数据(阻塞)
data = client_socket.recv(1024)
print(data.decode("utf-8"))
# 5. 关闭连接
client_socket.close()