[自己做个游戏服务器二] 游戏服务器的基石-Netty全解析,有例子,多图解释

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Netty的大名我想做java 的基本都知道,因为他实在太出名了,现在很多著名的软件都是使用netty作为通讯基础,今天就聊聊Netty,希望能讲清楚,如果懒得看理论,可以直接拉到后面看Hello world。把代码抄下来,运行一下看看。

Netty的大名我想做java 的基本都知道,因为他实在太出名了,现在很多著名的软件都是使用netty作为通讯基础,今天就聊聊Netty,希望能讲清楚,如果懒得看理论,可以直接拉到后面看Hello world。把代码抄下来,运行一下看看。


1、Netty 是什么


Netty是一个高性能、异步事件驱动的NIO框架,基于JAVA NIO提供的API实现。它提供了对TCP、UDP和文件传输的支持

作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。


作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。


Netty的官网 :netty.io/


41e87da8f55842d6b881bbb42c0bb8a1~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

2、Netty的优点

4861c11a2ce84fc89f8682e407dd200a~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

Netty的缺点就不说了,Netty的优点有很多:


统一的 API,支持多种传输类型,阻塞和非阻塞的。


功能强大,内置了多种解码编码器,支持多种协议,比如上图中的右侧黄色区域,通用的文本,二进制协议,google protobuf等。


性能高,对比其他主流的NIO框架,Netty的性能最优。


社区活跃,发现BUG会及时修复,迭代版本周期短,不断加入新的功能。


简单而强大的线程模型。


自带编解码器解决 TCP 粘包/拆包问题。


自带各种协议栈,比如 SSL 。


比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制,Zero-Copy Byte buffer。

安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。


成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ ,Elasticsearch等等。


3、核心组件


3.1 Netty的线程模型


Netty的线程模型是比较重要的,理解了Netty的线程模型才能很好地使用Netty,Netty常见的线程模型有三种:


1.单线程模型


单线程模型,是指所有的 I/O 操作都在同一个 NIO 线程上面完成的,此时NIO线程职责包括:接收新建连接请求、读写操作等,在游戏开发中不会使用,也不合理,不展开。


2.Reactor多线程模型


第一种不合理,升级一下,一个接受连接的线程, 所有的 I/O 操作都在同一个 NIO 线程池上面完成,这种线程模型可以满足大部分情况,但是如果在连接的时候需要做一些验证,就会阻塞线程。性能会出问题,服务器


3.Reactor主从多线程模型


服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel注 册 到 I/O 线 程 池(sub reactor 线 程 池)的某个I/O线程上, 由它负责SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。


这也是在游戏开发中最常用的线程模型,需要掌握,下面这张图将核心技术都做了展示

246c6159dcf14981a9c3049f90202fcf~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

3.2 EventLoopGroup


NioEventLoopGroup 核心实际上就是个线程池,是为了处理IO事件而存在的一个线程池。


一个 EventLoopGroup 包含一个或者多个 EventLoop;一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;所有有 EnventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;一个 Channel 在它的生命周期内只注册于一个 EventLoop;每一个 EventLoop 负责处理一个或多个 Channel;


我们实现服务端的时候,一般会初始化两个线程组:


bossGroup :接收连接。


workerGroup :负责具体的处理,交由对应的 Handler 处理


BossEventLoop 只负责处理连接,开销非常小,连接到来,马上将 SocketChannel 转发给 WorkerEventLoopGroup,WorkerEventLoopGroup 会由 next 选择其中一个 EventLoop 来将这 个SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理。


注:默认的线程数量 是当前cpu 数量 *2


public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup {
   private static final InternalLogger logger = InternalLoggerFactory.getInstance(MultithreadEventLoopGroup.class);
   private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
   protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
       super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
  }
复制代码


3.3 Channel


Channel 表示一个和客户端建立的连接,相当于电话建立了连接,Channel是双向的通道。


