来源:《Netty权威指南》  作者:李林峰

一、私有协议介绍

由于现代软件的复杂性,一个大型软件系统往往会被人为地拆分称为多个模块,另外随着移动互联网的兴起,网站的规模越来越大,业务功能越来越多,往往需要集群和分布式部署。模块之间的通信就需要进行跨节点通信。
传统的Java应用中节点通信的常用方式:

  • rmi远程服务调用
  • Java Socket + Java序列化
  • RPC框架 Thrift、Apache的Avro等
  • 利用标准的公有协议进行跨节点调用,例如HTTP+XML,Restful+JSON或WebService

下面使用Netty设计私有协议

除了链路层的物理连接外,还需要对请求和响应消息进行编解码。 在请求和应答之外,还需要控制和管理类指令,例如链路建立的握手信息,链路检测的心跳信息。这些功能组合到一起后,就会形成私有协议。

  • 每个Netty节点(Netty进程)之间建立长连接,使用Netty协议进行通信。
  • Netty节点没有客户端和服务端的区别,谁首先发起连接,谁就是客户端。

1. 网络拓扑图:

2. 协议栈功能描述:

  1. 基于Netty的NIO通信框架,提供高性能的异步通信能力;
  2. 提供消息的编解码框架,实现POJO的序列化和反序列化
  3. 提供基于IP地址的白名单接入认证机制;
  4. 链路的有效性校验机制;
  5. 链路的断线重连机制;

3. 通信模型:

具体步骤:

  1. Netty协议栈客户端发送握手请求信息,携带节点ID等有效身份认证信息;
  2. Netty协议服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和IP地址合法性校验,校验通过后,返回登录成功的握手应答消息;
  3. 链路建立成功之后,客户端发送业务消息;
  4. 链路成功之后,服务端发送心跳消息;
  5. 链路建立成功之后,客户端发送心跳消息;
  6. 链路建立成功之后,服务端发送业务消息;
  7. 服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。

4. 消息定义

类似于http协议,消息分为消息头消息体。其中消息体是一个Object类型,消息头则如下所示:

名称 类型 长度 描述
length 整型 int 32 消息长度,整个消息,包括消息头和消息体
sessionId 长整型long 64 集群节点内全局唯一,由会话ID生成器生成
type Byte 8

0: 表示请求消息

1: 业务响应消息

2: 业务ONE WAY消息(即是请求又是响应消息)

3: 握手请求消息

4: 握手应答消息

5: 心跳请求消息

6: 心跳应答消息

priority Byte 8 消息优先级: 0-255
attachment Map<String,Object> 变长 可选字段,用于扩展消息头

5. 支持的字段类型:

6. Netty协议的编解码规范

编码规范:

(1) crcCode: java.nio.ByteBuffer.putInt(int value),如果采用其它缓存区实现,必须与其等价

(2) length: java.nio.ByteBuffer.putInt(int value),如果采用其它缓冲区实现,必须与其等价

(3) sessionID: java.nio.ByteBuffer.putLong(long value),如果采用其它缓冲区实现,必须与其等价

(4) type: java.nio.ByteBuffer.put(byte b),如果采用其它缓冲区实现,必须与其等价

(5) priority: java.nio.ByteBuffer.put(byte b),如果采用其它缓冲区实现,必须与其等价

(6) attachment: 如果长度为0,表示没有可选附件,则将长度编码为0,即java.nio.ByteBuffer.putInt(0),如果大于0,表示有附件需要编码,具体规则如下:

首先对附件的个数进行编码,java.nio.ByteBuffer.putInt(attachment.size());

然后对Key进行编码,先编码长度,然后再将它转换成byte数组之后编码内容,具体代码如下:

String key = null;
byte[] value = null;
for (Map.Entry<String, Object> param: attachment:entrySet()) {
key = param.getKey();
buffer.writeString(key);
value = marshaller.writeObject(param.getValue());
buffer.writeBinary(value);
}
key = null;
value = null;

(7) body的编码: 通过JBoss Marshalling将其序列化为byte数组,然后调用java.nio.ByteBuffer.put(byte[] src);将其写入ByteBuffer缓冲区中。

在所有的内容都编码完成之后更新消息头的length字段。

解码规范:

(1) crcCode: java.nio.ByteBuffer.getInt()获取校验码字段,如果采用其它缓存区实现,必须与其等价

