Netty学习-01

156 阅读29分钟

网络协议

TCP

  • TCP提供面向连接、可靠、有序、字节流传输服务。应用程序要使用TCP,必须使用TCP

三次挥手

  1. client 发送数据给 server
  2. server 发送消息给client端确认连接
  3. client 收到 server的消息后,建立连接,发送消息给server,server建立连接

握手机制

UDP

UDP 提供无连接、不可靠、数据报尽力传输服务

IO模型

IO多路复用有三种实现方式,即select、epoll和poll

liunx基础概念

用户空间和内核空间

  • 内核空间可以访问操作系统的内存空间以及底层硬件设备的权限

进程切换

  • 为了控制进程的执行,内核空间可以挂起某个正在运行的进程并切换和恢复之前已挂起进程的执行,这个过程称为进程切换

  • 进程切换需要执行以下过程

    1. 保存处理机上下文,包括程序计数器和其他寄存器
    2. 更新PCB信息
    3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
    4. 选择另一个进程执行,并更新其PCB
    5. 更新内存管理的数据结构
    6. 恢复处理机上下文

    注:总而言之进程间的切换就是很耗费系统资源

文件描述符

  • 在linux和unix中,一切皆是文件,包括普通文件、链接文件、socket以及设备驱动等。在创建一个文件或编辑文件时,都会创建响应的文件描述符,linux通过文件描述符快速定位到响应的文件。

缓存IO

  • 应用程序读取数据时,首先会在内核态将物理机中的数据拷贝到操作系统内核的缓存区,然后切换到用户态将数据拷贝到应用程序的地址空间中

IO模型

BIO

主要特点

  • 同步堵塞型,一个线程对应一个请求,在一个线程中接收到请求后,则会一直堵塞直到有IO读写请求

单线程示例


public class SingleThreadBioTest {
  
  public static void main(String[] args) {
    ServerSocket server = new ServerSocket(9999);
    
    while (true) {
    Socket socket = server.accept();
    
    InputStream inputStream = socket.getInputStream();
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, CharsetUtil.UTF_8));
    String msg;
      
      while((msg = reader.readLine()) != null) {
        System.out.println(msg);
      }
    }
  }
  
}

多线程示例

package com.demo.iomodel;

import io.netty.util.CharsetUtil;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Bio测试
 *
 * @author tianluhua
 * @Classname BioTest
 * @Date 2021/7/3 09:17
 * @Created by tianluhua
 */
public class BioTest {