通道(Channel)是双向的,可读可写。在 Java NIO 中,Buffer 是一个顶层接口,它的常用子类有:


FileChannel:用于文件读写


DatagramChannel:用于 UDP 数据包收发


ServerSocketChannel:用于服务端 TCP 数据包收发


SocketChannel:用于客户端 TCP 数据包收发


游戏中常用的通道类型有以下:


NioSocketChannel:异步非阻塞的客户端 TCP Socket 连接。


NioServerSocketChannel:异步非阻塞的服务器端 TCP Socket 连接。


常用的就是这两个通道类型,因为是异步非阻塞的。所以是首选。


3.4  option()与childOption()


首先说一下这两个的区别。


option()设置的是服务端用于接收进来的连接,也就是boosGroup线程。


childOption()是提供给父管道接收到的连接,也就是workerGroup线程。


搞清楚了之后,我们看一下常用的一些设置有哪些:


SocketChannel参数,也就是childOption()常用的参数:


SO_RCVBUF Socket参数,TCP数据接收缓冲区大小。TCP_NODELAY TCP参数,立即发送数据,默认值为Ture。


SO_KEEPALIVE Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。


ServerSocketChannel参数,也就是option()常用参数:


SO_BACKLOG Socket参数,服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,Windows为200,其他为128。


3.5  inbound 和 outbound


inbound  表示 消息进入到服务器的路径,可以理解为输入


outBound 表示 消息输出到客户端的路径,可以理解为输出


ChannelPipeline p = ...;
  p.addLast("1", new InboundHandlerA());
  p.addLast("2", new InboundHandlerB());
  p.addLast("3", new OutboundHandlerA());
  p.addLast("4", new OutboundHandlerB());
  p.addLast("5", new InboundOutboundHandlerX());
复制代码


当一个输入事件来了之后,事件处理器的调用顺序为1,2,5


当一个输出事件来了之后,事件处理器的处理顺序为5,4,3。(注意输出事件的处理器发挥作用的顺序与定义的顺序是相反的)

可以理解为对handler 进行压栈操作。


ChannelInboundHandlerAdapter处理器常用的事件有:


注册事件 fireChannelRegistered。


连接建立事件 fireChannelActive。


读事件和读完成事件 fireChannelRead、fireChannelReadComplete。


异常通知事件 fireExceptionCaught。


用户自定义事件 fireUserEventTriggered。


Channel 可写状态变化事件 fireChannelWritabilityChanged。


连接关闭事件 fireChannelInactive。


ChannelOutboundHandler处理器常用的事件有:


端口绑定 bind。


连接服务端 connect。


写事件 write。


刷新时间 flush。


读事件 read。


主动断开连接 disconnect。


关闭 channel 事件 close。


还有一个类似的handler(),主要用于装配parent通道,也就是bossGroup线程。一般情况下,都用不上这个方法。


3.6 ByteBuf


ByteBuff有三种类型:


堆内存缓冲区(HeapByteBuf)


数据存储在堆中,可以认为就是我们常用的内存缓冲区


直接内存缓冲区(DirectByteBuf)


数据存储在内核中。由于数据本身就存储在内核中,因此使用网卡传输数据的时候直接可以传输,不需要多余的拷贝。因此,这也被称为零拷贝。


从硬盘中读取数据使用网卡发送出去,一般步骤如下:


数据从磁盘读取到内核的read buffer数据从内核缓冲区拷贝到用户缓冲区数据从用户缓冲区拷贝到内核的socket buffer

数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区使用内存缓冲区只需要两步:


调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer接着DMA从内核read buffer将数据拷贝到网卡接口buffer


6251ed73d1564f7e81bd111c9a122037~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

复合缓冲区(CompositeByteBuf)


复合缓冲区可以将多个ByteBuff组合


注:即内核功能模块运行在内核空间,而应用程序运行在用户空间

5eb6c52831674c2aa246f64e14d1448b~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

