学习笔记 | Netty
-
Netty编程的整体步骤
- 编写服务器
- 在服务器中添加自定义的Initializer对象
- 在Initializer对象中添加自定义的handler对象
- 在handler里重写netty提供的回调方法
-
Netty网络编程
- 编写Server
EventLoopGroup:bossGroup,workerGroup
ServerBootstrap: serverBootstrap
serverBootstrap
.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyInitializer());
- 编写Initializer
- 继承
ChannelInitializer<SocketChannel>
- 重写initChannel(SocketChannel)方法
- 获取ChannelPipeLine:
ChannelPipeline pipeline = socketChannel.pipeline();
- 向pipeLine中添加handler
- HttpServerCodec:处理Http请求的handler
- 获取ChannelPipeLine:
- 继承
- 编写Handler
- 继承
SimpleChannelInbondHandler<HttpObject>
- 重写channelRead0方法
- 继承
- 编写Server
-
NettySocket编程
- server端使用ServerBootstrap,client端使用Bootstrap
- Initialize用到的HandlerLengthFieldBasedFrameDecoder,LengthFieldPrepender,StringDecoder,StringEncoder
-
Netty聊天室Demo
- Initializer用到的Handler:DelimiterBasedFrameDecoder,StringDecoder,StringEncoder
- 自定义Handler:
- 定义一个静态私有变量ChannelGroup,用来保存所有已连接的client的Channel
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
- 定义一个静态私有变量ChannelGroup,用来保存所有已连接的client的Channel
-
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编码传输,并可以在另一端自动解析成对象.
- 需要在ChannelInitializer的ChannelPipeLine中添加ProtoBuf相关的handler:
-
gRPC的使用
- 添加grpc的maven插件
- 在
src/proto
文件夹下编写proto文件 - 使用mvn 编译项目,在arget/generated-sources/protobuf下能够找到生成的java文件.通常生成的service在grpc-java下
- 编写service的具体实现类,需要继承生成的
[ServiceName]Grpc.[ServiceName]ImplBase
类- eg:
public class StudentServiceImpl extends StudentServiceGrpc.StudentServiceImplBase
- 重写所有的service方法.方法中会有个
SteamObserver
类型的参数:responseObserver
,用来传递返回信息或错误信息 responseObserver.onNext(myResponse)
添加myResponse到返回信息中responseObserver.onError(throwable);
返会错误信息responseObserver.onCompleted();
- eg:
- 编写server端代码
- 使用ServerBuilder获取server对象
Server server = ServerBuilder.forPort(8899).addService(new StudentServiceImpl()).build();
- 调用server的start方法开启服务端
- 因为gRPC的server开启后,如果不作处理会自动关闭,所以要调用server的awaitTermination方法让程序一直在运行.
- 使用ServerBuilder获取server对象
- 编写client端代码
- 使用ManagedChannelBuilder创建ManagedChannel
- 使用生成的ServiceGrpc类的newBlockingStub方法(方法中传入上一步创建的managedChannel)获得blockingStub
- 使用blockingStub便可直接调用Service中的方法并获取返回值了.
-
NIO相关
- NIO三大组成部分
- selector
- channel
- 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使用说明
- ByteBuffer无法直接new出来,需要使用ByteBuffer.allocate(int capcity)方法来返回一个ByteBuffer实例.实际上返回的类型是ByteBuffer的子类HeapByteBuffer.
- 使用ByteBuffer.allocate(int capcity)生成的ByteBuffer的各变量的值:
- mark = -1
- position = 0
- limit = capcity
- buffer.flip()
- limit = position;
- posion = 0;
- mark = -1;
- buffer.rewind();
- position = 0;
- mark = -1;
- buffer.clear();
- position = 0;
- limit = capcity;
- mark = -1;
- buffer.mark();
- mark = position;
- 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:可读写的,创建在堆上的ByteBuffer; 通过
- HeapByteBuffer和DirectByteBuffer,零拷贝相关
- HeapByteBuffer是在JVM堆中创建的对象,其中保存数据的byte数组也是保存在堆中的.实际上操作系统在使用这个byte数组时,会将jvm堆中的byte数组拷贝到操作系统内存中,然后通过内存中的数据与IO设备交互,这样其实就多了一次拷贝的过程
- DirectBtyeBuffer在创建时就已经在堆外申请了一个内存地址,实际的byte数组数据直接存放在这个内存地址中了,堆中buffer对象持有这个地址的引用.每次在进行数据的读写时,操作系统直接通过这块内存地址与IO设备通信,省去了一次内存拷贝过程.
- jvm堆也是在操作系统的内存中,为啥HeapByteBuffer还需要将数据拷贝到内存中?
- 因为在堆中,可能会发生垃圾回收,对象的地址可能发生改变.
- NIO三大组成部分
-
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
- 使用selector的好处:
-
完整的Server客户端
- 第一步:将ServerSocketChannel注册到Selector上
- 创建一个selector对象
Selector selector = Selector.open();
- 使用
ServerSocketChannel.open()
获得ServerSocketChannel对象 - 配置serverSocketChannel为非阻塞,因为要注册到Selector的channel必须是非阻塞的
serverSocketChannel.configureBlocking(false)
- 设置ServerSocketChannel中ServerSocket的绑定的端口号
serverSocketChannel.socket().bind(new INetSocketAddress(5678))
- 将serverSocketChannel注册到selector上
serverSocketChannel.registor(selector,SelectionKey.OP_ACCEPT);
- 创建一个selector对象
- 第二步:编写Selector的轮询逻辑
int num = selector.select();
这个方法是一个阻塞方法,只有当selector中注册的channel触发了它在注册时感兴趣的事件,这个方法才会返回值,其返回值是可以进行I/O操作的channel的个数.Set<SelectionKey> selectionKeys = selector.selectionKeys()
获得当前准备好IO操作的channel对应的SelectionKey集合.Iterator<SelectionKey> iterator = selectionKeys.iterator();
- 循环遍历迭代器中的每个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()
- 每次遍历最后都要调用iterator.remove()方法,将当前的SelectionKey从selectionKeys中移除
- 因为selector不会自动删除selectionKeys集合中的SelectionKey,所以如果不手动删除的话,下次轮询结果中还会有上一次轮询时的结果.
- 第一步:将ServerSocketChannel注册到Selector上
-
java nio的零拷贝
- 从操作系统的角度
- user space用户空间
- kernel space内核空间
sendfile() syscall
sendfile() returns
- fileChannel.transferTo(startP,total,socketChannel)