Build Your Own Redis with C/C++ 学习(三)
Build Your Own Redis with C/C++ 学习(三)
kikock03. TCP 服务器与客户端开发指南
本章目标:熟悉 Socket API,编写一个基础的 TCP 回显服务器(Echo Server)和客户端。
⚠️ 注意:这里的代码虽然能跑,但仅仅是为了演示 API 的用法。真正的网络编程远不止调用 API 这么简单,完整的错误处理和架构设计将在后续章节展开。
3.1 先决条件 (Prerequisites)
1. 熟悉 Linux 环境
网络编程的原理通用,但 Windows/macOS 的系统调用细节差异很大。对于初学者,强烈推荐使用 Linux。
- 环境获取:VirtualBox 虚拟机、WSL (Windows Subsystem for Linux) 或云服务器 (VPS)。
- 必备技能:
- 文件操作:编辑、复制、移动、删除。
- 编译代码:使用
g++。不需要复杂的 Makefile。1
2g++ -Wall -Wextra -Og -g source.cpp -o program
./program
2. 基本编程技能 (C/C++)
虽然本项目主要使用 C 语言风格,但会用到少量的 C++ 特性(如 vector, string)以简化开发。
- 核心概念:数组、结构体、内存管理、指针。
- 调试技能:
printf():最朴素但有效的调试手段。assert():验证假设条件。strace:神器,用于跟踪程序执行的系统调用。gdb:调试崩溃(Core Dump)和查看堆栈。
- 动态数组:理解形如下面的结构:
1
struct MyString { char *data; size_t length; size_t capacity; };
3. 学会查阅文档 (Man Pages)
Linux 的手册页 (Man Pages) 是最权威的文档。
man socket.2:查看socket()系统调用(Section 2 代表系统调用)。man socket.7:查看套接字接口的综述(Section 7 代表杂项/协议)。- 提示:Man Pages 适合查阅细节,不适合入门学习。入门推荐阅读《Beej’s Guide to Network Programming》。
3.2 实战:编写 TCP 服务器 (Server)
我们将把上一章的伪代码转化为可运行的 C++ 代码。
步骤 1: 获取套接字句柄 (Socket Handle)
1 | int fd = socket(AF_INET, SOCK_STREAM, 0); |
AF_INET: IPv4。SOCK_STREAM: TCP 流。0: 默认协议。
步骤 2: 设置套接字选项 (Set Options)
1 | int val = 1; |
- **关键选项
SO_REUSEADDR**:必须设置为 1。 - 作用:允许服务器重启后立即绑定到同一个端口。如果不设置,重启时可能会报错 “Address already in use”,因为旧连接处于
TIME_WAIT状态。
步骤 3: 绑定地址 (Bind)
我们将服务器绑定到 0.0.0.0:1234。
1 | struct sockaddr_in addr = {}; |
- **字节序 (Endianness)**:
- **Little-endian (小端)**:低位字节在前 (Intel/AMD CPU)。
- **Big-endian (大端/网络字节序)**:高位字节在前。
- 转换函数:
htons()(Host to Network Short),htonl()(Host to Network Long)。网络传输必须使用大端序。
步骤 4: 监听 (Listen)
1 | rv = listen(fd, SOMAXCONN); |
SOMAXCONN: 监听队列的最大长度(Linux 上通常是 4096)。
步骤 5: 接受连接 (Accept)
1 | while (true) { |
步骤 6: 读写数据 (Read & Write)
1 | static void do_something(int connfd) { |
3.3 实战:编写 TCP 客户端 (Client)
客户端逻辑:连接 -> 发送 “hello” -> 读取响应 -> 退出。
1 | int fd = socket(AF_INET, SOCK_STREAM, 0); |
编译与运行:
1 | # 编译 |
3.4 深入 Socket API 细节
1. 奇怪的 struct sockaddr
Socket API 设计于几十年前,那时还没有 void* 这种通用指针的概念,也没有现代的泛型。
struct sockaddr: 一个通用的占位符,实际上毫无用处。struct sockaddr_in: IPv4 专用结构体(我们主要用这个)。struct sockaddr_in6: IPv6 专用结构体。- 用法:我们需要将
sockaddr_in强制类型转换为sockaddr*传给 API。
2. 系统调用 vs 库函数
- 在 Linux 上,socket 函数直接对应内核的 Syscalls。
getaddrinfo()是个特例:它不是系统调用,而是 libc 中的库函数。因为它涉及复杂的域名解析流程(读配置文件、DNS 查询等)。
3. 获取地址信息
如果你使用了通配符 IP 或动态端口,你可能不知道当前的连接地址。
getsockname(): 获取本地绑定的地址。getpeername(): 获取远程连接的地址。
4. Socket 与 IPC (进程间通信)
- Unix Domain Sockets: 用于同一台机器上进程间的高效通信。API 与网络 Socket 几乎一致,只是地址族不同 (
AF_UNIX),且不需要经过网卡协议栈。
5. 读写函数的变体
除了标准的 read/write,还有很多变体:
recv/send: 多了一个flags参数。readv/writev: 分散/聚集 I/O。可以一次性读写多个不连续的缓冲区(非常有用,后续可能会用到)。recvmsg/sendmsg: 最强大的变体,能控制所有细节。
下一步:
现在的服务器一次只能处理一个客户端,而且非常脆弱。下一章,我们将深入协议设计,学习如何处理 TCP 字节流的粘包与拆包问题。


