学习笔记 | Netty

学习笔记 | Netty

  • Netty编程的整体步骤

    1. 编写服务器
    2. 在服务器中添加自定义的Initializer对象
    3. 在Initializer对象中添加自定义的handler对象
    4. 在handler里重写netty提供的回调方法
  • Netty网络编程

    1. 编写Server
      • EventLoopGroup:bossGroup,workerGroup
      • ServerBootstrap: serverBootstrap
      • serverBootstrap
        .group(bossGroup,workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(new MyInitializer());
    2. 编写Initializer
      • 继承ChannelInitializer<SocketChannel>
      • 重写initChannel(SocketChannel)方法
        • 获取ChannelPipeLine:ChannelPipeline pipeline = socketChannel.pipeline();
        • 向pipeLine中添加handler
          • HttpServerCodec:处理Http请求的handler
    3. 编写Handler
      • 继承SimpleChannelInbondHandler<HttpObject>
      • 重写channelRead0方法
  • NettySocket编程

    • server端使用ServerBootstrap,client端使用Bootstrap
    • Initialize用到的HandlerLengthFieldBasedFrameDecoder,LengthFieldPrepender,StringDecoder,StringEncoder
  • Netty聊天室Demo

    • Initializer用到的Handler:DelimiterBasedFrameDecoder,StringDecoder,StringEncoder
    • 自定义Handler:
      1. 定义一个静态私有变量ChannelGroup,用来保存所有已连接的client的Channel
        private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
  • Netty心跳检测机制

    • Initializer用到的Handler:IdleStateHandler
    • 自定义的Handler:
      • 继承ChannelInboundHandlerAdapter
      • 重写userEventTrigged
  • websocket为了解决啥问题

    • HTTP的长连接问题
    • HTTP是一种无状态的,基于请求/响应模式的协议
      • 无状态:服务器不会记录请求的信息,
      • 请求/响应模式:客户端发送请求,服务器端返回响应.
    • websocket是基于HTTP协议的,在初次连接时,客户端会在header中携带websocket相关的信息Upgrade:Websocket,请求服务端将连接升级为websocket连接.服务端读到header中信息后,就将当前连接升级为可以支持长连接的websocket连接了.
    • 虽然websocket是基于HTML5规范的,但是也可以用在非浏览器的场合.像Android,iOS
  • 基于websocket的netty示例

    • Initializer中使用到的Handler:ChunkedWriteHandler,HttpObjectAggregator,WebSocketServerProtocolHandler
    • 自定义Handler
      • 继承SimpleChannelInboundHandler<TextWebSocketFrame>
      • 重写channelRead0
  • ProtoBuf相关

    • 用于数据交换的序列结构化数据格式.具有更小的传输体积,更高的编码解码能力.特备适用于数据存储,网络数据传输等对存储体积,实时性要求高的领域.
    • 优点:空间效率高,时间效率高,对数据大小敏感,传输效率高
    • 缺点:消息结构可读性不高
    • 通过.proto生成的Java对象有个toByteArray方法,可以生成对象的字节数组,同时,对象有个parseFrom方法,可以将字节数组转换成当前对象.
  • Netty中使用ProtoBuf

    • 需要在ChannelInitializer的ChannelPipeLine中添加ProtoBuf相关的handler: ProtobufVarint32FrameDecoder,ProtobufDecoder,ProtobufVarint32LengthFieldPrepender,ProtobufEncoder
    • 其中ProtobufDecoder解码器handler中需要传入的参数就是要转换的对象的实例
    • 这样就可以在channel中直接write需要传输的对象,netty会自动把对象使用protobuf编码传输,并可以在另一端自动解析成对象.
  • gRPC的使用

    1. 添加grpc的maven插件
    2. src/proto文件夹下编写proto文件
    3. 使用mvn 编译项目,在arget/generated-sources/protobuf下能够找到生成的java文件.通常生成的service在grpc-java下
    4. 编写service的具体实现类,需要继承生成的[ServiceName]Grpc.[ServiceName]ImplBase
      • eg:public class StudentServiceImpl extends StudentServiceGrpc.StudentServiceImplBase
      • 重写所有的service方法.方法中会有个SteamObserver类型的参数:responseObserver,用来传递返回信息或错误信息
      • responseObserver.onNext(myResponse) 添加myResponse到返回信息中
      • responseObserver.onError(throwable); 返会错误信息
      • responseObserver.onCompleted();
    5. 编写server端代码
      1. 使用ServerBuilder获取server对象
        Server server = ServerBuilder.forPort(8899).addService(new StudentServiceImpl()).build();
      2. 调用server的start方法开启服务端
      3. 因为gRPC的server开启后,如果不作处理会自动关闭,所以要调用server的awaitTermination方法让程序一直在运行.
    6. 编写client端代码
      1. 使用ManagedChannelBuilder创建ManagedChannel
      2. 使用生成的ServiceGrpc类的newBlockingStub方法(方法中传入上一步创建的managedChannel)获得blockingStub
      3. 使用blockingStub便可直接调用Service中的方法并获取返回值了.
  • NIO相关

    • NIO三大组成部分
      1. selector
      2. channel
      3. buffer
    • 数据通过channel.read方法写入buffer中,或者通过channel.write通过buffer写入到channel中
    • Buffer的常用类型: ByteBuffer,ShortBuffer,IntBuffer,FloatBuffer,LongBuffer,DoubleBuffer,CharBuffer
    • Buffer中成员变量说明
      • mark:使用mark()方法,将position赋值给mark,使用reset()将position赋值为标记的mark值.如果mark值是-1,报错.
      • position:下一个读或者写的位置
      • limit:buffer能够读或者写的最大位置+1
      • capcity: buffer的最大容量
      • long address: 保存direct buffer在内存中的地址
    • mark <= position <= limit <= capcity
    • ByteBuffer使用说明
      1. ByteBuffer无法直接new出来,需要使用ByteBuffer.allocate(int capcity)方法来返回一个ByteBuffer实例.实际上返回的类型是ByteBuffer的子类HeapByteBuffer.
      2. 使用ByteBuffer.allocate(int capcity)生成的ByteBuffer的各变量的值:
        1. mark = -1
        2. position = 0
        3. limit = capcity
      3. buffer.flip()
        • limit = position;
        • posion = 0;
        • mark = -1;
      4. buffer.rewind();
        • position = 0;
        • mark = -1;
      5. buffer.clear();
        • position = 0;
        • limit = capcity;
        • mark = -1;
      6. buffer.mark();
        • mark = position;
      7. buffer.reset();
        • if mark = -1 throw InvalidMarkException
        • position = mark;
    • ByteBuffer的几个具体类:
      • HeapByteBuffer:可读写的,创建在堆上的ByteBuffer; 通过ByteBuffer.allocate(int capcity)获得
      • HeapByteBufferR:只读的ByteBuffer, 通过bytebuffer.asReadOnlyBuffer()获得
      • DirectByteBuffer:通过ByteBuffer.allocateDirect(int capcity)获得
      • DirectByteBufferR
    • HeapByteBuffer和DirectByteBuffer,零拷贝相关
      • HeapByteBuffer是在JVM堆中创建的对象,其中保存数据的byte数组也是保存在堆中的.实际上操作系统在使用这个byte数组时,会将jvm堆中的byte数组拷贝到操作系统内存中,然后通过内存中的数据与IO设备交互,这样其实就多了一次拷贝的过程
      • DirectBtyeBuffer在创建时就已经在堆外申请了一个内存地址,实际的byte数组数据直接存放在这个内存地址中了,堆中buffer对象持有这个地址的引用.每次在进行数据的读写时,操作系统直接通过这块内存地址与IO设备通信,省去了一次内存拷贝过程.
      • jvm堆也是在操作系统的内存中,为啥HeapByteBuffer还需要将数据拷贝到内存中?
        • 因为在堆中,可能会发生垃圾回收,对象的地址可能发生改变.
  • Selector(选择器或者叫多路复用器)

    • 使用selector的好处:
      • selector使用轮询的方式来判断channel是否处于某些状态,因此使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。
    • Selector.open()返回一个新的Selector对象
    • selector.select()返回触发事件的channel的个数,其中事件是channel在注册时指定的感兴趣的事件.
      • SelectionKey.OP_ACCEPT = 1 << 4
      • SelectionKey.OP_CONNECT = 1 << 3
      • SelectionKey.OP_WRITE = 1 << 2
      • SelectionKey.OP_READ = 1 << 0
  • 完整的Server客户端

    • 第一步:将ServerSocketChannel注册到Selector上
      1. 创建一个selector对象Selector selector = Selector.open();
      2. 使用ServerSocketChannel.open()获得ServerSocketChannel对象
      3. 配置serverSocketChannel为非阻塞,因为要注册到Selector的channel必须是非阻塞的serverSocketChannel.configureBlocking(false)
      4. 设置ServerSocketChannel中ServerSocket的绑定的端口号serverSocketChannel.socket().bind(new INetSocketAddress(5678))
      5. 将serverSocketChannel注册到selector上serverSocketChannel.registor(selector,SelectionKey.OP_ACCEPT);
    • 第二步:编写Selector的轮询逻辑
      1. int num = selector.select();这个方法是一个阻塞方法,只有当selector中注册的channel触发了它在注册时感兴趣的事件,这个方法才会返回值,其返回值是可以进行I/O操作的channel的个数.
      2. Set<SelectionKey> selectionKeys = selector.selectionKeys()获得当前准备好IO操作的channel对应的SelectionKey集合.
      3. Iterator<SelectionKey> iterator = selectionKeys.iterator();
      4. 循环遍历迭代器中的每个SelectionKey,判断当前触发的是哪个IO操作,根据不同的IO操作处理不同的业务逻辑.
        • selectionKey.isAcceptable()判断当前SelectionKey是否触发accept,如果触发了,调用ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel()获得当前的ServerSocketChannel,在使用SocketChannel socketChannel = serverSocketChannel.accept()方法获得请求连接的SocketChannel,设置socketChannel为非阻塞模式并将其注册到selector上.
        • selectionKey.isConnectable()
        • selectionKey.isReadable()
        • selectionKey.isWritable()
      5. 每次遍历最后都要调用iterator.remove()方法,将当前的SelectionKey从selectionKeys中移除
        • 因为selector不会自动删除selectionKeys集合中的SelectionKey,所以如果不手动删除的话,下次轮询结果中还会有上一次轮询时的结果.
  • java nio的零拷贝

    • 从操作系统的角度
    • user space用户空间
    • kernel space内核空间

sendfile() syscall
sendfile() returns

  • fileChannel.transferTo(startP,total,socketChannel)
原文地址:https://www.cnblogs.com/Serenity1994/p/12445011.html