【Netty】自定义网络通信协议

介绍

本文主要介绍如何通过Netty自定义网络通信协议,协议比较简单和基础,相关的代码和原理也都不难理解,,重点是如何利用netty实现整个流程。我会提供完整的代码,有如何自定义编解码器的实现。
总共大概有如下几个步骤:

  • 定义消息通信协议
  • 定义解码器
  • 定义编码器
  • 定义服务器ChannelHandler
  • 定义客户端ChannelHandler
  • 定义服务器主程序
  • 定义客户端主程序

总共就这几步,做完这些就可以测试了~因为都比较简单,所以不过多解释,下面开始实现。

消息通信协议

消息

package cn.md.netty.codec;

/**
 * * @Author: Martin
 * * @Date    2024/9/4 22:25
 * * @Description
 **/
public class Msg {

    private MsgHeader msgHeader = new MsgHeader();

    private String body;

    public MsgHeader getMsgHeader() {
        return msgHeader;
    }

    public void setMsgHeader(MsgHeader msgHeader) {
        this.msgHeader = msgHeader;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }


    public static Msg buildFromReqBody(String body) {
        Msg msg = new Msg();
        msg.setMsgHeader(new MsgHeader(MsgType.LOGIN_REQ.getValue(), body.getBytes().length));
        msg.setBody(body);
        return msg;
    }

    public static Msg buildFromResBody(String body) {
        Msg msg = new Msg();
        msg.setMsgHeader(new MsgHeader(MsgType.LOGIN_RES.getValue(), body.getBytes().length));
        msg.setBody(body);
        return msg;
    }
}

消息头

package cn.md.netty.codec;

/**
 * * @Author: Martin
 * * @Date    2024/9/4 22:24
 * * @Description
 **/
public class MsgHeader {

    // 消息类型
    private byte msgType;

    // 消息体长度
    private int len;

    public MsgHeader() {
    }

    public MsgHeader(byte msgType, int len) {
        this.msgType = msgType;
        this.len = len;
    }

    public byte getMsgType() {
        return msgType;
    }

    public void setMsgType(byte msgType) {
        this.msgType = msgType;
    }

    public int getLen() {
        return len;
    }

    public void setLen(int len) {
        this.len = len;
    }
}

定义解码器

package cn.md.netty.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * * @Author: Martin
 * * @Date    2024/9/4 22:38
 * * @Description
 **/
public class MyDecoder extends ByteToMessageDecoder {

    /**
     * Decode the from one {@link ByteBuf} to an other. This method will be called till either the input
     * {@link ByteBuf} has nothing to read when return from this method or till nothing was read from the input
     * {@link ByteBuf}.
     *
     * @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
     * @param in  the {@link ByteBuf} from which to read data
     * @param out the {@link List} to which decoded messages should be added
     * @throws Exception is thrown if an error occurs
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() < 5) {
            // 至少需要消息类型(1 字节)和消息体长度(4 字节)
            return;
        }
        
        byte msgType = in.readByte();
        int len = in.readInt();
        if (in.readableBytes() < len) {
            // 消息不完整,等待更多数据
            in.resetReaderIndex();
            return;
        }

        byte[] bodyBytes = new byte[len];
        in.readBytes(bodyBytes);

        Msg msg = new Msg();
        msg.setMsgHeader(new MsgHeader(msgType, len));
        msg.setBody(new String(bodyBytes, StandardCharsets.UTF_8));
        out.add(msg);

    }
}

定义编码器

package cn.md.netty.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

import java.nio.charset.Charset;

/**
 * 将Msg编码为ByteBuf
 * * @Author: Martin
 * * @Date    2024/9/4 22:26
 * * @Description
 **/
public class MyEncoder extends MessageToByteEncoder<Msg> {


    /**
     * Encode a message into a {@link ByteBuf}. This method will be called for each written message that can be handled
     * by this encoder.
     *
     * @param ctx the {@link ChannelHandlerContext} which this {@link MessageToByteEncoder} belongs to
     * @param msg the message to encode
     * @param out the {@link ByteBuf} into which the encoded message will be written
     * @throws Exception is thrown if an error occurs
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, Msg msg, ByteBuf out) throws Exception {
        if (msg == null || msg.getMsgHeader() == null) {
            throw new Exception("The encode message is null");
        }

        MsgHeader header = msg.getMsgHeader();
        String body = msg.getBody();

        byte[] bodyBytes = body.getBytes(Charset.forName("utf-8"));

        // 计算消息体的长度
        int length = bodyBytes.length;

        // 写入消息类型
        out.writeByte(MsgType.LOGIN_REQ.getValue());
        // 写入消息体长度
        out.writeInt(length);
        // 写入消息体
        out.writeBytes(bodyBytes);
    }
}

定义服务器ChannelHandler

package cn.md.netty.codec;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.net.SocketAddress;

/**
 * * @Author: Martin
 * * @Date    2024/9/4 22:48
 * * @Description
 **/
public class MyServerHandler extends SimpleChannelInboundHandler<Object> {

