为什么Netty变慢了?

举报
赵KK日常技术记录 发表于 2023/06/24 13:37:00 2023/06/24
【摘要】 体验过众多Netty的demo以后,在跟朋友分享后实现了页面昵称输入,消息可回车发送,页面保持输入框在底部,消息页面支持滚动,但是为什么当多客户端接入,消息会延迟,并注册变慢呢?传统JAVA BIO 阻塞+同步特点:1.客户端启动一个socket 每个客户建立一个链接2.判断服务器是否有线程响应,没有会等待或被拒绝3.有线程,等待请求响应结束体验代码package com.chat.ut...

体验过众多Netty的demo以后,在跟朋友分享后实现了页面昵称输入,消息可回车发送,页面保持输入框在底部,消息页面支持滚动,但是为什么当多客户端接入,消息会延迟,并注册变慢呢?

传统JAVA BIO 阻塞+同步

特点:

1.客户端启动一个socket 每个客户建立一个链接

2.判断服务器是否有线程响应,没有会等待或被拒绝

3.有线程,等待请求响应结束

体验代码

package com.chat.util;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDate;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author zhaokkstart
 * @create 2020-01-12 21:00
 */
public class Bio {

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

        ExecutorService executorService = Executors.newCachedThreadPool();

        ServerSocket serverSocket = new ServerSocket(8088);
        System.out.println("服务器启动....");
        while (true) {
            final Socket accept = serverSocket.accept();
            System.out.println("客户端接入....."+ LocalDate.now());
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    handler(accept);
                }
            });

        }
    }

    public static void handler(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println("接受到数据" + new String(bytes, 0, read));
                } else {
                    System.out.println("读取完毕");
                    break;
                }

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭客户端连接");
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

启动:

请在此添加图片描述

cmd  telnet 127.0.0.1 8088
ctrl ]
send hello netty

请在此添加图片描述

Connected to the target VM, address: '127.0.0.1:51231', transport: 'socket'
服务器启动....
客户端接入.....2020-01-12
接受到数据hello netty

同样打开一个窗口 发送hello netty2

客户端接入.....2020-01-12
接受到数据hello netty 2

而Nio是如何做的呢?

NIO 非阻塞IO java1.4

channel buffer Selector

线程Thread

Selector 根据不同的事件在各个channel切换

Channel(read/write) 多个连接 Event决定切换到那个channel

 ↑↓

Buffer buffer底层有一个数组 数据的读/写 双向的flip切换

Client(socket)

目前没有数据可用 就什么都不做,不会阻塞

Http2.0使用多路IO复用技术同一个技术处理多个请求

public class BasicBuffer {

    public static void main(String[] args) {
        IntBuffer intBuffer =IntBuffer.allocate(5);
        intBuffer.put(10);
        intBuffer.put(11);
        intBuffer.put(12);
        intBuffer.put(13);
        intBuffer.put(14);
        //读取数据  读写切换
        intBuffer.flip();
        while (intBuffer.hasRemaining()){
            System.out.println(intBuffer.get());
        }
    }
}

Buffer源码

public abstract class Buffer {

    /**
     * The characteristics of Spliterators that traverse and split elements
     * maintained in Buffers.
     */
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;

    // Invariants: mark <= position <= limit <= capacity
    mark标记
    private int mark = -1;
     position下一次执行位置
    private int position = 0;
    放入长度限制
    private int limit;
    容量
    private int capacity;

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;
}

Buffer子类

ByteBuffer,存储字节数据到缓冲区
ShortBuffer,存储字符串数据到缓冲区
CharBuffer,存储字符数据到缓冲区
IntBuffer,存储整数数据到缓冲区
LongBuffer,存储长整型数据到缓冲区
DoubleBuffer,存储小数到缓冲区
FloatBuffer,存储小数到缓冲区