  public static void main(String[] args) {



    try {
      ServerSocket serverSocket = new ServerSocket(9999);

      ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

      try {
        
        while (true) {

          Socket socket = serverSocket.accept();

          executor.execute(() -> {

            try {
              System.out.println("创建新的连接:" + socket.getLocalAddress().toString());

              InputStream inputStream = socket.getInputStream();
              BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, CharsetUtil.UTF_8));

              String message;

              while ((message = reader.readLine()) != null) {
                System.out.println(message);
              }
            } catch (Exception e) {
              e.printStackTrace();
            }

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

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

NIO

  • 一个线程对应一个请求,但是服务端会将客户端的连接请求注册到多路复用器上,多路复用器轮询到连接有IO请求时,才会启动一个线程进行IO读写操作

 多路复用

  • 多路复用是指当进程制定的一个或多个IO条件准备读取时,就通知该进程。与多线程或多进程相比,系统开销小,不必创建和维护线程和进程,也没有响应的上下文切换问题
  • 实现多路复用主要有三种方式,即select、poll和epoll。其中,select和poll是采用轮询的形式,遍历所有文件描述符,看是否准备就绪;而epoll则是基于事件驱动,如果文件描述符准备就绪,则使用回调函数执行响应的操作。
select/poll
  • select实现多路复用的方式为
    1. 用户态将已经建立连接的Socket都放到同一个文件描述符集合中,在用户空间调用select函数将文件描述符拷贝到内核空间
    2. 内核遍历文件描述符集合,检查每个socket的状态,是否有网络事件,将socket标记为可读或可写状态,然后将*文件描述符集合拷贝到用户空间*,
    3. 用户空间遍历文件描述符集合,找到可读或可写的socket,然后进行响应的处理
  • 可以看到select经过了两次拷贝和两次遍历过程
  • select采用bitsmap来表示文件描述符集合。由于FD_SETSIZE限制,默认最大值为1024,即监听文件描述符的长度为1024
  • poll采用动态数组,以链表形式保存文件描述符,突破了FD_SETSIZE的限制
epoll
  • 采用事件通知方式,当有IO事件就绪时,系统注册的回调函数就会被调用,避免了很多无IO事件的空循环
epoll_create
epoll_ctl

NIO三大模块

  • NIO中有三个主要的模块,即Buffer、Channel和Selector
  • 每个Channel对应着一个Buffer,Channel读写数据必须通过Buffer进行
  • 一个Selector对应多个Channel和线程,Selector通过轮询快速切换不同的线程进行执行
Buffer
  • Buffer本质上是一个可以读写数据的内存块,可以理解为一个容器对象,底层使用数组来实现。java中七种基本类型都有对应的Buffer,除了布尔类型
  1. buffer的机制及字类
  2. buffer类定义了所有的缓存区,都据有四个属性来提供关于其所包含的数据元素
    • capacity Buffer容器最大容量
    • limit Buffer容器中存储的做大长度,可进行重新设置
    • position Buffer当前读取的位置,读取和写入时,都会改变改值,因此在写入数据时,需要使用flip进行复位,可进行重新设置
    • mark 标记
  3. 主要方法
    1. flip() 复位
    2. get() 和 put() 会使position+1
    3. get(int index)和put(int index, Object value) 不会使数据+1
    4. hasRemaining() 缓存中是否还有数据
    5. isReadOnly() 是否可读
    6. position(int length)
    7. limit(int length)
  4. MapperByteBuffer
    1. 可以让文件直接在内存(堆外内存)中进行修改,操作系统不需要拷贝一次
Channel
  1. 基本介绍
    • Channel可以同时读写,是双向的;流只能读或者写
    • 可以实现异步读写数据
  2. 主要的类
    1. ServerSocketChannel(类似于ServerSocket)
    2. SocketChannel(类似于Socket)
    3. FileChannel
      1. public int read(ByteBuffer bbf) 从通道中读取数据并放到缓存区中
      2. public int write(ByteBuffer bbf) 把缓存区的数据写入到Channel中
      3. public long transferfrom(ReadableByteBuffer src,long postion, long count) 从目标通道中复制到当前通道
      4. public long transferTo(long position, long count, WriteableBuffer target) 把数据从当前通道写入到目标通道
Selector
  1. Selector能够检测多个注册的通道上是否有事件发生
  2. selector.select(); //阻塞方法,返回有事件操作的通道的key
  3. selector.select(1000); // 阻塞1000毫秒再返回
  4. selector.selectNow(); //不阻塞,立刻返回
  5. selector.selectKeys(); //返回所有的通道的key
  6. selector.wakeup(); //唤醒selector
server端过程
  1. 创建一个serverSocketChannel并设置为非堵塞,使用configureBlocking()方法
  2. 监听端口并注册到selector中,设置模式为accept,并返回注册到selector中的selectionKey
  3. 遍历key值,并判断是否有通道连接或者发送数据,其中,发送数据通过isReadable来实现
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

Selector selector = Selector.open();

serverSocketChannel.socket().bind(new InetSocketAddress(8081));

serverSocketChannel.configureBlocking(false);

SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    if (selector.select(1000) == 0) {
        System.out.println("服务器等待一秒,无连接。。。。。。");
        continue;
    }
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        if (key.isAcceptable()) {
            // 这里依然会堵塞,但是这个已经判断了已经有连接请求了,这个地方很快就会进行连接
            SocketChannel socketChannel = serverSocketChannel.accept();
            System.out.println("客户端连接成功,生成了一个socketChannel");
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
        }
        if (key.isReadable()) {
            // 得到与之关联的共享数据
            ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
            byteBuffer.flip();
            SocketChannel channel = (SocketChannel) key.channel();
            channel.read(byteBuffer);
            System.out.println("读取到的数据为" + new String(byteBuffer.array()));
        }
        iterator.remove();
    }
}
SocketChannel socketChannel = SocketChannel.open();
// 设置非阻塞
socketChannel.configureBlocking(false);
// 提供服务器端的ip
InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8081);
if (!socketChannel.connect(inetSocketAddress)) {
    while (!socketChannel.finishConnect()) {
        System.out.println("因为连接需要时间,因此可以做其他工作");
    }
}
ByteBuffer byteBuffer = ByteBuffer.wrap("发送数据".getBytes());

socketChannel.write(byteBuffer);
System.in.read();

零拷贝(指的是没有CPU拷贝,并不是不拷贝)

  • DMA(Direct Memory Access)技术:在进行I/O设备和内存数据进行传输时,将数据传输的工作交给DMA控制器,而CPU不再参与数据传输,可以去执行其他的任务。
传统IO进行数据传输的过程
File file = new File("index.html");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
  • 第一步:线程在用户空间发起read()请求读取文件,线程从用户态切换到内核态
  • 第二步:DMA将磁盘数据拷贝到内核缓存中,CPU又将数据从内核缓存拷贝到用户缓存中,线程从内核态切换到用户态
  • 第三步:这时候知道了数据应该往哪里写,调用write方法,CPU将数据从用户缓存拷贝至socket缓存,线程又从用户态切换到内核态
  • 第四步:通过DMA控制器将Socket中的数据拷贝到网卡中,write()调用结束,线程从内核态切换到用户态

img.png

20191211205247582.png

传统IO模式的优化
mmap优化
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
  1. mmap通过内存映射,将文件映射到内存缓存区,同时,用户空间可以共享内存空间的数据,这样就减少了内核空间到用户空间的拷贝。此时,会产生3次拷贝,4次切换。和传统IO相比,减少一次拷贝
    • 第一步:线程在用户空间发起read()请求读取数据,用户态切换到内核态
    • 第二步:DMA将磁盘数据拷贝到内核缓存中,内核态切换到用户态
    • 第三步:由于使用mmap,CPU将内核缓存直接拷贝到Socket缓存中,用户态切换到内核态
    • 第四步:DMA将socket缓存拷贝到网卡中,read()调用结束,内核态切换到用户态

202111232147163.png

sendFile优化
  1. 数据不需要经过用户态,直接从内核缓存区进入SocketBuffer中,同时,由于与用户态无关,从而减少了一次上下文切换。此时会产生3次拷贝,2次切换
    • 第一步:线程在用户空间发起read()请求读取数据,用户态切换到内核态
    • 第二步:DMA将磁盘数据拷贝到内核缓存中,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里。此过程不需要进行切,read()调用结束,内核态切换到用户态

202111232147012.png

Netty介绍

  • netty是JBOSS开发的基于事件驱动的、异步的网络应用框架
    • 异步表示发起请求后,不需要等待执行完成后,才能执行下面的语句

高性能的原因

  • Netty采用NIO(同步非阻塞)线程模型,采用较少的线程资源处理更多的事情
  • 内存零拷贝:尽量避免使用不必要的内存拷贝,实现高效率的传输
  • 内存池设计
  • 串行化处理读写
  • 高性能序列化协议,采用protobuf等高性能协议

传输方式

  • IO模型在很大程度上决定了框架的性能。相对于BIO,netty采用了异步通信模型,通过一个线程,采用多路复用的形式处理IO连接和读写操作,这从根本上解决了传统堵塞IO的连接-线程模型

BIO

  1. 同步阻塞,实现模式为一个连接一个线程,即当有客户端连接时,服务器端需为其单独分配一个线程,如果该连接不做任何操作就会造成不必要的线程开销
  2. 当然我们也可以在采用线程池来处理连接和读写请求,实现同时进行多个连接,但是这种方式,当连接的客户端没有发送数据时,服务器端会阻塞在read操作上,等待客户端输入,造成线程资源浪费

NIO

  1. 从JDK1.4开始,java提供了一系列改进输入/输出的新特性,统称为NIO

  2. 同步非阻塞,服务器实现模式一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程处理。用户进程也需要时不时地询问IO操作是否就绪,这就要求用户进程不停的去询问。

支持协议

  • netty默认提供了google protobuf的支持,也可以通过扩展Netty编码接口,实现自己的高性能序列化框架

线程模型

  • netty默认采用主从Reactor线程模型,Reactor模型共有三种不同线程模型,即
    1. Reactor单线程模型
      • 一个线程接收客户端的连接,并将请求分发到对应的事件处理handler上,整个过程是在一个线程中进行操作的
    2. Reactor多线程模型
      • 一个线程处理客户端的连接,另一个线程池处理连接后的IO读写操作
    3. 主从Reactor多线程模型
      • 创建两个线程池,一般命名为boss和work,一个线程池负责处理客户端的连接请求,一个线程池负责处理客户端的IO读写操作

零拷贝技术

  1. Netty在接收和发送buffer数据时,使用直接内存进行Socket读写操作,不需要进行字节缓存区的二次拷贝。如果使用堆内存进行Socket读写,则需要将堆内存拷贝到直接内存,然后再进行Socket读写操作。
  2. Netty的文件传输调用FileRegion包装的transferTo方法,可以直接将缓存区的数据发送到对应的channel中。
  3. Netty提供了CompositeByteBuf类,可以将多个ByteBuf合并为一个ByteBuf,避免多个ByteBuf的相互拷贝。
  4. 通过wrap操作, 我们可以将byte[]数组、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象, 进而避免拷贝操作。
  5. ByteBuf支持slice操作,可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf, 避免内存的拷贝。

Reactor线程模式

  • Reactor模式是事件驱动模型,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。
  • 即通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
  • 服务器端处理传入的多个请求并将他们同步分发到响应的线程进行处理
  • Reactor在一个线程中运行,负责监听和分发事件
  • Handler负责处理具体的事件

单Reactor单线程

  • 在一个线程中处理连接、监听接收数据,发送数据

优点

  1. 模型简单
  2. 没有多线程、进程通信,竞争问题

缺点

  1. 性能问题,一个线程无法发挥CPU多核性能
  2. 如果一个处理事件出现问题,则会一直堵塞

单Reactor多线程

  1. Reactor通过selector监听客户端请求,根据请求类型分发给不同的handler
  2. Reactor在handler中不对消息处理,仅仅接收消息,将消息的处理放到其他线程中

优点

  1. 可以充分利用CPU多核能力处理问题

缺点

  1. 多线程处理事件,存在线程共享和访问的问题
  2. Reactor处理事件监听和响应在单线程中进行,容易出现性能瓶颈

主从Reactor多线程

  1. 一个Reactor主线程可以对应多个reactor子线程
  2. Reactor主线程通过selector监听连接事件,收到事件后,在主线中处理连接事件。将连接分配给subReactor
  3. subReactor将连接加入连接队列并监听,创建handler对各个事件进行处理
  4. 当有新事件发生时,subReactor调用对应的handler处理
  5. handler通过read读取数据后,分发给work线程进行处理并返回结果
  6. handler收到响应结果后,将结果返回给client

优点

  1. 父线程和子线程数据交互明确,父线程负责连接,子线程负责处理其他事件处理

Netty模型

  1. Netty抽象出两组线程池,BossGroup负责接收客户端的连接,WorkGroup负责网络的读写
  2. BossGroup和WorkGroup都是NioEventLoopGroup
  3. NioEventLoopGroup是一个循环组,这个组中包含多个事件循环,每个事件循环为NioEventLoop
  4. NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个Selctor,用于监听绑定在其上面的网络通讯
  5. 每个BossEventLoop处理步骤为
    1. 轮询accept事件
    2. 处理accept事件,和client建立连接,生成一个NioSocketChannel,并将channel注册到workEventLoop的selector中,以便于workEventLoop监听网络读写
    3. 处理任务队列中的任务
  6. 每个WorkEventLoop处理步骤
    1. 轮询Read、Write事件
    2. 处理Read、Write事件,在对应的NioSocketChannel中进行处理
    3. 处理任务队列中的任务
  7. pipeline中包含channel,pipeline总包含了很多的处理器

任务队列

  • 如果在服务器端的handler中接收数据后,进行的操作耗时比较长,则会一直堵塞在这个地方,这样可以尝试使用任务队列的形式

  • 用户队列中的Task有3种典型使用场景

    1. 用户程序自定义的普通任务
    2. 用户自定义的定时任务
    3. 非当前Reactor线程调用Channel的各种方法
      • 例如:在推送系统的业务线程种,根据根据用户的标识,找到对应的Channel引用,然后调用Write类方法向该用户提供推送消息服务。最终的Write会被提交到任务队列中被异步消费。

用户程序自定义的普通任务

  • 这里需要注意的是taskQueue中提交多个任务,这些任务是在同一个线程中进行的,因此,这几个任务执行会堵塞,即只有上一个完成了,才会执行下面的任务

  • 首先根据ChannelHandlerContext找到Channel中的eventLoop,通过EventLoop提交任务,即

    ctx.channel().eventLoop().execute(new Runnable() {
       // TODO处理逻辑
    });
    
package com.knxhd.netty.handler.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;

import java.nio.ByteBuffer;

/**
 * 1. 自定义一个handler,需要继承netty规定好的handlerAdapter
 * creator tianluhua
 * created 2021-05-04 19:28
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 读取数据的handler
     *
     * @param ctx 上下文对象,包含有pipeline(管道),Channel(通道),连接的地址
     * @param msg 客户端发送的数据,以对象的形式
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 这里有一个非常耗时的任务
        // 异步执行->提交该channel 对应的NIOEventLoop的taskQueue中
        ctx.channel().eventLoop().execute(() -> {
            try {
                Thread.sleep(10000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 客户端秒ヾ(≧▽≦*)o,喵2", CharsetUtil.UTF_8));
            } catch (Exception e) {
                System.out.println("发生异常" + e.getMessage());
            }
        });

        ctx.channel().eventLoop().execute(() -> {
            try {
                Thread.sleep(20000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 客户端秒ヾ(≧▽≦*)o,喵3", CharsetUtil.UTF_8));
            } catch (Exception e) {
                System.out.println("发生异常" + e.getMessage());
            }
        });
         System.out.println("go on。。。");
    }

    // 数据读取完毕
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        /**
         * writeAndFlush 是write和flush两个方法。作用:将数据写入缓存并刷新
         * 对发送数据进行编码发送
         */
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端。(●ˇ∀ˇ●)喵1", CharsetUtil.UTF_8));
    }

