私有协议栈的实现
协议栈
为什么要说是协议栈呢?因为一个网络通信解决方案并不是一个协议就可以完成的,需要多种协议协同使用,才能完成网络通信的功能。比如说HTTP协议栈,就需要TCP/IP协议做支持,再向下还需要一些数据链路层物理层的相关协议。由于这些协议是分层的,下层协议为上层协议提供服务,这就形成了一个栈式结构。
私有协议栈
HTTP是一个非常好的公有协议栈,现在这么流行,并且把Web在世界各地开枝散叶就足以说明其优越性。但HTTP协议因为是公有的,因此只要把网络接口暴露在公网环境中,任何人(通过了身份验证)都可以访问到接口,有时候并不能满足一些业务场景。比如说有时候在一个公司内部进行网络通信时,并不想让公司外部人员访问,并且由于公司有一些物理地址位置相距较远的分公司,这些分公司也要可以与总部可以方便通信,那么建立局域网的建设代价将是巨大的,充分利用已有的公用网络设施和低层协议可以尽可能减少成本,因而私有协议栈的解决方案就非常有必要性了。通过私有协议栈,可以充分保证内部网络通信的安全性和方便性,而且由于是在已有公网的基础上搭建起来的,成本已非常低,还具有垄断性,因为私有协议是自己开发的,其他人不能够非法使用。
私有协议栈的功能设计
作为网络通信,就必须保证通信的安全性、稳定性、故障恢复能力以及方便性。所以私有协议的设计就必有考虑到这些点,可以参考HTTP协议的实现。其中安全性可能通过IP地址白名单、身份验证和数据加密等方式加以保证,稳定性可以使用TCP/IP这种可靠的网络协议,因为TCP/IP协议是面向连接的,消息发送出去可以保证接收方能够收到,并且还能保证消息的有序性,还可以通过自己实现心跳检测,每间隔一段时间就发送一次心跳消息,等待对方回复,如果一个心跳周期对方还没有回复,就可以判定故对方已经断连,就可以启动重连策略了。故障恢复能力也是非常重要的一个性能指标,网络环境非常复杂,有时候网络连接可以会无意间断开,这时候需要故障恢复能力。在故障恢复的时候,需要及时释放相应的资源,比如说连接资源和一些业务资源,如果没有及时释放故障相关的资源,这些资源就会越各越多,系统能够利用的资源就会越来越少,系统负荷越大越大,系统的性能就会急剧下降,轻则系统死机,重则硬件损坏,业务崩坏,造成不可挽回的经济损失。方便性就是协议能够支持大部分的编程特性,比如说可以支持序列化和基于字符串的数据传输,并且对于扩展友好,方便进行再次开发,扩展业务的时候不需要关心底层实现,就如HTTP,我们可以在HTTP协议的基础上再次开发,设计我们自己的通信协议,可以在请求头中添加自定义请求头来实现自己的业务逻辑(WebSocket的HTTP连接请求就是一个非常好的例子)
详细设计
参考HTTP协议的实现,HTTP协议的请求分为请求头和请求体,请求头中包含了请求方法、请求地址、协议版本、请求媒体类型,接受媒体类型,语言等非常多的信息,而且可以自己添加额外的信息来进行相应扩展。因此我们的私有协议也可以参考HTTP协议设计消息头和消息体两部分,并且由于考虑后到期的扩展能力,应该把消息头和消息体都设置成可变长的。
消息头设计如下几个字段及相关功能
creCode-->32位整数类型包括两部分,前16位是固定魔数ABCD,表示这个消息是我们设计的私有协议消息,后16位表示消息的版本
length-->32位整型,包括消息头和消息体的消息的总长度
sessionID-->64位长整型,由于私有协议栈可以支持会话功能,因此用sessionID来唯一标识一个会话
type-->8位byte型,消息类型
priority-->8位byte型,表示消息的优先级
attachment-->Map<String,Object>类型,扩展字段,方便扩展
链路建立时,为了保证链路建立的成功性,设计客户端先向服务器端发送一个连接握手消息,服务器端进行相关验证后返回一个握手应答消息给客户端,如果客户端也成功接收到了握手返回消息并验证成功,就表示握手成功。握手请求消息头的type设置成0,attachment没有附件,消息体为空,而握手回复消息头的type设置成1,如果验证成功,则消息体为一个为1的byte数据,如果失败就为一个为0的byte数据。在链路关闭时,需要释放相应的系统资源,当消息读写过程发生异步,心跳读写发生异步,心跳没有及时回复等一些系统错误的时候就可以执行链路关闭操作。为了保证系统的安全,设计心跳机制,当网络处于空闲状态达到T时间后客户端向服务器端主动发送一个Ping心跳消息,如果在下一个周期T时间内没有收到服务器的Pong回复消息,就让心跳失败计数器加1,当心跳计数器达到N时就表示失去了连接,就可以启动重连操作了。如果接收到了Pong回复消息,就将心跳失败计算器清零。而在服务器这一边,每T时间内如果没有收到客户端的任何消息(业务消息或心跳消息)就让心跳失败计数器加1,当心跳计数器达到N时就判定与客户端失去了连接,也进行相应的释放资源操作。当失去连接并释放相应的资源后就可以进行重连了,重连是为了保证系统的稳定性,就算系统因为特殊网络异常短时间断开了连接,也可以通过重连机制重新获取网络连接来继续网络通信,不会导致业务系统的崩溃。断连后并不能马上开启重连,因为低层的网络恢复也需要一定的时间,因为每间隔一个固定的时间INTERVAL进行一次重连操作。为了进一步保护系统资源,要设计重复登录保护机制,当客户端连接认证成功后需要保存一份留底,表示此客户端已经连接上了,而断连时就删除些留底。这样可以防止客户端反复连接导致的系统资源消耗。为了系统的安全稳定,要设计消息缓存机制,如果链路突然中断,不应该抛弃掉要发的消息,而是缓存在消息队列中,等待重连成功后再发送,这样来保证业务的完整性。
细节实现
消息的定义
package study.netty.protocol;
import java.util.HashMap;
import java.util.Map;
public class Header {
private int creCode;
private int length;
private long sessionID;
private byte type;
private byte priority;
private Map<String, Object> attachment = new HashMap();
public int getCreCode() {
return creCode;
}
public void setCreCode(int creCode) {
this.creCode = creCode;
}
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;
}
}
package study.netty.protocol;
public class Message {
private Header header;
private Object body;
public Header getHeader() {
return header;
}
public void setHeader(Header header) {
this.header = header;
}
public Object getBody() {
return body;
}
public void setBody(Object body) {
this.body = body;
}
}
消息编码解码器,这里使用了JDK自带的序列化
package study.netty.protocol;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
/**
* @author Benson
* @date 2018年1月28日 下午1:20:37
* @emial 144813736@qq.com
* @description 消息解码器
*/
public class MessageDecoder extends LengthFieldBasedFrameDecoder {
public MessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment,
int initialBytesToStrip) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf buffer = (ByteBuf) super.decode(ctx, in);
if (buffer == null) {
return null;
}
Message message = new Message();
Header header = new Header();
header.setCreCode(buffer.readInt());
header.setLength(buffer.readInt());
header.setSessionID(buffer.readLong());
header.setType(buffer.readByte());
header.setPriority(buffer.readByte());
int size = buffer.readInt();
byte[] valueBytes;
if (size > 0) {
Map<String, Object> attachment = header.getAttachment();
int keySize;
byte[] keyBytes;
for (int i = 0; i < size; i++) {
keySize = buffer.readInt();
keyBytes = new byte[keySize];
buffer.readBytes(keyBytes);
valueBytes = new byte[buffer.readInt()];
attachment.put(new String(keyBytes, "UTF-8"), SerializableUtil.decode(valueBytes));
}
}
message.setHeader(header);
if (buffer.readableBytes() > 0) {
byte[] bodyBytes = new byte[buffer.readableBytes()];
buffer.readBytes(bodyBytes);
// message.setBody(SerializableUtil.decode(bodyBytes));
}
return message;
}
}
package study.netty.protocol;
import java.util.List;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
/**
* @author Benson
* @date 2018年1月28日 下午1:20:56
* @emial 144813736@qq.com
* @description 消息编码器
*/
public class MessageEncoder extends MessageToMessageEncoder<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception {
if (msg == null || msg.getHeader() == null) {// 消息为空或者消息格式错误
throw new Exception("the message is error");
}
ByteBuf buffer = Unpooled.buffer();
buffer.writeInt(msg.getHeader().getCreCode());
buffer.writeInt(msg.getHeader().getLength());
buffer.writeLong(msg.getHeader().getSessionID());
buffer.writeByte(msg.getHeader().getType());
buffer.writeByte(msg.getHeader().getPriority());
buffer.writeInt(msg.getHeader().getAttachment().size());
String key;
byte[] keyBytes;
byte[] valueBytes = null;
for (Map.Entry<String, Object> param : msg.getHeader().getAttachment().entrySet()) {
key = param.getKey();
keyBytes = key.getBytes("UTF-8");
buffer.writeInt(keyBytes.length);
buffer.writeBytes(keyBytes);
valueBytes = SerializableUtil.encode(param.getValue());
buffer.writeInt(valueBytes.length);
buffer.writeBytes(valueBytes);
}
if (msg.getBody() != null) {
buffer.writeBytes(SerializableUtil.encode(msg.getBody()));
}
buffer.setInt(4, buffer.readableBytes());// 最后才设置总长度
out.add(buffer);
}
}
package study.netty.protocol;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializableUtil {
public static byte[] encode(Object obj) throws IOException {
if (!(obj instanceof Serializable)) {
throw new IOException("the obj not implements Serializable");
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
return bos.toByteArray();
}
public static Object decode(byte[] bts) throws IOException, ClassNotFoundException {
ByteArrayInputStream bis = new ByteArrayInputStream(bts);
ObjectInputStream ois = new ObjectInputStream(bis);
ois.close();
return ois.readObject();
}
}
定义消息类型
package study.netty.protocol;
/**
*
* @author Benson
* @date 2018年1月28日 下午1:27:59
* @emial 144813736@qq.com
* @description 消息类型
*/
public class MessageType {
public static final byte LOGIN_REQ = 0;
public static final byte LOGIN_RESP = 1;
public static final byte HEARTBEAT_REQ = 2;
public static final byte HEARTBEAT_RESP = 3;
}
客户端的登录请求处理器
package study.netty.protocol;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
*
* @author Benson
* @date 2018年1月28日 下午1:25:02
* @emial 144813736@qq.com
* @description 客户端握手处理器
*/
public class LoginAuthReqHandler extends ChannelHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 三次握手连接上后即开始进行请求服务器连接
Message message = new Message();
Header header = new Header();
header.setType(MessageType.LOGIN_REQ);
message.setHeader(header);
ctx.writeAndFlush(message);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message message = (Message) msg;
if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_RESP) {
byte loginResult = 1;// (byte) message.getBody();
if (loginResult == (byte) 0) {// 握手失败,关闭连接
ctx.close();
} else {
System.out.println("login successful");
ctx.fireChannelRead(msg);
}
} else {
ctx.fireChannelRead(msg);
}
}
}
服务器端的登录响应处理器
package study.netty.protocol;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
*
* @author Benson
* @date 2018年1月28日 下午1:37:15
* @emial 144813736@qq.com
* @description 服务器端握手处理器
*/
public class LoginAuthRespHandler extends ChannelHandlerAdapter {
private Map<String, Boolean> loginCheck = new ConcurrentHashMap();
private String[] whiteList = { "127.0.0.1" };
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message message = (Message) msg;
if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_REQ) {
String address = ctx.channel().remoteAddress().toString();
Message resultMsg = new Message();
Header header = new Header();
header.setType(MessageType.LOGIN_RESP);
if (loginCheck.containsKey(address)) {// 重复登录
resultMsg.setBody((byte) 0);
} else {
InetSocketAddress addr = (InetSocketAddress) ctx.channel().remoteAddress();
String ip = addr.getAddress().getHostAddress();
boolean isOk = false;
for (String wIp : whiteList) {
if (wIp.equals(ip)) {
isOk = true;
}
}
if (isOk) {
resultMsg.setBody((byte) 1);
} else {
resultMsg.setBody((byte) 0);
}
}
resultMsg.setHeader(header);
System.out.println("send:" + resultMsg);
ctx.writeAndFlush(resultMsg);
} else {
ctx.fireChannelRead(msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}
客户端的心跳请求处理器
package study.netty.protocol;
import java.util.concurrent.TimeUnit;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.concurrent.ScheduledFuture;
/**
*
* @author Benson
* @date 2018年1月28日 下午1:50:59
* @emial 144813736@qq.com
* @description 心跳检测客户端请求处理器
*/
public class HeartBeatReqHandler extends ChannelHandlerAdapter {
private volatile ScheduledFuture<?> heartBeat;
private class HeartBeatTask implements Runnable {
private ChannelHandlerContext ctx;
public HeartBeatTask(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
@Override
public void run() {
Message pingMsg = new Message();
Header header = new Header();
header.setType(MessageType.HEARTBEAT_REQ);
pingMsg.setHeader(header);
ctx.writeAndFlush(pingMsg);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message message = (Message) msg;
if (message.getHeader() != null && message.getHeader().getType() == MessageType.LOGIN_RESP) {// 是服务器返回的登录返回消息
heartBeat = ctx.executor().scheduleAtFixedRate(new HeartBeatTask(ctx), 0, 5000, TimeUnit.MILLISECONDS);// 开启一个定时任务,每5秒执行一次心跳
} else if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_RESP) {// 心跳响应消息Pong
System.out.println("clientagereceive heart beat response message " + message);
} else {
ctx.fireChannelRead(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (heartBeat != null) {
heartBeat.cancel(true);
}
ctx.fireExceptionCaught(cause);
}
}
服务器端的心跳响应处理器
package study.netty.protocol;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
*
* @author Benson
* @date 2018年1月28日 下午2:03:20
* @emial 144813736@qq.com
* @description 心跳检测服务器端响应处理器
*/
public class HeartBeatRespHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message message = (Message) msg;
if (message.getHeader() != null && message.getHeader().getType() == MessageType.HEARTBEAT_REQ) {
System.out.println("receive client heart beat message " + message);
Message pongMsg = new Message();
Header header = new Header();
header.setType(MessageType.HEARTBEAT_RESP);
pongMsg.setHeader(header);
ctx.writeAndFlush(pongMsg);
} else {
ctx.fireChannelRead(msg);
}
}
}
最后是服务器端代码
package study.netty.protocol;
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 io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
*
* @author Benson
* @date 2018年1月28日 下午2:18:47
* @emial 144813736@qq.com
* @description 服务器端
*/
public class Server {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).option(ChannelOption.SO_BACKLOG, 100)
.channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new MessageEncoder())
.addLast(new MessageDecoder(1024 * 1024, 4, 4, -8, 0))
.addLast(new LoginAuthRespHandler()).addLast(new HeartBeatRespHandler());
}
});
try {
bootstrap.bind(8888).sync().channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端代码
package study.netty.protocol;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import io.netty.bootstrap.Bootstrap;
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;
/**
*
* @author Benson
* @date 2018年1月28日 下午2:18:28
* @emial 144813736@qq.com
* @description 客户端
*/
public class Client {
public static void connect() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).option(ChannelOption.TCP_NODELAY, true).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new MessageDecoder(1024 * 1024, 4, 4, -8, 0))
.addLast(new MessageEncoder()).addLast(new LoginAuthReqHandler())
.addLast(new HeartBeatReqHandler());
}
});
try {
bootstrap.connect("localhost", 8888).sync().channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
executor.execute(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);// 5秒后重连
connect();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
}
public static void main(String[] args) {
connect();
}
}
如此就完成了自定义的私有协议栈,如果我们想在此基础上再添加自己的一些业务逻辑来处理消息的话,直接继承ChannelHandlerAdapter类,然后在SocketChannel的初始化器中进行注册,添加到pipeline中就可以了,netty底层会用责任链模式对消息进行相应过滤,最后到达自定义的消息处理器的消息就是业务逻辑消息了。通过在attachment字段中添加自定义扩展字段,能够非常方便地进行业务逻辑扩展。也可以在编码器和解码器中添加加密算法,以此来更加保证网络通信数据的安全性。
上面的代码,执行后会在客户端和服务器端的控制吧每5秒打印一次心跳检测日志。