linux网络编程4——WebSocket协议及服务器的简易实现

文章目录

    • 1. WebSocket服务器介绍
        • 1.1 WebSocket 协议的特点
        • 1.2 WebSocket 与 HTTP 的区别:
        • 1.3 WebSocket 的应用场景:
    • 2. WebSocket握手协议详解
    • 3. 可能出现的错误
    • 4. 握手协议编码实现
    • 5. websocket传输协议实现
        • 5.1 websocket帧格式
        • 5.2 解包客户端数据
        • 5.3 服务端发包
    • 学习参考

1. WebSocket服务器介绍

本文详细介绍了WebSocket协议的特点、与HTTP的区别以及应用场景;然后分析了WebSocket协议的主要内容;最后借助前面的底层reactor的代码实现了一个WebSocket协议的Web服务器。

完整项目代码参考:我的github项目

WebSocket 是一种在客户端(通常是浏览器)和服务器之间建立双向通信通道的协议,允许它们通过一个持久的 TCP 连接进行实时数据交换。与传统的 HTTP 请求-响应模型不同,WebSocket 提供了全双工(full-duplex)的通信,即客户端和服务器都可以在任何时间向对方发送消息,而无需等待响应。

1.1 WebSocket 协议的特点
  1. 持久连接:WebSocket 建立连接后,它保持打开状态,客户端和服务器之间可以持续交换数据,直到连接被一方主动关闭。
  2. 全双工通信:双向通信通道可以同时发送和接收数据。服务器可以在不依赖客户端请求的情况下推送数据。
  3. 减少网络开销:WebSocket 通过升级一次 HTTP 请求来建立连接,之后的数据交换只通过轻量的 WebSocket 帧格式,而不像 HTTP 需要额外的请求头部信息,因而大大减少了网络开销。
  4. 实时数据传输:适合实时应用,如在线聊天、股票行情、游戏、物联网数据传输等。

WebSocket协议是通过HTTP1.1协议的握手过程建立的,但连接建立后两者的通信机制完全不同。

WebSocket握手头部

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

WebSocket握手成功后升级连接

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
1.2 WebSocket 与 HTTP 的区别:
  • 双向通信 vs 单向请求响应:HTTP 是单向的请求-响应模型,客户端必须发起请求,服务器响应。而 WebSocket 是双向的,双方可以随时发送数据。
  • 持久连接:HTTP 需要每次发起新的连接请求(即使是 HTTP/1.1,也需要保持连接),而 WebSocket 在建立连接后,连接是持久的,直到主动关闭。
  • 协议头部大小:HTTP 请求和响应头部信息较多,而 WebSocket 帧的协议头部相对较少,减少了数据传输的开销。
1.3 WebSocket 的应用场景:

Http协议和WebSocket协议常常结合使用,例如HTTP用于初始的页面加载和静态资源获取,WebSocket用于需要长时间实时交互的场景。

  1. 实时聊天:像 Slack、微信、Facebook Messenger 这样需要实时通信的应用。
  2. 实时股票行情:股票交易平台、加密货币交易所等,需要不断推送最新的市场数据。
  3. 多人在线游戏:游戏服务器需要与每个客户端频繁、实时交换数据。
  4. 实时通知系统:例如社交网络中的通知,或电子商务中的订单更新。
  5. IoT 设备管理:物联网应用可以使用 WebSocket 实时管理和监控设备的状态。

2. WebSocket握手协议详解

主要介绍握手协议,在握手阶段,客户端会在请求头中发送一个sec-websocket-key

sec-websocket-key: e3bLzpFK7Li8RHh8DZL87A==

服务器需要拿到这个key值,然后进行如下计算

  • 将key与一个GUID连结,该GUID值为258EAFA5-E914-47DA-95CA-C5AB0DC85B11,得到input
  • 使用SHA-1算法计算input得到input2
  • 使用base64算法计算input2得到ouput

最后在响应头中发送sec-websocket-Accpet头即可

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

之后,双方可以保持连接,进行实时的全双工的交互。

3. 可能出现的错误

  1. 编译时链接器显示找不到一些符号的定义

    记得链接ssl库和crypto库

    gcc -o xxx xxx1.c xxx2.c -lssl -lcrypto
    
  2. 客户端发送”unknown opcode",并主动关闭连接

​ 一定是服务端发送的数据不符合协议,或者Sec-WebSocket-Accept值有误。

4. 握手协议编码实现