(2) length: java.nio.ByteBuffer.getInt()获取Netty消息的长度,如果采用其它缓冲区实现,必须与其等价

(3) sessionID: java.nio.ByteBuffer.getLong()获取会话ID,如果采用其它缓冲区实现,必须与其等价

(4) type: java.nio.ByteBuffer.get()获取消息类型,如果采用其它缓冲区实现,必须与其等价

(5) priority: java.nio.ByteBuffer.get()获取消息优先级,如果采用其它缓冲区实现,必须与其等价

(6) attachment: 它的解码规则为-首先创建一个新的attachment对象,调用java.nio.ByteBuffer.getInt()获取附件的长度,如果为0,说明附件为空,解码结束,解析解消息体,否则,根据长度通过for循环进行解码。

(7) body: 使用JBoss marshaller对其进行解码

7. 链路的建立

不区分客户端和服务端:如果A节点需要B节点的服务,但是A和B之间还没有建立物理链路,则由调用方主动发起连接,此时调用方为客户端,被调用方为服务端。

使用简单的黑白名单进行认证,实际环境中,应该使用密钥,用户名密码等方式。

客户端发送请求消息:

  • 消息头的type字段为3;
  • 可选附件个数为0;
  • 消息体为空;
  • 握手消息的长度为22个字节;

服务端接收到握手请求消息,如果IP校验通过,返回握手成功应答给客户端,应用层链路建立成功。握手应答消息:

  • 消息头type为4
  • 可选附件个数为0
  • 消息体为byte类型的结果,"0"表示认证成功,"-1"表示认证失败。

链路成功建立后,客户端和服务端就可以相互发送业务消息了。

8. 链路的关闭

由于采用长连接通信,正常的业务运行期间,双方通过心跳和业务消息维持链路,任何一方不需要主动关闭连接。

但是,在以下情况下,客户端和服务端需要关闭连接。

(1) 当对方宕机或者重启时,会主动释放链路,另一方读取到操作系统的通知信号,得到对方REST链路,需要关闭连接,释放自身的句柄等资源。由于采用TCP全双工通信,通信双方都需要关闭连接,释放资源;

(2) 消息在读写过程中,发生了I/O异常,需要主动关闭连接;

(3) 心跳消息读写过程中发生了I/O异常,需要主动关闭连接;

(4) 心跳超时,需要主动关闭连接;

(5) 发生编码异常等不可恢复的错误时,需要主动关闭连接;

9. 可靠性设计

网络环境是恶劣的。意外无法避免,需要在出现意外的时候正常工作或者说是恢复,需要可靠性设计的保证。

(1) 心跳机制

在凌晨等业务低谷期,如果发生网络闪断、连接被Hang住等网络问题,由于没有业务消息,应用进程很难发现。到了白天业务高峰期,会发生大量的网络通信失败,严重的会导致一段时间进程内无法处理业务消息。

为了解决这个问题,在网络空闲的时候采用心跳机制来检测链路的互通性,一旦发现了网络故障,立即关闭链路,主动重连。

设计思路:

  • 当网络处于空闲时间达到了T(连续周期T没有读写消息)时,客户端主动发送Ping心跳消息给服务端;
  • 如果在下一个周期T到来时客户端没有收到对方发送的Pong心跳应答消息或者读取到服务端发送的其他业务消息,则心跳失败计数器+1
  • 每当客户端接收到服务的业务消息或者Pong应答消息时,将心跳失败计数器清0;连续N次没有接收到服务端的Pong消息或者业务消息,则关闭链路间隔INTERVAL时间后发起重连操作;
  • 服务端网络空闲状态持续时间达到T后,服务器端将心跳失败计数器+1;只要接收到客户端发送的Ping消息或者其他业务消息,计数器清0
  • 服务器端连续N次没有接收到客户端的Ping消息或者其他业务消息,则关闭链路,释放资源,等待客户端重连。
(2) 重连机制

如果链路中断,等待INTERVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL之后再继续重连。

无论什么场景下的重连失败,客户端必须保证自身资源被成功及时释放

重连失败,需要记录异常堆栈信息,方便问题定位。

(3) 重复登录保护

客户端握手成功之后,链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽

server在接收到握手消息后,首先进行ip合法性校验,如果成功,则在缓存的地址表中查看客户端是否已经登录,如果已经登录,则拒绝重复登录,返回错误码-1,同时关闭链路,并且在服务端日志中打印错误信息。