    // 如果出现异常,则关闭通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

用户自定义的定时任务

  • 和普通任务相同,只不过调用的方法不再是exceute而是schedule(),可以定时或者延时执行
  • 任务放到了scheduleTaskQueue中
     ctx.channel().eventLoop().schedule(() -> {
            try {
                Thread.sleep(5000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 客户端秒ヾ(≧▽≦*)o,喵4", CharsetUtil.UTF_8));
            } catch (Exception e) {
                System.out.println("发生异常" + e.getMessage());
            }
        }, 5, TimeUnit.SECONDS);

非当前Reactor线程调用Channel的各种方法

  • 将Channel保存到一个List或者Map中,需要的时候,调用即可

Netty组件

Netty组件有Channel、EventLoop、EventLoopGroup、ChannelHandler、ChannelPieline

  1. 一个EventLoopGroup包含多个EventLoop
  2. 一个EventLoop对应一个Thread
  3. 一个Channel注册到一个EventLoop上

NioEventLoopGroup

  • NioEventLoopGroup是一个线程组,用来处理连接请求、网络读写请求
  • 在Netty模型中,服务器端往往使用两个NioEventLoopGroup来处理请求。其中,bossGroup用于处理连接请求,workGroup用于处理网络读写请求。
  • 默认线程组的大小为服务器核心CPU数*2,也可自己指定
EventLoopGroup bossGroup = new NioEventLoopGroup(8)

NioEventLoop

  1. NioEventLoop采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由IO线程的NioEventLoop负责
    • NioEventLoopGroup下有多个NioEventLoop
    • 一个NioEventLoop包含一个Selector、taskQueue
    • 每个NioEventLoop中的Selector可以监听多个NioChannel
    • 每个NioChannel只绑定到唯一的NioEventLoop中
    • 每个NioChannel都绑定唯一一个ChannelPipeline

Unpooled

  • 此类用来管理和维护ByteBuf
  • ByteBuf不需要使用flip复位buf的位置
    1. 在ByteBuf中有两个字段,readerIndex和writerIndex
    2. 通过readerIndex、writerIndex和capacity分成三段
    3. 0~readerIndex表示已经读的范围
    4. readerIndex~writerIndex表示可读范围
package com.knxhd.netty.buf;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

public class NettyByteBuf01 {
    public static void main(String[] args) {
        // 创建一个ByteBuf
        ByteBuf buf = Unpooled.buffer(10);

        for (int i = 0; i < 10; i++) {
            buf.writeByte(i);
        }

        System.out.println("capacity=" + buf.capacity());

        //输出
        for (int i = 0; i < buf.capacity(); i++) {
            System.out.println(buf.getByte(i));
        }
    }
}

ByteBuf

和NIO的bytebuffer的区别
  1. ByteBuffer长度固定,一旦分配完成,就不能动态收缩和扩展,当需要编码的对象大于容量时,会引发索引越界异常,当编码对象小于容量时,可能会造成内存空间的浪费
  2. ByteBuffer由于只有一个标识位置的索引,因此有的时候需要通过flip和rewnd等进行调整,比较繁琐,稍有疏忽,有可能出现错误
  3. ByteBuffer的功能有限,一些高级和实用特性不支持,需要自己编码实现。
优点
  1. 可以被扩展
  2. 通过内置的复合缓存区类型实现了透明的零拷贝
  3. 容量可以按需增长
  4. 读写模式不需要调用flip方法
  5. 读写使用不同的索引
  6. 支持池化
  7. 支持引用计数
  8. 方法支持链式调用
ByteBuf工作原理
  • ByteBuf维护了两个索引,一个读索引(readIndex)、一个写索引(writeIndex),当进行读操作时,读索引增加1;当进行写操作时,写索引增加1,当读索引和写索引值相同时,在进行读操作时,会触发索引超出范围的异常
Bytebuf 分类

从内存分配角度来看,ByteBuf可以分成两类

  • 堆内存(HeapByteBuf)字节缓存区 - 优点:内存的分配和回收较快,可以被JVM回收 - 缺点:通过socket进行IO读写时的速度较慢。因为需要进行额外的一次复制,即从缓存区中复制数据到内核态的channel中
  • 直接内存(DirectByteBuf)字节缓存区
  • 缺点:直接内存在非堆内存中分配空间,因此,空间的分配和回收相对较慢
  • 优点:是通过socket进行IO读写时的速度相对较快
  • 正是由于有利也有弊,因此,一般在socket的IO读写逻辑中,使用直接内存;在后端业务消息的编码模块使用堆内存字节缓存区。

从内存回收角度来看,ByteBuf对象分为两类,基于对象池的ByteBuf和普通的ByteBuf

两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。测试表明使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。尽管推荐使用基于内存池的ByteBuf,但是内存池的管理和维护更加复杂,使用起来也需要更加谨慎,因此,Netty提供了灵活的策略供使用者来做选择。

索引变化图
  1. 初始分配
|       writable bytes          |
+-------------------------------+
|                               |
0=readerIndex=writerIndex       capacity
  1. 写入N个字符
+------------------+-------------------+
|  readable bytes  |    writable bytes |
+------------------+-------------------+
|                  |                   |
0=readerIndex      N=writerIndex       capacity
  1. 读取M(N>M)个字符后
+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0               M=readerIndex    N=writerIndex       capacity
  1. 调用discardReadBytes操作之后
+------------------+----------------------+
|  readable bytes  |    writable bytes    |
+------------------+----------------------+
|                  |                      |
0=readerIndex   N-M=writerIndex         capacity
  1. 调用clear操作后
+-------------------------------+
|       writable bytes          |
+-------------------------------+
|                               |
0=readerIndex=writerIndex       capacity
ByteBuf使用模式
堆缓存区
ByteBuf heapByteBuf = Unpooled.buffer(10);
直接内存
ByteBuf directByteBuf = Unpooled.directBuffer(10)
池化堆内存
PooledByteBufAllocator poolHeapByteBuf = new PooledByteBufAllocator(false);
ByteBuf buffer = allocator.buffer(10);
池直接内存
// 池化直接内存
PooledByteBufAllocator poolDirectByteBuf = new PooledByteBufAllocator(true);
ByteBuf pDirectByteBuf = allocator.buffer();
复合型内存
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(heapByteBuf, directByteBuf, poolHeapByteBuf, poolDirectByteBuf)

netty使用

Http服务器

  • 实现http服务器简单使用,可以通过浏览器访问到该请求
public class HttpServer {

  public static void main(String[] args) {

    NioEventLoopGroup bossGroup = new NioEventLoopGroup();
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();

    try {
      ServerBootstrap bootstrap = new ServerBootstrap();

      bootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(new ServerInitializer());

      ChannelFuture future = bootstrap.bind(8083).sync();
      future.channel().closeFuture().sync();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      bossGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }

  }

}
public class ServerInitializer extends ChannelInitializer<SocketChannel> {


  @Override
  protected void initChannel(SocketChannel ch) {
    ChannelPipeline pipeline = ch.pipeline();

    // netty 提供的http编解码
    pipeline.addLast("httpServerCodec", new HttpServerCodec());
    // 自定义处理器
    pipeline.addLast("httpServerHandler", new HttpServerHandler());
  }

}
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

  public static final String TITLE_ICO = "/favicon.ico";

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
    if (msg instanceof HttpRequest) {

      HttpRequest httpRequest = (HttpRequest) msg;
      String uri = httpRequest.uri();
      if (TITLE_ICO.equals(uri)) {
        System.out.println("请求/favicon.ico");
        return;
      }
      ByteBuf byteBuf = Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8);
      DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);