以上子类中每一个源码中都有一个数组

    final int[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;

对比StringBuffer

StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。

如何做到线程安全的?

@Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }


    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);
    }

    /**
     * @since      1.5
     */
    @Override
    public synchronized void trimToSize() {
        super.trimToSize();
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @see        #length()
     */
    @Override
    public synchronized void setLength(int newLength) {
        toStringCache = null;
        super.setLength(newLength);
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @see        #length()
     */
    @Override
    public synchronized char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }

    /**
     * @since      1.5
     */
    @Override
    public synchronized int codePointAt(int index) {
        return super.codePointAt(index);
    }

    /**
     * @since     1.5
     */
    @Override
    public synchronized int codePointBefore(int index) {
        return super.codePointBefore(index);
    }

    /**
     * @since     1.5
     */
    @Override
    public synchronized int codePointCount(int beginIndex, int endIndex) {
        return super.codePointCount(beginIndex, endIndex);
    }

    /**
     * @since     1.5
     */
    @Override
    public synchronized int offsetByCodePoints(int index, int codePointOffset) {
        return super.offsetByCodePoints(index, codePointOffset);
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public synchronized void getChars(int srcBegin, int srcEnd, char[] dst,
                                      int dstBegin)
{
        super.getChars(srcBegin, srcEnd, dst, dstBegin);
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @see        #length()
     */
    @Override
    public synchronized void setCharAt(int index, char ch) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        toStringCache = null;
        value[index] = ch;
    }

    @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

大部分的方法都是synchronized

Netty特点是一个线程对应多个channel,不需要启动多个线程连接的啊?为什么会变慢呢?

public class HappyChatMain {
    private static final Logger logger = LoggerFactory.getLogger(HappyChatMain.class);

    public static void main(String[] args) {
        final HappyChatServer server = new HappyChatServer(Constants.DEFAULT_PORT);
        server.init();
        System.out.println("服务器等待连接.............");
        System.out.println("当前服务器CPU核数"+Runtime.getRuntime().availableProcessors());
        server.start();
        // 注册进程钩子,在JVM进程关闭前释放资源
        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run(){
                server.shutdown();
                logger.warn(">>>>>>>>>> jvm shutdown");
                System.exit(0);
            }
        });
    }
}

消息处理

@Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame)
            throws Exception {
        UserInfo userInfo = UserInfoManager.getUserInfo(ctx.channel());
        String content = frame.text();
        System.out.println("服务器在------" + LocalDate.now() + "收到消息" + content +
                "当前线程的id----" + Thread.currentThread().getId() + "线程名称" + Thread.currentThread().getName());
        Channel incoming = ctx.channel();
        clients.forEach(channel -> {
            if (channel != incoming) {
                channel.writeAndFlush(new TextWebSocketFrame("[" + incoming.remoteAddress() + "]" + frame.text()));
            } else {
                channel.writeAndFlush(new TextWebSocketFrame("[you]" + frame.text()));
            }
        });
        if (userInfo != null && userInfo.isAuth()) {
            JSONObject json = JSONObject.parseObject(frame.text());
            // 广播返回用户发送的消息文本
            UserInfoManager.broadcastMess(userInfo.getUserId(), userInfo.getNick(), json.getString("mess"));
        }
    }

启动:

服务器等待连接.............
当前服务器CPU核数4//本地电脑
userCount:

请在此添加图片描述

服务器在------2020-01-12收到消息{'code':10086,'mess':'2020年1月12日21:35:35  登录用户'}
当前线程的id----19线程名称DEFAULTEVENTLOOPGROUP_1

再启动一个客户端连接

请在此添加图片描述

服务器在------2020-01-12收到消息{'code':10086,'mess':'祖儿  登录2020年1月12日21:37:23'}
当前线程的id----22线程名称DEFAULTEVENTLOOPGROUP_2

UserInfoManager.java:124, broadCastPing userCount: 2个用户

但是当服务器接收多个客户端消息会出现延迟

请在此添加图片描述

服务器在------2020-01-12收到消息{'code':10086,'mess':'迪丽热巴 2020年1月12日21:39:47'}
当前线程的id----24线程名称DEFAULTEVENTLOOPGROUP_3

?????????

不是说好的Nio异步执行,不需要每次启动线程保持连接的吗?

现在为何每个客户端都进行了线程启动?

