当前位置: 首页 > news >正文

【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 调用中。
      • 此时内核认为应用程序正在处理数据,不会再次触发事件,导致剩余数据被 “饿死”。
    • 正确做法:

      • 设置 FD 为非阻塞模式(如 fcntl(fd, F_SETFL, O_NONBLOCK))。
        • 在事件触发后,循环读取 / 写入数据,直到返回 EAGAIN(表示缓冲区已空 / 满)。
场景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
      • 对编程能力要求较高,需谨慎处理边界情况(如半关闭连接、超时)。
    • 混合模式:在同一应用中可根据不同 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;
}
函数解释:
  1. http_request(struct conn *c)
    • 功能:处理客户端发送的 HTTP 请求。
    • 当前实现:仅清空发送缓冲区并重置连接状态,未实际解析请求内容。
    • 设计意图:为后续扩展请求解析逻辑预留接口。
  2. 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 协议的核心功能,主要包括:

    1. 数据结构:定义了三种数据帧头部结构,分别处理不同长度的消息
    2. Base64 编码:提供了将二进制数据转换为 Base64 字符串的功能
    3. 数据帧编解码:
      • 解码函数:解析 WebSocket 数据帧头部,提取掩码和负载长度,并应用掩码解密数据
      • 编码函数:根据负载长度构建合适的头部,应用掩码并编码数据
    4. 握手处理:实现了 WebSocket 协议的 HTTP 升级握手过程,包括:
      • 解析客户端发送的 Sec-WebSocket-Key
      • 计算 SHA-1 哈希和 Base64 编码
      • 构建并返回握手响应
    5. 请求 / 响应处理:基于状态机实现 WebSocket 通信的状态管理和消息处理
  • 编译时记得链接 openssl 库,sudo gcc reactor.c webserver.c websocket.c -o websocket -lssl -lcrypto

下一章:2.2.1 Posix API与网络协议栈

https://github.com/0voice

相关文章:

  • ngx_http_proxy_protocol_vendor_module 模块
  • FreeSWITCH 简单图形化界面43 - 使用百度的unimrcp搞个智能话务台,用的在线的ASR和TTS
  • STM32SPI通信基础及CubeMX配置
  • 从零开始实现大语言模型(十五):并行计算与分布式机器学习
  • symfonos: 1靶场
  • 算法第21天 | 第77题. 组合、216. 组合总和 III、17. 电话号码的字母组合
  • React方向:react的基本语法-数据渲染
  • API 玩出新花样:我如何构建自己的智能翻译助手
  • 08 Nginx模块
  • 【Docker】Docker Compose方式搭建分布式协调服务(Zookeeper)集群
  • Text2SQL:自助式数据报表开发---0517
  • Java求职者面试:从Spring Boot到微服务的技术点解析
  • 【GESP】C++三级真题 luogu-B3925 [GESP202312 三级] 小猫分鱼
  • 【PostgreSQL系列】PostgreSQL 复制参数详解
  • MLLM常见概念通俗解析(四)
  • 项目的部署发布和访问的流程
  • Jsoup库和Apache HttpClient库有什么区别?
  • 嵌入式学习笔记 - U(S)ART 模块HAL 库函数总结
  • [C++面试] const相关面试题
  • C# 深入理解类(成员常量)
  • 盲人不能刷脸认证、营业厅拒人工核验,央媒:别让刷脸困住尊严
  • 持续降雨存在落石风险,贵州黄果树景区水帘洞将封闭至6月初
  • 圆桌丨全球化博弈与人工智能背景下,企业如何落地合规体系
  • 美联储官员:美国经济增速可能放缓,现行关税政策仍将导致物价上涨
  • 刘小涛任江苏省委副书记
  • 一涉嫌开设赌场的网上在逃人员在山东威海落网