首页 > 其他分享 >Websocket协议

Websocket协议

时间:2024-06-30 19:53:16浏览次数:1  
标签:协议 WebSocket len ws Websocket buf payload 客户端

一、websocket简介

websocket是在单个TCP连接上进行全双工通信的协议,允许Server主动向Client推送数据。

客户端和服务器只需要完成一次握手,就可以创建持久性的连接,进行双向数据传输。

websocket是独立的,作用在TCP上的协议。

为了向前兼容, WebSocket 协议使用 HTTP Upgrade 协议升级机制来进行 WebSocket 握手, 当握手完成之后, 客户端和服务端就可以依据WebSocket 协议的规范格式进行数据传输。

二、WebSocket与HTTP

2.1、WebSocket与HTTP的关系

相同点

  • 都是一样基于TCP的,都是可靠性传输协议。
  • 都是应用层协议。

不同点

  • WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息。HTTP是单向的。
  • WebSocket是需要握手进行建立连接的。

联系
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。

2.2、websocket相对HTTP协议的优点

  1. 支持双向通信,数据的实时性更新更强。
  2. 开销小;客户端和服务端进行数据通信时,websocket的header(数据头)较小。服务端到客户端的header只有2~10 Bytes,客户端到服务端的需要加上额外的4 Bytes的masking-key。而HTTP协议每次通信都需要携带完整的数据头。
  3. 扩展性。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。
  4. 二进制数据支持更好。

三、连接握手

WebSocket为了兼容HTTP协议,是在HTTP协议的基础之上进行升级得到的。在客户端和服务器端建立HTTP连接之后,客户端会向服务器端发送一个升级到webSocket的协议,如下所示:

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

注意,这里的HTTP版本必须是1.1以上。HTTP的请求方法必须是GET

通过设置Upgrade和Connection这两个header,表示我们准备升级到webSocket了。

除了这里列的属性之外,其他的HTTP自带的header属性都是可以接受的。

这里还有两个比较特别的header,他们是Sec-WebSocket-Version和Sec-WebSocket-Key。

Sec-WebSocket-Version

客户端请求的WebSocket的版本号。如果服务器端并不明白客户端发送的请求,则会返回一个400 ("Bad Request"),在这个返回中,服务器端会返回失败的信息。如果是不懂客户端发送的Sec-WebSocket-Version,服务器端同样会将Sec-WebSocket-Version返回,以告知客户端。

Sec-WebSocket-Key

当服务器端收到客户端的请求之后,会返回给客户端一个响应,告诉客户端协议已经从HTTP升级到WebSocket了。

返回的响应可能是这样的:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

这里的Sec-WebSocket-Accept是根据客户端请求中的Sec-WebSocket-Key来生成的。具体是将客户端发送的Sec-WebSocket-Key 和 字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 进行连接。然后使用SHA1算法求得其hash值。最后将hash值进行base64编码即可。

当服务器端返回Sec-WebSocket-Accept之后,客户端可以对其进行校验,以完成整个握手过程。

四、数据传输

4.1 消息格式

      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 ...                |
     +---------------------------------------------------------------+

FIN: 1 bit

表示这是消息的最后一个片段。第一个片段也有可能是最后一个片段。

RSV1,RSV2,RSV3: 每个 1 bit

必须设置为 0,除非扩展了非 0 值含义的扩展。如果收到了一个非 0 值但是没有扩展任何非 0 值的含义,接收终端必须断开 WebSocket 连接。

Opcode: 4 bit

定义“有效负载数据”的解释。如果收到一个未知的操作码,接收终端必须断开 WebSocket 连接。下面的值是被定义过的。

  • %x0 表示一个持续帧
  • %x1 表示一个文本帧
  • %x2 表示一个二进制帧
  • %x3-7 预留给以后的非控制帧
  • %x8 表示一个连接关闭包
  • %x9 表示一个 ping 包
  • %xA 表示一个 pong 包
  • %xB-F 预留给以后的控制帧

Mask: 1 bit

mask 标志位,定义“有效负载数据”是否添加掩码。如果设置为 1,那么掩码的键值存在于 Masking-Key 中,所有的从客户端发送到服务端的帧都需要设置这个 bit 位为 1。

Payload length: 7 bits, 7+16 bits, 7+64 bits

