Netty学习——私有协议栈的实现

                        私有协议栈的实现

协议栈

       为什么要说是协议栈呢?因为一个网络通信解决方案并不是一个协议就可以完成的,需要多种协议协同使用,才能完成网络通信的功能。比如说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秒打印一次心跳检测日志。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值