webSocket消息推送之入门学习

一. 为什么需使用webSocket ?

        初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

        答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

        举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

        这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

        轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

二. webSocket简介 :

        WebSocket 协议在2008年诞生,2011年成为国际标准。WebSocket 能够在建立连接之后,在服务器端推送数据到客户端,解决HTTP协议的弊端。

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

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

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

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

三. webscoket 特点 :

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

四. websocket相对HTTP协议的优点 :

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

WebSocket 协议主要为了解决 HTTP/1.x 缺少双向通信机制的问题, 它使用 TCP 作为传输层协议, 使用 HTTP Upgrade 机制来握手,它与 HTTP 是相互独立的协议, 二者没有上下的分层关系

五. websocket的应用场景 :

从websocket的优点可以知道,主要应用场景有:

  1. 视频弹幕。
  2. 媒体即时通讯。
  3. 需要实时位置/数据的应用。
  4. 金融行业的股票基金价格实时更新等。

六. websocket握手

1. 客户端:Upgrade(申请升级到websocket协议)

协议包含两个部分:握手和数据传输。WebSocket复用了HTTP的握手通道。

客户端通过HTTP请求与WebSocket服务端协商升级到websocket协议。协议升级完成后,后续的数据传输按照WebSocket的data frame进行。

WebSocket 握手采用 HTTP Upgrade 机制,使用标准的HTTP报文格式,只支持使用HTTP的GET方法,客户端发送如下所示的结构发起握手:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://fly.example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

 说明: 

参数含义
Upgrade:websocket升级到websocket协议
Connection:Upgrade升级协议
Sec-WebSocket-Key:(key value)与服务端响应的sec-websocket-accept对应,提供安全防护
Sec-WebSocket-Version:13指示websocket的版本

2. 服务器:响应协议升级

服务端如果支持 WebSocket 协议,则返回 101 的 HTTP 状态码。返回如下所示的结构:

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

参数说明: 

参数说明
Sec-WebSocket-Accept必须有,与客户端的Sec-WebSocket-Key对应
Sec-WebSocket-Version必须有, 返回服务端和客户端都支持的 WebSocket 协议版本。如果服务端不支持客户端的协议版本则立即终止握手, 并返回 HTTP 426 状态码,同时设置 Sec-WebSocket-Version 说明服务端支持的 WebSocket 协议版本列表
Sec-WebSocket-Protocol可选, 是否支持 WebSocket 子协议
Sec-WebSocket-Extensions可选, 是否支持拓展列表

 注意:每个HTTP的header都以\r\n结尾,并且最后一行要加上一个额外的\r\n。这是由于http协议制定的时候,就是用分隔符进行分包。

3. Sec-WebSocket-Accept值的计算

客户端发起握手时通过 Sec-WebSocket-Key 传递了一个安全防护字符串,服务端将该值与 WebSocket 魔数 "258EAFA5-E914-47DA- 95CA-C5AB0DC85B11" 进行字符串拼接,将得到的字符串做 SHA-1 哈希, 将得到的哈希值再做 base64 编码,最后得到的值就是Sec-WebSocket-Accept值。

计算公式为:

(1)将Sec-WebSocket-Key的值与258EAFA5-E914-47DA-95CA-C5AB0DC85B11魔数进行字符串拼接;

(2)使用SHA1对拼接的字符串做哈希,得到一个哈希值;

(3)将哈希值做base64编码得到Sec-WebSocket-Accept值。

伪代码:

......

// 字符串拼接
char *str=Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

// 计算sha1哈希
char sec_data[64];
SHA1(str,strlen(str),sec_data);

// 编码成base64
char sec_accept[64];
base64_encode(sec_data,strlen(sec_data),sec_accept);
......

base64_encode函数实现: 

#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>

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

七. 心跳包--保持连接

有些场景,客户端、服务端虽然长时间没有数据交互,但仍需要保持连接。这个时候,可以采用心跳来实现。

逻辑:

  • 发送方 --> 接收方:ping,探测,实现 WebSocket 的 Keep-Alive,可以有Payload。
  • 接收方 --> 发送方:pong,Ping 的响应,Payload 的内容需要和 Ping frame 相同

ping、pong的操作对应opcode分别是0x9、0xA。

八. Sec-WebSocket-Key/Sec-WebSocket-Accept的作用

