公司本来使用了第三方提供的IM消息系统,随着业务发展须要,三方的服务有限,而且出现问题也很难处理和排查,因此此次新版本迭代,咱们的server同事呕心沥血作了一个新的IM消息系统,咱们也所以配合作了一些事情。 对于前端来讲,被告知须要用到protocol buffer,什么gui?最开始我一直没弄懂究竟是个什么东西,感受和平时接触的技术差异比较大。 还有二进制什么的,之前感受历来就没在前端使用过。 久经波折,此次的旅途学到了不少东西,因此做此博客。javascript
简称protobuf,google开源项目,是一种数据交换的格式,google 提供了多种语言的实现:php、JavaScript、java、c#、c++、go 和 python等。 因为它是一种二进制的格式,比使用 xml, json 进行数据交换快许多。以上描述太官方很差理解,通俗点来解释一下,就是经过protobuf定义好数据结构生成一个工具类,这个工具类能够把数据封装成二进制数据来进行传输,在另外一端收到二进制数据再用工具类解析成正常的数据。php
message Person { required string name = 1; required int32 id = 2; optional string email = 3; }
定义了人的类,有三个描述变量。经过protobuf编译器,把当前配置的类编译成你所须要语言的代码。 好比编译成JavaScript,这个时候会生成一个js文件,咱们重命名就叫person.js吧,里面的代码依赖google-protobuf,因此咱们要先npm google-protobuf,而后经过webpack或者browserify之类的打包工具把 google-protobuf 引入到当前 person.js 中,最后再引入到咱们的工程中。html
定义的person,前端要使用的话大体代码以下:前端
//封装 var person = new proto.protocal.Person(); person.setName('子慕'); person.setId('1'); person.setEmail('xx@xx.com'); var binary = person.serializeBinary(); //解析 var person= new proto.protocal.Person.deserializeBinary(unit8array); var obj = { name: person.getName(); id: person.getId(); email: person.getEmail(); }
前端经过websocket拿到后端下行的arrayBuffer对象,把它转化成unit8array,Person的deserializeBinary方法就能把二进制解析成Person对象,能够经过get+变量命拿到相应值。 serializeBinary方法能够直接把当前的对象转换成二进制数据,用于发送到另外一端。
可是,这样就显得很是难以使用了,甚至数据类型不少,结构也都不同,若是每次收发一个消息都要这样去处理的话,太麻烦了。这里须要进行一层封装处理,方便业务使用,封装后使用大概以下代码:java
//咱们封装生成的对象假比就叫ImInstance //发送时候直接写一个json,会自动封装 ImInstance.send({ person:{ id: '1', name: '子慕', email: 'xx@xx.com' } }) //接收也会自动,解析 var msg= ImInstance.parse(arrayBuffer);//{person:{name:'x',id:'x',email:'xxx'}}
假如咱们后端是php,前端是web,protobuf生成一个两个语言的工具类,相互通讯都要经过各自的类解析和封装,以下图:python
实际是咱们会有三个前端:webpack
一个消息系统是须要长链接的,前端须要随时接收消息,APP使用了tcp长链接,前端就是websocket了。 websocket也是基于tcp的,至关于在tcp基础上封装了一层。 某种程度来讲tcp的性能优于websocket,由于websocket就是在tcp的基础上多了一层转化,可是websocket使用更简单,用tcp的app端须要本身去读tcp流,根据包头和包体组装数据包,而websocket不须要,由于websocket会是一个整包的数据并非流的形式。 具体来讲,后端经过缓存区把数据冲刷(flush)给前端,app端拿到tcp数据流,须要根据消息头给定的消息体长度,去拿取后面多少位的数据,而后组装成一个数据包。 而websocket传输过来就是一个个的包,也就是帧并非数据流,因此后端在给websocket数据的时候,必需要把一个整包,在缓冲区一次性冲刷过来,而给tcp的话就能够自由冲刷。c++
(引用)概念上,WebSocket确实只是TCP上面的一层,作下面的工做:git
前端也许不多会接触到二进制,至少我没怎么接触过。 以前说的二进制传输,经过设置websocket对象的binaryType属性: binaryType = 'arraybuffer'(若是没有配置默认返回的是个Blob对象,protobuf解析时会报错),消息下行的时候 onmessage 拿到的 MessageEvent.data 会是一个ArrayBuffer对象,如图:github
关于ArrayBuffer,MDN解释: ArrayBuffer对象被用来表示一个通用的,固定长度的二进制数据缓冲区。你不能直接操纵ArrayBuffer的内容;
相反,你应该建立一个表示特定格式的buffer的类型化数组对象(typed array objects)或数据视图对象DataView
来对buffer的内容进行读取和写入操做。
类型化数组(typed array objects)有下图这些类型:
实际就是一个ArrayBuffer咱们是不能直接操做它的,须要转成能够操做的对象类型,咱们是须要转换成Unit8Array,好比这样:
var unit8= new Uint8Array(arrayBuffer);
可是我发如今微信里这样用会报错,在手机默认的浏览器里仍是好的,看来还存在必定兼容问题。后来用到DataView才没问题的:
var dataview = new DataView(arrayBuffer); var unit8= new Uint8Array(dataview.buffer, dataview.byteOffset, dataview.byteLength);
兼容问题不止这一点,在phone5测试的时候,一直有问题(同事说那台手机被苹果封过,不晓得会不会和这个有关系),一步步查下去,发现是Unit8Array一些方法在phone5里显示undefined,好比 Unit8Array.slice 和 Unit8Array.from,把 Unit8Array.slice用 Unit8Array.subarray 替换,Unit8Array.from 用 new 替换,像这样:Uint8Array.from([1, 0, 0]) == new Uint8Array([1, 0, 0]),目前来讲就没出现其余兼容问题了。
咱们会封装一个独立的websocket类,处理websocket的创建、链接、重连、心跳、监听等,提供一些钩子函数,配合前面说的ImInstance实现业务功能。长链接确定是会出现断开或者弱网等一系类状况,保证业务的健壮和稳定性,须要作心跳重连。这块以前的博客已经写过,此次项目以后又对代码和博客进行了一些完善,具体能够看以前的博客《初探和实现websocket心跳重连》和心跳的github源码《https://github.com/zimv/WebSocketHeartBeat》。
下面两个问题有一个知识点: Number类型统一按浮点数处理,64位(bit)存储,整数是按最大54位(bit)来算最大最小数的,不然会丧失精度;某些操做(如数组索引还有位操做)是按32位处理的。
1.位移运算:
每一条消息有个惟一id,id是根据时间戳加上一些其余参数再经过位移运算得出的。 自己根据id能够得出时间,因此就没有专门给时间的字段,这里就须要前端对id进行一次运算,得出时间,可是我在作位移操做的时候发现得出的值不对。 后来才查到了上面的知识点。 server给咱们的是64位的int,可是js的位移是按照32位处理的,因此得出的值不对,后来邱桑找到了一个Long.js库,它能够把64位整数拆分红两个32位的去计算,最后我就获得了正确的时间。Long.js
2.number丢失精度:
由于js的整数最大只支持到54bit,范围在 −9007199254740992 到 9007199254740992,而咱们的id是超过了54bit的(这一点受到了后端同事的疯狂嘲笑)。 在作消息回执(收到一条消息,发送当前消息的id给后端,告知我收到这个消息了)的时候,由于超过了js的最大值,因此前端传出去的id就会是错误的。 好比后端返回了一个id为111111111111111111的值(18个1),前端经过protobuf类解析以后拿到的值直接变成了111111111111111100(16个1加2个0),由于超过了最大值,js用0来占位显示,这样回执给后端的id就是111111111111111100了。 我觉得当前存放数字的变量就已是这个值了,我无论作什么都没用了,那么我但愿后端给我一个字符串的id我才好处理(发现这个问题的时候项目正在准备上线),可是邱桑以为这样多一个字段太浪费。 后来他查了一些资料告诉我,就用Long.js,它能够帮我转换成正确的字符串,我不信,我认为js存不到那么大的数据,js直接把数据给丢失了,而邱桑说值实际还在内存里精度没有丢失,只是js展现不出来,并且很是确定,我当时不信,在他强烈的要求下,我使用了Long.js的转换方法,结果他是对的。 虽然收到的值超过了js的范围,可是数值仍然是原封不动的在内存里,这个也是被狠狠的打了一下脸,果真仍是邱桑厉害! Long.js的代码量仍是比较多,当时我想我只用位移就把位移的相关代码抽出来整合了一下,这样比较节约。 后来发现我如今说的这个问题也须要用到Long.js的其它方法,我又尝试抽离,发现要抽的代码太多了,后来干脆就直接把Long.js所有引入进来了(装逼失败)。
ps:因为当时咱们的id是18位的number,经过long.js转换是没有问题的。可是后面id到19位之后,全部的结果都再也不正确了。js中的number超过安全限制之后,开始变得不安全,有些19位的number能够解析成功,有些不能够,当超过20位之后几乎所有出问题。因此咱们的结论是id若是可能特别长,尽可能用string。
4.websocket断线重连把本身踢下线的问题:
咱们会避免用户重复登陆websocket,若是当前用户第二次链接websocket的话 会把上一次登陆的一端给踢下线,被踢下线的一端会收到一个消息,当收到踢下线的消息以后我便不会进行重连。 由于网络缘由、异常缘由或者后端主动要求我重连,我便会去进行重连,可是有时候出现就在同一个地方执行了重复链接,实际都是本身这一个端,那么就会出现登陆上以后,又收到踢下去的消息,把本身给踢下去了,踢下去就不会再重连了,这样就永久断开了,这属于逻辑没控制好。 解决这个问题是首先要保证重连以前先主动对当前的websocket执行一次close,close的时候后端是会收到断开的通知,这样咱们再去链接就不会重复登陆了。
此次本身碰到不少不熟悉的知识,也问了server同事不少问题,学到不少,有靠谱的大牛同事就是爽! 也出过一些bug和问题,屡次反复追溯才查出问题的根源,有时候1个bug多是几个地方代码写错形成的问题。 第一个版本已经顺利上线,后面还有不少重要的工做要作,单从前端来讲,还须要把封装的websocket和ImInstance写得更好,文档,扩展性这些都要考虑(已是一个公共类了,之后还会做为sdk开放给三方平台);还须要作一个监控展现,帮助实时监控服务器CPU,带宽,性能等。 经历了一次大版本的迭代,加了一个月的班,熬了几天夜,和团队一块儿在进步,收获到这么多经验包也是很开心的。