ByteBuf有读readerIndex和写writerIndex两个指针,用来标记“可读”、“可写”、“可丢弃”的字节


调用write*方法写入数据后,写指针将会向后移动


调用read*方法读取数据后,读指针将会向后移动


写入数据或读取数据时会检查是否有足够多的空间可以写入和是否有数据可以读取


写入数据之前,会进行容量检查,当剩余可写的容量小于需要写入的容量时,需要执行扩容操作


扩容时有一个4MB的阈值,需要扩容的容量小于阈值或大于阈值所对应的扩容逻辑不同


clear等修改读写指针的方法,只会更改读写指针位置的值,并不会影响ByteBuf中已有的内容


setZero等修改字节值的方法,只会修改对应字节的值,不会影响读写指针的值以及字节的可读写状态


Netty又为我们提供了两个工具类:Pooled、Unpooled,分类用来分配池化的和未池化的ByteBuf,进一步简化了创建ByteBuf的步骤,只需要调用这两个工具类的静态方法即可。


fa227e18d78b4f4aa6e4b9ac522ff83e~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


3.7 使用 Netty 自带的解码器


LineBasedFrameDecoder :


发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。


DelimiterBasedFrameDecoder :


可以自定义分隔符解码器,LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。


FixedLengthFrameDecoder:


固定长度解码器,它能够按照指定的长度对消息进行相应的拆包,每个数据包的长度都是固定的。


LengthFieldBasedFrameDecoder:


这个是后面服务器将要使用的解码器,下期会有实例


3.8 Netty 版本


netty5 中使用了 ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显


多个分支的代码同步工作量很大


作者觉得当下还不到发布一个新版本的时候


在发布版本之前,还有更多问题需要调查一下,比如是否应该废弃 exceptionCaught, 是否暴露EventExecutorChooser等等。


当前最新版本:4.1.68.Final


4、Hello World


4.1 官方的demo


官方的demo下载源码就可以在example下看到所有的demo,


gitHub 地址:github.com/netty/netty…

88b76a984fb8432cab04c6a14012ea67~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

4.2 idea 建立maven项目


我这里使用了idea,所以下面的截图也是用Idea。


4.2.1 File ->New 进入下面的界面

25e0d81da259466bac84babbb3c09178~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

4.2.2  next 如下图填入自己的信息

6782333e7c7d42b9a4a8c60e71ce09d7~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

4.2.3 等待一下,知道maven加载项目完成,如下结构

dee71bd2c6584828bd1af80c1d168d53~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg

4.3 服务端代码


为了尽可能的仅仅展示Netty的代码,去掉那些花里胡哨的技术,只是简单的程序


package com.xiangcai;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* 服务端代码
* @author 香菜
*/
public class GameServer {
   /**
    * 启动
    */
   public static void start() throws InterruptedException {
       EventLoopGroup boss = new NioEventLoopGroup(1);
       EventLoopGroup worker = new NioEventLoopGroup();
       try {
           ServerBootstrap serverBootstrap = new ServerBootstrap();
           serverBootstrap.group(boss, worker)
                  .channel(NioServerSocketChannel.class)
                   //服务端可连接队列数,对应TCP/IP协议listen函数中backlog参数
                  .option(ChannelOption.SO_BACKLOG, 1024)
                   //设置TCP长连接,一般如果两个小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
                  .childOption(ChannelOption.SO_KEEPALIVE, true)
                   //将小的数据包包装成更大的帧进行传送,提高网络的负载,即TCP延迟传输
                  .childOption(ChannelOption.TCP_NODELAY, true)
                  .childHandler(new NettyServerHandlerInitializer());
           ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();
           System.out.println("服务器启动了");
           channelFuture.channel().closeFuture().sync();
      } finally {
           // 关闭线程
           boss.shutdownGracefully();
           worker.shutdownGracefully();
      }
  }
   public static void main(String[] args) throws InterruptedException {
       start();
  }
}
复制代码