Sec-WebSocket-Key主要目的不是确保数据的安全性,最主要作用是提供基本的安全防护,减少恶意连接。连接是否安全、数据是否安全、客户端/服务端是否合法,并没有实际性的保证。

九. 数据掩码(Masking-key)的作用

WebSocket协议中,数据掩码的作用是增强websocket协议的安全性,并不是为了保护数据本身。

数据掩码并不是为了防止数据泄密,而是为了防止代理缓存污染攻击(proxy cache poisoning attacks)问题。

十. websocket服务器实现

处理流程:

  1. 接收到client发送的请求升级协议包。
  2. 解析请求包,获取Sec-WebSocket-Key字符串,转换到数据解析状态。
  3. 解析升级协议包,获取相关信息,转换到数据交互状态。
  4. 打包websocket协议头,发送frame。

代码简单示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>

#include <assert.h>

#define GUID				"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
#define BUFFER_LENGTH		1024

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

struct ws_ophdr
{
	unsigned char opencode : 4,
		rsv3 : 1,
		rsv2 : 1,
		rsv1 : 1,
		fin : 1;
	unsigned char pl_len : 7,
		mask : 1;

};

struct ntyevent {
	int fd;
	int events;
	void *arg;
	int (*callback)(int fd, int events, void *arg);
	
	int status;
	char buffer[BUFFER_LENGTH];
	int length;
	//long last_active;

	char wbuffer[BUFFER_LENGTH]; //response
	int wlength;

	char sec_accept[ACCEPT_KEY_LENGTH];

