公司原本使用了第三方提供的IM消息系统,随着业务发展需要,三方的服务有限,并且出现问题也很难处理和排查,所以这次新版本迭代,我们的server同事呕心沥血做了一个新的IM消息系统,我们也因此配合做了一些事情。 对于前端来说,被告知需要用到protocol buffer,什么gui?最开始我一直没弄懂到底是个什么东西,感觉和平时接触的技术差别比较大。 还有二进制什么的,以前感觉从来就没在前端使用过。 久经波折,这次的旅途学到了很多东西,所以作此博客。
protocol buffer:简称protobuf,google开源项目,是一种数据交换的格式,google 提供了多种语言的实现:php、JavaScript、java、c#、c++、go 和 python等。 由于它是一种二进制的格式,比使用 xml, json 进行数据交换快许多。以上描述太官方不好理解,通俗点来解释一下,就是通过protobuf定义好数据结构生成一个工具类,这个工具类可以把数据封装成二进制数据来进行传输,在另一端收到二进制数据再用工具类解析成正常的数据。
为什么用protobuf(以下是后端大大“邱桑”的意思): 优:
1.json占用流量大,用了protobuf的二进制传输会帮助传输更轻量,节约用户和服务端流量 。之前旧消息系统使用json的时候发现,当一台服务器访问量很大的时候,cpu占用很低,但是带宽已经满了,服务器承载量也就满了。
2.json太随意太灵活了,字段想加就加,PM提的需求后来维护的人不考虑什么就加上去, 搞乱架构,用protobuf,一看到这东西,就会谨慎。
3.生成代码对于除了javascript来说的其它语言,真的就是福利,用json的话,后端要写好多类,去把对象解析到类上,然后遇到有子对象,还要再解析,都是体力活,传上来一个protobuf,后端拿到就可以decode了,不用再关心类型,不用再去挨个判断做解析,很开心啊,另外,虽然php也是弱类型,拿到json也可以decode成数组或stdClass,但那个没有意义,不是具体的业务实体, 代码依旧很难维护。(对于JavaScript这样的语言来说,确实不是福利,用json会更好操作,引入编译和解析二进制数据不仅增加了工作量,还要对protobuf生成的js再次做一次封装方便业务开发,加大了业务复杂度)
4.有一定安全性, 传输过程中是二进制,抓包是看不出具体数据的,并且是自己定义protobuf生成的代码,才能正确解析出数据(有点类似于对称加密)。 前端代码压缩过,即便想通过前端来看懂,也比较费劲,只能说比一般的json安全性高一些。
劣:1.可读性比较差,需要通过工具来解析
2.对于JavaScript,没有json友好,会增加解析和封装的工作量。
使用protobuf:
对于web前端来说,用它主要是能为用户节约流量,但是业务代码变得多了一层,会复杂一点,但是确实对传输性能做了很大提升。下面我们来看看大致的使用流程。使用protobuf,你需要安装protobuf编译器,然后通过.proto文件配置自定义的数据格式,如下代码(person.proto):
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 中,最后再引入到我们的工程中。
定义的person,前端要使用的话大致代码如下:
person = new proto.protocal.Person(); person.setName('子慕'); person.setId('1'); person.setEmail('xx@xx.com'); var binary = person.serializeBinary(); 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方法可以直接把当前的对象转换成二进制数据,用于发送到另一端。
但是,这样就显得非常难以使用了,甚至数据类型很多,结构也都不一样,如果每次收发一个消息都要这样去处理的话,太麻烦了。这里需要进行一层封装处理,方便业务使用,封装后使用大概如下代码:
//我们封装生成的对象假比就叫ImInstance //发送时候直接写一个json,会自动封装 ImInstance.send({ person:{ id: '1', name: '子慕', email: 'xx@xx.com' } }) msg= ImInstance.parse(arrayBuffer);//{person:{name:'x',id:'x',email:'xxx'}}
假如我们后端是php,前端是web,protobuf生成一个两个语言的工具类,相互通信都要通过各自的类解析和封装,如下图:
实际是我们会有三个前端:
长连接:
一个消息系统是需要长连接的,前端需要随时接收消息,APP使用了tcp长连接,前端就是websocket了。 websocket也是基于tcp的,相当于在tcp基础上封装了一层。 某种程度来说tcp的性能优于websocket,因为websocket就是在tcp的基础上多了一层转化,但是websocket使用更简单,用tcp的app端需要自己去读tcp流,根据包头和包体组装数据包,而websocket不需要,因为websocket会是一个整包的数据并不是流的形式。 具体来说,后端通过缓存区把数据冲刷(flush)给前端,app端拿到tcp数据流,需要根据消息头给定的消息体长度,去拿取后面多少位的数据,然后组装成一个数据包。 而websocket传输过来就是一个个的包,也就是帧并不是数据流,所以后端在给websocket数据的时候,必须要把一个整包,在缓冲区一次性冲刷过来,而给tcp的话就可以自由冲刷。
(引用)概念上,WebSocket确实只是TCP上面的一层,做下面的工作:
ArrayBuffer: