1
阻塞, 非阻塞和异步,同步的理解
  • 阻塞/非阻塞: 是相对于数据而言的, 即服务端数据没有准备好时, 阻塞情况下, 客户端要一直等待, 不能做其他操作, 直到服务端返回数据, 而非阻塞就是客户端此时可以暂时离开去做其他的处理操作;
  • 同步/异步: 是相对于网络IO而言, 即在同步的时候, 如果此时服务端有未处理完的请求, 其他客户端是不能连接上服务端的, 要一直等待, 而异步就是, 即使服务端有未处理完的的请求, 客户端依然可以连接服务端, 进行请求处理;
BIO,NIO
  • BIO:同步阻塞, java中Socket连接, 由于是阻塞式的, 实现通信的效率较低, 通常的实现方式是服务端每接受到一个请求就开启一个线程处理, 但是这种模式, 会因为线程数量而限制服务端的效率;
  • NIO:异步非阻塞, java中的NioSocket连接, 为解决BIO的问题, NIO使用的是, NIO是基于selector线程模型设计, 即服务端存在一个线程不断轮询的注册链接过来的channel(可理解为客户端连接), 并对其进行监听, 当某个Channel上发生读或者写的操作时, 就会被轮询出来, 进行处理;
NIO实现代码
服务端
try {
            ServerSocketChannel socketChannel = ServerSocketChannel.open();
            socketChannel.configureBlocking(false);// 设置非阻塞
            socketChannel.socket().bind(new InetSocketAddress(8081), 1024);

            Selector selector = Selector.open();

            socketChannel.register(selector, SelectionKey.OP_ACCEPT); //准备接收消息
            System.out.println("server is started");

            while(true) {
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();

                    if (selectionKey.isAcceptable()) {
                        ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel accept = channel.accept();
                        accept.configureBlocking(false);
                        accept.register(selector, SelectionKey.OP_READ); //设置为可读取
                    }

                    if (selectionKey.isReadable()) {
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        ByteBuffer allocate = ByteBuffer.allocate(1024);
                        int read = channel.read(allocate);
                        if (read > 0) {
                            allocate.flip();
                            byte[] bytes = new byte[allocate.remaining()];
                            allocate.get(bytes);
                            String content = new String(bytes, "UTF-8");
                            System.out.println("server recieve message "+content);

                            long timeMillis = System.currentTimeMillis();
                            byte[] response = (timeMillis + "").getBytes();
                            allocate.clear();
                            allocate.put(response);
                            allocate.flip();
                            channel.write(allocate);
                        } else {
                            channel.close();
                        }
                    }
                }
            }


        } catch (IOException e) {
            e.printStackTrace();
        }
