在Android中使用Netty进行通讯,附带服务端代码

Netty

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke’s Choice Award,见https://www.java.net/dukeschoice/2011)。它活跃和成长于用户社区,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

依赖引入

由于使用最新版本的话,发现有个类找不到,后面查了下是因为jdk版本,在android中的话,太高的jdk版本肯定不支持,所以我找了19年的发行版,测试ok。

implementation 'io.netty:netty-all:4.1.42.Final'

服务端代码实现

NettyServer

@Slf4j
public class NettyServer {
     

    public void start(InetSocketAddress socketAddress) {
     
        //new 一个主线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //new 一个工作线程组
        EventLoopGroup workGroup = new NioEventLoopGroup(200);
        ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ServerChannelInitializer())
                .localAddress(socketAddress)
                //设置队列大小
                .option(ChannelOption.SO_BACKLOG, 1024)
                // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
                .childOption(ChannelOption.SO_KEEPALIVE, true);
        //绑定端口,开始接收进来的连接
        try {
     
            ChannelFuture future = bootstrap.bind(socketAddress).sync();
            log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
//            future.channel().writeAndFlush("你好啊");
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            //关闭主线程组
            bossGroup.shutdownGracefully();
            //关闭工作线程组
            workGroup.shutdownGracefully();
        }
    }
}

ServerChannelInitializer

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
     
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
     
        //添加编解码
        socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
        socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
        socketChannel.pipeline().addLast(new NettyServerHandler());
    }
}

