netty实现http server端

在网上查了很多netty server端实现的例子,感觉还是有很多坑,这里记录一下自己的实现,也把自己踩的坑记录一下,利人利己。

首先创建一个server类,用来初始化netty的server端

public class NettyHttpServer {

    private Logger logger = LoggerFactory.getLogger(NettyHttpServer.class);

    private int inetPort;

    public NettyHttpServer(final int inetPort) {
        this.inetPort = inetPort;
    }

    public int getInetPort() {
        return inetPort;
    }

    public void init() throws Exception {
        //根据自己机器的核心数设置parent和child数量,也可以不设定,netty会读取机器的核心数信息自行配置
        EventLoopGroup parentGroup = new NioEventLoopGroup(8);
        EventLoopGroup childGroup = new NioEventLoopGroup(16);

        try {
            ServerBootstrap server = new ServerBootstrap();

            // 1. 绑定两个线程组分别用来处理客户端通道的accept和读写时间
            server.group(parentGroup, childGroup)
                    // 2. 绑定服务端通道NioServerSocketChannel
                    .channel(NioServerSocketChannel.class)
                     //这句会在日志中打印netty日志,需要的可以加上
                    //.handler(new LoggingHandler(LogLevel.INFO))
                    // 3. 给读写事件的线程通道绑定handler去真正处理读写
                    // ChannelInitializer初始化通道SocketChannel
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline p = socketChannel.pipeline();
                            //这里要注意解码器和转码器的顺序,server端用下面的顺序,但是cilent端要反过来
                            //请求解码器
                            p.addLast("http-decoder", new HttpRequestDecoder());
                            // 响应转码器
                            p.addLast("http-encoder", new HttpResponseEncoder());
                            // 将HTTP消息的多个部分合成一条完整的HTTP消息,对于报文较长的请求需要设置,短请求可以不设置
                            p.addLast("http-aggregator", new HttpObjectAggregator(1024*1024));
                            // 自定义处理handler,这个handler中写自己的业务处理代码
                            p.addLast("http-server", new NettyHttpServerHandler());
                        }
                    });
            //开启http的长连接,这个根据自己的请求场景来设置
            server.option(ChannelOption.SO_KEEPALIVE, true)
                    //开启tcp立即响应,这样会影响整体性能,但是能保证单个响应不会延迟太高
                    .option(ChannelOption.TCP_NODELAY, true)
                    //开启请求缓存队列
                    .option(ChannelOption.SO_BACKLOG, 1024 * 1024)
                    //开启地址重用
                    .option(ChannelOption.SO_REUSEADDR, true);
            // 4. 监听端口(服务器host和port端口),同步返回
            logger.info("netty started");
            Channel ch = server.bind(this.inetPort).sync().channel();
            logger.info("netty started success");
            // 当通道关闭时继续向后执行,这是一个阻塞方法
            ch.closeFuture().sync();
        } finally {
            childGroup.shutdownGracefully();
            parentGroup.shutdownGracefully();
            logger.info("netty shutdown");

        }
    }
}

然后实现handler,完成自己的业务逻辑

//这边有多种类可以用来继承,server端一般使用ChannelInboundHandlerAdapter或者//SimpleChannelInboundHandler,不同之处在于后者会自动关闭conetxt,可能会导致通道在响应发送之前关//闭,所以我这边用ChannelInboundHandlerAdapter,需要手动关闭context
//client端一般使用ChannelOutboundHandlerAdapter或者SimpleChannelOutboundHandler
public class NettyHttpServerHandler extends ChannelInboundHandlerAdapter {