接受到的数据:[服务器在]2020-01-10T10:16:14.943接受到消息, 消息为:1002-------KK-------{'code':10086,'mess':'测试'}
2020-01-10 10:16:30.409  INFO 5514 --- [pool-1-thread-1] com.wolfbe.chat.handler.UserInfoManager  : broadCastPing userCount: 2
2020-01-10 10:16:30.443  INFO 5514 --- [VENTLOOPGROUP_1] com.wolfbe.chat.handler.UserAuthHandler  : receive pong message, address: 39.155.237.42:13007
2020-01-10 10:16:30.621  INFO 5514 --- [VENTLOOPGROUP_2] com.wolfbe.chat.handler.UserAuthHandler  : receive pong message, address: 223.104.3.131:25731
接受到的数据:[服务器在]2020-01-10T10:16:37.634接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'缺点不能去重'}
接受到的数据:[服务器在]2020-01-10T10:16:42.171接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:42.307接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:42.567接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:42.764接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'11'}
接受到的数据:[服务器在]2020-01-10T10:16:42.890接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:43.118接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:43.242接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:43.327接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:43.450接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:43.902接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:43.987接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:44.101接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:44.222接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:44.359接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
接受到的数据:[服务器在]2020-01-10T10:16:44.459接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'1'}
2020-01-10 10:16:50.389  INFO 5514 --- [pool-1-thread-1] com.wolfbe.chat.HappyChatServer          : scanNotActiveChannel --------
接受到的数据:[服务器在]2020-01-10T10:17:09.481接受到消息, 消息为:1002-------KK-------{'code':10086,'mess':'有没有延迟'}
接受到的数据:[服务器在]2020-01-10T10:17:18.279接受到消息, 消息为:1001-------kk-------{'code':10086,'mess':'有延迟'}
2020-01-10 10:17:20.409  INFO 5514 --- [pool-1-thread-1] com.wolfbe.chat.handler.UserInfoManager  : broadCastPing userCount: 2
2020-01-10 10:17:20.434  INFO 5514 --- [VENTLOOPGROUP_1] com.wolfbe.chat.handler.UserAuthHandler  : receive pong message, address: 39.155.237.42:13007
2020-01-10 10:17:20.484  INFO 5514 --- [VENTLOOPGROUP_2] com.wolfbe.chat.handler.UserAuthHandler  : receive pong message, address: 223.104.3.131:25731
2020-01-10 10:17:50.389  INFO 5514 --- [pool-1-thread-1] com.wolfbe.chat.HappyChatServer          : scanNotActiveChannel --------
2020-01-10 10:17:51.332  WARN 5514 --- [VENTLOOPGROUP_2] com.wolfbe.chat.handler.UserInfoManager  : channel will be remove, address is :223.104.3.131:25731
2020-01-10 10:17:51.333  WARN 5514 --- [VENTLOOPGROUP_2] com.wolfbe.chat.handler.UserInfoManager  : channel will be remove, address is :223.104.3.131:25731
2020-01-10 10:18:10.409  INFO 5514 --- [pool-1-thread-1] com.wolfbe.chat.handler.UserInfoManager  : broadCastPing userCount: 1
2020-01-10 10:18:10.437  INFO 5514 --- [VENTLOOPGROUP_1] com.wolfbe.chat.handler.UserAuthHandler  : receive pong message, address: 39.155.237.42:13007
2020-01-10 10:18:24.418  WARN 5514 --- [VENTLOOPGROUP_1] com.wolfbe.chat.handler.UserInfoManager  : channel will be remove, address is :39.155.237.42:13007

第一有延迟

而我可怜的阿里云私人服务器1 vCPU 2 GiB (I/O优化)!!!

1核啊,不管是cpu密集型,还是io密集型4个线程就够服务器响应的了,而且每个客户端为了保持长连接,还不会断开,所以当客户端多了,消息多了以后就会变慢,这严重违背了Nio的思想。

修改:

mainGroup = new NioEventLoopGroup();
subGroup = new NioEventLoopGroup();

启用Nio事件驱动组即可解决!!!

Connected to the target VM, address: '127.0.0.1:52594', transport: 'socket'
当前服务器CPU核数4
服务器等待连接.............
接收到的数据是:     hello  netty
TextWebSocketFrame(data: UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 78, cap: 156))
当前执行线程名字nioEventLoopGroup-5-1

请在此添加图片描述

永远不会延迟加载。

Netty的0拷贝不是不拷贝 是指没有CPU拷贝

只要你想开始,随时可以启程

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。