Netty自学实践
最近遇到自主研发产品涉及到TCP通信方面的问题所以才开始自学的Netty,既然要学就要学的好一点,不能只是简单的将网上的代码直接copy搬过来直接使用,如果遇到bug处理起来还是比较棘手的,原因很简单,并不了解Netty框架,只会copy,对于解决问题的效率会有非常大的影响,虽然Netty初次使用的时候是在2023/06月份,现在打算再复习深入一下,我希望将复习的内容的百分之五十的内容copy到我们的脑子面,我就知足了,学无止境接下来开始整活
学习Netty太难受了,看源码没有几行注释,呜呜~~~
Netty实操
开发一个简单的服务端可客户端
需求:
客户端项服务端发送hello,word!的内容
服务器只是接收不进行返回的操作
需要引入的依赖是
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.43.Final</version>
</dependency>
书写服务端代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
/**
* @program: Netty-Hello-Word
* @description: 服务器端
* @author: wsw
* @create:
**/
public class HelloWordService {
public static void main(String[] args) {
new ServerBootstrap()//服务器端启动器将下面的所有的组件组装,进行协调工作启动服务器
.group(new NioEventLoopGroup())//加入事件循环组里面包含了线程和选择器 循环有没有什么新的事件
.channel(NioServerSocketChannel.class)//选择服务器的通道 OIO BIO
.childHandler(//boss处理连接 worker(child)负责处理读写决定的 worker(child)能执行那些操作(handler)
new ChannelInitializer<NioSocketChannel>() {//代表和客户端进行数据读写的通道 以及对于handler进行初始化负责添加别的handler
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {//在连接建立通道以后会进行执行下面的handler
//添加具体的handler
nioSocketChannel.pipeline().addLast(new StringDecoder());//new StringDecoder()作为解码将ByteBuf字节转为字符串
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {//自定义的handler进行处理
//处理读事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//打印上一步转换好的字符串
System.out.println("msg = " + msg);
}
});
}
})
.bind(6001);//绑定服务器的监听端口号
}
}
书写客户端代码
/**
* @program: Netty-Hello-Word
* @description: 客户端
* @author: wsw
* @create:
**/
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//创建启动器类
new Bootstrap()
.group(new NioEventLoopGroup())//加入事件循环组里面包含了线程和选择器
.channel(NioSocketChannel.class)//选择客户端channel实现
.handler(new ChannelInitializer<NioSocketChannel>() {//选择处理器
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {//再建立连接的时候进行调用
nioSocketChannel.pipeline().addLast(new StringEncoder());//编码器,将字符串编码为字节码
}
})
.connect(new InetSocketAddress("localhost", 6001))//连接服务器
.sync()//阻塞方法,直到连接建立
.channel()//客户端和服务器端的之间的连接对象
.writeAndFlush("Hello Word!");//写出数据
//这里不管发送或者接收消息,都会走到handler里面这里相当于就是走到了new StringEncoder()里面进行将Hello Word!转换为ByteBuf
}
}
– | 解释说明 |
---|---|
channel | 理解为数据通道 |
msg | 理解为流动的数据,最开始输入的时候ByteBuf,但是经过pipeline的加工以后,会变成其他类型的对象,最后输出右边为了ByteBuf |
handler | 可以理解为数据处理工序,工序有多道,合在一起就是plpeline,plpeline负责发布事件(读,读取完成…)传播给每个handler,handler对自己感兴趣的时间进行处理(重写了相对应的事件处理方法),handler是分为Inbound和Outbound二类 |
EnventLoop | 可以理解为是处理数据的工人,可以管理多个channel的IO操作,并且一旦负责某个channel,就要负责到底(绑定),也可以执行IO操作,也可以进行任务处理,每个EnventLoop有任务队列,任务队列里面可以堆放多个channel的待处理任务,任务分为普通任务,定时任务,EnventLoop按照plpeline顺序,依次按照handler的规划(代码)处理数据,可以为每道工序指定不同的工人 |
EnventLoop
事件循环对象
EnventLoop的本质是一个单线程的执行器(同时维护一个Selector),里面有run方法进行处理Channel上源源不断的IO事件.
继承的关系
- 一条线是继承来自java.util.concurrent.ScheduledExecutorService 因此包含了线程池中的所有的方法
- 另一条线继承的是来自netty自己的OrderedEventExecutor,
(1). 提供了boolean inEvenLoop(Thread thread)方法判断一个线程是否哦属于此EnventLoop
(2). 提供了parent方法来看看自己属于哪个EnventLoopGroup
EventLoopGroup
事件循环组
EventLoopGroup是一组EventLoop,Channel一般会调用EventLoopGroup的register方法来绑定其中一个EventLoop后续这个Channel上的IO事件都由此EventLoop来进行处理(保证了IO事件处理时的线程安全)
- 继承自nettyEventLoopGroup
(1). 实现iterable接口提供遍历EventLoop的能力
(2). 另有next方法获取集合中下一个EventLoop
NioEventLoopGroup的默认线程数是多少,或者是默认创建几个事件循环
看源码
源码看完了看实操
import io.netty.channel.DefaultEventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.NettyRuntime;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @program: Netty-Hello-Word
* @description: 事件循环
* @author: wsw
* @create:
**/
@Slf4j
public class TestEventLoop {
public static void main(String[] args) {
//创建时间循环组
//NioEventLoopGroup groupNio = new NioEventLoopGroup();//io事件 普通任务 定时任务
//DefaultEventLoop groupDefault = new DefaultEventLoop();//普通任务 定时任务
NioEventLoopGroup groupNio1 = new NioEventLoopGroup(2);//io事件 普通任务 定时任务
//获取当前系统的CPU物理核心数
System.out.println("当前系统的CPU物理核心数 = " + NettyRuntime.availableProcessors());
//获取下一个事件循环对象
System.out.println("groupNio1.next() = " + groupNio1.next());
System.out.println("groupNio1.next() = " + groupNio1.next());
System.out.println("groupNio1.next() = " + groupNio1.next());
System.out.println("groupNio1.next() = " + groupNio1.next());
//执行普通任务--这里相当于就是一个异步处理交给这里的2个线程进行处理
groupNio1.next().submit(() -> {
//获取当前的线程
System.out.println("当前线程 = " + Thread.currentThread());
System.out.println("当前线程 = " + Thread.currentThread());
});
System.out.println("main = " + Thread.currentThread());
//执行定时任务
groupNio1.next()
.scheduleAtFixedRate(
() -> {
System.out.println("当前线程 = " + Thread.currentThread());
},
0,//初始延迟事件,0表示立刻执行操作
1,//间隔时间
TimeUnit.SECONDS);//时间单位
}
}
接下来看看处理IO事件
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.nio.charset.Charset;
/**
* @program: Netty-Hello-Word
* @description: 处理IO事件,执行这个方法以后可以再客户端那边进行打断点进行发送数据到这边来
* @author: wsw
* @create:
**/
public class EventLoopServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(
new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将消息转换为 msg是一个ByteBuf类型指的是字节数组
ByteBuf buf = (ByteBuf) msg;
System.out.println("buf = " + Thread.currentThread() + "[空]" + buf.toString(Charset.defaultCharset()));
}
}
);
}
})
.bind(6001);
}
}
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.nio.charset.Charset;
/**
* @program: Netty-Hello-Word
* @description: 处理IO事件
* @author: wsw
* @create:
**/
public class EventLoopServer {
public static void main(String[] args) {
/**
* 当我们的netty服务端启动后,就可以对外服务了,此时客户端会发送一个请求过来,这个时候会有对应的线程进行处理,第一个阶段就是accept过程,它为后面的read和write做准备,
*/
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup(2))//第一个参数指的是boss只处理ServerSocketChannel 上的 accept事件 第二个参数指的是worker只负责处理socketChannel上的读写操作
/**
* 这里的第一个参数为什么不需要进行设置主要就是因为我们的服务端只有一个进行接收做所以这里不需要进行设置,我们只有本一个服务器进行处理accept事件
*/
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(
new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将消息转换为 msg是一个ByteBuf类型指的是字节数组
ByteBuf buf = (ByteBuf) msg;
System.out.println("buf = " + Thread.currentThread() + "[空]" + buf.toString(Charset.defaultCharset()));
}
}
);
}
})
.bind(6001);
}
}
我们的handler的执行时间比较长,可能不是仅仅只是打印,nio的线程耗时非常长就会影响到其他客户端的读写操作,有可能一个worker管理的许多的客户端
所以针对某刻个handler占用时间过长不要影响到worker的操作
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.nio.charset.Charset;
/**
* @program: Netty-Hello-Word
* @description: 处理IO事件
* @author: wsw
* @create:
**/
public class EventLoopServer {
public static void main(String[] args) {
//创建一个独立EventLoopGroup
EventLoopGroup group = new DefaultEventLoopGroup(3);
/**
* 当我们的netty服务端启动后,就可以对外服务了,此时客户端会发送一个请求过来,这个时候会有对应的线程进行处理,第一个阶段就是accept过程,它为后面的read和write做准备,
*/
new ServerBootstrap()
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))//第一个参数指的是boss只处理ServerSocketChannel 上的 accept事件 第二个参数指的是worker只负责处理socketChannel上的读写操作
/**
* 这里的第一个参数为什么不需要进行设置主要就是因为我们的服务端只有一个进行接收做所以这里不需要进行设置,我们只有本一个服务器进行处理accept事件
*/
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline()
.addLast("handlerNotImportant",
new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将消息转换为 msg是一个ByteBuf类型指的是字节数组
ByteBuf buf = (ByteBuf) msg;
System.out.println("buf = " + Thread.currentThread() + "[空]" + buf.toString(Charset.defaultCharset()));
ctx.fireChannelRead(msg);//让消息传递给下一个handler
}
}
)
.addLast(group, "handlerImportant",
new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将消息转换为 msg是一个ByteBuf类型指的是字节数组
ByteBuf buf = (ByteBuf) msg;
System.out.println("buf = " + Thread.currentThread() + "[空]" + buf.toString(Charset.defaultCharset()));
}
}
);
}
})
.bind(6001);
}
}
多个handler之间如果用的是不同的EvenLoopGroup是如何进行线程的切换
进行切换
io.netty.channel.AbstractChannelHandlerContext
public ChannelHandlerContext fireChannelRead(Object msg) {
invokeChannelRead(this.findContextInbound(32), msg);
return this;
}
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
//下一个handler的事件循环是否与当前的时间群欢是在同一个线程中
EventExecutor executor = next.executor();
//是直接进行调用,当前线程是否和下一哥handler是同一个线程
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
//不是,将要执行的代码将会作为任务提交到下一个时间循环处理中
executor.execute(new Runnable() {
//executor指的是下一个handler
public void run() {
next.invokeChannelRead(m);
}
});
}
}
Channel
Channel 代表网络 Socket 或能够进行 I/O 操作的组件的关系。这些 I/O 操作包括读、写、连接和绑定。,就是代表连接,实体之间的连接,程序之间的连接,文件之间的连接,设备之间的连接。同时它也是数据入站和出站的载体。简而言之比喻为是一个管道.
特点
特点 | 解释 |
---|---|
I/O 操作都是异步的 | Channel中的所有 I/O 操作都是异步的,一经调用就马上返回,而不保证所请求的 I/O 操作在调用结束时已完成。相反,在调用时都将返回一个 ChannelFuture实例,用来代表未来的结果。该实例会在 I/O 操作真正完成后通知用户,然后就可以得到具体的 I/O操作的结果。 |
Channel 是分层的 | 一个 Channel会有一个对应的parent,该parent也是一个Channel。并且根据 Channel创建的不同,它的parent也会不一样。例如,在一个SocketChannel连接上ServerSocketChannel之后,该SocketChannel的parent就会是该ServerSocketChannel。层次的结构的语义取决于Channel使用了何种传输实现方式。 |
向下转型以下访问特定于输出的操作 | 某些传输公开了特定于该传输的相关操作,因此可以将该Channel向下转换为子类型以调用此类操作。 |
释放资源 | 一旦Channel完成,调用ChannelOutboundInvoker.close()或ChannelOutboundInvoker.close(ChannelPromise)来释放所有自恋是非常重要的。这样可以确保以适当的方式(以文件句柄)释放所有的资源。 |
下面主要说的还是api
io.netty.channel.Channel
方法 | 解释说明 |
---|---|
close() | 可以用来关闭channel,回收资源,该通道的生命周期完全结束 |
closeFuture() | 用来处理channel的关闭 |
sync() | 方法作用是同步等待channel关闭 |
addListener() | 是异步等待channel关闭 |
pipeline() | 添加处理器 |
write() | 方法将数据写入,相当于当前只是先把数据放在channel的缓存区中 |
flush() | 和write()方法搭配使用,将缓存数据刷出 |
writeAndFlush() | 将数据写入并且刷出 |
id() | 返回全局唯一的channel id |
eventLoop() | 返回该通道注册的事件轮询器 |
parent() | 返回该通道的父通道,如果是ServerSocketChannel实例则返回null,SocketChannel实例则返回对应的ServerSocketChannel |
config() | 返回该通道的配置参数 |
isOpen() | 端口是否处于open,通道默认一创建isOpen方法就会返回true,close方法被调用后该方法返回false |
isRegistered() | 是否已注册到EventLoop |
isActive() | 通道是否处于激活 |
metadata() | 返回channel的元数据 |
localAddress() | 服务器的ip地址 |
remoteAddress() | 客户端的ip地址 |
closeFuture() | 通道的关闭凭证(许可),这里是多线程编程一种典型的设计模式,一个channle返回一个固定的 |
isWritable() | 是否可写,如果通道的写缓冲区未满,即返回true,表示写操作可以立即 操作缓冲区,然后返回。 |
alloc() | 返回ByteBuf内存分配器 |
register(EventLoop var1, ChannelPromise var2) | 把channel注册进EventLoop |
bind(SocketAddress var1, ChannelPromise var2) | 给channel绑定一个 adress, |
connect(SocketAddress var1, SocketAddress var2, ChannelPromise var3) | Netty客户端连接到服务端 |
disconnect(ChannelPromise var1) | 断开连接,但不会释放资源,该通道还可以再通过connect重新与服务器建立连接 |
deregister(ChannelPromise var1) | 取消注册 |
beginRead() | 从channel中读取IO数据 |
ChannelFuture
ChannelFuture连接问题(异步非阻塞问题)
首先connect是一个异步非阻塞的线程,不关心结果,不需要等待结果返回直接往下执行代码,当前调用connect方法的是main主线程,真正执行连接connect是nio线程(new NioEventLoopGroup()),
所以如果这个时候下面不执行sync()方法的时候
就会造成直接往下执行获取通道channel了,但是这个时候channel有可能还没有建立连接完成
科普:
之后再Netty里面看到带有Future/Promise的类型的都是和异步方法配套进行使用的,用来处理结果的
使用异步处理结果使用addListener(回调对象)方法异步处理结果
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
/**
* @program: Netty-Hello-Word
* @description: 客户端
* @author: wsw
* @create:
**/
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//创建启动器类
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())//加入事件循环组里面包含了线程和选择器
.channel(NioSocketChannel.class)//选择客户端channel实现
.handler(new ChannelInitializer<NioSocketChannel>() {//选择处理器
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {//再建立连接的时候进行调用
nioSocketChannel.pipeline().addLast(new StringEncoder());//编码器,将字符串编码为字节码
}
})
//连接服务器,首先connect是一个异步非阻塞的线程,不关心结果,不需要等待结果返回直接往下执行代码,当前调用connect方法的是main主线程,真正执行连接connect是nio线程(new NioEventLoopGroup()),
.connect(new InetSocketAddress("localhost", 6001));
//1.使用sync方法同步处理结果
//channelFuture.sync();//阻塞方法,直到连接建立
//Channel channel= channelFuture.channel();//客户端和服务器端的之间的连接对象
//channel.writeAndFlush("Hello Word!");//写出数据
//2.使用addListener(回调对象)方法异步处理结果,就不是交给main主线程进行处理了
channelFuture.addListener(new ChannelFutureListener() {
//再nio线程连接建立好以后,会进行调用operationComplete
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
Channel channel = channelFuture.channel();//客户端和服务器端的之间的连接对象
channel.writeAndFlush("Hello Word!");//写出数据
}
});
}
}
总结:Netty还在学习中本文章后期会进行优化一下现在的观点,
关于现在可以多尝试使用异步线程的方式,这里的同步线程和异步线程,并不是异步线程使用的是多线程的原因,首先有下面几个观点:
1.异步,可以使系统的吞吐量得到提升及时的处理多个信息
2.异步处理需要利用好多线程,合理的任务拆分才能达到提高效率的问题
3.异步处理并没有达到缩短响应时间的这个需要注意
ChannelFuture关闭问题
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
import java.util.Scanner;
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//创建启动器类
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())//加入事件循环组里面包含了线程和选择器
.channel(NioSocketChannel.class)//选择客户端channel实现
.handler(new ChannelInitializer<NioSocketChannel>() {//选择处理器
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {//再建立连接的时候进行调用
nioSocketChannel.pipeline().addLast(new StringEncoder());//编码器,将字符串编码为字节码
}
})
//连接服务器,首先connect是一个异步非阻塞的线程,不关心结果,不需要等待结果返回直接往下执行代码,当前调用connect方法的是main主线程,真正执行连接connect是nio线程(new NioEventLoopGroup()),
.connect(new InetSocketAddress("localhost", 6001));
//我想要在关闭的之后做一些事情咋搞哦? 迷茫
Channel channel = channelFuture.sync().channel();
new Thread(() -> {
Scanner scanner = new Scanner(System.in);//键盘输入
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
channel.close();//close()方法指的是异步操作,指派给其他的线程进行关闭的,烦人,烦烦烦到处都是异步
System.out.println("我来做最后的关闭后的处理工作");//实际上是有可能还没有进行close还没有执行完以后就执行到这一步了,抱怨一下:close()你给我等着,过一会看看我怎么收拾你
break;
}
channel.writeAndFlush(line);
}
}, "input").start();
System.out.println("我来做最后的关闭后的处理工作 ");//这里绝对不行,因为执行完sync方法以后就不是同步阻塞了,再加上之后又重新开了线程当然不需要等待上面的线程执行完成就可以执行到这一步了这个简单
}
}
ChannelFuture处理关闭问题
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Scanner;
/**
* @program: Netty-Hello-Word
* @description: 客户端
* @author: wsw
* @create:
**/
@Slf4j
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
//创建启动器类
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())//加入事件循环组里面包含了线程和选择器
.channel(NioSocketChannel.class)//选择客户端channel实现
.handler(new ChannelInitializer<NioSocketChannel>() {//选择处理器
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {//再建立连接的时候进行调用
//添加一个handler 以给定的日志级别打印出LoggingHandler中的日志,可以对入站\出站事件进行日志记录,从而方便我们进行问题排查
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
//编码器,将字符串编码为字节码
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
//连接服务器,首先connect是一个异步非阻塞的线程,不关心结果,不需要等待结果返回直接往下执行代码,当前调用connect方法的是main主线程,真正执行连接connect是nio线程(new NioEventLoopGroup()),
.connect(new InetSocketAddress("localhost", 6001));
//1.使用sync方法同步处理结果
//channelFuture.sync();//阻塞方法,直到连接建立
//Channel channel= channelFuture.channel();//客户端和服务器端的之间的连接对象
//channel.writeAndFlush("Hello Word!");//写出数据
//2.使用addListener(回调对象)方法异步处理结果,就不是交给main主线程进行处理了
/* channelFuture.addListener(new ChannelFutureListener() {
//再nio线程连接建立好以后,会进行调用operationComplete
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
Channel channel = channelFuture.channel();//客户端和服务器端的之间的连接对象
channel.writeAndFlush("Hello Word!");//写出数据
}
});*/
Channel channel = channelFuture.sync().channel();
log.debug("{}", channel);
new Thread(() -> {
Scanner scanner = new Scanner(System.in);//键盘输入
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
channel.close();//close()方法指的是异步操作,指派给其他的线程进行关闭的,烦人,烦烦烦到处都是异步
//log.debug("我来做最后的关闭后的处理工作");//实际上是有可能还没有进行close还没有执行完以后就执行到这一步了,抱怨一下:close()你给我等着,过一会看看我怎么收拾你
break;
}
channel.writeAndFlush(line);
}
}, "input").start();
//log.debug("我来做最后的关闭后的处理工作");//这里绝对不行,因为执行完sync方法以后就不是同步阻塞了,再加上之后又重新开了线程当然不需要等待上面的线程执行完成就可以执行到这一步了这个简单
//获取ClosedFuture对象
ChannelFuture closeFuture = channel.closeFuture();
//1)同步处理关闭
/*
log.debug("正在等待关闭");
closeFuture.sync();
log.debug("处理关闭结果");
*/
//2)异步处理关闭
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
log.debug("处理关闭结果");
}
});
}
}
结束java进程
再我们进行上面的close之后java并没有进行结束这是为什么了,加下来来看看为什么和怎么办
为什么channel已经关闭了,我们创建的线程也是进行了break了,但是整个java进程并没有结束,这是因为new NioEventLoopGroup()里面还有一些进行没有进行结束
为什么Netty要用异步
接下在网上找到的一些资料进行解读
总结
- 单线程没法异步提高效率,必须配合多线程,多核cpu才能发挥异步的优势
- 异步没有缩短响应时间,反而是有所增加的,这里增加的是吞吐量,单位时间内处理请求的速度
Future & Promise
- netty 的 Future 继承自 jdk 的 Future
- Promise 是对 netty Future 进行了扩展
– jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
–netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
–netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称 | jdk Future | netty Future | Promise |
---|---|---|---|
cancel | 取消任务 | - | - |
isCanceled | 任务是否取消 | - | - |
isDone | 任务是否完成,不能区分成功失败 | - | - |
get | 获取任务结果,阻塞等待 | - | - |
getNow | - | 获取任务结果,非阻塞,还未产生结果时返回 null | - |
await | - | 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断 | - |
sync | - | 等待任务结束,如果任务失败,抛出异常 | - |
isSuccess | - | 判断任务是否成功 | - |
cause | - | 获取失败信息,非阻塞,如果没有失败,返回null | - |
addLinstener | - | 添加回调,异步接收结果 | - |
setSuccess | - | - | 设置成功结果 |
setFailure | - | - | 设置失败结果 |
jdk
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//提交任务
Future<Integer> future = service.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行中ing");
Thread.sleep(5000);
return 123;
}
});
//主线程通过future获取结果
log.debug("等待结果");
log.debug("结果是{}",future.get());
netty Future
NioEventLoopGroup group1 = new NioEventLoopGroup(2);
EventLoop eventLoop = group1.next();
//提交任务
Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行中ing");
Thread.sleep(5000);
return 123;
}
});
//主线程通过future获取结果
log.debug("等待结果");
log.debug("结果是{}",future.get());
netty Future 异步处理结果
NioEventLoopGroup group1 = new NioEventLoopGroup(2);
EventLoop eventLoop = group1.next();
//提交任务
Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行中ing");
Thread.sleep(5000);
return 123;
}
});
//异步
future.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.debug("接收结果:{}", future.getNow());
}
});
Promise
//准备EventLoop对象
EventLoop eventLoop1 = new NioEventLoopGroup().next();
//主动的创建promise,结果容器
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop1);
new Thread(()->{
log.debug("开始计算ing");
//任意一个线程执行计算,计算完毕以后项promise填充结果
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
promise.setSuccess(123);
}).start();
//接收结果的线程
log.debug("等待结果");
log.debug("等待结果{}",promise.get());
Handler & Pipeline
服务端
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(1);
ctx.fireChannelRead(msg); // 1
}
});
//**************************************入站*********************************
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(2);
ctx.fireChannelRead(msg); // 2
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(3);
ctx.channel().write(msg); // 3
}
});
//**************************************出站*********************************
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(4);
ctx.write(msg, promise); // 4
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(5);
ctx.write(msg, promise); // 5
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(6);
ctx.write(msg, promise); // 6
}
});
}
})
.bind(6001);
客户端
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1", 8080)
.addListener((ChannelFutureListener) future -> {
future.channel().writeAndFlush("hello,world");
});
- ChannelHandler 用来处理 Channel 上的各种事件,分为入站、出站两种
- 所有 ChannelHandler 被连成一串,就是 Pipeline
- 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果
- 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
ChannelInboundHandlerAdapter 是按照 addLast 的顺序执行的,而 ChannelOutboundHandlerAdapter 是按照 addLast 的逆序执行的。ChannelPipeline 的实现是一个 ChannelHandlerContext(包装了 ChannelHandler) 组成的双向链表
* 入站处理器中,ctx.fireChannelRead(msg) 是 调用下一个入站处理器
- 如果注释掉 1 处代码,则仅会打印 1
- 如果注释掉 2 处代码,则仅会打印 1 2
- 3 处的 ctx.channel().write(msg) 会 从尾部开始触发 后续出站处理器的执行
- 如果注释掉 3 处代码,则仅会打印 1 2 3
- 类似的,出站处理器中,ctx.write(msg, promise) 的调用也会 触发上一个出站处理器
- 如果注释掉 6 处代码,则仅会打印 1 2 3 6
- ctx.channel().write(msg) vs ctx.write(msg)
- 都是触发出站处理器的执行
- ctx.channel().write(msg) 从尾部开始查找出站处理器
- ctx.write(msg) 是从当前节点找上一个出站处理器
- 3 处的 ctx.channel().write(msg) 如果改为 ctx.write(msg) 仅会打印 1 2 3,因为节点3 之前没有其它出站处理器了
- 6 处的 ctx.write(msg, promise) 如果改为 ctx.channel().write(msg) 会打印 1 2 3 6 6 6… 因为 ctx.channel().write() 是从尾部开始查找,结果又是节点6 自己
服务端 pipeline 触发的原始流程,图中数字代表了处理步骤的先后次序
ByteBuf
是对字节数据的封装
//创建了一个默认的 ByteBuf(池化基于直接内存的 ByteBuf),初始容量是 10
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
log(buffer);
//输出结果:read index:0 write index:0 capacity:10