netty的學習記錄

最近開始學習netty,自己寫了個服務端的demo,包含從接收到客戶端的數據流到完成業務邏輯並回發數據給客戶端這一整個過程,下面開始正文。


先看一下工程目錄


添加netty包

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.14.Final</version>
            <scope>compile</scope>
</dependency>


netty的一些初始化設置

public class NettyServer {
    private int port;

    public NettyServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();// 用於處理I/O操作的多線程事件循環
        EventLoopGroup workerGroup = new NioEventLoopGroup();// 線程池的數量和線程池都可以自己配置,默認的線程池數量爲CPU數量 *2
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();// netty的一個幫助類,用來設置服務器
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline channelPipeline = socketChannel.pipeline();
                            channelPipeline.addLast("decoder", new PacketFrameDecoder());
                            channelPipeline.addLast("encoder", new PacketFrameEncoder());
                            channelPipeline.addLast(new NettyServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 綁定端口並開啓服務
            ChannelFuture channelFuture = bootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws Exception {
        System.out.println("server executed");

        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = ConfigInfo.SERVER_PORT;
        }
        new NettyServer(port).run();

    }
}

EventLoopGroup可以理解爲線程池,這裏初始化了兩個EventLoopGroup,一個專門用於處理客戶端連入事件,另一個用於處理讀寫事件。


 channelPipeline.addLast("decoder", new PacketFrameDecoder());
 channelPipeline.addLast("encoder", new PacketFrameEncoder());
 channelPipeline.addLast(new NettyServerHandler());

這裏配置了編解碼器和一個處理通道事件的類,因爲netty是基於數據流的形式和客戶端交互的,因此需要對數據進行編解碼,在接收到數據時將其轉換爲string,int等,在回發時要將數據轉換爲byte[]


這三個類的交互順序是PacketFrameDecoder--->NettyServerHandler--->PacketFrameEncoder


public class PacketFrameDecoder extends LengthFieldBasedFrameDecoder {
    private static final int MAX_PACKET_LENGTH = 8192 * 2;
    private static final int LENGTH_FIELD_OFFSET = 0;
    private static final int LENGTH_FIELD_LENGTH = 4;// 前幾位代表報文長度,這裏只能是1,2,3,4,8
    private static final int LENGTH_ADJUSTMENT = -4;
    private static final int INITIAL_BYTES_TO_STRIP = 0;

    private Logger logger = Logger.getLogger(PacketFrameDecoder.class);

