通信协议从广义上来区分,可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,因因如此,升级起来会非常方便,灵活性较好。本博客基于《Netty 权威指南》,设计并实现私有协议。
私有协议本质上是厂商内部发展和采用的标准,除非授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己制订,协议实现细节不愿公开,只在企业自己生产的设备之间使用的协议。私有协议具有封闭性、垄断性、排他性等特点。如果网上大量存在私有(非标准)协议,现行网络或用户一旦使用了它,后进入的厂家设备就必须跟着使用这种非标准协议,才能够互连互通,否则根本不可能进入现行网络。这样,使用非标准协议的厂家就实现了垄断市场的愿望。
在传统的Java应用中,通常使用以下4种方式进行跨节点通信。
跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应消息进行编解码。在请求和应答消息本身以外,也需要携带一些其他控制和管理类指令,例如链路建立的握手请求和响应消息、链路检测的心跳消息等。当这些功能组合到一起之后,就会形成私有协议。
本博客中介绍的基于Netty的私有协议主要有以下5个功能:
本文设计的私有协议通信过程如下:
本文的消息定义与博客中第二个案例定义相同,在这里就不再介绍。
考虑到安全,链路建立需要通过基于IP地址或者号段的黑白名单安全认证机制,作为阳历,本协议使用IP地址的安全认证机制,如果有多个IP,通过逗号进行分割。
客户端与服务器链路建立成功之后,由客户端发送握手请求消息,握手请求消息的定义如下:
服务端接受客户端的握手请求消息之后,如果IP校验中国,返回握手成功的应答消息给客户端,应用层链路建立成功之后,握手应答消息定义如下:
链路建立连接之后,客户端和服务端就可以互相发送消息。下面将分客户端和服务端分别介绍实现代码。
package netty.protocol.client;
import io.netty.channel.*;
import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;
/**
* created by LMR on 2020/5/23
*/
public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
//客户端激活时就向服务端发送连接请求
ctx.writeAndFlush(buildLoginReq());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
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 {
System.out.println("Login is ok : " + message);
//传递给下一个handler
ctx.fireChannelRead(msg);
}
} else
//传递给下一个handler
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;
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.fireExceptionCaught(cause);
}
}
package netty.protocol.server;
import io.netty.channel.*;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;
/**
1. created by LMR on 2020/5/23
*/
public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter {
private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
private String[] whitekList = {"127.0.0.1", "192.168.100.155", "171.128.110.115"};
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
NettyMessage message = (NettyMessage) msg;
// 如果是握手请求消息,处理,其它消息透传
if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_REQ.value()) {
//获取请求的ip地址
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;
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
nodeCheck.remove(ctx.channel().remoteAddress().toString());// 删除缓存
ctx.close();
ctx.fireExceptionCaught(cause);
}
}
相较于客户端的diamagnetic,服务端现得复杂得多,这是由于我们需要在服务端进行安全认证。与上面设计得一样,我们设置了重复登陆检查和白名单检查。成功连接标识为0,失败则标识为-1。
心跳检测是为了防止网络状况波动,网络通信失败,影响正常得业务。具体的设计思路如下:
package netty.protocol.client;
import io.netty.channel.ChannelHandlerContext;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* created by LMR on 2020/5/23
*/
public class HeartBeatReqHandler extends ChannelInboundHandlerAdapter {
private volatile ScheduledFuture<?> heartBeat;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
NettyMessage message = (NettyMessage) msg;
// 握手成功,主动发送心跳消息
if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_RESP.value()) {
heartBeat = ctx.executor().scheduleAtFixedRate(new HeartBeatTask(ctx), 0, 5000, TimeUnit.MILLISECONDS);
} else if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_RESP.value()) {
System.out.println("Client receive server heart beat message : ---> " + message);
} else
ctx.fireChannelRead(msg);
}
private class HeartBeatTask implements Runnable {
private final ChannelHandlerContext ctx;
public HeartBeatTask(final ChannelHandlerContext ctx) {
this.ctx = ctx;
}
@Override
public void run() {
NettyMessage heatBeat = buildHeatBeat();
System.out.println("Client send heart beat messsage to server : ---> " + heatBeat);
ctx.writeAndFlush(heatBeat);
}
private NettyMessage buildHeatBeat() {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(MessageType.HEARTBEAT_REQ.value());
message.setHeader(header);
return message;
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
if (heartBeat != null) {
heartBeat.cancel(true);
heartBeat = null;
}
ctx.fireExceptionCaught(cause);
}
}
在客户端代码中如果登陆验证成功,会创建一个线程来定时发送心跳消息。在HeartBeatTask 得run方法中,会创建一个心跳NettyMessage消息,用于心跳验证,然后调用传入得ChannelHandlerContext 对象将消息传递给服务端。
package netty.protocol.server;
import io.netty.channel.ChannelHandlerContext;
import netty.protocol.MessageType;
import netty.protocol.struct.Header;
import netty.protocol.struct.NettyMessage;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* created by LMR on 2020/5/23
*/
public class HeartBeatRespHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
NettyMessage message = (NettyMessage) msg;
// 返回心跳应答消息
if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_REQ.value()) {
System.out.println("Receive client heart beat message : ---> " + message);
NettyMessage heartBeat = buildHeatBeat();
System.out.println("Send heart beat response message to client : ---> " + heartBeat);
ctx.writeAndFlush(heartBeat);
} else
ctx.fireChannelRead(msg);
}
private NettyMessage buildHeatBeat() {
NettyMessage message = new NettyMessage();
Header header = new Header();
header.setType(MessageType.HEARTBEAT_RESP.value());
message.setHeader(header);
return message;
}
}
服务端得新体哦啊检测十分简单,接收到心跳消息之后,构造心跳应答消息返回,并打印接受和发送得心跳消息。
心跳超时得实现,我们直接利用Netty得ReadTimeouthandler机制来实现,当一定周期内(默认50s)没有读取到对方得任何消息时,需要主动关闭链路,如果是客户端需要自己主动重连。如果是服务端则释放资源等待客户端重连。
重连机制主要是在客户端实现,当发现连接断开是就需要进行重连,在这里我们是客户端启动类使用一个线程池来重新连接
executor.execute(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
try {
connect(NettyConstant.PORT, NettyConstant.REMOTEIP);// 发起重连操作
} catch (Exception e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
package netty.protocol.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.io.IOException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import netty.protocol.codec.NettyMessageDecoder;
import netty.protocol.codec.NettyMessageEncoder;
/**
* created by LMR on 2020/5/23
*/
public class NettyServer {
public static void main(String[] args) throws Exception {
new NettyServer().bind(8080);
}
public void bind(int port) throws Exception {
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws IOException {
ch.pipeline().addLast(
//自定义消息解码器
new NettyMessageDecoder(1024 * 1024, 4, 4));
//自定义消息编码器
ch.pipeline().addLast(new NettyMessageEncoder());
//处理超时
ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
//用户验证
ch.pipeline().addLast(new LoginAuthRespHandler());
ch.pipeline().addLast("ServerHandler", new ServerHandler());
//心跳检测
ch.pipeline().addLast("HeartBeatHandler", new HeartBeatRespHandler());
}
});
// 绑定端口,同步等待成功
b.bind(port).sync();
System.out.println("Netty server start ok : " + port);
}
}
package netty.protocol.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import netty.protocol.NettyConstant;
import netty.protocol.codec.NettyMessageDecoder;
import netty.protocol.codec.NettyMessageEncoder;
/**
* created by LMR on 2020/5/23
*/
public class NettyClient {
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
EventLoopGroup group = new NioEventLoopGroup();
public void connect(int port, String host) throws Exception {
// 配置客户端NIO线程组
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
ch.pipeline().addLast("MessageEncoder", new NettyMessageEncoder());
ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));
ch.pipeline().addLast("LoginAuthHandler", new LoginAuthReqHandler());
ch.pipeline().addLast("ClientHandler", new ClientHandler());
ch.pipeline().addLast("HeartBeatHandler", new HeartBeatReqHandler());
}
});
// 发起异步连接操作
ChannelFuture future = b.connect(new InetSocketAddress(host, port), new InetSocketAddress(NettyConstant.LOCALIP, NettyConstant.LOCAL_PORT)).sync();
// 当对应的channel关闭的时候,就会返回对应的channel。
future.channel().closeFuture().sync();
} finally {
// 所有资源释放完成之后,清空资源,再次发起重连操作
executor.execute(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
try {
connect(NettyConstant.PORT, NettyConstant.REMOTEIP);// 发起重连操作
} catch (Exception e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
public static void main(String[] args) throws Exception {
new NettyClient().connect(NettyConstant.PORT, NettyConstant.REMOTEIP);
}
}
在客户端与服务端启动类中的消息编解码器以及客户端和服务端的处理类均在博客中有讲解,在这里我们就不再进行重复介绍。
服务端截图:
客户端截图:
由于截图时间不一致,所以心跳消息不一致。
参考书籍和博客:
《Netty权威指南》
如果喜欢的话希望点赞收藏,关注我,将不间断更新博客。
希望热爱技术的小伙伴私聊,一起学习进步
来自于热爱编程的小白