    private static Logger logger = LoggerFactory.getLogger(NettyHttpServerHandler.class);

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx){
        ctx.flush();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws  Exception{
       //这个方法中读取msg中的信息,进行自定义的业务逻辑处理
        BusinessHandler.processRequest(ctx,msg);
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws  Exception{
        logger.error("handler error:" + cause);
        ctx.close();
    }

}

需要注意的是channelRead方法中的两个参数,第一个参数看名字就明白是上下文信息,第二个参数msg,如果使用SimpleChannelInboundHandler,可以指定msg的类型,例如

public class NettyHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {}

那么就能在override的channelRead0()方法中指定msg的类型为FullHttpRequest,方便解析。

当然,这边的msg是可以有多种类型的,我这边使用的是FullHttpRequest,包含HttpRequest和FullHttpMessage两个类,http中的参数和内容等所有信息都在这两个类中。

然后实现BusinessHandler类

public class BusinessHandler {

    public static void processRequest(ChannelHandlerContext ctx, Object msg){

        FullHttpRequest fullHttpRequest = (FullHttpRequest)msg;
        //请求内容在content中,通过ByteBuf可以转换成bytes
        ByteBuf bb = fullHttpRequest.content();
        //获取uri
        fullHttpRequest.uri();
        //获取paramteer
        String sNow = getParameter(fullHttpRequest, "now");


        /**
        真·业务逻辑
        **/



        //判断是否是长连接
        boolean close = fullHttpRequest.headers().contains(CONNECTION, HttpHeaders.Values.CLOSE, true)
                        || fullHttpRequest.protocolVersion().equals(HttpVersion.HTTP_1_0)
                        && !fullHttpRequest.headers().contains(CONNECTION, HttpHeaders.Values.KEEP_ALIVE, true);
         // 返回
         FullHttpResponse response = processor.doResponse();
         if(close){
               //使用netstat查看你的服务器连接情况,如果存在大量time-wait状态的连接,就可能是这个地方的问题
               channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
          } else {
                channelHandlerContext.writeAndFlush(response);
          }
          //手动释放,防止内存泄露
          fullHttpRequest.release();
    }

    public String getParameter(FullHttpRequest fullHttpRequest, String par) throws IOException{
        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(fullHttpRequest);
        decoder.offer(fullHttpRequest);
        for (InterfaceHttpData parm : decoder.getBodyHttpDatas()) {
            Attribute data = (Attribute) parm;
            if(par.equals(data.getName())){
                return data.getValue();
            }
        }
        return null;
    }

}

如果自身的业务逻辑耗时较长,也可以使用线程池进行处理,这样能提升整体性能,如果追求单个响应的速度,就不要用线程池。

大致贴一下doResponse方法

public FullHttpResponse doResponse(){
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(200));
        //响应内容,内容的格式需要与下面设置的content-Type对应
        String resp = "response content";
        
         ByteBuf buffer = Unpooled.copiedBuffer(new StringBuilder(resp), CharsetUtil.UTF_8);
         response.content().writeBytes(buffer);
         //必须手动release,防止内存泄漏
         buffer.release();
         response.headers().set("Content-Type", "application/json;charset=utf-8");
         //在返回内容不为空的情况下,Content-Length必须手动设置,否者通道会阻塞
         response.headers().setInt("Content-Length", response.content().readableBytes());

         return response;
}

这样netty server端就创建完成了

 

更新一下:

最近做了netty包版本的升级,具体从4.1.14.Final升级到4.1.56.Final

升级之后出现了对外内存溢出的问题,排查之后发现是bytebuf没有回收,所以在所有使用了bytebuf的地方手动进行了release,之后问题解决,这个问题可以通过在jvm启动参数中加入

java -jar -Dio.netty.leakDetection.level=advanced your-server.jar   

来打印出可能存在内存泄露的地方来进行问题排查

另外一个问题就是可能会出现

io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement:1  

这样的报错,是因为同一个fullhttprequest的release次数大于了被引用次数,根据报错的地方修改一下release的次数就可以了

还有一个问题就是

channelHandlerContext.writeAndFlush(response);
fullHttpRequest.release();

在4.1.14.Final版本中,是不会报错的,但是在4.1.56.Final版本中会报错,就是上述IllegalReferenceCountException,这个问题没有细查,可能是在4.1.14.Final之后的版本中加入了retain的逻辑

另外有疑问可以参考官方文档https://netty.io/wiki/reference-counted-objects.html

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值