一、什麼是Netty?
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
Netty 是一個異步的、基於事件驅動的網絡應用框架,用於快速開發可維護、高性能的網絡服務器和客戶端
二、使用Netty的著名組件有哪些?
- Cassandra - nosql 數據庫
- Spark - 大數據分佈式計算框架
- Hadoop - 大數據分佈式存儲框架
- RocketMQ - 阿里巴巴開源的消息隊列
- ElasticSearch - 搜索引擎
- gRPC - rpc 框架
- Dubbo - rpc 框架
- Spring 5.x - flux api 完全拋棄了 tomcat ,使用 netty 作爲服務器端
- Zookeeper - 分佈式協調框架
三、簡單使用
下面我們先簡單看下如何使用Netty完成hello world。
在java中使用Netty,必然要引入其依賴。關於Netty的版本變更如下:
- 2.x 2004
- 3.x 2008
- 4.x 2013
- 5.x 已廢棄(沒有明顯的性能提升,維護成本高)
如上所示,5.x沒有太大的意義,所以我們這裏也是用maven4.x的版本,首先在項目中引入maven依賴:
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.52.Final</version>
</dependency>
服務端代碼:
public class HelloWorldServer {
public static void main(String[] args) {
new ServerBootstrap()
// 1、創建 NioEventLoopGroup,可以簡單理解爲 `線程池 + Selector`
.group(new NioEventLoopGroup())
// 2、選擇服務 ServerSocketChannel 實現類,這裏選擇Nio
.channel(NioServerSocketChannel.class)
// 3、此處是給客戶端SocketChannel使用,ChannelInitializer執行一次,待客戶端建立連接後,執行initChannel,添加更多的處理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
// 5、客戶端SocketChannel處理器,解碼:ByteBuffer -> String
ch.pipeline().addLast(new StringDecoder());
// 6、客戶端SocketChannel業務處理器,使用上一個處理器的結果
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
// 4、服務端ServerSocketChannel綁定監聽端口
.bind(8080);
}
}
客戶端代碼:
public class HelloWorldClient {
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
// 1、創建 NioEventLoopGroup,可以簡單理解爲 `線程池 + Selector`
.group(new NioEventLoopGroup())
// 2、選擇服務 ServerSocketChannel 實現類,這裏選擇Nio
.channel(NioSocketChannel.class)
// 3、此處是給客戶端SocketChannel使用,ChannelInitializer執行一次,待客戶端建立連接後,執行initChannel,添加更多的處理器
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
// 8、消息會經過通道 handler 處理,這裏是將 String => ByteBuf 發出
ch.pipeline().addLast(new StringEncoder());
}
})
// 4、指定要連接的服務器端口
.connect("127.0.0.1", 8080)
// 5、同步方法,等待connect()連接完畢
.sync()
// 6、獲取channel對象,即通道,可讀寫操作
.channel()
// 7、寫入消息並清空緩衝區
.writeAndFlush(new Date() + ": hello world!");
}
}
結果:
Tue Nov 09 10:21:18 CST 2021: hello world!
可以使用以下方式快速理解和記憶:
- 把 channel 理解爲數據的通道
- 把 msg 理解爲流動的數據,最開始輸入是 ByteBuf,但經過 pipeline 的加工,會變成其它類型對象,最後輸出又變成 ByteBuf
- 把 handler 理解爲數據的處理工序
- 工序有多道,合在一起就是 pipeline,pipeline 負責發佈事件(讀、讀取完成...)傳播給每個 handler, handler 對自己感興趣的事件進行處理(重寫了相應事件處理方法)
- handler 分 Inbound 和 Outbound 兩類
- 把 eventLoop 理解爲處理數據的工人
- 工人可以管理多個 channel 的 io 操作,並且一旦工人負責了某個 channel,就要負責到底(綁定)
- 工人既可以執行 io 操作,也可以進行任務處理,每位工人有任務隊列,隊列裏可以堆放多個 channel 的待處理任務,任務分爲普通任務、定時任務
- 工人按照 pipeline 順序,依次按照 handler 的規劃(代碼)處理數據,可以爲每道工序指定不同的工人
三、主要組件
3.1 EventLoop
根據其名稱直譯叫做“事件循環”對象,其用於處理channel的IO操作。
EventLoop 本質是一個單線程執行器(同時維護了一個 Selector),裏面有 run 方法處理 Channel 上源源不斷的 io 事件。
其依賴關係如下:
如上如所示,其繼承關係較複雜。
- 一條線是繼承自 j.u.c.ScheduledExecutorService 因此包含了線程池中所有的方法。
- 另一條線是繼承自 netty 自己的 OrderedEventExecutor。
3.2 EventLoopGroup
事件循環組。
EventLoopGroup 是一組 EventLoop,Channel 一般會調用 EventLoopGroup 的 register 方法來綁定其中一個 EventLoop,後續這個 Channel 上的 io 事件都由此 EventLoop 來處理(保證了 io 事件處理時的線程安全)
- 繼承自 netty 自己的 EventExecutorGroup
- 實現了 Iterable 接口提供遍歷 EventLoop 的能力
- 另有 next 方法獲取集合中下一個 EventLoop
3.2.1 常用EventLoopGroup
主要有以下兩種EventLoopGroup:
1)NioEventLoopGroup:處理IO事件,普通任務,定時任務
2)DefaultEventLoopGroup:處理普通任務,定時任務
這裏先給出一個結論,EventLoopGroup當中的每一個EventLoop,和客戶端Channel實際是綁定的:簡單來說,就是一個channel發送的內容,會被同一個線程處理,後面代碼會體現。
3.2.2 EventLoopGroup代碼使用示例
下面以NioEventLoopGroup爲例,介紹簡單使用:
3.2.2.1 遍歷EventLoopGroup:
public class TestEventLoopGroup {
public static void main(String[] args) {
//構造方法可以指定線程數,默認不設置會首先根據Netty的環境變量,否則根據線程核心數*2,最小爲1
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(2);
// 使用期next方法 獲取內部的EventLoop
System.out.println(nioEventLoopGroup.next());
System.out.println(nioEventLoopGroup.next());
System.out.println(nioEventLoopGroup.next());
System.out.println("-------------------------------------");
// for循環獲取內部的EventLoop
for (EventExecutor group: nioEventLoopGroup) {
System.out.println(group);
}
}
}
結果:
io.netty.channel.nio.NioEventLoop@294425a7
io.netty.channel.nio.NioEventLoop@67d48005
io.netty.channel.nio.NioEventLoop@294425a7
-------------------------------------
io.netty.channel.nio.NioEventLoop@294425a7
io.netty.channel.nio.NioEventLoop@67d48005
Process finished with exit code 0
3.2.2.2 執行普通任務 和 定時任務:
public class TestEventLoopGroup {
public static void main(String[] args) throws InterruptedException {
//構造方法可以指定線程數,默認不設置會首先根據Netty的環境變量,否則根據線程核心數*2,最小爲1
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(2);
// 執行普通任務
nioEventLoopGroup.next().execute(TestEventLoopGroup::print);
// 執行定時任務,延後一秒打印
System.out.println(new Date());
nioEventLoopGroup.next().schedule(TestEventLoopGroup::print, 1000, TimeUnit.MILLISECONDS);
}
private static void print() {
System.out.println(new Date() + " " +Thread.currentThread());
}
}
結果:
Tue Nov 09 14:45:50 CST 2021
Tue Nov 09 14:45:50 CST 2021 Thread[nioEventLoopGroup-2-1,10,main]
Tue Nov 09 14:45:51 CST 2021 Thread[nioEventLoopGroup-2-2,10,main]
3.2.2.3 執行IO任務:
其實所謂IO任務就是,客戶端和服務端的通信,在此例子的基礎上,我們在添加一個職責劃分的概念。
1)職責劃分1
何爲職責劃分?就是在我們創建EventLoopGroup時,指定兩個,不使用單一的一個,讓職責更加明確,其構造方法如下所示:
/**
* 爲父級(接受者)和子級(客戶端)設置EventLoopGroup 。
*/
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup");
return this;
}
在上面的構造當中,第一個參數負責ServerSocketChannel的accept操作,而第二個參數負責SocketChannel的讀寫。
關於第一個參數是否需要指定線程數據呢?實際只需要指定爲1即可,因爲ServerSocketChannel只有一個,只會綁定一個EventLoop。
第二個參數是工作線程,根據實際工作需要設置,默認不設置會首先根據Netty的環境變量,否則根據線程核心數*2,最小爲1。
下面進行代碼的演示,模擬兩個EventLoop處理事件。
根據前面的分析,這裏我們設置兩個EventLoopGroup,第一個給1或者不設置,第二個需要注意了,如果給1的話,那表示只會有一個線程在進行SocketChannel的讀寫操作,並不是兩個線程同時操作,所以下面我們給第二個參數的線程數設置爲2。
服務端代碼:
public class EventLoopGroupServer {
public static void main(String[] args) throws InterruptedException {
new ServerBootstrap()
.group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println(Thread.currentThread().getName() + ": " + s);
}
});
}
}).bind(8080).sync();
}
}
分別依次啓動三個客戶端,每個客戶端發送兩個不同的字符串消息(分別是aaa,bbb,ccc),下面只提供aaa,看結果。
客戶端代碼:
public class EventLoopGroupClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup(1))
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
System.out.println("init...");
ch.pipeline().addLast(new StringEncoder());
}
})
.channel(NioSocketChannel.class).connect("localhost", 8080)
.sync()
.channel();
channel.writeAndFlush("aaa");
Thread.sleep(1000);
channel.writeAndFlush("aaa");
}
}
結果:
nioEventLoopGroup-3-1: aaa
nioEventLoopGroup-3-1: aaa
nioEventLoopGroup-3-2: bbb
nioEventLoopGroup-3-2: bbb
nioEventLoopGroup-3-1: ccc
nioEventLoopGroup-3-1: ccc
根據結果我們得到結論:
客戶端SocketChannel會和EventLoop進行綁定,後面發送的消息,依然由其處理。
下一個客戶端連接後,會默認輪詢到下一個EventLoop。
第三個客戶端來的時候,又會連接第一EventLoop,其內部是多路複用,一個EventLoop管理多個channel。
2)職責劃分2
前面講了一種職責劃分,是在ServerSocketChannel和SocketChannel的劃分。
針對SocketChannel還可以進一步的劃分,實現方式就是我們可以指定另外一個EventLoopGroup,具體如下所示:
public class EventLoopGroupServer {
public static void main(String[] args) throws InterruptedException {
// 定義一個defaultEventLoopGroup,對職責進一步劃分
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
new ServerBootstrap()
.group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
// 解碼處理器
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println(Thread.currentThread().getName() + ": " + s);
// 要想使下了一個處理器,能夠收到此處理器的結果,需要使用西面這個方法傳遞
channelHandlerContext.fireChannelRead(s);
}
// 添加另一個處理器,使用額外的EventLoopGroup
}).addLast(defaultEventLoopGroup,"otherHandler",new SimpleChannelInboundHandler<String>(){
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println(Thread.currentThread().getName() + ": " + s);
}
});
}
}).bind(8080).sync();
}
}
注意使用 channelHandlerContext.fireChannelRead(s)在處理器傳遞。
客戶端隨便發送個消息,我們看看兩個處理器的打印內容:
nioEventLoopGroup-4-2: Tue Nov 09 15:44:47 CST 2021: hello world!
defaultEventLoopGroup-2-2: Tue Nov 09 15:44:47 CST 2021: hello world!
連個處理內容分別通過不同的EventLoop處理,分別是原本的nio和後創建的default。
並且,同一個客戶端再次發送內容,此處兩個線程仍然會是和channel進行綁定。不具體演示了。
我們最後看下上述的兩個處理器如何切換不同的線程處理的?
通過 channelHandlerContext.fireChannelRead(s)向下跟蹤,到以下代碼處:
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
// 下一個 handler 的事件循環是否與當前的事件循環是同一個線程,此處就是EventLoop
EventExecutor executor = next.executor();
// 是,直接調用
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
}
// 不是,將要執行的代碼作爲任務提交給下一個事件循環處理(換人)
else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
看到這了,有幫助的話幫忙點個贊吧~~