      response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
      response.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());

      ctx.writeAndFlush(response);
    }
  }
}

心跳机制

使用netty自带的心跳检测处理器来处理心跳检测,即

new IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit timeUnit)
  1. readerIdleTime:如果在规定时间内没有进行读操作,则发送心跳检测包,触发IdleStateEvent事件
  2. writerIdleTime:如果在规定时间内么有进行写操作,则发送心跳检测包,触发IdleStateEvent事件
  3. allIdleTime:如果在规定范围内既没有发生读操作也没有发生写操作,则发送心跳检测包,触发IdleStateEvent事件
  4. timeUnit:时间单位

在发送了心跳检测包,触发IdleStateEvent事件后,将事件传递到管道中的下一handler中,通过handler的userEventTiggered方法中进行处理可以定义重连几次后,关闭通道。

ServerBootstrappackage com.knxhd.netty.heartbeat.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class HeartbeatServer {

    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            serverBootstrap.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    /*
                     * 在bossGroup中增加日志处理器,会把boss相关的日志输出
                     * 使用netty只带的日志处理器
                     */
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            /*
                             * 加入netty提供的空闲状态处理器 IdleStateHandler
                             * 参数1:long readerIdleTime: 表示多长时间没有读,就会发送一个心跳检测包检测是否连接
                             * 参数2:long writerIdleTime: 表示多长时间没有写,就发送一个心跳检测包检测是否连接
                             * 参数3:long allIdleTime: 表示多长时间既没有读也没有写,就发送一个心跳检测包检测是否连接
                             * 在发送了心跳检测包之后,会触发一个心跳状态事件(IdleSateEvent),
                             * 之后会传递到管道中的下一个handler处理(通过调用下一个handler的 userEventTiggered,在该方法中进行处理)
                             *
                             */
                            pipeline.addLast(new IdleStateHandler(3, 5, 7, TimeUnit.SECONDS));
                            /**
                             * 加入一个对空闲检测进一步处理的handler(自定义)
                             */
                            pipeline.addLast(new HeartbeatHandler());
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(9996).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

}

public class HeartbeatHandler extends SimpleChannelInboundHandler<Object> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

    }

    /**
     * @param ctx 上下文
     * @param evt 事件
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        /**
         * 判断是否是心跳状态事件
         */
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;

            String eventType = null;
            switch (event.state()) {
                case READER_IDLE:
                    eventType = "读空闲";
                    break;
                case WRITER_IDLE:
                    eventType = "写空闲";
                    break;
                case ALL_IDLE:
                    eventType = "全部空闲";
                    break;
            }
            System.out.println(ctx.channel().remoteAddress() + "--超时时间--" + eventType);
            // 做相应的处理
            /**
             * 如果发生空闲,直接关闭通道
             */
            //ctx.channel().close();
        }
    }


}

WebSocket长连接

什么是WebSocket

  • WebSocket是应用层协议,依赖于Http协议一次握手,建立握手之后,数据从TCP通道中出传输,与Http无关。即:WebSocket分为握手阶段和数据传输阶段,即进行了Http握手和双工的TCP连接

与http的区别

  1. 基于http协议建立长连接

    • 首先客户端向服务器端发送一个http请求,其消息头为

      GET /chat HTTP/1.1  // 请求行
      Host: server.example.com
      Upgrade: websocket  // required
      Connection: Upgrade // required
      Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // required,一个 16bits 编码得到的 base64 串
      Origin: http://example.com  // 用于防止未认证的跨域脚本使用浏览器 websocket api 与服务端进行通信
      Sec-WebSocket-Protocol: chat, superchat  // optional, 子协议协商字段
      Sec-WebSocket-Version: 13
      
    • 如果服务支持该版本的websocket,则返回101响应

      HTTP/1.1 101 Switching Protocols  // 状态行
      Upgrade: websocket   // required
      Connection: Upgrade  // required
      Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // required,加密后的 Sec-WebSocket-Key
      Sec-WebSocket-Protocol: chat // 表明选择的子协议
      
    • 握手完成后,接下来的 TCP 数据包就都是 WebSocket 协议的帧了

    • 可以看到,这里的握手不是 TCP 的握手,而是在 TCP 连接内部,从 HTTP/1.1 upgrade 到 WebSocket 的握手。

    • WebSocket 提供两种协议:不加密的 ws:// 和 加密的 wss://. 因为是用 HTTP 握手,它和 HTTP 使用同样的端口:ws 是 80(HTTP),wss 是 443(HTTPS)

  • 双工通信模式,即客户端和服务器端都可以主动推送消息到另外一端
  1. http 1.0 默认建立短连接,可通过keep-alive参数来建立长连接
  2. http 1.1 默认建立长连接,
  3. http 2.0 允许服务器端主动推送数据到客户端,但是这种推送对于web app是无法感知的,也就是不能通过监听的形式来获取服务器端推送的数据。
  4. http在发送数据时,不仅仅需要发送数据本身,还需要发送消息头header的一些信息。webSocket仅仅在建立连接时,会携带header信息,一旦建立连接后,则不会再发送header信息,这节省了传输数据的大小。
  5. webSocket可建立长连接、监听客户端和服务器端的消息。