	int wsstatus; //0, 1, 2, 3
	char mask_key[4];

};
/*
......
*/
// 按行读取数据
int readline(char *buffer,int idx,char *linebuffer)
{
	int len = strlen(buffer);
	for (; idx < len; ++idx)
	{
		if (buffer[idx] == '\r' && buffer[idx + 1] == '\n')
			return idx + 2;
		*(linebuffer++) = buffer[idx];
	}
	return -1;
}
// base64 encode
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;
}
// 握手,获取Sec-WebSocket-Key的字符串,并转换为Sec-WebSocket-Accept所需字符串
int ws_open_shark(struct ntyevent *ev)
{
	int idx = 0;
	char sec_data[128] = { 0 };
	char sec_accept[128] = { 0 };
	do
	{
		char linebuff[BUFFER_LENGTH] = { 0 };
		idx = readline(ev->buffer, idx, linebuff);
		if (strstr(linebuff, "Sec-WebSocket-Key"))
		{
			strcat(linebuff, GUID);
			int keylen = strlen("Sec-WebSocket-Key: ");
			SHA1(linebuff + keylen, strlen(linebuff + keylen), sec_data);
			base64_encode(sec_data, strlen(sec_data), sec_accept);
			printf("index %d, line : %s\n", idx, sec_accept);
			memcpy(ev->sec_accept, sec_accept, ACCEPT_KEY_LENGTH);
		}

	} while ((ev->buffer[idx] != '\r' || ev->buffer[idx + 1] != '\n')
		&& idx != -1);
	return 0;
}
// 掩码、反掩码
void umask(char *payload, int len, char *mask)
{
	int i = 0;
	for (i = 0; i < len; i++)
	{
		payload[i] ^= mask[i % 4];
	}
}
// 解析payloade数据
int ws_tranmission(struct ntyevent *ev)
{
	struct ws_ophdr *ophdr = (struct ws_ophdr *)ev->buffer;
	char *payload = NULL;
	size_t datalen = 0;

	int mask_key_offset = 0;

	if (ophdr->pl_len < 126)
	{
		if (ophdr->mask)
		{
			payload = ev->buffer + 6;
			mask_key_offset = 2;
			umask(payload, ophdr->pl_len, ev->buffer + mask_key_offset);
		}
		else
			payload = ev->buffer + 2;

		datalen = ophdr->pl_len;
		
	}
	else if(ophdr->pl_len == 126)
	{
		printf("%x %x\n", (unsigned char)ev->buffer[2], (unsigned char)ev->buffer[3]);
		datalen = (((unsigned char)ev->buffer[2]) << 8) | ((unsigned char)ev->buffer[3]);
		if (ophdr->mask)
		{
			payload = ev->buffer + 8;
			mask_key_offset = 4;
			umask(payload, datalen, ev->buffer + mask_key_offset);
		}
		else
			payload = ev->buffer + 4;

		

	}
	else if (ophdr->pl_len == 127)
	{
		int i = 0;
		for (i = 2; i <10; i++)
		{
			datalen |= ((unsigned char)ev->buffer[i]);
			if (i + 1 < 10)
				datalen <<= 8;
		}

		if (ophdr->mask)
		{
			payload = ev->buffer + 14;
			mask_key_offset = 10;
			umask(payload, datalen, ev->buffer + mask_key_offset);
		}
		else
			payload = ev->buffer + 10;
		
			
	}
	else
		assert(0);
	printf("fin : %d\n", ophdr->fin);
	printf("rsv1: %d,rsv2: %d,rsv3: %d\n", ophdr->rsv1, ophdr->rsv2, ophdr->rsv3);
	printf("opcode: %d\n", ophdr->opencode);
	printf("mask : %d\n", ophdr->mask);
	printf("payload len : %d\n", ophdr->pl_len);
	if (mask_key_offset)
		printf("mask-key: %x %x %x %x\n",
			ev->buffer[mask_key_offset],
			ev->buffer[mask_key_offset + 1],
			ev->buffer[mask_key_offset + 2],
			ev->buffer[mask_key_offset + 3]);
	printf("data len: %lu\n", datalen);
	printf("payload data [len = %ld]: %s\n", strlen(payload), payload);
	

	strcpy(ev->wbuffer, payload);
	ev->wlength = datalen;
	memcpy(ev->mask_key, ev->buffer + mask_key_offset, 4);

	return datalen;
}
// 解析获取申请升级协议请求,转换状态
void ws_status(struct ntyevent *ev)
{
	char linebuff[BUFFER_LENGTH] = { 0 };
	readline(ev->buffer, 0, linebuff);
	if (strstr(linebuff, "GET "))
		ev->wsstatus = WS_HANDSHARK;
	else
		ev->wsstatus = WS_TRANMISSION;
}
// 响应请求
int ws_request(struct ntyevent *ev)
{
	ev->wlength = ev->length;
	ws_status(ev);
	if (ev->wsstatus == WS_HANDSHARK)
	{
		ws_open_shark(ev);
	}
	else if (ev->wsstatus = WS_TRANMISSION)
	{
		ws_tranmission(ev);
	}
} 
// 处理大小端的函数
void ws_inverted_string(char *str,int len)
{
	int i = 0;
	char temp;
	for (i = 0; i < len / 2; ++i)
	{
		temp = *(str + i);
		*(str + i) = *(str + len - i - 1);
		*(str + len - i - 1) = temp;
	}
}
// 发送websocket的header
int ws_send_hdr(struct ntyevent *ev)
{
	struct ws_ophdr ophdr;

	char extend[16] = { 0 };
	int extend_length = 0;
	int ret = 0;

	ophdr.fin = 1;
	ophdr.rsv1 = ophdr.rsv2 = ophdr.rsv3 = 0;
	ophdr.mask = 1;
	ophdr.opencode = 1;

	if (ev->wlength<126)
		ophdr.pl_len = ev->wlength;
	else if (ev->wlength < 0xFFFF)
	{
		ophdr.pl_len = 126;
		extend_length += 2;
		extend[2] = (ev->wlength >> 8) & 0xFF;
		extend[3] = ev->wlength & 0xFF;
		printf("plelode length: %x%x\n", extend[2], extend[3]);
	}
	else
	{
		ophdr.pl_len = 127;
		extend_length += 8;
		printf("plelode length: ");
		int i = 0;
		for (i = 0; i<8; i++)
		{
			extend[i+2] = (ev->wlength >> ((7-i)*8)) & 0xFF;
			printf("%x", extend[i+2]);
		}

		// 处理大小端问题
		//ws_inverted_string((char *)extend + 2, sizeof(unsigned long long));

		printf("\n");
	}

	extend_length += 2;// ophdr length

	if (ophdr.mask)
	{
		printf("mask key start index: %d\n", extend_length);
		extend[extend_length]   = ev->mask_key[0];
		extend[extend_length+1] = ev->mask_key[1];
		extend[extend_length+2] = ev->mask_key[2];
		extend[extend_length+3] = ev->mask_key[3];

		printf("mask-key: %x %x %x %x\n",
			extend[extend_length],
			extend[extend_length + 1],
			extend[extend_length + 2],
			extend[extend_length + 3]);
		
		umask(ev->wbuffer, ev->wlength, ev->mask_key);

		extend_length += 4;
		printf("mask key end index: %d\n", extend_length);
	}

	printf("fin: %d\nmask: %d\nopcode: %d\n", ophdr.fin, ophdr.mask, ophdr.opencode);
	printf("send hdr[%d],extend_length=%d\n\n", ophdr.pl_len, extend_length);


	char *tmp = (char*)&ophdr;
	extend[0] = tmp[0];
	extend[1] = tmp[1];

	struct ws_ophdr_mask *maskkey=(struct ws_ophdr_mask *)extend;
	printf("mask key: %s,%d,%s\n",ev->mask_key,
		maskkey->mask,
		maskkey->mask_key);

	int i = 0;
	printf("\n\nALL: ");
	for (i = 0; i < extend_length; i++)
	{
		printf("%x ", extend[i]);
	}
	printf("\n\n");

	send(ev->fd, &extend, extend_length, 0);

	return ret;
}
// 响应请求
int ws_response(struct ntyevent *ev)
{

	if (ev->wsstatus == WS_HANDSHARK)
	{
		ev->wlength = sprintf(ev->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", ev->sec_accept);
	}
	else if (ev->wsstatus = WS_TRANMISSION)
	{
		ws_send_hdr(ev);
	}

	
	return ev->wlength;
}
/*
......
*/
int main(int argc,char *argv[])
{
	int listenfd=socket(AF_INET,SOCK_STREAM,0);
	
	struct sockaddr_in server;
	memset(&server,0,sizeof(server));
	
	server.sin_family=AF_INET;
	server.sin_addr.s_addr=htonl(INADDR_ANY);
	server.sin_port=htons(8888);
	
	bind(listenfd,(struct sockaddr*)&server,sizeof(server));
	
	if(listen(listenfd,10)<0)
		return -1;
	
	while(1)
	{
		struct sockaddr_in client;
		socklen_t len=sizeof(client);
		int clientfd=accept(listenfd,(struct sockaddr *)&client,&len);
		
		struct ntyevent ev;
		memset(&ev,0,sizeof(ev));
		recv(clientfd,ev.buffer,BUFFER_LENGTH,0);
		
		// 解析请求
		ws_request(&ev);
		// 响应请求
		ws_response(&ev);

		send(clientfd,ev->wbuffer,ev->wlength,0);
	}
	return 0;
}

十一. 服务端的实现 :

 WebSocket 服务器的实现,可以查看维基百科的列表

常用的 Node 实现有以下三种。

具体的用法请查看它们的文档,这里不详细介绍了。

十二. WebSocketd :

下面,我要推荐一款非常特别的 WebSocket 服务器:Websocketd

它的最大特点,就是后台脚本不限语言,标准输入(stdin)就是 WebSocket 的输入,标准输出(stdout)就是 WebSocket 的输出。

十三. GoEasy WebSocket 消息推送服务:

大家看了前面的介绍,可能已经发现了,WebSocket 的使用有一个前提条件,就是要自己搭建一个服务。

但是很多时候,它只是一个前后端消息的中介,没有其他功能。单独搭一个服务似乎有点麻烦,尤其是在你的应用并不大的情况下。

很多开发者都希望,直接使用现成的 WebSocket 服务,免得自己搭建,最好还是免费的。

下面就介绍一个国内这样的 WebSocket 服务商 GoEasy。你不需要自己搭建了,前后端接入他们的服务器就可以了,他们的机器都在国内,速度和可靠性有保证。

示例代码可以参考文档,只要几行,就能立刻使用 WebSocket 了。

服务端使用 PHP、C#、Java、Go、Node、Python 等各种语言,都没有问题。客户端支持 Uniapp、各种小程序、Web 等不同应用和 Vue、React、Taro 等主流框架。

GoEasy 2015年就上线了,有很多大企业客户,做到了百万级用户同时在线,每秒千万级消息的实时送达。他们保证消息数据全程加密传输,高并发、低延时,99.95%的高可用。

只要你的 DAU(日活跃用户数)不超过200,他们的服务是永久免费的,对于个人的小型应用应该够用了。企业的商业项目需要付费,还提供私有部署。

十四. 参考链接 :

WebSocket 教程 - 阮一峰的网络日志 (ruanyifeng.com)

Linux网络编程之websocket服务器实现 - 知乎 (zhihu.com)

WebSockets 教程 (tutorialspoint.com)

GoEasy介绍 | GoEasy文档


 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值