    /**
     * Is called for each message of type {@link I}.
     *
     * @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
     *            belongs to
     * @param obj the message to handle
     * @throws Exception is thrown if an error occurred
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object obj) throws Exception {
        if (obj instanceof Msg) {
            Msg msg = (Msg) obj;
            String body = msg.getBody();
            System.out.println("服务端收到消息:" + body);

            Msg resMsg = Msg.buildFromResBody("没听清,你再说一边");
            ctx.writeAndFlush(resMsg);
        }
    }
}

定义客户端ChannelHandler

package cn.md.netty.codec.client;

import cn.md.netty.codec.Msg;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * * @Author: Martin
 * * @Date    2024/9/4 22:52
 * * @Description
 **/
public class MyClientHandler extends SimpleChannelInboundHandler<Object> {

    /**
     * Is called for each message of type {@link I}.
     *
     * @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
     *            belongs to
     * @param obj the message to handle
     * @throws Exception is thrown if an error occurred
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object obj) throws Exception {
        if (obj instanceof Msg) {
            Msg msg = (Msg) obj;
            String body = msg.getBody();
            System.out.println("客户端收到消息:" + body);
        }
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelInactive");
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved");
    }
}

定义服务器主程序

package cn.md.netty.codec;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * * @Author: Martin
 * * @Date    2024/9/4 22:53
 * * @Description
 **/
public class MyServer {

    public static void main(String[] args) throws InterruptedException {

        ServerBootstrap b = new ServerBootstrap();
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {

            b.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,64)
                    .childOption(ChannelOption.SO_KEEPALIVE,true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        /**
                         * This method will be called once the {@link Channel} was registered. After the method returns this instance
                         * will be removed from the {@link ChannelPipeline} of the {@link Channel}.
                         *
                         * @param ch the {@link Channel} which was registered.
                         * @throws Exception is thrown if an error occurs. In that case it will be handled by
                         *                   {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
                         *                   the {@link Channel}.
                         */
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast("decoder", new MyDecoder())
                                    .addLast("encoder", new MyEncoder())
                                    //.addLast("codec", new MyCodec())
                                    .addLast("handler", new MyServerHandler());
                        }
                    });


            ChannelFuture channelFuture = b.bind(7788).sync();
            channelFuture.channel().closeFuture().sync();

        } catch (Exception e) {
            throw e;
        } finally{
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

定义客户端主程序

package cn.md.netty.codec.client;

import cn.md.netty.codec.Msg;
import cn.md.netty.codec.MyCodec;
import cn.md.netty.codec.MyDecoder;
import cn.md.netty.codec.MyEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.util.Scanner;

/**
 * * @Author: Martin
 * * @Date    2024/9/4 22:56
 * * @Description
 **/
public class MyClient {

    public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup loopGroup = new NioEventLoopGroup(1);
        Bootstrap b = bootstrap.group(loopGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    /**
                     * This method will be called once the {@link Channel} was registered. After the method returns this instance
                     * will be removed from the {@link ChannelPipeline} of the {@link Channel}.
                     *
                     * @param ch the {@link Channel} which was registered.
                     * @throws Exception is thrown if an error occurs. In that case it will be handled by
                     *                   {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
                     *                   the {@link Channel}.
                     */
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast("decoder", new MyDecoder())
                                .addLast("encoder", new MyEncoder())
//                                .addLast("codec", new MyCodec())
                                .addLast("handler", new MyClientHandler());
                    }
                });

        ChannelFuture future = b.connect("localhost", 7788);

        while (true) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入要发送的消息内容");
            String msg = scanner.nextLine();
            if ("exit".equals(msg)) {
                future.channel().close();
                break;
            }
            Msg sendMsg = Msg.buildFromReqBody(msg);
            future.channel().writeAndFlush(sendMsg);
        }
    }

}

测试结果

客户端:
在这里插入图片描述
服务端:
在这里插入图片描述

其他

在客户端和服务端的主程序中,在添加ChannelHandler时,都设置了编码器和解码器

							ch.pipeline()
                                .addLast("decoder", new MyDecoder())
                                .addLast("encoder", new MyEncoder())
//                                .addLast("codec", new MyCodec())
                                .addLast("handler", new MyClientHandler());

也可以使用一个聚合的编解码器实现,就是MyCodec 等同于 MyDecoder + MyEncoder。
把下面的注释打开,把上面的编解码器注释,如下:

							ch.pipeline()
                                //.addLast("decoder", new MyDecoder())
                                //.addLast("encoder", new MyEncoder())
                                .addLast("codec", new MyCodec())
                                .addLast("handler", new MyClientHandler());
                               

MyCodec.java

package cn.md.netty.codec;

import io.netty.channel.CombinedChannelDuplexHandler;

/**
 * * @Author: Martin
 * * @Date    2024/9/5 12:10
 * * @Description
 **/
public class MyCodec extends CombinedChannelDuplexHandler<MyDecoder,MyEncoder> {


    public MyCodec()
    {
        super(new MyDecoder(),new MyEncoder());
    }


}


文章到这结束了,本文介绍了如何自定义一个简单的通信协议,并通过Netty进行通络通信交互。
代码很简单,希望能举一反三,融会贯通。后续我还会持续分享一些关于Netty的文章,不妨点个关注~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马丁的代码日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值