【Linux高级全栈开发】2.1.3 http服务器的实现
【Linux高级全栈开发】2.1.3 http服务器的实现
高性能网络学习目录
基础内容(两周完成):
-
2.1网络编程
- 2.1.1多路复用select/poll/epoll
- 2.1.2事件驱动reactor
- 2.1.3http服务器的实现
-
2.2网络原理
- 百万并发
- PosixAPI
- QUIC
-
2.3协程库
- NtyCo的实现
-
2.4dpdk
- 用户态协议栈的实现
-
2.5高性能异步io机制
项目内容(两周完成):
- 9.1 KV存储项目
- 9.2 RPC项目
- 9.3 DPDK项目
2.1.3 http服务器的实现
1 基础知识
1.1 什么是webserver,websocket
1. WebServer(Web 服务器)
WebServer 是一种软件程序,用于处理客户端(如浏览器)的 HTTP 请求并返回响应。它是互联网的基础设施之一,主要功能包括:
- 静态资源服务:直接返回 HTML、CSS、JavaScript、图片等文件。
- 动态内容生成:通过后端脚本(如 PHP、Python、Java)生成动态内容。
- 请求路由:将不同 URL 的请求分发到对应的处理逻辑。
- 安全处理:处理 HTTPS 加密、访问控制、请求过滤等。
常见的 WebServer 软件:
- Nginx:高性能、轻量级,常用于反向代理和负载均衡。
- Apache HTTP Server:功能丰富,支持大量模块。
- Node.js(Express、Koa):基于 JavaScript 的服务器端框架。
- Tomcat:Java Web 应用服务器。
工作流程:
客户端(浏览器) → HTTP请求 → WebServer → 处理请求 → 返回HTTP响应 → 客户端
2. WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,由 RFC 6455 定义。它在 HTTP 协议基础上发展而来,但具有以下特点:
- 持久连接:一旦建立连接,客户端和服务器可以随时双向发送数据,无需频繁创建新连接。
- 低延迟:避免了 HTTP 请求 - 响应模式的额外开销。
- 二进制支持:支持发送二进制数据(如图片、视频)。
- 跨域支持:通过
Origin
头处理跨域请求。
应用场景:
- 实时聊天应用(如微信、Slack)
- 实时数据推送(如股票行情、通知)
- 在线游戏(如多人协作游戏)
- 实时协作工具(如 Google Docs)
1.2 WebSocket 的工作原理
1.2.1 握手阶段(Handshake)
WebSocket 连接通过 HTTP 请求发起,使用 Upgrade
头部升级协议:
客户端请求:
GET /ws HTTP/1.1
Host: example.com
Origin: http://localhost:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13服务器响应:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- 关键头:
Upgrade: websocket
:告知服务器升级到 WebSocket 协议。Sec-WebSocket-Key
:客户端生成的随机值,用于验证服务器合法性。Sec-WebSocket-Accept
:服务器根据客户端 Key 计算的响应值,确保双方理解 WebSocket 协议。
1.2.2 数据传输阶段
握手成功后,双方通过帧(Frame)格式传输数据:
- 文本帧:用于传输 UTF-8 编码的文本数据。
- 二进制帧:用于传输二进制数据(如图片、视频)。
- 控制帧:用于管理连接(如关闭连接、心跳检测)。
1.3 Reactor的IO事件触发——LT/ET
-
LT 和 ET 的核心区别
-
水平触发(LT)
-
触发条件:只要文件描述符(FD)处于就绪状态(如可读缓冲区有数据),就会持续触发事件。
-
特性:
- 事件会重复通知,直到应用程序处理完所有数据。
- 编程简单,不易遗漏事件(但可能导致不必要的系统调用)。
-
-
边缘触发(ET)
-
触发条件:仅在 FD 状态变化时触发一次(如数据从无到有)。
-
特性:
- 事件仅触发一次,必须一次性处理完所有数据(否则剩余数据不会再通知)。
- 要求应用程序使用非阻塞 I/O,并在事件触发后尽可能读 / 写完整数据。
-
-
-
epoll同时支持 LT 和 ET:默认是 LT 模式,通过
EPOLLET
标志可启用 ET 模式。 -
为什么 ET 模式要求非阻塞 I/O?
-
阻塞 I/O 与 ET 的矛盾:
- 若使用阻塞 I/O,当应用程序在 ET 模式下读取数据时,若数据未读完,线程会被阻塞在
read
调用中。 - 此时内核认为应用程序正在处理数据,不会再次触发事件,导致剩余数据被 “饿死”。
- 若使用阻塞 I/O,当应用程序在 ET 模式下读取数据时,若数据未读完,线程会被阻塞在
-
正确做法:
- 设置 FD 为非阻塞模式(如
fcntl(fd, F_SETFL, O_NONBLOCK)
)。- 在事件触发后,循环读取 / 写入数据,直到返回
EAGAIN
(表示缓冲区已空 / 满)。
- 在事件触发后,循环读取 / 写入数据,直到返回
- 设置 FD 为非阻塞模式(如
-
场景 | LT 模式 | ET 模式 |
---|---|---|
编程复杂度 | 低(无需循环处理) | 高(必须循环处理 + 非阻塞 I/O) |
性能 | 中等(可能有冗余通知) | 高(减少系统调用次数) |
适用场景 | 简单应用(如小规模连接) | 高性能服务器(如 Nginx、Redis) |
数据处理要求 | 可部分处理数据 | 必须一次性处理完所有数据 |
-
适用场景:
-
水平触发 (LT) :
1.1 简单应用与小规模连接
- 特点:连接数较少,业务逻辑简单,无需极致性能优化。
- 示例:
- 小型 Web 服务器(如个人博客)。
- 内部系统 API 服务(连接数通常在数百以内)。
- 优势:编程模型简单,无需复杂的循环读取逻辑,降低开发难度。
1.2 数据处理不及时的场景
- 特点:应用程序可能无法立即处理所有数据,需要多次读取。
- 示例:
- 数据处理逻辑复杂,单次处理耗时较长。
- 依赖外部资源(如数据库、文件系统)的响应。
- 优势:LT 模式会持续通知,确保数据不会丢失。
1.3 阻塞 I/O 场景
- 特点:应用程序使用阻塞 I/O,无法在一次调用中处理完所有数据。
- 示例:
- 基于同步编程模型的框架(如早期的 Python Flask)。
- 使用标准库阻塞 API 的应用。
- 优势:LT 模式允许分多次读取数据,避免线程永久阻塞。
1.4 资源受限的环境
- 特点:系统内存或 CPU 资源有限,无法支持复杂的 ET 模式实现。
- 示例:
- 嵌入式设备或低配置服务器。
- 单核 CPU 环境下的单线程应用。
- 优势:LT 模式的简单实现可减少系统资源消耗。
-
**边缘触发 (ET) **
边缘触发模式下,仅在文件描述符状态变化时触发一次事件,要求应用程序必须一次性处理完所有数据。这种模式适合以下场景:
2.1 高性能服务器与大规模并发
- 特点:需要处理大量并发连接(如数万至数百万),追求极致性能。
- 示例:
- 大型 Web 服务器(如 Nginx、Apache)。
- 实时消息系统(如 MQTT 服务器)。
- 数据库中间件(如 ProxySQL)。
- 优势:减少 epoll_wait 的系统调用次数,降低内核与用户空间的切换开销。
2.2 非阻塞 I/O 与事件驱动架构
- 特点:应用程序使用非阻塞 I/O,并基于事件驱动模型构建。
- 示例:
- 使用 libevent、libev 等事件库的应用。
- Node.js、Netty 等异步编程框架。
- 优势:ET 模式与非阻塞 I/O 完美配合,避免不必要的事件通知。
2.3 数据处理快速且完整的场景
- 特点:应用程序能够在短时间内处理完所有数据。
- 示例:
- 数据转发代理(如 TCP/UDP 转发)。
- 简单协议解析(如 Redis 的 RESP 协议)。
- 优势:一次性读取所有数据,减少系统调用次数。
2.4 低延迟与高吞吐量需求
- 特点:对响应时间和吞吐量有严格要求。
- 示例:
- 高频交易系统。
- 实时游戏服务器。
- 优势:ET 模式通过减少事件通知次数,降低系统开销,提升整体性能。
3. 典型案例对比
应用场景 推荐模式 理由 Nginx 反向代理 ET 需处理数万并发连接,ET 模式可减少系统调用,提升吞吐量。 Redis 内存数据库 ET 单线程处理大量请求,ET 模式配合非阻塞 I/O 可最大化性能。 小型 Python Web 应用 LT 基于同步模型,使用阻塞 I/O,LT 模式更易实现。 实时视频流服务器 ET 需快速处理大量数据,避免缓冲区堆积,ET 模式适合一次性读取完整帧数据。 企业内部管理系统 LT 连接数少,业务逻辑复杂,LT 模式降低开发难度。 4. 选择建议
- 优先使用 LT:除非明确需要极致性能,且有足够经验处理 ET 模式的复杂性。
- ET 模式注意事项:
- 必须使用非阻塞 I/O,并在事件触发后循环读取 / 写入数据直至返回
EAGAIN
。 - 对编程能力要求较高,需谨慎处理边界情况(如半关闭连接、超时)。
- 必须使用非阻塞 I/O,并在事件触发后循环读取 / 写入数据直至返回
- 混合模式:在同一应用中可根据不同 FD 的特性混合使用 LT 和 ET 模式(如监听套接字用 LT,数据套接字用 ET)。
-
1.4 HTTP协议
HTTP 是一个基于 TCP/IP 通信协议,在TCP连接,socket连接的基础上来传递数据的协议(首先要建立tcp连接)
- HTTP 是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
客户端请求信息
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 0penssL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
HTTP1.0 定义了三种请求方法:
- GET,
- POST 和
- HEAD 方法。
HTTP1.1 新增了六种请求方法:
- OPTIONS、
- PUT、
- PATCH、
- DELETE、
- TRACE 和
- CONNECT 方法。
服务器响应信息
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
1.5 有限状态机fsm解析http
使用状态机来管理连接的不同状态,实现对连续数据的分阶段发送
-
状态 0(默认状态):
连接的初始状态,主要用于初始化和处理连接的基本读写操作。else if (conn_list[fd].status == 0) {if (conn_list[fd].wlength != 0) {count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);}set_event(fd, EPOLLIN, 0); }
在状态 0 下,如果写缓冲区有数据,则发送数据。然后设置事件为
EPOLLIN
,监听可读事件。 -
状态 1(主动发送数据):
当需要主动发送数据时,连接进入状态 1。if (conn_list[fd].status == 1) {count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);set_event(fd, EPOLLOUT, 0); }
在状态 1 下,发送写缓冲区中的数据,然后设置事件为
EPOLLOUT
,继续监听可写事件,以便处理可能的剩余数据。 -
状态 2(等待发送确认或准备发送):
连接在某些情况下进入状态 2,主要用于等待发送确认或准备发送数据。else if (conn_list[fd].status == 2) {set_event(fd, EPOLLOUT, 0); }
在状态 2 下,不发送数据,仅设置事件为
EPOLLOUT
,监听可写事件。 -
在
http_response
函数中状态机的应用:
在http_response
函数中,根据连接的状态分阶段发送 HTTP 响应。if (c->status == 0) {c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Accept-Ranges: bytes\r\n""Content-Length: %ld\r\n""Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", stat_buf.st_size);c->status = 1; } else if (c->status == 1) {int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);if (ret == -1) {printf("errno: %d\n", errno);}c->status = 2; } else if (c->status == 2) {c->wlength = 0;memset(c->wbuffer, 0, BUFFER_LENGTH);c->status = 0; }
- 状态 0:构造 HTTP 响应头,并切换到状态 1。
- 状态 1:使用
sendfile
发送文件内容,并切换到状态 2。 - 状态 2:重置缓冲区和状态,切换回状态 0。
1.6 ftp协议
2 「代码实现」HTTP服务器
2.1 「WebServer」实现过程
核心逻辑:代码实现了一个简易 HTTP 服务器的请求处理和响应生成功能,支持返回静态 HTML 页面或 PNG 图片,通过状态机管理不同阶段的响应生成,可根据宏定义选择不同的响应模式。
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/sendfile.h>
#include <errno.h>#include "server.h"// 定义Web服务器的根目录
#define WEBSERVER_ROOTDIR "./"/*** 处理HTTP请求* * 解析客户端发送的HTTP请求,但当前实现为空,* 仅清空发送缓冲区并重置连接状态为0* * @param c 连接结构体指针,包含请求和响应信息* @return 始终返回0*/
int http_request(struct conn *c) {// 打印请求内容(调试用,当前注释掉)//printf("request: %s\n", c->rbuffer);// 清空发送缓冲区并重置长度memset(c->wbuffer, 0, BUFFER_LENGTH);c->wlength = 0;// 设置连接状态为0(初始状态)c->status = 0;
}/*** 生成HTTP响应* * 根据预定义的宏选择不同的响应模式:* 1. 直接返回固定HTML页面* 2. 读取index.html文件并返回* 3. 使用sendfile系统调用分阶段发送文件* 4. 发送PNG图片文件* * @param c 连接结构体指针,包含请求和响应信息* @return 响应数据的长度*/
int http_response(struct conn *c) {
#if 1// 模式1:直接返回固定HTML页面c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Accept-Ranges: bytes\r\n""Content-Length: 82\r\n""Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n""<html><head><title>0voice.king</title></head><body><h1>King</h1></body></html>\r\n\r\n");
#elif 0// 模式2:读取index.html文件并返回int filefd = open("index.html", O_RDONLY);struct stat stat_buf;fstat(filefd, &stat_buf);// 构造HTTP响应头c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Accept-Ranges: bytes\r\n""Content-Length: %ld\r\n""Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", stat_buf.st_size);// 读取文件内容到缓冲区int count = read(filefd, c->wbuffer + c->wlength, BUFFER_LENGTH - c->wlength);c->wlength += count;close(filefd);#elif 0// 模式3:使用sendfile系统调用分阶段发送文件int filefd = open("index.html", O_RDONLY);struct stat stat_buf;fstat(filefd, &stat_buf);if (c->status == 0) {// 状态0:构造HTTP响应头c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Accept-Ranges: bytes\r\n""Content-Length: %ld\r\n""Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", stat_buf.st_size);c->status = 1;} else if (c->status == 1) {// 状态1:使用sendfile发送文件内容int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);if (ret == -1) {printf("errno: %d\n", errno);}c->status = 2;} else if (c->status == 2) {// 状态2:重置缓冲区和状态c->wlength = 0;memset(c->wbuffer, 0, BUFFER_LENGTH);c->status = 0;}close(filefd);#else// 模式4:发送PNG图片文件(与模式3类似,但发送图片)int filefd = open("c1000k.png", O_RDONLY);struct stat stat_buf;fstat(filefd, &stat_buf);if (c->status == 0) {// 状态0:构造HTTP响应头(Content-Type为image/png)c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n""Content-Type: image/png\r\n""Accept-Ranges: bytes\r\n""Content-Length: %ld\r\n""Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", stat_buf.st_size);c->status = 1;} else if (c->status == 1) {// 状态1:使用sendfile发送图片内容int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);if (ret == -1) {printf("errno: %d\n", errno);}c->status = 2;} else if (c->status == 2) {// 状态2:重置缓冲区和状态c->wlength = 0;memset(c->wbuffer, 0, BUFFER_LENGTH);c->status = 0;}close(filefd);#endifreturn c->wlength;
}
函数解释:
- http_request(struct conn *c)
- 功能:处理客户端发送的 HTTP 请求。
- 当前实现:仅清空发送缓冲区并重置连接状态,未实际解析请求内容。
- 设计意图:为后续扩展请求解析逻辑预留接口。
- http_response(struct conn *c)
- 功能:生成 HTTP 响应并填充到发送缓冲区。
- 实现模式:
- 模式 1:直接返回固定 HTML 页面,适合快速测试。
- 模式 2:读取文件内容到缓冲区并返回,简单但受缓冲区大小限制。
- 模式 3:使用
sendfile
系统调用,高效地将文件内容直接发送到套接字,避免用户空间拷贝。 - 模式 4:与模式 3 类似,但专门用于发送 PNG 图片。
- 状态机:通过
c->status
管理响应生成的不同阶段,确保头信息和内容分开发送。
-
sendfile
是一个高效的系统调用,用于在文件描述符之间直接传输数据,避免了用户空间和内核空间之间的数据拷贝,从而显著提高了传输效率。#include <sys/sendfile.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
参数说明:
out_fd
:输出文件描述符(如 socket)。in_fd
:输入文件描述符(如打开的文件),必须支持mmap
(如普通文件)。offset
:文件读取的起始位置(若为NULL
则从当前位置开始)。count
:传输的最大字节数。
-
模式3使用状态机来管理连接的不同状态,实现对连续数据的分阶段发送
-
状态 0(默认状态):
连接的初始状态,主要用于初始化和处理连接的基本读写操作。else if (conn_list[fd].status == 0) {if (conn_list[fd].wlength != 0) {count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);}set_event(fd, EPOLLIN, 0); }
在状态 0 下,如果写缓冲区有数据,则发送数据。然后设置事件为
EPOLLIN
,监听可读事件。 -
状态 1(主动发送数据):
当需要主动发送数据时,连接进入状态 1。if (conn_list[fd].status == 1) {count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);set_event(fd, EPOLLOUT, 0); }
在状态 1 下,发送写缓冲区中的数据,然后设置事件为
EPOLLOUT
,继续监听可写事件,以便处理可能的剩余数据。 -
状态 2(等待发送确认或准备发送):
连接在某些情况下进入状态 2,主要用于等待发送确认或准备发送数据。else if (conn_list[fd].status == 2) {set_event(fd, EPOLLOUT, 0); }
在状态 2 下,不发送数据,仅设置事件为
EPOLLOUT
,监听可写事件。 -
在
http_response
函数中状态机的应用:
在http_response
函数中,根据连接的状态分阶段发送 HTTP 响应。if (c->status == 0) {c->wlength = sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Accept-Ranges: bytes\r\n""Content-Length: %ld\r\n""Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n", stat_buf.st_size);c->status = 1; } else if (c->status == 1) {int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);if (ret == -1) {printf("errno: %d\n", errno);}c->status = 2; } else if (c->status == 2) {c->wlength = 0;memset(c->wbuffer, 0, BUFFER_LENGTH);c->status = 0; }
- 状态 0:构造 HTTP 响应头,并切换到状态 1。
- 状态 1:使用
sendfile
发送文件内容,并切换到状态 2。 - 状态 2:重置缓冲区和状态,切换回状态 0。
-
-
如何在简历里写你的webserver项目,
- 并发量
- QPS(使用wrk工具测试)
sudo wrk -c 50 -d 10s -t 10 http://192.168.21.129:2000
- 问题:为什么我的QPS只到1000多,是因为使用了非阻塞的io吗
2.2 「WebSocket」实现过程
核心逻辑: 代码实现了一个基于 OpenSSL 的 WebSocket 协议通信模块,包含握手、数据帧编解码功能,支持不同长度的消息处理及掩码操作。
- 首先记得安装
openssl
开发库:sudo apt install libssl-dev
#include <stdio.h>
#include <string.h>#include "server.h"#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>// WebSocket GUID常量,用于握手阶段计算Sec-WebSocket-Accept
#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"/*
key: "fUNa6rJwr4/VDpwcgvceYA=="
fUNa6rJwr4/VDpwcgvceYA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11SHA-1计算后得到20字节结果,再进行base64编码WebSocket协议处理流程:
1. Handshark: 完成HTTP升级握手
2. Transmission: 数据传输阶段- decode: 解码客户端消息- encode: 编码服务端响应
*/// WebSocket数据帧头部结构定义 - 基础头部
struct _nty_ophdr {unsigned char opcode:4, // 操作码(4位): 0x01表示文本帧,0x08表示关闭帧rsv3:1, // 保留位3(1位)rsv2:1, // 保留位2(1位)rsv1:1, // 保留位1(1位)fin:1; // 结束标志(1位): 1表示当前为最后一帧unsigned char payload_length:7, // 负载长度(7位): 0-125表示实际长度,126/127有特殊含义mask:1; // 掩码标志(1位): 1表示数据经过掩码处理(客户端发送必须置1)
} __attribute__ ((packed));// WebSocket数据帧头部结构定义 - 长度为126(2字节)的扩展头部
struct _nty_websocket_head_126 {unsigned short payload_length; // 实际负载长度(2字节)char mask_key[4]; // 掩码密钥(4字节)unsigned char data[8]; // 数据起始位置
} __attribute__ ((packed));// WebSocket数据帧头部结构定义 - 长度为127(8字节)的扩展头部
struct _nty_websocket_head_127 {unsigned long long payload_length; // 实际负载长度(8字节)char mask_key[4]; // 掩码密钥(4字节)unsigned char data[8]; // 数据起始位置
} __attribute__ ((packed));// 类型重定义,方便后续使用
typedef struct _nty_websocket_head_127 nty_websocket_head_127;
typedef struct _nty_websocket_head_126 nty_websocket_head_126;
typedef struct _nty_ophdr nty_ophdr;/*** 功能: Base64编码函数* 参数:* - in_str: 输入字符串* - in_len: 输入字符串长度* - out_str: 输出编码结果* 返回值: 编码后的字符串长度,失败返回-1*/
int base64_encode(char *in_str, int in_len, char *out_str) { BIO *b64, *bio; BUF_MEM *bptr = NULL; size_t size = 0; if (in_str == NULL || out_str == NULL) return -1; // 创建Base64编码的BIO链b64 = BIO_new(BIO_f_base64()); bio = BIO_new(BIO_s_mem()); bio = BIO_push(b64, bio);// 写入数据并刷新BIO_write(bio, in_str, in_len); BIO_flush(bio); // 获取内存指针并复制结果BIO_get_mem_ptr(bio, &bptr); memcpy(out_str, bptr->data, bptr->length); out_str[bptr->length-1] = '\0'; size = bptr->length; // 释放资源BIO_free_all(bio); return size;
}/*** 功能: 从缓冲区读取一行数据(以\r\n结尾)* 参数:* - allbuf: 完整缓冲区* - level: 起始位置* - linebuf: 输出的行数据* 返回值: 下一行的起始位置,失败返回-1*/
int readline(char* allbuf, int level, char* linebuf) { int len = strlen(allbuf); for (; level < len; ++level) { if (allbuf[level] == '\r' && allbuf[level+1] == '\n') return level + 2; else *(linebuf++) = allbuf[level]; } return -1;
}/*** 功能: 应用掩码解密/加密数据(WebSocket协议要求客户端发送的数据必须掩码)* 参数:* - data: 待处理数据* - len: 数据长度* - mask: 掩码密钥(4字节)*/
void demask(char *data, int len, char *mask) { int i; for (i = 0; i < len; i++) *(data + i) ^= *(mask + (i % 4)); // 异或操作实现加解密(掩码操作可逆)
}/*** 功能: 解码WebSocket数据帧* 参数:* - stream: 输入数据流* - mask: 输出的掩码密钥* - length: 流长度* - ret: 输出的负载长度* 返回值: 指向负载数据的指针*/
char* decode_packet(unsigned char *stream, char *mask, int length, int *ret) {nty_ophdr *hdr = (nty_ophdr*)stream;unsigned char *data = stream + sizeof(nty_ophdr);int size = 0;int start = 0;int i = 0;// 根据payload_length字段的值判断扩展头部类型if ((hdr->mask & 0x7F) == 126) {// 情况1: 负载长度为126,表示实际长度存储在接下来的2字节中nty_websocket_head_126 *hdr126 = (nty_websocket_head_126*)data;size = hdr126->payload_length;// 提取掩码密钥for (i = 0; i < 4; i++) {mask[i] = hdr126->mask_key[i];}start = 8; // 数据起始位置(基础头部+扩展头部)} else if ((hdr->mask & 0x7F) == 127) {// 情况2: 负载长度为127,表示实际长度存储在接下来的8字节中nty_websocket_head_127 *hdr127 = (nty_websocket_head_127*)data;size = hdr127->payload_length;// 提取掩码密钥for (i = 0; i < 4; i++) {mask[i] = hdr127->mask_key[i];}start = 14; // 数据起始位置(基础头部+扩展头部)} else {// 情况3: 负载长度在0-125之间,表示实际长度直接存储在payload_length字段中size = hdr->payload_length;// 提取掩码密钥memcpy(mask, data, 4);start = 6; // 数据起始位置(基础头部+掩码)}*ret = size; // 返回负载长度demask(stream + start, size, mask); // 应用掩码解密数据return stream + start; // 返回指向负载数据的指针
}/*** 功能: 编码WebSocket数据帧* 参数:* - buffer: 输出缓冲区* - mask: 掩码密钥* - stream: 输入的负载数据* - length: 负载长度* 返回值: 编码后的总长度*/
int encode_packet(char *buffer, char *mask, char *stream, int length) {nty_ophdr head = {0};head.fin = 1; // 设置FIN标志为1,表示这是最后一帧head.opcode = 1; // 设置操作码为1,表示文本帧int size = 0;// 根据负载长度选择合适的头部格式if (length < 126) {// 情况1: 负载长度小于126,直接使用基础头部head.payload_length = length;memcpy(buffer, &head, sizeof(nty_ophdr));size = 2; // 基础头部长度} else if (length < 0xffff) {// 情况2: 负载长度在126-65535之间,使用2字节扩展头部nty_websocket_head_126 hdr = {0};hdr.payload_length = length;memcpy(hdr.mask_key, mask, 4); // 设置掩码密钥// 构建完整头部memcpy(buffer, &head, sizeof(nty_ophdr));memcpy(buffer + sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_126));size = sizeof(nty_websocket_head_126);} else {// 情况3: 负载长度大于65535,使用8字节扩展头部nty_websocket_head_127 hdr = {0};hdr.payload_length = length;memcpy(hdr.mask_key, mask, 4); // 设置掩码密钥// 构建完整头部memcpy(buffer, &head, sizeof(nty_ophdr));memcpy(buffer + sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_127));size = sizeof(nty_websocket_head_127);}// 复制负载数据到缓冲区memcpy(buffer + 2, stream, length);return length + 2; // 返回总长度
}// WebSocket握手阶段使用的密钥长度常量
#define WEBSOCK_KEY_LENGTH 19/*** 功能: 处理WebSocket握手请求* 参数:* - c: 连接结构体指针* 返回值: 0表示成功*/
int handshark(struct conn *c) {char linebuf[1024] = {0};int idx = 0;char sec_data[128] = {0};char sec_accept[32] = {0};// 逐行解析HTTP请求头do {memset(linebuf, 0, 1024);idx = readline(c->rbuffer, idx, linebuf);// 查找Sec-WebSocket-Key字段if (strstr(linebuf, "Sec-WebSocket-Key")) {// 格式示例: Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==// 拼接WebSocket GUIDstrcat(linebuf, GUID);// 计算SHA-1哈希SHA1(linebuf + WEBSOCK_KEY_LENGTH, strlen(linebuf + WEBSOCK_KEY_LENGTH), sec_data);// 对SHA-1结果进行Base64编码base64_encode(sec_data, strlen(sec_data), sec_accept);// 构建WebSocket握手响应memset(c->wbuffer, 0, BUFFER_LENGTH); c->wlength = sprintf(c->wbuffer, "HTTP/1.1 101 Switching Protocols\r\n""Upgrade: websocket\r\n""Connection: Upgrade\r\n""Sec-WebSocket-Accept: %s\r\n\r\n", sec_accept);printf("ws response : %s\n", c->wbuffer);break;}// 循环直到遇到空行(表示HTTP头结束)} while ((c->rbuffer[idx] != '\r' || c->rbuffer[idx+1] != '\n') && idx != -1);return 0;
}/*** 功能: 处理WebSocket请求* 参数:* - c: 连接结构体指针* 返回值: 0表示成功*/
int ws_request(struct conn *c) {printf("request: %s\n", c->rbuffer);// 根据连接状态进行不同处理if (c->status == 0) {// 状态0: 初始状态,处理握手请求handshark(c);c->status = 1; // 更新状态为已握手} else if (c->status == 1) {// 状态1: 已握手,处理数据帧char mask[4] = {0};int ret = 0;// 解码数据帧c->payload = decode_packet(c->rbuffer, c->mask, c->rlength, &ret);printf("data : %s , length : %d\n", c->payload, ret);c->wlength = ret; // 设置响应长度c->status = 2; // 更新状态为待响应}return 0;
}/*** 功能: 处理WebSocket响应* 参数:* - c: 连接结构体指针* 返回值: 0表示成功*/
int ws_response(struct conn *c) {if (c->status == 2) {// 状态2: 待响应,编码并发送响应c->wlength = encode_packet(c->wbuffer, c->mask, c->payload, c->wlength);c->status = 1; // 更新状态为已握手,继续等待下一个请求}return 0;
}
-
代码实现了 WebSocket 协议的核心功能,主要包括:
- 数据结构:定义了三种数据帧头部结构,分别处理不同长度的消息
- Base64 编码:提供了将二进制数据转换为 Base64 字符串的功能
- 数据帧编解码:
- 解码函数:解析 WebSocket 数据帧头部,提取掩码和负载长度,并应用掩码解密数据
- 编码函数:根据负载长度构建合适的头部,应用掩码并编码数据
- 握手处理:实现了 WebSocket 协议的 HTTP 升级握手过程,包括:
- 解析客户端发送的 Sec-WebSocket-Key
- 计算 SHA-1 哈希和 Base64 编码
- 构建并返回握手响应
- 请求 / 响应处理:基于状态机实现 WebSocket 通信的状态管理和消息处理
-
编译时记得链接
openssl
库,sudo gcc reactor.c webserver.c websocket.c -o websocket -lssl -lcrypto
下一章:2.2.1 Posix API与网络协议栈
https://github.com/0voice