以字节为单位的“有效负载数据”长度,如果值为 0-125,那么就表示负载数据的长度。如果是 126,那么接下来的 2 个 bytes 解释为 16bit 的无符号整形作为负载数据的长度。如果是 127,那么接下来的 8 个 bytes 解释为一个 64bit 的无符号整形(最高位的 bit 必须为 0)作为负载数据的长度。

有效负载长度是指“扩展数据”+“应用数据”的长度。

Masking-Key: 0 or 4 bytes

所有从客户端发往服务端的数据帧都已经与一个包含在这一帧中的 32 bit 的掩码进行过了运算。如果 mask 标志位(1 bit)为 1,那么这个字段存在,如果标志位为 0,那么这个字段不存在。在 5.3 节中会介绍更多关于客户端到服务端增加掩码的信息。

Payload data: (x+y) bytes

“有效负载数据”是指“扩展数据”和“应用数据”。

  • Extension data: x bytes

    除非协商过扩展,否则“扩展数据”长度为 0 bytes。在握手协议中,任何扩展都必须指定“扩展数据”的长度,这个长度如何进行计算,以及这个扩展如何使用。如果存在扩展,那么这个“扩展数据”包含在总的有效负载长度中。

  • Application data: y bytes

    任意的“应用数据”,占用“扩展数据”后面的剩余所有字段。“应用数据”的长度等于有效负载长度减去“扩展应用”长度。

4.2 Websocket消息实例

这是客户端给服务器发送 "Hello, WebSocket!" 字符串的示例数据报文:

81 91 BD 14 A5 6E F5 71 C9 02 D2 38 85 39 D8 76 F6 01 DE 7F C0 1A 9C

报文分析:

  • 0x81(b1000 0001): FIN=1, opcode=1,说明这一帧是文本数据;

  • 0x91(b1001 0001): MASK=1,Payload len: 17(Hello, WebSocket!的长度);

    • 如果Payload len长度是126,则扩展2个字节 extend payload length 作为数据长度;
    • 如果Payload len长度是127,则扩展8个字节 extend payload length 作为数据长度;
  • BD 14 A5 6E, 如果MASK位设置位1,这里4字节为 掩码键(Masking-key),它是由客户端挑选出来的32位随机数;

  • F5 71 C9 02 D2 38 85 39 D8 76 F6 01 DE 7F C0 1A 9C Payload掩码算法(payload data与masking-key进行异或)处理后的结果,该操作不会影响数据载荷的长度;

五、示例

客户端

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket 客户端</title>
</head>
<body>
    <h1>WebSocket 客户端</h1>
    <label for="server">服务器:</label>
    <input type="text" id="server" value="ws://192.168.10.167:8888" />
    <button onclick="connect()">连接</button><br/>
    <label for="message">消息:</label>
    <input type="text" id="message" value="hello" />
    <button onclick="sendMessage()">发送</button><br/>
    <div id="output"></div>

    <script>
        var ws;

        function connect() {
            var server = document.getElementById('server').value;
            ws = new WebSocket(server);

            ws.onopen = function() {
                document.getElementById('output').innerHTML += '连接已打开<br/>';
            };

            ws.onmessage = function(event) {
                document.getElementById('output').innerHTML += '接收: ' + event.data + '<br/>';
            };

            ws.onclose = function() {
                document.getElementById('output').innerHTML += '连接已关闭<br/>';
            };
        }

        function sendMessage() {
            var message = document.getElementById('message').value;
            ws.send(message);
            document.getElementById('output').innerHTML += '发送: ' + message + '<br/>';
        }
    </script>
</body>
</html>

服务器

下面只给了websocket相关的函数

#define BUFFER_LENGTH	 	4096
#define GUID  				"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
#define WEBSOCK_KEY_LENGTH	19 //length of "Sec-WebSocket-Key: "


enum {
  WS_HANDSHARK = 0,
  WS_TRANMISSION = 1,
  WS_END = 2,
};

typedef struct ws_ophdr_s 
{
	unsigned char opcode:4,
				  rsv3:1,
				  rsv2:1,
				  rsv1:1,
				  fin:1;
	unsigned char pl_len:7,
				  mask:1;
} ws_ophdr_t;


typedef struct ws_head_126_s 
{
  unsigned short 	pl_len;
  char 				mask_key[4];
} ws_head_126_t;

typedef struct ws_head_127_s 
{
  unsigned long long 	pl_len;
  char 					mask_key[4];
} ws_head_127_t;