基本使用

  • 服务器端
public class WebSocketServer {

  public static void main(String[] args) throws Exception {
    NioEventLoopGroup bossGroup = new NioEventLoopGroup();
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();

    try {
      ServerBootstrap server = new ServerBootstrap();

      server.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new WebSocketServerInitializer());

      ChannelFuture future = server.bind(8080).sync();
      future.channel().closeFuture().sync();
    } finally {
      bossGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
  }

}
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
  @Override
  protected void initChannel(SocketChannel ch) {
    ChannelPipeline pipeline = ch.pipeline();

    // http 编解码器
    pipeline.addLast(new HttpServerCodec());
    // 以块的方式写的处理器
    pipeline.addLast(new ChunkedWriteHandler());
    // 聚合处理器
    pipeline.addLast(new HttpObjectAggregator(8192));
    // 对ws协议的支持,参数代表url,websocket的URL为ws://ip:host/ws
    pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
    pipeline.addLast(new WebSocketServerHandler());

  }
}
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
    System.out.println(msg.text());
    // Send the uppercase string back.
    ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间:" + LocalDateTime.now()));
  }


  @Override
  public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    Channel channel = ctx.channel();
    channel.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + channel.remoteAddress() + " 加入"));
    System.out.println("客户端连接:" + ctx.channel().id().asLongText());
  }

  @Override
  public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    System.out.println("客户端断开连接:" + ctx.channel().id().asLongText());
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    cause.printStackTrace();
    ctx.channel().closeFuture().sync();
  }
}
  • 客户端
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>WebSocket连接测试</title>
  </head>
  <body>
    <form onsubmit="return false;">
      <div>
        <h3>客户端发送数据</h3>
        <label>
          <textarea id="requestText" name="message" style="width: 400px;height: 200px"></textarea>
        </label>
        <input type="button" value="发送数据" id="sendMessage" onclick="send()"/>
      </div>

      <div>
        <h3>服务器响应</h3>
        <label for="responseArea">
          <textarea id="responseArea" style="width: 400px; height: 200px;"></textarea>
        </label>
        <input onclick="document.getElementById('responseArea').value = ''" type="button" value="清空内容">
      </div>
    </form>
    <script type="text/javascript">

      var webSocket;
      let message = document.getElementById("responseArea");
      if (window.WebSocket) {
        webSocket = new WebSocket("ws://127.0.0.1:8080/ws");

        webSocket.onmessage = function (msg) {
          message.value = message.value + "\n" + msg.data;
        }

        webSocket.onerror = function (error) {
          console.log(error)
        }

        webSocket.onclose = function () {
          message.value = message.value + "\n" + "连接关闭";
        }

        webSocket.onopen = function () {
          message.value = "连接成功";
        }

        let element = document.getElementById("requestText");

        function send() {
          let value = element.value;
          webSocket.send(value)
        }


      } else {
        alert("浏览器不支持WebSocket");
      }

    </script>

  </body>
