您当前位于: 首页 » web前端编程, 互联网2.0, 网络通信协议 » websocket 通信协议(已更新到version 13)

websocket 通信协议(已更新到version 13)05/11/2010

UPDATE:前些天有网友mail和我讨论websocket协议,当时颇忙!更惭愧的是,此篇文章竟已不能使用,原因是当时我写的还是websocket草稿时候的协议!终于,我将websocket 协议更新到了version 13版本 —–2012.5.19

websocket通信协议实现的是基于浏览器的原生socket,在客户端用JS即可轻松完成,前些天都在学习websocket 协议(但实际上websocket 协议甚为简约),并且粗略的思考过websocket的对于下一代web应用会产生怎样的影响,我想最大的巨变应该是就是实时性上吧!另外诸如上传大文件之类的优于http的应用。但问题也随之而来,服务端怎么办?前些天我弄了个websocket 聊天室的demo,现在还得在服务器上专门开个进程来跑呢,也许到时候不再是简单架设个web server就能跑应用的了。也许过不了多久,会出不同的服务端方案吧!先期待一下。

websocket的协议是很简单的,这里我把它分成客户端和服务端来讲。在客户端,new WebSocket即可实例化一个新的websocket对象,但其参数略微有一点不一样,参数格式是这样的ws://yourdomain:port/path ,这个从我的聊天室demo里面就可以轻松看出(ws = new WebSocket( “ws://www.zendstudio.net:9108/chat” ); ),WebSocket对象会自动解析这段字符串,发送到指定服务器端口,首先执行的是双方握手(handshake),客户端发送数据格式类似这样:

GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: www.zendstudio.net:9108
Origin: http://www.zendstudio.net
Sec-WebSocket-Key: U00QUfV1CRfIIU0NkcUCnA==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: x-webkit-deflate-frame

这很是有些类似于http的头信息,同样每行都是以”\r\n”结尾的,上面这段格式无需我们去构造,WebSocket对象会自动发送,对客户端这是透明的。此时服务端应该返回的信息是:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 7GGzyIJjf9bX7pej+3tc5Vv87S0=
WebSocket-Origin: http://www.zendstudio.net
WebSocket-Location: ws://www.zendstudio.net:9108/chat

从这里我们太容易看出来,websocket协议的握手部分根本就是个类http的协议,所不同的是http每次都会有这样子的头信息交互,这在某些时候不得不显得很糟糕。而websocket只会执行一次这个过程,之后的传输信息就变得异常简洁了。

2012.5.19新增内容)这里我们发现,version 13这个版本的websocket协议,与我之前文中叙述之最大不同就是多了一个验证,客户端会发送一个“Sec-WebSocket-Key”的base64编码的密钥,要求服务端必须返回一个“Sec-WebSocket-Accept”,否则客户端会抛出一个“Error during WebSocket handshake: Sec-WebSocket-Accept mismatch”错误之后,关闭连接,当然,这个Sec-WebSocket-Accept的值是计算出来的,胡乱的返回也是要遭到历史唾弃的。Sec-WebSocket-Accept的算法很简单:将客户端提交的Sec-WebSocket-Key+”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″,然后sha1,然后base64_encode,以下列出我用nodejs写的代码片段:

sha1 = crypto.createHash('sha1');
sha1.update(headers["Sec-WebSocket-Key"]+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
ws_accept=sha1.digest('base64');

客户端在握手成功后,会触发WebSocket对象的onopen事件,告诉客户端连接已经成功建立了。客户端的WebSocket对象一共绑定了四个事件:1、onopen:连接建立时触发;2、onmessage:收到服务端消息时触发;3、onerror:连接出错时触发;4、onclose:连接关闭时触发;有了这4个事件,我们就可以很容易很轻松的驾驭websocket,并且需要说明的是websocket支持二进制数据的传输,因此,它远不止聊天室应用这么简单。

服务端呢?服务端也是非常简单的,但是仍然需要注意的问题是,作为服务器,安全和性能是不可忽略的,除此之外,只管往socket里面写数据就可以了,websocket的通信数据全部是以”\x00″开头以”\xFF”结尾的,无论是服务端发出的数据还是客户端发送的数据都遵从这个格式,唯一不同的是客户端的WebSocket对象能够自动将头尾去除,获得主体数据,这就省却了我们在客户端处理原始数据的必要,真是个体贴周到的对象啊!顺便说一句,WebSocket通信数据的编码总是UTF-8格式的

服务端相对于草稿版还是改动很大的,从Sec-WebSocket-Extensions部分的声明来看,websocket是会支持压缩的,现在这个version 13版本已经不是当初的utf-8传字符的版本了,而是完全的二进制传输,服务端和客户端的通信经过了mask转换,这样就不太容易看出原文的内容了。这其中的算法颇有些复杂,并非一两句能够说的清楚,这里我写了两个函数来打包和解包数据:

/**
 *
 * 打包数据,实际上payload_len == 127时的打包方法是有待商榷的,这里先这样简单实现
 *
 **/
function parse_msg(data){
	data = data || null;
	if( ( data.length <= 0 ) || ( !Buffer.isBuffer(data) ) ){
		return null;
	}
 
	var mask_flag = (data[1] & 0x80 == 0x80) ? 1 : 0;//All frames sent from client to server have this bit set to 1.
	var payload_len = data[1] & 0x7F;//0111 1111
 
	if( payload_len == 126 ){
		masks = data.slice(4,8);
		payload_data = data.slice(8);
		payload_len = data.readUInt16BE(2);
	}else if( payload_len == 127 ){
		masks = data.slice(10,14);
		payload_data = data.slice(14);
		payload_len = data.readUInt32BE(2) * Math.pow(2,32) + data.readUInt32BE(6);
	}else{
		masks = data.slice(2,6);
		payload_data = data.slice(6);
	}
	//console.log(payload_len);
	//console.log(payload_data.length);
	for( var i=0;i< payload_len;i++ ){
		payload_data[i]= payload_data[i] ^ masks[i%4];
	}
 
	return payload_data;
}
 
/**
 * 很简陋的实现打包,并且不支持mask,不支持其他命令,不支持拆包、装包、不支持大于16位长度的数据……
 **/
function build_msg( str_msg, mask ){
	str_msg = str_msg || "";
	mask = mask || false;
 
	var msg_len = Buffer.byteLength(str_msg,"utf-8"), packed_data;
	if( msg_len <= 0 ){
		return null;
	}
 
	if( msg_len < 126 ){
		packed_data = new Buffer(2+msg_len);
		packed_data[0] = 0x81;
		packed_data[1] = msg_len;
		packed_data.write( str_msg, 2 );
	}else if( msg_len <= 0xFFFF ){//用16位表示数据长度
		packed_data = new Buffer(4 + msg_len);
		packed_data[0] = 0x81;
		packed_data[1] = 126;
		packed_data.writeUInt16BE( msg_len, 2 );
		packed_data.write( str_msg, 4 );
	}else{//用64位表示数据长度
		/*packed_data = new Buffer(10+msg_len);
		packed_data[0] = 0x81;
		packed_data[1] = 127;
		packed_data.writeUInt32BE(msg_len & 0xFFFF0000 >> 32, 2);
		packed_data.writeUInt32BE(msg_len & 0xFFFF, 6);
		packed_data.write( str_msg, 10 );*/
	}
 
	return packed_data;
}

这两个都是没有完整实现的方法,要完美实现,得需要根据rfc6455来做了。(另外我同时把demo给更新了,欢迎围观

好了,websocket协议就是这么简单。到这里,写一个服务端应该不是什么困难的事情了吧?这仅仅需要一点点socket编程知识,任何语言都可以轻松实现。另外,我想说下源码的事情,有童鞋给我留言希望看看我的服务端源码,我想想还是算了,当我公布的源码徒增自己一堆麻烦,因为一部分人把我当成写应用的了,他们总是会说:“这代码怎么不能用?”,或者说“你能再修改下源代码,以便实现下我们公司当前需要用到的XXX功能吗?”我本为技术交流,之前公布飞信php源代码的时候,就遇到太多这样的情况,我并没有为飞信php建立项目,我不可能花很多时间去跟踪飞信协议变化,不断维护我的代码。同样的,这次的websocket php服务端源代码我也不打算献丑了,还是不了。非常感谢大家持久以来的支持,希望我们能继续讨论技术本身。

参考文章:https://dev.w3.org/html5/websockets/
https://tools.ietf.org/html/rfc6455

| 28条评论 标签:  

28条评论
  1. 有空来玩玩说道:

    PHP的socket性能不是太好。主要是不支持线程。我现在也写了一个SOCKET服务端。用C实现的。可以利用CLI调用PHP进行处理。应该比直接用PHP要好些。准备研究下hiphop用socket如何。

    • gently说道:

      我咋看不太明白你在做什么?用C实现的东西为什么又利用PHP进行调用处理?难道是用C实现的PHP扩展,还是用C写的进程管理,调用PHP CLI模拟多线程?

    • howlanderson说道:

      恩,我也有这方面需求,能不能和你联系呢,好好交流交流,我QQ:三5996153务 (部分数字用汉字替代了)加我时 注明 websocket 我不加陌生人所以不注明 我是会拒绝的拉,谢谢拉

    • 小红帽说道:

      PHP的socket性能其实不比c差多少的,我写了一个多进程的php socket 框架,四核PC上压力测试helloworld QPS能够达到3W/S,如果是和客户端长连接,QPS能够达到9.7W,cpu使用率为55%,代码就不发出来了,我怕下半身永久性创伤 : )

  2. 有空来玩玩说道:

    C写的socket服务端,在逻辑处理时调用PHP来处理(在每一个线程里利用管道来调用)。当然,这种方式没有直接使用C来处理效率高,但PHP写逻辑部分相对简单嘛。而且这段时间研究hiphop-php后发现,php转换成C++是件轻松的事情,转换后,可以在C里使用vfork+exec进行调用。相信表现会不错的。

  3. lixiphp说道:

    聊天室 demo 不对啊?怎么无法发送呢?websocket第一次见识到。。。

  4. Eva说道:

    我一直没明白怎么启动pywebsocket

  5. zhuowater说道:

    协议都升级了,加了安全部分啊

  6. zhuowater说道:

    我放到我的blog里了,http://zhuowater.appspot.com/

  7. Witeman说道:

    担心服务器的性能和安全,用Erlang吧,十几行就可以写一个websocket的服务器端。看看,misultin项目。

  8. howlanderson说道:

    楼主大人,能和你联系么?交流交流技术阿,我正好也在高websocket,但是很多地方不懂阿,希望指导知道阿

  9. 本文有更新说道:

    更新了websocket协议,现在是最新版的协议了哇,还有海量demo,太棒了

  10. Thyiad说道:

    咳。。我可以弱弱地问句吗:客户端与服务端握手的这个数据是怎么查看到的。。

    • gently说道:

      在我提到的参考文章里呀

      • Thyiad说道:

        知道,不过我是指怎么在调试中查看浏览器发送过来的是什么数据。

        • gently说道:

          chrome的developer tools 就可以查看websocket协议呀

          • Thyiad说道:

            呵呵,谢谢。
            我想问一下:ws://yourdomain:port/path,这里面的path应该填什么?
            我试着建立了一个websocket的网站,局域网内用ip访问没问题,不过跨域访问时,怎么都连不起来,domain这个时侯用的域名了,port是socket服务监听的端口,path该填什么?

            • yuheshuang说道:

              请问你的问题解决了么?怎么解决的?我也遇到这个问题了

        • nonocast说道:

          wireshark,无敌了

  11. helloweb说道:

    麻烦楼主加下扣扣,有几个问题想请教下。扣扣:七9210三三68

  12. rainyluo说道:

    不错
    php目前有pthread可以实现真正的多线程了
    在配合libevent库,应该可以在性能上提升不少.

    socket这块用golang做还是比较方便的,比PHP在性能上有不少优势.平均一个链接4k内存,32g内存能上80w左右连接,不知道PHP的内存怎么样.到80w还是有困难吧.
    当然要写逻辑go就比php麻烦了.哎

  13. kenkyoken说道:

    请问一下,为何rfc6455里面这样规定“A server MUST NOT mask any frames that it sends to the client.” 不允许服务端的数据增加掩码的用意何在?

  14. 海参说道:

    用swoole扩展吧,http://www.swoole.com/

  15. codder说道:

    workerman-chat是一个基于websocket 的 PHP 多进程聊天室,有兴趣可以看看

发表评论