为了防止由服务端和客户端对链路状态理解不一致的问题,当服务端连续N次心跳超时之后需要主动关闭链路,同时清空该客户端的缓存信息,保证后续的客户端可以重连。

(5) 消息缓存重发

无论是客户端还是服务端,在发生链路中断之后,恢复链路之前,缓存在消息队列的待发送的消息不能丢失。同时考虑到内存溢出风险,应该在消息缓存队列中设置上限。

10  可扩展性设计

Netty协议栈需要具备一定的扩展能力,例如统一的消息拦截、接口日志、安全、加密解密等可以被方便地添加和删除,推荐使用Servelt的FilterChain机制,考虑到性能因素,不推荐AOP。

二、Netty协议栈开发

2.1 数据结构定义

不管心跳消息、握手请求和握手应答消息都可以用NettyMessage来定义,只是type不同而已。

消息头:
import java.util.HashMap;
import java.util.Map; /**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月14日
*/
public final class Header { private int crcCode = 0xabef0101; private int length;// 消息长度 private long sessionID;// 会话ID private byte type;// 消息类型 private byte priority;// 消息优先级 private Map<String, Object> attachment = new HashMap<String, Object>(); // 附件 /**
* @return the crcCode
*/
public final int getCrcCode() {
return crcCode;
} /**
* @param crcCode the crcCode to set
*/
public final void setCrcCode(int crcCode) {
this.crcCode = crcCode;
} /**
* @return the length
*/
public final int getLength() {
return length;
} /**
* @param length the length to set
*/
public final void setLength(int length) {
this.length = length;
} /**
* @return the sessionID
*/
public final long getSessionID() {
return sessionID;
} /**
* @param sessionID the sessionID to set
*/
public final void setSessionID(long sessionID) {
this.sessionID = sessionID;
} /**
* @return the type
*/
public final byte getType() {
return type;
} /**
* @param type the type to set
*/
public final void setType(byte type) {
this.type = type;
} /**
* @return the priority
*/
public final byte getPriority() {
return priority;
} /**
* @param priority the priority to set
*/
public final void setPriority(byte priority) {
this.priority = priority;
} /**
* @return the attachment
*/
public final Map<String, Object> getAttachment() {
return attachment;
} /**
* @param attachment the attachment to set
*/
public final void setAttachment(Map<String, Object> attachment) {
this.attachment = attachment;
} /*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Header [crcCode=" + crcCode + ", length=" + length
+ ", sessionID=" + sessionID + ", type=" + type + ", priority="
+ priority + ", attachment=" + attachment + "]";
} }
消息:
/**
* @author lilinfeng
* @version 1.0
* @date 2014年3月14日
*/
public final class NettyMessage { private Header header; private Object body; /**
* @return the header
*/
public final Header getHeader() {
return header;
} /**
* @param header the header to set
*/
public final void setHeader(Header header) {
this.header = header;
} /**
* @return the body
*/
public final Object getBody() {
return body;
} /**
* @param body the body to set
*/
public final void setBody(Object body) {
this.body = body;
} /*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "NettyMessage [header=" + header + "]";
}
}

2.2 消息编解码

由于依赖于JBoss Marshalling...,添加maven依赖

        <dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling</artifactId>
<version>1.4.10.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling-serial</artifactId>
<version>1.4.10.Final</version>
</dependency>

JBossMarshallingFactory:

import org.jboss.marshalling.*;

import java.io.IOException;

/**
* @author Administrator
* @version 1.0
* @date 2014年3月15日
*/
public final class MarshallingCodecFactory { /**
* 创建Jboss Marshaller
*
* @return
* @throws IOException
*/
protected static Marshaller buildMarshalling() throws IOException {
final MarshallerFactory marshallerFactory = Marshalling
.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
Marshaller marshaller = marshallerFactory
.createMarshaller(configuration);
return marshaller;
} /**
* 创建Jboss Unmarshaller
*
* @return
* @throws IOException
*/
protected static Unmarshaller buildUnMarshalling() throws IOException {
final MarshallerFactory marshallerFactory = Marshalling
.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
final Unmarshaller unmarshaller = marshallerFactory
.createUnmarshaller(configuration);
return unmarshaller;
}
}

增加JBossMarshalling序列化对象->ByteBuf工具

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import org.jboss.marshalling.Marshaller; import java.io.IOException; /**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月14日
*/
@Sharable
public class MarshallingEncoder { private static final byte[] LENGTH_PLACEHOLDER = new byte[4];
Marshaller marshaller; public MarshallingEncoder() throws IOException {
marshaller = MarshallingCodecFactory.buildMarshalling();
} // 使用marshall对Object进行编码,并且写入bytebuf...
protected void encode(Object msg, ByteBuf out) throws Exception {
try {
//1. 获取写入位置
int lengthPos = out.writerIndex();
//2. 先写入4个bytes,用于记录Object对象编码后长度
out.writeBytes(LENGTH_PLACEHOLDER);
//3. 使用代理对象,防止marshaller写完之后关闭byte buf
ChannelBufferByteOutput output = new ChannelBufferByteOutput(out);
//4. 开始使用marshaller往bytebuf中编码
marshaller.start(output);
marshaller.writeObject(msg);
//5. 结束编码
marshaller.finish();
//6. 设置对象长度
out.setInt(lengthPos, out.writerIndex() - lengthPos - 4);
} finally {
marshaller.close();
}
}
}
import io.netty.buffer.ByteBuf;
import org.jboss.marshalling.ByteOutput; import java.io.IOException; /**
* {@link ByteOutput} implementation which writes the data to a {@link ByteBuf}
*
*
*/
class ChannelBufferByteOutput implements ByteOutput { private final ByteBuf buffer; /**
* Create a new instance which use the given {@link ByteBuf}
*/
public ChannelBufferByteOutput(ByteBuf buffer) {
this.buffer = buffer;
} @Override
public void close() throws IOException {
// Nothing to do
} @Override
public void flush() throws IOException {
// nothing to do
} @Override
public void write(int b) throws IOException {
buffer.writeByte(b);
} @Override
public void write(byte[] bytes) throws IOException {
buffer.writeBytes(bytes);
} @Override
public void write(byte[] bytes, int srcIndex, int length) throws IOException {
buffer.writeBytes(bytes, srcIndex, length);
} /**
* Return the {@link ByteBuf} which contains the written content
*
*/
ByteBuf getBuffer() {
return buffer;
}
}

增加JBossMarshalling反序列化对象<-ByteBuf工具

import io.netty.buffer.ByteBuf;
import org.jboss.marshalling.ByteInput;
import org.jboss.marshalling.Unmarshaller; import java.io.IOException;
import java.io.StreamCorruptedException; /**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月14日
*/
public class MarshallingDecoder { private final Unmarshaller unmarshaller; /**
* Creates a new decoder whose maximum object size is {@code 1048576} bytes.
* If the size of the received object is greater than {@code 1048576} bytes,
* a {@link StreamCorruptedException} will be raised.
*
* @throws IOException
*/
public MarshallingDecoder() throws IOException {
unmarshaller = MarshallingCodecFactory.buildUnMarshalling();
} protected Object decode(ByteBuf in) throws Exception {
//1. 读取第一个4bytes,里面放置的是object对象的byte长度
int objectSize = in.readInt();
ByteBuf buf = in.slice(in.readerIndex(), objectSize);
//2 . 使用bytebuf的代理类
ByteInput input = new ChannelBufferByteInput(buf);
try {
//3. 开始解码
unmarshaller.start(input);
Object obj = unmarshaller.readObject();
unmarshaller.finish();
//4. 读完之后设置读取的位置
in.readerIndex(in.readerIndex() + objectSize);
return obj;
} finally {
unmarshaller.close();
}
}
}
import io.netty.buffer.ByteBuf;
import org.jboss.marshalling.ByteInput; import java.io.IOException; /**
* {@link ByteInput} implementation which reads its data from a {@link ByteBuf}
*/
class ChannelBufferByteInput implements ByteInput { private final ByteBuf buffer; public ChannelBufferByteInput(ByteBuf buffer) {
this.buffer = buffer;
} @Override
public void close() throws IOException {
// nothing to do
} @Override
public int available() throws IOException {
return buffer.readableBytes();
} @Override
public int read() throws IOException {
if (buffer.isReadable()) {
return buffer.readByte() & 0xff;
}
return -1;
} @Override
public int read(byte[] array) throws IOException {
return read(array, 0, array.length);
} @Override
public int read(byte[] dst, int dstIndex, int length) throws IOException {
int available = available();
if (available == 0) {
return -1;
} length = Math.min(available, length);
buffer.readBytes(dst, dstIndex, length);
return length;
} @Override
public long skip(long bytes) throws IOException {
int readable = buffer.readableBytes();
if (readable < bytes) {
bytes = readable;
}
buffer.readerIndex((int) (buffer.readerIndex() + bytes));
return bytes;
} }

下面根据上述所说的进行对消息编解码:

import demo.protocol.netty.struct.NettyMessage;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder; import java.io.IOException;
import java.util.Map; /**
* Created by carl.yu on 2016/12/19.
*/
public class NettyMessageEncoder extends MessageToByteEncoder<NettyMessage> { MarshallingEncoder marshallingEncoder; public NettyMessageEncoder() throws IOException {
this.marshallingEncoder = new MarshallingEncoder();
} @Override
protected void encode(ChannelHandlerContext ctx, NettyMessage msg, ByteBuf sendBuf) throws Exception {
if (null == msg || null == msg.getHeader()) {
throw new Exception("The encode message is null");
}
//---写入crcCode---
sendBuf.writeInt((msg.getHeader().getCrcCode()));
//---写入length---
sendBuf.writeInt((msg.getHeader().getLength()));
//---写入sessionId---
sendBuf.writeLong((msg.getHeader().getSessionID()));
//---写入type---
sendBuf.writeByte((msg.getHeader().getType()));
//---写入priority---
sendBuf.writeByte((msg.getHeader().getPriority()));
//---写入附件大小---
sendBuf.writeInt((msg.getHeader().getAttachment().size())); String key = null;
byte[] keyArray = null;
Object value = null;
for (Map.Entry<String, Object> 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);
}
// for gc
key = null;
keyArray = null;
value = null; if (msg.getBody() != null) {
marshallingEncoder.encode(msg.getBody(), sendBuf);
} else
sendBuf.writeInt(0);
// 之前写了crcCode 4bytes,除去crcCode和length 8bytes即为更新之后的字节
sendBuf.setInt(4, sendBuf.readableBytes() - 8);
}
}
import demo.protocol.netty.struct.Header;
import demo.protocol.netty.struct.NettyMessage;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import java.io.IOException;
import java.util.HashMap;
import java.util.Map; /**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月15日
*/
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<String, Object> attch = new HashMap<String, Object>(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;
}
}

