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

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)
{
    // handshake
    unsigned 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                   3
      0 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.kler.cn/news/367989.html

相关文章:

  • 【容器】容器化详解:提升开发与运维效率的关键技术
  • 生成式 AI 与向量搜索如何扩大零售运营:巨大潜力尚待挖掘
  • C语言[求x的y次方]
  • Pyspark中pyspark.sql.functions常用方法(4)
  • 基于物联网的智慧考场系统设计(论文+源码)
  • 《分布式机器学习模式》:解锁分布式ML的实战宝典
  • 苏州金龙技术创新赋能旅游新质生产力
  • Navicat导入Excel数据时数据被截断问题分析与解决方案
  • 论文阅读与写作入门
  • mit6824-03-GFS论文记录
  • 微信小程序版本更新管理——实现自动更新
  • Linux复习-C++
  • vue3组件通信--props
  • 虚拟现实新纪元:VR/AR技术将如何改变娱乐与教育
  • 桥接模式,外界与主机通,与虚拟机不通
  • 提示词高级阶段学习day3.3如何写好结构化 Prompt ?
  • AndroidStudio Koala更改jdk版本 2024-1-2
  • 关于我的数据库——MySQL——第二篇
  • Qt/C++路径轨迹回放/回放每个点信号/回放结束信号/拿到移动的坐标点经纬度
  • JavaEE初阶---多线程(三)---内存可见性/单例模式/wait,notify的使用解决线程饿死问题
  • ubuntu虚拟机网络配置
  • C++STL之stack
  • 二十、行为型(访问者模式)
  • Java学习Day53:铲除紫云山金丹原料厂厂长(手机快速登录、权限控制)
  • 浅谈AI大模型的数据特点和应用问题
  • JavaEE初阶---多线程(五)---定时器/线程池介绍