typedef struct ws_ctx_s
{
	char					buf[BUFFER_LENGTH];
	struct event_base       *base;
	struct evconnlistener   *listener;
	struct bufferevent 		*bev;
	int						state;
} ws_ctx_t;


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;

    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;
}


int readline(char *allbuf, int idx, char *linebuf) 
{
    int len = strlen(allbuf);

    for(; idx < len; idx++) 
	{
        if (allbuf[idx] == '\r' && allbuf[idx+1] == '\n') 
		{
            return idx+2;
        } 
		else 
		{
            *(linebuf++) = allbuf[idx];
        }
    }

    return -1;
}


int handshark(struct bufferevent *bev, char *buf, int len)
{
	char	linebuf[1024] = {0};
	int		idx = 0;
	char	sec_data[20] = {0};
	char	sec_accept[128] = {0};

    /*读每一行找到Sec-WebSocket-Key,对这个key进行SHA1和base64获取对应的accept-key,再生成一个符合规范的响应报文回给客户端*/
	do{
		memset(linebuf, 0, 1024);
		idx = readline(buf, idx, linebuf);

		if(strstr(linebuf, "Sec-WebSocket-Key"))  
		{
			strcat(linebuf, GUID);

			SHA1(linebuf + WEBSOCK_KEY_LENGTH, strlen(linebuf + WEBSOCK_KEY_LENGTH), sec_data);
			base64_encode(sec_data, strlen(sec_data), sec_accept);

			memset(buf, 0, BUFFER_LENGTH);
			len = sprintf(buf, "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("Response: %s\n", buf);

			bufferevent_write(bev, buf, len);  //我的服务器端是基于libevent来实现的,这里就是把buf里的消息传给客户端
			break;
		}
	} while ((buf[idx] != '\r' || buf[idx+1] != '\n') && idx != -1);

	return 0;
}


void umask(char *payload, int length, char *mask_key) 
{
    int i = 0;

	for (i = 0; i < length; i++) {
        payload[i] ^= mask_key[i % 4];
    }
}


int transmission(ws_ctx_t *ws_ctx, char *buf, int len)
{
	ws_ophdr_t		*hdr = NULL;
	ws_head_126_t	*hdr126 = NULL;
	ws_head_127_t	*hdr127 = NULL;

	unsigned char 	*payload = NULL;
	int				pl_len = 0;

	printf("buf:%s\n", buf);
	hdr = (ws_ophdr_t *)buf;
	if( hdr->pl_len < 126 )
	{
		pl_len = hdr->pl_len; 
		payload = (unsigned char *)(buf + sizeof(ws_ophdr_t) + 4);

		if (hdr->mask)  //mask=1,需要解掩码
		{
            umask((char *)payload, pl_len, buf + sizeof(ws_ophdr_t));
        }

		printf("payload:%s\n", payload);
	}
	else if ( hdr->pl_len == 126 )
	{
		hdr126 = (ws_head_126_t *)(buf + sizeof(ws_ophdr_t));
		pl_len = ntohs(hdr126->pl_len);
		payload =(unsigned char *) (buf + sizeof(ws_ophdr_t) + sizeof(ws_head_126_t) + 4);

		if (hdr->mask)  
		{
            umask(payload, pl_len, buf + sizeof(ws_ophdr_t) + sizeof(ws_head_126_t));
        }

		printf("payload:%s\n", payload);
	}
	else
	{
		hdr127 = (ws_head_127_t *)(buf + sizeof(ws_ophdr_t));
		pl_len = ntohll(hdr127->pl_len);
		payload =(unsigned char *) (buf + sizeof(ws_ophdr_t) + sizeof(ws_head_127_t) + 4);

		if (hdr->mask)  
		{
            umask(payload, hdr->pl_len, buf + sizeof(ws_ophdr_t) + sizeof(ws_head_127_t));
        }

		printf("payload:%s\n", payload);
	}

	return 0;
}

void ws_request(struct bufferevent *bev, char *buf, int len, void *arg) 
{
    ws_ctx_t *ws_ctx = (ws_ctx_t *)arg;

    printf("state: %d\n", ws_ctx->state);
    if (ws_ctx->state == WS_HANDSHARK) 
	{
        ws_ctx->state = WS_TRANMISSION;
        handshark(bev, buf, len);
    } 
	else if (ws_ctx->state == WS_TRANMISSION) 
	{
        transmission(ws_ctx, buf, len);
    }

    printf("websocket request: %d\n", ws_ctx->state);
}

标签:协议,WebSocket,len,ws,Websocket,buf,payload,客户端
From: https://www.cnblogs.com/LiBlog--/p/18276850

相关文章

  • C#使用MQTT通讯协议发布订阅主题报文
    一、服务端1.添加引用MQTTnet类库   2.代码:启动一个MQTT服务1//启动一个MQTT服务器2//MQTT3IMqttServerserver=newMqttFactory().CreateMqttServer();4server.ClientConnecte......
  • python创建websocket服务器,实现循环发送消息
    WebSocket协议是在2008年由Web应用程序设计师和开发人员创建的,目的是为了在Web浏览器和服务器之间提供更高效、更低延迟的双向通信。它允许客户端和服务器在任何时候发送消息,无需重新建立TCP连接。WebSocket可以在Web浏览器和服务器之间传输文本和二进制数据,使得构建实时Web......
  • 【杂记-浅谈FTP文件传输协议】
    FTP文件传输协议一、FTP协议概述二、FTP的安全隐患三、FTP服务器配置问题四、FTP的安全加固方法一、FTP协议概述FTP,FileTransferProtocol,即文件传输协议,是一种用于在网络上进行文件传输的标准协议,它允许用户在客户端和服务器之间传输文件,支持上传、下载、删除和重......
  • 通过ESP32读取I2C温湿度传感器项目:协议与代码实例
    简介在本项目中,我们将使用ESP32开发板读取I2C温湿度传感器的数据。我们将详细介绍I2C协议,并提供图文并茂的代码实例,帮助你快速上手。项目流程选择硬件:ESP32开发板、I2C温湿度传感器(如DHT12、HTU21D、SHT30等)、连接线和面包板。了解I2C协议:I2C(Inter-IntegratedCircuit)是......
  • 深度解析RocketMq源码-高可用存储组件(一) raft协议详解
    1.绪论前面的文章已经分析过,以前rocketmq通过主从复制的思想实现系统的高可用,即在搭建集群的时候会手动的设置一个主节点和从节点,在写入数据的时候,会先写入到主broker,然后再同步到从节点中。但是这样会有一个问题,就是主节点宕机过后,需要手动的修改从节点成为新的主节点。在roc......
  • 深入理解协议栈的内部结构——收发和断开
    1.上期问题的答案如果客户端connect操作时,服务端对应的端口号不接受连接,在这种情况下不会设置SYN的值,而是会把RST比特设为12.本期主题上一期讲解了在TCP下协议栈的socket操作和connect操作,那么本期我们会讲解TCP协议栈的write操作,read操作和close操作。3.网络包的大小3.1......
  • FFmpeg开发笔记(三十二)利用RTMP协议构建电脑与手机的直播Demo
    不管是传统互联网还是移动互联网,实时数据传输都是刚需,比如以QQ、微信为代表的即时通信工具,能够实时传输文本和图片。其中一对一的图文通信叫做私聊,多对多的图文通信叫做群聊。除了常见的图文即时通信,还有实时音视频通信,比如一对一的音频通话、一对一的视频通话等等,此时可采用WebR......
  • Java学习 - 网络IP协议簇 讲解
    IP协议IP协议全称InternetProtocol互联网互连协议IP协议作用实现数据在网络节点上互相传输IP协议特点不面向连接不保证可靠IP协议数据报结构组成说明版本目前有IPv4和IPv6两种版本首部长度单位4字节,所以首部长度最大为15*4=60字节区分服务不同服务的优先级不......
  • Navicat Premium for Mac(多协议数据库管理工具) 16.3.4版
    NavicatPremium16是一款功能强大的跨平台数据库管理工具,支持多种数据库类型,如MySQL、MariaDB、Oracle、SQLite、PostgreSQL等等。它提供了丰富的数据库管理功能和工具,可以帮助开发人员和数据库管理员快速地创建、管理和维护数据库。NavicatPremiumforMac(多协议数据库管......
  • 【Linux】TCP协议
    目录TCP协议TCP协议段格式确认应答机制序号与确认序号超时重传六个标记位连接管理机制三次握手四次挥手窗口大小流量控制滑动窗口拥塞控制延迟应答捎带应答面向字节流粘包TCP异常情况TCP小结基于TCP应用层协议TCP与UDP的对比用UDP实现可靠传输理解listen的第二个参数......