# nettyLearn **Repository Path**: yzg999/nettyLearn ## Basic Information - **Project Name**: nettyLearn - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-09-05 - **Last Updated**: 2026-07-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [TOC] # netty学习 首先要明确的一点是: netty并未实现Servlet的相关接口。所以跟我们使用的tomcat的容器是不太一样的。
简单的来说,netty的编程主要分为三个步骤: 1. server端、client端 ```java public static void main(String[] args) { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new MyServerInitializer()); ChannelFuture channelFuture = serverBootstrap.bind(8899).sync(); channelFuture.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } ``` 2. 实现相关的ChannelInitializer继承`ChannelInitializer`; ```java /** * 客户端与服务端建立连接后,这个方法就被调用 * @param socketChannel * @throws Exception */ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast("LengthFieldBasedFrameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)); pipeline.addLast("LengthFieldPrepender", new LengthFieldPrepender(4)); pipeline.addLast("StringDecoder", new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast("StringEncoder", new StringEncoder(CharsetUtil.UTF_8)); pipeline.addLast("MyClientHandler", new MyClientHandler()); } ``` 3. 实现对应的handler继承`SimpleChannelInboundHandler` ```java /** * * @param channelHandlerContext * @param s 客户端发送过来的数据 * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception { System.out.println("客户端:" + channelHandlerContext.channel().remoteAddress() + " : " + s); channelHandlerContext.channel().writeAndFlush("get msg form server: " + UUID.randomUUID().toString()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } ``` ## 代码大纲 1. 基于netty得到`helloworld`程序; 2. 群聊广播demo; 3. 心跳检测demo; 4. 基于netty的websocket实现; 5. `protobuf`的应用 1. `protobuf`的简单使用和代码生成 2. `protobuf`接收多协议的处理方式
a. `proto`的文件声明
```proto //最外层传递的消息 message MyMessage { enum DataType { StudentType = 1; DogType = 2; CatType = 3; } required DataType date_type = 1; // message中有多个可选字段,最多只有一个字段被设置,它是共享内存的。 // r如果设置了两个oneof,那么后面的oneof会覆盖前面设置的。 oneof dataBody { Student student = 2; Dog dog = 3; Cat cat = 4; } } message Student{ required int32 id = 1; required int32 age = 2; required string name = 3; optional string address = 4; } message Dog { optional string name = 1; optional int32 age = 2; } message Cat { optional string name = 1; } ``` b. `Initializer`的处理
```java @Override protected void initChannel(SocketChannel ch) throws Exception { // 传入protobuf的编解码器。这样在handler端就能直接以对象的方式来传递 ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new ProtobufVarint32FrameDecoder()); // 将对象转换为字节数组 pipeline.addLast(new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance())); pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); pipeline.addLast(new ProtobufEncoder()); pipeline.addLast(new ProtobufServerHandler()); } ``` c. `Handler`的处理 ```java public class ProtobufServerHandler extends SimpleChannelInboundHandler { @Override protected void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception { MyDataInfo.MyMessage.DataType dataType = msg.getDateType(); if (dataType == MyDataInfo.MyMessage.DataType.StudentType) { MyDataInfo.Student student = msg.getStudent(); System.out.println("student...."); System.out.println("name : " + student.getName()); System.out.println("id : " + student.getId()); System.out.println("address : " + student.getAddress()); System.out.println("age : " + student.getAge()); } else if (dataType == MyDataInfo.MyMessage.DataType.DogType) { System.out.println("dog...."); MyDataInfo.Dog dog = msg.getDog(); System.out.println(dog.getName()); System.out.println(dog.getAge()); } else { System.out.println("cat...."); MyDataInfo.Cat cat = msg.getCat(); System.out.println(cat.getName()); } } } ``` ## netty的执行流程 1. 客户端连上netty服务器后,马上调用handlerAdded方法完成channel的添加操作(所谓channel可以理解为一个客户端) 2. 添加操作执行完成以后立马调用channelRegistered方法将channel注入到netty中管理起来 3. 注册好以后调用服务器端的channelActive方法,让其处于激活状态 4. 调用channelRead0方法完成客户端数据的读取和相应 5. 调用完成以后curl主动断开服务器的链接,并通知服务器端,服务器端就会调用channelInactive方法处理回调事件 6. 最后从netty的注册中将该channel删除掉 ## 一个简单的`Hello World`程序 ### 服务端编写 1. 声明两个EventLoopGroup; 1. boss: 接收连接的进程 2. worker: 处理接收到的连接 2. 绑定NioSocketServerClass; 3. 绑定 childChannel; 1. channelInitializer; 2. SimpleChannelInBoundHandler; ## 广播的实现 在服务端启动的时候,有新的客户端连接进的时候,服务端向其他的客户端发送:[xxx 加入]信息。 当客户端广播消息的时候,其他客户端接收消息显示[ xxx 发送];自己显示:[自己发送]。
**实现这个需求的关键是知道以下几个知识点:** 1. 当新的客户端连接建立的时候,调用什么回调方法;
`handlerAdded`。该方法是当有新的客户端连接服务端的时候,服务端触发的回调方法。 2. 服务端怎么保存所有连接进来的客户端连接(channel);
```java private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); ``` * `Initilatizer`
```java @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("DelimiterBasedFrameDecoder", new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter())); pipeline.addLast("StringDecoder", new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast("StringEncoder", new StringEncoder(CharsetUtil.UTF_8)); pipeline.addLast(new MyChatClientChannelHandler()); } ``` * `handler`
```java /** * 保存channel对象 */ private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); private static final String DATE_PARTTEN = "yyyy-MM-dd HH:mm:ss:SSS"; @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { Channel channel = ctx.channel(); channelGroup.forEach(ch -> { // 当前遍历的channel不是发送msg的channel对象。则向其他客户端广播 if (channel != ch) { ch.writeAndFlush(channel.remoteAddress() + ", 发送的消息" + msg + "\n"); } else { ch.writeAndFlush("[自己] " + msg + " \n"); } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); System.out.println(channel.remoteAddress() + " 上线了!"); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); System.out.println(channel.remoteAddress() + " 离开了!"); } /** * 客户端链接建立的时候调用 * @param ctx * @throws Exception */ @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { //super.handlerAdded(ctx); // 服务端与客户端建立 Channel channel = ctx.channel(); // 向其他链接的客户端发送广播信息 SocketAddress socketAddress = channel.remoteAddress(); String date = DateTimeFormatter.ofPattern(DATE_PARTTEN).format(LocalDateTime.now()); // 向channelGroup中的每一个channel对象发送一个消息 channelGroup.writeAndFlush(date + " [服务器] - " + socketAddress + " 加入 \n"); // 保存该客户端链接 channelGroup.add(channel); } /** * 链接断开 * @param ctx * @throws Exception */ @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); String date = DateTimeFormatter.ofPattern(DATE_PARTTEN).format(LocalDateTime.now()); channelGroup.writeAndFlush(date + " [服务器] - " + channel.remoteAddress() + " 离开 \n"); } ``` ## 心跳检查程序 * server端还是老样子。 * `initializer`的写法
服务端超过5S未读取到信息或者超过7秒未写,或者超过10秒既没有读也没有写,那么就心跳检测失败。
```java @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // 在一定的事件间隔之内,链接没有发生任何读写事件,会触发该事件 // server端的空闲检测; 读空闲: 5 server5秒内未读取到数据,则提示读超时 pipeline.addLast("IdleStateHandler", new IdleStateHandler(5, 7, 10, TimeUnit.SECONDS)); pipeline.addLast(new MyServerHandler()); } ``` * `channelHandler`
这里的channelHandler就不再是继承`SimpleChannelInBoundHandler`了,而是专门继承另一个;
```java public class MyServerHandler extends ChannelInboundHandlerAdapter { /** * 触发事件 * @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(); } } } ``` ## 利用netty实现一个WebSocket服务器 * server还是老样子 * `initializerHandler`
```java public class WebSocketChannelInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new ChunkedWriteHandler()); // 声明websocket的协议信息 // netty处理请求是按分段的方式来进行的,这里指定每段的长度 pipeline.addLast(new HttpObjectAggregator(8192)); pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); // 添加超时检查机制 pipeline.addLast("IdleStateHandler", new IdleStateHandler(5, 7, 10, TimeUnit.SECONDS)); pipeline.addLast(new MyIdleChannelHandler()); pipeline.addLast(new TextWebSocketServerHandler()); } } ``` * `ChannelHandler`
我这里有两个handler,分别是上一节讲述的心跳检测的handler和处理文本信息的websocket * TextWebSocketServerHandler (文本信息的websocket处理) ```java public class TextWebSocketServerHandler extends SimpleChannelInboundHandler { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println("客户端消息内容: " + msg.text()); ctx.writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now())); } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerAdded:" + ctx.channel().id().asLongText()); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerRemoved: " + ctx.channel().id().asLongText()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } ``` ## RPC框架 1. 定义一个接口的说明文件 2. 通过编译器将这个文件编译成你想调用的那种语言文件 3. 引入编译好的文件发起远程调用 ### [protobuf说明文件](./protobuf.md) ## thrift * 包名: `thrift`
* thrift的声明文件
src/thrift
* 生成java代码
```shell script thrift --gen java src/thrift/data.thrift ``` ## ***nio 相关知识*** [***nio相关***](./nio.md) ## Netty 的工作模式 ### Reactor模式 ###**[Reactor模式介绍](./reactor-desc.md)** ### `EventLoop`的相关知识点 1. 一个`EventLoopGroup`当中包含一个或者多个`EventLoop`; 2. 一个`EventLoop`在它的整个生命周期当中只与唯一的Thread进行绑定; 3. 所有的`EventLoop`所处理的各种I/O事件都将在它所关联的Thread上面进行处理; 4. **一个`Channel`在它的生命周期只会注册在一个`EventLoop`上**; 5. 一个`EventLoop`会被一个或者多个`Channel`所绑定; 由以上观点可以看出,在Netty中,Channel的实现一定是线程安全的;基于此,我们可以存储一个Channel的引用,并且需要向远程端点发送数据时,通过引用 也能调用channel的相关方法,即便当时有很多线程在使用它也不会出现多线程的问题;而且**消息一定会按照顺序发送出去** 重要结论:我们在业务开发中,不需要将长时间的耗时任务放到`EventLoop`的执行队列中,因为它将会一直阻塞该线程所绑定的的所有Channel的其他执行任务; 如果要进行阻塞调用或者耗时操作,那么我们将要使用一个专门的`EventExecutor`(业务线程池)。
实现`EventExecutor`有以下两种方式. 1. 使用JDK提供的线程池操作; 2. 使用Netty提供的向ChannelPipeline`addLast(EventExecutorGroup group, ChannelHandler... handlers);`,这样所有的任务就全部丢给所有的这个group线程组执行; ### ByteBuf #### 创建ByteBuf ```java CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); ByteBuf heapBuff = Unpooled.buffer(10); ByteBuf directBuffer = Unpooled.directBuffer(8); ``` #### 特性 Netty的ByteBuf提供了三种缓冲区类型 1. heap buffer 这是常用的类型,ButeBuf将数据存储到JVM中,并且将实际的数据存放到byte array中实现。 优点:由于数据是存储在JVM中,因此可以快速的创建和快速释放,并且它提供了直接访问内部字节数组的方法。
缺点: 每次读写数据时,都需要先将数据复制到直接缓冲区中再进行网络传输 2. direct buffer 在堆之外直接分配内存空间,直接缓冲区并不会占用堆的容量空间,因为它是由操作系统在本地进行的数据分配。 优点: 在使用socket网络传输时,可以直接将操作本地内存的数据,不需要从JVM将数据复制到直接缓冲区。
缺点: 因为direct buffer是直接在本地内存的,所有在分配和释放比堆空间更加复杂,而且速度更慢些。
Netty通过内存池来解决这个问题,直接缓冲区并不支持通过字节数组方式访问数据。 **对于后端业务消息的编码来说,推荐使用Heap buffer;对于I/O通信线程来说,推荐使用 Driect buffer** 3. composite buff(组合缓冲区) #### JDK ByteBuffer与ByteBuf的区别 1. Netty的ByteBuf采用了读写索引分离的策略(readIndex/writeIndex),一个初始化(里面尚无数据)的ByteBuf的readIndex和writeInde都是0; 2. 当读索引与写索引处在同一个位置时,如果继续读取,那么将会抛出IndexOutBoundsException 3. 对于ByteBuf的任何读写操作都会分别单独维护读索引和写索引,maxCapacity最大容量默认的是Integer.MAX_VALUE #### JDK ByteBuffer的缺点 1. `final byte[] hb`;这时JDK ByteBuffer对象中用于存储疏忽的对象声明,可以看出,其字节数组被声明`final`,也就是长度是固定不变的,一旦分配好后不能动态扩容和收缩;二期当待存储的数据字节很大的时候容易出现异常。如果要防止这个异常那就需要在存储之前完全确定好字节数组长度。如果ByteBuffer的空间不足,我们只有一种解决方案:创建一个新的ByteBuffer对象,然后将之前的字节数组中的数据赋值过去,不过这一切需要开发者自己去手动完成 2. ByteBuffer只能使用一个position至真来标识位置信息,在进行数据读写切换时就需要调用`flip()`或是`rewind()置0` #### Netty中ByteBuf的优点 1. 存储字节的数组是动态的,最大值是Integer.MAX_VALUE,这里的动态性提现在write方法中。write方法在执行是会判读buffer容量,如果不足就自动扩容; ````java final void ensureWritable0(int minWritableBytes) { // 获取到当前ByteBuf的writeIndex final int writerIndex = writerIndex(); // final int targetCapacity = writerIndex + minWritableBytes; // 当前数组容量是否足够 if (targetCapacity <= capacity()) { ensureAccessible(); return; } if (checkBounds && targetCapacity > maxCapacity) { ensureAccessible(); throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", writerIndex, minWritableBytes, maxCapacity, this)); } // Normalize the target capacity to the power of 2. final int fastWritable = maxFastWritableBytes(); int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable : alloc().calculateNewCapacity(targetCapacity, maxCapacity); // Adjust to the new capacity. capacity(newCapacity); } ```` 2. ByteBuf的读写索引事分开的,使用起来很方便; #### ByteBuf判断是否可用 引用计数,referencecount,通过调用`release()、retain()`对bytebuf对象计数做减、加操作。如果bytebuf的引用计数变为0,那么表示这个对象将要被回收。`retain()` ```java // rawIncrement == increment << 1 private T retain0(T instance, final int increment, final int rawIncrement) { int oldRef = updater().getAndAdd(instance, rawIncrement); if (oldRef != 2 && oldRef != 4 && (oldRef & 1) != 0) { throw new IllegalReferenceCountException(0, increment); } // don't pass 0! if ((oldRef <= 0 && oldRef + rawIncrement >= 0) || (oldRef >= 0 && oldRef + rawIncrement < oldRef)) { // overflow case // CAS 操作 updater().getAndAdd(instance, -rawIncrement); throw new IllegalReferenceCountException(realRefCnt(oldRef), increment); } return instance; } public int getAndAdd(T obj, int delta) { int prev, next; // 死循环,知道CAS操作成功为止 do { prev = get(obj); next = prev + delta; } while (!compareAndSet(obj, prev, next)); return prev; } ``` ##### AtomicIntegerFieldUpdater 要点总结 1. 更新器只能更新int类型的变量,不能包含其他类型 ```java if (field.getType() != int.class) throw new IllegalArgumentException("Must be integer type"); ``` 2. 更新的字段必须要有`volatile`修饰 ```java if (!Modifier.isVolatile(modifiers)) throw new IllegalArgumentException("Must be volatile type"); ``` 3. 待更新的变量不能是static(底层使用`unSafa.objectFieldOffet`来操作,CAS操作本质是根据对象实例的偏移量进行赋值的) 4. 只能更新他可见范围内的变量(底层通过反射来实现) ## 目录说明 1. `official` package:[netty官网运行的看起来比较好玩的示例](https://netty.io/wiki/index.html) 2. `以数字序号命名` 的package:B站学习示例 ## 学习地址 [B站视频地址信息](https://www.bilibili.com/video/av33707223?p=4)