1、异步通信,基于Netty的NIO
2、提供消息的编解码
3、提供基于IP地址的白名单接入认证机制
4、链路的有效性校验机制
5、链路的断连重连机制
1、客户端发送握手请求消息,携带节点ID等有效身份认证信息
2、服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和IP合法性校验,校验通过后,返回登录成功的握手应答消息
3、链路建立成功后,客户端发送业务消息
4、服务端发送心跳消息
5、客户端发送心跳消息
6、服务端发送业务消息
7、服务端退出后,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接
客户端服务端建立通信链路后,心跳采用Ping-Pong机制,当链路空闲时,客户端主动发送Ping消息给服务端,服务端在接收到Ping消息后,发送Pong消息应发给客户端,如果客户端连续发送N条Ping消息都没有接收到服务端返回的Pong消息,说明链路已经挂死或者对方处于异常状态,客户端主动关闭连接,间隔周期T后发起重连操作,知道重连成功。
NettyMessage.java
Header.java
MessageType.java
public enum MessageType {
SERVICE_REQ((byte) 0), SERVICE_RESP((byte) 1), ONE_WAY((byte) 2),
LOGIN_REQ((byte) 3), LOGIN_RESP((byte) 4), HEARTBEAT_REQ((byte) 5), HEARTBEAT_RESP((byte) 6);
private byte value;
private MessageType(byte value) {
this.value = value;
}
public byte value() {
return this.value;
}
}
编码
NettyMessageEncoder.java
public final class NettyMessageEncoder extends MessageToByteEncoder {
MarshallingEncoder marshallingEncoder;
public NettyMessageEncoder() throws IOException {
this.marshallingEncoder = new MarshallingEncoder();
}
@Override
protected void encode(ChannelHandlerContext ctx, NettyMessage msg, ByteBuf sendBuf) throws Exception {
if (msg == null || msg.getHeader() == null)
throw new Exception("The encode message is null");
sendBuf.writeInt((msg.getHeader().getCrcCode()));
sendBuf.writeInt((msg.getHeader().getLength()));
sendBuf.writeLong((msg.getHeader().getSessionID()));
sendBuf.writeByte((msg.getHeader().getType()));
sendBuf.writeByte((msg.getHeader().getPriority()));
sendBuf.writeInt((msg.getHeader().getAttachment().size()));
String key = null;
byte[] keyArray = null;
Object value = null;
for (Map.Entry param : msg.getHeader().getAttachment()
.entrySet()) {
key = param.getKey();
keyArray = key.getBytes("UTF-8");
sendBuf.writeInt(keyArray.length);
sendBuf.writeBytes(keyArray);
value = param.getValue();
marshallingEncoder.encode(value, sendBuf);
}
key = null;
keyArray = null;
value = null;
if (msg.getBody() != null) {
marshallingEncoder.encode(msg.getBody(), sendBuf);
} else
sendBuf.writeInt(0);
sendBuf.setInt(4, sendBuf.readableBytes() - 8);
}
}
解码
NettyMessageDecoder.java
/**
* 继承自LengthFieldBasedFrameDecoder,支持自动的TCP粘包半包处理
*/
public class NettyMessageDecoder extends LengthFieldBasedFrameDecoder {
MarshallingDecoder marshallingDecoder;
public NettyMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) throws IOException {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength);
marshallingDecoder = new MarshallingDecoder();
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) {
return null;
}
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setCrcCode(frame.readInt());
header.setLength(frame.readInt());
header.setSessionID(frame.readLong());
header.setType(frame.readByte());
header.setPriority(frame.readByte());
int size = frame.readInt();
if (size > 0) {
Map attch = new HashMap(size);
int keySize = 0;
byte[] keyArray = null;
String key = null;
for (int i = 0; i < size; i++) {
keySize = frame.readInt();
keyArray = new byte[keySize];
frame.readBytes(keyArray);
key = new String(keyArray, "UTF-8");
attch.put(key, marshallingDecoder.decode(frame));
}
keyArray = null;
key = null;
header.setAttachment(attch);
}
if (frame.readableBytes() > 4) {
message.setBody(marshallingDecoder.decode(frame));
}
message.setHeader(header);
return message;
}
}
服务调用方为客户端,服务被调用方为服务端
LoginAuthReqHandler.java
/**
* 基于IP白名单的登录认证请求处理和服务端应答处理
*/
public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buildLoginReq());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage) msg;
// 如果是握手应答消息,需要判断是否认证成功
if (message.getHeader() != null
&& message.getHeader().getType() == MessageType.LOGIN_RESP.value()) {
byte loginResult = (byte) message.getBody();
//握手应答消息没有消息体
if (loginResult != (byte) 0) {
// 握手失败,关闭连接
ctx.close();
} else {
//握手成功,将消息传给后面的ChannelHandler处理
System.out.println("Login is ok : " + message);
ctx.fireChannelRead(msg);
}
} else
//如果不是握手请求,将消息传给后面的ChannelHandler处理
ctx.fireChannelRead(msg);
}
private NettyMessage buildLoginReq() {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(MessageType.LOGIN_REQ.value());
message.setHeader(header);
return message;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
}
LoginAuthRespHandler.java
/**
* 认证应答和客户端请求认证处理
*/
public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {
/**
* 重复登录保护
*/
private Map nodeCheck = new ConcurrentHashMap();
/**
* 白名单
*/
private String[] whitekList = { "127.0.0.1", "192.168.1.104" };
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyMessage message = (NettyMessage) msg;
// 接入认证逻辑。如果是握手请求消息,处理,其它消息透传
if (message.getHeader() != null
&& message.getHeader().getType() == MessageType.LOGIN_REQ.value()) {
String nodeIndex = ctx.channel().remoteAddress().toString();
NettyMessage loginResp = null;
// 重复登陆,拒绝,防止客户端由于重复登录导致的句柄泄露
if (nodeCheck.containsKey(nodeIndex)) {
loginResp = buildResponse((byte) -1);
} else {
//获取发送方的原地址
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
String ip = address.getAddress().getHostAddress();
boolean isOK = false;
for (String WIP : whitekList) {
if (WIP.equals(ip)) {
isOK = true;
break;
}
}
loginResp = isOK ? buildResponse((byte) 0)
: buildResponse((byte) -1);
if (isOK)
nodeCheck.put(nodeIndex, true);
}
System.out.println("The login response is : " + loginResp
+ " body [" + loginResp.getBody() + "]");
ctx.writeAndFlush(loginResp);
} else {
ctx.fireChannelRead(msg);
}
}
private NettyMessage buildResponse(byte result) {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(MessageType.LOGIN_RESP.value());
message.setHeader(header);
message.setBody(result);
return message;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
//异常关闭链路,需要移除发送方的地址信息,保证后续客户端可以重连
nodeCheck.remove(ctx.channel().remoteAddress().toString());
ctx.close();
ctx.fireExceptionCaught(cause);
}
}
哪些场景需要关闭链路
1、当对方宕机或者重启,会主动关闭链路,另一方读取到操作系统的通知信号,需要关闭资源,释放自身的句柄资源,由于采用TCP全双工通信,通信双方都需要关闭连接,释放资源。
2、消息读写过程中,发生IO异常,需要主动关闭链路
3、心跳消息读写过程中发生IO异常,需要主动关闭链路
4、心跳超时,需要主动关闭连接
5、发生编码异常等不可恢复错误时,需要主动关闭连接
为了保证能够在极端环境下协议栈依然可以正常工作或者自动恢复,需要对它的可靠性进行统一的规划和设计
1、心跳机制
在网络空闲时采用心跳机制来检测链路的互通性,一旦发生网络故障立即关闭链路。
2、重连机制
3、重复登录保护
4、消息缓存重发
如果暴露在公网中,需要更严格的安全认证机制,例如基于密钥和AES加密的用户名+密码认证机制,也可以采用SSL/TSL安全传输。
预留的attachment字段,可选
消息编解码
握手和安全认证
心跳检测机制
断连重连
客户端
服务端
这篇文章是这个系列的第六篇笔记。由于是自学netty,没有在工作中实践netty,所以代码需要自己敲、运行、调试,概念性的东西也需要记笔记,反复读和思考。目的是让以后写网络编程使用netty的时候,能够花尽可能少的时间从头学基础,只需要回过头来复习一下这些文章就可以直接根据项目需要编码。