    public PacketFrameDecoder() {
        super(MAX_PACKET_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        super.decode(ctx, in);
        in.resetReaderIndex();// 重置索引

        MsgHeader msgHeader = new MsgHeader();
        byte[] bytes = new byte[in.readableBytes()];
        in.readBytes(bytes);// 這裏要用讀的方式獲取數據流,不能直接用getBytes方法,不然會出現一直可讀而導致死循環的狀態
        msgHeader.fromBytes(bytes);

        logger.info(msgHeader.toString());

        return msgHeader;
    }
}

MsgHeader是自定義的一個消息類,包含消息長度,消息類型,消息正文。這裏的消息正文就是我們通常理解的pojo,只不過是以byte[]的形式存在。


public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    private Logger logger = Logger.getLogger(NettyServerHandler.class);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ServiceDispatcher serviceDispatcher = new ServiceDispatcher();
        serviceDispatcher.dispatch(ctx, (MsgHeader) msg);
        ctx.close();
    }

    /**
     * 這個方法是在客戶端接入時會調用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("一個客戶端連入");
        super.channelActive(ctx);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 當引發異常時關閉連接
        cause.printStackTrace();
        ctx.close();
    }

}

這個類主要看channelRead方法,在這裏將之前解析出來的MsgHeader分發給service,這裏要傳入上下文,之後還要通過它來發送消息給客戶端


public class ServiceDispatcher {
    private Logger logger = Logger.getLogger(ServiceDispatcher.class);

    public void dispatch(ChannelHandlerContext ctx, MsgHeader msgHeader) {
        IService service = null;
        switch (msgHeader.getType()) {
            case MsgType.REQ_LOGIN:
                logger.info("接收到了客戶端登錄請求");
                service = new LoginServiceImpl();
                break;
            default:
                break;
        }
        if (service != null) {
            service.handle(ctx, msgHeader);
        }
    }
}

在這裏根據報文類型將數據分發給對應的service

接下來就是我們熟悉的業務邏輯啦,這裏省略數據庫操作

public class LoginServiceImpl implements LoginService {
    private Logger logger = Logger.getLogger(LoginServiceImpl.class);
    private Req_Login req_login;
    private Res_Login res_login;


    public void handle(ChannelHandlerContext ctx, MsgHeader msgHeader) {
        fromBytes(msgHeader.getContent());
        // 在這裏執行數據庫操作,比對賬號密碼
        if (req_login.getPassword().equals("123456") && req_login.getUsername().equals("Mike")) {
            logger.info("登錄成功");
            res_login = new Res_Login(MsgType.SUCCESS);
        } else {
            logger.info("登錄失敗");
            res_login = new Res_Login(MsgType.FAILURE);
        }
        MsgHeader resMsg = new MsgHeader();
        byte[] resContent = toBytes();
        resMsg.setContent(resContent);
        resMsg.setType(MsgType.RES_LOGIN);
        resMsg.setLength(resContent.length + 8);
        // 響應報文組裝完成,接下來發送
        logger.info("即將發送的東西是:" + msgHeader.toString() + "-->實體類:" + res_login.toString());
        final ChannelFuture channelFuture = ctx.writeAndFlush(resMsg);
        channelFuture.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                logger.info("響應報文發送成功");
                channelFuture.channel().close();
            }
        });
    }

    public byte[] toBytes() {
        if (res_login == null) {
            throw new NullPointerException("response entity is null");
        }
        return BigEndian.putInt(res_login.getErrCode());
    }

    public void fromBytes(byte[] src) {
        req_login = new Req_Login();
        int offset = 0;
        int usernameLength = BigEndian.getInt(src, offset);
        offset += 4;
        req_login.setUsername(BigEndian.getString(src, offset, usernameLength));
        offset += usernameLength;
        int passwordLength = BigEndian.getInt(src, offset);
        offset += 4;
        req_login.setPassword(BigEndian.getString(src, offset, passwordLength));
    }
}

還記MsgHeader的消息正文嗎?它是數據流,因此需要把它轉換爲pojo我們才能操作,具體看fromBytes方法,邏輯簡單這裏就不贅述了。

判斷完登錄數據後我們該發響應報文給客戶端了,通過MsgHeader進行組裝報文,其中報文正文則是通過toBytes方法轉換爲數據流。

接着調用上下文的writeAndFlush方法將數據寫入


之後數據被傳到編碼器的類,在這裏MsgHeader自身的toBytes方法把所有數據都轉換成流的形式,寫入ByteBuf中,之後客戶端將收到消息

public class PacketFrameEncoder extends MessageToByteEncoder<MsgHeader> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, MsgHeader msgHeader, ByteBuf byteBuf) throws Exception {
        byteBuf.writeBytes(msgHeader.toBytes());
    }
}

客戶端同理,導入netty包,其他操作和服務端類似

客戶端可以通過ChannelFuture的引用向服務端發送消息

public class Net {
    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap(); // (1)
            b.group(workerGroup); // (2)
            b.channel(NioSocketChannel.class); // (3)
            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline channelPipeline = ch.pipeline();
                    channelPipeline.addLast("decoder", new PacketFrameDecoder());
                    channelPipeline.addLast("encoder", new PacketFrameEncoder());
                    channelPipeline.addLast(new NetHandler());
                }
            });

            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)

            System.out.println("請輸入賬號和密碼");

            Scanner scanner = new Scanner(System.in);
            String username = scanner.nextLine();
            String password = scanner.nextLine();
            AccountEntity entity = new AccountEntity(username, password);
            MsgHeader msgHeader = new MsgHeader();
            msgHeader.setContent(entity.toBytes());
            msgHeader.setType(MsgType.REQ_LOGIN);
            msgHeader.setLength(msgHeader.getContent().length + 8);

            f.channel().writeAndFlush(msgHeader);

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}




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