广义上区分,通信协议可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好。绝大多数的私有协议传输层都基于TCP/IP,所以利用Netty的NIO/TCP协议栈可以非常方便地进行私有协议的定制和开发。
私有协议本质上是厂商内部发展和采用的标准,除非授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己制订,协议实现细节不愿公开,只在企业自己生产的设备之间使用的协议。由于现代软件系统的复杂性,一个大型软件系统往往会被人为地拆分成多个模块,另外随着移动互联网的兴起,网站的规模也越来越大,业务的功能越来越多,为了能够支撑业务的发展,往往需要集群和分布式部署,这样,各个模块之间就要进行跨节点通信。
在传统的Java应用中,通常使用以下4种方式进行跨节点通信。
(1)通过RMI进行远程服务调用;
(2)通过Java的Socket+Java序列化的方式进行跨节点调用;
(3)利用一些开源的RPC框架进行远程服务调用,例如Facebook的Thrift、Apache的Avro等;
(4)利用标准的公有协议进行跨节点服务调用,例如HTTP+XML、RESTfuI+JSON或者WebService;
跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应消息进行编解码。在请求和应答消息本身以外,也需要携带一些其他控制和管理类指令,例如链路建立的握手请求和响应消息、链路检测的心跳消息等。当这些功能组合到一起之后,就会形成私有协议。
协议栈功能描述
Netty协议栈承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下。
1.基于Netty的NIO通信框架,提供高性能的异步通信能力.
2.提供消息的编解码框架,可以实现POJO的序列化和反序列化.
3.提供基于IP地址的白名单接入认证机制.
4.链路的有效性校验机制.
5.链路的断连重连机制·
通信模型如图:
具体步骤如下:
1.Netty协议栈客户端发送握手请求消息,携带节点ID等有效身份认证信息.
2.Netty协议栈服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和地址合法性校验,校验通过后,返回登录成功的握手应答消息.
3.链路建立成功之后,客户端发送业务消息.
4.链路成功之后,服务端发送心跳消息.
5.链路建立成功之后,客户端发送心跳消息.
6.镒路建立成功之后,服务端发送业务消息,私有协议栈开发.
7.服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。
消息定义:
名称 | 类型 | 长度 | 描述 |
---|---|---|---|
header | Header | 变长 | 消息头定义 |
body | Object | 变长 | 请求消息:参数 响应消息:返回值 |
NettyMessage.java
package com.viagra.chapter12.protocol;
/**
* @Auther: viagra
* @Date: 2019/8/3 09:29
* @Description:
*/
public final class NettyMessage {
private NettyMessageHeader header;
private Object body;
public NettyMessageHeader getHeader() {
return header;
}
public void setHeader(NettyMessageHeader header) {
this.header = header;
}
public Object getBody() {
return body;
}
public void setBody(Object body) {
this.body = body;
}
@Override
public String toString(){
return "NettyMessage [header="+header+"]";
}
}
Header:
header | 类型 | 长度 | 描述 |
---|---|---|---|
crcCode | int | 32 | Netty消息校验码(三部分) 1. 0xABEF:固定值,表明消息是Netty协议消息,2字节 2. 主版本号:1~255,1字节 3.次版本号:1~255,1字节 crcCode=0xABEF+主版本号+次版本号 |
length | int | 32 | 消息长度,包括消息头,消息体 |
sessionID | long | 64 | 集群节点全局唯一,由会话生成器生成 |
type | Byte | 8 | 0:业务请求消息 1:业务响应消息 2:业务ONE-WAY消息(既是请求又是响应) 3:握手请求消息 4:握手应答消息 5:心跳请求消息 6:心跳应答消息 |
priority | Byte | 8 | 消息优先级:0~255 |
attachment | Map<String,Object> | 变长 | 可选,用于扩展消息头 |
NettyMessageHeader.java
package com.viagra.chapter12.protocol;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: viagra
* @Date: 2019/8/3 09:30
* @Description:
*/
public class NettyMessageHeader {
private int crcCode = 0xabef0101;
private int length;
private long sessionId;
private byte type;
private byte priority;
private Map<String, Object> attachment = new HashMap<String, Object>();
public int getCrcCode() {
return crcCode;
}
public void setCrcCode(int crcCode) {
this.crcCode = crcCode;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public long getSessionId() {
return sessionId;
}
public void setSessionId(long sessionId) {
this.sessionId = sessionId;
}
public byte getType() {
return type;
}
public void setType(byte type) {
this.type = type;
}
public byte getPriority() {
return priority;
}
public void setPriority(byte priority) {
this.priority = priority;
}
public Map<String, Object> getAttachment() {
return attachment;
}
public void setAttachment(Map<String, Object> attachment) {
this.attachment = attachment;
}
public String toString(){
return "Header [crcCode=" + crcCode + ", length=" + length + ", sessionID=" + sessionId
+ ", type=" + type + ", priority=" + priority + ", attachment=" + attachment + "]";
}
}
设计
链路的关闭
由于采用长连接通信,在正常的业务运行期间,双方通过心跳和业务消息维持链路,任何一方都不需要主动关闭连接。
但是,在以下情况下·客户端和服务端需要关闭连接。
(1)当对方宕机或者重启时,会主动关闭链路,另一方读取到操作系统的通知信号,得知对方REST链路,需要关闭连接,释放自身的句柄等资源。由于采用TCP全双工通信,通信双方都需要关团连接,释放资源;
(2)消息读写过程中,发生了I/O异常,需要主动关闭连接;
(3)心跳消息读写过程中发生了l/O异常,需要主动关闭连接;
(4)心跳超时·需要主动关闭连接;
(5)发生编码异常等不可恢复错误时,需要主动关闭连接。
可靠性设计
Netty协议栈可能会运行在非常恶劣的网络环境中.网络超时、闪断、对方进程僵死或者处缓慢等情况都有可能发生.为了保证在这些极端异常场景下Netty协议栈仍能够正常工作或者自动恢复,需要对它的可靠性进行统一规划和设计。
心跳机制
在凌晨等业务低谷期时段,如果发生网络闪断、连接被Hang住等网络问题时,由于没有业务消息,应用进程很难发现到了白天业务高峰期时,会发生大量的网络通信失畋,严重的会导致一段时间进程内无法处理业务消息.为了解决这个问题在网络窄闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。
貝体的设计思路如下·
(1)当网络处于空困状态持续时间达到(连续周期T没有读写消息)时,客户端主动发送Ping心跳消息给服务端的
(2)如果在下一个周期T到来时客户端没有收到对方发送的Pong跳应答消息或者读取到服务端发送的其他业务消息,則心跳失败计数器加1
(3)每当客户端接收到服务的业务消息或者Pong应答消息时,将心跳失敗计数器清零;连续N次没有接收到服务端的Pong消息或者业务消息,則关闭路,间隔INTERVAL时间后发起重连操作。
(4)服务景网络窄闲状态持续时间达到T后,服务端将心跳失败计数器加只要接收到客户端发送的Ping消息或者其他业务消息,计数器清零·
(5)服务端连缕N次没有接收到客户端的Ping消息或者其他业务消息.则关闭涟路,释放资源,等待客户端重连·
通过Ping-Pong双向心跳机制,可以保证无论通信哪一方出现网络故障,都能被及时地检思出来,为了防止由于对方时间内繁忙没有及时返回应答造成的误判,只有连续N次心跳检都失畋才认定链路己经损害,需要关闭涟路并重建链路·当读或者与心跳消息发生以)异常的时候,说明链路已经中断.此时需要立即关闭链路,如果是客户端,需要重新发起连接.如果是服务端,需要清窄缓存的半包信息,等待客户端重连。
重连机制
如果链路中断,等lNTERVAL时间后,中客户端发起重连操作,如果重连失败,间隔周期TERVAL后冉次发起重连,直到重连成功。为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户需要等待INTERVAL时间之后冉发起重连,而不是失败后就立即重连·
为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放,包括但不限于SocketChannel、Socket等“重连失畋后,需要打印异常堆栈信息,方便后续的向題定位.
重复登录保护
当客户端握手成功之后·在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽的服务端接收到客户端的握手请求消息之后,首先对IP地址进行合法性检验,如果校验成功.在缓存的地斛表中查看客户端是否己登录。如果已经登录.则拒绝重复登录,返回错误码,同时关闭TCP链路,并在服务的日志中打印握手失效的原因·
客户端接收到握手失敗的应答消息之后,关闭客户端的TCP连接,等待INTERVAL时间之后,再次发起TCP连接,直到认证成功。
为了防止服务端和客户端对链接状态理解不一致导致的客户端无法握手成功的问题,当服务瑞连续N次心跳超时之后需要主动关閉链路,清空该客户端的地址缓存信息,以保证后续该客户端可以重连成动,防止被重复登录保护机制拒绝掉。
消息缓存重发
无论客户端还是服务端,当发生路中断之后,在链潞恢复之前,缓存在消息队列中,待发送的消息不能丢失,等路恢复之后,重新发送这些消息,保证链路中断期司消息不去失。考虑到内存溢出的风险,建议消息缓存队列设置上限,当达到上限之后,应该拒绝继续向该队列添加新的消息。
实现
握手的发起是在客户端和服务端TCP链路建立成功通道激活时,握手消息的接入和安全认证在服务端处理。
首先开发一个握手认证的客户端ChannelHandler,用于在通道激活时发起握手请求,具体代码实现如下。
LoginAuthReqHandler
package com.viagra.chapter12.protocol;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
* @Auther: viagra
* @Date: 2019/8/3 11:33
* @Description:
*/
public class LoginAuthReqHandler extends ChannelHandlerAdapter{
public void channelActive(ChannelHandlerContext ctx) throws Exception {
NettyMessage nettyMsg = buildLoginReq();
System.out.println("client send login auth request to server:"+nettyMsg);
ctx.writeAndFlush(buildLoginReq());
}
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
NettyMessage nettyMsg = (NettyMessage)msg;
if(nettyMsg!=null&&nettyMsg.getHeader()!=null){
if(nettyMsg.getHeader().getType()==MessageType.LOGIN_RESP_SUCCESS
||nettyMsg.getHeader().getType()==MessageType.LOGIN_RESP_FAILT){
System.out.println("client received login auth response from server:"+nettyMsg);
}
ctx.fireChannelRead(msg);
}
}
private NettyMessage buildLoginReq() {
NettyMessage message = new NettyMessage();
NettyMessageHeader header = new NettyMessageHeader();
header.setType(MessageType.LOIGN_REQ);
message.setHeader(header);
message.setBody("It is request");
return message;
}
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
}
}
服务端
package com.viagra.chapter12.protocol;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
* @Auther: viagra
* @Date: 2019/8/3 10:59
* @Description:
*/
public class LoginAuthRespHandler extends ChannelHandlerAdapter {
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
NettyMessage message = (NettyMessage) msg;
if(message!=null&&message.getHeader()!=null){
if(message.getHeader().getType()==MessageType.LOIGN_REQ){
System.out.println("server recevied login auth msg from client :" + message);
ctx.writeAndFlush(buildLoginResponse());
}else{
ctx.fireChannelRead(msg);
}
}
}
private NettyMessage buildLoginResponse() {
NettyMessage message = new NettyMessage();
NettyMessageHeader header = new NettyMessageHeader();
header.setType(MessageType.LOGIN_RESP_SUCCESS);
message.setHeader(header);
return message;
}
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
}
}
3.心跳机制实现
有两种方式一种用netty自带的,一种自己实现
Netty提供了对心跳机制的天然支持,Netty4.0提供了一个类,名为IdleStateHandler,这个类可以对三种类型的心跳检测
这个类的构造参数是这样的:
前三个的参数解释如下:
1)readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)
2)writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)
3)allIdleTime:所有类型的超时时间
这个类主要也是一个ChannelHandler,也需要被载入到ChannelPipeline中,加入我们在服务器端的ChannelInitializer中加入如下的代码:
package com.viagra.chapter12.protocol;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import java.util.concurrent.TimeUnit;
/**
* @Auther: viagra
* @Date: 2019/8/3 11:32
* @Description:
*/
public class HeartBeatReqHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
NettyMessage nettyMsg = (NettyMessage) msg;
if(nettyMsg!=null&&nettyMsg.getHeader()!=null){
if(nettyMsg.getHeader().getType()==MessageType.LOGIN_RESP_SUCCESS){
ctx.executor().scheduleWithFixedDelay(new HeartBeatTask(ctx), 0, 5, TimeUnit.SECONDS);
}else if(nettyMsg.getHeader().getType()==MessageType.HEARBEAR_RESP){
System.out.println("client recived heart beat msg :"+nettyMsg);
}else{
ctx.fireChannelRead(msg);
}
}
}
private class HeartBeatTask implements Runnable{
private final ChannelHandlerContext ctx;
private NettyMessage hearBeatMsg;
public HeartBeatTask(ChannelHandlerContext ctx){
this.ctx = ctx;
this.hearBeatMsg = buildHearBeatMsg();
}
@Override
public void run() {
System.out.println("client send heart beat msg :"+hearBeatMsg);
ctx.writeAndFlush(hearBeatMsg);
}
private NettyMessage buildHearBeatMsg(){
NettyMessage hearBeatMsg = new NettyMessage();
hearBeatMsg.setHeader(new NettyMessageHeader());
hearBeatMsg.getHeader().setType(MessageType.HEARTBEAT_REQ);
return hearBeatMsg;
}
}
}
服务端
package com.viagra.chapter12.protocol;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
* @Auther: viagra
* @Date: 2019/8/3 11:02
* @Description:
*/
public class HeartBeatRespHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
NettyMessage nettyMsg = (NettyMessage) msg;
if(nettyMsg!=null&&nettyMsg.getHeader()!=null){
if(nettyMsg.getHeader().getType()==MessageType.HEARTBEAT_REQ){
System.out.println("server recevied client heart beat msg :"+nettyMsg);
NettyMessage respMsg = buildHeartBeatRespMsg();
ctx.writeAndFlush(respMsg);
}else{
ctx.fireChannelRead(msg);
}
}
}
private NettyMessage buildHeartBeatRespMsg(){
NettyMessage msg = new NettyMessage();
msg.setHeader(new NettyMessageHeader());
msg.getHeader().setType(MessageType.HEARBEAR_RESP);
return msg;
}
}
4.断连重试
当客户端感知断连事件之后,释放资源,重新发起连接,具体代码实现
# NettyClient.java
finally{
executor.execute(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
try {
connect(NettyConfig.HOST, NettyConfig.PORT);
} catch (Exception e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果:
服务端:
server recevied login auth msg from client :NettyMessage [header=Header [crcCode=-1410399999, length=46, sessionID=0, type=1, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]
客户端:
server recevied login auth msg from client :NettyMessage [header=Header [crcCode=-1410399999, length=46, sessionID=0, type=1, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]
server recevied client heart beat msg :NettyMessage [header=Header [crcCode=-1410399999, length=22, sessionID=0, type=4, priority=0, attachment={}]]