Build Your Own Redis with C/C++ 学习(二)
Build Your Own Redis with C/C++ 学习(二)
kikock02. 套接字编程基础
前提条件:具备基本的网络知识。
2.1 核心概念:从黑盒到代码
计算机网络常被简化为线条连接的方框,但在实际编程中,我们需要处理更具体的细节。如果给你一个包含“发送”和“接收”两个方法的 API,你还需要了解什么?
1. TCP 字节流与协议 (The TCP Byte Stream)
人们常误以为网络交互就是节点间互相扔“消息包”。但实际上,最常用的 TCP 协议并不生产“消息”,它生产的是**连续的字节流 (Continuous Stream of Bytes)**。
- 无边界:TCP 字节流内部没有天然的边界。
- 应用层协议:解释这个字节流是应用层协议的工作。你需要制定规则来切分数据流,将其还原为有意义的消息。
- 难点:在事件循环(Event Loop)中将字节流正确地拆分为消息,比解析静态文件要复杂得多。
2. 数据序列化 (Data Serialization)
网络只认识 0 和 1。
- **序列化 (Serialization)**:将高级对象(如字符串、结构体、列表)转换为字节的过程。
- **反序列化 (Deserialization)**:将字节还原为对象的过程。
- 虽然可以使用 JSON 或 Protobuf 等现成库,但通过操作位和字节手动实现序列化是学习底层编程的绝佳起点。
3. 并发编程 (Concurrent Programming)
有了协议规范,写客户端很容易,但写服务器很难,因为服务器必须处理多连接。
- C10K 问题:历史上,同时处理 1 万个并发连接(即使大部分是空闲的)是一个巨大的难题。
- 现代解决方案:基于事件的并发 (Event-based Concurrency) 和 **事件循环 (Event Loops)**。
- 这是驱动 NGINX、Redis、Node.js 和 Go 运行时的核心机制。虽然复杂,但必须通过实践来掌握。
2.2 程序员眼中的网络模型
1. 协议分层 (Layers)
网络协议是分层的,下层作为上层的载体,上层增加新的功能。比起 OSI 模型,我们更关注简化的 TCP/IP 功能模型:
| 层级 | 协议 | 功能 | 说明 |
|---|---|---|---|
| 高层 | TCP | 可靠且有序的字节 | 解决丢包、乱序问题,提供稳定的数据流。 |
| 中层 | Port | 多路复用 (Multiplexing) | 使用 16 位端口号区分同一台机器上的不同应用程序。 |
| 底层 | IP | 小型离散消息 | 负责寻址(源 IP -> 目标 IP),只能处理小块数据包。 |
- 四元组:计算机使用
(源IP, 源端口, 目标IP, 目标端口)来唯一标识一个信息流。 - 我们关注的层:作为应用开发者,我们主要关注 IP 层之上。我们将像 Redis 一样,直接在 TCP 之上构建自己的协议。
2. 请求-响应模型 (Request-Response)
Redis、HTTP/1.1 和大多数 RPC 都是请求-响应协议。
- 每个请求必须对应一个响应。
- 为了确保请求和响应能正确配对,必须依赖 TCP 提供的可靠性和顺序性(DNS 是个例外)。
3. 数据包 (Packet) vs 流 (Stream)
- UDP:基于数据包。功能少,不可靠,无序。
- TCP:基于字节流。可靠,有序。
- 兼容性:TCP 和 UDP 的语义是不兼容的。在开发网络应用的第一步,就必须决定使用哪一个。大多数应用(包括 Redis)为了简便都选择 TCP。
2.3 套接字原语 (Socket Primitives)
虽然我们在 Linux 上编码,但这些概念是跨平台的。
1. 什么是套接字 (Socket)?
- 定义:Socket 是一个**句柄 (Handle)**,用于引用连接。
- **句柄/文件描述符 (fd)**:一个不透明的整数,用于指代跨越 API 边界的资源。在 Linux 中,它被称为文件描述符。它与磁盘文件无关,只是名字相近。
- 生命周期:
socket()分配句柄,使用完毕后必须调用close()释放资源。
2. 两种类型的套接字
socket() 创建时是无类型的,其角色由后续调用决定:
A. 监听套接字 (Listening Socket) - 服务端
告知操作系统准备接受连接。
socket():获取句柄。bind():绑定 IP 和 端口。listen():开始监听。accept():阻塞等待,直到有新连接进来,返回一个新的“连接套接字”。
服务端伪代码:
1 | fd = socket() |
B. 连接套接字 (Connection Socket) - 客户端
由客户端发起连接。
socket():获取句柄。connect():发起连接。
客户端伪代码:
1 | fd = socket() |
3. 读与写 (Read and Write)
尽管 TCP (流) 和 UDP (包) 服务不同,但它们共用一套 API。
read()/recv()write()/send()- 注意:在 Linux 上,
send/recv只是通用的read/write系统调用的变体。虽然 API 相同,但同一段代码很难同时兼容 TCP 和 UDP。
总结:核心 API 清单
| 操作 | TCP 服务端 | TCP 客户端 | 描述 |
|---|---|---|---|
| 初始化 | socket() |
socket() |
创建套接字句柄 |
| 准备 | bind(), listen() |
- | 绑定端口并监听 |
| 建立连接 | accept() |
connect() |
服务端接受,客户端发起 |
| 数据传输 | read(), write() |
read(), write() |
发送和接收数据 |
| 结束 | close() |
close() |
释放资源 |


