Netty学习-高并发-耗时业务

只记录遇到的问题,适用于有Netty基础的小伙伴,如果可以帮到别人那最好,如果有问题,请及时指出。

ByteBuf内存泄漏

handler中如果继承的是ChannelInboundHandlerAdapter类,则需要在重写消息处理channelRead方法中,手动释放ByteBuf,可以在finally块中写上:

ReferenceCountUtil.release(msg);

如果继承的是SimpleChannelInboundHandler类,重写channelRead0方法,则不需要,因为这个类继承了ChannelInboundHandlerAdapter类,重写了ChannelRead方法,并帮咱们在finally中释放了ByteBuf。

客户端给服务端发消息注意Channel是否建立成功

客户端connect(IP,PORT)的时候,最好加上监听或者sync()方法,要监听绑定IP或端口是否连接成功,否则如果连接不成功,就发送消息,会导致消息发不出去。

//监听方式
ChannelFuture future = b.connect(HOST,PORT).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                //监听建立连接成功在发送消息
                    future.channel().writeAndFlush(JSONObject.toJSONString("要发送的数据")+"$_");
                }
            });
或者
ChannelFuture future = b.connect(HOST,PORT).sync();//确保同步建立连接
future.channel().writeAndFlush(JSONObject.toJSONString("要发送的数据")+"$_");

粘包问题处理

我使用的是Netty自带的DelimiterBasedFrameDecoder类来使用特定字符来分隔数据的,这个也是网上看到的,当然客户端和服务端都需要加上这个分隔操作。

bootstrap.childHandler(new ChannelInitializer(){
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
            	//创建分隔符缓冲对象,使用"$_"作为分隔符
                ByteBuf delimiter= Unpooled.copiedBuffer("$_".getBytes());
                ch.pipeline().addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, delimiter));
                //读超时 5秒  写超时 1秒  读写超时 1秒  时间单位:秒
                ch.pipeline().addLast(new IdleStateHandler(5,1,1, TimeUnit.SECONDS));
                ch.pipeline().addLast(new ServerHeadler());//自定义的headler
                ch.pipeline().addLast(new StringEncoder());
            }
        });

sync()方法

个人理解:它就像synchronized关键字效果一样,只要加上sync()方法,那么它就是同步的,所以如果并发很大,就避免使用sync()方法,除非不影响多线程操作的地方可以使用,否则尽量不要使用,很影响性能。

ctx.close和ctx.channel.close

这两个的区别,ctx.close就是关闭当前上下文的。ctx.channel.close是关闭整个channel。
比如添加了多个handler,然后使用ctx.channel.close,那么它会依次关闭各个handler,然后关闭channel。
比如添加了多个handler,有1、2、3,3个handler,在走到第1个handler,然后使用ctx.close,那么第2个、第3个handler就不会执行了,执行完第1个,就直接关闭channel了。如果走到第二个handler,然后使用ctx.close,那么第1个会执行,第2个会执行,然后就关闭了第3个handler不会执行就关闭了channel了。
这样来看,这两个方法其实差不多,都能达到关闭channel的效果。使用哪个根据自己的业务场景来定。

耗时业务

在服务端的handler中,channelRead0处理消息方法中,采用线程池进行业务处理,从而保证netty自身的线程不被阻塞。
线程池类:

package com.cnpc.Executor;

import java.util.concurrent.*;

/**
 * 描述:
 * 创建: 一念丶 - LiMingQiang
 * 时间: 2019-03-29 13:43
 */
public class MyExecutor {

    private static volatile Executor executor = null;

    /**
     * 业务线程池
     * @param threadSize 池大小
     * @param queues 队列  - 为0时 设置为20000
     * @return
     */
    public static Executor getBusPool(int threadSize,int queues){
        if(executor==null){
            synchronized (MyExecutor.class){
                if(executor==null){
                    executor = new ThreadPoolExecutor(threadSize, threadSize, 0L, TimeUnit.SECONDS,
                        queues == 0 ? new LinkedBlockingQueue<>(5000) : new LinkedBlockingQueue<>(queues),
                        new RejectedExecutionHandler() {
                            //线程队列拒绝策略
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                                if (!e.isShutdown()) {
                                    //移除队头元素
                                    e.getQueue().poll();
                                    //再尝试入队
                                    e.execute(r);
                                }
                            }
                    });
                }
            }
        }
        return executor;
    }
}

