Netty 长连接 Server 主动推送功能实现及问题

Posted 毕小宝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty 长连接 Server 主动推送功能实现及问题相关的知识,希望对你有一定的参考价值。

背景

使用 Netty 实现长连接,Client 端主动发起请求到 Server 端后,Server 缓存 Channel ,后续业务需要主动向 Client 推送数据时,直接遍历 Channel 写入数据。

用 Netty 实现 Http 服务器还是挺方便的,对 Netty 不太熟悉的时候,看着各种类似的代码,都能用,没有细究添加到 Pipeline 的处理器及区别。用了一个 HttpContentCompressor ,导致主动推送 HttpResponse 数据时写入失败。

本文将记录该问题的始末。

常规 Netty HttpServer 的代码

Netty 作为服务端,构建代码很常见:

public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) {
        channel.pipeline().addLast(new HttpServerCodec());
        // 提供在握手时聚合 HttpRequest,设置最大长度为 100Mb:超长则会报 413Request Entity Too Large
        channel.pipeline().addLast(new HttpObjectAggregator(104857600));
        // 压缩返回数据 HttpResponse
        channel.pipeline().addLast(new HttpContentCompressor());
        channel.pipeline().addLast(new ChunkedWriteHandler());
        channel.pipeline().addLast(new MyServerHandler());
    }
}

服务端与客户端保持长连接,必要的时候通过 Channel 向 Client 主动推送数据。上面的代码,在主动推送数据时,会报异常。

Netty Server 端主动向通道写入异常

Server 端的业务需要通过 Channel 向 Client 推送数据,推送代码为:

FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(result, CharsetUtil.UTF_8));   //  Unpooled.wrappedBuffer(responseJson)
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");       // HttpHeaderValues.TEXT_PLAIN.toString()
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
if (channel.isWritable()) {
    channel.writeAndFlush(httpResponse).addListener((ChannelFutureListener) future -> {
        if (future.isSuccess()) {
            logger.info("回写成功");
        } else {
            logger.error("回写失败",future.cause());
        }
    });
}

在使用了 HttpContentCompressor 后,writeAndFlush 回调总是进入异常分支:
在这里插入图片描述
完整的异常信息:

io.netty.handler.codec.EncoderException: java.lang.IllegalStateException: 
cannot send more responses than requests
        at io.netty.handler.codec.
        MessageToMessageEncoder.write(MessageToMessageEncoder.java:107)

去掉 HttpContentCompressor

去掉 HttpContentCompressor 后Server 主动推送的数据能够正确发送,为什么这个压缩 HttpResponse 的处理器会对数据写入有这么大的影响呢?

启示录

最初以为 Netty 实现的是 http 服务,Client 发送请求通过 Http 协议完成的,Server 主动推送数据失败是因为 Http 协议的局限性导致的。

于是尝试用 WebSocket 协议,这样反向推送是 OK 的。但是 Client 端全程只用一个 Socket 连接,导致其他正常的 Http 请求都升级为 WebSocket 请求了,其他 Http 协议处理流程无法正常运行。

结论:

  1. 如果要使用 HttpContentCompressor ,那么反向数据推送只能通过 WebSocket 协议进行。Client 端就需要区分 Http 请求和 Websocket 请求,用不同的 Socket 进行数据发送。
  2. 去掉 HttpContentCompressor 类,Client 采用 Http 请求缓存的 Channel 能正常推送数据,但是响应 Body 数据不会有压缩功能。应用中没有大数据量的传输业务,所以去掉也没啥影响。

以上是关于Netty 长连接 Server 主动推送功能实现及问题的主要内容,如果未能解决你的问题,请参考以下文章

Android 心跳包心跳连接 如何实现android和服务器长连接呢?推送消息的原理

Netty进阶 -- WebSocket长连接开发

Netty 实现长连接服务的难点和优化点

Netty 实现长连接服务的难点和优化点

Netty中使用WebSocket实现服务端与客户端的长连接通信发送消息

5.智能快递柜(通信篇-Server程序)