网络7层协议
如果按照5层模型来分的话,应用层+表示层+会话层 = 应用层 我们可以看到http,rpc协议是属于应用层协议
为什么网络要分层
复杂的程序都要分层,这是程序设计的要求。就像从操作系统 -> 浏览器 -> h5应用,也是一层一层的剥离开的,就像手机制造,你也不可能从原材料开始就直接自己怼,分层的目的就是让将复杂的问题一一拆解开来解决,所以网络要分层,底层的协议和硬件设备和更贴合一点,而应用层是和业务项目等更贴合。所以也能看出应用层的协议也更多一些。而这么多应用层协议也是为了满足不同场景下的不同诉求。
rpc调用(rpc协议)与ajax调用(使用http协议)的区别
相同点
- 都是2个计算机之间的网络通信
- 需要双方约定一个数据格式
不同点
- ajax主要用在浏览器和服务端的调用,rpc主要是用来内网服务器之间的互相调用(更快更安全);之所以内网服务器之间使用rpc调用,是因为rpc调用一般是二进制传输,需要辩解码的对象是二进制数据(计算机处理起来更快),而rpc调用也更加安全(协议可以自己定)
- ajax主要是用http协议(传输方式主要是json,xml,html),rpc调用需要自己去定义一个协议(二进制协议,传输解析编码更快);
- ajax使用dns寻址(浏览器面向普通用户,所以你得对很多网址进行ip解析,用户肯定不愿意直接浏览器输入一个ip进行页面的访问,一定程度上损耗了一些性能),而rpc调用则不需要这种寻址(完全可以在内网自己整一个map通过id或者其他标识进行映射即可)
说几个场景,加深一下印象
- 当后端的微服务要互相调用接口时,这种情况下,rpc调用非常合适
- 当一个rpg游戏客户端与后端进行频繁的数据通信时,此时如果用http协议显得非常的慢,游戏体验肯定很差,这时候可以用websocket + protobuf来模拟rpc调用。
撸代码前想一想
- rpc调用使用二进制传输数据,所以核心要处理的就是编解码数据,这里想一下如果是编解码json该怎么操作?JSON.stringify, JSON.parse一下即可,非常方便。如果是编解码二进制该如何操作呢?
- rpc调用其实可以实现单工通信,半双工通信,全双工通信这几种模式的通信,那么他们有什么区别,为什么会需要这几种方式?
- rpc调用中双工通信中的序列化,粘包,该怎么处理(TCP底层传输二进制时,会将同时发出的几个包进行粘包,然后统一发出,也可能会将一个大的包分成几份发出去)
带着上面的问题咱们撸一撸代码
先整个node项目,我这里创建了3个项目,分别是双工通信,半双工通信,和单工通信。这里要实现的rpc其实是node之间的互相调用,如果是node和java调用,则还需要和java后端约定协议的格式,以及2个语言之间的差异(譬如对long number的处理)
围绕着上面的第2个问题,这几种通信方式说来也简单。
- 半双工通信 = 独木桥(同时只能从一端向另一端发送数据);
- 单工通信 = 单向行驶单车道(只能固定从一端向另一端发送数据);
- 双工通信 = 双向多车道(2端可以随意向另一端发送数据); 由次可见,大家会觉得双工通信多牛逼啊,用这一个就完事了,其实存在即合理(就像生活中有单向单车道,独木桥,双向多车道一样。。。都是根据实际的需求以及实现难度来进行合理的选择)。
这次主要一步一步实现最难实现的双工通信。
我们先让客户端和服务端进行连接
rpc对二进制的编解码是很麻烦的,尤其是对一个复杂的数据结构进行二进制写入。需要根据数据的类型长度进行判断,然后依次写入,然后偏移继续写入下一个字段,解析的时候也要根据约定好的数据格式进行一一读取,在塞入对象里。我们可以使用protobuf规范(由js实现版本),node版本我们可以使用protocal-buffers。
rpc中双工通信比较常见的问题就是假如客户端同时发送多个请求过去,后端的处理时长时不一定的不一定是先发送的就先回来,如果客户端是根据发送的先后顺序来处理后端返回,显然就会出错,这种情况比较好解决,只需要在每个包前加一个seq数字,后端处理完之后把seq原封不动返回即可。粘包问题其实也很好解决,每个包里面加一个bodylength,后端如果接受到一个大的包就可以根据bodylength进行切割,然后再一一处理。
这里主要实现一个用protocal-buffers进行数据编解码,然后通过seq序列化保证数据不出错。
- 安装protocal-buffers
- 书写proto文件(传输的数据格式,用来编解码用的)
- client.js server.js代码(直接看代码注释很多)
// client.js 代码
const net = require('net')
const fs = require('fs')
const protobuf = require('protocol-buffers')
// 用protobuf实例化一个解析器,解析器会包含encode和decode函数,可以理解为生成了一对JSON.stringify和JSON.parse函数
const schemas = protobuf(fs.readFileSync(`${__dirname}/member.proto`))
// 客户端要去主动连接服务端,所以创建的是一个socket
const socket = new net.Socket({})
// 连接服务器
socket.connect({
host: '127.0.0.1',
port: 4000
})
const memberIds = ['1', '2', '3', '4', '5']
// 包的id,自增
let seq = 0
// 编码函数,客户端发送时,只发送了id过去人员的id过去
function enCode() {
// 随机一个人员id
const memberId = memberIds[Math.floor(Math.random() * memberIds.length)]
// seq自增
seq += 1
const header = Buffer.alloc(4)
// BE是大端字节序 LE是小端字节序(这玩意就是各个硬件厂商搞的,就是字节编码是是从高到低排还是从低到高排)
// 网络协议都是采用大端编码的方式进行传输数据,所以这里我们使用be的api
header.writeInt32BE(seq)
const body = Buffer.alloc(4)
body.writeInt32BE(memberId)
// 打印出seq以及memberId
console.log('发送:seq, memberId', seq, memberId)
// 将header,body数据打包发送
const reqBuffer = Buffer.concat([header, body])
return reqBuffer
}
// 解析后端返回的数据,这里有人员信息
function deCode(buffer) {
let resStr = ''
// header存放了seq
const header = buffer.slice(0, 4)
// 这个body是包含了memberId以及memberInfo,memberId在前4个字节
const body = buffer.slice(4)
// memberInfo数据在第8个字节之后
const bodyMember = buffer.slice(8)
// 读取seq
const seq = header.readInt32BE()
// 读取memberId
const memberId = body.readInt32BE()
// memberInfo需要用再进行decode处理
const memberInfo = schemas.Member.decode(bodyMember)
// 打印接收到的数据
console.log('接收:seq,memberId,memberInfo:', seq, memberId, memberInfo)
return memberInfo
}
socket.on('data', (buffer) => {
// 收到服务端数据,进行解析
const info = deCode(buffer)
// socket.write(enCode())
})
// 每隔500毫秒会向服务端发送一个请求人员信息的数据包
setInterval(() => {
socket.write(enCode())
}, 500)
/**
* 这是服务端代码
*/
const net = require('net')
const fs = require('fs')
const protobuf = require('protocol-buffers')
const schemas = protobuf(fs.readFileSync(`${__dirname}/member.proto`))
// 可以理解这是个数据库里的数据
const datas = {
'1': {
name: '我是1号',
sex: '男',
age: 18,
},
'2': {
name: '我是2号',
sex: '女',
age: 20,
},
'3': {
name: '我是3号',
sex: '男',
age: 26,
},
'4': {
name: '我是4号',
sex: '男',
age: 28,
},
'5': {
name: '我是5号',
sex: '男',
age: 29,
}
}
// 创建一个server服务
const server = net.createServer((socket) => {
// 这里回调回来的事socket,也很好理解,服务器会处理各种其他服务器(或者客户端)和自己建立起的连接,
// 每一个socket可以了解为是该服务器和客户端建立起的一个通信通道(之后的通信都是通过这个通道进行的,各个socket之间互相独立)
socket.on('data', (buffer) => {
// 对每个请求的反应时间不一样,加一个随机数
setTimeout(() => {
// seq
const header = buffer.slice(0, 4)
// memberId
const body = buffer.slice(4, 8)
const id = body.readInt32BE()
const info = datas[id]
// console.log('info:', info, schemas.Member.encode(info), Buffer.concat([header, body, schemas.Member.encode(info)]))
// 将seq,memberId,memberInfo依次塞入buffer,和client的decode函数一一对应
socket.write(Buffer.concat([header, body, schemas.Member.encode(info)]))
}, 500 + 1000 * Math.random())
})
})
// 监听4000端口
server.listen(4000)
看下打印结果
可以看到序列为3的请求返回的数据是正确的,这里是无序请求,服务端也是无序返回。
收尾
rpc协议远没有这么简单,需要处理的事情还很多,不过我们要始终遵循一个原则就是适可而止(You Ain’t Gonna Need It,YAGNI原则) 所以找到适合你们业务的rpc协议即可。