Netty总结
Netty
定义
Netty是一个基于NIO的异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端
使用场景
- 需要自定义网络协议
- 需要自己构建异步,高性能的基础通信框架
- 当第三方只有TCP,UDP接口
优点
- API简单,开发门槛低
- 功能强大,预置了多种编解码功能,支持多种主流协议
- 定制能力强,可以通过ChannelHandler对通信框架进行灵活扩展
- 性能高,通过与其他主流NIO框架对比,Netty的综合性能最优
- 成熟,稳定,社区活跃,在Doubbo中体现出
高性能表现
- Netty架构按照Reactor模式设计,基于NIO的IO线程模型,采用异步事件驱动,利用多路IO复用技术进行处理,与传统的多线程/多进程模型对比,减少了系统对线程和进程的维护,节省了系统资源
- 内存0拷贝实现,Netty接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行socket读写,不需要进行字节缓冲区的二次拷贝,传统使用的堆内存(HEAP BUFFERS)JVM会将堆内存Buffer拷贝一份到直接内存中然后才写入socket中,相比于堆外直接内存多了一次缓冲区的内存拷贝
- 串行无锁化设计,一个消息在一个线程内处理完成,期间不进行线程切换,避免多线程竞争和同步锁,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程来并行运行
BIO,NIO,AIO的区别
BIO:一个连接一个线程,客户端有连接请求时服务端就需要启动一个线程进行处理,线程开销大
NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理
AIO:一个有效请求一个线程,客户端的I/O请求都是有OS先完成了再通知服务器应用去启动线程进行处理
BIO是面向流的,NIO是面向缓冲区的;BIO的各种流是阻塞的,而NIO是非阻塞的;BIO的stream是单向的,而NIO的channel是双向的
NIO的特点:事件驱动模型,单线程处理多任务,非阻塞I/O,I/O读写不再阻塞,而是返回0,基于block的传输比基于流的传输更高效,更高级的IO函数zero-copy,IO多路复用大大提高了java网络应用的可伸缩性和实用性,基于Reactor
在Reactor模式中,事件分发器等待某个事件或某个操作的状态发生,事件分发器就把这个事件分发给实现注册的事件处理函数或者回调函数,由后者来做实际的读写操作
NIO的组成
Channel:表示IO源与目标打开的连接,是双向的,不能直接访问数据,只能与Buffer进行交互
Buffer:与Channel进行交互,数据从Channel读入缓冲区,从缓冲区写入channel中
Selector:可使一个单独的线程管理多个Channel,监听读,写,连接,accept,注册等事件类型
DirectByteBuffer:堆外内存,可以减少一次从系统空间到用户空间的拷贝,
HeapBuffer:堆内存,由JVM进行管理,建议在数据量比较小的情况下使用
filp方法:切换读写模式,反转缓冲区
clear方法:清除缓冲区
I/O模型
传统I/O模型
- 一个客户端对应一个线程
- 一个线程处理读取数据,处理数据,响应数据,线程之间互不干扰
- 如果客户端长时间没有请求数据时,线程会处于堵塞状态,一直阻塞于read方法
- 如果客户端数量很大,会增加CPU的工作压力
模型优化
I/O多路复用
多路复用器专门负责客户端请求的监听,读取数据,分发数据,但是不负责具体的业务处理,相比于上一个线程模型,应用程序只需要阻塞一个地方即可,不用阻塞每个线程的read方法
线程池复用线程资源
通过线程池来处理业务,响应业务,处理完成之后释放线程,好处在于无需针对每个客户端独立创建子线程,线程池不会阻塞
单Reactor单线程
Select是I/O多路复用模型的标准网络API,实现通过一个阻塞对象监听多路连接请求,收到请求后通过Dispatch进行分发;
如果事件是连接请求,则由Acceptor进行处理,如果是普通业务类型,则交给Handler处理,由Handler来处理数据,业务处理,响应数据
-
如果客户端数量多,负责处理客户端请求的Reactor将无法支撑
-
负责处理业务的Handler只有一个,也就是只有一个子线程处理业务,无法很好的利用多核CPU的性能
-
如果Reactor出现问题,那么整个系统将都会收到影响,造成通讯故障
单Reactor多线程
相比单线程模型,Handler将不负责具体的业务处理,而是通过read()方法来读取数据,将再次分发给线程池来处理,线程池分配一个子线程来处理具体的业务,处理完成之后再把结果返回给Handler,最后释放连接给连接池
- 充分利用多核CPU的资源,提高处理任务的性能
- 把业务处理交给线程池,避免了因为某个业务处理或某次业务处理太慢导致其他业务受到影响
- 单Reactor处理所有事件的监听,响应,都是单线程,在高并发的情况,处理业务的线程可能正常,但Reactor就会容易受到性能瓶颈
- 如果Reactor出现故障,那么整个通讯就会故障
主从Reactor多线程
相比单Reactor多线程,主从Reactor多了多个子Reactor,主Reactor只负责通过Acceptor处理连接事件,处理完成之后交给子Reactor处理,然后由子Reactor将连接添加到连接队列里面进行监听,再创建Handler处理对应的事件,Handler再交给线程池来进行处理
- 责任明确,单一功能拆分的更细,处理效率很高
- 编程复杂度高
Netty线程模型
Netty线程模型比主从Reactor线程模型还要复杂
-
BossGroup负责处理接收客户端连接,WorkGroup负责处理客户端请求处理,默认都是使用NioEventLoopGroup
-
NioEventLoopGroup是一个定时任务线程池,NioEventLoop是真正执行工作的线程
-
BossGroup的NioEventLoopGroup分别执行三个步骤
- 每个NioEventLoopGroup都有一个Selector,并且轮训accept事件
- 处理accept事件,与客户端建立连接,生成NioSocketChannel,并且将其注册到某个WorkerGroup下的NioEventLoop上的Selector上
- 处理交任务队列中的任务,即runAllTasks
-
每个WorkerGroup的NioEventLoop分别循环执行三个操作
- 轮训read和write事件
- 处理I/O事件,并在其对应的NioSocketChannel处理
- 处理任务队列里的任务,即runAllTasks
Bootstrap
配置Netty程序,串联各个组件,Bootstrap是客户端的引导类,ServerBootstrap是服务端的引导类
Future,ChannelFuture
注册监听事件,当操作成功或失败时监听会自动触发注册的监听类型
Channel
Netty的网络通信组件,客户端和服务端建立连接后会维持一个channel,里面包括了基本的I/O操作,如bind,connect,read,write等,一个客户端对应一个Channel;
EventLoopGroup
是一组EventLoop的抽象,可以简单理解为线程池,一般分为BossEventLoopGroup和WorkEventLoopGroup
NioEventLoopGroup
管理EventLoop的生命周期,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应一个线程
NioEventLoop
监听端口,处理端口事件,将其分发,处理队列事件,维护一个线程和任务队列,支持异步提交执行任务,任务分IO任务和非IO任务,两种任务的执行时间由ioRatio控制,默认50,表示相等
核心方法run是一个死循环,主要操作有三个操作
- select(curDeadlineNanos)监听客户端事件
- processSelectedKeys:将监听到的客户端事件进行分发
- runAllTasks:执行队列里面的任务
@Override
protected void run() {
int selectCnt = 0;
for (;;) {
try {
int strategy;
try {
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// fall-through to SELECT since the busy-wait is not supported with NIO
case SelectStrategy.SELECT:
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
try {
if (!hasTasks()) {
strategy = select(curDeadlineNanos);
}
} finally {
// This update is just to help block unnecessary selector wakeups
// so use of lazySet is ok (no race condition)
nextWakeupNanos.lazySet(AWAKE);
}
// fall through
default:
}
} catch (IOException e) {
// If we receive an IOException here its because the Selector is messed up. Let's rebuild
// the selector and retry. https://github.com/netty/netty/issues/8566
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
boolean ranTasks;
if (ioRatio == 100) {
try {
if (strategy > 0) {
processSelectedKeys();
}
} finally {
// Ensure we always run tasks.
ranTasks = runAllTasks();
}
} else if (strategy > 0) {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else {
ranTasks = runAllTasks(0); // This will run the minimum number of tasks
}
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;
} else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
selectCnt = 0;
}
} catch (CancelledKeyException e) {
// Harmless exception - log anyway
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
selector, e);
}
} catch (Error e) {
throw e;
} catch (Throwable t) {
handleLoopException(t);
} finally {
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Error e) {
throw e;
} catch (Throwable t) {
handleLoopException(t);
}
}
}
}
Selector
多路IO复用的实现,高性能的关键,通过Selector,一个线程可以监听多个Channel事件,内部通过不断查询多个channel实现
ByteBuf
ByteBuf是一个字节容器,提供了常见的API,Netty是面向ByteBuf来传输数据的
分类
- Pooled和Unpooled,池化和非池化
- Heap和Direct,堆内存和直接内存
- Safe和Unsafe,安全和非安全
池化和非池化:池化就是用完就放回池子里面,比如我们所熟悉的数据库连接池。非池化就是每次使用都重新创建,使用完成则立马销毁。从性能的角度来说,池化会比非池化相对高,因为可以重复利用,避免每次都重新创建。
堆内存和直接内存:堆内存是 JVM 内部开辟的一块内存空间,它的生命周期受到 JVM 来管理,不容易造成内存溢出的情况。直接内存则是直接受操作系统管理了,如果数据量很大的情况,容易造成内存溢出情况。
安全和非安全:主要是 Java 操作底层操作数据的一种安全和非安全的方式。
实现类
- 池化+堆内存:PooledHeapByteBuf;
- 池化+直接内存:PooledDirectByteBuf;
- 池化+堆内存+不安全:PooledUnsafeHeapByteBuf;
- 池化+直接内存+不安全:PooledUnsafeDirectByteBuf;
- 非池化+堆内存:UnpooledHeapByteBuf;
- 非池化+直接内存:UnpooledDirectByteBuf;
- 非池化+堆内存+不安全:UnpooledUnsafeHeapByteBuf;
- 非池化+直接内存+不安全:UnpooledUnsafeDirectByteBuf;
ChannelFuture
Netty所有IO操作都是异步的,通过注册监听器来监听执行结果的返回
核心方法
-
sync:等待异步操作执行完毕,会抛出异常
-
await:等待异步操作执行完毕,不会抛出异常,主线程无法捕获子线程抛出的异常
-
isDone:任务是否执行完成
-
isSuccess:任务是否执行成功
-
isCancelled:任务是否被取消
-
cause:获取执行异常信息
ChannelFuture channelFuture=bootstrap.connect("127.0.0.1",80);
channelFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
if(future.isDone()){
if(future.isSuccess()){
System.out.println("执行成功...");
}else if(future.isCancelled()){
System.out.println("任务被取消...");
}else if(future.cause()!=null){
System.out.println("执行出错:"+future.cause().getMessage());
}
}
}
});
ChannelHandlerContext
保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象(自定义Hanndler)
ChannelPipeline
管理ChannelHandler链的容器,底层采用责任链模式,保存ChannelHandler的双向链表,里面一个节点就是一个ChannelHandlerContext,用于处理或拦截Channel的入站事件和出站操作,ChannelPipline实现了一种了高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及ChannelHandler如何相互交互
-
入站:事件从链表的头部一直往后执行,ChannelInboundHandlerAdapter子类会被触发;
-
出站:事件从链表的尾部一直往前执行,ChannelOutboundHandlerAdpater子类会被触发;
-
核心方法
- addFirst(): 添加ChannelHandler在ChannelPipeline的第一个位置
- addBefore():在ChannelPipline中指定ChannelHandler名称之前添加ChannelHandler
- addAfter():在ChannelPipline中指定ChannelHandler名称之后添加ChannelHandler
- addLast():在ChannelOPipline的末尾添加ChannelHandler
- remove():删除ChannelPipline中指定的ChannelHandler
- replace():替换ChannelPipline中的ChannelHandler
- first():获取链表当中的第一个节点
- last():获取链表当中的最后一个节点
-
Inbound执行顺序:
- 按添加时的顺序执行,通过手动调用super.channelRead(ctx,msg)传递给下一个handler
-
outbound执行顺序:
- 按添加时的顺序相反执行,通过手动调用ctx.channel().writeAndFlush()向下一个传递
ChannelHandler
Handler的顶层接口,自定义业务的Hanndler需要实现他的子类,提供了一整套生命周期方法,处理连接,数据接收,异常,数据转换等IO事件或者IO拦截事件,将其转发到ChannelPipline中的下一个处理程序
核心方法
- channelActive:客户端建立连接成功调用,只会调用一次,一般用来做初始化工作,登录认证等
- channelInactive:连接断开时触发,无论是客户端或者服务端都会监听到事件
- channelRead:当Channel上面有数据时触发
- channelReadComplete:当read0个字节或read到的字节数小于Buffer的容量时触发
- exceptionCaught:发生异常时触发
优化
@Shareable
客户端每次连接都会创建Handler对象,添加到链表节点中,如果使用@Shareble,那么Handler就会变成共享,也就是所有客户端都会这一个对象,共享Handler,很容易出现线程安全问题,注意不要使用全局变量,全局静态变量
ChannelInboundHandler
是ChannelHandler的子类,用于处理入站IO操作
ChannelOutboundHandler
是ChannelHandler的子类,用于处理出站IO操作
ChannelInboundHandlerAdapter
用于处理入站IO事件,因为直接实现ChannelInbound需要实现的接口非常多,所以Netty封装了这个实现类,简化了开发工作
核心方法:
- handlerAdded:handler被加入Pipline时触发,表示channel中已经成功添加了一个handler到双向链表中
- channelRegistered:channelRegistered注册成功时触发,表示channle已经和Nio中的一个线程完成了绑定
- channelActive: channel连接就绪时触发:表示channel中的所有业务逻辑都准备完毕
- channelRead: channel有数据可读时触发
- channelReadComplete:有数据可读,并且读完时触发
- channelInactive:channel断开时触发,表示TCP层面的连接已经关闭,不再是ESTABLISH状态
- channelUnregistered:channel取消注册时触发,表示channel与Nio中的一个线程解除绑定
- handlerRemoved: handler被从Pipline移除时触发,表示这条连接上的所有连接都已经移除掉
channel在创建时需要绑定channelPipline和EventLoop等操作,完成这些操作时会触发channelRegistered方法
ChannelOutboundHandlerAdapter
用于处理出站IO事件
ChannelDuplexHandler
用于处理入站和出站事件
SimpleChannelInboundHandler
根据客户端提交的参数类型,自动流转到指定的Handler处理
常用组件详解
Channel
常用操作
检查当前网络连接的通道状态
ctx.channel().isOpen();
配置网络连接的参数
提供异步的网络IO操作(建立连接,读写,绑定端口)
关联IO操作对应的处理程序
常见类型
- NioSocketChannel:异步的客户端TCP Socket连接
- NioServerSocketChannel:异步的服务器端TCP Socket连接
- NioDatagramChammel:异步的UDP连接
- NioSctpChannel:异步的客户端Sctp连接
- NioSctpServerChannel:异步的Sctp服务器端连接这些通道涵盖了UDP和TCP网络IO以及文件IO
内置编解码器
MessageToByteEncoder
把消息内容转换成Byte,也就是编码
子类
- SimpleChannelInboundHandler
:由Decoder进行基础帧解码后合并为一个自定义完整包进行后续处理。
ByteToMessageDecoder
把Byte类型的数据转换成对应的实体数据对象,也就是解码
子类
- LineBasedFrameDecoder:通过换行符进行拆包粘包处理,\n或者\r\n
- DelimiterBasedFrameDecoder:通过指定分隔符进行粘包拆包处理
- FixedLengthFrameDecoder:通过固定长度进行粘包拆包处理
常用场景
创建UDP服务端
@Slf4j
public class UDPServer {
public void bind(int port){
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new ChannelInitializer<NioDatagramChannel>() {
@Override
protected void initChannel(NioDatagramChannel ch) {
ch.pipeline().addLast(new NioEventLoopGroup(),new UDPHandler());
}
});
try {
ChannelFuture f = bootstrap.bind(port).sync();
if(f.isSuccess()){
log.info("udp服务启动成功,端口:{}",port);
/**
* 等待服务器监听端口关闭
*/
try {
f.channel().closeFuture().sync();
}catch (InterruptedException e){
log.warn("udp服务关闭异常:{}",e.getMessage());
}finally {
group.shutdownGracefully();
}
}else{
bootstrap.config().group().schedule(() -> bind(port),5, TimeUnit.SECONDS);
log.error("创建udp服务失败:{}",f.cause().getMessage());
}
} catch (InterruptedException e) {
log.warn("udp服务启动异常:{}",e.getMessage());
}
}
}
创建UDP客户端
@Slf4j
public class UDPServer {
public void bind(int port){
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new ChannelInitializer<NioDatagramChannel>() {
@Override
protected void initChannel(NioDatagramChannel ch) {
ch.pipeline().addLast(new NioEventLoopGroup(),new UDPHandler());
}
});
try {
ChannelFuture f = bootstrap.bind(port).sync();
if(f.isSuccess()){
log.info("udp服务启动成功,端口:{}",port);
/**
* 等待服务器监听端口关闭
*/
try {
f.channel().closeFuture().sync();
}catch (InterruptedException e){
log.warn("udp服务关闭异常:{}",e.getMessage());
}finally {
group.shutdownGracefully();
}
}else{
bootstrap.config().group().schedule(() -> bind(port),5, TimeUnit.SECONDS);
log.error("创建udp服务失败:{}",f.cause().getMessage());
}
} catch (InterruptedException e) {
log.warn("udp服务启动异常:{}",e.getMessage());
}
}
}
创建TCP服务端
@Slf4j
public class TCPServer {
public void bind(int port) {
/**
* 配置服务端的NIO线程组
* NioEventLoopGroup 是用来处理I/O操作的Reactor线程组
* bossGroup:用来接收进来的连接,workerGroup:用来处理已经被接收的连接,进行socketChannel的网络读写,
* bossGroup接收到连接后就会把连接信息注册到workerGroup
* workerGroup的EventLoopGroup默认的线程数是CPU核数的二倍
*/
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
/**
* ServerBootstrap 是一个启动NIO服务的辅助启动类
*/
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
/**
* 设置group,将bossGroup, workerGroup线程组传递到ServerBootstrap
*/
serverBootstrap = serverBootstrap.group(bossGroup, workerGroup);
/**
* ServerSocketChannel是以NIO的selector为基础进行实现的,用来接收新的连接,这里告诉Channel通过NioServerSocketChannel获取新的连接
*/
serverBootstrap = serverBootstrap.channel(NioServerSocketChannel.class);
// option是设置 bossGroup,childOption是设置workerGroup
/**
* 服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝(队列被接收后,拒绝的客户端下次连接上来只要队列有空余就能连上)
*/
serverBootstrap = serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
/**
* 立即发送数据,默认值为Ture(Netty默认为True而操作系统默认为False)。
* 该值设置Nagle算法的启用,改算法将小的碎片数据连接成更大的报文来最小化所发送的报文的数量,如果需要发送一些较小的报文,则需要禁用该算法。
* Netty默认禁用该算法,从而最小化报文传输延时。
*/
serverBootstrap = serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
/**
* 连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。
* 可以将此功能视为TCP的心跳机制,默认的心跳间隔是7200s即2小时, Netty默认关闭该功能。
*/
serverBootstrap = serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
/**
* 设置 I/O处理类,主要用于网络I/O事件,记录日志,编码、解码消息
*/
serverBootstrap = serverBootstrap.childHandler(new BootNettyChannelInitializer<SocketChannel>());
/**
* 绑定端口,同步等待成功
*/
ChannelFuture f = serverBootstrap.bind(port).sync();
if(f.isSuccess()){
log.info("tcp服务启动成功,端口:{}",port);
/**
* 等待服务器监听端口关闭
*/
f.channel().closeFuture().sync();
}else{
serverBootstrap.config().group().schedule(() -> bind(port),5, TimeUnit.SECONDS);
log.error("创建tcp服务失败:{}",f.cause().getMessage());
}
} catch (InterruptedException e) {
log.error("tcp服务关闭异常:{}",e.getMessage());
} finally {
/**
* 退出,释放线程池资源
*/
try{
bossGroup.shutdownGracefully().sync();
workerGroup.shutdownGracefully().sync();
}catch (Exception e){
log.error("tcp服务退出异常:{}",e.getMessage());
}
}
}
}
创建TCP客户端
@Slf4j
public class TCPClient {
public void connect(String host,int port){
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new NioEventLoopGroup()) //线程模型
.channel(NioSocketChannel.class) //指定IO模型为NIO模型
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000) //连接超时时间
.option(ChannelOption.SO_KEEPALIVE,true) // 开启心跳机制
.option(ChannelOption.TCP_NODELAY,true) // 开启Nagle算法,即有数据就发送
.handler(new BootNettyChannelInitializer<SocketChannel>());
//开始连接
ChannelFuture future = bootstrap.connect(host, port);
future.addListener(future1 -> {
if(future1.isSuccess()){
log.info("连接ip:{},端口:{}成功",host,port);
}else{
log.warn("连接失败:{}",future1.cause().getMessage());
//添加失败重连,每隔5秒重连一次
EventLoopGroup eventLoopGroup = bootstrap.config().group();
eventLoopGroup.schedule(new Runnable() {
@Override
public void run() {
connect(host,port);
}
}, 5, TimeUnit.SECONDS);
}
});
}
}
创建WebSocket服务端
@Slf4j
public class WebSocketServer {
public void bind(int port){
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
//1.Netty提供的针对Http的编解码
pipeline.addLast(new HttpServerCodec());
//是以块方式写,添加ChunkedWriteHandler处理器
pipeline.addLast(new ChunkedWriteHandler());
//http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
pipeline.addLast(new HttpObjectAggregator(8192));
//将 http协议升级为 ws协议 , 保持长连接
pipeline.addLast(new WebSocketServerProtocolHandler("/"));
//2.自定义处理WebSocket的业务Handler
pipeline.addLast(new WebSocketHandler());
}
});
ChannelFuture f = serverBootstrap.bind(port).sync();
if(f.isSuccess()){
log.info("WebSocket服务启动成功,端口:{}",port);
/**
* 等待服务器监听端口关闭
*/
f.channel().closeFuture().sync();
}else{
serverBootstrap.config().group().schedule(new Runnable() {
@Override
public void run() {
bind(port);
}
},5, TimeUnit.SECONDS);
log.error("创建WebSocket服务失败:{}",f.cause().getMessage());
}
} catch (InterruptedException e) {
log.warn("创建WebSocketServer异常:{}",e.getMessage());
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
创建MQTT服务端
@Slf4j
public class MQTTServer {
private NioEventLoopGroup bossGroup;
private NioEventLoopGroup workGroup;
public void bind(int port) {
try {
bossGroup = new NioEventLoopGroup(1);
workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.option(ChannelOption.SO_RCVBUF, 10485760);
bootstrap.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ChannelPipeline channelPipeline = ch.pipeline();
// 设置读写空闲超时时间
channelPipeline.addLast(new IdleStateHandler(600, 600, 1200));
channelPipeline.addLast("encoder", MqttEncoder.INSTANCE);
channelPipeline.addLast("decoder", new MqttDecoder());
channelPipeline.addLast(new MQTTHandler());
}
});
ChannelFuture f = bootstrap.bind(port).sync();
if (f.isSuccess()) {
log.info("MQTT服务启动成功,端口:{}", port);
/**
* 等待服务器监听端口关闭
*/
f.channel().closeFuture().sync();
} else {
bootstrap.config().group().schedule(() -> bind(port), 5, TimeUnit.SECONDS);
log.error("创建MQTT服务失败:{}", f.cause().getMessage());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
自定义编码器
@Slf4j
public class MyMessageEncoder extends MessageToByteEncoder<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
//进行具体的编码处理 这里对字节数组进行打印
log.info("编码器收到数据:{}",msg);
MyMessageProtocol protocol = new MyMessageProtocol();
protocol.setTime(DateTime.now().toStringDefaultTimeZone());
protocol.setId(RandomUtil.randomInt());
protocol.setBody(msg);
//写入并传送数据
out.writeBytes(JSONUtil.toJsonPrettyStr(protocol).getBytes());
}
}
自定义解码器
@Slf4j
public class MyMessageDecoder extends ByteToMessageDecoder{
/**
* 协议开始符
*/
private String prefix = "{";
/**
* 协议结束符
*/
private String suffix = "}";
/**
* 测试数据
* {
* "id": 1,
* "time": "2023-02-01 23:36:24",
* "body": "l0h58jygus"
* }
* @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
* @param buffer the {@link ByteBuf} from which to read data
* @param out the {@link List} to which decoded messages should be added
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out){
try {
if (buffer.readableBytes() > 0) {
// 待处理的消息包
byte[] bytesReady = new byte[buffer.readableBytes()];
buffer.readBytes(bytesReady);
String s = new String(bytesReady);
if(s.startsWith(prefix) && s.endsWith(suffix)){
MyMessageProtocol protocol = JSONUtil.toBean(s, MyMessageProtocol.class);
out.add(protocol);
}else{
log.info("收到客户端数据:{},格式错误无法解码",s);
}
}
}catch(Exception ex) {
log.error("解码异常:{}", ex.getMessage());
}
}
}
自定义心跳
@Slf4j
public class PingPangHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent)evt;
if(event.state() == IdleState.WRITER_IDLE){
log.info("有一段时间没写入数据了");
}else if(event.state() == IdleState.READER_IDLE){
log.info("有一段时间没读到数据了");
}else if(event.state() == IdleState.ALL_IDLE){
log.info("有一段时间没读写到数据了");
}
}else{
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
MyMessageProtocol data = (MyMessageProtocol) msg;
if(data.getBody().equals("pingPang")){
log.info("收到心跳包数据");
}else{
log.info("不是心跳包,传递给下一个handler");
ctx.fireChannelRead(msg);
}
}
}
断线重连机制
//开始连接
ChannelFuture future = bootstrap.connect(host, port);
future.addListener(future1 -> {
if(future1.isSuccess()){
log.info("连接ip:{},端口:{}成功",host,port);
}else{
log.warn("连接失败:{}",future1.cause().getMessage());
//添加失败重连,每隔5秒重连一次
EventLoopGroup eventLoopGroup = bootstrap.config().group();
eventLoopGroup.schedule(new Runnable() {
@Override
public void run() {
connect(host,port);
}
}, 5, TimeUnit.SECONDS);
}
});