文章目录
-
- 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 协议的特点
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用于需要长时间实时交互的场景。
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. 可能出现的错误
编译时链接器显示找不到一些符号的定义
记得链接ssl库和crypto库
gcc -o xxx xxx1.c xxx2.c -lssl -lcrypto
客户端发送”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。
评论前必须登录!
注册