关键在于解码器继承了LengthFieldBasedFrameDecoder,三个参数:

ch.pipeline().addLast(
new NettyMessageDecoder(1024 * 1024, 4, 4));

第一个参数:1024*1024: 最大长度

第二个参数: 从第4个bytes开始表示是长度

第三个参数: 有4个bytes的长度表示是长度

2.3 握手和安全认证

Netty的机制大多是基于Handler链。

client端在通道激活时构建login请求:

/**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月15日
*/
public class LoginAuthRespHandler extends ChannelInboundHandlerAdapter { private final static Log LOG = LogFactory.getLog(LoginAuthRespHandler.class); /**
* 本地缓存
*/
private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
private String[] whitekList = {"127.0.0.1", "192.168.1.104"}; /**
* Calls {@link ChannelHandlerContext#fireChannelRead(Object)} to forward to
* the next {@link ChannelHandler} in the {@link ChannelPipeline}.
* <p>
* Sub-classes may override this method to change behavior.
*/
@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);
}
LOG.info("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)
throws Exception {
cause.printStackTrace();
nodeCheck.remove(ctx.channel().remoteAddress().toString());// 删除缓存
ctx.close();
ctx.fireExceptionCaught(cause);
}
}

server端判断是否是login请求,并对ip进行验证:

