diff --git a/Cpp_example/A06_WIFI/CMakeLists.txt b/Cpp_example/A06_WIFI/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..8190cf9ef465aa1d396bb993a468431833ecef09 --- /dev/null +++ b/Cpp_example/A06_WIFI/CMakeLists.txt @@ -0,0 +1,37 @@ +# CMake最低版本要求 +cmake_minimum_required(VERSION 3.10) + +project(test_lcd) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# ---------- LockzhinerVisionModule SDK 根目录 ---------- +set(LOCKZHINER_SDK_ROOT "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(THIRD_PARTY_ROOT "${LOCKZHINER_SDK_ROOT}/third_party") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${LOCKZHINER_SDK_ROOT}") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${THIRD_PARTY_ROOT}/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +add_executable(Test_WIFI Test_WIFI.cc) +target_include_directories(Test_WIFI PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS}) +target_link_libraries(Test_WIFI PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS Test_WIFI + RUNTIME DESTINATION . +) diff --git a/Cpp_example/A06_WIFI/README.md b/Cpp_example/A06_WIFI/README.md new file mode 100644 index 0000000000000000000000000000000000000000..41d26d3ece89671c1fcea1c0a66893c839b40848 --- /dev/null +++ b/Cpp_example/A06_WIFI/README.md @@ -0,0 +1,560 @@ +# WIFI 无线图传使用指南 + +## 章节说明 + +本文档详细介绍 WIFI 无线图传示例的实现原理与代码结构,包含以下章节: + +1. **Socket 基础知识** — 讲解 TCP Socket 生命周期、关键选项、字节序转换及帧协议 +2. **v1.cc 版本一讲解** — 过程式实现的完整分析 +3. **v2.cc (Test_WIFI.cc) 版本二讲解** — 面向对象重构的架构分析 +4. **v1 → v2 迭代对比** — 两个版本的架构改进对比 +5. **编译过程** — 交叉编译环境配置与编译步骤 +6. **例程运行示例** — 开发板上的运行方法 + +--- + +## 1. Socket 基础知识 + +### 1.1 TCP Socket 生命周期 + +TCP 服务器的标准工作流程分为以下六个阶段: + +``` +socket() → bind() → listen() → accept() → send/recv() → close() +``` + +| 函数 | 作用 | 关键参数 | +|------|------|----------| +| `socket()` | 创建套接字描述符 | `AF_INET`(IPv4)、`SOCK_STREAM`(TCP) | +| `bind()` | 绑定 IP 地址和端口 | `sockaddr_in` 结构体 | +| `listen()` | 开始监听连接 | 第二个参数为等待队列长度 | +| `accept()` | 接受客户端连接 | 返回新的连接套接字 | +| `send/recv()` | 数据传输 | 阻塞/非阻塞模式 | +| `close()` | 关闭套接字 | 释放资源 | + +### 1.2 关键 Socket 选项 + +代码中使用了以下重要选项来优化传输性能: + +#### SO_REUSEADDR + +```cpp +int reuse = 1; +setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); +``` + +**作用**:允许端口快速复用。服务器关闭后,端口可能处于 `TIME_WAIT` 状态,设置此选项可立即重新绑定同一端口。 + +#### TCP_NODELAY + +```cpp +int nodelay = 1; +setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); +``` + +**作用**:禁用 Nagle 算法。默认情况下,TCP 会合并小数据包以减少网络开销,但这会增加延迟。视频流需要低延迟,因此禁用此算法。 + +#### SO_SNDBUF + +```cpp +int sndbuf = 256 * 1024; +setsockopt(client_sock, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); +``` + +**作用**:设置发送缓冲区大小为 256KB。较大的缓冲区可以提高网络吞吐量,但也会增加内存占用。 + +### 1.3 字节序转换 + +网络传输使用**大端序**(Big-Endian),而不同的 CPU 架构可能使用不同的字节序。代码中使用以下函数进行转换: + +| 函数 | 转换方向 | 适用类型 | +|------|----------|----------| +| `htonl()` | 主机字节序 → 网络字节序 | 32 位整数 | +| `ntohl()` | 网络字节序 → 主机字节序 | 32 位整数 | +| `htons()` | 主机字节序 → 网络字节序 | 16 位整数 | +| `ntohs()` | 网络字节序 → 主机字节序 | 16 位整数 | + +### 1.4 长度前缀帧协议 + +视频流传输采用**长度前缀帧协议**,解决 TCP 粘包问题: + +``` +┌────────────┬─────────────────────────────┐ +│ 4 bytes │ N bytes │ +│ (长度) │ JPEG 图像数据 │ +│ (大端序) │ │ +└────────────┴─────────────────────────────┘ +``` + +**传输流程**: +1. 发送端:先发送 4 字节大端序长度,再发送图像数据 +2. 接收端:先读取 4 字节获取长度,再读取对应长度的数据 + +### 1.5 非阻塞 I/O 与 select + +代码使用 `select()` 实现非阻塞发送检查: + +```cpp +fd_set wfds; +FD_ZERO(&wfds); +FD_SET(sock, &wfds); +struct timeval tv = {0, 1000}; // 1ms 超时 +int ret = select(sock + 1, nullptr, &wfds, nullptr, &tv); +``` + +**作用**:检查发送缓冲区是否可写,避免发送线程阻塞。若缓冲区满,则丢弃当前帧,防止数据积压。 + +--- + +## 2. v1.cc 版本一讲解 + +### 2.1 代码结构概览 + +[v1.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A06_WIFI/v1.cc) 采用**过程式编程**,所有逻辑通过全局函数实现: + +``` +┌─────────────────────────────────────────────────────────┐ +│ main() │ +│ ├── VideoCapture 初始化 │ +│ ├── socket() → bind() → listen() │ +│ └── while 循环: accept() → 创建线程 → join() │ +├─────────────────────────────────────────────────────────┤ +│ send_frame(int sock, Mat& frame) │ +│ ├── imencode() 编码 JPEG │ +│ ├── htonl() 转换长度 │ +│ └── send() 发送长度 + 图像数据 │ +├─────────────────────────────────────────────────────────┤ +│ stream_sender(int sock, atomic&, cap) │ +│ ├── 帧率控制 (30FPS) │ +│ ├── cap >> frame 读取摄像头 │ +│ ├── select() 检查缓冲区 │ +│ └── send_frame() 发送帧 │ +├─────────────────────────────────────────────────────────┤ +│ client_receiver(int sock, atomic&) │ +│ ├── fcntl() 设置非阻塞模式 │ +│ └── recv() 检测客户端断开 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 核心函数详解 + +#### send_frame 函数 + +```cpp +bool send_frame(int sock, const Mat &frame, int quality = 75) { + vector buf; + vector params = {IMWRITE_JPEG_QUALITY, quality}; + if (!imencode(".jpg", frame, buf, params)) { return false; } + + uint32_t len = htonl(static_cast(buf.size())); + ssize_t sent = send(sock, &len, sizeof(len), MSG_NOSIGNAL); + if (sent != sizeof(len)) return false; + + size_t total = 0; + while (total < buf.size()) { + ssize_t n = send(sock, buf.data() + total, buf.size() - total, MSG_NOSIGNAL); + if (n <= 0) return false; + total += n; + } + return true; +} +``` + +**关键点**: +- 使用 `imencode()` 将 OpenCV Mat 编码为 JPEG +- 使用 `MSG_NOSIGNAL` 标志避免发送失败时产生 SIGPIPE 信号 +- 循环 `send()` 确保数据全部发送(TCP 可能分批发送) + +#### stream_sender 线程 + +```cpp +void stream_sender(int sock, atomic &shutdown, VideoCapture &cap) { + const double FPS = 30.0; + const double interval = 1.0 / FPS; + const int QUALITY = 75; + + using Clock = chrono::steady_clock; + auto next_time = Clock::now(); + int frame_count = 0; + auto fps_start = Clock::now(); + + while (!shutdown.load()) { + auto now = Clock::now(); + if (now < next_time) { + this_thread::sleep_for(next_time - now); + continue; + } + next_time += chrono::milliseconds(static_cast(interval * 1000)); + + Mat frame; + cap >> frame; + if (frame.empty()) { + this_thread::sleep_for(chrono::milliseconds(10)); + continue; + } + + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sock, &wfds); + struct timeval tv = {0, 1000}; + int ret = select(sock + 1, nullptr, &wfds, nullptr, &tv); + if (ret <= 0) { continue; } + + if (!send_frame(sock, frame, QUALITY)) { + shutdown.store(true); + break; + } + + ++frame_count; + auto now2 = Clock::now(); + double elapsed = chrono::duration(now2 - fps_start).count(); + if (elapsed >= 1.0) { + cout << "Stream FPS: " << frame_count / elapsed << endl; + frame_count = 0; + fps_start = now2; + } + } +} +``` + +**关键点**: +- 使用 `chrono::steady_clock` 实现精确帧率控制(30 FPS) +- 使用 `select()` 非阻塞检查发送缓冲区状态 +- 缓冲区满时丢弃帧,防止数据积压 +- 实时统计并输出 FPS + +#### client_receiver 线程 + +```cpp +void client_receiver(int sock, atomic &shutdown) { + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); + + char dummy[1024]; + while (!shutdown.load()) { + ssize_t n = recv(sock, dummy, sizeof(dummy), 0); + if (n == 0) { + cout << "Client disconnected." << endl; + break; + } else if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + this_thread::sleep_for(chrono::milliseconds(10)); + continue; + } else { + cout << "Connection error." << endl; + break; + } + } + } + shutdown.store(true); +} +``` + +**关键点**: +- 使用 `fcntl()` 设置非阻塞模式 +- 通过 `recv()` 返回值检测客户端断开(返回 0 表示正常关闭) +- 使用 `errno == EAGAIN || errno == EWOULDBLOCK` 处理非阻塞模式下的无数据情况 +- 使用 `atomic` 线程安全地通知发送线程停止 + +### 2.3 main() 函数 — 服务器主循环 + +```cpp +int main() { + const char *HOST = "0.0.0.0"; + const int PORT = 6810; + + VideoCapture cap; + if (!cap.open(0)) { cerr << "Failed to open camera" << endl; return 1; } + cap.set(CAP_PROP_FRAME_WIDTH, 640); + cap.set(CAP_PROP_FRAME_HEIGHT, 480); + + atomic shutdown(false); + + int server_sock = socket(AF_INET, SOCK_STREAM, 0); + int reuse = 1; + setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + int nodelay = 1; + setsockopt(server_sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(PORT); + inet_pton(AF_INET, HOST, &addr.sin_addr); + + bind(server_sock, (struct sockaddr*)&addr, sizeof(addr)); + listen(server_sock, 5); + + while (!shutdown.load()) { + int client_sock = accept(server_sock, nullptr, nullptr); + if (client_sock < 0) continue; + + int sndbuf = 256 * 1024; + setsockopt(client_sock, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + thread sender(stream_sender, client_sock, ref(shutdown), ref(cap)); + thread receiver(client_receiver, client_sock, ref(shutdown)); + sender.join(); + receiver.join(); + close(client_sock); + } + + cap.release(); + close(server_sock); + return 0; +} +``` + +**关键点**: +- 按顺序执行 `socket() → bind() → listen()` 完成服务器初始化 +- `while` 循环持续调用 `accept()` 接受客户端连接 +- 为每个连接创建发送线程和接收线程,使用 `join()` 等待线程结束 +- 使用 `atomic` 实现线程间的安全通信 + +--- + +## 3. v2.cc (Test_WIFI.cc) 版本二讲解 + +### 3.1 架构概览 + +[Test_WIFI.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A06_WIFI/Test_WIFI.cc) 采用**面向对象架构**,将功能分解为独立的类: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ StreamServer │ +│ ├── TcpServer ← 通用网络层:socket/bind/listen/accept │ +│ ├── CameraCapture ← 摄像头捕获层 │ +│ └── ClientHandler ← 客户端连接管理 │ +│ ├── FrameSender ← 字节流发送(与数据格式无关) │ +│ └── FramePacketizer ← 数据打包(Mat → 带长度头的字节流) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 类职责详解 + +#### TcpServer — 通用网络层 + +```cpp +class TcpServer { +public: + bool init(); // socket() → bind() → listen() + int acceptClient(); // 接受连接,返回客户端套接字 + void close(); // 关闭服务器套接字 +}; +``` + +**职责**:封装 TCP 服务器的基础操作,与业务逻辑完全解耦。可复用为其他 TCP 服务的网络层。 + +#### FrameSender — 数据发送辅助 + +```cpp +class FrameSender { +public: + bool sendPacket(const vector &packet); // 发送完整数据包 + bool isWritable(int timeout_ms = 1) const; // 检查缓冲区是否可写 +}; +``` + +**职责**:负责底层字节流发送,与数据格式无关。可复用为其他类型数据的发送。 + +#### FramePacketizer — 数据打包层 + +```cpp +class FramePacketizer { +public: + static bool packetize(const Mat &frame, vector &out, int quality = 75); +}; +``` + +**职责**:将 OpenCV Mat 编码为带长度前缀的字节流。设计为静态类,便于替换为其他数据类型的打包器。 + +#### CameraCapture — 摄像头捕获 + +```cpp +class CameraCapture { +public: + bool open(); // 打开摄像头并设置分辨率 + bool read(Mat &frame); // 读取一帧图像 + void release(); // 释放资源 +}; +``` + +**职责**:封装摄像头操作,提供统一的接口。便于替换为其他数据源(如视频文件)。 + +#### ClientHandler — 客户端处理器 + +```cpp +class ClientHandler { +public: + void start(); // 启动发送和接收线程 + void join(); // 等待线程结束 +private: + void senderLoop(); // 发送循环 + void receiverLoop(); // 接收循环(检测断开) +}; +``` + +**职责**:管理单个客户端连接的生命周期,协调发送和接收线程。 + +#### StreamServer — 流服务器 + +```cpp +class StreamServer { +public: + bool init(); // 初始化摄像头和 TCP 服务器 + void run(); // 主循环:接受连接 → 创建 Handler + void stop(); // 停止服务器 +}; +``` + +**职责**:组装所有组件,提供统一的服务器接口。 + +### 3.3 主函数 + +```cpp +int main() { + StreamServer server("0.0.0.0", 6810, 0); + if (!server.init()) { + cerr << "Init failed" << endl; + return 1; + } + server.run(); + cout << "Server stopped." << endl; + return 0; +} +``` + +**关键点**:主函数简洁明了,只需创建服务器对象、初始化并运行。 + +--- + +## 4. v1 → v2 迭代对比 + +### 4.1 架构改进 + +| 维度 | v1.cc | v2.cc (Test_WIFI.cc) | +|------|-------|----------------------| +| **代码组织** | 过程式,所有逻辑在全局函数和 main() 中 | 面向对象,职责分离到独立类 | +| **可复用性** | 低,函数与业务紧密耦合 | 高,TcpServer、FrameSender 可独立复用 | +| **可测试性** | 差,难以单元测试 | 好,每个类可独立测试 | +| **可维护性** | 差,修改一处可能影响全局 | 好,修改局部不影响其他模块 | +| **扩展性** | 差,新增功能需要修改大量代码 | 好,通过新增类或继承扩展 | + +### 4.2 关键改进点 + +#### 1. 职责分离 + +v1 中 `send_frame()` 同时负责 JPEG 编码和网络发送;v2 将其拆分为: +- `FramePacketizer`:负责 JPEG 编码和打包 +- `FrameSender`:负责网络发送 + +#### 2. 通用组件提取 + +`TcpServer` 和 `FrameSender` 是与业务无关的通用组件,可用于其他项目。 + +#### 3. 资源管理改进 + +v2 使用 RAII 原则,通过析构函数自动释放资源: +- `TcpServer` 析构时关闭套接字 +- `ClientHandler` 析构时停止线程并关闭连接 +- `StreamServer` 析构时停止服务器 + +#### 4. 静态方法设计 + +`FramePacketizer::packetize()` 设计为静态方法,便于替换为其他数据类型的打包器。 + +--- + +## 5. 编译过程 + +### 5.1 编译环境搭建 + +- 请确保你已经按照 [开发环境搭建指南](../../../../docs/introductory_tutorial/cpp_development_environment.md) 正确配置了开发环境。 +- 同时以正确连接开发板。 + +### 5.2 Cmake介绍 + +```cmake +cmake_minimum_required(VERSION 3.10) + +project(test_wifi) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +set(LOCKZHINER_SDK_ROOT "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(THIRD_PARTY_ROOT "${LOCKZHINER_SDK_ROOT}/third_party") + +set(LockzhinerVisionModule_ROOT_PATH "${LOCKZHINER_SDK_ROOT}") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +set(OpenCV_ROOT_PATH "${THIRD_PARTY_ROOT}/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +add_executable(Test_WIFI Test_WIFI.cc) +target_include_directories(Test_WIFI PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS}) +target_link_libraries(Test_WIFI PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS Test_WIFI + RUNTIME DESTINATION . +) +``` + +### 5.3 编译项目 + +使用 Docker Destop 打开 LockzhinerVisionModule 容器并执行以下命令来编译项目 + +```bash +# 进入Demo所在目录 +cd /LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A06_WIFI +# 创建编译目录 +rm -rf build && mkdir build && cd build +# 配置交叉编译工具链 +export TOOLCHAIN_ROOT_PATH="/LockzhinerVisionModuleWorkSpace/arm-rockchip830-linux-uclibcgnueabihf" +# 使用cmake配置项目 +cmake .. +# 执行编译项目 +make -j8 && make install +``` + +在执行完上述命令后,会在build目录下生成可执行文件。 + +--- + +## 6. 例程运行示例 + +### 6.1 准备工作 + +- 请确保你以按照前文要求链接好设备 + +### 6.2 运行过程 + +在凌智视觉模块中输入以下命令: + +```shell +chmod 777 Test_WIFI +./Test_WIFI +``` + +### 6.3 客户端连接 + +在同一局域网内的电脑上,使用支持 TCP 流接收的客户端工具连接到开发板的 IP 地址和端口 6810,即可接收视频流。 + +--- + +## 附录:代码文件说明 + +| 文件 | 说明 | +|------|------| +| [v1.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A06_WIFI/v1.cc) | 版本一:过程式实现 | +| [Test_WIFI.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A06_WIFI/Test_WIFI.cc) | 版本二:面向对象重构实现 | diff --git a/Cpp_example/A06_WIFI/Test_WIFI.cc b/Cpp_example/A06_WIFI/Test_WIFI.cc new file mode 100644 index 0000000000000000000000000000000000000000..eb802f7c62f00f4330690acec018b689c47eec5b --- /dev/null +++ b/Cpp_example/A06_WIFI/Test_WIFI.cc @@ -0,0 +1,304 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace cv; + +// ==================================================================== +// 1. 通用网络层:TcpServer(创建、监听、接受连接) +// ==================================================================== +class TcpServer { +public: + TcpServer(const string &host, int port) : host_(host), port_(port), sock_(-1) {} + + ~TcpServer() { close(); } + + bool init() { + sock_ = socket(AF_INET, SOCK_STREAM, 0); + if (sock_ < 0) { perror("socket"); return false; } + + int reuse = 1; + setsockopt(sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + int nodelay = 1; + setsockopt(sock_, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port_); + if (inet_pton(AF_INET, host_.c_str(), &addr.sin_addr) <= 0) { + perror("inet_pton"); close(); return false; + } + if (bind(sock_, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); close(); return false; + } + if (listen(sock_, 5) < 0) { + perror("listen"); close(); return false; + } + cout << "Server listening on " << host_ << ":" << port_ << endl; + return true; + } + + int acceptClient() { + struct sockaddr_in client_addr; + socklen_t len = sizeof(client_addr); + int client = accept(sock_, (struct sockaddr*)&client_addr, &len); + if (client >= 0) { + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &client_addr.sin_addr, ip, INET_ADDRSTRLEN); + cout << "New connection from " << ip << ":" << ntohs(client_addr.sin_port) << endl; + } + return client; + } + + void close() { if (sock_ >= 0) { ::close(sock_); sock_ = -1; } } + int getSocket() const { return sock_; } + +private: + string host_; + int port_; + int sock_; +}; + +// ==================================================================== +// 2. 数据发送辅助:FrameSender(负责发送字节流,与数据格式无关) +// ==================================================================== +class FrameSender { +public: + explicit FrameSender(int sock) : sock_(sock) {} + + bool sendPacket(const vector &packet) { + return sendAll(packet.data(), packet.size()); + } + + bool isWritable(int timeout_ms = 1) const { + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sock_, &wfds); + struct timeval tv = {timeout_ms/1000, (timeout_ms%1000)*1000}; + return select(sock_+1, nullptr, &wfds, nullptr, &tv) > 0; + } + +private: + int sock_; + bool sendAll(const void *data, size_t len) { + const char *ptr = (const char*)data; + size_t sent = 0; + while (sent < len) { + ssize_t n = send(sock_, ptr+sent, len-sent, MSG_NOSIGNAL); + if (n <= 0) return false; + sent += n; + } + return true; + } +}; + +// ==================================================================== +// 3. 数据打包层:FramePacketizer(将 Mat 编码为带长度头的字节流) +// 您可以为其他数据类型编写新的 Packetizer 替换它 +// ==================================================================== +class FramePacketizer { +public: + static bool packetize(const Mat &frame, vector &out, int quality = 75) { + vector jpeg; + vector params = {IMWRITE_JPEG_QUALITY, quality}; + if (!imencode(".jpg", frame, jpeg, params)) return false; + + uint32_t netlen = htonl((uint32_t)jpeg.size()); + out.clear(); + out.reserve(sizeof(uint32_t) + jpeg.size()); + const uint8_t* p = (const uint8_t*)&netlen; + out.insert(out.end(), p, p + sizeof(uint32_t)); + out.insert(out.end(), jpeg.begin(), jpeg.end()); + return true; + } +}; + +// ==================================================================== +// 4. 摄像头捕获(特定业务数据源) +// ==================================================================== +class CameraCapture { +public: + explicit CameraCapture(int id = 0) : device_(id), width_(640), height_(480) {} + bool open() { + cap_.open(device_); + if (!cap_.isOpened()) return false; + cap_.set(CAP_PROP_FRAME_WIDTH, width_); + cap_.set(CAP_PROP_FRAME_HEIGHT, height_); + return true; + } + bool read(Mat &frame) { cap_ >> frame; return !frame.empty(); } + void release() { cap_.release(); } +private: + int device_, width_, height_; + VideoCapture cap_; +}; + +// ==================================================================== +// 5. 客户端处理器(管理一个连接的两个线程) +// ==================================================================== +class ClientHandler { +public: + ClientHandler(int sock, CameraCapture &cam) : sock_(sock), camera_(cam), shutdown_(false) {} + + ~ClientHandler() { + shutdown_ = true; + if (sender_.joinable()) sender_.join(); + if (receiver_.joinable()) receiver_.join(); + if (sock_ >= 0) close(sock_); + } + + void start() { + sender_ = thread(&ClientHandler::senderLoop, this); + receiver_ = thread(&ClientHandler::receiverLoop, this); + } + + void join() { + if (sender_.joinable()) sender_.join(); + if (receiver_.joinable()) receiver_.join(); + } + +private: + int sock_; + CameraCapture &camera_; + atomic shutdown_; + thread sender_, receiver_; + + void senderLoop() { + const double FPS = 30.0; + auto next = chrono::steady_clock::now(); + FrameSender sender(sock_); + vector packet; + int cnt = 0; + auto fps_start = chrono::steady_clock::now(); + + while (!shutdown_) { + auto now = chrono::steady_clock::now(); + if (now < next) { this_thread::sleep_for(next - now); continue; } + next += chrono::milliseconds((int)(1000.0 / FPS)); + + Mat frame; + if (!camera_.read(frame)) { this_thread::sleep_for(chrono::milliseconds(10)); continue; } + + if (!FramePacketizer::packetize(frame, packet, 75)) continue; + + if (!sender.isWritable(1)) continue; // 缓冲区满则丢帧 + + if (!sender.sendPacket(packet)) { + cerr << "Send failed" << endl; + shutdown_ = true; + break; + } + + if (++cnt % 30 == 0) { // 简单打印 + auto now2 = chrono::steady_clock::now(); + double elapsed = chrono::duration(now2 - fps_start).count(); + if (elapsed >= 1.0) { + cout << "FPS: " << cnt/elapsed << endl; + cnt = 0; fps_start = now2; + } + } + } + } + + void receiverLoop() { + int flags = fcntl(sock_, F_GETFL, 0); + fcntl(sock_, F_SETFL, flags | O_NONBLOCK); + char buf[1024]; + while (!shutdown_) { + ssize_t n = recv(sock_, buf, sizeof(buf), 0); + if (n == 0) { cout << "Client disconnected." << endl; break; } + else if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + this_thread::sleep_for(chrono::milliseconds(10)); + continue; + } + cerr << "recv error" << endl; + break; + } + } + shutdown_ = true; + } +}; + +// ==================================================================== +// 6. 流服务器(组装所有组件) +// ==================================================================== +class StreamServer { +public: + StreamServer(const string &host, int port, int cam_id = 0) + : host_(host), port_(port), cam_id_(cam_id), running_(false), tcp_server_(host, port) {} + + ~StreamServer() { stop(); } + + bool init() { + if (!camera_.open()) { cerr << "Camera open failed" << endl; return false; } + if (!tcp_server_.init()) return false; + running_ = true; + return true; + } + + void run() { + while (running_) { + int client = tcp_server_.acceptClient(); + if (client < 0) { + if (!running_) break; + continue; + } + // 设置发送缓冲区 + int sndbuf = 256*1024; + setsockopt(client, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + int nodelay = 1; + setsockopt(client, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + ClientHandler handler(client, camera_); + handler.start(); + handler.join(); + cout << "Connection closed." << endl; + } + } + + void stop() { + running_ = false; + if (tcp_server_.getSocket() >= 0) { + shutdown(tcp_server_.getSocket(), SHUT_RDWR); + tcp_server_.close(); + } + camera_.release(); + } + +private: + string host_; + int port_; + int cam_id_; + atomic running_; + CameraCapture camera_; + TcpServer tcp_server_; +}; + +// ==================================================================== +// 主函数 +// ==================================================================== +int main() { + StreamServer server("0.0.0.0", 6810, 0); + if (!server.init()) { + cerr << "Init failed" << endl; + return 1; + } + server.run(); + cout << "Server stopped." << endl; + return 0; +} diff --git a/Cpp_example/A06_WIFI/images/1.png b/Cpp_example/A06_WIFI/images/1.png new file mode 100755 index 0000000000000000000000000000000000000000..d0cc474fb92cd284b9b7cbcc71bebc925494e78a Binary files /dev/null and b/Cpp_example/A06_WIFI/images/1.png differ diff --git a/Cpp_example/A06_WIFI/images/ST7789V.png b/Cpp_example/A06_WIFI/images/ST7789V.png new file mode 100755 index 0000000000000000000000000000000000000000..22118b34dbc14fa3fa1d5616f27c9e9ecfbe4916 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/ST7789V.png differ diff --git a/Cpp_example/A06_WIFI/images/image_1.png b/Cpp_example/A06_WIFI/images/image_1.png new file mode 100755 index 0000000000000000000000000000000000000000..2742105b9493ef90255f67da8933194ad4dcd1d7 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/image_1.png differ diff --git a/Cpp_example/A06_WIFI/images/ip_addr.jpg b/Cpp_example/A06_WIFI/images/ip_addr.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f1fe5307510e221a88e076ce3a987516481f0f37 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/ip_addr.jpg differ diff --git a/Cpp_example/A06_WIFI/images/net_model.jpg b/Cpp_example/A06_WIFI/images/net_model.jpg new file mode 100644 index 0000000000000000000000000000000000000000..052605c6760832f4d7395dd0855882f96ffa39bf Binary files /dev/null and b/Cpp_example/A06_WIFI/images/net_model.jpg differ diff --git a/Cpp_example/A06_WIFI/images/net_package.jpg b/Cpp_example/A06_WIFI/images/net_package.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4ac488d17cb28a86de39cd03d5ce97e6a0e58ee3 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/net_package.jpg differ diff --git a/Cpp_example/A06_WIFI/images/socket/echo.jpg b/Cpp_example/A06_WIFI/images/socket/echo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cb45d18757951006a8d3ae5fe88032d1d3a7eb46 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/socket/echo.jpg differ diff --git a/Cpp_example/A06_WIFI/images/socket/socket_comunciation.jpg b/Cpp_example/A06_WIFI/images/socket/socket_comunciation.jpg new file mode 100644 index 0000000000000000000000000000000000000000..93580f672eb2553205c760e5b4aaafb681282315 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/socket/socket_comunciation.jpg differ diff --git a/Cpp_example/A06_WIFI/images/tcp.jpg b/Cpp_example/A06_WIFI/images/tcp.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ef5d9749ec0c00ce9dde302a8a4b488b50214b5a Binary files /dev/null and b/Cpp_example/A06_WIFI/images/tcp.jpg differ diff --git a/Cpp_example/A06_WIFI/images/tcp_create01.jpg b/Cpp_example/A06_WIFI/images/tcp_create01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..18200ff82375818ca0562db5d1caee5e9c08b9e3 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/tcp_create01.jpg differ diff --git a/Cpp_example/A06_WIFI/images/tcp_state.jpg b/Cpp_example/A06_WIFI/images/tcp_state.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d997da998da8fcbe83070258fa35f3e58514d52d Binary files /dev/null and b/Cpp_example/A06_WIFI/images/tcp_state.jpg differ diff --git a/Cpp_example/A06_WIFI/images/tcp_stop.jpg b/Cpp_example/A06_WIFI/images/tcp_stop.jpg new file mode 100644 index 0000000000000000000000000000000000000000..350f3337b62ff14e7d73cb81c265655e1c96efc0 Binary files /dev/null and b/Cpp_example/A06_WIFI/images/tcp_stop.jpg differ diff --git a/Cpp_example/A06_WIFI/v1.cc b/Cpp_example/A06_WIFI/v1.cc new file mode 100644 index 0000000000000000000000000000000000000000..ddc3994cf568eb6e584f2142a14297f616637f55 --- /dev/null +++ b/Cpp_example/A06_WIFI/v1.cc @@ -0,0 +1,214 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace cv; + +// 发送一帧:将 Mat 编码为 JPEG,先发送 4 字节大端长度,再发送图像数据 +bool send_frame(int sock, const Mat &frame, int quality = 75) { + vector buf; + vector params = {IMWRITE_JPEG_QUALITY, quality}; + if (!imencode(".jpg", frame, buf, params)) { + return false; + } + uint32_t len = htonl(static_cast(buf.size())); + // 发送长度 + ssize_t sent = send(sock, &len, sizeof(len), MSG_NOSIGNAL); + if (sent != sizeof(len)) return false; + // 发送图像数据(循环确保全部发送) + size_t total = 0; + while (total < buf.size()) { + ssize_t n = send(sock, buf.data() + total, buf.size() - total, MSG_NOSIGNAL); + if (n <= 0) return false; + total += n; + } + return true; +} + +// 发送线程:持续读取摄像头,按帧率发送,并检测缓冲区拥塞 +void stream_sender(int sock, atomic &shutdown, VideoCapture &cap) { + const double FPS = 30.0; + const double interval = 1.0 / FPS; + const int QUALITY = 75; + + using Clock = chrono::steady_clock; + auto next_time = Clock::now(); + + int frame_count = 0; + auto fps_start = Clock::now(); + + while (!shutdown.load()) { + // 帧率控制 + auto now = Clock::now(); + if (now < next_time) { + this_thread::sleep_for(next_time - now); + continue; + } + next_time += chrono::milliseconds(static_cast(interval * 1000)); + + // 读取一帧 + Mat frame; + cap >> frame; + if (frame.empty()) { + this_thread::sleep_for(chrono::milliseconds(10)); + continue; + } + + // 非阻塞检查发送缓冲区是否可写(避免积压) + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sock, &wfds); + struct timeval tv = {0, 1000}; // 1ms 超时 + int ret = select(sock + 1, nullptr, &wfds, nullptr, &tv); + if (ret <= 0) { + // 缓冲区不可写,丢弃该帧 + continue; + } + + // 发送帧 + if (!send_frame(sock, frame, QUALITY)) { + shutdown.store(true); + break; + } + + // 统计 FPS + ++frame_count; + auto now2 = Clock::now(); + double elapsed = chrono::duration(now2 - fps_start).count(); + if (elapsed >= 1.0) { + cout << "Stream FPS: " << frame_count / elapsed << endl; + frame_count = 0; + fps_start = now2; + } + } +} + +// 接收线程:仅检测客户端断开,不处理任何命令 +void client_receiver(int sock, atomic &shutdown) { + // 设置为非阻塞模式 + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); + + char dummy[1024]; + while (!shutdown.load()) { + ssize_t n = recv(sock, dummy, sizeof(dummy), 0); + if (n == 0) { + // 客户端关闭连接 + cout << "Client disconnected." << endl; + break; + } else if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // 无数据,继续循环 + this_thread::sleep_for(chrono::milliseconds(10)); + continue; + } else { + // 其他错误 + cout << "Connection error." << endl; + break; + } + } + // 若有数据,忽略,继续循环 + } + shutdown.store(true); +} + +int main() { + const char *HOST = "0.0.0.0"; + const int PORT = 6810; + + // 打开摄像头 + VideoCapture cap; + if (!cap.open(0)) { + cerr << "Failed to open camera" << endl; + return 1; + } + cap.set(CAP_PROP_FRAME_WIDTH, 640); + cap.set(CAP_PROP_FRAME_HEIGHT, 480); + cout << "Camera ready" << endl; + + atomic shutdown(false); + + // 创建 TCP 服务器 + int server_sock = socket(AF_INET, SOCK_STREAM, 0); + if (server_sock < 0) { + perror("socket"); + return 1; + } + + int reuse = 1; + setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + // 禁用 Nagle 算法 + int nodelay = 1; + setsockopt(server_sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(PORT); + inet_pton(AF_INET, HOST, &addr.sin_addr); + + if (bind(server_sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(server_sock); + return 1; + } + + if (listen(server_sock, 5) < 0) { + perror("listen"); + close(server_sock); + return 1; + } + cout << "Server listening on " << HOST << ":" << PORT << endl; + + // 主循环:接受客户端连接 + while (!shutdown.load()) { + struct sockaddr_in client_addr; + socklen_t client_len = sizeof(client_addr); + int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len); + if (client_sock < 0) { + if (shutdown.load()) break; + perror("accept"); + continue; + } + + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &client_addr.sin_addr, ip, INET_ADDRSTRLEN); + cout << "New connection from " << ip << ":" << ntohs(client_addr.sin_port) << endl; + + // 设置客户端 socket 选项 + int sndbuf = 256 * 1024; + setsockopt(client_sock, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + socklen_t optlen = sizeof(sndbuf); + getsockopt(client_sock, SOL_SOCKET, SO_SNDBUF, &sndbuf, &optlen); + cout << "Send buffer size: " << sndbuf << " bytes" << endl; + + // 启动发送和接收线程 + thread sender(stream_sender, client_sock, ref(shutdown), ref(cap)); + thread receiver(client_receiver, client_sock, ref(shutdown)); + + // 等待线程结束 + sender.join(); + receiver.join(); + + close(client_sock); + cout << "Connection closed, waiting for next client..." << endl; + } + + cap.release(); + close(server_sock); + cout << "Server stopped." << endl; + return 0; +} diff --git a/Cpp_example/A07_WIFI_ASIO/CMakeLists.txt b/Cpp_example/A07_WIFI_ASIO/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e25a66460ef090c1f1e260001b97cd2fa9a025b7 --- /dev/null +++ b/Cpp_example/A07_WIFI_ASIO/CMakeLists.txt @@ -0,0 +1,40 @@ +# CMake最低版本要求 +cmake_minimum_required(VERSION 3.10) + +project(asio_image) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 定义项目根目录路径 +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +message("PROJECT_ROOT_PATH = " ${PROJECT_ROOT_PATH}) + +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +# ---------- LockzhinerVisionModule SDK 根目录 ---------- +set(LOCKZHINER_SDK_ROOT "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(THIRD_PARTY_ROOT "${LOCKZHINER_SDK_ROOT}/third_party") + +# 定义 LockzhinerVisionModule SDK 路径 +set(LockzhinerVisionModule_ROOT_PATH "${LOCKZHINER_SDK_ROOT}") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +# 定义 OpenCV SDK 路径 +set(OpenCV_ROOT_PATH "${THIRD_PARTY_ROOT}/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + + +set(ASIO_ROOT "${PROJECT_ROOT_PATH}/third_party/asio-1.38.2") + +add_executable(asio_image asio_image.cc) +target_include_directories(asio_image PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS} ${ASIO_ROOT}) +target_link_libraries(asio_image PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install( + TARGETS asio_image + RUNTIME DESTINATION . +) diff --git a/Cpp_example/A07_WIFI_ASIO/README.md b/Cpp_example/A07_WIFI_ASIO/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bc604002fe95e2b730b8fc618f884159e508188a --- /dev/null +++ b/Cpp_example/A07_WIFI_ASIO/README.md @@ -0,0 +1,591 @@ +# WIFI 无线图传使用指南(Asio 版本) + +## 章节说明 + +本文档详细介绍基于 Asio 异步 I/O 库实现的 WIFI 无线图传示例,包含以下章节: + +1. **Asio 简介** — 介绍 Asio 库的核心概念与优势 +2. **Asio 核心概念** — 讲解 io_context、异步操作、回调链等关键概念 +3. **echo_server.cc 讲解** — Asio 入门示例:简单的回显服务器 +4. **asio_image.cc 讲解** — 完整的视频流服务器实现分析 +5. **阻塞模型 vs Asio 异步模型对比** — 与 A06_WIFI 版本的架构对比 +6. **编译过程** — 交叉编译环境配置与编译步骤 +7. **例程运行示例** — 开发板上的运行方法 + +--- + +## 1. Asio 简介 + +### 1.1 什么是 Asio + +**Asio**(Asyncronous I/O)是一个跨平台的 C++ 异步 I/O 库,提供了优雅的异步编程接口。它采用 **Proactor 模式**,允许程序在等待 I/O 操作完成时执行其他任务,从而避免线程阻塞。 + +### 1.2 Asio 的核心优势 + +| 特性 | 说明 | +|------|------| +| **纯头文件库** | 无需链接额外库,只需包含头文件即可使用 | +| **跨平台** | 支持 Windows、Linux、macOS 等主流平台 | +| **异步模型** | 基于事件驱动,避免阻塞线程 | +| **统一接口** | 网络、串口、定时器等统一的异步接口 | +| **高性能** | 底层使用 epoll/kqueue/iocp,单线程即可处理大量并发连接 | + +### 1.3 版本信息 + +本项目使用 **Asio 1.38.2**,位于 `third_party/asio-1.38.2` 目录。 + +--- + +## 2. Asio 核心概念 + +### 2.1 io_context + +`io_context` 是 Asio 的核心调度器,负责管理异步操作的执行: + +```cpp +asio::io_context io_context; +io_context.run(); // 启动事件循环 +``` + +**关键点**: +- `run()` 方法会阻塞直到所有异步操作完成 +- 所有异步操作都需要关联到一个 `io_context` + +### 2.2 异步操作 + +Asio 的异步操作遵循统一的模式:`async_xxx()` + 回调函数: + +```cpp +socket.async_read_some( + asio::buffer(data_, max_length), + [this, self](std::error_code ec, std::size_t length) { + // 操作完成时的回调 + }); +``` + +**关键点**: +- `async_read_some` 发起异步读取,立即返回 +- 读取完成后,回调函数被调用 +- 回调函数接收 `error_code` 和操作结果 + +### 2.3 enable_shared_from_this + +用于管理异步操作中对象的生命周期: + +```cpp +class Session : public std::enable_shared_from_this { + void do_read() { + auto self = shared_from_this(); // 延长生命周期 + socket_.async_read_some(..., [this, self](...) { ... }); + } +}; +``` + +**关键点**: +- 在回调中捕获 `shared_from_this()` 确保对象在回调期间不被销毁 +- 避免悬空指针问题 + +### 2.4 steady_timer + +定时器用于控制帧率或实现延时操作: + +```cpp +asio::steady_timer timer_(socket_.get_executor()); +timer_.expires_after(std::chrono::milliseconds(10)); +timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); +}); +``` + +### 2.5 异步回调链 + +Asio 程序通常形成回调链结构: + +``` +do_accept() → async_accept() → 回调中创建 Session → Session::start() + │ + ┌──────────────────────────────────────────┴──────────────────────────────────┐ + ▼ ▼ + do_receive() → async_read_some() → 回调中再次调用 do_receive() do_send() → async_write() → 回调中再次调用 do_send() +``` + +--- + +## 3. echo_server.cc 讲解 + +### 3.1 代码结构概览 + +[echo_server.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A07_WIFI_ASIO/echo_server.cc) 是一个简单的回显服务器,演示 Asio 的基本用法: + +``` +┌─────────────────────────────────────────────────────────┐ +│ Server │ +│ └── do_accept() → async_accept() → 创建 Session │ +├─────────────────────────────────────────────────────────┤ +│ Session │ +│ ├── do_read() → async_read_some() → do_write() │ +│ └── do_write() → async_write() → do_read() │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3.2 Session 类 — 连接处理 + +```cpp +class Session : public std::enable_shared_from_this { +public: + explicit Session(tcp::socket socket) : socket_(std::move(socket)) {} + + void start() { + do_read(); + } + // ... +}; +``` + +**关键点**: +- 使用 `enable_shared_from_this` 管理生命周期 +- `socket_` 通过移动语义获取,避免拷贝开销 + +### 3.3 do_read 方法 — 异步读取 + +```cpp +void do_read() { + auto self(shared_from_this()); + socket_.async_read_some(asio::buffer(data_, max_length), + [this, self](std::error_code ec, std::size_t length) { + if (!ec) { + do_write(length); + } else { + std::cerr << "Read error: " << ec.message() << std::endl; + } + }); +} +``` + +**关键点**: +- `asio::buffer()` 创建内存区域的视图,不复制数据 +- 回调中捕获 `self` 确保 Session 在回调期间存活 +- 读取成功后调用 `do_write()`,形成回调链 + +### 3.4 do_write 方法 — 异步写入 + +```cpp +void do_write(std::size_t length) { + auto self(shared_from_this()); + asio::async_write(socket_, asio::buffer(data_, length), + [this, self](std::error_code ec, std::size_t /*length*/) { + if (!ec) { + do_read(); + } else { + std::cerr << "Write error: " << ec.message() << std::endl; + } + }); +} +``` + +**关键点**: +- `async_write()` 确保所有数据写入(与 `async_read_some` 不同) +- 写入成功后调用 `do_read()`,形成循环 + +### 3.5 Server 类 — 监听连接 + +```cpp +class Server { +public: + Server(asio::io_context& io_context, short port) + : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)) { + do_accept(); + } + +private: + void do_accept() { + acceptor_.async_accept( + [this](std::error_code ec, tcp::socket socket) { + if (!ec) { + std::make_shared(std::move(socket))->start(); + } + do_accept(); // 继续接受下一个连接 + }); + } + + tcp::acceptor acceptor_; +}; +``` + +**关键点**: +- `async_accept()` 异步接受连接 +- 使用 `std::make_shared` 创建 Session,自动管理内存 +- 回调中递归调用 `do_accept()` 持续接受连接 + +--- + +## 4. asio_image.cc 讲解 + +### 4.1 架构概览 + +[asio_image.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A07_WIFI_ASIO/asio_image.cc) 是完整的视频流服务器,架构如下: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ StreamServer │ +│ ├── CameraCapture ← 摄像头捕获层 │ +│ ├── tcp::acceptor ← 异步接受连接 │ +│ └── do_accept() → 创建 Session │ +├─────────────────────────────────────────────────────────────────┤ +│ Session │ +│ ├── tcp::socket ← 客户端套接字 │ +│ ├── steady_timer ← 帧率控制定时器 │ +│ ├── do_receive() ← 异步读取(检测断开) │ +│ └── do_send() ← 异步发送(摄像头帧) │ +├─────────────────────────────────────────────────────────────────┤ +│ FramePacketizer │ +│ └── packetize() ← 将 Mat 编码为带长度前缀的 JPEG │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.2 CameraCapture 类 + +```cpp +class CameraCapture { +public: + explicit CameraCapture(int id = 0) : device_(id), width_(640), height_(480) {} + bool open() { + cap_.open(device_); + if (!cap_.isOpened()) return false; + cap_.set(CAP_PROP_FRAME_WIDTH, width_); + cap_.set(CAP_PROP_FRAME_HEIGHT, height_); + return true; + } + bool read(Mat &frame) { cap_ >> frame; return !frame.empty(); } + void release() { cap_.release(); } +}; +``` + +### 4.3 FramePacketizer 类 + +```cpp +class FramePacketizer { +public: + static bool packetize(const Mat &frame, vector &out, int quality = 75) { + vector jpeg; + vector params = {IMWRITE_JPEG_QUALITY, quality}; + if (!imencode(".jpg", frame, jpeg, params)) return false; + + uint32_t netlen = htonl((uint32_t)jpeg.size()); + out.clear(); + out.reserve(sizeof(uint32_t) + jpeg.size()); + const uint8_t* p = (const uint8_t*)&netlen; + out.insert(out.end(), p, p + sizeof(uint32_t)); + out.insert(out.end(), jpeg.begin(), jpeg.end()); + return true; + } +}; +``` + +**协议格式**: +``` +┌────────────┬─────────────────────────────┐ +│ 4 bytes │ N bytes │ +│ (长度) │ JPEG 图像数据 │ +│ (大端序) │ │ +└────────────┴─────────────────────────────┘ +``` + +### 4.4 Session 类 — 客户端连接管理 + +#### 构造函数与 start() + +```cpp +Session(tcp::socket socket, CameraCapture &camera, int fps = 30) + : socket_(std::move(socket)), + camera_(camera), + timer_(socket_.get_executor()), + fps_(fps), + interval_(std::chrono::milliseconds(1000 / fps)), + stopped_(false) {} + +void start() { + do_receive(); // 启动接收检测断开 + do_send(); // 启动发送循环 +} +``` + +**关键点**: +- `timer_` 使用 `socket_.get_executor()` 获取执行器,确保在同一线程执行 +- `interval_` 根据 FPS 计算帧间隔 + +#### do_receive() — 检测客户端断开 + +```cpp +void do_receive() { + auto self = shared_from_this(); + socket_.async_read_some( + asio::buffer(recv_buf_, 1), + [this, self](std::error_code ec, size_t /*len*/) { + if (ec) { + std::cerr << "Client disconnected: " << ec.message() << std::endl; + stop(); + return; + } + do_receive(); + }); +} +``` + +**关键点**: +- 仅读取 1 字节,用于检测连接状态 +- 连接断开时 `ec` 不为空,调用 `stop()` 清理资源 + +#### do_send() — 视频帧发送 + +```cpp +void do_send() { + auto self = shared_from_this(); + + // 1. 读取一帧 + Mat frame; + if (!camera_.read(frame)) { + timer_.expires_after(std::chrono::milliseconds(10)); + timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); + }); + return; + } + + // 2. 打包 + if (!FramePacketizer::packetize(frame, send_buffer_, 75)) { + timer_.expires_after(std::chrono::milliseconds(10)); + timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); + }); + return; + } + + // 3. 异步发送完整包 + asio::async_write( + socket_, + asio::buffer(send_buffer_), + [this, self](std::error_code ec, size_t /*bytes_sent*/) { + if (ec) { + std::cerr << "Send error: " << ec.message() << std::endl; + stop(); + return; + } + timer_.expires_after(interval_); + timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); + }); + }); +} +``` + +**关键点**: +- 使用 `steady_timer` 控制帧率,避免忙等待 +- `async_write()` 确保完整数据包发送 +- 发送成功后等待 `interval_` 毫秒再发送下一帧 + +### 4.5 StreamServer 类 + +```cpp +class StreamServer { +public: + StreamServer(asio::io_context &io_context, short port, int cam_id = 0) + : io_context_(io_context), + acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), + camera_(cam_id) { + acceptor_.set_option(asio::socket_base::reuse_address(true)); + } + + bool init() { + if (!camera_.open()) return false; + do_accept(); + return true; + } + + void run() { + io_context_.run(); + } + // ... +}; +``` + +**关键点**: +- `set_option(asio::socket_base::reuse_address(true))` 允许端口复用 +- `io_context_.run()` 启动事件循环,阻塞直到服务器停止 + +### 4.6 主函数 + +```cpp +int main() { + try { + asio::io_context io_context; + StreamServer server(io_context, 6810, 0); + + if (!server.init()) { + std::cerr << "Server initialization failed" << std::endl; + return 1; + } + + std::cout << "Echo video server started on 0.0.0.0:6810" << std::endl; + server.run(); + } catch (std::exception &e) { + std::cerr << "Exception: " << e.what() << std::endl; + } + return 0; +} +``` + +--- + +## 5. 阻塞模型 vs Asio 异步模型对比 + +### 5.1 架构对比 + +| 维度 | A06_WIFI (阻塞模型) | A07_WIFI_ASIO (异步模型) | +|------|---------------------|-------------------------| +| **并发模型** | 每连接 2 个线程(发送 + 接收) | 单线程事件驱动,无需额外线程 | +| **线程管理** | 手动创建/join 线程,使用 atomic 通信 | 自动管理,回调链驱动 | +| **阻塞方式** | `select()` + `sleep()` 轮询 | 纯异步,无阻塞等待 | +| **资源开销** | 线程栈内存 + 上下文切换开销 | 仅一个 io_context 线程 | +| **代码复杂度** | 需要处理线程同步和生命周期 | 回调链清晰,自动管理 | + +### 5.2 关键差异 + +#### 连接管理 + +**A06_WIFI**: +```cpp +// 每连接创建两个线程 +thread sender(stream_sender, client_sock, ref(shutdown), ref(cap)); +thread receiver(client_receiver, client_sock, ref(shutdown)); +sender.join(); +receiver.join(); +``` + +**A07_WIFI_ASIO**: +```cpp +// 异步接受,无需线程 +acceptor_.async_accept([this](std::error_code ec, tcp::socket socket) { + std::make_shared(std::move(socket), camera_, 30)->start(); + do_accept(); +}); +``` + +#### 帧率控制 + +**A06_WIFI**: +```cpp +// 使用 std::this_thread::sleep_for 阻塞等待 +auto now = Clock::now(); +if (now < next_time) { + this_thread::sleep_for(next_time - now); + continue; +} +next_time += chrono::milliseconds(static_cast(interval * 1000)); +``` + +**A07_WIFI_ASIO**: +```cpp +// 使用 steady_timer 异步等待 +timer_.expires_after(interval_); +timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); +}); +``` + +--- + +## 6. 编译过程 + +### 6.1 编译环境搭建 + +- 请确保你已经按照 [开发环境搭建指南](../../../../docs/introductory_tutorial/cpp_development_environment.md) 正确配置了开发环境。 +- 同时以正确连接开发板。 + +### 6.2 CMake 介绍 + +```cmake +cmake_minimum_required(VERSION 3.10) + +project(asio_image) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(PROJECT_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +include("${PROJECT_ROOT_PATH}/toolchains/arm-rockchip830-linux-uclibcgnueabihf.toolchain.cmake") + +set(LOCKZHINER_SDK_ROOT "${PROJECT_ROOT_PATH}/third_party/lockzhiner_vision_module_sdk") +set(THIRD_PARTY_ROOT "${LOCKZHINER_SDK_ROOT}/third_party") + +set(LockzhinerVisionModule_ROOT_PATH "${LOCKZHINER_SDK_ROOT}") +set(LockzhinerVisionModule_DIR "${LockzhinerVisionModule_ROOT_PATH}/lib/cmake/lockzhiner_vision_module") +find_package(LockzhinerVisionModule REQUIRED) + +set(OpenCV_ROOT_PATH "${THIRD_PARTY_ROOT}/opencv-mobile-4.10.0-lockzhiner-vision-module") +set(OpenCV_DIR "${OpenCV_ROOT_PATH}/lib/cmake/opencv4") +find_package(OpenCV REQUIRED) +set(OPENCV_LIBRARIES "${OpenCV_LIBS}") + +set(ASIO_ROOT "${PROJECT_ROOT_PATH}/third_party/asio-1.38.2") + +add_executable(asio_image asio_image.cc) +target_include_directories(asio_image PRIVATE ${LOCKZHINER_VISION_MODULE_INCLUDE_DIRS} ${ASIO_ROOT}) +target_link_libraries(asio_image PRIVATE ${OPENCV_LIBRARIES} ${LOCKZHINER_VISION_MODULE_LIBRARIES}) + +install(TARGETS asio_image RUNTIME DESTINATION .) +``` + +**关键点**: +- Asio 是纯头文件库,只需添加 include 路径,无需链接 +- 使用 `ASIO_ROOT` 指定 Asio 头文件位置 + +### 6.3 编译项目 + +使用 Docker Destop 打开 LockzhinerVisionModule 容器并执行以下命令来编译项目 + +```bash +# 进入 Demo 所在目录 +cd /LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A07_WIFI_ASIO +# 创建编译目录 +rm -rf build && mkdir build && cd build +# 配置交叉编译工具链 +export TOOLCHAIN_ROOT_PATH="/LockzhinerVisionModuleWorkSpace/arm-rockchip830-linux-uclibcgnueabihf" +# 使用 cmake 配置项目 +cmake .. +# 执行编译项目 +make -j8 && make install +``` + +在执行完上述命令后,会在 build 目录下生成可执行文件。 + +--- + +## 7. 例程运行示例 + +### 7.1 准备工作 + +- 请确保你以按照前文要求链接好设备 + +### 7.2 运行过程 + +在凌智视觉模块中输入以下命令: + +```shell +chmod 777 asio_image +./asio_image +``` + +### 7.3 客户端连接 + +在同一局域网内的电脑上,使用支持 TCP 流接收的客户端工具连接到开发板的 IP 地址和端口 6810,即可接收视频流。 + +--- + +## 附录:代码文件说明 + +| 文件 | 说明 | +|------|------| +| [echo_server.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A07_WIFI_ASIO/echo_server.cc) | Asio 入门示例:简单回显服务器 | +| [asio_image.cc](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A07_WIFI_ASIO/asio_image.cc) | 完整视频流服务器:基于 Asio 的异步实现 | +| [CMakeLists.txt](file:///LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule/Cpp_example/A07_WIFI_ASIO/CMakeLists.txt) | 项目构建配置 | diff --git a/Cpp_example/A07_WIFI_ASIO/asio_image.cc b/Cpp_example/A07_WIFI_ASIO/asio_image.cc new file mode 100644 index 0000000000000000000000000000000000000000..1c8e85f8ed2f2b0300493f562e3f90ccc5470260 --- /dev/null +++ b/Cpp_example/A07_WIFI_ASIO/asio_image.cc @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace cv; +using asio::ip::tcp; + +// ==================================================================== +// 1. 摄像头捕获 +// ==================================================================== +class CameraCapture { +public: + explicit CameraCapture(int id = 0) : device_(id), width_(640), height_(480) {} + bool open() { + cap_.open(device_); + if (!cap_.isOpened()) return false; + cap_.set(CAP_PROP_FRAME_WIDTH, width_); + cap_.set(CAP_PROP_FRAME_HEIGHT, height_); + return true; + } + bool read(Mat &frame) { cap_ >> frame; return !frame.empty(); } + void release() { cap_.release(); } +private: + int device_, width_, height_; + VideoCapture cap_; +}; + +// ==================================================================== +// 2. 帧打包器 +// ==================================================================== +class FramePacketizer { +public: + static bool packetize(const Mat &frame, vector &out, int quality = 75) { + vector jpeg; + vector params = {IMWRITE_JPEG_QUALITY, quality}; + if (!imencode(".jpg", frame, jpeg, params)) return false; + + uint32_t netlen = htonl((uint32_t)jpeg.size()); + out.clear(); + out.reserve(sizeof(uint32_t) + jpeg.size()); + const uint8_t* p = (const uint8_t*)&netlen; + out.insert(out.end(), p, p + sizeof(uint32_t)); + out.insert(out.end(), jpeg.begin(), jpeg.end()); + return true; + } +}; + +// ==================================================================== +// 3. Asio Session +// ==================================================================== +class Session : public std::enable_shared_from_this { +public: + Session(tcp::socket socket, CameraCapture &camera, int fps = 30) + : socket_(std::move(socket)), + camera_(camera), + timer_(socket_.get_executor()), + fps_(fps), + interval_(std::chrono::milliseconds(1000 / fps)), + stopped_(false) {} + + void start() { + do_receive(); // 启动接收检测断开 + do_send(); // 启动发送循环 + } + + // 停止会话 + void stop() { + if (stopped_.exchange(true)) return; + std::error_code ec; + socket_.close(ec); + try { + timer_.cancel(); + } catch (const std::exception &e) { + std::cerr << "Timer cancel exception: " << e.what() << std::endl; + } + } + +private: + // ---------- 接收(检测客户端断开) ---------- + void do_receive() { + auto self = shared_from_this(); + socket_.async_read_some( + asio::buffer(recv_buf_, 1), + [this, self](std::error_code ec, size_t /*len*/) { + if (ec) { + // 连接断开或出错,停止会话 + std::cerr << "Client disconnected: " << ec.message() << std::endl; + stop(); + return; + } + // 收到任何数据(可扩展为控制指令),继续等待 + do_receive(); + }); + } + + // ---------- 发送(摄像头读取 + 打包 + 异步发送) ---------- + void do_send() { + auto self = shared_from_this(); + + // 1. 读取一帧 + Mat frame; + if (!camera_.read(frame)) { + // 读取失败,稍后重试 + timer_.expires_after(std::chrono::milliseconds(10)); + timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); + }); + return; + } + + // 2. 打包 + if (!FramePacketizer::packetize(frame, send_buffer_, 75)) { + // 打包失败,稍后重试 + timer_.expires_after(std::chrono::milliseconds(10)); + timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); + }); + return; + } + + // 3. 异步发送完整包 + asio::async_write( + socket_, + asio::buffer(send_buffer_), + [this, self](std::error_code ec, size_t /*bytes_sent*/) { + if (ec) { + std::cerr << "Send error: " << ec.message() << std::endl; + stop(); + return; + } + // 发送成功,等待帧间隔后再发送下一帧 + timer_.expires_after(interval_); + timer_.async_wait([this, self](std::error_code ec) { + if (!ec) do_send(); + }); + }); + } + + tcp::socket socket_; + CameraCapture &camera_; + asio::steady_timer timer_; + int fps_; + std::chrono::milliseconds interval_; + std::atomic stopped_; + + // 发送缓冲区(重用以减少分配) + std::vector send_buffer_; + // 接收缓冲区(仅1字节,用于检测断开) + char recv_buf_[1]; +}; + +// ==================================================================== +// 4. Asio 流服务器 +// ==================================================================== +class StreamServer { +public: + StreamServer(asio::io_context &io_context, short port, int cam_id = 0) + : io_context_(io_context), + acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), + camera_(cam_id) { + // 设置 SO_REUSEADDR + acceptor_.set_option(asio::socket_base::reuse_address(true)); + } + + bool init() { + if (!camera_.open()) { + std::cerr << "Camera open failed" << std::endl; + return false; + } + do_accept(); + return true; + } + + void run() { + io_context_.run(); + } + + void stop() { + std::error_code ec; + acceptor_.close(ec); + camera_.release(); + io_context_.stop(); + } + +private: + void do_accept() { + acceptor_.async_accept( + [this](std::error_code ec, tcp::socket socket) { + if (!ec) { + std::cout << "New connection from " + << socket.remote_endpoint().address().to_string() + << ":" << socket.remote_endpoint().port() << std::endl; + // 设置 TCP_NODELAY + socket.set_option(tcp::no_delay(true)); + // 调整发送缓冲区大小(可选) + asio::socket_base::send_buffer_size sndbuf(256 * 1024); + socket.set_option(sndbuf); + + // 创建 Session 并启动 + std::make_shared(std::move(socket), camera_, 30)->start(); + } else { + std::cerr << "Accept error: " << ec.message() << std::endl; + } + // 继续接受下一个连接 + do_accept(); + }); + } + + asio::io_context &io_context_; + tcp::acceptor acceptor_; + CameraCapture camera_; +}; + +// ==================================================================== +// 5. 主函数 +// ==================================================================== +int main() { + try { + asio::io_context io_context; + StreamServer server(io_context, 6810, 0); // 监听 0.0.0.0:6810,摄像头 ID 0 + + if (!server.init()) { + std::cerr << "Server initialization failed" << std::endl; + return 1; + } + + std::cout << "Echo video server started on 0.0.0.0:6810" << std::endl; + server.run(); + } catch (std::exception &e) { + std::cerr << "Exception: " << e.what() << std::endl; + } + + return 0; +} diff --git a/Cpp_example/A07_WIFI_ASIO/echo_server.cc b/Cpp_example/A07_WIFI_ASIO/echo_server.cc new file mode 100644 index 0000000000000000000000000000000000000000..56def8b60d6d3281ed447f04589f9554d2fd1c81 --- /dev/null +++ b/Cpp_example/A07_WIFI_ASIO/echo_server.cc @@ -0,0 +1,91 @@ +#include +#include +#include +#include + +using asio::ip::tcp; + +class Session : public std::enable_shared_from_this { +public: + explicit Session(tcp::socket socket) : socket_(std::move(socket)) {} + + void start() { + do_read(); + } + +private: + void do_read() { + auto self(shared_from_this()); + socket_.async_read_some(asio::buffer(data_, max_length), + [this, self](std::error_code ec, std::size_t length) { + if (!ec) { + std::cout << "Received " << length << " bytes from " + << socket_.remote_endpoint().address().to_string() + << ":" << socket_.remote_endpoint().port() << std::endl; + do_write(length); + } else { + std::cerr << "Read error: " << ec.message() << std::endl; + } + }); + } + + void do_write(std::size_t length) { + auto self(shared_from_this()); + asio::async_write(socket_, asio::buffer(data_, length), + [this, self](std::error_code ec, std::size_t /*length*/) { + if (!ec) { + do_read(); + } else { + std::cerr << "Write error: " << ec.message() << std::endl; + } + }); + } + + tcp::socket socket_; + enum { max_length = 1024 }; + char data_[max_length]; +}; + +class Server { +public: + Server(asio::io_context& io_context, short port) + : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)) { + do_accept(); + } + +private: + void do_accept() { + acceptor_.async_accept( + [this](std::error_code ec, tcp::socket socket) { + if (!ec) { + std::cout << "New connection from " + << socket.remote_endpoint().address().to_string() + << ":" << socket.remote_endpoint().port() << std::endl; + std::make_shared(std::move(socket))->start(); + } else { + std::cerr << "Accept error: " << ec.message() << std::endl; + } + do_accept(); + }); + } + + tcp::acceptor acceptor_; +}; + +int main(int argc, char* argv[]) { + try { + if (argc != 2) { + std::cerr << "Usage: echo_server \n"; + return 1; + } + + asio::io_context io_context; + Server s(io_context, std::atoi(argv[1])); + std::cout << "Echo server started on port " << argv[1] << std::endl; + io_context.run(); + } catch (std::exception& e) { + std::cerr << "Exception: " << e.what() << std::endl; + } + + return 0; +} \ No newline at end of file diff --git a/Cpp_example/A07_WIFI_ASIO/images/1.png b/Cpp_example/A07_WIFI_ASIO/images/1.png new file mode 100755 index 0000000000000000000000000000000000000000..d0cc474fb92cd284b9b7cbcc71bebc925494e78a Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/1.png differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/ST7789V.png b/Cpp_example/A07_WIFI_ASIO/images/ST7789V.png new file mode 100755 index 0000000000000000000000000000000000000000..22118b34dbc14fa3fa1d5616f27c9e9ecfbe4916 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/ST7789V.png differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/image_1.png b/Cpp_example/A07_WIFI_ASIO/images/image_1.png new file mode 100755 index 0000000000000000000000000000000000000000..2742105b9493ef90255f67da8933194ad4dcd1d7 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/image_1.png differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/ip_addr.jpg b/Cpp_example/A07_WIFI_ASIO/images/ip_addr.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f1fe5307510e221a88e076ce3a987516481f0f37 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/ip_addr.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/net_model.jpg b/Cpp_example/A07_WIFI_ASIO/images/net_model.jpg new file mode 100644 index 0000000000000000000000000000000000000000..052605c6760832f4d7395dd0855882f96ffa39bf Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/net_model.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/net_package.jpg b/Cpp_example/A07_WIFI_ASIO/images/net_package.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4ac488d17cb28a86de39cd03d5ce97e6a0e58ee3 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/net_package.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/socket/echo.jpg b/Cpp_example/A07_WIFI_ASIO/images/socket/echo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cb45d18757951006a8d3ae5fe88032d1d3a7eb46 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/socket/echo.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/socket/socket_comunciation.jpg b/Cpp_example/A07_WIFI_ASIO/images/socket/socket_comunciation.jpg new file mode 100644 index 0000000000000000000000000000000000000000..93580f672eb2553205c760e5b4aaafb681282315 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/socket/socket_comunciation.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/tcp.jpg b/Cpp_example/A07_WIFI_ASIO/images/tcp.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ef5d9749ec0c00ce9dde302a8a4b488b50214b5a Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/tcp.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/tcp_create01.jpg b/Cpp_example/A07_WIFI_ASIO/images/tcp_create01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..18200ff82375818ca0562db5d1caee5e9c08b9e3 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/tcp_create01.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/tcp_state.jpg b/Cpp_example/A07_WIFI_ASIO/images/tcp_state.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d997da998da8fcbe83070258fa35f3e58514d52d Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/tcp_state.jpg differ diff --git a/Cpp_example/A07_WIFI_ASIO/images/tcp_stop.jpg b/Cpp_example/A07_WIFI_ASIO/images/tcp_stop.jpg new file mode 100644 index 0000000000000000000000000000000000000000..350f3337b62ff14e7d73cb81c265655e1c96efc0 Binary files /dev/null and b/Cpp_example/A07_WIFI_ASIO/images/tcp_stop.jpg differ diff --git a/README.md b/README.md index e405f8c745c2bf04ce7809ce6dcd8ebb68a135ba..42726cfa147536a266138cbedd5a6ed6162aa3fc 100644 --- a/README.md +++ b/README.md @@ -126,9 +126,19 @@ Lockzhiner Vision Module 拥有丰富的 IO 接口,其接口图片如下图所 ### 🔌 无线图传案例 -无线图传,全称“无线图像传输”,是指利用无线电波将视频图像信号从发送端(如摄像机、视觉模块)实时传输到接收端(如监视器、手机、PC),并在接收端还原显示的技术。简单来说,就是给摄像头“剪掉”视频线,换成无形的电磁波。为降低使用成本,在本模块中,我们提供了基于 socket 实现的无线图传功能示例。 - -* [凌智视觉模块无线图传案例](./example/periphery/wifi) +无线图传,全称“无线图像传输”,是指利用无线电波将视频图像信号从发送端(如摄像机、视觉模块)实时传输到接收端(如监视器、手机、PC),并在接收端还原显示的技术。简单来说,就是给摄像头“剪掉”视频线,换成无形的电磁波。为降低使用成本,在本模块中,我们提供了基于 socket 实现的无线图传功能示例。同时,提供了部分网络编程相关的内容,以及相关的示例代码,包含 c++ 和 python。 + +❗ **注意事项**: +1. 无线图传功能需要在视觉模块上烧录 wifi 版本的镜像,否则 wifi 模块无法使用。 +2. 无线图传功能需要在视觉模块上配置 wifi 连接,否则无法使用。 +3. [wifi下载](https://pan.baidu.com/s/1qMcI_k3_-R2fAgaFyRF2GA#list/path=%2Fsharelink3524191093-750367099385248%2Frk1106_r10_20250516%2Fwifi&parentPath=%2Fsharelink3524191093-750367099385248),镜像烧录方式和无wifi配置方式相同。 + +* [WIFI 模块使用指南](./example/periphery/wifi/README.md) +* [网络编程基础知识](./docs/网络编程/1-网络编程基础知识.md) +* [socket 网络编程](./docs/网络编程/2-SOCKET%20网络编程.md) +* [凌智视觉模块无线图传案例 python 实现](./example/periphery/wifi) +* [凌智视觉模块无线图传案例 c++ socket 实现](./Cpp_example/A06_WIFI/README.md) +* [凌智视觉模块无线图传案例 c++ asio 实现](./Cpp_example/A07_WIFI_ASIO/README.md) ## 🧠 深度学习视觉部署教程 diff --git a/docs/introductory_tutorial/cpp_development_environment.md b/docs/introductory_tutorial/cpp_development_environment.md index 12e099b2c33fee95c3b1369d601cb1b018569fa8..6c024909048af57b9b075d28cf10d8d8e9ae8ae1 100644 --- a/docs/introductory_tutorial/cpp_development_environment.md +++ b/docs/introductory_tutorial/cpp_development_environment.md @@ -261,6 +261,10 @@ unzip -qo jsoncpp.zip -d third_party rm -rf opus-v1.5.2.zip wget https://gitee.com/LockzhinerAI/opus/releases/download/v0.1/opus-v1.5.2.zip unzip -qo opus-v1.5.2.zip -d third_party + +rm -rf asio-1.38.2.zip +wget https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v1.0.0/asio-1.38.2.zip +unzip -qo asio-1.38.2.zip -d third_party ``` ### 6.5 下载/更新 LockzhinerVisionModule SDK @@ -271,8 +275,8 @@ unzip -qo opus-v1.5.2.zip -d third_party cd /LockzhinerVisionModuleWorkSpace/LockzhinerVisionModule rm -rf third_party/lockzhiner_vision_module_sdk rm -rf lockzhiner_vision_module_sdk.zip -# 其中v0.0.6为SDK的版本号,最新版本号请以发行版更新为准。 -wget https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v0.0.6/lockzhiner_vision_module_sdk.zip +# 其中v1.0.0为SDK的版本号,最新版本号请以发行版更新为准。 +wget https://gitee.com/LockzhinerAI/LockzhinerVisionModule/releases/download/v1.0.0/lockzhiner_vision_module_sdk.zip unzip -qo lockzhiner_vision_module_sdk.zip -d third_party ``` diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/1-\347\275\221\347\273\234\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/1-\347\275\221\347\273\234\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" new file mode 100644 index 0000000000000000000000000000000000000000..3a2025bcf09bec547bffbe26eee658880598a975 --- /dev/null +++ "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/1-\347\275\221\347\273\234\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -0,0 +1,268 @@ +# SOCKET 编程基础 + +互联网通信的本质是数字通信,任何数字通信都离不开通信协议的制定,通信设备只有按照约定的、统一的方式去封装和解析信息,才能实现通信。互联网通信所要遵守的众多协议,被统称为TCP/IP协议族。 +## 1 网络相关知识简介 +### 1.1 网络协议分层模型 + +TCP/IP是一个庞大的协议族,它是众多网络协议的集合,包括:ARP、IP、ICMP、UDP、TCP、DNS、DHCP、HTTP、FTP、MQTT等等。这些协议按照功能,可以被划分为几个不同的层次,如图所示。比如HTTP、FTP、MQTT,它们隶属于应用层,。那么TCP/IP为什么需要分层,分层又是依靠什么依据呢? +![](./images/net_model.jpg) + +TCP/IP 协议栈中不同协议所完成的功能是不一样的, 某些协议的实现要依赖于其它协议,依据这种依赖关系,可以将协议栈分层。在图中,低层协议为相邻的上层协议提供服务,是上层协议得以实现的基础。 +- 物理层(PHY)规定了传输信号所需要的物理电平、介质特征; +- 链路层(MAC)规定了数据帧能被网卡接收的条件,最常见的方式是利用网卡的MAC地址,发送方会在欲发送的数据帧的首部加上接收方网卡的MAC地址信息,接收方只有监听到属于自己的MAC地址信息后,才会去接收并处理该数据。 +- 每台网络设备都应该有自己的网络地址,网络层规定了主机的网络地址该如何定义,以及如何在网络地址和MAC地址之间进行映射,即ARP协议;网络层实现了数据包在主机之间的传递,而一台主机内部可能运行着多个网络程序。 +- 传输层可以区分数据包是属于哪一个应用程序的,可以说传输层实现了数据包端到端的传递。另外,数据包在传输过程中可能会出现丢包、乱序和重复的现象,网络层并没有提供应对这些错误的机制,而传输层可以解决这些问题,如TCP协议; +- 应用层以下的工作完成了数据的传递工作,应用层则决定了你如何应用和处理这些数据,之所以会有许多的应用层协议,是因为互联网中传递的数据种类很多、差异很大、应用场景十分多样。 + +### 1.2 协议层报文间的封装与拆封 +在这里,我们简单解释一下在数据的发送和接收过程中,TCP/IP都做了哪些事儿。 + +当用户发送数据时,将数据向下交给传输层,这是处于应用层的操作,而应用层也有相关的协议,对用户的数据进行封装,比如MQTT、HTTP等协议,最后应用层通过调用传输层的接口来将数据递交到传输层中。传输层会在数据前面加上传输层首部(此处以TCP协议为例,图中的传输层首部为TCP首部,也可以是UDP首部),然后向下交给网络层。同样地,网络层会在数据前面加上网络层首部(IP首部),然后将数据向下交给链路层,链路层会对数据进行最后一次封装,即在数据前面加上链路层首部(此处使用以太网接口为例),然后将数据交给网卡。最后,网卡将数据转换成物理链路上的电平信号,数据就这样被发送到了网络中。数据的发送过程,可以概括为TCP/IP的各层协议对数据进行封装的过程,如图所示: + +![](./images/net_package.jpg) + +当设备的网卡接收到某个数据包后,它会将其放置在网卡的接收缓存中,并告知TCP/IP内核。然后TCP/IP内核就开始工作了,它会将数据包从接收缓存中取出,并逐层解析数据包中的协议首部信息,并最终将数据交给某个应用程序。数据的接收过程与发送过程正好相反,可以概括为TCP/IP的各层协议对数据进行解析的过程。 + +## 2 IP 协议相关知识简介 + +IP协议(Internet Protocol),又称之为网际协议,IP协议处于IP层工作,它是整个TCP/IP协议栈的核心协议,上层协议都要依赖IP协议提供的服务,IP协议负责将数据报从源主机发送到目标主机,通过IP地址作为唯一识别码,简单来说,不同主机之间的IP地址是不一样的,在发送数据报的过程中,IP协议还可能对数据报进行分片处理,同时在接收数据报的时候还可能需要对分片的数据报进行重装等等。 + +IP协议是一种无连接的不可靠数据报交付协议,协议本身不提供任何的错误检查与恢复机制。 + +### 2.1 IP 地址简述 + +在TCP/IP设计之初,为了标识互联网中的每台主机的身份,设计人员为每个接入网络中的主机都分配一个IP地址(Internet Protocol Address),是一个32位的整数地址,只有合法的IP地址才能接入互联网中并且与其他主机进行通信,IP地址是软件地址,不是硬件地址,硬件MAC地址是存储在网卡中的,应用于本地网络中寻找目标主机。而IP地址能让一个网络中的主机能够与另一个网络中的主机进行通信,无需理会这些主机之间的MAC地址。 + +在学习IP地址之前,我们需要简述一下主机与路由器连入网络的通信的方法:一台主机通常只有一条链路连接到网络,一般只有一个网卡;当主机中想发送一个数据报时,它就在该链路上发送,一个网卡对应一个MAC地址与IP地址,当然,主机也可能有多个网卡,这样子主机就有多个MAC地址与IP地址。 + +每个IP地址长度为32比特(4字节),因此总共有2^32个可能的IP地址,大约有40亿个IP地址能被使用的。这些地址一般按所谓点分十进制记法(dotted-decimal notation)书写,即地址中的每个字节用它的十进制形式书写,各字节间以点分隔开。例如, IP地址为192.168.0.122,192是该地址第一个8比特的十进制等价数,168是该地址第二个8比特的十进制等价数,依次类推。 + +### 2.2 IP 地址编址 + +在全球的互联网中,每个主机都要唯一的一个IP地址作为身份识别,那么这么多IP地址是怎么样分配的呢?这不可能是随意自由选择IP地址的,实际上每个主机的IP地址的一部分都由其所在的子网决定的,所以又出现了IP地址分类编址的概念,网络的类型决定了IP 地址将如何划分成网络部分和节点部分,在分类编址中,设计者把所有的IP地址划分为5大类,分别为A、B、C、D、E五类,每一类地址都觉定了其中IP地址的一部分组成,具体见: + +![](./images/ip_addr.jpg) + +A 类网络地址的第一个字节的第一位必须为0 ,因此, A 类网络地址第一个字节的取值范围为0~127(注意:但0 和127 不是有效的A 类网络地址号),A类地址总共有128个网络号,其中3个网络号用作特殊用途,因此可以在互联网上使用的还有125个,而每一个网络号后面的3字节主机号表示能用于A类网络地址不同网络号的的主机数量(多达2^24-2 = 16777214个),所以总的来说A类网络支持的主机数量为125*16777214=2097151750个,大约占据了40亿IP地址的半壁江山,这些IP地址通常被分配给世界上超大型的机构使用,但是通常没有任何一个机构能使用那么大量的IP地址,所以A类地址的很多IP地址都会被白白浪费掉。 + +B 类网络地址的第一个字节的第一位必须为1 ,且第二位必须为0,因此, B 类网络地址第一个字节的取值范围为128~191,前2字节剩余的14bit表示网络号,多达16384个网络号,其中16个网络号被保留使用,因此可以分配给企业使用的网络号有16368个,每一个网络号中可以拥有2^16-2 =65534个主机,总的来说B类网络支持的主机个数为16368*65534=1072660512,大约占据了所有IP地址的四分之一。 + +C类网络地址的第一个字节的第一位必须为1,第二位必须为1,第三位必须为0,因此C类网络第一个字节的取值范围为192~223,C类地址的前三个字节用于表示网络号(剩下的21bit),所以拥有2097152个网络号,但是其中的256个网络号被保留出来,因此C类地址的可用网络号为2096896个,每个网络号下的主机个数为254,因此C类网络支持的主机数量为532611584,大约占据了所有IP地址的八分之一。 + +D类IP地址的第一个字节前四位为1110,那么它第一个字节的取值范围为224~239,约占据了所有IP地址的十六分之一,它是一个专门保留的地址,它并不指向特定的网络,目前这一类地址被用于多播。 + +E类IP地址的第一个字节前四位必须为1111,那么它第一个字节的取值范围为240~255,约占据了所有IP地址的十六分之一,是为将来使用而保留的,其中32bit全为1的IP地址(255.255.255.255)用作广播地址。 + +|类别|第一字节|第一字节取值范围|网络号个数|主机号个数|适用范围| +|--|--|--|--|--|--| +|A|0XXX XXXX|0~127|125|16777214|大型网络| +|B|10XX XXXX|128~191|16368|65534|中型网络| +|C|110X XXXX|192~223|2097152|254|小型网络| +|D|1110 XXXX|224~239|-|-|多播| +|E|1111 XXXX|240~255|-|-|保留| + +### 2.3 特殊 IP 地址 +除了上述的网络地址外,还有一些特殊用途的地址,这些地址是不允许分配给任何一个网络的主机使用的,下面简单介绍几个常见的特殊地址。 +#### 2.3.1 受限广播地址 +广播通信是一对所有的通信方式,受限广播地址用于定义整个互联网,如果设备想使IP数据报被整个网络所接收,就发送这个目的地址全为1的广播包,但这样会给整个互联网带来灾难性的负担,所以在任何情况下,路由器都会禁止转发目的地址为255.255.255.255的广播数据包,因此这样的数据包仅会出现在本地网络中(局域网),255.255.255.255这个地址指本网段内的所有主机, 相当于“房子里面的人都听着”通知所有主机。 + +注意:此处不要与以太网的广播地址(255-255-255-255-255-255)混淆了。 + +#### 2.3.2 直接广播地址 +受限广播地址是网络号与主机号都为1的地址,但是直接广播地址是主机号全为1而得到的地址,广播地址代表本网络内的所有主机,使用该地址可以向网络内的所有主机发送数据,比如一个IP地址是192.168.0.181,这是C类地址,所以它的主机号只有一个字节,那么对主机号全取1得到一个广播地址192.168.0.255,向这个地址发送数据就能让同一网络下的所有主机接收到。 +A、B、C三类地址的广播地址结构如下: +- A类地址的广播地址为:XXX.255.255.255(XXX为A类地址的第一个字节取值范围)。 +- B类地址的广播地址为:XXX. XXX.255.255(XXX为B类地址的前两个字节取值范围)。 +- C类地址的广播地址为:XXX. XXX. XXX.255(XXX为C类地址的前三个字节取值范围)。 + +注意:这个地址在IP数据报中只能作为目的地址。另外,直接广播地址使一个网段中可分配给设备的地址数减少了1个。 + +#### 2.3.3 多播地址 +多播地址用在一对多的通信中,即一个发送者,多个接收者,不论接受者员数量的多少,发送者只发送一次数据包。多播地址属于分类编址中的D类地址, D类地址只能用作目的地址,而不能作为主机中的源地址。 + +#### 2.3.4 环回地址 +127网段的所有地址都称为环回地址,主要用来测试网络协议是否工作正常的作用。比如在电脑中使用ping 命令去ping 127.1.1.1就可以测试本地TCP/IP协议是否正常。用通俗的话表示,就是“我自己”,不能以127网段中的IP地址作为主机地址,因此A类地址又少了一个可用网络号。 + +#### 2.3.5 本网络本主机 +IP地址32bit全为0的地址(0.0.0.0)表示的是本网络本主机,这个IP地址在IP数据报中只能用作源IP地址,这发生在当设备启动时但又不知道自己的IP地址情况下。在使用DHCP分配IP地址的网络环境中,这样的地址是很常见的,主机为了获得一个可用的IP地址,就给DHCP服务器发送IP数据报,并用这样的地址(0.0.0.0)作为源地址,目的地址为255.255.255.255(因为主机这时还不知道DHCP服务器的IP地址),然后DHCP服务器就会知道这个主机暂时没有IP地址,那么就会分配一个IP给这个主机。 + +## 3 UDP 协议 +UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是一种无连接、不可靠的协议,它只是简单地实现从一端主机到另一端主机的数据传输功能,这些数据通过IP层发送,在网络中传输,到达目标主机的顺序是无法预知的,因此需要应用程序对这些数据进行排序处理,这就带来了很大的不方便,此外,UDP协议更没有流量控制、拥塞控制等功能,在发送的一端,UDP只是把上层应用的数据封装到UDP报文中,在差错检测方面,仅仅是对数据进行了简单的校验,然后将其封装到IP数据报中发送出去。而在接收端,无论是否收到数据,它都不会产生一个应答发送给源主机,并且如果接收到数据发送校验错误,那么接收端就会丢弃该UDP报文,也不会告诉源主机,这样子传输的数据是无法保障其准确性的,如果想要其准确性,那么就需要应用程序来保障了。 UDP协议的特点: + +- 无连接、不可靠。 +- 尽可能提供交付数据服务,出现差错直接丢弃,无反馈。 +- 面向报文,发送方的UDP拿到上层数据直接添加个UDP首部,然后进行校验后就递交给IP层,而接收的一方在接收到UDP报文后简单进行校验,然后直接去除数据递交给上层应用。 +- 支持一对一,一对多,多对一,多对多的交互通信。 +- 速度快,UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快,即使在网络拥塞的时候UDP也不会降低发送的数据。 + +UDP虽然有很多缺点,但是也不排除其能用于很多场合,因为在如今的网络环境下,UDP协议传输出现错误的概率是很小的,并且它的实时性是非常好,常用于实时视频的传输,比如直播、网络电话等,因为即使是出现了数据丢失的情况,导致视频卡帧,这也不是什么大不了的事情,所以,UDP协议还是会被应用与对传输速度有要求,并且可以容忍出现差错的数据传输中。 + +## 4 TCP 协议 +### 4.1 TCP 协议简介 +TCP与UDP一样,都是传输层的协议,但是提供的服务却大不相同,UDP为上层应用提供的是一种不可靠的,无连接的服务,而TCP则提供一种面向连接、可靠的字节流传输服务,TCP让两个主机建立连接的关系,应用数据以数据流的形式进行传输,这与UDP协议是不一样: + +- UDP运载的数据是以报文的形式,各个报文在网络中互不相干传输,UDP每收到一个报文就递交给上层应用,因此如果对于大量数据来说,应用层的重装是非常麻烦的,因为UDP报文在网络中到达目标主机的顺序是不一样的; +- 而TCP采用数据流的形式传输,先后发出的数据在网络中虽然也是互不相干的传输,但是这些数据本身携带的信息却是紧密联系的,TCP协议会给每个传输的字节进行编号,当然啦,两个主机方向上的数据编号是彼此独立的,在传输的过程中,发送方把数据的起始编号与长度放在TCP报文中,在接收方将所有数据按照编号组装起来,然后返回一个确认,当所有数据接收完成后才将数据递交到应用层中。 + +### 4.2 TCP 特性 +#### 4.2.1 连接机制 + +TCP是一个面向连接的协议,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一个连接,否则将无法发送数据,一个TCP连接必须有双方IP地址与端口号,就像打电话一样,必须知道双方的电话号码才会打电话,关于具体的连接我们在后文讲解。 +#### 4.2.2 确认与重传机制 +一个完整的TCP传输必须有数据的交互,接收方在接收到数据之后必须正面进行确认,向发送方报告接收的结果,而发送方在发送数据之后必须等待接收方的确认,同时发送的时候会启动一个定时器,在指定超时时间内没收到确认,发送方就会认为发送失败,然后进行重发操作,这就是重传报文。 +TCP提供可靠的运输层,但它依赖的是IP层的服务,IP数据报的传输是无连接、不可靠的,因此它要通过确认来知道接收方确实已经收到数据了。但数据和确认都有可能会丢失,因此TCP通过在发送时设置一个超时机制(定时器)来解决这种问题,如果当超时时间到达的时候还没有收到对方的确认,它就重传该数据。 + +#### 4.2.3 缓冲机制 +在发送方想要发送数据的时候,由于应用程序的数据大小、类型都是不可预估的,而TCP协议提供了缓冲机制来处理这些数据,如在数据量很小的时候,TCP会将数据存储在一个缓冲空间中,等到数据量足够大的时候在进行发送数据,这样子能提供传输的效率并且减少网络中的通信量,而且在数据发送出去的时候并不会立即删除数据,还是让数据保存在缓冲区中,因为发送出去的数据不一定能被接收方正确接收,它需要等待到接收方的确认再将数据删除。同样的,在接收方也需要有同样的缓冲机制,因为在网络中传输的数据报到达的时间是不一样的,而且TCP协议还需要把这些数据报组装成完整的数据,然后再递交到应用层中。 + +#### 4.2.4 全双工通信 +在TCP连接建立后,那么两个主机就是对等的,任何一个主机都可以向另一个主机发送数据,数据是双向流通的,所以TCP协议是一个全双工的协议,这种机制为TCP协议传输数据带来很大的方便,一般来说,TCP协议的确认是通过捎带的方式来实现,即接收方把确认信息放到反向传来的是数据报文中,不必单独为确认信息申请一个报文,捎带机制减少了网络中的通信流量。由于双方主机是对等的存在,那么任意一方都可以断开连接,此时这个方向上的数据流就断开了,但是另一个 方向上的数据仍是连通的状态,这种情况就称之为半双工。 + +#### 4.2.5 流量控制 +在前面讲过,一条TCP连接每一侧的主机都设置了缓冲区域。当该接收方收到数据后,它就将数据放入接收缓冲区,当确认这段数据是正常的时候,就会向发送方返回一个确认。并且向相关的应用层递交该数据,但不一定是数据刚一到达就立即递交。事实上,接收方应用也许正忙于其他任务,甚至要过很长时间后才会去处理这些数据。这样子如果接收方处理这些数据时相对缓慢,而发送方发送得太多、太快,就会很容易地使接收方的接收缓冲区发生溢出。 +因此TCP提供了流量控制服务(flow-control service)以消除发送方使接收方缓冲区溢出的可能性。流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配,TCP通过让发送方维护一个称为接收窗口(receive window)的变量来提供流量控制,是的,你没看错,是接收窗口(rwnd),它用于给发送方一个指示:接收方还能接收多少数据,接收方会将此窗口值放在 TCP 报文的首部中的窗口字段,然后传递给发送方,这个窗口的大小是在发送数据的时候动态调整的。 + +那可能有人问,这个窗口既然是动态调整的,那有没有可能是0,这样子发送方不就是没法继续发送数据到接收方了?为了解决这个问题,TCP协议的规范中有些要求,当接收方主机的接收窗口为0时,发送方继续发送只有一个字节的报文段,这些报文段将被接收方接收,直到缓存清空,并在确认报文中包含一个非0的接收窗口值。 + +流量控制是双方通信之间的控制信息,这是很有必要的,比如两个新能不对等的主机,建立了TCP协议连接,但是其中一个主机一直发送数据,但是接收的主机来不及处理,这样子的处理就不是最佳的,因此,TCP协议中使用滑动窗口的流量控制方法,它允许接收方根据自身的处理能力来确定能接收数据的多少,因此会告诉发送方可以发送多少数据过来,即窗口的大小,而发送方尽可能将数据都多发到对方那里,所以发送方会根据这个窗口的大小发送对应的数据 ,通俗地来说就是接收方告诉发送方“我还有能力处理那么多的数据,你就发那么多数据给我就行了,不要发多了,否则我处理不了”。 + +#### 4.2.6 差错控制 +除了确认与重传之外,TCP协议也会采用校验和的方式来检验数据的有效性,主机在接收数据的时候,会将重复的报文丢弃,将乱序的报文重组,发现某段报文丢失了会请求发送方进行重发,因此在TCP往上层协议递交的数据是顺序的、无差错的完整数据。 + +#### 4.2.7 拥塞控制 +什么是拥塞?当数据从一个大的管道(如一个快速局域网)向一个较小的管道(如一个较慢的广域网)发送时便会发生拥塞。当多个输入流到达一个路由器,而路由器的输出流小于这些输入流的总和时也会发生拥塞,这种是网络状况的原因。 如果一个主机还是以很大的流量给另一个主机发送数据,但是其中间的路由器通道很小,无法承受这样大的数据流量的时候,就会导致拥塞的发生,这样子就导致了接收方无法在超时时间内完成接收(接收方此时完全有能力处理大量数据),而发送方又进行重传,这样子就导致了链路上的更加拥塞,延迟发送方必须实现一直自适应的机制,在网络中拥塞的情况下调整自身的发送速度,这种形式对发送方的控制被称为拥塞控制(congestion control),与前面我们说的流量控制是非常相似的,而且TCP协议采取的措施也非常相似,均是限制发送方的发送速度。 + +## 5 端口号的概念 +TCP协议的连接是包括上层应用间的连接,简单来说,TCP连接是两个不同主机的应用连接,而传输层与上层协议是通过端口号进行识别的,如IP协议中以IP地址作为识别一样,端口号的取值范围是0~65535,这些端口标识着上层应用的不同线程,一个主机内可能只有一个IP地址,但是可能有多个端口号,每个端口号表示不同的应用线程。一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址只能识别一台主机而非主机提供的服务,这些服务就是主机上的应用线程,因此是通过“IP地址+端口号”来区分主机不同的线程。 + +常见的TCP协议端口号有21、53、80等等,更多端口描述具体见表格,其中80端口号是我们日常生活中最常见的一个端口号,它也是HTTP服务器默认开放的端口。 + +|端口号|协议|说明| +|---|---|---| +|20|FTP|文件传输协议,使得客户端可以与服务器之间传输文件。| +|21|SFTP|安全文件传输协议,基于SSH协议,提供加密传输。| +|25|SMTP|简单邮件传输协议,用于发送电子邮件。| +|69|TFTP|简单文件传输协议,用于在无连接的网络中传输文件。| +|80|HTTP|超文本传输协议,用于在Web浏览器和Web服务器之间传输数据。| +|110|POP3|邮局协议版本3,用于从服务器接收电子邮件。| + +## 6 TCP 报文段 +TCP报文段由 首部 + 数据区域 组成,TCP报文段的首部我们称之为TCP首部,其首部内容很丰富,各个字段都有不一样的含义,如果不计算选项字段,一般来说TCP首部只有20个字节,具体见图: + +![](./images/tcp.jpg) + +每个TCP报文段都包含源主机和目标主机的端口号,用于寻找发送端和接收端应用线程,这两个值加上IP首部中的源IP地址和目标IP地址就能确定唯一一个TCP连接。 + +序号字段用来标识从TCP发送端向TCP接收端发送的数据字节流,它的值表示在这个报文段中的第一个数据字节所处位置码,根据接收到的数据区域长度,就能计算出报文最后一个数据所处的序号,因为TCP协议会对发送或者接收的数据进行编号(按字节的形式),那么使用序号对每个字节进行计数,就能很轻易管理这些数据。序号是32 bit的无符号整数。 + +当建立一个新的连接时,TCP报文段首部的 SYN标志变1,序号字段包含由这个主机随机选择的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为 ISN+1,因为SYN标志会占用一个序号,在这里我们只需要了解一下即可,后面会讲解的。 + +既然TCP协议给每个传输的字节都了编号,那么确认序号就包含接收端所期望收到的下一个序号,因此,确认序号应当是上次已成功收到数据的最后一个字节序号加 1。当然,只有ACK标志为 1时确认序号字段才有效,TCP为应用层提供全双工服务,这意味数据能在两个方向上独立地进行传输,因此确认序号通常会与反向数据(即接收端传输给发送端的数据)封装在同一个报文中(即捎带),所以连接的每一端都必须保持每个方向上的传输数据序号准确性。 + +首部长度字段占据4bit空间,它指出了TCP报文段首部长度,以字节为单位,最大能记录15*4=60字节的首部长度,因此,TCP报文段首部最大长度为60字节。在字段后接下来有6bit空间是保留未用的。 + +此外还有6bit空间,是TCP报文段首部的标志字段,用于标志一些信息: +- URG:首部中的紧急指针字段标志,如果是1表示紧急指针字段有效。 +- ACK:首部中的确认序号字段标志,如果是1表示确认序号字段有效。 +- PSH:该字段置一表示接收方应该尽快将这个报文段交给应用层。 +- RST:重新建立TCP连接。 +- SYN:用同步序号发起连接。 +- FIN:中止连接。 + +TCP的流量控制由连接的每一端通过声明的窗口大小来提供,窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的数据序号,发送方根据窗口大小调整发送数据,以实现流量控制。窗口大小是一个占据16 bit空间的字段,因而窗口最大为 65535字节,当接收方告诉发送方一个大小为0的窗口时,将完全阻止发送方的数据发送。 +检验和覆盖了整个的 TCP报文段:TCP首部和TCP数据区域,由发送端计算和填写,并由接收端进行验证。 + +只有当URG标志置1时紧急指针才有效,紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。简单来说,本TCP报文段的紧急数据在报文段数据区域中,从序号字段开始,偏移紧急指针的值结束。 + +选项字段我们暂时就无需理会了。 + +## 7 TCP 建立连接与终止连接 +TCP是一个面向连接的协议,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接,俗称“握手”,可能在学习网络之前,大家或多或少都听过“三次握手”、“四次挥手”这两个词汇,那么“三次握手”、“四次挥手”是怎么样的呢?本小节将详细讨论一个 TCP连接是如何建立的以及通信结束后是如何终止的。 +### 7.1 “三次握手”建立连接 +首先建立连接的过程是由客户端发起,而服务器无时无刻都在等待着客户端的连接,其示意图具体见图,TCP连接一般来说会经历以下过程: + +![](./images/tcp_create01.jpg) + +第一步:客户端的TCP首先向服务器端的TCP发送一个特殊的TCP报文段。该报文段中不包含应用层数据,但是在报文段的首部中的SYN标志位会被置为1。因此,这个特殊报文段被称为SYN报文段(我们暂且称之为握手请求报文)。另外,客户会随机地选择一个初始序号(ISN,假设为A),并将此序号放置于该SYN报文段的序号字段中;但SYN报文段中的ACK标志位0,此时它的确认序号段是无效的。该报文段会被封装在一个IP数据报中,然后发送给目标服务器。 + +第二步:一旦服务器收到了客户端发出的SYN报文段,知道客户端要请求握手了,服务器便会从SYN报文段中提取对应的信息,为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段(握手应答报文)。这个报文段同样也不包含任何应用层数据,但是,在报文段的首部却包含3个重要的信息: +- SYN与ACK标志都被置为1。 +- 将TCP报文段首部的确认序号字段设置为A+1(这个A(ISN)是从握手请求报文中得到)。 +- 服务器随机选择自己的初始序号(ISN,注意此ISN是服务器端的ISN,假设为B),并将其放置到TCP报文段首部的序号字段中。 + +第三步:当客户端收到服务器的握手应答报文后,会将ACK标志置位,此时客户端的TCP报文段的 ACK标志被设置为1,而对于SYN标志,因为连接已经建立了,所以该标志会被置为0,同时客户端也要给该TCP连接分配缓存和变量,并且客户端还需要返回一个应答报文段,这个报文对服务器的应答报文段作出应答,将TCP报文段首部的确认序号字段设置为B+1,同时也会告知服务器的窗口大小。(补充提示:在三次握手的第三个阶段可以在报文段数据区域中携带客户到服务器的数据。) + +在完成握手后,客户端与服务器就建立了连接,同时双方都得到了彼此的窗口大小,序列号等信息,在传输TCP报文段的时候,每个TCP报文段首部的SYN标志都会被置0,因为它只用于发起连接,同步序号。 + + +### 7.2 “四次挥手”终止连接 +建立一个连接需要三次握手,而终止一个连接要经过 四次挥手(有一些书上也会称为“四次握手”),这由 TCP的特性造成的,因为 TCP连接是全双工连接的服务,因此每个方向上的连接必须单独关闭。当一端完成它的数据发送任务后就能发送一个 FIN报文段(可以称之为终止连接请求,其实就是FIN标志位被设置为1)来终止这个方向上的连接。另一端收到一个 FIN报文段,它必须通知应用层对方几经终止了那个方向的连接,发送FIN报文段通常是应用层进行关闭的结果。 +“四次挥手”终止连接具体过程如下: + +![](./images/tcp_stop.jpg) + +第一步:客户端发出一个FIN报文段主动进行关闭连接,此时报文段的FIN标志位为1,假设序号为C,一般来说ACK标志也会被置一,但确认序号字段是无效的。 + +第二步:当服务器收到这个 FIN报文段,它发回一个ACK报文段(此报文段是终止连接应答),确认序号为收到的序号加 1(C+1),和SYN一样,一个FIN将占用一个序号,此时断开客户端->服务器的方向连接。 + +第三步:服务器会向应用程序请求关闭与这个客户端的连接,接着服务器就会发送一个FIN报文段(这个报文段是服务器向客户端发出,请求终止连接),此时假设序号为D,ACK标志虽然也为1,但是确认序号字段是无效的。 + +第四步:客户端返回一个ACK报文段来确认终止连接的请求,ACK标志置一,并将确认序号设置为收到序号加1(D+1),此时断开服务器->客户端的方向连接。 + +## 8 TCP 连接状态 + +TCP协议根据连接时接收到报文的不同类型,采取相应动作也不同,还要处理各个状态的关系,如当收到握手报文时候、超时的时候、用户主动关闭的时候等都需要不一样的状态去采取不一样的处理。 + +TCP协议的状态如下: + +- LISTENING状态:提供某种服务,侦听远方TCP端口的连接请求,当提供的服务没有被连接时,处于LISTENING状态,端口是开放的,等待被连接。 +- SYN_SENT (客户端状态):客户端调用connect()函数,将会发送一个SYN请求建立一个连接,在发送连接请求后等待匹配的连接请求,此时状态为SYN_SENT. +- SYN_RECEIVED (服务端状态):在收到和发送一个连接请求后,等待对方对连接请求的确认,当服务器收到客户端发送的同步信号时,将标志位ACK和SYN置1发送给客户端,此时服务器端处于SYN_RCVD状态,如果连接成功了就变为ESTABLISHED,正常情况下SYN_RCVD状态非常短暂。 +- ESTABLISHED状态:这个状态是处于稳定连接状态,建立连接的TCP协议两端的主机都是处于这个状态,它们相互知道彼此的窗口大小、序列号、最大报文段等信息。 +- FIN_WAIT_1与FIN_WAIT_2状态:处于这个状态一般都是单向请求终止连接,然后主机等待对方的回应,而如果对方产生应答,则主机状态转移为FIN_WAIT_2,此时{主机->对方}方向上的TCP连接就断开,但是{对方->主机}方向上的连接还是存在的。此处有一个注意的地方:如果主机处于FIN_WAIT_2状态,说明主机已经发出了FIN报文段,并且对方也已对它进行确认,除非主机是在实行半关闭状态,否则将等待对方主机的应用层处理关闭连接,因为对方已经意识到它已收到FIN报文段,它需要主机发一个 FIN 来关闭{对方->主机}方向上的连接。只有当另一端的进程完成这个关闭,主机这端才会从FIN_WAIT_2状态进入TIME_WAIT状态。否则这意味着主机这端可能永远保持这个FIN_WAIT_2状态,另一端的主机也将处于 CLOSE_WAIT状态,并一直保持这个状态直到应用层决定进行关闭。 +- CLOSE-WAIT状态:等待从本地用户发来的连接中断请求 ,被动关闭端TCP接到FIN后,就发出ACK以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序),并进入CLOSE_WAIT. +- TIME_WAIT状态:TIME_WAIT状态也称为 2MSL等待状态。每个具体TCP连接的实现必须选择一个TCP报文段最大生存时间MSL(Maximum Segment Lifetime),如IP数据报中的TTL字段,表示报文在网络中生存的时间,它是任何报文段被丢弃前在网络内的最长时间,这个时间是有限的,为什么需要等待呢?我们知道IP数据报是不可靠的,而TCP报文段是封装在IP数据报中,TCP协议必须保证发出的ACK报文段是正确被对方接收, 因此处于该状态的主机必须在这个状态停留最长时间为2倍的MSL,以防最后这个ACK丢失,因为TCP协议必须保证数据能准确送达目的地。 + +TCP协议在建立连接与终止连接的时候所有的状态转移处理状态图: + +![](./images/tcp_state.jpg) + +注: +- 虚线:表示服务器的状态转移。 +- 实线:表示客户端的状态转移。 +- 图中所有“关闭”、“打开”都是应用程序主动处理。 +- 图中所有的“超时”都是内核超时处理。 + +三次握手过程: + +- 图中(7):服务器的应用程序主动使服务器进入监听状态,等待客户端的连接请求。 +- 图中(1):首先客户端的应用程序会主动发起连接,发送SNY报文段给服务器,在发送之后就进入SYN_SENT状态等待服务器的SNY ACK报文段进行确认,如果在指定超时时间内服务器不进行应答确认,那么客户端将关闭连接。 + +- 图中(8):处于监听状态的服务器收到客户端的连接请求(SNY报文段),那么服务器就返回一个SNY ACK报文段应答客户端的响应,并且服务器进入SYN_RCVD状态。 + +图中(1):如果客户端收到了服务器的SNY ACK报文段,那么就进入ESTABLISHED稳定连接状态,并向服务器发送一个ACK报文段。 + +图中(9):同时,服务器收到来自客户端的ACK报文段,表示连接成功,进入ESTABLISHED稳定连接状态,这正是我们建立连接的三次握手过程。 + +四次挥手过程: + +图中(3):一般来说,都是客户端主动发送一个FIN报文段来终止连接,此时客户端从ESTABLISHED稳定连接状态转移为FIN_WAIT_1状态,并且等待来自服务器的应答确认。 + +图中(10):服务器收到FIN报文段,知道客户端请求终止连接,那么将返回一个ACK报文段到客户端确认终止连接,并且服务器状态由稳定状态转移为CLOSE_WAIT等待终止连接状态。 + +图中(4):客户端收到确认报文段后,进入FIN_WAIT_2状态,等待来自服务器的主动请求终止连接,此时 {客户端->服务器} 方向上的连接已经断开。 + +图中(11):一般来说,当客户端终止了连接之后,服务器也会终止 {客户端->服务器} 方向上的连接,因此服务器的原因程序会主动关闭该方向上的连接,发送一个FIN报文段给客户端。 + +图中(5):处于FIN_WAIT_2的客户端收到FIN报文段后,发送一个ACK报文段给服务器。 + +图中(12):服务器收到ACK报文段,就直接关闭,此时 {客户端->服务器} 方向上的连接已经终止,进入CLOSED状态。 + +图中(6):客户端还会等待2MSL,以防ACK报文段没被服务器收到,这就是四次挥手的全部过程。 注意:对于图中(13)(14)(15)的这些状态都是一些比较特殊的状态,我们暂时就不讲解了,总的来说都是一样的。 + + + + +## 9 参考资料 + +[嵌入式Linux基础与应用开发实战指南——基于i.MX6ULL开发板](https://doc.embedfire.com/linux/imx6/linux_base/zh/latest/system_programing/tcp_ip/tcp_ip.html) diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/2-SOCKET \347\275\221\347\273\234\347\274\226\347\250\213.md" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/2-SOCKET \347\275\221\347\273\234\347\274\226\347\250\213.md" new file mode 100644 index 0000000000000000000000000000000000000000..3b754d5905d618b8010f2207de05ecce818a36d9 --- /dev/null +++ "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/2-SOCKET \347\275\221\347\273\234\347\274\226\347\250\213.md" @@ -0,0 +1,1838 @@ +# SOCKET 网络编程 + +> 本章节主要介绍 SOCKET 网络编程的基础知识,包括SOCKET的创建、连接、发送、接收、关闭等操作。如需了解网络编程基础知识,建议阅读[网络编程基础知识](网络编程基础知识.md)章节。 + +socket 通讯过程图解 + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant Server as 服务端 + + rect rgb(240, 240, 240) + Note over Server: 服务端初始化 + Server->>Server: Socket() 创建套接字 + Server->>Server: Bind() 绑定监听端口 + Server->>Server: Listen() 设置监听队列 + end + + loop 循环等待客户端连接 + Server->>Server: Accept() 阻塞等待 + end + + Client->>Client: Socket() 创建套接字 + Client->>Server: Connect() 请求连接 + Note over Client,Server: 连接建立 + + Client->>Server: Send() 发送数据 + Server->>Client: Receive() 接收数据 + Server->>Client: Send() 发送响应数据 + Client->>Server: Receive() 接收响应数据 + + Client->>Client: Close() 关闭连接 + Server->>Server: Close() 关闭连接 + + Note over Client,Server: 任何步骤均可捕捉异常(try-catch) +``` +## 1. echo 服务端和客户端代码示例 + +为了降低代码复杂度,仅展示基本的 socket 通讯过程,不包含业务逻辑。在讲解中,我们将使用 Python 语言作为示例讲解代码,同时也会提供 C++ 代码示例。 + +在本章中,我们会实现一个简易的 echo 服务端和客户端,用于测试 socket 通讯过程,同时快速熟悉 socket 通讯的基本流程。 + +### 1.1 服务端代码示例 + +将视觉模组作为服务端。 + +```python +import socket +import threading + +def handle_client(conn, addr): + """处理单个客户端连接:接收数据并回显""" + print(f"新连接来自 {addr}") + try: + while True: + data = conn.recv(1024) + print(data.decode('utf-8')) + if not data: # 客户端关闭连接 + break + conn.sendall(data) # 回显数据 + except ConnectionResetError: + print(f"客户端 {addr} 异常断开") + finally: + conn.close() + print(f"连接 {addr} 已关闭") + +def main(): + HOST = '0.0.0.0' + PORT = 6810 + + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # 端口复用 + server_sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) + server_sock.bind((HOST, PORT)) + server_sock.listen(5) + print(f"服务器监听 {HOST}:{PORT}") + + try: + while True: + conn, addr = server_sock.accept() + # 为每个客户端创建一个线程,避免阻塞主循环 + thread = threading.Thread(target=handle_client, args=(conn, addr)) + thread.daemon = True # 主线程退出时自动结束子线程(可选) + thread.start() + except KeyboardInterrupt: + print("\n服务器正在关闭...") + finally: + server_sock.close() + +if __name__ == '__main__': + main() +``` + +### 1.2 客户端代码示例 + +将 PC 作为客户端。 + +```python +import socket + +def main(): + HOST = '192.168.1.62' # 服务器地址(如运行在本地,否则改为服务器IP) + PORT = 6810 + + try: + # 创建 TCP 套接字并连接 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_sock: + client_sock.connect((HOST, PORT)) + print(f"已连接到服务器 {HOST}:{PORT},输入消息,输入 'exit' 退出") + + while True: + # 从标准输入获取消息 + msg = input("> ") + if msg.strip().lower() == 'exit': + print("退出客户端") + break + + # 发送数据 + client_sock.sendall(msg.encode('utf-8')) + + # 接收回显数据(最多1024字节) + data = client_sock.recv(1024) + if not data: # 服务器关闭连接 + print("服务器已关闭连接") + break + + print(f"服务器回显: {data.decode('utf-8')}") + + except ConnectionRefusedError: + print("连接失败:请确保服务器已启动且端口 6810 可访问") + except ConnectionResetError: + print("服务器异常断开连接") + except KeyboardInterrupt: + print("\n客户端被用户中断") + except Exception as e: + print(f"发生错误: {e}") + +if __name__ == '__main__': + main() +``` + +启动服务端代码后,启动客户端代码,测试 echo 通讯过程, 并在服务端代码中查看客户端发送的数据。下图展示了通讯过程,黑色部分为服务端,白色部分为客户端。 + +![](./images/socket/echo.jpg) + +## 2. 核心 API 讲解 + +在完成 Echo 服务端和客户端代码后,接下来详细讲解 Socket 相关 API。本教程仅讲解核心 API 和常用套接字选项,更详细的内容请自行查阅相关文档。 + +### API 概览 + +| 函数 | 作用 | 参数要点 | +| :--- | :--- | :--- | +| `socket()` | 创建套接字 | `socket.AF_INET`:IPv4 地址族,`socket.SOCK_STREAM`:TCP 协议 | +| `bind()` | 将套接字绑定到本地地址端口 | 服务端必须调用,客户端无需调用 | +| `listen()` | 服务端调用,监听客户端连接 | 指定最大连接数 | +| `accept()` | 服务端调用,接受客户端连接,从监听队列中获取一个连接请求,返回新的套接字对象和客户端地址信息 | 阻塞或非阻塞模式 | +| `connect()` | 客户端调用,连接到服务器 | 指定服务器地址和端口 | +| `send()` / `sendall()` | 发送数据 | `data`:要发送的字节序列 | +| `recv()` | 接收数据 | `buf`:接收缓冲区大小 | +| `close()` | 关闭套接字 | 无 | +| `setsockopt()` | 设置套接字选项 | `level`:选项级别,`optname`:选项名称,`optval`:选项值 | + +--- + +### 2.1 `socket()` 函数 + +```python +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) +``` + +#### 协议簇(domain)参数分类表 + +| **协议簇** | **通信范围** | **地址格式** | **特点** | **典型应用场景** | +| :--- | :--- | :--- | :--- | :--- | +| **`AF_UNIX` / `AF_LOCAL`** | 同一台主机(进程间) | 文件系统路径(如 `/tmp/sock`) | ① 不经过网络协议栈,速度极快
② 基于文件权限控制访问 | Nginx 与 PHP-FPM 通信、本地数据库连接、容器间高效 IPC | +| **`AF_INET`** | 跨主机(IPv4 网络) | 32位 IPv4 地址 + 端口 | ① 互联网主流协议
② 使用 `struct sockaddr_in` | 绝大多数公网/局域网服务(Web、视频流、游戏) | +| **`AF_INET6`** | 跨主机(IPv6 网络) | 128位 IPv6 地址 + 端口 | ① 解决 IPv4 地址枯竭
② 使用 `struct sockaddr_in6` | 下一代互联网应用、双栈服务器 | +| **`AF_PACKET`** | 数据链路层(直接操作网卡) | 物理接口 + MAC 地址 | ① 绕过 TCP/IP 协议栈
② 可捕获/发送原始帧 | 抓包工具(tcpdump)、网络嗅探、自定义二层协议 | +| **`AF_NETLINK`** | 内核与用户空间 | 特定协议号(如 `NETLINK_ROUTE`) | ① 用于内核与用户进程通信
② 异步消息机制 | 路由表管理、网络事件监控 | + +#### 套接字类型(type)参数分类表 + +| 类型常量 | 特性 | 对应协议(常用) | 典型场景 | +| :--- | :--- | :--- | :--- | +| **`SOCK_STREAM`** | ① 面向连接(需先建立连接)
② 可靠传输(确认重传)
③ 保序(先发先到)
④ 无边界(流式,存在“粘包”问题) | **TCP**(传输控制协议) | 网页浏览(HTTP)、文件传输(FTP)、视频流服务器、远程登录(SSH) | +| **`SOCK_DGRAM`** | ① 无连接(直接发,无需握手)
② 不可靠(可能丢包、乱序)
③ 有边界(保留消息边界,一次 `recvfrom` 收一个完整报文) | **UDP**(用户数据报协议) | 实时音视频通话(RTP)、DNS域名解析、网络游戏位置同步、广播 | +| **`SOCK_RAW`** | ① 提供原始网络协议访问
② 可操作底层 IP 头或数据链路帧
③ 需要 **root** 权限 | IP、ICMP(如 Ping 命令) | 网络嗅探器(Wireshark)、实现自定义协议、路由协议(OSPF)、Ping 程序 | +| **`SOCK_SEQPACKET`** | ① 面向连接(类似 STREAM)
② 可靠且保序(类似 STREAM)
③ 保留消息边界(类似 DGRAM)
④ 数据按“报文”为单位收发 | **SCTP**(流控制传输协议) | 电信信令(SS7)、要求高可靠且必须明确边界的应用(相对小众) | +| **`SOCK_RDM`** | ① 可靠传输(不丢包)
② 不保序(可能乱序到达)
③ 有消息边界 | 极少使用(部分 Unix 的 RDS 协议) | 特定高性能计算集群(几乎已被淘汰) | + +#### 协议(protocol)参数 + +`socket()` 函数的第三个参数是 `protocol`(协议),用于进一步明确在给定的协议簇(domain)和套接字类型(type)下,具体使用哪一种传输协议。 + +大多数情况下,`domain` + `type` 的组合通常只对应一种默认协议,直接填 `0`(表示让系统自动选择)。但在某些特殊情况下(尤其是原始套接字),这个参数必须显式指定。 + +--- + +> 以上表格覆盖了最常见的域、类型和协议选项,更多细节可查阅系统手册页(如 `man socket`)或官方文档。 + + +### 2.2 `bind()` 函数详解 + +`bind()` 是服务端编程中最常用的函数之一,它的作用是为套接字赋予本地地址(即 IP 地址和端口号),让操作系统知道该套接字监听哪个地址和端口。 + +```python +sock.bind(address) +``` + +#### 2.2.1 参数详解 + +`address` 是一个包含 IP 地址和端口号的元组,格式为 `(host, port)`。 + +| **参数成员** | **类型** | **说明** | +| :--- | :--- | :--- | +| **`host`** | 字符串 | IP 地址。可以是具体的 IP(如 `"192.168.1.100"`)、主机名(如 `"localhost"`)或通配符(`"0.0.0.0"` 或空字符串 `""`) | +| **`port`** | 整数 | 端口号,范围 0 ~ 65535。0 表示由操作系统自动分配(常用于客户端),1024 以下为特权端口(需要 root / 管理员权限) | + +**示例:** +```python +# 绑定到本地回环地址(仅本机可访问) +sock.bind(("127.0.0.1", 8080)) + +# 绑定到所有可用网络接口(本机所有 IP 均可访问) +sock.bind(("0.0.0.0", 8080)) + +# 等价于上面的通配写法(空字符串代表 INADDR_ANY) +sock.bind(("", 8080)) +``` + + + +#### 2.2.2 谁必须调用 `bind()`? + +| **角色** | **是否必须调用 `bind()`** | **原因** | +| :--- | :--- | :--- | +| **服务端(Server)** | **必须** | 服务端需要固定一个众所周知的端口(如 80/443/8080),让客户端能够找到并连接它 | +| **客户端(Client)** | **可选**(通常不调用) | 客户端发起 `connect()` 时,操作系统会自动分配一个临时端口(Ephemeral Port),无需手动绑定。只有在特殊场景下(如 P2P 需要固定端口)才会显式调用 | + +#### 2.2.3 常见错误排查 + +| **错误信息** | **可能原因** | **解决办法** | +| :--- | :--- | :--- | +| `Address already in use` | 端口被占用或处于 `TIME_WAIT` | 启用 `SO_REUSEADDR`,或更换端口,或等待 2~4 分钟 | +| `Permission denied` | 尝试绑定 < 1024 的特权端口 | 使用 sudo 运行,或改用 1024 以上的端口 | +| `Cannot assign requested address` | 绑定的 IP 地址在本地不存在 | 使用 `ifconfig` / `ip addr` 检查网卡 IP,改用 `0.0.0.0` 或正确的 IP | +| `Invalid argument` | 端口号超出 0~65535 范围 | 检查端口数值是否正确 | + +### 2.3 `listen()` 函数详解 + +`listen()` 是 TCP 服务端专属的函数,它告诉操作系统内核:这个套接字准备接受客户端的连接请求了。调用 `listen()` 后,套接字从主动套接字(可发起连接)转变为被动套接字(只能接受连接),并启动内核中的连接队列管理机制。 + +```python +sock.listen(backlog) +``` + +#### 2.3.1 参数详解 + +`backlog` 指定了已完成连接队列的最大长度(即已经完成三次握手、等待 `accept()` 取走的连接数量上限)。 + +| **参数** | **类型** | **说明** | +| :--- | :--- | :--- | +| **`backlog`** | 整数 | 表示连接请求队列的容量。超出该数量的连接请求将被拒绝(客户端收到 `ECONNREFUSED`)或忽略(视操作系统实现而定) | + +**示例:** +```python +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.bind(("0.0.0.0", 8080)) +sock.listen(5) # 最多允许 5 个已完成连接在队列中等待 accept() +``` + +--- + +#### 2.3.2 连接队列机制 + +TCP 服务端在 `listen()` 后,内核会维护两个队列: + +| **队列名称** | **状态** | **描述** | +| :--- | :--- | :--- | +| **半连接队列(SYN 队列)** | `SYN_RCVD` | 客户端发送 SYN 包后,服务端回复 SYN+ACK,但尚未收到客户端的 ACK 确认。该队列存放未完成三次握手的连接 | +| **已完成连接队列(Accept 队列)** | `ESTABLISHED` | 三次握手全部完成,连接已建立,等待应用层调用 `accept()` 取走 | + +`backlog` 参数直接限制的是已完成连接队列的长度(部分 Linux 内核中,`backlog` 也会影响半连接队列的大小,但一般不需深究)。 + +#### 2.3.3 队列工作流程图解 + +``` +客户端 服务端内核 服务端应用 + | | | + |--- SYN (connect) ------------> | (放入半连接队列) | + | | | + |<-- SYN+ACK ---------------- | | + | | | + |--- ACK ----------------------> | (移入已完成连接队列) | + | | | | + | | | (等待 accept) | + | | v | + | | <--- accept() 取走连接 ----- | +``` + +--- + +#### 2.3.4 `backlog` 值的选择 + +| **场景** | **推荐值** | **原因** | +| :--- | :--- | :--- | +| 高并发短连接服务(如 Web 服务器) | 128 ~ 1024 | 高并发下队列容易爆满,设置较大值可缓冲突发流量 | +| 长连接服务(如数据库连接池) | 5 ~ 50 | 连接建立后长期复用,队列压力较小 | +| 本地调试 / 学习示例 | 5 ~ 10 | 足够应对简单测试 | + +> 操作系统对 `backlog` 有硬上限(通常由 `/proc/sys/net/core/somaxconn` 控制,默认 128)。即使传入 1024,实际生效值也可能被内核截断为 128(或 `somaxconn` 的值)。生产环境如需要调整,需同时修改系统参数。 + +查看当前系统上限: +```bash +cat /proc/sys/net/core/somaxconn +``` + +临时修改(重启失效): +```bash +sudo sysctl -w net.core.somaxconn=1024 +``` + +--- + +#### 2.3.5 调用 `listen()` 的时机 + +`listen()` 必须在 `bind()` 之后、`accept()` 之前调用,顺序不可颠倒: + +```python +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.bind(("0.0.0.0", 8080)) +sock.listen(5) # 此时套接字变为被动监听状态 +while True: + conn, addr = sock.accept() # 从队列中取连接 + # 处理连接... +``` + +`listen()` 本身是非阻塞的,它立即返回,不会阻塞程序执行。 + +--- + +#### 2.3.6 常见问题 + +| **问题** | **原因** | **解决方案** | +| :--- | :--- | :--- | +| `accept()` 永远阻塞,客户端却连不上 | 未调用 `listen()` 或 `backlog` 被设置成 0 | 确保先调用 `listen()`,且 `backlog` > 0 | +| 高并发下连接请求大量失败(客户端连接超时) | `backlog` 过小或 `accept()` 处理速度跟不上新连接到达速率 | 增大 `backlog` 值,并优化应用层处理逻辑(使用多线程/异步 I/O) | +| 设置 `backlog=1000` 但实际队列长度只有 128 | 受系统 `somaxconn` 限制 | 调整内核参数 `net.core.somaxconn`,并确保在 `listen()` 之前生效 | +| `listen()` 后忘记处理 `accept()`,导致队列满,新 SYN 被丢弃 | 应用层未及时取走已完成连接 | 编写高效的事件循环,及时调用 `accept()` | + +--- + +#### 2.3.7 与其他 API 的关系 + +``` +socket() → bind() → listen() → accept() (服务端) + ↑ + (必须顺序调用) +``` + +`listen()` 是服务端开始监听连接的信号,之后客户端就可以发起 `connect()`,而服务端通过 `accept()` 从队列中取出已完成的连接。 + +### 2.4 `accept()` 函数详解 + +`accept()` 从已完成连接队列中取出第一个已成功完成三次握手的连接请求,并为其创建一个新的套接字,专门用于与该客户端进行后续的数据收发。 + + +```python +conn, address = sock.accept() +``` + +#### 2.4.1 返回值 + +`accept()` 返回一个包含两个元素的元组: + +| **返回值** | **类型** | **说明** | +| :--- | :--- | :--- | +| **`conn`** | 新的套接字对象 | **通信套接字**。专门用来与本次连接的客户端进行 `send()`/`recv()` 操作。它是 `socket()` 创建出来的新文件描述符,与监听套接字相互独立 | +| **`address`** | 元组 `(host, port)` | 客户端的 IP 地址和端口号,用于标识连接对端的身份 | + +--- + +#### 2.4.2 监听套接字 vs 通信套接字 + +| **对比维度** | **监听套接字(Listening Socket)** | **通信套接字(Connected Socket)** | +| :--- | :--- | :--- | +| **创建方式** | `socket()` + `bind()` + `listen()` 后的 `sock` | `accept()` 返回的 `conn` | +| **作用** | 监听和接收新连接请求 | 与特定客户端进行数据读写 | +| **生命周期** | 贯穿整个服务端运行周期,通常不关闭 | 每个客户端连接对应一个,处理完业务逻辑后关闭 | +| **能否收发数据** | 不能(调用 `recv`/`send` 会报错) | 可以(通过 `send`/`recv` 与客户端交互) | + +**流程示意:** +```python +# 监听套接字:只负责接受连接 +listen_sock = socket.socket(...) +listen_sock.bind(...) +listen_sock.listen(5) + +while True: + # 通信套接字:负责与某个特定客户端通信 + conn, addr = listen_sock.accept() # 获取新的通信套接字 + data = conn.recv(1024) # 用 conn 收数据 + conn.sendall(b"Hello") # 用 conn 发数据 + conn.close() # 本次会话结束,关闭通信套接字 + +# 服务关闭时才关闭监听套接字 +listen_sock.close() +``` + +--- + +#### 2.4.3 阻塞与非阻塞模式 + +| **模式** | **行为** | **适用场景** | +| :--- | :--- | :--- | +| **阻塞模式(默认)** | 如果已完成连接队列为空,`accept()` 会一直阻塞,直到有新的客户端连接进来 | 传统多线程/多进程服务器(每个连接开一个线程) | +| **非阻塞模式** | 队列为空时立即抛出 `BlockingIOError` 异常,程序可继续执行其他任务 | 配合 I/O 多路复用(如 `select`/`poll`/`epoll`)的高并发服务器 | + +**设置非阻塞模式:** +```python +listen_sock.setblocking(False) +try: + conn, addr = listen_sock.accept() +except BlockingIOError: + # 没有新连接,继续做其他事情 + pass +``` + +**设置超时模式:** +```python +listen_sock.settimeout(3.0) # 最多等待 3 秒 +try: + conn, addr = listen_sock.accept() +except socket.timeout: + print("3 秒内没有新连接,执行超时逻辑") +``` + +--- + +#### 2.4.4 异常处理与常见错误 + +| **异常类型** | **触发条件** | **应对策略** | +| :--- | :--- | :--- | +| `BlockingIOError` | 非阻塞模式下队列为空 | 忽略或在事件循环中稍后重试 | +| `socket.timeout` | `settimeout()` 设置了超时且时间到 | 处理超时逻辑或继续轮询 | +| `InterruptedError`(`EINTR`) | `accept()` 阻塞期间被系统信号中断 | 通常重试 `accept()` 即可(可配合循环捕获) | +| `OSError: [Errno 24] Too many open files` | 通信套接字未及时关闭,文件描述符耗尽 | 确保每个 `conn` 用完后调用 `conn.close()`,或使用 `with` 上下文管理器 | +| `ConnectionAbortedError` / `ECONNABORTED` | 连接在 `accept()` 取走前已被客户端或网络中断 | 捕获异常并继续运行,不影响监听套接字 | + +**服务端循环示例:** +```python +while True: + try: + conn, addr = listen_sock.accept() + except InterruptedError: + continue # 信号中断,重试 + except socket.timeout: + continue # 超时,继续等待 + except Exception as e: + print(f"Unexpected error: {e}") + break + + with conn: # 使用 with 确保自动关闭 + try: + data = conn.recv(1024) + if not data: + break + conn.sendall(data) + except ConnectionResetError: + print(f"Client {addr} abruptly disconnected") +``` + +### 2.5 `connect()` 函数详解 + +`connect()` 是 TCP 客户端的核心函数,它负责向服务端发起三次握手,建立一条可靠的传输通道。客户端调用 `connect()` 后,操作系统内核会自动完成握手流程,成功返回后即可通过该套接字进行数据传输。 + +```python +sock.connect(address) +``` + +#### 2.5.1 参数详解 + +| **参数** | **类型** | **说明** | +| :--- | :--- | :--- | +| **`address`** | 元组 `(host, port)` | 服务端的 IP 地址(或主机名)和端口号。`host` 可以是域名(如 `"example.com"`)或 IP 字符串(如 `"192.168.1.100"`),`port` 为 1~65535 的整数 | + +**示例:** +```python +# 连接到本地服务 +sock.connect(("127.0.0.1", 8080)) + +# 连接到公网域名 +sock.connect(("www.baidu.com", 80)) + +# 连接前自动解析域名 +sock.connect(("github.com", 443)) +``` + +若传入的是域名,`connect()` 会调用系统 DNS 解析器将其转换为 IP 地址;若有多个 IP,会按顺序尝试,直到成功连接一个。 + +--- + +#### 2.5.2 三次握手的过程 + +调用 `connect()` 后,内核会主动向服务端发起三次握手,整个过程是阻塞式的(默认情况下): + +``` +客户端 服务端 + | | + |---- SYN (seq=x) -------->| (connect() 发起) + | | + |<--- SYN+ACK (seq=y, ack=x+1) (服务端响应) + | | + |---- ACK (ack=y+1) ------>| (握手完成,连接建立) + | | + | connect() 返回成功 | +``` + +- 成功返回:三次握手完成,套接字进入 `ESTABLISHED` 状态,可立即进行 `send()`/`recv()`。 +- 失败返回:握手过程中任何环节出错(如服务端无响应、拒绝连接、网络不可达等),`connect()` 抛出异常。 + +--- + +#### 2.5.3 阻塞模式与超时控制 + +| **模式** | **行为** | **适用场景** | +| :--- | :--- | :--- | +| **阻塞模式(默认)** | `connect()` 会一直阻塞到连接建立成功或出错为止。若服务端未响应,可能阻塞数分钟(取决于系统 TCP 超时参数) | 简单客户端、对响应时间不敏感的场景 | +| **非阻塞模式** | 调用后立即返回(抛出 `BlockingIOError` 或返回 `-1` 且 `errno == EINPROGRESS`),连接在后台继续建立,需通过 `select`/`poll`/`epoll` 监听可写事件来确认连接是否成功 | 高并发客户端、异步 I/O 框架 | +| **设置超时** | `sock.settimeout(seconds)` 后,`connect()` 在超时时间内未完成则抛出 `socket.timeout` 异常 | 对响应时间有严格要求的应用(如监控工具) | + +**设置超时示例:** +```python +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.settimeout(5.0) # 5 秒超时 +try: + sock.connect(("192.168.1.100", 8080)) +except socket.timeout: + print("连接超时,服务器可能不可达") +``` + +**非阻塞连接示例:** +```python +sock.setblocking(False) +try: + sock.connect(("example.com", 80)) +except BlockingIOError as e: + # errno 为 115 (EINPROGRESS) 表示连接正在建立 + if e.errno != 115: + raise + # 使用 select 等待套接字可写(连接完成) + import select + select.select([], [sock], [], 5.0) # 等待 5 秒 + # 检查连接是否真正成功(可通过 getsockopt 获取错误码) +``` + +--- + +#### 2.5.4 异常处理与常见错误 + +| **异常类型** | **常见原因** | **解决/应对** | +| :--- | :--- | :--- | +| `ConnectionRefusedError`(`ECONNREFUSED`) | 服务端未监听该端口,或防火墙拒绝 | 检查服务端是否启动、端口是否正确、防火墙策略 | +| `ConnectionTimeoutError`(`ETIMEDOUT`) | 服务端无响应(网络不通、路由不可达、SYN 包丢失) | 检查网络连通性、服务端 IP 是否正确 | +| `socket.timeout` | 由 `settimeout()` 设置的超时时间到 | 增大超时时间,或处理超时后的重试逻辑 | +| `gaierror`(`EAI_NONAME`) | 域名解析失败 | 检查 DNS 配置、域名是否正确 | +| `OSError: [Errno 99] Cannot assign requested address` | 绑定源地址(如调用 `bind()` 后)指定了本机不存在的 IP | 检查本机 IP 地址,或省略 `bind()` 让系统自动分配 | +| `InterruptedError`(`EINTR`) | 系统调用被信号中断 | 重试 `connect()`(但注意 Linux 下自动重试机制,通常捕获后重试即可) | + +**带重试的连接示例:** +```python +def connect_with_retry(host, port, timeout=5.0, retries=3): + for i in range(retries): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect((host, port)) + return sock + except (socket.timeout, ConnectionRefusedError) as e: + print(f"连接失败 (尝试 {i+1}/{retries}): {e}") + sock.close() + time.sleep(1) + raise Exception(f"无法连接到 {host}:{port}") +``` + +--- + +#### 2.5.5 客户端是否必须调用 `bind()`? + +绝大多数客户端不需要调用 `bind()`。当调用 `connect()` 时,操作系统会自动为该套接字分配一个临时端口(Ephemeral Port,范围通常在 32768~60999),并选择一个合适的源 IP(根据路由表决定)。 + +若确实需要固定源 IP 或端口(如防火墙白名单要求),可在 `connect()` 之前先调用 `bind()`,但务必确保绑定的 IP 和端口未被占用,否则 `connect()` 也会失败。 + +--- + +#### 2.5.6 `connect()` 与 `send()`/`recv()` 的关系 + +- `connect()` 成功返回后,套接字处于已连接状态,必须先成功建立连接才能调用 `send()`/`recv()`。 +- 如果连接建立后服务端主动关闭连接(发送 FIN),客户端再调用 `send()` 会抛出 `BrokenPipeError`,而 `recv()` 会返回空字节串(表示 EOF)。 +- 调用 `close()` 或 `shutdown()` 会终止连接,此后不能再通过该套接字发送数据。 + + +### 2.6 `send()` 函数详解 + + + +```python +bytes_sent = sock.send(data) +``` + +#### 2.6.1 `send()` 参数与返回值 + +| **参数/返回值** | **类型** | **说明** | +| :--- | :--- | :--- | +| **`data`** | 字节序列(`bytes` 或 `bytearray`) | 要发送的数据,必须是二进制形式(字符串需用 `.encode()` 转换) | +| **返回值** | 整数 | 实际发送的字节数。可能小于 `len(data)`,表示本次只发送了部分数据,剩余数据需继续发送 | + +`send()` 并不保证一次将全部数据发送完毕。它只是把数据交给内核的发送缓冲区,若缓冲区空间不足,只会发送一部分,返回实际发送的字节数。 + +- 阻塞模式(默认):若发送缓冲区满了,`send()` 会阻塞,直到有空间容纳数据。 +- 非阻塞模式:缓冲区满时立即抛出 `BlockingIOError`,需配合 I/O 多路复用处理。 +- 部分发送:返回值可能小于 `len(data)`,必须循环调用直到所有数据发完,或使用 `sendall()`。 + +#### 2.6.2 `send()` 与 `sendall()` 的区别 + +| **函数** | **行为** | **推荐场景** | +| :--- | :--- | :--- | +| `send()` | 尝试一次发送,返回实际发送字节数(可能不足) | 底层开发、或与 I/O 多路复用配合时 | +| `sendall()` | 循环发送直到所有数据发完,或发生错误抛出异常 | 绝大多数应用层代码(更安全、更简单) | + +`sendall()` 内部就是循环调用 `send()`,但对开发者屏蔽了细节,推荐优先使用。 + +```python +# 推荐做法 +conn.sendall(b"Hello, World!") # 保证全部发送完成 +``` + +### 2.7 `recv()` 函数详解 + + +```python +data = sock.recv(bufsize) +``` + +#### 2.7.1 `recv()` 参数与返回值 + +| **参数/返回值** | **类型** | **说明** | +| :--- | :--- | :--- | +| **`bufsize`** | 整数 | 最多接收的字节数(不是缓冲区大小,而是本次读取的上限) | +| **返回值** | 字节序列(`bytes`) | 实际接收到的数据,长度 ≤ `bufsize`。若返回空字节串 `b''`,表示对端已关闭连接(EOF) | + +`recv(bufsize)` 并不保证能收到 `bufsize` 个字节,它只保证最多收到这么多。实际收到的可能更少,因此必须循环读直到收完一个完整的应用层消息。 + +- 阻塞模式(默认):若无数据可读,`recv()` 阻塞直到有数据到达或连接关闭。 +- 非阻塞模式:无数据时立即抛出 `BlockingIOError`。 +- 返回空字节串:表示对端调用了 `close()` 或 `shutdown()`,本端收到 FIN,应关闭本端套接字,不再继续读取。 + +--- + +#### 2.7.2 TCP 的流式特性与粘包问题 + +由于 TCP 是流式协议,数据没有边界,`send()` 发送的多个数据包可能在接收端被合并(粘包),也可能被拆分(拆包)。`recv()` 只管取数据,不知道消息边界。 + +| **现象** | **说明** | +| :--- | :--- | +| **粘包** | 两次 `send()` 的数据被一次 `recv()` 全部接收(或部分合并) | +| **拆包** | 一次 `send()` 的数据被多次 `recv()` 分次接收 | + +**解决方案**:应用层需自行定义消息边界,常见方式: +1. 固定长度:每条消息固定大小(如 1024 字节)。 +2. 分隔符:如 HTTP 的 `\r\n\r\n` 或自定义换行符。 +3. 长度前缀:在消息头部固定位置(如 4 字节)写入消息总长度,先读取长度再读取完整消息。 + +--- + + +#### 2.7.3 常见错误与异常 + +| **异常类型** | **触发条件** | **应对策略** | +| :--- | :--- | :--- | +| `ConnectionResetError` | 对端强制关闭(RST) | 关闭本端套接字,结束通信 | +| `TimeoutError` | `settimeout()` 超时后无数据 | 处理超时逻辑 | +| `BlockingIOError` | 非阻塞模式下无数据可读 | 注册读事件,等待可读 | +| 返回 `b''` | 对端正常关闭(收到 FIN) | 主动关闭本端套接字,退出循环 | + +--- + +#### 2.7.4 完整交互流程(客户端-服务端) + +``` +服务端: +listen_sock = socket() → bind() → listen() +while True: + conn, addr = accept() + data = conn.recv(1024) # 可能收到部分数据 + conn.sendall(b"OK") # 全部发送 + conn.close() + +客户端: +sock = socket() → connect() +sock.sendall(b"Hello") # 全部发送 +data = sock.recv(1024) # 可能收到部分数据 +# 需要循环直到收完 +sock.close() +``` + +--- + +#### 2.7.5 核心对比表格 + +| **函数** | **作用** | **返回值含义** | **是否保证全部传输** | **常见搭档** | +| :--- | :--- | :--- | :--- | :--- | +| `send(data)` | 向对端发送数据 | 实际发送的字节数(可能小于 `len(data)`) | 否,需循环或使用 `sendall()` | `sendall()` 作为替代 | +| `sendall(data)` | 发送全部数据(循环) | 无返回值(成功完成)或抛出异常 | 是(全部成功才返回) | 应用层首选 | +| `recv(bufsize)` | 从对端接收数据 | 实际接收的数据(≤ `bufsize`),空表示关闭 | 否(只取当前缓冲区内的数据) | 配合长度协议循环读取 | + +### 2.8 `setsockopt()` 函数详解 + +`socket` 创建后,其行为由一组默认参数控制。`setsockopt()` 允许修改这些参数,从而控制套接字的底层行为,如地址重用、超时、缓冲区大小、Nagle 算法、Keep‑Alive 等。 + +--- + +#### 2.8.1 函数原型与参数 + +```python +sock.setsockopt(level, optname, value) +``` + +| **参数** | **类型** | **说明** | +| :--- | :--- | :--- | +| **`level`** | 整数 | 选项所属的协议层,决定了 `optname` 的命名空间。常用值:`SOL_SOCKET`(通用套接字)、`IPPROTO_TCP`(TCP 专用)、`IPPROTO_IP`(IPv4)、`IPPROTO_IPV6`(IPv6) | +| **`optname`** | 整数 | 具体选项名称(通常以 `SO_`、`TCP_`、`IP_` 等开头),需与 `level` 配合使用 | +| **`value`** | 整数或字节序列 | 选项值。大部分选项接受整数(0/1 或具体数值),少数需要结构体(如 `struct linger` 或 `struct timeval`) | + +**示例:启用地址重用** +```python +sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +``` + +--- + +#### 2.8.2 常用选项速查表 + +以下表格按照协议层分类,列出了常用的选项。完整列表请参阅系统手册(`man 7 socket`、`man 7 tcp` 等)。 + +##### (1)通用套接字选项(`level = SOL_SOCKET`) + +| 选项名 | 数据类型 | 含义 | 典型用途 | +| :--- | :--- | :--- | :--- | +| `SO_REUSEADDR` | int | 允许地址重用(快速重启服务,无需等待 `TIME_WAIT`) | 服务端崩溃后立即重启,避免 `Address already in use` | +| `SO_REUSEPORT` | int | 允许多个进程/线程绑定相同 IP:端口(需内核支持 3.9+) | 实现多进程负载均衡,提高并发能力 | +| `SO_KEEPALIVE` | int | 启用 TCP 保活探测(检测死连接) | 长连接场景(数据库、消息队列),自动清理僵尸连接 | +| `SO_LINGER` | `struct linger` | 控制 `close()` 行为:是否等待未发送数据发送完毕 | 确保重要数据不丢失,或强制立即关闭 | +| `SO_RCVBUF` | int | 设置接收缓冲区大小(字节) | 调节吞吐量(大缓冲区适合大流量) | +| `SO_SNDBUF` | int | 设置发送缓冲区大小(字节) | 同上 | +| `SO_RCVTIMEO` | `struct timeval` | 设置接收超时时间(`recv()` 阻塞最长等待) | 避免永久阻塞,配合超时重试 | +| `SO_SNDTIMEO` | `struct timeval` | 设置发送超时时间(`send()` 阻塞最长等待) | 同上 | +| `SO_BROADCAST` | int | 允许发送广播数据报(仅 UDP) | 局域网设备发现(如 DHCP、ARP 模拟) | +| `SO_OOBINLINE` | int | 将带外数据(紧急数据)放入普通数据流 | 某些协议(如 Telnet)需要处理紧急数据 | +| `SO_ERROR` | int | 只读:获取并清除套接字错误状态(需 `getsockopt`) | 非阻塞连接后检查是否成功 | + +##### (2)TCP 专用选项(`level = IPPROTO_TCP`) + +| 选项名 | 数据类型 | 含义 | 典型用途 | +| :--- | :--- | :--- | :--- | +| `TCP_NODELAY` | int | 禁用 Nagle 算法(1=禁用,0=启用) | 实时性要求高的应用(游戏、RPC),减少延迟 | +| `TCP_MAXSEG` | int | 设置 TCP 最大分段大小(MSS) | 优化 MTU 感知,避免 IP 分片 | +| `TCP_KEEPIDLE` | int | 首次保活探测前的空闲时间(秒) | 调整保活灵敏度(默认 7200s) | +| `TCP_KEEPINTVL` | int | 两次保活探测的间隔(秒) | 同上 | +| `TCP_KEEPCNT` | int | 断开前最大探测失败次数 | 同上 | +| `TCP_QUICKACK` | int | 启用快速 ACK(1=启用,0=禁用) | 减少延迟,常用于交互式应用 | +| `TCP_DEFER_ACCEPT` | int | 仅当有数据到达时才唤醒 `accept()`(秒) | 减少 `accept()` 后的空循环,提高效率 | +| `TCP_CORK` | int | 延迟发送小包以合并(类似 Nagle 的增强版) | 批量发送场景(如文件传输)减少小包数量 | + +##### (3)IPv4 选项(`level = IPPROTO_IP`) + +| 选项名 | 数据类型 | 含义 | 典型用途 | +| :--- | :--- | :--- | :--- | +| `IP_TTL` | int | 设置 IP 存活时间(TTL) | 限制报文跳数,或实现 traceroute | +| `IP_TOS` | int | 设置服务类型(TOS)字段 | 区分服务(DiffServ),设置优先级 | +| `IP_MULTICAST_TTL` | unsigned char | 多播数据包的 TTL | 控制多播报文传播范围 | +| `IP_MULTICAST_LOOP` | unsigned char | 是否回环到本地(1=回环) | 禁用回环防止本机收到自己发的多播包 | +| `IP_ADD_MEMBERSHIP` | `struct ip_mreq` | 加入多播组 | 接收多播数据(如视频流、组播路由协议) | +| `IP_DROP_MEMBERSHIP` | `struct ip_mreq` | 离开多播组 | 同上 | +| `IP_PKTINFO` | int | 启用接收辅助数据(包含源地址、接口等) | 高级路由应用,如 UDP 服务器获取目标地址 | + +##### (4)IPv6 选项(`level = IPPROTO_IPV6`) + +| 选项名 | 数据类型 | 含义 | 典型用途 | +| :--- | :--- | :--- | :--- | +| `IPV6_V6ONLY` | int | 仅使用 IPv6(1=禁用 IPv4 映射,0=启用) | 双栈服务器控制是否同时接受 IPv4 连接 | +| `IPV6_UNICAST_HOPS` | int | 设置单播跳数限制(类似 TTL) | 同 IPv4 的 `IP_TTL` | +| `IPV6_MULTICAST_HOPS` | int | 多播跳数限制 | 同 IPv4 多播 | +| `IPV6_MULTICAST_LOOP` | int | 多播回环控制 | 同 IPv4 | +| `IPV6_ADD_MEMBERSHIP` | `struct ipv6_mreq` | 加入 IPv6 多播组 | IPv6 多播应用 | +| `IPV6_RECVPKTINFO` | int | 接收数据包信息 | 类似 IPv4 的 `IP_PKTINFO` | + +--- + +#### 2.8.3 典型使用场景与代码示例 + +##### (1)地址重用(`SO_REUSEADDR`) + +```python +server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +# 必须在 bind() 之前设置 +server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +server_sock.bind(('0.0.0.0', 8080)) +server_sock.listen(5) +``` + +`SO_REUSEADDR` 在 Windows 和 Linux 行为略有差异,但大多数场景下都能让服务快速重启。 + +##### (2)禁用 Nagle 算法(`TCP_NODELAY`) + +```python +client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +client_sock.connect(('server', 8888)) +client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) +``` + +##### (3)设置接收/发送超时(`SO_RCVTIMEO` / `SO_SNDTIMEO`) + +```python +import struct +# 构造 struct timeval(秒 + 微秒) +timeout = struct.pack('ll', 5, 0) # 5 秒 +sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, timeout) +sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, timeout) +``` + +或者使用 Python 更简化的方式(`settimeout`): +```python +sock.settimeout(5.0) # 等价于同时设置 RCVTIMEO 和 SNDTIMEO +``` + +##### (4)启用 Keep‑Alive 并调整参数 + +```python +sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) +# 以下仅 Linux 支持 +sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # 空闲 60s 后探测 +sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # 间隔 10s +sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) # 失败 3 次断开 +``` + +##### (5)设置缓冲区大小 + +```python +# 建议在 bind/listen 之前设置 +sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024) # 1MB +sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 1024) +``` + +实际设置后,可用 `getsockopt()` 检查内核实际分配的大小(可能被系统上限截断)。 + +--- + +#### 2.8.4 注意事项 + +| **注意点** | **说明** | +| :--- | :--- | +| **设置时机** | 绝大多数选项应在 `bind()` 或 `connect()` 之前设置,以保证对后续操作生效。例外:`TCP_NODELAY` 可在连接后设置。 | +| **缓冲区大小限制** | 内核会对缓冲区大小设置软上限(通常由 `net.core.rmem_max` 等控制)。即使设置很大,实际值可能被截断。需通过 `getsockopt()` 获取实际值。 | +| **平台差异** | 不同操作系统(Windows/Linux/macOS)对选项支持程度不一,如 `SO_REUSEPORT` 在 macOS 上行为不同。开发跨平台应用需仔细测试。 | +| **协议匹配** | 选项必须与套接字类型匹配。例如,UDP 套接字设置 `TCP_NODELAY` 会抛出 `Protocol not available` 错误。 | +| **错误检查** | `setsockopt()` 失败时会抛出 `OSError` 异常,建议捕获并处理(尤其在关键选项上)。 | +| **使用 `getsockopt()` 读取** | 某些选项(如 `SO_ERROR`)只能读取不能设置;另外,读取缓冲区大小可确认内核实际分配值。 | + +```python +# 读取实际接收缓冲区大小 +actual = sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) +print(f"实际接收缓冲区: {actual} 字节") +``` + +--- + +#### 2.8.5 小结 + +`socket` 选项是网络编程中容易被忽视但很有用的工具。合理配置它们,可以在不修改业务逻辑的情况下提升程序的性能(如缓冲区调优)、可靠性(Keep‑Alive)和可用性(地址重用)。建议在编写服务端程序时,至少设置 `SO_REUSEADDR`;在实时性要求高的场景考虑禁用 Nagle 算法;在长连接场景启用 Keep‑Alive。 + +### 2.9 `close()` 与 `shutdown()` 函数详解 + +在 TCP 编程中,关闭连接并非简单的“立刻断网”。TCP 关闭是一个双向、分步的过程(四次挥手)。Python 提供了 `close()` 和 `shutdown()` 两个函数,它们有着本质的区别。 + +- `close()`:减少套接字引用计数,计数归零时关闭整个套接字。 +- `shutdown()`:立即触发关闭序列,可独立关闭读端或写端,与引用计数无关。 + +--- + +#### 2.9.1 `close()` 函数 + +`close()` 是最常用的关闭方式,它的行为取决于套接字描述符的引用计数。 + +```python +sock.close() +``` + +**引用计数**:在支持多线程或多进程的程序中,同一个套接字可能被多个文件描述符引用(如 `fork()` 后父子进程共享)。`close()` 实际上只是将当前进程的引用计数减 1。只有当引用计数变为 0 时,内核才会真正开始 TCP 四次挥手,发送 FIN 包关闭连接。 + +| **特点** | **说明** | +| :--- | :--- | +| **引用计数依赖** | 若计数 > 1,`close()` 仅减少计数,不发送 FIN,连接依然存活 | +| **全双工关闭** | 引用计数归零时,同时关闭读和写,无法单独关闭某一方向 | +| **延迟关闭** | 若发送缓冲区仍有数据,`close()` 会尝试发送完毕(受 `SO_LINGER` 选项影响) | +| **默认行为** | 阻塞直到数据发送完或超时(取决于系统实现) | + +--- + +#### 2.9.2 `shutdown()` 函数 + +`shutdown()` 提供了更精细的控制,允许立即关闭连接的指定方向,并且不受引用计数影响——无论有多少个文件描述符指向该套接字,调用 `shutdown()` 都会立即触发对应的关闭动作。 + +```python +sock.shutdown(how) +``` + +##### 参数 `how` 详解 + +| **常量** | **整数值** | **行为** | **后续操作影响** | +| :--- | :--- | :--- | :--- | +| `SHUT_RD` | 0 | 关闭读端。套接字不再接收数据,内核接收缓冲区中的未读数据将被丢弃,后续 `recv()` 返回 `b''`(EOF) | 对端继续发送数据将触发 RST 或窗口为 0 | +| `SHUT_WR` | 1 | 关闭写端。套接字发送 FIN,后续 `send()` 会抛出异常 | 常用于“半关闭”场景(发完数据后告知对端不再发送) | +| `SHUT_RDWR` | 2 | 同时关闭读端和写端。等价于先 `SHUT_RD` 再 `SHUT_WR` | 完全关闭通信,但套接字描述符仍未被释放 | + +--- + +#### 2.9.3 区别对比表 + +| **对比维度** | **`close()`** | **`shutdown()`** | +| :--- | :--- | :--- | +| **引用计数影响** | 受影响。只减计数,计数为 0 时才真正关闭 | 不受影响。立即生效,直接触发 TCP 状态变化 | +| **关闭方向控制** | 全双工关闭(读写同时关闭) | 可独立控制(读、写、或读写) | +| **释放套接字描述符** | 会释放(当计数归零时),释放资源 | 不会释放。即使调用 `SHUT_RDWR`,仍需 `close()` 释放描述符 | +| **缓冲区处理** | 尝试发送完发送缓冲区中的剩余数据(默认) | `SHUT_WR` 立即发送 FIN,不等待未发送数据(但受 `SO_LINGER` 影响) | +| **多进程/线程场景** | 若一个线程 `close()`,其他线程仍可操作(直到计数归零) | 一旦 `shutdown()`,所有进程/线程的读写操作都会立即受影响 | +| **典型错误** | 忘记 `close()` 导致文件描述符泄漏 | 误用 `SHUT_RD` 导致对端发送数据时本端复位(RST) | + +--- + +#### 2.9.4 典型使用场景 + +##### (1)半关闭(`SHUT_WR`) + +在 HTTP/1.1 或自定义协议中,客户端发送完请求后,可以调用 `shutdown(SHUT_WR)` 告诉服务端“我没有更多数据要发了”,但仍然保持接收能力,等待服务端响应。 + +```python +# 客户端示例 +client_sock.sendall(request_data) +client_sock.shutdown(socket.SHUT_WR) # 发送 FIN,主动关闭写端 + +# 此时仍可接收服务端响应 +response = client_sock.recv(1024) +client_sock.close() # 最后释放描述符 +``` + +`SHUT_WR` 会立即发送 FIN 包,告诉对端本端数据已发送完毕。对端 `recv()` 会读取到所有数据后返回 `b''`,表示 EOF。 + +##### (2)强制关闭(`SHUT_RDWR`) + +当检测到对端异常(如心跳超时)时,可以立即切断双向通信: + +```python +sock.shutdown(socket.SHUT_RDWR) # 立即发送 FIN/RST,不再接收或发送 +sock.close() # 释放描述符 +``` + +##### (3)多进程/多线程中的协作关闭 + +假设主进程 `fork()` 了多个子进程,所有子进程都持有同一个监听套接字。如果某个子进程需要退出,调用 `close()` 可能不会真正关闭监听(因为其他子进程仍在引用)。此时若想彻底停止监听,父进程应调用 `shutdown(SHUT_RDWR)`,强制所有子进程的 `accept()` 立即失败(返回错误),随后再各自 `close()`。 + +--- + +#### 2.9.5 调用顺序与四次挥手流程 + +``` +客户端 服务端 + | | + |---- FIN (shutdown(SHUT_WR)) --------->| (客户端主动关闭写端) + | | + |<---- ACK ------------------------------| (服务端确认 FIN) + | | + |<---- 数据/响应 (recv) ----------------| (服务端继续发送数据) + | | + |---- ACK ------------------------------>| (客户端确认接收) + | | + |<---- FIN (close() on server) ---------| (服务端关闭连接) + | | + |---- ACK ------------------------------>| (四次挥手完成) + | | +``` + +--- + +#### 2.9.6 注意事项 + +| **陷阱** | **解释与建议** | +| :--- | :--- | +| **`close()` 后数据丢失** | 默认 `close()` 会尝试发完缓冲区数据,但若设置了 `SO_LINGER` 且超时时间过短,剩余数据会被丢弃。重要数据务必使用 `shutdown(SHUT_WR)` 并等待对端确认(通过 `recv()` 返回空)后再 `close()` | +| **`SHUT_RD` 的副作用** | 调用 `SHUT_RD` 后,若对端继续发送数据,本端会回复 RST 重置连接,导致对端收到 `ConnectionResetError`。除非有特殊需求,一般不推荐单独关闭读端 | +| **描述符泄漏** | `shutdown()` 不会释放文件描述符,必须配合 `close()` 使用,否则会导致资源泄漏(`Too many open files`) | +| **多线程下的并发关闭** | 一个线程调用 `shutdown(SHUT_RDWR)` 会导致其他线程正在阻塞的 `recv()`/`send()` 立即抛出异常,务必做好异常捕获 | +| **`close()` 的返回时机** | `close()` 返回并不代表对端已收到 FIN,只代表本端已发送 FIN 并将该套接字标记为关闭。若要确保对端收到,需在业务层设计确认机制 | + +--- + +#### 2.9.7 实践建议 + +```python +def safe_close(sock): + """优雅关闭套接字:先半关闭写端,等待对端关闭,再释放资源""" + try: + sock.shutdown(socket.SHUT_WR) # 发送 FIN,不再发送 + except OSError: + pass # 忽略已经关闭的异常 + + # 等待对端关闭(读取直到 EOF) + try: + while True: + data = sock.recv(1024) + if not data: + break + except (ConnectionResetError, OSError): + pass + + try: + sock.close() + except OSError: + pass +``` + +在大多数简单的客户端/服务端程序中,直接调用 `close()` 已经足够。但在实现网关、代理、长连接保活、协议解析等场景时,合理使用 `shutdown()` 能对 TCP 连接的生命周期进行更精细的控制。 + +--- + +#### 2.9.8 `shutdown()` 参数宏详解对照表 + +| 宏常量 (Python) | 整数值 | 关闭方向 | 本端(调用方)行为 | 对端(远端)感知 | 用途与警示 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **`SHUT_RD`** | `0` | 关闭接收通道 | ① 清空内核接收缓冲区中尚未取走的数据(丢弃)。
② 后续调用 `recv()` 会立即返回 `b''`(EOF)。
③ 本端不再响应或接收新的数据包。 | 若对端继续向本端发送数据,本端 TCP 协议栈会回复 RST 报文,对端将收到 `ConnectionResetError` 异常。 | 极少使用,风险较高。仅用于特殊安全场景(如拒绝后续数据),常规开发不建议使用,容易导致对方程序崩溃。 | +| **`SHUT_WR`** | `1` | 关闭发送通道 | ① 将发送缓冲区中残留的数据尽力发出。
② 发出后,立即向对端发送 FIN 包(四次挥手的第一次)。
③ 后续调用 `send()` 会立即抛出异常。 | 对端 `recv()` 在读完本端发送的所有数据后,会返回 `b''`(EOF),精确得知“本端已无数据可发”。 | 最常用!核心功能“半关闭”。用于告知对端“我的请求/响应发完了,但我还准备接收你的数据”。 | +| **`SHUT_RDWR`** | `2` | 同时关闭接收和发送通道 | ① 等同于先后调用 `SHUT_RD` 和 `SHUT_WR`。
② 立即发送 FIN,丢弃未读数据。
③ 本端所有读写操作均失效。 | 对端读写操作会立即感知异常(`recv` 返回空或 `send` 触发 RST)。 | 强制快速断开。用于超时踢线、检测到恶意连接时立即重置会话,不关心剩余数据。 | + +### 2.10 I/O 多路复用简介(进阶过渡) + +在 1.1 节中,我们使用 `threading`(多线程)来处理多个客户端连接。这种方式在连接数不多(几十到几百)时简单有效。但在高并发场景下(如网关、消息推送、实时行情服务器),成千上万个客户端同时在线,多线程模型会面临资源瓶颈。 + +本小节介绍 I/O 多路复用(I/O Multiplexing)的基本思想,理解从“线程模型”过渡到“事件驱动模型”的原因,并给出最简示例。 + +--- + +#### 2.10.1 为什么需要 I/O 多路复用? + +| **并发模型** | **机制** | **优点** | **缺点** | **适用场景** | +| :--- | :--- | :--- | :--- | :--- | +| **多进程/多线程** | 每个客户端连接分配一个线程或进程 | 编程模型直观,符合顺序思维 | ① 线程/进程切换开销大(上下文切换)
② 内存占用高(每个线程默认 ~8MB 栈空间)
③ 海量连接下系统资源迅速耗尽 | 并发数 < 500 的中小型服务 | +| **I/O 多路复用** | 单线程监控多个套接字,有事件时才处理 | ① 无线程切换开销
② 内存占用极低(一个线程即可处理数千连接)
③ 资源利用率高 | 编程逻辑相对复杂(回调/状态机),代码不如线程直观 | 高并发(数千~数万连接),如 Nginx、Redis、消息中间件 | + +在多线程模型中,每一个新客户端进来,都要创建一个线程。当 1 万个客户端同时在线时,操作系统需要维护 1 万个线程,频繁的上下文切换会让 CPU 大部分时间浪费在“换出/换入”上,而非真正处理业务。 + +--- + +#### 2.10.2 I/O 多路复用的演进:`select` -> `poll` -> `epoll` + +Linux 内核提供了三种主流的 I/O 多路复用接口,它们的功能是相同的——让内核帮我们监视一批文件描述符(套接字),当其中任何一个可读/可写/发生异常时,通知应用程序。 + +| **接口** | **数据结构** | **最大监视数量** | **性能特性** | **适用平台** | +| :--- | :--- | :--- | :--- | :--- | +| **`select`** | 固定大小的 `fd_set` 位图 | 通常 1024(受 `FD_SETSIZE` 限制) | 每次调用需将整个集合从用户态拷贝到内核态,O(n) 轮询扫描全部描述符 | 几乎所有操作系统(POSIX),便携但效率低 | +| **`poll`** | 动态数组 `pollfd` | 无上限(受系统内存限制) | 同样 O(n) 轮询扫描,但无 1024 硬限制,数据结构更灵活 | 大多数 Unix/Linux | +| **`epoll`** | 内核事件表(红黑树 + 就绪链表) | 无上限 | O(1) 高效,仅返回活跃的就绪描述符(事件驱动),无需全量扫描 | Linux 专属(2.6+),高并发首选 | +| **`kqueue`** | 事件过滤器 | 无上限 | 类似 `epoll`,O(1) 高效 | BSD/macOS 系统 | + +简单来说,`select` 和 `poll` 是“遍历所有连接,找出谁有动静”;`epoll` 是“谁有动静,谁主动上报”。 + +--- + +#### 2.10.3 Python 中的 I/O 多路复用 + +在生产环境(尤其是 Linux 服务器)中,Python 生态推荐使用以下抽象层来使用 I/O 多路复用: + +1. `selectors` 模块(Python 3.4+ 标准库):封装了 `select`/`poll`/`epoll`/`kqueue`,自动选择当前系统最高效的实现。推荐业务开发者使用,避免直接操作底层 API。 +2. `asyncio` 模块(Python 3.4+ 标准库):基于 `selectors` 构建的异步 I/O 框架,提供协程(`async/await`)语法,是目前 Python 高并发网络编程的主流方案。 + +--- + +#### 2.10.4 最简示例:使用 `select` 实现单线程 Echo 服务 + +本示例使用经典的 `select` 接口,用于演示原理。代码中通过一个列表维护所有待监视的套接字(监听套接字 + 所有已连接客户端套接字),在单线程中实现了“同时管理多个客户端”的效果。 + +```python +import socket +import select + +def main(): + host = '0.0.0.0' + port = 6810 + + # 1. 创建监听套接字并设置为非阻塞 + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.setblocking(False) # 关键:非阻塞模式 + server.bind((host, port)) + server.listen(5) + print(f"单线程 select 服务器启动 {host}:{port}") + + # 2. 维护一个"待监测列表" + inputs = [server] # 可读事件集合(监听套接字 + 已连接客户端) + outputs = [] # 可写事件集合(暂不演示) + # 字典:用于记录每个连接收到的"半包"数据(演示粘包/拆包处理) + # 注:本示例仅做回显,此处为占位 + + try: + while True: + # 3. 调用 select,阻塞等待事件发生 + # 返回三个列表:可读的、可写的、异常的 + readable, writable, exceptional = select.select(inputs, outputs, inputs) + + # 4. 处理所有可读的套接字 + for sock in readable: + if sock is server: + # ---- 处理新连接(监听套接字可读) ---- + try: + conn, addr = server.accept() + conn.setblocking(False) # 新连接也设为非阻塞 + inputs.append(conn) # 加入监视列表 + print(f"新连接: {addr}") + except Exception as e: + print(f"accept 出错: {e}") + else: + # ---- 处理客户端数据(通信套接字可读) ---- + try: + data = sock.recv(1024) + if data: + # 收到数据,简单回显(实际业务中可放入 outputs 等待发送) + sock.sendall(data) # 注:sendall 在非阻塞模式仍可能阻塞,生产环境应监控可写事件 + print(f"回显数据: {data.decode('utf-8').strip()}") + else: + # recv 返回空字节串 => 对端正常关闭(FIN) + print(f"客户端 {sock.getpeername()} 主动关闭") + sock.close() + inputs.remove(sock) + except (ConnectionResetError, OSError) as e: + # 客户端异常断开 + print(f"客户端异常断开: {e}") + sock.close() + inputs.remove(sock) + + # 5. 处理异常列表(如连接错误) + for sock in exceptional: + print(f"异常事件,关闭套接字: {sock}") + sock.close() + if sock in inputs: + inputs.remove(sock) + + except KeyboardInterrupt: + print("\n服务器关闭") + finally: + server.close() + +if __name__ == '__main__': + main() +``` + +**代码要点**: + +- 非阻塞模式:`setblocking(False)` 使得 `accept()` 和 `recv()` 不会阻塞主线程,若无可处理事件则 `select` 会统一等待。 +- 统一监视:将 `server` 和所有 `conn` 都放入 `inputs` 列表,`select` 会批量通知“哪些套接字有数据可读”。 +- 单线程优势:无论连接多少客户端,只有一个线程运行,没有上下文切换,CPU 利用率高。 +- 潜在风险:示例中直接在可读事件里调用 `sendall()`,若发送缓冲区满(极端流量下),仍会阻塞,生产环境中通常会将待发送数据放入 `outputs` 列表,等待可写事件触发再发送。 + +客户端示例代码 +```python +import socket +import select +import sys + +def main(): + # 1. 解析服务器地址 + host = sys.argv[1] if len(sys.argv) > 1 else '127.0.0.1' + port = 6810 + + # 2. 创建并连接服务器 + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + client.connect((host, port)) + print(f"✅ 已连接到服务器 {host}:{port}") + print("💬 输入消息按回车发送 (Ctrl+D 退出)") + except ConnectionRefusedError: + print("❌ 连接失败:请确保服务器已启动") + return + + # 3. 设置待监测的输入源:网络套接字 + 键盘输入 + # 注意:在 Windows 上,select 不支持监控普通文件句柄(包括 sys.stdin)。 + # 本示例适用于 Linux/macOS。跨平台方案请考虑使用 threading 或 asyncio。 + inputs = [client, sys.stdin] + + try: + while inputs: + # 4. 阻塞等待任意输入源就绪 + readable, _, exceptional = select.select(inputs, [], inputs) + + # 5. 处理异常事件 + if exceptional: + print("⚠️ 发生异常事件,退出") + break + + for fd in readable: + # ---------- 场景 A:服务器发来数据 ---------- + if fd is client: + data = client.recv(1024) + if not data: + # 服务器主动关闭连接(收到 FIN) + print("\n🔌 服务器已关闭连接") + inputs.remove(client) + client.close() + # 注意:这里没有 break,继续处理其他输入源(如还在等待的 stdin) + else: + # 正常回显服务器数据(加换行避免与输入提示混在一起) + print(f"\n📩 [服务器]: {data.decode('utf-8')}") + # 打印完后重新显示输入提示符(可选) + # 但 sys.stdout.write("> ") 需配合 flush,简单起见此处不实现 + + # ---------- 场景 B:用户在键盘输入 ---------- + elif fd is sys.stdin: + line = sys.stdin.readline() + if not line: + # 用户按了 Ctrl+D (EOF),表示不再输入 + print("\n👋 用户主动退出,关闭写端...") + # 关键:只关闭写端,告诉服务器 "我不会再发数据了" + # 但仍保持读端,等待服务器发送最后的响应(优雅半关闭) + client.shutdown(socket.SHUT_WR) + inputs.remove(sys.stdin) # 不再监控 stdin + # 注意:不关闭 client 套接字,继续监听服务器可能发来的剩余数据 + else: + # 去除换行符,发送给服务器 + msg = line.strip() + if msg: + try: + client.sendall(msg.encode('utf-8')) + except (BrokenPipeError, ConnectionResetError): + print("\n❌ 发送失败:连接已断开") + inputs.remove(client) + client.close() + break + + except KeyboardInterrupt: + print("\n 用户中断 (Ctrl+C)") + finally: + # 确保资源释放 + client.close() + print(" 客户端已退出") + +if __name__ == '__main__': + main() +``` + +--- + +#### 2.10.5 从示例到生产环境 + +| **进阶方向** | **关键模块/技术** | **说明** | +| :--- | :--- | :--- | +| **更优雅的 API** | Python `selectors` 模块 | 自动选用 `epoll`/`kqueue`/`select`,提供统一的 `register`/`unregister` 接口,代码比手写 `select` 更整洁 | +| **协程异步编程** | Python `asyncio` + `async/await` | 在 I/O 多路复用之上封装了协程,让异步代码写起来像同步代码,告别回调地狱 | +| **高性能框架** | `Tornado`、`Sanic`、`FastAPI`(配合 `uvicorn`) | 底层均依赖 `asyncio` 或 `uvloop`(基于 `libuv` 的高性能事件循环),可支撑数万并发连接 | +| **底层 Rust/Go** | Golang 的 `goroutine`、Rust 的 `tokio` | 将 I/O 多路复用与轻量级协程结合,进一步降低开发心智负担,适合对性能极致追求的场景 | + +--- + +#### 2.10.6 小结 + +I/O 多路复用是网络编程从入门走向高阶的必经之路。虽然本节仅演示了原始的 `select`,但其核心思想是“用一个线程,管理所有连接,只处理有事件的连接”。 + +在实际工作中,你几乎不需要直接使用 `select`/`poll`/`epoll`,而是会使用 `asyncio` 或基于它构建的 Web 框架。但理解这些底层原理,对读懂异步框架源码、排查高并发性能问题有帮助。 + +> 推荐阅读:继续学习可深入 Python 官方文档的 [`selectors`](https://docs.python.org/3/library/selectors.html) 模块和 [`asyncio`](https://docs.python.org/3/library/asyncio.html) 教程。 + +## 3. `select` / `poll` / `epoll` + +在上一节的 I/O 多路复用简介中,我们看到了如何使用 `select` 在一个线程中管理多个连接。`select`、`poll` 和 `epoll` 是 Linux/Unix 系统下三种主流的 I/O 多路复用接口。它们解决的是同一个核心问题:**如何让一个进程(或线程)同时监控多个文件描述符(如 socket),并在其中任何一个有 I/O 事件发生时得到通知**。 + +本章我们将深入剖析这三种机制的演进脉络、核心原理和差异。 + +### 3.1 核心概念:从“轮询”到“事件驱动” + +这三种机制的核心区别在于它们如何发现“哪些文件描述符就绪了”: + +| 机制 | 工作模式 | 通俗理解 | +| :--- | :--- | :--- | +| **`select` / `poll`** | **主动轮询** | 每次调用时,应用程序将全部待监控的文件描述符列表传给内核,内核**遍历**整个列表,检查每个描述符的状态,最后将就绪的描述符返回。 | +| **`epoll`** | **事件驱动** | 应用程序提前将待监控的文件描述符注册到内核中的一个**事件表**里。当某个描述符就绪时,内核通过**回调函数**将其主动放入一个就绪队列。应用程序调用等待函数时,内核只需检查这个就绪队列。 | + +可以把 `select/poll` 想象成老师每节课都**点名**(遍历所有学生),问谁有问题;而 `epoll` 则是学生有问题时**主动举手**(回调),老师只需处理举手的学生。 + +### 3.2 详细对比:`select` vs `poll` vs `epoll` + +| 对比维度 | **`select`** | **`poll`** | **`epoll`** | +| :--- | :--- | :--- | :--- | +| **数据结构** | 固定大小的 `fd_set` 位图 | 动态数组 `pollfd` | 内核中的**红黑树** + **就绪链表** | +| **描述符数量限制** | **有上限**,通常为 1024 (`FD_SETSIZE`) | **无硬性限制** | **无硬性限制** | +| **工作方式** | **轮询 (O(n))**,每次遍历所有描述符 | **轮询 (O(n))**,每次遍历所有描述符 | **事件驱动 (O(1))**,只处理就绪的描述符 | +| **数据拷贝** | **每次调用**都将整个 `fd_set` 从用户态拷贝到内核态 | **每次调用**都将整个 `pollfd` 数组拷贝到内核态 | **仅注册时拷贝一次**,通过 `epoll_ctl` 管理 | +| **触发模式** | 仅支持**水平触发 (LT)** | 仅支持**水平触发 (LT)** | 同时支持**水平触发 (LT)** 和**边缘触发 (ET)** | +| **平台支持** | **几乎全平台**,POSIX 标准 | **几乎全平台**,POSIX 标准 | **Linux 特有** (2.5.45+ 内核) | +| **API 复杂度** | 简单 | 较简单 | **相对复杂**,需要三个函数配合 | + +### 3.3 `epoll` 为什么更快? + +`epoll` 的高性能源于其精巧的内核设计,主要体现在三个方面: + +#### 3.3.1 高效的数据结构:红黑树 + 就绪链表 + +`epoll` 在内核中维护了两个核心数据结构: +* **红黑树 (Red-Black Tree)**:用于存储所有被监控的文件描述符。增删改查的时间复杂度为 **O(log N)**,即使管理数十万连接也非常高效。 +* **就绪链表 (Ready List)**:用于存储所有已就绪的文件描述符。当事件发生时,内核通过回调将描述符移入此链表。 + +#### 3.3.2 事件驱动机制:回调函数 + +这是 `epoll` 与 `select/poll` 最本质的区别。当向 `epoll` 注册一个描述符时,内核会为该描述符绑定一个**回调函数**。当该描述符就绪(如数据到达)时,内核触发回调,将其**自动**加入就绪链表。应用程序调用 `epoll_wait` 时,只需检查就绪链表是否为空。 + +#### 3.3.3 一次拷贝,终身受益 + +`select/poll` 每次调用都需要将整个描述符集合从用户空间拷贝到内核空间。而 `epoll` 通过 `epoll_ctl` 在内核中**维护一份永久的描述符列表**,只需在**添加或删除时拷贝一次**。 + +### 3.4 触发模式:LT 与 ET + +`epoll` 独有的**边缘触发 (Edge Triggered, ET)** 模式是其高性能的又一关键。 + +* **水平触发 (Level Triggered, LT) - 默认模式** + * **行为**:只要一个文件描述符处于**就绪状态**(如读缓冲区有数据),`epoll_wait` 就会**不断**返回该事件,直到数据被完全读取。 + * **特点**:与 `select/poll` 行为一致,编程简单,不易出错,是 `epoll` 的默认模式。 + +* **边缘触发 (Edge Triggered, ET) - 高效模式** + * **行为**:仅在文件描述符的**状态发生变化时**(如从“不可读”变为“可读”)通知一次。即使缓冲区里还有数据,只要状态不再变化,就不会再次通知。 + * **特点**:**高效**,减少了事件被重复触发的次数。但编程**更复杂**,要求开发者必须**一次性将数据全部读完**(通常需要循环读取直到 `EAGAIN` 错误),否则会丢失数据。ET 模式是 `epoll` 性能优于 `select/poll` 的重要原因之一。 + +### 3.5 如何选择? + +* **需要跨平台**:选择 `select` 或 `poll`。 +* **实时性要求极高**:`select` 的超时精度为微秒级,而 `poll/epoll` 为毫秒级,可考虑 `select`。 +* **高并发 Linux 服务器**:**首选 `epoll`**。它是为处理海量连接而设计的。 +* **连接数少且非常活跃**:`select/poll` 的简单轮询可能比 `epoll` 更高效,因为 `epoll` 的回调机制也有一定开销。 + +### 3.6 Python 中的实践 + +Python 的 `select` 模块对这三种机制都提供了支持。 + +#### 3.6.1 `select` 示例 +已在 2.10.4 节给出,可参考其实现。 + +#### 3.6.2 `poll` 示例 +```python +import socket +import select + +server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +server.bind(('0.0.0.0', 8888)) +server.listen(5) +server.setblocking(False) + +# 1. 创建 poll 对象 +poll = select.poll() +# 2. 注册监听套接字,关注可读事件 (POLLIN) +poll.register(server, select.POLLIN) + +# 维护文件描述符到 socket 对象的映射 +fd_to_socket = {server.fileno(): server} + +print("Poll 服务器启动,监听端口 8888...") + +try: + while True: + # 3. 等待事件发生,超时时间 1000ms + events = poll.poll(1000) + + for fd, event in events: + sock = fd_to_socket[fd] + + # 处理新连接 + if sock is server: + conn, addr = server.accept() + conn.setblocking(False) + # 将新连接也注册到 poll 中 + poll.register(conn, select.POLLIN) + fd_to_socket[conn.fileno()] = conn + print(f"新连接: {addr}") + # 处理客户端数据 + elif event & select.POLLIN: + data = sock.recv(1024) + if data: + print(f"收到数据: {data.decode()}") + # 回显数据(实际应注册 POLLOUT 事件来发送) + sock.send(data) + else: + # 客户端关闭 + print(f"连接关闭: {sock.getpeername()}") + poll.unregister(sock) + sock.close() + del fd_to_socket[fd] + # 处理错误事件 + elif event & (select.POLLHUP | select.POLLERR): + print(f"连接异常: {sock.getpeername()}") + poll.unregister(sock) + sock.close() + del fd_to_socket[fd] +except KeyboardInterrupt: + print("服务器关闭") +finally: + server.close() +``` + +#### 3.6.3 `epoll` 示例 + +```python +import socket +import select + +server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +server.bind(('0.0.0.0', 8888)) +server.listen(5) +server.setblocking(False) + +# 1. 创建 epoll 对象 +epoll = select.epoll() +# 2. 注册监听套接字,关注可读事件 +epoll.register(server.fileno(), select.EPOLLIN) + +fd_to_socket = {server.fileno(): server} + +print("Epoll 服务器启动,监听端口 8888...") + +try: + while True: + # 3. 等待事件发生 + events = epoll.poll(1) + + for fd, event in events: + sock = fd_to_socket[fd] + + # 处理新连接 + if sock is server: + conn, addr = server.accept() + conn.setblocking(False) + # 注册新连接,使用边缘触发 (EPOLLET) 模式 + epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET) + fd_to_socket[conn.fileno()] = conn + print(f"新连接: {addr}") + # 处理客户端数据 + elif event & select.EPOLLIN: + # 边缘触发模式下,需要循环读取直到读完 + while True: + try: + data = sock.recv(1024) + if not data: + # 客户端关闭 + print(f"连接关闭: {sock.getpeername()}") + epoll.unregister(fd) + sock.close() + del fd_to_socket[fd] + break + print(f"收到数据: {data.decode()}") + sock.send(data) + except BlockingIOError: + # 数据已读完,退出循环 + break + # 处理错误事件 + elif event & (select.EPOLLHUP | select.EPOLLERR): + print(f"连接异常: {sock.getpeername()}") + epoll.unregister(fd) + sock.close() + del fd_to_socket[fd] +except KeyboardInterrupt: + print("服务器关闭") +finally: + epoll.close() + server.close() +``` + +**代码要点**: +- `poll` 和 `epoll` 的使用模式非常相似:创建对象 → 注册事件 → 循环等待。 +- `epoll` 示例中使用了 `EPOLLET`(边缘触发)标志,因此在读取数据时需要**循环**直到收到 `BlockingIOError`,以确保数据被完整读取。 + +### 3.7 总结 + +| 机制 | 一句话总结 | +| :--- | :--- | +| **`select`** | 最古老、最通用,但有 1024 连接数限制,效率随连接数增加而线性下降。 | +| **`poll`** | `select` 的改进版,去除了连接数限制,但未解决轮询效率问题。 | +| **`epoll`** | Linux 下高性能的解决方案,通过事件驱动、红黑树和回调机制,实现了 O(1) 的事件通知。 | + +理解这三种 I/O 多路复用技术的演进,是掌握高并发网络编程的关键一步。在实际开发中,Python 的 `asyncio` 库已经为我们封装好了这些底层细节,通常会根据平台自动选择最高效的实现。 + +## 4 `selectors` 与 `asyncio` 模块 + +### 4.1 `selectors` 模块:更高级的 I/O 多路复用 + +`selectors` 模块是 Python 3.4 引入的一个高级 I/O 复用库。它建立在 `select` 模块之上,为你提供一个**统一的、更易用的 API**,用于在不同操作系统上实现高效的 I/O 多路复用。 + +#### 4.1.1 为什么需要 `selectors`? + +在第三节中,我们学习了 `select`、`poll` 和 `epoll`。它们在不同平台上各有优劣,且 API 细节不同。`selectors` 模块的价值在于: + +1. **自动选择最佳实现**:`DefaultSelector` 会为当前操作系统自动选择最高效的 I/O 多路复用机制(如 Linux 上的 `epoll`,BSD/macOS 上的 `kqueue`,Windows 上的 `select`)。 +2. **统一的 API**:它抽象了底层差异,让你只需学习和使用一套接口(`register`、`unregister`、`select`)。 +3. **回调驱动**:它天然适合基于**回调**的编程模型,你可以将事件和对应的处理函数(回调)绑定在一起。 + +官方文档也明确建议:**除非你需要对底层 OS 机制进行精确控制,否则应优先使用 `selectors` 模块**。 + +#### 4.1.2 核心概念与用法 + +使用 `selectors` 主要涉及以下几个概念和步骤: + +| 概念 | 说明 | +| :--- | :--- | +| `DefaultSelector` | 创建选择器对象,是管理所有事件的核心。 | +| `register(fileobj, events, data)` | 注册文件对象(如 socket),指明关注的事件(`EVENT_READ` / `EVENT_WRITE`)。`data` 通常绑定回调函数或附带数据。 | +| `unregister(fileobj)` | 注销不再监控的文件对象。 | +| `select(timeout=None)` | 阻塞等待事件,返回就绪事件列表。 | +| `SelectorKey` | 包含 `fileobj`、`fd` 和注册时附带的 `data`,由 `select()` 返回。 | + +#### 4.1.3 示例:使用 `selectors` 实现 Echo 服务器 + +```python +import selectors +import socket + +# 1. 创建默认选择器(自动选择最高效的实现) +sel = selectors.DefaultSelector() + +def accept(sock): + """处理新连接的回调函数""" + conn, addr = sock.accept() + print(f'接受来自 {addr} 的连接') + conn.setblocking(False) + # 2. 注册新连接,关注可读事件,绑定 read 回调 + sel.register(conn, selectors.EVENT_READ, read) + +def read(conn): + """处理客户端数据的回调函数""" + data = conn.recv(1024) + if data: + print(f'回显: {data.decode()}') + conn.sendall(data) # Echo 回数据 + else: + # 客户端关闭连接,注销并关闭 socket + print('客户端关闭连接') + sel.unregister(conn) + conn.close() + +# 创建监听 socket +host, port = 'localhost', 65432 +lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +lsock.bind((host, port)) +lsock.listen() +print(f'服务器监听 {host}:{port}') +lsock.setblocking(False) + +# 3. 注册监听 socket,关注可读事件,绑定 accept 回调 +sel.register(lsock, selectors.EVENT_READ, accept) + +try: + # 4. 事件循环 + while True: + events = sel.select(timeout=None) # 阻塞等待事件 + for key, _ in events: + callback = key.data # 获取回调函数 + callback(key.fileobj) # 执行回调 +except KeyboardInterrupt: + print('服务器关闭') +finally: + sel.close() +``` + +**代码解析**: +- 完全在**单线程**中运行,无线程切换开销。 +- 通过 `register` 将 socket 与回调函数绑定,事件触发时自动调用。 +- 适合 I/O 密集型的高并发场景。 + +> `selectors` 是理解现代异步编程的重要基石,`asyncio` 的事件循环底层正是构建在它之上。 + +--- + +### 4.2 `asyncio` 模块:协程时代的异步编程 + +如果说 `selectors` 是对 I/O 多路复用机制的高级封装,那么 `asyncio` 则是在此基础上,为**异步编程**提供了一整套**更高级、更人性化的框架**。 + +#### 4.2.1 为什么需要 `asyncio`? + +`selectors` 虽然高效,但基于回调的编程模型容易导致“回调地狱”——代码层层嵌套,难以理解和维护。`asyncio` 通过引入 `async/await` 语法,让你可以用**看起来像同步代码的方式编写异步程序**,极大地提升了代码的可读性和可维护性。 + +`asyncio` 是 Python 官方提供的异步 I/O 库,也是众多高性能 Python 框架(如 FastAPI、Sanic、Tornado)的基础。 + +#### 4.2.2 核心概念 + +| 概念 | 说明 | +| :--- | :--- | +| **事件循环 (Event Loop)** | `asyncio` 应用的核心,负责管理和调度所有异步任务的执行。 | +| **协程 (Coroutine)** | 通过 `async def` 定义的函数,是 `asyncio` 的基本执行单元。 | +| **可等待对象 (Awaitable)** | 可在 `await` 表达式中使用的对象,包括协程、任务 (Task) 和未来 (Future)。 | +| **`await` 关键字** | 挂起当前协程,将控制权交还给事件循环,等待异步操作完成。 | +| **任务 (Task)** | 用于在事件循环中**并发地**运行协程。 | + +#### 4.2.3 基础用法示例 + +**定义和运行一个简单的协程** + +```python +import asyncio + +async def say_hello(): + print("Hello") + await asyncio.sleep(1) # 模拟 I/O 操作,挂起协程 + print("World") + +asyncio.run(say_hello()) # 入口点,创建事件循环并运行协程 +``` + +**并发运行多个任务** + +```python +import asyncio + +async def task(name, delay): + print(f"任务 {name} 开始,等待 {delay} 秒") + await asyncio.sleep(delay) + print(f"任务 {name} 完成") + return f"结果 {name}" + +async def main(): + # 创建三个任务并发执行 + task1 = asyncio.create_task(task("A", 2)) + task2 = asyncio.create_task(task("B", 1)) + task3 = asyncio.create_task(task("C", 3)) + + # 等待所有任务完成并收集结果 + results = await asyncio.gather(task1, task2, task3) + print(f"所有结果: {results}") + +asyncio.run(main()) +``` + +- `asyncio.create_task()` 将协程包装成 `Task` 并安排并发执行。 +- `asyncio.gather()` 并发等待多个任务,总耗时约等于最长的那个(3秒)。 + +#### 4.2.4 Echo 服务器实战案例 + +下面用 `asyncio` 实现一个完整的 **Echo 服务器**和对应的客户端,展示其在 TCP 网络编程中的简洁与优雅。 + +**服务端代码** + +```python +import asyncio + +async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + """处理单个客户端连接""" + addr = writer.get_extra_info('peername') + print(f'新连接来自 {addr}') + + try: + while True: + data = await reader.readline() # 读取一行(以 \n 结尾) + if not data: # 客户端关闭连接 + break + + message = data.decode().strip() + print(f'收到 {addr}: {message}') + + writer.write(data) # 回显 + await writer.drain() # 刷新缓冲区 + except ConnectionResetError: + print(f'客户端 {addr} 异常断开') + finally: + print(f'关闭连接 {addr}') + writer.close() + await writer.wait_closed() + +async def main(): + server = await asyncio.start_server( + handle_client, + host='0.0.0.0', + port=8888, + backlog=100, + start_serving=True + ) + addr = server.sockets[0].getsockname() + print(f'Echo 服务器启动,监听 {addr}') + + async with server: + await server.serve_forever() + +if __name__ == '__main__': + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('\n服务器已关闭') +``` + +**客户端代码** + +```python +import asyncio + +async def client_main(): + reader, writer = await asyncio.open_connection('127.0.0.1', 8888) + print('已连接到服务器,输入消息 (输入 exit 退出)') + + try: + while True: + msg = input('> ') + if msg.strip().lower() == 'exit': + break + + writer.write((msg + '\n').encode()) + await writer.drain() + + data = await reader.readline() + if not data: + print('服务器关闭连接') + break + + print(f'服务器回显: {data.decode().strip()}') + finally: + print('关闭客户端') + writer.close() + await writer.wait_closed() + +if __name__ == '__main__': + asyncio.run(client_main()) +``` + +> **注意**:客户端使用了同步的 `input()`,这会阻塞事件循环。生产环境应使用 `asyncio.to_thread` 或 `loop.run_in_executor` 异步读取标准输入,详见后文“扩展思考”。 + +**运行与测试** + +1. 启动服务器: + ``` + Echo 服务器启动,监听 ('0.0.0.0', 8888) + ``` +2. 启动客户端: + ``` + > hello + 服务器回显: hello + > world + 服务器回显: world + > exit + 关闭客户端 + ``` +3. 服务器侧同步打印日志。 + +**关键点解析** + +| 组件 | 说明 | +| :--- | :--- | +| `asyncio.start_server` | 创建 TCP 服务器,第一个参数为客户端处理协程,自动传入 `StreamReader` / `StreamWriter`。 | +| `StreamReader` / `StreamWriter` | 封装流式读写,`readline()` 适用于行协议,`write()` + `drain()` 确保数据发送。 | +| `await writer.drain()` | 刷新写缓冲区,相当于同步编程中的 `flush()`。 | +| `writer.wait_closed()` | 等待连接完全关闭,释放资源。 | +| `server.serve_forever()` | 使服务器持续运行,直到事件循环停止。 | + +**扩展思考:异步读取标准输入** + +若要使客户端完全异步(不阻塞事件循环),可这样实现: + +```python +import asyncio + +async def client_main(): + reader, writer = await asyncio.open_connection('127.0.0.1', 8888) + print('已连接,输入消息') + + async def read_responses(): + while True: + data = await reader.readline() + if not data: + print('服务器断开') + break + print(f'服务器: {data.decode().strip()}') + + async def send_loop(): + loop = asyncio.get_running_loop() + while True: + msg = await loop.run_in_executor(None, input, '> ') + if msg.strip().lower() == 'exit': + break + writer.write((msg + '\n').encode()) + await writer.drain() + writer.close() + await writer.wait_closed() + + await asyncio.gather(read_responses(), send_loop()) + +asyncio.run(client_main()) +``` + +这样,客户端在等待服务器回显的同时,仍能响应用户输入,完全异步。 + +**与底层方式的对比** + +| 对比维度 | `select` / `poll` / `epoll` | `selectors` | `asyncio` | +| :--- | :--- | :--- | :--- | +| **代码行数** | 约 60–80 行 | 约 40–50 行 | 约 30 行 | +| **编程模型** | 手动事件轮询 | 回调注册 | 同步风格的协程 | +| **并发控制** | 手动管理 fd 状态 | 回调自动触发 | 协程自动调度 | +| **可读性** | 较低,逻辑分散 | 中等,易回调嵌套 | **非常高**,贴近业务 | +| **错误处理** | 分散在各分支 | 在回调中捕获 | `try/except` 集中处理 | + +**小结** + +通过这个 Echo 案例,你可以直观感受到 `asyncio` 的威力: +- 代码更少、逻辑更集中。 +- 无需手动管理文件描述符、轮询或回调。 +- 并发编程像同步代码一样自然。 + +`asyncio` 是现代 Python 网络编程的基石,后续学习 FastAPI、Sanic、`aiohttp` 等框架时,你会发现它们的底层都离不开 `asyncio` 的事件循环和协程模型。 + +--- + +### 4.3 `selectors` 与 `asyncio` 对比总结 + +| 特性 | `selectors` 模块 | `asyncio` 模块 | +| :--- | :--- | :--- | +| **定位** | 高级 I/O 多路复用库 | 异步 I/O 框架 | +| **编程模型** | 基于回调 (Callback) | 基于协程 (Coroutine),`async/await` | +| **代码风格** | 易陷入“回调地狱” | 同步风格,逻辑清晰 | +| **依赖关系** | 独立模块 | 底层依赖 `selectors` 实现事件循环 | +| **适用场景** | 轻量级底层网络服务或自定义协议 | 复杂的高层 I/O 密集型应用(Web、爬虫、API 客户端) | +| **学习曲线** | 较低 | 较高(需理解 `async/await`、事件循环等) | + +### 4.4 如何选择? + +- **追求极致的轻量和对底层的精确控制**,且能接受回调风格 → 选择 `selectors`。 +- **绝大多数现代 Python 网络应用开发** → 直接选择 `asyncio`。它不仅代码更清晰易维护,而且底层自动为你选择最高效的 I/O 多路复用机制。 + +从 `select` 到 `selectors`,再到 `asyncio`,Python 网络编程的演进清晰地展示了从**底层机制**到**高级抽象**的发展路径,其最终目标都是让开发者能够更高效、更优雅地编写高并发网络应用。 diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/1.png" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/1.png" new file mode 100755 index 0000000000000000000000000000000000000000..d0cc474fb92cd284b9b7cbcc71bebc925494e78a Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/1.png" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/ST7789V.png" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/ST7789V.png" new file mode 100755 index 0000000000000000000000000000000000000000..22118b34dbc14fa3fa1d5616f27c9e9ecfbe4916 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/ST7789V.png" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/image_1.png" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/image_1.png" new file mode 100755 index 0000000000000000000000000000000000000000..2742105b9493ef90255f67da8933194ad4dcd1d7 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/image_1.png" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/ip_addr.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/ip_addr.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..f1fe5307510e221a88e076ce3a987516481f0f37 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/ip_addr.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/net_model.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/net_model.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..052605c6760832f4d7395dd0855882f96ffa39bf Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/net_model.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/net_package.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/net_package.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..4ac488d17cb28a86de39cd03d5ce97e6a0e58ee3 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/net_package.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/socket/echo.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/socket/echo.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..cb45d18757951006a8d3ae5fe88032d1d3a7eb46 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/socket/echo.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/socket/socket_comunciation.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/socket/socket_comunciation.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..93580f672eb2553205c760e5b4aaafb681282315 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/socket/socket_comunciation.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..ef5d9749ec0c00ce9dde302a8a4b488b50214b5a Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_create01.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_create01.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..18200ff82375818ca0562db5d1caee5e9c08b9e3 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_create01.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_state.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_state.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..d997da998da8fcbe83070258fa35f3e58514d52d Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_state.jpg" differ diff --git "a/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_stop.jpg" "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_stop.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..350f3337b62ff14e7d73cb81c265655e1c96efc0 Binary files /dev/null and "b/docs/\347\275\221\347\273\234\347\274\226\347\250\213/images/tcp_stop.jpg" differ diff --git a/example/periphery/wifi/python/README.md b/example/periphery/wifi/python/README.md index 2d8930810c3a7f4288a983c88e7232f32b0babc6..1d5ac306f9cd6cb59c602b58ec2ac673c1119143 100644 --- a/example/periphery/wifi/python/README.md +++ b/example/periphery/wifi/python/README.md @@ -61,7 +61,7 @@ sequenceDiagram - **依赖库**:`lockzhiner_vision_module.cv2`(基于OpenCV)、`threading`、`socket`、`select`、`time` - **功能模块**: - **主线程**:创建TCP服务端监听,接收客户端连接,启动发送与接收子线程。 - - **发送线程(`stream_sender`)**:以固定帧率(默认30fps)读取摄像头,使用JPEG编码(质量75),通过非阻塞select检查发送缓冲区,丢弃过载帧,确保实时性。 + - **发送线程(`stream_sender`)**:以固定帧率读取摄像头,使用JPEG编码(质量75),通过非阻塞select检查发送缓冲区,丢弃过载帧,确保实时性。 - **接收线程(`client_receiver`)**:仅监测客户端连接状态,若客户端断开则设置停止事件,通知发送线程退出。 ### 2.2 客户端(接收端)