【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 应用服务器。

工作流程

plaintext 复制代码
客户端(浏览器) → 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 头部升级协议:

plaintext 复制代码
客户端请求:
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(默认状态)

    连接的初始状态,主要用于初始化和处理连接的基本读写操作。

    c 复制代码
    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。

    c 复制代码
    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,主要用于等待发送确认或准备发送数据。

    c 复制代码
    else if (conn_list[fd].status == 2) {
        set_event(fd, EPOLLOUT, 0);
    }

    在状态 2 下,不发送数据,仅设置事件为EPOLLOUT,监听可写事件。

  • http_response函数中状态机的应用

    http_response函数中,根据连接的状态分阶段发送 HTTP 响应。

    c 复制代码
    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 图片,通过状态机管理不同阶段的响应生成,可根据宏定义选择不同的响应模式。

cpp 复制代码
#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);

#endif
	return 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(默认状态)

      连接的初始状态,主要用于初始化和处理连接的基本读写操作。

      c 复制代码
      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。

      c 复制代码
      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,主要用于等待发送确认或准备发送数据。

      c 复制代码
      else if (conn_list[fd].status == 2) {
          set_event(fd, EPOLLOUT, 0);
      }

      在状态 2 下,不发送数据,仅设置事件为EPOLLOUT,监听可写事件。

    • http_response函数中状态机的应用

      http_response函数中,根据连接的状态分阶段发送 HTTP 响应。

      c 复制代码
      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
c++ 复制代码
#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-C5AB0DC85B11

SHA-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 GUID
            strcat(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

相关推荐
休息一下接着来18 分钟前
C++ I/O多路复用
linux·开发语言·c++
舰长11525 分钟前
ubuntu 安装mq
linux·运维·ubuntu
不是吧这都有重名35 分钟前
利用systemd启动部署在服务器上的web应用
运维·服务器·前端
liuyunluoxiao1 小时前
进程间通信--信号量【Linux操作系统】
linux
传知摩尔狮1 小时前
Linux 中断源码性能分析实战:从内核深处榨取每一丝性能
linux·运维·服务器
矿工学编程2 小时前
在宝塔中使用.NET环境管理部署 .NET Core项目
linux·.net
Roc-xb2 小时前
解决Ubuntu22.04 安装vmware tools之后,不能实现文件复制粘贴和拖拽问题
linux·ubuntu22.04·vmvare
JANYI20182 小时前
一文读懂-嵌入式Ubuntu平台
linux·运维·ubuntu
君鼎2 小时前
muduo库TcpServer模块详解
linux·网络·c++
开***能2 小时前
包装设备跨系统兼容:Profinet转Modbus TCP的热收缩包装机改造方案
服务器·网络·tcp/ip