初識Netty六(自定義協議(私有協議)開發)

實現功能

  1. 基於Netty的NIO通信框架,提供高性能的異步通信能力;
  2. 提供消息的編碼解碼框架,可以實現POJO的序列化和反序列化;
  3. .提供基於IP地址的白名單接入認證機制;
  4. 鏈路的有效性校驗機制;
  5. 鏈路的斷連重連機制;

通信模型

Netty協議通信雙方鏈路建立成功之後,雙方可以進行全雙工通信,無論客戶端還是服務端,都可以主動發送請求消息給對方,通信方式可以是TWO WAY或者ONE WAY。雙方之間都心跳採用Ping-Pong機制,當鏈路處於空閒狀態時,客戶端主動發送Ping消息給服務端,服務端接收到Ping消息後發送應答消息Pong給客戶端,如果客戶端連續發送N條Ping消息都沒有接收到服務端端Ping消息,說明鏈路已經掛死或者對方處於異常狀態,客戶端主動關閉連接,間隔週期T後發起重連操作,知道重連成功

在這裏插入圖片描述
具體步驟:

  1. Netty協議棧客戶端發送握手請求消息,攜帶節點ID等有效身份認證信息;
  2. Netty協議棧服務端對握手請求消息進行合法性校驗,包括節點ID有效性校驗、節點重複登錄校驗和IP地址合法性校驗,校驗通過之後,返回登錄成功的握手應答消息;
  3. 鏈路建立成功之後,客戶端發送業務消息;
  4. 鏈路建立成功之後,服務端發送心跳消息;
  5. 鏈路建立成功之後,客戶端發送心跳消息;
  6. 鏈路建立成功之後,服務端發送業務消息
  7. 服務端推出時,服務端關閉連接,客戶端感知對方關閉連接後,被動關閉客戶端連接。

Netty協議通信雙方練了路建立成功後,雙方可以進行全雙工通信,無論客戶端還是服務端,都可以主動發送請求消息給對方,通信方式可以是TWO WAY或者ONE WAY。雙方之前的心跳採用Ping-Pong機制,當鏈路處理控線狀態時,客戶端主動發送Ping消息給服務端,服務端接收到Ping消息後發送應答消息Pong給客戶端,如果客戶端連續發送N條Ping消息都沒有接收到服務端返回的Pong消息,說明鏈路已經掛死或者對方處理異常狀態,客戶端主動關閉連接,間隔週期T後發起重連操作,知道重連成功

Netty協議的編解碼規範

###Netty協議編碼
Netty協議NettyMessage的編碼規範如下:

  1. rcCode:java.nio.ByteBuffer.putInt(int value),如果採用其他緩衝區實現,必須與其等價;
    2.length:java.nio.ByteBuffer.putInt(int value),如果採用其他緩衝區實現,必須與其等價;
  2. sessionID:java.nio.ByteBuffer.putLong(long value),如果採用其他緩衝區實現,必須與其等價;
  3. type: java.nio.ByteBuffer.put(byte b),如果採用其他緩衝區實現,必須與其等價;
  4. priority:java.nio.ByteBuffer.put(byte b),如果採用其他緩衝區實現,必須與其等價;
  5. attachment:它的編碼規則爲——如果attachment長度爲0,表示沒有可選附件,則將長度編碼設爲0,java.nio.ByteBuffer.putInt(0);如果大於0,說明有附件需要編碼,具體的編碼規則如下:首先對附件的個數進行編碼,java.nio.ByteBuffer.putInt(attachment.size());然後對Key進行編碼,再將它轉換成byte數組之後編碼內容.
  6. body的編碼:通過JBoss Marshalling將其序列化爲byte數組,然後調用java.nio.ByteBuffer.put(byte [] src)將其寫入ByteBuffer緩衝區中。
    由於整個消息的長度必須等全部字段都編碼完成之後才能確認,所以最後需要更新消息頭中的length字段,將其重新寫入ByteBuffer中。

Netty協議解碼

相對於NettyMessage的編碼,仍舊以java.nio.ByteBuffer爲例,給出Netty協議的解碼規範:

  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對其進行解碼。

代碼實現

依賴

<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權威指南
博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章