</html>

粘包拆包

  • 编写网络应用程序时,由于数据在网络中传输,都是通过二进制字节码,在发送时,需要对数据进行编码,在接受数据时,需要进行解码。

  • codec(编码器)的组成部分为两个,即decoder(解码器)和encoder(编码器),decoder负责将业务数据转换为二进制字节码数据,decoder负责将字节码数据转换为业务数据。

  • java自身提供了一些编码和解码器

    1. StringDecoder和StringEncoder
      • 对字符串操作的解码编码器
    2. ObjectDecoder和ObjectEncoder
      • 对对象的解码编码器

    • Netty自带的ObjectEcoder和ObjectDecoder可以用来对POJO进行解码和编码,但是底层依然使用的是java序列化技术而这个效率相对不高。会产生一些问题
    • 无法跨语言
    • 序列化之后,性能较低

LineBasedFrameDecoder

  • 回车编码器,配合StringDecoder进行使用

FixedLengthFrameDecoder

  • 固定长度编码器

LengthFieldBasedFrameDecoder

  • 不能超过1024个字节不然会报错
  • 基于'长度'解码器(私有协议最常用)

DelimiterBasedFrameDecoder

  • 分隔符编码器

解码器

ByteToMessageDecoder

  • 自解析

LengthFieldPrepender

  • 长度解码器