/**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月15日
*/
public class LoginAuthReqHandler extends ChannelInboundHandlerAdapter { private static final Log LOG = LogFactory.getLog(LoginAuthReqHandler.class); /**
* Calls {@link ChannelHandlerContext#fireChannelActive()} to forward to the
* next {@link ChannelHandler} in the {@link ChannelPipeline}.
* <p/>
* Sub-classes may override this method to change behavior.
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buildLoginReq());
} /**
* Calls {@link ChannelHandlerContext#fireChannelRead(Object)} to forward to
* the next {@link ChannelHandler} in the {@link ChannelPipeline}.
* <p/>
* Sub-classes may override this method to change behavior.
*/
@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 {
LOG.info("Login is ok : " + message);
ctx.fireChannelRead(msg);
}
} else
//调用下一个channel链..
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)
throws Exception {
ctx.fireExceptionCaught(cause);
}
}

2.4 心跳机制检测

握手成功之后,由客户端主动发送心跳消息,服务端接收到心跳消息之后,返回应答,由于心跳消息的目的是为了检测链路的可用性,因此不需要携带消息体。

/**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月15日
*/
public class HeartBeatReqHandler extends ChannelInboundHandlerAdapter { private static final Log LOG = LogFactory.getLog(HeartBeatReqHandler.class); //使用定时任务发送
private volatile ScheduledFuture<?> heartBeat; @Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
NettyMessage message = (NettyMessage) msg;
// 当握手成功后,Login响应向下透传,主动发送心跳消息
if (message.getHeader() != null
&& message.getHeader().getType() == MessageType.LOGIN_RESP
.value()) {
//NioEventLoop是一个Schedule,因此支持定时器的执行,创建心跳计时器
heartBeat = ctx.executor().scheduleAtFixedRate(
new HeartBeatReqHandler.HeartBeatTask(ctx), 0, 5000,
TimeUnit.MILLISECONDS);
} else if (message.getHeader() != null
&& message.getHeader().getType() == MessageType.HEARTBEAT_RESP
.value()) {
LOG.info("Client receive server heart beat message : ---> "
+ message);
} else
ctx.fireChannelRead(msg);
} //Ping消息任务类
private class HeartBeatTask implements Runnable {
private final ChannelHandlerContext ctx; public HeartBeatTask(final ChannelHandlerContext ctx) {
this.ctx = ctx;
} @Override
public void run() {
NettyMessage heatBeat = buildHeatBeat();
LOG.info("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);
}
}
import demo.protocol.netty.MessageType;
import demo.protocol.netty.struct.Header;
import demo.protocol.netty.struct.NettyMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; /**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月15日
*/
public class HeartBeatRespHandler extends ChannelInboundHandlerAdapter { private static final Log LOG = LogFactory.getLog(HeartBeatRespHandler.class); @Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
NettyMessage message = (NettyMessage) msg;
// 返回心跳应答消息
if (message.getHeader() != null
&& message.getHeader().getType() == MessageType.HEARTBEAT_REQ
.value()) {
LOG.info("Receive client heart beat message : ---> "
+ message);
NettyMessage heartBeat = buildHeatBeat();
LOG.info("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)没有接收到任何对方消息时,需要主动关闭链路。如果是客户端,则重新发起连接,如果是服务端,则释放资源,清除客户端登录缓存信息,等待服务器端重连。

2.5 断线重连机制

在client感知到断连事件之后,释放资源,重新发起连接,具体代码如以下部分

首先监听网络断连事件,如果Channel关闭,则执行后续的重连任务,通过Bootstrap重新发起连接,客户端挂在closeFuture上监听链路关闭信号,一旦关闭,则创建定时器,重连。

服务端在监听到断连事件后,还需要清空缓存中的登录认证注册信息,以保证后续客户端可以正常重连。

2.6 客户端代码

public final class NettyConstant {
public static final String REMOTEIP = "127.0.0.1";
public static final int PORT = 8080;
public static final int LOCAL_PORT = 12088;
public static final String LOCALIP = "127.0.0.1";
}
import demo.protocol.netty.NettyConstant;
import demo.protocol.netty.codec.NettyMessageDecoder;
import demo.protocol.netty.codec.NettyMessageEncoder;
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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; /**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月15日
*/
public class NettyClient { private static final Log LOG = LogFactory.getLog(NettyClient.class); 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("HeartBeatHandler",
new HeartBeatReqHandler());
}
});
// 发起异步连接操作
ChannelFuture future = b.connect(
new InetSocketAddress(host, port),
new InetSocketAddress(NettyConstant.LOCALIP,
NettyConstant.LOCAL_PORT)).sync();
// 当对应的channel关闭的时候,就会返回对应的channel。
// Returns the ChannelFuture which will be notified when this channel is closed. This method always returns the same future instance.
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();
}
}
});
}
} /**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
new NettyClient().connect(NettyConstant.PORT, NettyConstant.REMOTEIP);
} }

2.7 服务端

import demo.protocol.netty.NettyConstant;
import demo.protocol.netty.codec.NettyMessageDecoder;
import demo.protocol.netty.codec.NettyMessageEncoder;
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;
import io.netty.handler.timeout.ReadTimeoutHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import java.io.IOException; /**
* @author Lilinfeng
* @version 1.0
* @date 2014年3月15日
*/
public class NettyServer { private static final Log LOG = LogFactory.getLog(NettyServer.class); public void bind() 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)
.handler(new LoggingHandler(LogLevel.INFO))
.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("HeartBeatHandler",
new HeartBeatRespHandler());
}
}); // 绑定端口,同步等待成功
b.bind(NettyConstant.REMOTEIP, NettyConstant.PORT).sync();
LOG.info("Netty server start ok : "
+ (NettyConstant.REMOTEIP + " : " + NettyConstant.PORT));
} public static void main(String[] args) throws Exception {
new NettyServer().bind();
}
}