下面看下Channel的初始化代码:


使用了2个解码器


package com.xiangcai;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
/**
* 服务端代码
* @author 香菜
*/
public class NettyServerHandlerInitializer extends ChannelInitializer<Channel> {
   @Override
   protected void initChannel(Channel ch) throws Exception {
       ch.pipeline()
               //基于换行符的解码器
              .addLast(new LineBasedFrameDecoder(1024))
               // 强转字符串
              .addLast(new StringDecoder())
               // 业务处理
              .addLast(new NettyServerHandler());
  }
}
复制代码


下面是业务的代码展示:


这里只是展示了简单的收发消息


package com.xiangcai;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
/**
* 服务端代码
* @author 香菜
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
   @Override
   public void channelActive(ChannelHandlerContext ctx) throws Exception {
       super.channelActive(ctx);
  }
   /**
    * 超时处理 如果5秒没有接受客户端的心跳,就触发; 如果超过两次,则直接关闭;
    */
   @Override
   public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
       if (obj instanceof IdleStateEvent) {
           IdleStateEvent event = (IdleStateEvent) obj;
           if (IdleState.READER_IDLE.equals(event.state())) { // 如果读通道处于空闲状态,说明没有接收到心跳命令
               System.out.println("已经5秒没有接收到客户端的信息了");
          }
      } else {
           super.userEventTriggered(ctx, obj);
      }
  }
   /**
    * 业务逻辑处理
    */
   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
       System.out.println("服务端收到信息:" + msg);
       String respStr = "收到了 " + System.getProperty("line.separator");
       ByteBuf resp = Unpooled.copiedBuffer(respStr.getBytes());
       ctx.writeAndFlush(resp);
  }
   /**
    * 异常处理
    */
   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
       cause.printStackTrace();
       ctx.close();
  }
}
复制代码


4.4 客户端代码


客户端启动代码


package com.xiangcai;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import org.junit.Test;
/**
* 服务端代码
* @author 香菜
*/
public class TestClient {
   public static void clientStart() {
       EventLoopGroup group = new NioEventLoopGroup();
       try {
           Bootstrap b = new Bootstrap();
           b.group(group)
                  .channel(NioSocketChannel.class)
                  .option(ChannelOption.TCP_NODELAY, true)
                  .handler(new ChannelInitializer<SocketChannel>() {
                       protected void initChannel(SocketChannel ch) throws Exception {
                           ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                           ch.pipeline().addLast(new StringDecoder());
                           ch.pipeline().addLast(new ClientHandler());
                      };
                  });
           // 发起异步连接操作
           ChannelFuture f = b.connect("127.0.0.1", 8088).sync();
           // 等待客户端连接关闭
           f.channel().closeFuture().sync();
      } catch (InterruptedException e) {
           e.printStackTrace();
      } finally {
           // 优雅退出,释放NIO线程组
           group.shutdownGracefully();
      }
  }
   public static void main(String[] args) {
       clientStart();
  }
}
复制代码


客户端业务处理:


也是简单的收发消息


package com.xiangcai;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.nio.charset.StandardCharsets;
/**
* 服务端代码
* @author 香菜
*/
public class ClientHandler extends ChannelInboundHandlerAdapter {
  @Override
  public void channelActive(ChannelHandlerContext ctx) {
      System.out.println("建立连接");
      byte[] bytes = ("连接上了,开始说话" + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8);
      ByteBuf message = Unpooled.buffer(bytes.length);
      message.writeBytes(bytes);
      ctx.writeAndFlush(message);
  }
  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) {
      String body = (String) msg;
      System.out.println("收到信息 " + body);
  }
  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
      System.out.println("发生异常");
      ctx.close();
  }
}
复制代码

5、总结

