Build Your Own Redis with C/C++ 学习(二)

02. 套接字编程基础

前提条件:具备基本的网络知识。

2.1 核心概念:从黑盒到代码

计算机网络常被简化为线条连接的方框,但在实际编程中,我们需要处理更具体的细节。如果给你一个包含“发送”和“接收”两个方法的 API,你还需要了解什么?

1. TCP 字节流与协议 (The TCP Byte Stream)

人们常误以为网络交互就是节点间互相扔“消息包”。但实际上,最常用的 TCP 协议并不生产“消息”,它生产的是**连续的字节流 (Continuous Stream of Bytes)**。

  • 无边界:TCP 字节流内部没有天然的边界。
  • 应用层协议:解释这个字节流是应用层协议的工作。你需要制定规则来切分数据流,将其还原为有意义的消息。
  • 难点:在事件循环(Event Loop)中将字节流正确地拆分为消息,比解析静态文件要复杂得多。

2. 数据序列化 (Data Serialization)

网络只认识 01

  • **序列化 (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) - 服务端

告知操作系统准备接受连接。

  1. socket():获取句柄。
  2. bind():绑定 IP 和 端口。
  3. listen():开始监听。
  4. accept()阻塞等待,直到有新连接进来,返回一个新的“连接套接字”。

服务端伪代码:

1
2
3
4
5
6
7
fd = socket()
bind(fd, address)
listen(fd)
while True:
conn_fd = accept(fd) # 返回一个新的句柄用于通信
do_something_with(conn_fd)
close(conn_fd)

B. 连接套接字 (Connection Socket) - 客户端

由客户端发起连接。

  1. socket():获取句柄。
  2. connect():发起连接。

客户端伪代码:

1
2
3
4
fd = socket()
connect(fd, address)
do_something_with(fd)
close(fd)

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() 释放资源