三、测试

3.1 正常测试

启动server端,再启动client端

2016-12-19 20:52:23 INFO  HeartBeatRespHandler:44 - Receive client heart beat message : ---> NettyMessage [header=Header [crcCode=-1410399999, length=18, sessionID=0, type=5, priority=0, attachment={}]]
2016-12-19 20:52:23 INFO HeartBeatRespHandler:47 - Send heart beat response message to client : ---> NettyMessage [header=Header [crcCode=-1410399999, length=0, sessionID=0, type=6, priority=0, attachment={}]]
2016-12-19 20:52:28 INFO HeartBeatRespHandler:44 - Receive client heart beat message : ---> NettyMessage [header=Header [crcCode=-1410399999, length=18, sessionID=0, type=5, priority=0, attachment={}]]
2016-12-19 20:52:28 INFO HeartBeatRespHandler:47 - Send heart beat response message to client : ---> NettyMessage [header=Header [crcCode=-1410399999, length=0, sessionID=0, type=6, priority=0, attachment={}]]
2016-12-19 20:52:33 INFO HeartBeatRespHandler:44 - Receive client heart beat message : ---> NettyMessage [header=Header [crcCode=-1410399999, length=18, sessionID=0, type=5, priority=0, attachment={}]]
2016-12-19 20:52:33 INFO HeartBeatRespHandler:47 - Send heart beat response message to client : ---> NettyMessage [header=Header [crcCode=-1410399999, length=0, sessionID=0, type=6, priority=0, attachment={}]]
2016-12-19 20:52:38 INFO HeartBeatRespHandler:44 - Receive client heart beat message : ---> NettyMessage [header=Header [crcCode=-1410399999, length=18, sessionID=0, type=5, priority=0, attachment={}]]
2016-12-19 20:52:38 INFO HeartBeatRespHandler:47 - Send heart beat response message to client : ---> NettyMessage [header=Header [crcCode=-1410399999, length=0, sessionID=0, type=6, priority=0, attachment={}]]
2016-12-19 20:52:43 INFO HeartBeatRespHandler:44 - Receive client heart beat message : ---> NettyMessage [header=Header [crcCode=-1410399999, length=18, sessionID=0, type=5, priority=0, attachment={}]]