服务端handler采用线程池处理耗时业务:

package com.cnpc.netty;

import com.alibaba.fastjson.JSONObject;
import com.cnpc.Executor.MyExecutor;
import com.cnpc.medium.Media;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import java.nio.charset.Charset;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 描述: NettyServerHandler - 处理消息的headler
 *       此headler是每个请求对应一个headler
 * 创建: 一念丶 - LiMingQiang
 * 时间: 2019-03-11 20:54
 */
public class ServerHeadler extends ChannelInboundHandlerAdapter {

    public static final AtomicLong aid = new AtomicLong(1);

    static{
        //定时任务,每10秒统计一次连接数
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() ->{
            System.out.println("当前连接数:"+aid.get());
        },0,1, TimeUnit.SECONDS);
    }

    private final Executor executor = MyExecutor.getBusPool(500);//500大小,队列5000

    private int lossConnectCount = 0;
    //消息处理
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception {
        try {
            if(msg instanceof ByteBuf){
          		final Object message = msg;
                final ChannelHandlerContext chx = ctx;
				//线程池处理耗时业务
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                    	ByteBuf b = (ByteBuf) message;
                		String content = b.toString((Charset.defaultCharset()));
                    	Thread.sleep(7000);//模拟耗时业务
                        ChannelFuture channelFuture = chx.channel().writeAndFlush(JSONObject.toJSONString("要返回的数据") + "$_");
                        channelFuture.addListener(new ChannelFutureListener() {
                            @Override
                            public void operationComplete(ChannelFuture future) throws Exception {
                                System.out.println("ctx:"+chx+"服务端响应完毕");
                                //释放ByteBuf防止内存溢出
                                ReferenceCountUtil.release(msg);
                            }
                        });
                    }
                });
            }
        }catch (Exception e){
//            e.printStackTrace();
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        aid.incrementAndGet();//连接进来+1
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        aid.decrementAndGet();//连接断开-1
    }

    //异常处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception {
        System.out.println(this.getClass().getName()+" -> [连接异常] "+ctx.channel().id()+
                "通道异常,异常原因:"+cause.getMessage());
        ctx.close();
    }

    //心跳监测
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent)evt;
            if(event.state().equals(IdleState.READER_IDLE)){ //如果读通道处于空闲状态,说明没有接收到心跳命令
                lossConnectCount++;
                if (lossConnectCount > 2) {
//                    System.out.println("[释放不活跃通道]"+ctx.channel().id());
                    ctx.channel().close();
                }
            }
        }else{
            super.userEventTriggered(ctx, evt);
        }
    }
}

高并发option的参数 - 慎用

区别option和childOption,简单就是说,在服务端程序中,option是针对自己的服务配置(监听socket使用),childOption是客户端连接成功后的channel配置选项(客户端连接后使用)。参数要慎用,知其然知其所以然方可使用,不然很容易就接不到消息,或者发布出去消息。

bootstrap.option(ChannelOption.SO_BACKLOG,4096);//请求的队列的最大长度
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);//重用缓冲区
bootstrap.option(ChannelOption.TCP_NODELAY, true);//立即发送数据
bootstrap.option(ChannelOption.SO_RCVBUF,128);//接收缓冲区大小
bootstrap.option(ChannelOption.SO_SNDBUF,256);//发送缓冲区大小
bootstrap.option(ChannelOption.SO_REUSEADDR,true);//快速端口复用
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);//连接保活

有的消息通信成功,有的消息通信失败

若出现这种情况,请注意以下问题。以下为个人遇到问题后梳理,仅供参考。

1、保证有足够的端口可以使用
2、保证有足够的连接数可以使用
3、保证channel的生命周期,确保channel可用
4、注意心跳检测关闭时机,可能出现先关闭后发送的情况,导致服务端发送消息成功,但客户端接收不到消息的情况。
5、大并发情况下,注意对象的创建、代码的编写可靠,尽量避免垃圾创建消耗服务器。
6、最后是服务器带宽上限问题。

你可能感兴趣的:(java)