这里只实现了建立握手协议这一环节,连接建立后发送的消息都应该遵守websocket协议的格式。完整的websocket回声服务器代码可参考完整项目代码参考:我的github项目。

完整的websocket协议请参考rfc6455。

#include <string.h>#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>#include "webserver.h"
#include "websocket.h"#define DEBUG#define WEBSOCKET_KEY_LENGTH 256// 负责按照rfc6455的规定输出Sec-WebSocket-Accept的值
static int encode_key(unsigned char *key, size_t n, unsigned char *output)
{unsigned char hash[SHA_DIGEST_LENGTH];SHA1(key, n, hash);BIO *bmem, *b64;BUF_MEM *bptr;b64 = BIO_new(BIO_f_base64());bmem = BIO_new(BIO_s_mem());b64 = BIO_push(b64, bmem);BIO_write(b64, hash, SHA_DIGEST_LENGTH);BIO_flush(b64);BIO_get_mem_ptr(b64, &bptr);memcpy(output, bptr->data, bptr->length);// 这里切记是bptr->length字符数组的长度output[bptr->length - 1] = 0;BIO_free_all(b64);return 0;
}int handshake(struct Conn *conn)
{// handshakeunsigned char output[WEBSOCKET_KEY_LENGTH] = {0};unsigned char input[WEBSOCKET_KEY_LENGTH] = {0};char *key = strstr(conn->rbuffer, "Sec-WebSocket-Key:");if (!key){conn->wlength = 0;return 1;}key += 19;int i = 0;while (*key != 0 && *key != ' ' && *key != '\r'){input[i++] = *key++;}strcpy((char *)&input[i], "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");encode_key(input, strlen((char *)input), output);struct stat filestat = {0};int sended = snprintf(conn->wbuffer, BUFFER_LENGTH,"HTTP/1.1 101 Switching Protocols\r\n""Upgrade: websocket\r\n""Connection: Upgrade\r\n""Sec-WebSocket-Accept: %s\r\n\r\n", (char *)output);printf("%s|||\n", output);conn->wlength = sended;return 0;
}int ws_request(struct Conn *conn)
{printf("<<<<<input<<<<<\n %s\n", conn->rbuffer);if (conn->status == 0){handshake(conn);conn->status = 1;}else if (conn->status == 1){int ret = 0;conn->payload = decode_packet((unsigned char *)conn->rbuffer, conn->mask, conn->rlength, &ret);printf("data: %s, length: %d\n", conn->payload, ret);conn->wlength = ret;conn->status = 2;}return 0;
}int ws_response(struct Conn *conn)
{if (conn->status == 2){conn->wlength = encode_packet(conn->wbuffer, conn->mask, conn->payload, conn->wlength);conn->status = 1;}conn->wbuffer[conn->wlength] = 0;printf(">>>>output>>>>\n%s\n", conn->wbuffer);return 0;
}

5. websocket传输协议实现

5.1 websocket帧格式
	  0                   1                   2                   30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-------+-+-------------+-------------------------------+|F|R|R|R| opcode|M| Payload len |    Extended payload length    ||I|S|S|S|  (4)  |A|     (7)     |             (16/64)           ||N|V|V|V|       |S|             |   (if payload len==126/127)   || |1|2|3|       |K|             |                               |+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +|     Extended payload length continued, if payload len == 127  |+ - - - - - - - - - - - - - - - +-------------------------------+|                               |Masking-key, if MASK set to 1  |+-------------------------------+-------------------------------+| Masking-key (continued)       |          Payload Data         |+-------------------------------- - - - - - - - - - - - - - - - +:                     Payload Data continued ...                :+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +|                     Payload Data continued ...                |+---------------------------------------------------------------+

rfc6455 5.2介绍了各个域的作用,对应的如果要实现该协议就要定义相关的结构体