NettyServerHandler

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
     
    /**
     * 客户端连接会触发
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
     
        log.info("Channel active......");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
     
        log.info("Channel Inactive......");
    }

    /**
     * 客户端发消息会触发
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
     
        //----------- 只改了这里 -----------
        log.info("服务器收到消息1111: {}", msg.toString());

        ctx.write("{\"data\":{\"taskData\":{\"collectionRule\":{\"id\":1,\"name\":\"IP主播直播室互动用户\",\"rule\":\"[{\\\"label\\\":\\\"抖音号\\\",\\\"key\\\":\\\"dyId\\\",\\\"type\\\":\\\"string\\\"},{\\\"key\\\":\\\"count\\\",\\\"label\\\":\\\"数量\\\",\\\"type\\\":\\\"string\\\"}]\",\"ruleType\":\"collect\",\"source\":\"collectLiveAudience\"},\"description\":\"粉丝列表-付鹏的财经世界3\",\"ruleId\":\"1\",\"ruleParam\":\"{\\\"dyId\\\":\\\"ghsys\\\",\\\"count\\\":\\\"140000\\\"}\"},\"taskId\":\"64\",\"taskType\":\"collection\"},\"devicesId\":\"5011bbdcd5006a93\",\"type\":\"task\"}");
        ctx.flush();
    }

    /**
     * 发生异常触发
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
     
        cause.printStackTrace();
        ctx.close();
    }
}

启动服务

@SpringBootApplication
public class ServerApplication {
     

    public static void main(String[] args) {
     
        SpringApplication.run(ServerApplication.class, args);
        //启动服务端
        NettyServer nettyServer = new NettyServer();
        nettyServer.start(new InetSocketAddress("192.18.52.95", 8091));
    }
}

客户端代码实现

其实netty的使用客户端和服务器端整体上是差不多的,所以这里只列出来核心代码。

初始化操作

abstract class McnNettyTask : Runnable {
     

    private var socketChannel: SocketChannel? = null
    private var isConnected = false

    override fun run() {
     
        createConnection()
    }

    private fun createConnection() {
     
        val nioEventLoopGroup = NioEventLoopGroup()
        val bootstrap = Bootstrap()
        bootstrap
            .group(nioEventLoopGroup)
            .option(ChannelOption.TCP_NODELAY, true)  //无阻塞
            .channel(NioSocketChannel::class.java)
            .option(ChannelOption.SO_KEEPALIVE, true) //长连接
            .option(ChannelOption.SO_TIMEOUT, 30_000) //收发超时
            .handler(McnClientInitializer(object : McnClientListener {
     
                override fun disConnected() {
     
                    isConnected = false
                }

                override fun connected() {
     
                    isConnected = true
                }
            }, object : McnEventListener {
     
                override fun onReceiverMessage(messageRequest: MessageRequest) {
     
                    dispatchMessage(messageRequest)
                }
            }))
        try {
     
            val channelFuture = bootstrap.connect(McnNettyConfig.ip, McnNettyConfig.port)
                .addListener(object : ChannelFutureListener {
     
                    override fun operationComplete(future: ChannelFuture) {
     
                        if (future.isSuccess) {
     
                            socketChannel = future.channel() as SocketChannel;
                            isConnected = true
                            CommonConsole.log("netty connect success (ip: ${
       McnNettyConfig.ip}, port: ${
       McnNettyConfig.port})")

                            sendMsg(MessageRequest.createDevicesState(0))
                        } else {
     
                            CommonConsole.log("netty connect failure (ip: ${
       McnNettyConfig.ip}, port: ${
       McnNettyConfig.port})")
                            isConnected = false
                            future.channel().close()
                            nioEventLoopGroup.shutdownGracefully()
                        }
                    }
                }).sync()//阻塞,直到连接完成
            channelFuture.channel().closeFuture().sync()
        } catch (ex: Exception) {
     
            ex.printStackTrace()
        } finally {
     
            //释放所有资源和创建的线程
            nioEventLoopGroup.shutdownGracefully()
        }
    }

    fun isConnected(): Boolean {
     
        return isConnected
    }

    fun disConnected() {
     
        socketChannel?.close()
    }

    abstract fun dispatchMessage(messageRequest: MessageRequest)

    fun sendMsg(msg: String, nettyMessageListener: McnMessageListener? = null) {
     
        if (!isConnected()) {
     
            nettyMessageListener?.sendFailure()
            return
        }
        socketChannel?.run {
     
            writeAndFlush(msg + "###").addListener {
      future ->
                if (future.isSuccess) {
     
                    //消息发送成功
                    CommonConsole.log("netty send message success (message: $msg")
                    nettyMessageListener?.sendSuccess()
                } else {
     
                    //消息发送失败
                    CommonConsole.log("netty send message failure (message: $msg")
                    nettyMessageListener?.sendFailure()
                }
            }
        }
    }
}

加载handler和Initializer

class McnClientInitializer(
    private val nettyClientListener: McnClientListener,
    private val nettyEventListener: McnEventListener
) :
    ChannelInitializer<SocketChannel>() {
     

    override fun initChannel(socketChannel: SocketChannel) {
     
        val pipeline = socketChannel.pipeline()
//        pipeline.addLast("decoder", McnStringDecoder())
//        pipeline.addLast("encoder", McnStringEncoder())
//        pipeline.addLast(LineBasedFrameDecoder(1024))
        pipeline.addLast("decoder", StringDecoder())
//        pipeline.addLast("encoder", StringEncoder())
        pipeline.addLast(DelimiterBasedFrameEncoder("###"))
        pipeline.addLast(McnClientHandler(nettyClientListener, nettyEventListener))
    }
}

核心数据接收处理handler

class McnClientHandler(
    private val nettyClientListener: McnClientListener,
    private val nettyEventListener: McnEventListener
) :
    SimpleChannelInboundHandler<String>() {
     

    override fun channelActive(ctx: ChannelHandlerContext?) {
     
        super.channelActive(ctx)
        CommonConsole.log("Netty channelActive.........")
        nettyClientListener.connected()
    }

    override fun channelInactive(ctx: ChannelHandlerContext?) {
     
        super.channelInactive(ctx)
        nettyClientListener.disConnected()
    }

    override fun channelReadComplete(ctx: ChannelHandlerContext?) {
     
        super.channelReadComplete(ctx)
        CommonConsole.log("Netty channelReadComplete.........")
    }

    override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) {
     
        super.exceptionCaught(ctx, cause)
        CommonConsole.log("Netty exceptionCaught.........${cause?.message}")
        cause?.printStackTrace()
        ctx?.close()
    }

    override fun channelRead0(ctx: ChannelHandlerContext?, msg: String?) {
     
        CommonConsole.log("Netty channelRead.........${msg}")
        msg?.run {
     
            try {
     
                val messageRequest =
                    Gson().fromJson<MessageRequest>(msg, MessageRequest::class.java)
                nettyEventListener.onReceiverMessage(messageRequest)
            } catch (ex: Exception) {
     
                ex.printStackTrace()
            }
            ReferenceCountUtil.release(msg)
        }
    }
}

处理数据粘包 & 数据分包

如果使用netty,你肯定会碰到数据粘包和数据分包的问题的。所谓数据粘包就是当数据量比较小的情况下,相近时间内的多个发送数据会被作为一个数据包接收解析。而数据分包就是会将一个比较大的数据包分成为很多个小的数据包。不管是数据的分包还是粘包,都会导致我们使用的时候不能简单使用数据,所以我们要对粘包和分包数据做处理,让每次发送的数据都是独立且完整的。

对于数据编码,使用自定义的解析器处理,相当于是对数据使用特定字符串做拼接和反取操作。

服务端:

ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
        Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串数据
ch.pipeline().addLast(new StringDecoder());
// 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder("###"));
// 最终处理数据并且返回响应的handler
ch.pipeline().addLast(new EchoServerHandler());

客户端:

pipeline.addLast(IdleStateHandler(15, 15, 15))
pipeline.addLast(
    DelimiterBasedFrameDecoder(
        1024,
        Unpooled.copiedBuffer("###".toByteArray())
    )
)
pipeline.addLast(StringDecoder(StandardCharsets.UTF_8))
pipeline.addLast(McnClientHandler(nettyClientListener, nettyEventListener))
pipeline.addLast(DelimiterBasedFrameEncoder("###"))
pipeline.addLast(StringEncoder(StandardCharsets.UTF_8));

DelimiterBasedFrameEncoder

public class DelimiterBasedFrameEncoder extends MessageToByteEncoder<String> {
     

    private String delimiter;

    public DelimiterBasedFrameEncoder(String delimiter) {
     
        this.delimiter = delimiter;
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out)
            throws Exception {
     
        // 在响应的数据后面添加分隔符
        ctx.writeAndFlush(Unpooled.wrappedBuffer((msg + delimiter).getBytes()));
    }
}

如果是

对客户端 & 服务端通讯加入[暗号]

在客户端和服务端数据通讯的时候,为了确保数据的完整性和安全性,通常会加入一段暗号,作为安全校验。其实两端真实的通信结构就变成了如下:

完整数据 = 暗号字节 + 真实数据内容

客户端和服务端在获取到数据之后,按照约定将暗号数据移除之后,剩下的就是正式的数据。

对于暗号的处理,可以通过MessageToMessageEncoder来实现,通过对获取到的通讯字节码编解码来对数据处理。

McnStringEncoder

/**
 * copy自 StringEncoder源码,进行修改,增加了业务处理暗号
 */