序列化

Protobuf

  • Protobuf是一种轻便高效的结构化数据存储结构,可以用于结构化数据串行化,适合数据存储和Rpc的数据交换格式

  • Protobuf跨语言,可以在多个语言中使用

  • Protobuf传输数据会对数据进行压缩,以此来减少传输的数据流量

基本类型

javaproto类型备注
floatfloat
intint32使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint32。
longint64使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint64。
intunit32总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。
doubledouble
longunit64总是8个字节。如果数值比总是比256大的话,这个类型会比int64高效。
intsint32使用可变长编码方式。有符号的整型值。编码时比通常的int32高效。
longsint64使用可变长编码方式。有符号的整型值。编码时比通常的int64高效。
ing[1]fixed32
long[1]fixed64总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效
intsfixed32总是4个字节。
longsfixed64总是8个字节。
booleanbool
Stringstring一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。
ByteStringbytes可能包含任意顺序的字节数据

特殊字段

英文中文备注
enum枚举(数字从零开始)作用是为字段指定某”预定义值序列”enum Type {MAN = 0;WOMAN = 1; OTHER= 3;}
message消息体message User{}
repeated数组/集合repeated User users = 1
import导入定义import "protos/other_protos.proto"
//注释//用于注释
extend扩展extend User {}
package包名相当于命名空间,用来防止不同消息类型的明明冲突

proto文件

syntax = "proto2";

package Person;

option optimize_for = SPEED;
option java_package = 'com.knxhd.netty.protobuf.dto';
option java_outer_classname = 'Student';

message Person {
  required int64 id = 1;
  required string name = 2;
  optional string address = 3;
}


  • syntax说明使用的版本
  • package为包名
  • java_package为生成java的包名
  • java_outer_classname为生成的文件名,这里需要注意的是文件中的Person和Sutdent不能相同

编写完成后,使用protoc命令可以生成响应的文件,具体命令为:

protoc --java_out=main/java  main/java/com/knxhd/protobuf/Person.proto
package com.knxhd.netty.protobuf;


import com.google.protobuf.InvalidProtocolBufferException;
import com.knxhd.netty.protobuf.dto.PersonInfo;

/**
 * @author tianluhua
 * @Classname ProtobufTest
 * @Date 2021/11/23 22:33
 */
public class ProtobufTest {
  public static void main(String[] args) throws InvalidProtocolBufferException {
    PersonInfo.Person.Builder builder = PersonInfo.Person.newBuilder()
                                                  .setName("111")
                           												.setId(1123L)
      																						.setAddress("232");
    
    PersonInfo.Person person = builder.build();

    // 转换成字节数据
    byte[] personBytes = person.toByteArray();

    PersonInfo.Person parseFrom = PersonInfo.Person.parseFrom(personBytes);

    System.out.println(parseFrom);
  }
}

  • 通过toByteArray可以进行序列化,如果是跨平台的,使用其他语言收到序列化数据后,可通过其他语言生成的文件中的反序列化方法,反序列化为其对象

基本使用

编码部分
  1. 引入依赖

    • 这里使用的版本是4.0.0-rc-2
     <dependency>
       <groupId>com.google.protobuf</groupId>
       <artifactId>protobuf-java</artifactId>
       <version>${version}</version>
     </dependency>
    
  2. 编写proto文件,即新建Student.proto文件

    syntax = "proto3";
    
    // 指定版本
    
    option java_outer_classname = "StudentPOJO";
    
    //生成的java的class名,即生成的java名
    // Protobuf使用 message 管理数据
    message Student {
      int32 id = 1;
      string name = 2;
    }
    
  3. 使用protoc编译proto文件,生成java文件

    // java_out=. 表示java生成的地址,Student.proto表示要编译的文件名
    protoc --java_out=. Student.proto
    
  4. 编写代码构造数据

    StudentPOJO.Student.Builder builder = StudentPOJO.Student.newBuilder();
    builder.setId(4);
    builder.setName("豹子头-林冲");
    StudentPOJO.Student student = builder.build();
    
  5. 在pipeLine中引入编码和解码器

    // 在pipeline中加入编码器 ProtoBufEncoder编码器
    pipeline.addLast("protobufEncoder", new ProtobufEncoder());
    // 添加protobuf解码器,需要指定对哪种类型进行解码
    pipeline.addLast("protobufDecoder", new              ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));