【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

相关推荐
知白守黑26715 分钟前
Linux磁盘阵列
linux·运维·服务器
维尔切1 小时前
Linux中基于Centos7使用lamp架构搭建个人论坛(wordpress)
linux·运维·架构
tan77º2 小时前
【项目】分布式Json-RPC框架 - 项目介绍与前置知识准备
linux·网络·分布式·网络协议·tcp/ip·rpc·json
TPBoreas3 小时前
Jenkins启动端口修改失败查找日志
运维·服务器·jenkins
正在努力的小河5 小时前
Linux设备树简介
linux·运维·服务器
荣光波比5 小时前
Linux(十一)——LVM磁盘配额整理
linux·运维·云计算
小张快跑。5 小时前
Tomcat下载、安装及配置详细教程
java·服务器·tomcat
LLLLYYYRRRRRTT5 小时前
WordPress (LNMP 架构) 一键部署 Playbook
linux·架构·ansible·mariadb
轻松Ai享生活6 小时前
crash 进程分析流程图
linux
没有不重的名么6 小时前
Tmux Xftp及Xshell的服务器使用方法
服务器·人工智能·深度学习·机器学习·ssh