现在越来越多的游戏公司使用Java进行开发,Netty是绕不开的网络基础,搞懂Netty 很重要,Netty也很简单,只要记得线程模型,编解码,其他的都是细节问题,在开发的过程中进行学习也不迟,希望这篇文章能帮助你理解,如果你有疑问可以留言给我,一起学习交流。


完整项目源码下载地址:download.csdn.net/download/pe…


最后一张图结尾


f1eb313fb25845ecaad01a9f0dab38af~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


写的好累,希望大佬们能点个赞

目录
相关文章
|
4天前
|
机器学习/深度学习 人工智能 弹性计算
阿里云GPU服务器全解析_GPU价格收费标准_GPU优势和使用说明
阿里云GPU云服务器提供强大的GPU算力,适用于深度学习、科学计算、图形可视化和视频处理等场景。作为亚太领先的云服务商,阿里云GPU云服务器具备高灵活性、易用性、容灾备份、安全性和成本效益,支持多种实例规格,满足不同业务需求。
|
17天前
|
存储 弹性计算 NoSQL
"从入门到实践,全方位解析云服务器ECS的秘密——手把手教你轻松驾驭阿里云的强大计算力!"
【10月更文挑战第23天】云服务器ECS(Elastic Compute Service)是阿里云提供的基础云计算服务,允许用户在云端租用和管理虚拟服务器。ECS具有弹性伸缩、按需付费、简单易用等特点,适用于网站托管、数据库部署、大数据分析等多种场景。本文介绍ECS的基本概念、使用场景及快速上手指南。
57 3
|
21天前
|
监控 网络协议 安全
DNS服务器故障不容小觑,从应急视角谈DNS架构
DNS服务器故障不容小觑,从应急视角谈DNS架构
44 4
|
28天前
|
存储 固态存储 安全
阿里云服务器X86计算架构解析与X86计算架构云服务器收费价格参考
阿里云服务器架构分为X86计算、Arm计算、高性能计算等多种架构,其中X86计算是用户选择最多的一种架构,本文将深入探讨阿里云X86计算架构的云服务器,包括其技术特性、适用场景、性能优势以及最新价格情况。
|
28天前
|
域名解析 网络协议 CDN
阿里云服务器购买后如何解析域名,三步操作即可解析绑定
阿里云服务器购买后如何解析域名,三步操作即可解析绑定
|
29天前
|
编解码 弹性计算 应用服务中间件
阿里云服务器Arm计算架构解析:Arm计算架构云服务器租用收费标准价格参考
阿里云服务器架构分为X86计算、Arm计算、高性能计算等多种架构,其中Arm计算架构以其低功耗、高效率的特点受到广泛关注。本文将深入解析阿里云Arm计算架构云服务器的技术特点、适用场景以及包年包月与按量付费的收费标准与最新活动价格情况,以供选择参考。
|
1月前
|
存储 SQL 分布式计算
湖仓一体架构深度解析:构建企业级数据管理与分析的新基石
【10月更文挑战第7天】湖仓一体架构深度解析:构建企业级数据管理与分析的新基石
55 1
|
21天前
|
人工智能 关系型数据库 双11
2024年阿里双十一活动解析:助力大家优惠上云!云服务器79元1年起
2024年阿里云双十一活动已启动,提供云服务器79元1年起等特惠,涵盖云数据库、对象存储、无影云电脑等140余款产品免费试用,企业用户还可申请百万补贴金及5亿算力补贴,助力优惠上云。
|
28天前
|
域名解析 缓存 网络协议
Windows系统云服务器自定义域名解析导致网站无法访问怎么解决?
Windows系统云服务器自定义域名解析导致网站无法访问怎么解决?
|
1月前
|
网络安全 Docker 容器
【Bug修复】秒杀服务器异常,轻松恢复网站访问--从防火墙到Docker服务的全面解析
【Bug修复】秒杀服务器异常,轻松恢复网站访问--从防火墙到Docker服务的全面解析
24 0

推荐镜像

更多