websocket协议与服务器实现

目录

1 什么是websocket

原理

特点

2 websocket的应用场景

3 websocket协议的解析分析

1websocket的协议格式

2 websocket如何验证客户端合法

3 明文和密文如何传输

4 websocket如何断开

4 自定义实现websocket的服务器的代码实现和关键代码展示

全部代码连接

5 一个疑问:既然客户端可以直接调用一个close来断开tcp的连接,为什么websocket还需要留出一个fin位来断开连接


1 什么是websocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输

原理

很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯

特点

较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。

更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。

保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。

可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。

更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

2 websocket的应用场景

主要应用于服务器主动的像客户端推送数据的情况下,当然不排除其他的情况也可以是用websocket协议来通讯

举个例子

websocket协议与服务器实现_第1张图片

eg:当我们通过(浏览器)网页端开始登陆CSDN账号的时候,有一种登陆的方式是通过微信扫码进行登陆

我们微信扫码后,手机微信会将扫码的到的信息发送到微信服务器,微信服务器会分析出接收到的信息是关于CSDN的信息,然后这个信息请求转发到CSDN的服务器上面,然后CSDN服务器会将一个CSDN登陆成功后的页面信息主动发送到浏览器上面.而CSDN服务器向浏览器主动发送信息D的这个过程就是采用的是websocet协议

3 websocket协议的解析分析

1websocket的协议格式

握手的协议格式

http协议是无状态的,不支持持久(非持久化)连接的(长连接、轮询连接除外的话)

websocket是一个持久化的协议

http和websocket协议都是基于TCP/IP协议之上,websocket可以说是基于http协议的一个持久化协议。

websocket协议连接需要以http形式发起,三次握手告诉将http协议转换为websocket协议后,之后客户端和服务端就会开启持久化的TCP信道进行信息传输。

 信息传输的协议格式

websocket协议与服务器实现_第2张图片

2 websocket如何验证客户端合法

websocket客户端发送的请求消息  

websocket协议与服务器实现_第3张图片

首先客户端在websocket的请求下消息中会有一个 

websocket服务器收到客户端发送过来的websocket请求后将sec-websocket-key 后面的base64格式的字符串后面加上一个websocket公认的GUID字符串

GUID:

 将GUID拼接在sec-websocket-key后面然后 进行sha1(哈希),将hash结果再base64编码生成一串base64编码的结果放入服务器 返回客户端的信当中进行返回,然后客户端将接收到的结果和自己的算的结果进行对比,如果一样则握手成功。

3 明文和密文如何传输

 websocket协议与服务器实现_第4张图片

如果传输明文:websocket协议与服务器实现_第5张图片的mask设置为0,payload data直接存放的是明文就可以了。makeing-key不会有值

如果传输的是密文:mask的标志位则会被设置成1,payload data会是密文,making-key有四个字节的值。用于密文的加密和解密。

加密过程

payload[i] = payload[i] ^masking-key[i%4]

解密过程(就是再异或一遍)

payload[i] = payload[i] ^masking-key[i%4]

4 websocket如何断开

直接将FIN位置1就可以,payload可以不用填数据。

4 自定义实现websocket的服务器的代码实现和关键代码展示

websocket协议交互的状态机和opcode内容和部分协议内容

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


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

typedef struct _ws_head_126 {

	unsigned short payload_length;
	char mask_key[4];

} ws_head_126;

typedef struct _ws_head_127 {

	long long payload_length;
	char mask_key[4];

} ws_head_127;

 握手的过程实现

int handshark(struct ntyevent *ev) {

	//ev->buffer , ev->length

	char linebuf[1024] = {0};
	int idx = 0;
	char sec_data[128] = {0};
	char sec_accept[32] = {0};

	do {

		memset(linebuf, 0, 1024);
		idx = readline(ev->buffer, idx, linebuf);

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

			//linebuf: Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==
			strcat(linebuf, GUID);

			//linebuf: 
			//Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

			
			SHA1(linebuf + WEBSOCK_KEY_LENGTH, strlen(linebuf + WEBSOCK_KEY_LENGTH), sec_data); // openssl

			base64_encode(sec_data, strlen(sec_data), sec_accept);

			memset(ev->buffer, 0, BUFFER_LENGTH); 

			ev->length = sprintf(ev->buffer, "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("ws response : %s\n", ev->buffer);

			break;
			
		}

	} while((ev->buffer[idx] != '\r' || ev->buffer[idx+1] != '\n') && idx != -1 );

	return 0;
}

 解析收到的数据的代码实现(此处用的是密文)

int transmission(struct ntyevent *ev) {

	//ev->buffer; ev->length

	ws_ophdr *hdr = (ws_ophdr*)ev->buffer;

	printf("length: %d\n", hdr->pl_len);

	if (hdr->pl_len < 126) { //

		
		unsigned char *payload = ev->buffer + sizeof(ws_ophdr) + 4; // 6  payload length < 126
		if (hdr->mask) { // mask set 1

			umask(payload, hdr->pl_len, ev->buffer+2);
			
		}
		printf("payload : %s\n", payload);
		
	
	} else if (hdr->pl_len == 126) {

		ws_head_126 *hdr126 = ev->buffer + sizeof(ws_ophdr);

	} else {

		ws_head_127 *hdr127 = ev->buffer + sizeof(ws_ophdr);

	}

}

全部代码连接

https://github.com/xiaoyeyihao/xioayeyihao.github.io/blob/master/websocket_server.c

5 一个疑问:既然客户端可以直接调用一个close来断开tcp的连接,为什么websocket还需要留出一个fin位来断开连接

客户端再调用close之前,先发送一个应用层的fin的包给服务器,服务器接收到这个fin的包,把对应的客户端的用户数据,业务数据做清空,然后再调用close时候,服务器调用close会比较顺畅,不会出现存在大量的close_wait的情况存在。

你可能感兴趣的:(C/C++服务器开发,websocket,服务器,网络协议)