3.2 服务端宕机重启

关闭服务端,client由于心跳,一直报错:

io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: /127.0.0.1:8080
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)
at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:347)
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:340)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:627)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:551)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:465)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:437)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:873)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:144)
at java.lang.Thread.run(Thread.java:745)

需要测试信息如下:

(1) 客户端是否能够正常发起重连

(2) 重连之后,不再重连

(3) 断连期间,心跳定时器停止工作,不再发送心跳请求消息

(4) 服务器重启成功后,允许客户端重新登录

(5) 服务器重启成功之,客户端能够重连和握手成功

(6) 重连成功之后,双方的心跳能够正常护法

(7) 性能指标:重连期间,客户端能源得到了正常回收,不会导致句柄等资源泄露

使用vituralvm或者Jconsole工具,监控断连期间,cpu,线程,堆内存等资源占用正常.

重连之后,可以继续通信

3.3 客户端断开重连

也可以重新启动,且清空缓存信息,清空代码在LoginAuthHandler中的异常捕获部分:

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
nodeCheck.remove(ctx.channel().remoteAddress().toString());// 删除缓存
ctx.close();
ctx.fireExceptionCaught(cause);
}

netty(5)高级篇-私有协议栈的更多相关文章

  1. netty(4)高级篇-Websocket协议开发

    一.HTTP协议的弊端 将HTTP协议的主要弊端总结如下: (1) 半双工协议:可以在客户端和服务端2个方向上传输,但是不能同时传输.同一时刻,只能在一个方向上传输. (2) HTTP消息冗长:相比于 ...

  2. 基于Netty的私有协议栈的开发

    基于Netty的私有协议栈的开发 书是人类进步的阶梯,每读一本书都使自己得以提升,以前看书都是看了就看了,当时感觉受益匪浅,时间一长就又还回到书本了!所以说,好记性不如烂笔头,以后每次看完一本书都写一 ...

  3. netty——私有协议栈开发案例

    netty--私有协议栈开发案例 摘要: 在学习李林峰老师的Netty权威指南中,觉得第十二章<私有协议栈开发>中的案例代码比较有代表性,讲的也不错,但是代码中个人认为有些简单的错误,个人 ...

  4. netty 私有协议栈

    通信协议从广义上区分,可以分为公有协议和私有协议.由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好.绝大多数的私有协议传输层都基于TCP/I ...

  5. 【.net深呼吸】动态类型(高级篇)

    前面老周给大家介绍了动态类型使用的娱乐级别用法,其实,在很多情景下,娱乐级别的用法已经满足需求了. 如果,你想自己来控制动态类型的行为和数据的存取,那么,就可以考虑用今天所说的高大上技术了.比如,你希 ...

  6. 简易RPC框架-私有协议栈

    *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...

  7. VFP的数据策略:高级篇

    VFP的数据策略:高级篇 引语 在“VFP中的数据策略:基础篇”一文中,我们研究了VFP应用程序中访问非VFP数据(如SQL Server)的不同机制:远程视图.SQL Passthrough.ADO ...

  8. Devops 开发运维高级篇之Jenkins+Docker+SpringCloud微服务持续集成(上)

    Devops 开发运维高级篇之Jenkins+Docker+SpringCloud微服务持续集成(上) Jenkins+Docker+SpringCloud持续集成流程说明 大致流程说明: 1) 开发 ...

  9. Devops 开发运维高级篇之容器管理

    Devops 开发运维高级篇之容器管理 安装docker Dockerfile镜像脚本入门制作 Harbor镜像仓库安装及使用 不过多解释docker直接秀基操 安装docker:(jenkins服务 ...

随机推荐

  1. MVC框架的插件与拦截器基础

    自制MVC框架的插件与拦截器基础 上篇谈到我自己写的MVC框架,接下来讲讲插件及拦截器! 在处理一些通用的逻辑最好把它封装一个插件或者拦截器,以便日后可以直接拿过来直接使用.在我的框架中可以通过继承以 ...

  2. DefaultModelBinder

    Asp.net MVC的Model Binder工作流程以及扩展方法(3) - DefaultModelBinder Default Binder是MVC中的清道夫,把守着Model Binder中的 ...

  3. Bug Tracker

    Bug Tracker 使用笔记(有图有真相)   目的:管理Bug,完善业务流程. 前提条件:BugTracker是基于IIS和SQL Server和Asp.Net的.相当于一个Web端的管理系统. ...

  4. discuz 取消门户首页url中的portal.php

    这几天准备用discuz搭建一个素食网站,一切就绪之后,访问discuz的门户时总是带着portal.php,可能是职业毛病,在url中总是带着,感觉太碍眼了,并且discuz就是搜索引擎收录一直抵制 ...

  5. Scrum与高效能人士

    Scrum与高效能人士的执行4原则   分享了高效能人士的执行4原则,发现它和Scrum非常相近,可以形成互补. Scrum框架: 高效能人士的执行4原则框架: Scrum与4原则 Sprint Ba ...

  6. Linux内核中链表实现

    关于双链表实现,一般教科书上定义一个双向链表节点的方法如下: struct list_node{ stuct list_node *pre; stuct list_node *next; ElemTy ...

  7. jquery.post用法补充(type设置问题)

    jquery.post用法 http://blog.csdn.net/itmyhome1990/article/details/12578275 当使用ajax获取data数据的时候,直接data.f ...

  8. Java 多线程系列

    要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问. Java中的主要同步机制是关键字synchronized,它提供了一种独 ...

  9. Play 起步

    *****************jdk下载地址: http://download.oracle.com/otn-pub/java/jdk/7u79-b15/jdk-7u79-linux-x64.ta ...

  10. html5 “拖放”

    拖放主要是两个部分组成,drag:拖动,drop:放置!既抓取元素后拖到另一个位置! 要实现拖放首先要把被拖动元素设置为可拖动,既: draggbile="true" 然后要拖动什 ...