class McnStringEncoder : MessageToMessageEncoder<CharSequence> {
     

    var charset: Charset? = null

    constructor(charset: Charset?) {
     
        if (charset == null) {
     
            throw NullPointerException("charset")
        } else {
     
            this.charset = charset
        }
    }

    constructor() : this(Charset.defaultCharset())

    override fun encode(ctx: ChannelHandlerContext?, msg: CharSequence?, out: MutableList<Any>?) {
     
        if (msg?.isNotEmpty() == true) {
     
            out?.add(
                ByteBufUtil.encodeString(
                    ctx!!.alloc(),
                    CharBuffer.wrap(McnNettyConfig.private_key + msg),
                    charset
                )
            )
        }
    }
}

McnStringDecoder

/**
 * copy自 StringDecoder源码,进行修改,增加了业务处理暗号
 */
class McnStringDecoder : MessageToMessageDecoder<ByteBuf> {
     
    var charset: Charset? = null

    constructor(charset: Charset?) {
     
        if (charset == null) {
     
            throw NullPointerException("charset")
        } else {
     
            this.charset = charset
        }
    }

    constructor() : this(Charset.defaultCharset())

    override fun decode(ctx: ChannelHandlerContext?, msg: ByteBuf?, out: MutableList<Any>?) {
     
        msg?.run {
     
            Log.e("info", "decoder结果====>${
       msg.toString(charset)}")
            //校验报文长度是否合法
            if (msg.readableBytes() <= McnNettyConfig.keyLength) {
     
                out?.add(ErrorData.creator(ErrorData.LENGTH_ERROR, "报文长度校验失败"))
                return
            }
            val privateKey = this.readBytes(McnNettyConfig.keyLength)
            //校验报文暗号是否匹配
            if (privateKey.toString(charset) != McnNettyConfig.private_key) {
     
                out?.add(ErrorData.creator(ErrorData.PRIVATE_KEY_ERROR, "报文暗号校验失败"))
                return
            }
            //获取真实报文内容
            out?.add(this.toString(charset))
        }
    }

    data class ErrorData(
        var errorCode: Int,
        var errorMsg: String
    ) {
     

        companion object {
     

            //长度异常
            const val LENGTH_ERROR = -10001

            //报文校验失败
            const val PRIVATE_KEY_ERROR = -10002

            @JvmStatic
            fun creator(errorCode: Int, message: String): String {
     
                val errorData = ErrorData(errorCode, message)
                return JSON.toJSONString(errorData)
            }
        }
    }
}

最后的使用就很简单了。只需要将我们的处理加到处理链就行了。

val pipeline = socketChannel.pipeline()
pipeline.addLast("decoder", McnStringDecoder())
pipeline.addLast("encoder", McnStringEncoder())

增加ssl安全配置

因为netty使用的话涉及到端口和ip,所以访问的安全就显的很重要了。网上看了一堆关于netty使用的,但是发现其实都写的很复杂,咨询了对接的java端大佬之后,发现其实使用简单的设置就可以支持ssl

首先将后端服务器生成的cer证书和key文件放置到asset目录下,我这里在assets下创建的ssl文件中,然后加载即可。

val keyInput = App.app.assets.open("ssl/xxx.key")
val cerInput= App.app.assets.open("ssl/xxx.cer")
val engine =
    SslContextBuilder.forServer(cerInput, keyInput).build().newEngine(socketChannel.alloc())
engine.useClientMode = true
//这里将sslhandler加载在其他的decoder和encoder之前。
pipeline.addFirst(SslHandler(engine))

增加心跳

由于netty机制,如果设备闲置一段时间后,会自动断开,所以为了使用正常,就需要设置心跳,netty提供了心跳的支持。

pipeline.addLast(IdleStateHandler(15, 15, 15))

使用IdleStateHandler,可以在读写超过设置的值之后出发userEventTrigger回调。

为了防止读写闲置超时,一般的做法是在客户端每过一段时间,发一条心跳消息,服务端如果收到心跳消息之后,则回复一条消息,这样让netty认为连接是正常可用的。

参考

https://www.cnblogs.com/AIPAOJIAO/p/10631551.html

你可能感兴趣的:(android,android,java,netty)