客户端
try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);

            Selector selector = Selector.open();
            if (socketChannel.connect(new InetSocketAddress("127.0.0.1", 8081))) {
                socketChannel.register(selector, SelectionKey.OP_READ);
                doWrite(socketChannel);
            } else {
                socketChannel.register(selector, SelectionKey.OP_CONNECT);
            }

            while (true) {
                selector.select(1000);
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();
                if (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();

                    SocketChannel channel = (SocketChannel) key.channel();
                    if (key.isConnectable()) {
                        if (channel.finishConnect()) {
                            channel.register(selector, SelectionKey.OP_READ);
                            doWrite(socketChannel);
                        }else {
                            socketChannel.register(selector, SelectionKey.OP_CONNECT);
                        }
                    }

                    if (key.isReadable()) {
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int read = channel.read(buffer);
                        if (read > 0) {
                            byte[] bytes = new byte[buffer.remaining()];
                            buffer.get(bytes);
                            String response = new String(bytes, "UTF-8");
                            System.out.println("client recieve message "+response);
                        }else {
                            key.cancel();
                            channel.close();
                        }
                    }

                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    private static void doWrite(SocketChannel socketChannel) throws IOException {
        byte[] bytes = "query time order".getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        buffer.put(bytes);
        buffer.flip();
        socketChannel.write(buffer);
    }
netty介绍
netty封装了java NIO, 并使用多线程实现了异步非阻塞, 同时还解决了NIO在linux系统下出现空轮询的bug(会造成CPU 100%问题), 主要是在selector.select(time)时, 添加一个计数器, 当空轮询超过512次的时候, 跳出循环, 将原来的channel注册在新的selector上, 原来的selector关闭;
netty解决粘包/拆包问题
发生拆包和粘包的原因主要是服务端只负责接受消息, 但并不知道客户端到底发送了什么, 以及发送的数据逻辑, 只是根据接收到的字节数据进行解码, 而服务端每次读取的数据量是不确定的, 因此会出现拆包和粘包;

通常的解决思路

  • 发送定长200消息的包, 不够的使用空格补齐;
  • 消息末尾使用空格换行符标记;
  • 将一个消息分为消息头和消息体, 消息头中定义消息体的长度;

netty的解码器

  1. LineBasedFrameDecordor: 使用空格换行符(/r/n)对消息进行解码;
  2. DelimitorBaseFrameDecordor: 以分割符做为消息结束解码;
  3. FixedLengthBaseFrameDecordor: 定长解码器;

使用的方式就是将解码器加入到pipeline的末尾;

编解码技术
java原有序列化的缺点
  1. 不能跨语言
  2. 编解码的码流大, 占用空间多
  3. 效率低
业界主流的序列化技术
  1. 谷歌的protobuf: 体量小, 解析快;
  2. Facebook的Thrift
  3. MessagePack
netty的channel的执行顺序
inboundHandler是用来接收请求的
outboundHandler是用来发送请求的
1、InboundHandler是通过fire事件决定是否要执行下一个InboundHandler,如果哪个InboundHandler没有调用fire事件,那么往后的Pipeline就断掉了。  
2、InboundHandler是按照Pipleline的加载顺序,顺序执行。  
3、OutboundHandler是按照Pipeline的加载顺序,逆序执行。  
4、有效的InboundHandler是指通过fire事件能触达到的最后一个InboundHander。  
5、如果想让所有的OutboundHandler都能被执行到,那么必须把OutboundHandler放在最后一个有效的InboundHandler之前。  
6、推荐的做法是通过addFirst加载所有OutboundHandler,再通过addLast加载所有InboundHandler。  
7、OutboundHandler是通过write方法实现Pipeline的串联的。  
8、如果OutboundHandler在Pipeline的处理链上,其中一个OutboundHandler没有调用write方法,最终消息将不会发送出去。  
9、ctx.writeAndFlush是从当前ChannelHandler开始,逆序向前执行OutboundHandler。  
10、ctx.writeAndFlush所在ChannelHandler后面的OutboundHandler将不会被执行。
netty服务端创建流程

image.png

netty客户端创建流程

image.png

netty的pipeline和channelHandler
  • pipeline更像是一个容器, 里面维护了由channelHandler组成的链表;
  • channelHandler分为in和out两种, in是用来接收请求的, out开头的是用来发送请求的;
  • 通常情况下我们只需要继承ChannelHandlerAdapter接口即可, 也只需要关注其中的几个方法
他们的执行顺序
handlerAdded
channelRegistered 
channelActive #连接完成时, 客户端可以用来发送数据
read 
channelRead #读取数据, 用来读取数据
channelReadComplete #读取完成时, 调用flush方法将缓存中数据刷进socket中
read 
channelReadComplete 
read 
exceptionCaught #发生异常时
channelInactive 
handlerRemoved

* 使用firexxx方法触发下一个channel执行读事件
* 使用write方法返回给客户端
netty架构高性能的原因
  1. 使用java NIO非阻塞模式
  2. reactor线程模型的使用, 单线程模式, 多线程模式和主从模式, netty实现了者三种情况;
  3. 无锁化的串行机制
  4. 使用高性能序列化框架
  5. 零拷贝
* Netty提供了CompositeByteBuf(复合缓冲区),可以将多个ByteBuf合并成一个逻辑上的ByteBuf,避免各个ByteBuf间的拷贝;
* 通过wrap操作,可以将byte[]数组,ByteBuf,ByteBuffer等包装成一个Netty ByteBuf对象,进而避免拷贝操作;
* 直接使用堆外内存;
* 通过FileRegion包装的FileChannel的transferTo实现文件传输,直接将文件缓冲区的数据发送到目标channel,避免传统通过循环write方式导致的内存拷贝问题。
关于零拷贝
传统的拷贝
image.png
零拷贝原理
image.png
netty实现的零拷贝
image.png

红番茄
7 声望2 粉丝

« 上一篇
MongoDB笔记
下一篇 »
Nginx笔记