文章目錄
實現功能
- 基於Netty的NIO通信框架,提供高性能的異步通信能力;
- 提供消息的編碼解碼框架,可以實現POJO的序列化和反序列化;
- .提供基於IP地址的白名單接入認證機制;
- 鏈路的有效性校驗機制;
- 鏈路的斷連重連機制;
通信模型
Netty協議通信雙方鏈路建立成功之後,雙方可以進行全雙工通信,無論客戶端還是服務端,都可以主動發送請求消息給對方,通信方式可以是TWO WAY或者ONE WAY。雙方之間都心跳採用Ping-Pong機制,當鏈路處於空閒狀態時,客戶端主動發送Ping消息給服務端,服務端接收到Ping消息後發送應答消息Pong給客戶端,如果客戶端連續發送N條Ping消息都沒有接收到服務端端Ping消息,說明鏈路已經掛死或者對方處於異常狀態,客戶端主動關閉連接,間隔週期T後發起重連操作,知道重連成功
具體步驟:
- Netty協議棧客戶端發送握手請求消息,攜帶節點ID等有效身份認證信息;
- Netty協議棧服務端對握手請求消息進行合法性校驗,包括節點ID有效性校驗、節點重複登錄校驗和IP地址合法性校驗,校驗通過之後,返回登錄成功的握手應答消息;
- 鏈路建立成功之後,客戶端發送業務消息;
- 鏈路建立成功之後,服務端發送心跳消息;
- 鏈路建立成功之後,客戶端發送心跳消息;
- 鏈路建立成功之後,服務端發送業務消息
- 服務端推出時,服務端關閉連接,客戶端感知對方關閉連接後,被動關閉客戶端連接。
Netty協議通信雙方練了路建立成功後,雙方可以進行全雙工通信,無論客戶端還是服務端,都可以主動發送請求消息給對方,通信方式可以是TWO WAY或者ONE WAY。雙方之前的心跳採用Ping-Pong機制,當鏈路處理控線狀態時,客戶端主動發送Ping消息給服務端,服務端接收到Ping消息後發送應答消息Pong給客戶端,如果客戶端連續發送N條Ping消息都沒有接收到服務端返回的Pong消息,說明鏈路已經掛死或者對方處理異常狀態,客戶端主動關閉連接,間隔週期T後發起重連操作,知道重連成功
Netty協議的編解碼規範
###Netty協議編碼
Netty協議NettyMessage的編碼規範如下:
- rcCode:java.nio.ByteBuffer.putInt(int value),如果採用其他緩衝區實現,必須與其等價;
2.length:java.nio.ByteBuffer.putInt(int value),如果採用其他緩衝區實現,必須與其等價; - sessionID:java.nio.ByteBuffer.putLong(long value),如果採用其他緩衝區實現,必須與其等價;
- type: java.nio.ByteBuffer.put(byte b),如果採用其他緩衝區實現,必須與其等價;
- priority:java.nio.ByteBuffer.put(byte b),如果採用其他緩衝區實現,必須與其等價;
- attachment:它的編碼規則爲——如果attachment長度爲0,表示沒有可選附件,則將長度編碼設爲0,java.nio.ByteBuffer.putInt(0);如果大於0,說明有附件需要編碼,具體的編碼規則如下:首先對附件的個數進行編碼,java.nio.ByteBuffer.putInt(attachment.size());然後對Key進行編碼,再將它轉換成byte數組之後編碼內容.
- body的編碼:通過JBoss Marshalling將其序列化爲byte數組,然後調用java.nio.ByteBuffer.put(byte [] src)將其寫入ByteBuffer緩衝區中。
由於整個消息的長度必須等全部字段都編碼完成之後才能確認,所以最後需要更新消息頭中的length字段,將其重新寫入ByteBuffer中。
Netty協議解碼
相對於NettyMessage的編碼,仍舊以java.nio.ByteBuffer爲例,給出Netty協議的解碼規範:
- crcCode:通過java.nio.ByteBuffer.getInt()獲取校驗碼字段,其他緩衝區需要與其等價;
- length:通過java.nio.ByteBuffer.getInt()獲取Netty消息的長度,其他緩衝區需要與其等價;
- sessionID:通過java.nio.ByteBuffer.getLong()獲取會話ID,其他緩衝區需要與其等價;
- type:通過java.nio.ByteBuffer.get()獲取消息類型,其他緩衝區需要與其等價;
- priority:通過java.nio.ByteBuffer.get()獲取消息優先級,其他緩衝區需要與其等價;
- attachment:它的解碼規則爲——首先創建一個新的attachment對象,調用java.nio.ByteBuffer.getInt()獲取附件的長度,如果爲0,說明附件爲空,解碼結束,繼續解消息體;如果非空,則根據長度通過for循環進行解碼;
- body:通過JBoss的marshaller對其進行解碼。
代碼實現
依賴
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha1</version>
</dependency>
<!--其他可能用到的依賴-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling-serial</artifactId>
<version>2.0.2.Final</version>
</dependency>
</dependencies>
消息結構定義
消息頭定義 Header
@Data
public final class Header {
private int crcCode = 0xadaf0105;
/**
* 消息長度
*/
private int length;
/**
* 會話ID
*/
private long sessionId;
/**
* 消息類型
*/
private byte type;
/**
* 消息優先級
*/
private byte priority;
/**
* 附件
*/
private Map<String, Object> attachment = new HashMap<>();
}
消息定義 NettyMessage
@Data
public final class NettyMessage {
/**
* 消息頭
*/
private Header header;
/**
* 消息體
*/
private Object body;
}
消息類型定義 MessageType
public enum MessageType {
/**
* 業務請求消息
*/
SERVICE_REQ((byte) 0),
/**
* 業務響應(應答)消息
*/
SERVICE_RESP((byte) 1),
/**
* 業務ONE WAY消息(既是請求消息又是響應消息)
*/
ONE_WAY((byte) 2),
/**
* 握手請求消息
*/
LOGIN_REQ((byte) 3),
/**
* 握手響應(應答)消息
*/
LOGIN_RESP((byte) 4),
/**
* 心跳請求消息
*/
HEARTBEAT_REQ((byte) 5),
/**
* 心跳響應(應答)消息
*/
HEARTBEAT_RESP((byte) 6);
private byte value;
MessageType(byte value) {
this.value = value;
}
public byte value() {
return value;
}
}
返回結果定義
public enum ResultType {
/**
* 認證成功
*/
SUCCESS((byte) 0),
/**
* 認證失敗
*/
FAIL((byte) -1),
;
private byte value;
private ResultType(byte value) {
this.value = value;
}
public byte value() {
return this.value;
}
}
端口常量定義 NettyConstant
public interface 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";
}
消息編解碼
ChannelBufferByteInput
/**
* @author WH
* @version 1.0
* @date 2020/5/31 13:58
* @Description 消息編碼器
*/
public 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;
}
}
ChannelBufferByteOutput
package codec;
import io.netty.buffer.ByteBuf;
import org.jboss.marshalling.ByteOutput;
import java.io.IOException;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 14:10
* @Description 消息解碼器
*/
public 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;
}
}
MarshallingCodecFactory
package codec;
import org.jboss.marshalling.*;
import java.io.IOException;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 14:12
* @Description 編碼器
*/
public 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;
}
}
MarshallingDecoder
package codec;
import io.netty.buffer.ByteBuf;
import org.jboss.marshalling.Unmarshaller;
import java.io.IOException;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 14:26
* @Description TODO
*/
public class MarshallingDecoder {
private final Unmarshaller unmarshaller;
public MarshallingDecoder() throws IOException {
unmarshaller = MarshallingCodecFactory.buildUnMarshalling();
}
protected Object decode(ByteBuf in) throws Exception {
//1 首先讀取4個長度(實際body內容長度)
int objectSize = in.readInt();
//2 獲取實際body的緩衝內容
int readIndex = in.readerIndex();
ByteBuf buf = in.slice(readIndex, objectSize);
//3 轉換
ChannelBufferByteInput input = new ChannelBufferByteInput(buf);
try {
//4 讀取操作:
unmarshaller.start(input);
Object obj = unmarshaller.readObject();
unmarshaller.finish();
//5 讀取完畢以後, 更新當前讀取起始位置:
//因爲使用slice方法,原buf的位置還在readIndex上,故需要將位置重新設置一下
in.readerIndex(in.readerIndex() + objectSize);
return obj;
} finally {
unmarshaller.close();
}
}
}
MarshallingEncoder
package codec;
import io.netty.buffer.ByteBuf;
import org.jboss.marshalling.Marshaller;
import java.io.IOException;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 14:21
* @Description TODO
*/
public class MarshallingEncoder {
//空白佔位: 用於預留設置 body的數據包長度
private static final byte[] LENGTH_PLACEHOLDER = new byte[4];
Marshaller marshaller;
public MarshallingEncoder() throws IOException {
marshaller = MarshallingCodecFactory.buildMarshalling();
}
protected void encode(Object msg, ByteBuf out) throws Exception {
try {
//必須要知道當前的數據位置是哪: 起始數據位置
//長度屬性的位置索引
int lengthPos = out.writerIndex();
//佔位寫操作:先寫一個4個字節的空的內容,記錄在起始數據位置,用於設置內容長度
out.writeBytes(LENGTH_PLACEHOLDER);
ChannelBufferByteOutput output = new ChannelBufferByteOutput(out);
marshaller.start(output);
marshaller.writeObject(msg);
marshaller.finish();
//總長度(結束位置) - 初始化長度(起始位置) - 預留的長度 = body數據長度
int endPos = out.writerIndex();
out.setInt(lengthPos, endPos - lengthPos - 4);
} finally {
marshaller.close();
}
}
}
NettyMessageDecoder
package codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import message.Header;
import message.NettyMessage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 13:48
* @Description TODO
*/
public final class NettyMessageDecoder extends LengthFieldBasedFrameDecoder {
MarshallingDecoder marshallingDecoder;
/**
*
* @param maxFrameLength 第一個參數代表最大的序列化長度
* @param lengthFieldOffset 代表長度屬性的偏移量 簡單來說就是message中 總長度的起始位置(Header中的length屬性的起始位置) 本例中爲4
* @param lengthFieldLength 代表長度屬性的長度 整個屬性佔多長(length屬性爲int,佔4個字節) 4
* @throws IOException
*/
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();
//附件個數大於0,則需要解碼操作
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;
//解碼完成放入attachment
header.setAttachment(attch);
}
message.setHeader(header);
//對於ByteBuf來說,讀一個數據,就會少一個數據,所以讀完header,剩下的應該就是body了
if (frame.readableBytes() > 4) {
message.setBody(marshallingDecoder.decode(frame));
}
return message;
}
}
NettyMessageEncoder
package codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.util.CharsetUtil;
import message.Header;
import message.NettyMessage;
import java.io.IOException;
import java.util.Map;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 14:15
* @Description TODO
*/
public class NettyMessageEncoder extends MessageToByteEncoder<NettyMessage> {
private 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("編碼失敗,沒有數據信息!");
}
//Head:
Header header = msg.getHeader();
sendBuf.writeInt(header.getCrcCode());//校驗碼
sendBuf.writeInt(header.getLength());//總長度
sendBuf.writeLong(header.getSessionId());//會話id
sendBuf.writeByte(header.getType());//消息類型
sendBuf.writeByte(header.getPriority());//優先級
//對附件信息進行編碼
//編碼規則爲:如果attachment的長度爲0,表示沒有可選附件,則將長度 編碼設置爲0
//如果attachment長度大於0,則需要編碼,規則:
//首先對附件的個數進行編碼
sendBuf.writeInt((header.getAttachment().size())); //附件大小
String key = null;
byte[] keyArray = null;
Object value = null;
//然後對key進行編碼,先編碼長度,然後再將它轉化爲byte數組之後編碼內容
for (Map.Entry<String, Object> param : header.getAttachment()
.entrySet()) {
key = param.getKey();
keyArray = key.getBytes(CharsetUtil.UTF_8);
sendBuf.writeInt(keyArray.length);//key的字符編碼長度
sendBuf.writeBytes(keyArray);
value = param.getValue();
marshallingEncoder.encode(value, sendBuf);
}
key = null;
keyArray = null;
value = null;
//Body:
Object body = msg.getBody();
//如果不爲空 說明: 有數據
if(body != null){
//使用MarshallingEncoder
this.marshallingEncoder.encode(body, sendBuf);
} else {
//如果沒有數據 則進行補位 爲了方便後續的 decoder操作
sendBuf.writeInt(0);
}
//最後我們要獲取整個數據包的總長度 也就是 header + body 進行對 header length的設置
// TODO: 解釋: 在這裏必須要-8個字節 ,是因爲要把CRC和長度本身佔的減掉了
//(官方中給出的是:LengthFieldBasedFrameDecoder中的lengthFieldOffset+lengthFieldLength)
//總長度是在header協議的第二個標記字段中
//第一個參數是長度屬性的索引位置
sendBuf.setInt(4, sendBuf.readableBytes() - 8);
}
}
客戶端實現
ClientHandler
package client;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;
import message.NettyMessage;
/**
* @author WH
* @version 1.0
* @date 2020/6/1 21:30
* @Description TODO
*/
@Slf4j
public class ClientHandler extends ChannelHandlerAdapter {
// 連接成功監聽
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
NettyMessage message = (NettyMessage)msg;
System.err.println("Client receive message from server: " + message.getBody());
} finally {
ReferenceCountUtil.release(msg);
}
}
// 客戶端斷開連接監聽
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
log.info("----------客戶端斷開連接-----------");
ctx.close();
}
}
客戶端心跳檢測 HeartBeatReqHandler
package client;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.concurrent.ScheduledFuture;
import lombok.extern.slf4j.Slf4j;
import message.Header;
import message.MessageType;
import message.NettyMessage;
import java.util.concurrent.TimeUnit;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 15:29
* @Description 客戶端心跳檢測
*/
@Slf4j
public class HeartBeatReqHandler extends ChannelHandlerAdapter {
private volatile ScheduledFuture<?> heartBeat;
@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()) {
heartBeat = ctx.executor().scheduleAtFixedRate(
new HeartBeatReqHandler.HeartBeatTask(ctx), 0, 5000,
TimeUnit.MILLISECONDS);
log.info("客戶端發送心跳包");
} 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);
}
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);
}
}
客戶端握手認證 LoginAuthReqHandler
package client;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import message.Header;
import message.MessageType;
import message.NettyMessage;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 15:23
* @Description 客戶端握手認證
*/
@Slf4j
public class LoginAuthReqHandler extends ChannelHandlerAdapter {
@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 {
log.info("Login is ok : {}", message);
ctx.fireChannelRead(msg);
}
} else
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);
}
}
客戶端 NettyClient
package client;
import codec.NettyMessageDecoder;
import codec.NettyMessageEncoder;
import constant.NettyConstant;
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;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 15:33
* @Description 客戶端
*/
public class NettyClient {
public static void main(String[] args) throws Exception {
new NettyClient().connect(NettyConstant.PORT, NettyConstant.REMOTEIP);
}
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(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();
//手動發測試數據,驗證是否會產生TCP粘包/拆包情況
// Channel c = future.channel();
//
// for (int i = 0; i < 500; i++) {
// NettyMessage message = new NettyMessage();
// Header header = new Header();
// header.setSessionID(1001L);
// header.setPriority((byte) 1);
// header.setType((byte) 0);
// message.setHeader(header);
// message.setBody("我是請求數據" + i);
// c.writeAndFlush(message);
// }
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();
}
}
});
}
}
}
服務端實現
服務端心跳檢測 HeartBeatRespHandler
package server;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import message.Header;
import message.MessageType;
import message.NettyMessage;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 15:31
* @Description TODO
*/
@Slf4j
public class HeartBeatRespHandler extends ChannelHandlerAdapter {
@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;
}
}
服務端握手認證 LoginAuthRespHandler
package server;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import message.Header;
import message.MessageType;
import message.NettyMessage;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 15:26
* @Description 服務端握手和安全認證
*/
@Slf4j
public class LoginAuthRespHandler extends ChannelHandlerAdapter {
private Map<String, Boolean> nodeCheck = new ConcurrentHashMap<String, Boolean>();
private String[] whitekList = { "127.0.0.1", "192.168.56.1" };
@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 : {} body [ {} ]", loginResp, 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);
}
}
ServerHandler
package server;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import message.Header;
import message.NettyMessage;
/**
* @author WH
* @version 1.0
* @date 2020/6/1 21:31
* @Description TODO
*/
@Slf4j
public class ServerHandler extends ChannelHandlerAdapter {
/**
* 當我們通道進行激活的時候 觸發的監聽方法
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("通道激活");
}
/**
* 當我們的通道里有數據進行讀取的時候 觸發的監聽方法
*/
@Override
public void channelRead(ChannelHandlerContext ctx /*NETTY服務上下文*/, Object msg /*實際的傳輸數據*/) throws Exception {
NettyMessage requestMessage = (NettyMessage) msg;
System.err.println("Server receive message from Client: " + requestMessage.getBody());
NettyMessage responseMessage = new NettyMessage();
Header header = new Header();
header.setSessionId(2002L);
header.setPriority((byte) 2);
header.setType((byte) 1);
responseMessage.setHeader(header);
responseMessage.setBody("我是響應數據: " + requestMessage.getBody());
ctx.writeAndFlush(responseMessage);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
log.info("--------數據讀取完畢----------");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
log.info("--------服務器數據讀異常----------:");
cause.printStackTrace();
ctx.close();
}
}
NettyServer
package server;
import codec.NettyMessageDecoder;
import codec.NettyMessageEncoder;
import constant.NettyConstant;
import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* @author WH
* @version 1.0
* @date 2020/5/31 15:35
* @Description TODO
*/
@Slf4j
public class NettyServer {
public static void main(String[] args) throws Exception {
new NettyServer().bind();
}
public void bind() throws Exception {
//1 用於接受客戶端連接的線程工作組
EventLoopGroup bossGroup = new NioEventLoopGroup();
//2 用於對接受客戶端連接讀寫操作的線程工作組
EventLoopGroup workerGroup = new NioEventLoopGroup();
//3 輔助類。用於幫助我們創建NETTY服務
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)//綁定兩個工作線程組
.channel(NioServerSocketChannel.class) //設置NIO的模式
.option(ChannelOption.SO_BACKLOG, 1024) //設置TCP緩衝區
.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());
ch.pipeline().addLast(new ServerHandler());
}
});
// 綁定端口,同步等待成功
ChannelFuture cf = b.bind(NettyConstant.REMOTEIP, NettyConstant.PORT).sync();
log.info("Netty server start ok : {} : {}",NettyConstant.REMOTEIP, NettyConstant.PORT);
//釋放連接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
測試
-
服務端
-
客戶端
-
其他測試:服務端宕機重連,客戶端宕機重連
源碼下載
參考
Netty權威指南
博客