struct _nty_ophdr {unsigned char opcode:4,rsv3:1,rsv2:1,rsv1:1,fin:1;unsigned char payload_length:7,mask:1;} __attribute__ ((packed));struct _nty_websocket_head_126 {unsigned short payload_length;char mask_key[4];unsigned char data[8];
} __attribute__ ((packed));struct _nty_websocket_head_127 {unsigned long long payload_length;char mask_key[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;

__attribute__ ((packed)) 是 GNU C 编译器(GCC)的一个扩展,用于告诉编译器不对结构体的成员进行内存对齐。通常,编译器为了提高访问效率,会按照特定的字节对齐规则来放置结构体成员。使用 packed 属性后,编译器不会对齐字段,而是按照定义的顺序紧凑地存储它们,节省内存空间。

5.2 解包客户端数据

rfc6455 5.3规定了客户端向服务端发送的数据必须经过掩码加密,其原理是用8位的mask-key对原数据一次逐字节进行异或操作,这样加密和解密的过程是完全一样的。

在协议帧中masking-key有4字节,协议规定,对payload[i]对应的maskkey为masking-key[i mod 4],这样就可以写出其加解密算法了:

void demask(char *data,int len,char *mask){    int i;    for (i = 0;i < len;i ++)        *(data+i) ^= *(mask+(i%4));
}

这样对于服务端的解包操作,就是解密payload,拿到原数据。

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;//char mask[4] = {0};int i = 0;if ((hdr->payload_length & 0x7F) == 126) {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->payload_length & 0x7F) == 127) {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 {size = hdr->payload_length;memcpy(mask, data, 4);start = 6;}*ret = size;demask(stream+start, size, mask);return stream + start;
}
5.3 服务端发包

对于服务端的发包操作,只需要填充相应的协议字段即可,不需要掩码加密。

int encode_packet(char *buffer,char *mask, char *stream, int length) {nty_ophdr head = {0};head.fin = 1;head.opcode = 1;int size = 0;if (length < 126) {head.payload_length = length;memcpy(buffer, &head, sizeof(nty_ophdr));size = 2;} else if (length < 0xffff) {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 {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;
}

学习参考

学习更多相关知识请参考零声 github。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/883456.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在短时间内入门并掌握深度学习?

如何在短时间内快速入门并掌握深度学习&#xff0c;是很多读者的困惑——晦涩难懂的数学 知识、复杂的算法、烦琐的编程……深度学习虽然让无数读者心怀向往&#xff0c;却也让不少人望而生畏&#xff0c;深感沮丧&#xff1a;时间没少花&#xff0c;却收效甚微。 如何才能更好…

python对文件的读写操作

任务:读取文件夹下的批量txt数据&#xff0c;并将其写入到对应的word文档中。 txt文件中包含&#xff1a;编号、报告内容和表格数据。写入到word当中&#xff1a;编号、报告内容、表格数据、人格雷达图以及对应的详细说明&#xff08;详细说明是根据表格中的标识那一列中的加号…

设计模式(二)工厂模式详解

设计模式&#xff08;二&#xff09;工厂模式详解 简单工厂模式指由一个工厂对象来创建实例,适用于工厂类负责创建对象较少的情况。例子&#xff1a;Spring 中的 BeanFactory 使用简单工厂模式&#xff0c;产生 Bean 对象。 工厂模式简介 定义&#xff1a;工厂模式是一种创建…

js构造函数和原型对象,ES6中的class,四种继承方式

一、构造函数 1.构造函数是一种特殊的函数&#xff0c;主要用来初始化对象 2.使用场景 常见的{...}语法允许创建一个对象。可以通过构造函数来快速创建多个类似的对象。 const Peppa {name: 佩奇,age: 6,sex: 女}const George {name: 乔治,age: 3,sex: 男}const Mum {nam…

pytorch的标签平滑介绍

什么是标签平滑(Label Smoothing)? 标签平滑(Label Smoothing)是一种正则化技术,旨在防止模型过度自信(即输出的概率分布过于“尖锐”)。在分类任务中,标准的目标标签是one-hot编码,也就是正确类别的概率为 1,其他类别的概率为 0。而标签平滑通过将正确类别的概率从…

小程序开发实战:PDF转换为图片工具开发

目录 一、开发思路 1.1 申请微信小程序 1.2 编写后端接口 1.3 后端接口部署 1.4 微信小程序前端页面开发 1.5 运行效果 1.6 小程序部署上线 今天给大家分享小程序开发系列&#xff0c;PDF转换为图片工具的开发实战&#xff0c;感兴趣的朋友可以一起来学习一下&#xff01…

基于Springboot无人驾驶车辆路径规划系统(源码+定制+开发)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

Hadoop:yarn的Rust API接口

今天头一次接触了yarn的Rust API接口&#xff0c;在本地搭建了集群&#xff0c;能够得到每个任务的详细信息。 (一)得到所有任务的所有信息命令&#xff1a; 默认是json格式&#xff0c;也可以指定xml的格式&#xff0c;如(curl --compressed -H "Accept: application/x…

【大模型理论篇】主流大模型的分词器选择及讨论(BPE/BBPE/WordPiece/Unigram)

1. 背景分析 分词是将输入和输出文本拆分成更小单位的过程&#xff0c;使得大模型能够处理。token可以是单词、字符、子词或符号&#xff0c;取决于模型的类型和大小。分词可以帮助模型处理不同的语言、词汇和格式&#xff0c;并降低计算和内存成本。分词还可以通过影响token的…

fmql之Linux RTC

模拟i2c&#xff0c;连接rtc芯片。 dts&#xff1a; /{ // 根节点i2c_gpio: i2c-gpio {#address-cells <1>;#size-cells <0>;compatible "i2c-gpio";// MIO56-SDA, MIO55-SCL // 引脚编号gpios <&portc 2 0&portc 1 0 >;i2c-gp…

Modbus TCP报错:Response length is only 0 bytes

问题描述&#xff1a; 使用modbus_tk库&#xff0c;通过Modbus tcp连接PLC时&#xff0c;python中的一个报错信息&#xff1a; Response length is only 0 bytes报错原因&#xff1a; 与Modbus TCP 服务端建立连接后没有断开&#xff0c;继续作为长连接使用&#xff0c;客户端…

随笔—git操作

1. 创建一个 GitHub 仓库 登录到 GitHub。点击右上角的 “” 按钮&#xff0c;然后选择 “New repository”。填写仓库名称和描述&#xff0c;选择是否公开&#xff0c;最后点击 “Create repository”。 2. 在本地初始化 Git 仓库&#xff08;如果尚未初始化&#xff09; 在…

【Lammps】atomsk安装与环境变量设置(Linux环境)

【Lammps】atomsk安装与环境变量设置&#xff08;Linux环境&#xff09; 官网配置环境变量测试 官网 https://atomsk.univ-lille.fr/dl.php 下载的安装包如下&#xff1a; 使用Linux的解压命令进行解压&#xff1a; tar -xzvf file.tar.gz注意&#xff1a;file.tar.gz 替换…

【Vue 3】最全组件设计指南:从基础到进阶

&#x1f9d1;‍&#x1f4bc; 一名茫茫大海中沉浮的小小程序员&#x1f36c; &#x1f449; 你的一键四连 (关注 点赞收藏评论)是我更新的最大动力❤️&#xff01; &#x1f4d1; 目录 &#x1f53d; 前言1️⃣ 组件的基础概念与构建2️⃣ 组件通信的核心技术3️⃣ 组件的生命…

【文献及模型、制图分享】中国自然保护地典型治理模式成效比较——基于社区居民感知视角

采取何种治理模式能够更好地提升自然保护地治理的生态、社会和经济成效?基于制度分析与发展&#xff08;IAD&#xff09;框架&#xff0c;选择大熊猫国家公园内部及周边17个社区&#xff0c;通过问卷调查、半结构化访谈、单因素方差、逐步回归分析&#xff0c;比较统治、分治和…

CSP/信奥赛C++刷题训练:经典二分答案例题(3): 洛谷P2920:Time Management S

CSP/信奥赛C++刷题训练:经典二分答案例题(3) [USACO08NOV] Time Management S 题目描述 Ever the maturing businessman, Farmer John realizes that he must manage his time effectively. He has N jobs conveniently numbered 1…N (1 <= N <= 1,000) to accompl…

Spring Boot集成iText实现电子签章

文章目录 一 电子签章1.1 什么是电子签章1.2 签名流程1.3 技术选型 二 实战2.1 生成数字证书2.2 生成印章图片2.3 PDF 签名 一 电子签章 1.1 什么是电子签章 基于《中华人民共和国电子签名法》等相关法规和技术规范&#xff0c;具有法律效力的电子签章一定是需要使用 CA 数字…

Unbounded:一个无限生成式交互的角色生活模拟游戏

❤️ 如果你也关注大模型与 AI 的发展现状&#xff0c;且对大模型应用开发非常感兴趣&#xff0c;我会快速跟你分享最新的感兴趣的 AI 应用和热点信息&#xff0c;也会不定期分享自己的想法和开源实例&#xff0c;欢迎关注我哦&#xff01; &#x1f966; 微信公众号&#xff…

Vertx实现和spring的application.yml自动配置加载

前言 在用vertx写项目的时候&#xff0c;由于需要不同的环境加载不同的配置文件&#xff0c;这里就需要和spring架构的application.yml配置文件一样&#xff0c;可以根据环境变量加载不同的配置。 代码 引入vertx相关依赖 <dependency><groupId>io.vertx</gr…

ECharts饼图-饼图自定义样式,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个饼图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供详…