更新時(shí)間:2020-04-07 來(lái)源:黑馬程序員 瀏覽量:
Netty是一個(gè)提供 asynchronous event-driven (異步事件驅(qū)動(dòng))的網(wǎng)絡(luò)應(yīng)用框架,是一個(gè)用以快速開(kāi)發(fā)高性能、高可靠性協(xié)議的服務(wù)器和客戶端。換句話說(shuō),Netty 是一個(gè) NIO 客戶端服務(wù)器框架,使用它可以快速簡(jiǎn)單地開(kāi)發(fā)網(wǎng)絡(luò)應(yīng)用程序,比如服務(wù)器和客戶端的協(xié)議。Netty 大大簡(jiǎn)化了網(wǎng)絡(luò)程序的開(kāi)發(fā)過(guò)程比如 TCP 和 UDP 的 socket 服務(wù)的開(kāi)發(fā)。 “快速和簡(jiǎn)單”并不意味著應(yīng)用程序會(huì)有難維護(hù)和性能低的問(wèn)題,Netty 是一個(gè)精心設(shè)計(jì)的框架,它從許多協(xié)議的實(shí)現(xiàn)中吸收了很多的經(jīng)驗(yàn)比如 FTP、SMTP、HTTP、許多二進(jìn)制和基于文本的傳統(tǒng)協(xié)議.因此,Netty 已經(jīng)成功地找到一個(gè)方式,在不失靈活性的前提下來(lái)實(shí)現(xiàn)開(kāi)發(fā)的簡(jiǎn)易性,高性能,穩(wěn)定性。推薦了解黑馬程序員java培訓(xùn)課程。
讓我們開(kāi)始吧
本章圍繞Netty 的核心架構(gòu),通過(guò)簡(jiǎn)單的示例帶你快速入門。當(dāng)你讀完本章節(jié),你馬上就可以用 Netty 寫出一個(gè)客戶端和服務(wù)器。
開(kāi)始之前
在開(kāi)始之前我們先說(shuō)明下開(kāi)發(fā)環(huán)境,我們使用netty-4.1.30這個(gè)版本,jdk使用1.8及以上版本。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
jdk請(qǐng)自行下載。
先來(lái)個(gè)丟棄服務(wù)
世上最簡(jiǎn)單的協(xié)議不是'Hello, World!' 而是 DISCARD(丟棄)。這個(gè)協(xié)議將會(huì)丟掉任何收到的數(shù)據(jù),而不響應(yīng)。 為了實(shí)現(xiàn) DISCARD 協(xié)議,你只需忽略所有收到的數(shù)據(jù)。讓我們從 handler (處理器)的實(shí)現(xiàn)開(kāi)始,handler 是由Netty 生成用來(lái)處理 I/O 事件的。
先創(chuàng)建一個(gè)處理器
package com.netty.first;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* 處理服務(wù)端 channel.
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
System.out.println(msg);
// 默默地丟棄收到的數(shù)據(jù)
((ByteBuf) msg).release(); // (3)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// 當(dāng)出現(xiàn)異常就關(guān)閉連接
cause.printStackTrace();
ctx.close();
}
}
(1) DiscardServerHandler 繼承自 ChannelInboundHandlerAdapter ,這個(gè)類實(shí)現(xiàn)了ChannelInboundHandler接口,ChannelInboundHandler提供了許多事件處理的接口方法,然后你可以覆蓋這些方法?,F(xiàn)在僅僅只需要繼承ChannelInboundHandlerAdapter 類而不是你自己去實(shí)現(xiàn)接口方法。
(2)這里我們覆蓋了chanelRead() 事件處理方法。每當(dāng)從客戶端收到新的數(shù)據(jù)時(shí),這個(gè)方法會(huì)在收到消息時(shí)被調(diào)用,這個(gè)例子中,收到的消息的類型是 ByteBuf。
(3)為了實(shí)現(xiàn)DISCARD協(xié)議,處理器不得不忽略所有接受到的消息。ByteBuf是一個(gè)引用計(jì)數(shù)對(duì)象,這個(gè)對(duì)象必須顯示地調(diào)用release() 方法來(lái)釋放。請(qǐng)記住處理器的職責(zé)是釋放所有傳遞到處理器的引用計(jì)數(shù)對(duì)象。通常, channelRead() 方法的實(shí)現(xiàn)就像下面的這段代碼:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}
(4)exceptionCaught()事件處理方法是當(dāng)出現(xiàn)Throwable對(duì)象才會(huì)被調(diào)用,即當(dāng)Netty由于IO錯(cuò)誤或者處理器在處理事件時(shí)拋出的異常時(shí)。在大部分情況下,捕獲的異常應(yīng)該被記錄下來(lái)并且把關(guān)聯(lián)的 channel 給關(guān)閉掉。然而這個(gè)方法的處理方式會(huì)在遇到不同異常的情況下有不同的實(shí)現(xiàn),比如你可能想在關(guān)閉連接之前發(fā)送一個(gè)錯(cuò)誤碼的響應(yīng)消息。
編寫服務(wù)端代碼
目前為止一切都還不錯(cuò),我們已經(jīng)實(shí)現(xiàn)了DISCARD服務(wù)器的一半功能,剩下的需要編寫一個(gè)main()方法來(lái)啟動(dòng)服務(wù)端的DiscardServerHandler 。
package com.netty.first;
import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;
/**
* 丟棄任何進(jìn)入的數(shù)據(jù)
*/
public class DiscardServer {
private int port;
public DiscardServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128) // (5)
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
// 綁定端口,開(kāi)始接收進(jìn)來(lái)的連接
ChannelFuture f = b.bind(port).sync(); // (7)
// 等待服務(wù)器 socket 關(guān)閉 。
// 在這個(gè)例子中,這不會(huì)發(fā)生,但你可以優(yōu)雅地關(guān)閉你的服務(wù)器。
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new DiscardServer(port).run();
}
}
1、NioEventLoopGroup是用來(lái)處理I/O操作的多線程事件循環(huán)器,Netty提供了許多不同的EventLoopGroup的實(shí)現(xiàn)用來(lái)處理不同的傳輸。在這個(gè)例子中我們實(shí)現(xiàn)了一個(gè)服務(wù)端的應(yīng)用,因此會(huì)有2個(gè) NioEventLoopGroup 會(huì)被使用。第一個(gè)經(jīng)常被叫做‘boss’,用來(lái)接收進(jìn)來(lái)的連接。第二個(gè)經(jīng)常被叫做‘worker’,用來(lái)處理已經(jīng)被接收的連接,一旦‘boss’接收到連接,就會(huì)把連接信息注冊(cè)到‘worker’上。如何知道多少個(gè)線程已經(jīng)被使用,如何映射到已經(jīng)創(chuàng)建的 Channel上都需要依賴于 EventLoopGroup 的實(shí)現(xiàn),并且可以通過(guò)構(gòu)造函數(shù)來(lái)配置他們的關(guān)系。
2、ServerBootstrap是一個(gè)啟動(dòng)NIO服務(wù)的輔助啟動(dòng)類。你可以在這個(gè)服務(wù)中直接使用 Channel,但是這會(huì)是一個(gè)復(fù)雜的處理過(guò)程,在很多情況下你并不需要這樣做。
3、這里我們指定使用NioServerSocketChannel類來(lái)舉例說(shuō)明一個(gè)新的 Channel 如何接收進(jìn)來(lái)的連接。
4、這里的事件處理類經(jīng)常會(huì)被用來(lái)處理一個(gè)最近的已經(jīng)接收的Channel。ChannelInitializer是一個(gè)特殊的處理類,他的目的是幫助使用者配置一個(gè)新的Channel。也許你想通過(guò)增加一些處理類比如DiscardServerHandler 來(lái)配置一個(gè)新的 Channel 或者其對(duì)應(yīng)的ChannelPipeline 來(lái)實(shí)現(xiàn)你的網(wǎng)絡(luò)程序。當(dāng)你的程序變的復(fù)雜時(shí),可能你會(huì)增加更多的處理類到 pipline 上,然后提取這些匿名類到最頂層的類上。
5、你可以設(shè)置這里指定的Channel實(shí)現(xiàn)的配置參數(shù)。我們正在寫一個(gè)TCP/IP的服務(wù)端,因此我們被允許設(shè)置socket的參數(shù)選項(xiàng)比如tcpNoDelay 和 keepAlive。請(qǐng)參考 ChannelOption 和詳細(xì)的ChannelConfig實(shí)現(xiàn)的接口文檔以此可以對(duì)ChannelOption 的有一個(gè)大概的認(rèn)識(shí)。
6、你關(guān)注過(guò) option() 和 childOption() 嗎?option() 是提供給NioServerSocketChannel 用來(lái)接收進(jìn)來(lái)的連接。childOption() 是提供給由父管道 ServerChannel接收到的連接,在這個(gè)例子中也是 NioServerSocketChannel。
7、我們繼續(xù),剩下的就是綁定端口然后啟動(dòng)服務(wù)。這里我們?cè)跈C(jī)器上綁定了機(jī)器所有網(wǎng)卡上的8080端口。當(dāng)然現(xiàn)在你可以多次調(diào)用bind() 方法(基于不同綁定地址)。恭喜!你已經(jīng)熟練地完成了第一個(gè)基于 Netty 的服務(wù)端程序。
查看收到的數(shù)據(jù)
現(xiàn)在我們已經(jīng)編寫出我們第一個(gè)服務(wù)端,我們需要測(cè)試一下他是否真的可以運(yùn)行。最簡(jiǎn)單的測(cè)試方法是用telnet命令。例如,你可以在命令行上輸入telnet localhost 8080或者其他類型參數(shù)。
在telnet終端中輸入任意字符,服務(wù)端向控制臺(tái)輸出信息。證明服務(wù)端接收到客戶端發(fā)送的消息了。但是我們并不能看到服務(wù)端接收到了什么東西,我們可以把channelRead方法改成如下內(nèi)容:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
//System.out.println(msg);
ByteBuf message = (ByteBuf) msg;
System.out.println(message.toString(CharsetUtil.US_ASCII));
// 默默地丟棄收到的數(shù)據(jù)
((ByteBuf) msg).release(); // (3)
}
這樣控制臺(tái)就可以看到客戶端發(fā)送的數(shù)據(jù)了。
寫個(gè)應(yīng)答服務(wù)器
到目前為止,我們雖然接收到了數(shù)據(jù),但沒(méi)有做任何的響應(yīng)。然而一個(gè)服務(wù)端通常會(huì)對(duì)一個(gè)請(qǐng)求作出響應(yīng)。讓我們學(xué)習(xí)怎樣在ECHO協(xié)議的實(shí)現(xiàn)下編寫一個(gè)響應(yīng)消息給客戶端,這個(gè)協(xié)議針對(duì)任何接收的數(shù)據(jù)都會(huì)返回一個(gè)響應(yīng)。
和 discard server 唯一不同的是把在此之前我們實(shí)現(xiàn)的channelRead()方法,返回所有的數(shù)據(jù)替代打印接收數(shù)據(jù)到控制臺(tái)上的邏輯。因此,需要把channelRead()方法修改如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
(1)ChannelHandlerContext對(duì)象提供了許多操作,使你能夠觸發(fā)各種各樣的I/O事件和操作。這里我們調(diào)用了write(Object) 方法來(lái)逐字地把接受到的消息寫入。請(qǐng)注意不同于DISCARD的例子我們并沒(méi)有釋放接受到的消息,這是因?yàn)楫?dāng)寫入的時(shí)候 Netty 已經(jīng)幫我們釋放了。
(2)ctx.write(Object)方法不會(huì)使消息寫入到通道上,他被緩沖在了內(nèi)部,你需要調(diào)用 ctx.flush() 方法來(lái)把緩沖區(qū)中數(shù)據(jù)強(qiáng)行輸出?;蛘吣憧梢杂酶?jiǎn)潔的cxt.writeAndFlush(msg)以達(dá)到同樣的目的。
如果你再一次運(yùn)行telnet命令,你會(huì)看到服務(wù)端會(huì)發(fā)回一個(gè)你已經(jīng)發(fā)送的消息。
猜你喜歡:
疫情期間黑馬程序員如何抓學(xué)生的學(xué)習(xí)質(zhì)量?
2020-04-03如何使用Neo4j構(gòu)建人物關(guān)系圖?[Javaee培訓(xùn)]
2020-04-03黑馬程序員疫情期間如何保證項(xiàng)目實(shí)戰(zhàn)課程效果?
2020-04-03Docker教程:企業(yè)級(jí)應(yīng)用容器化實(shí)踐之容器鏡像倉(cāng)庫(kù)
2020-03-262小時(shí)實(shí)現(xiàn)黑馬AI機(jī)器人實(shí)戰(zhàn)教程[Java培訓(xùn)]
2020-03-26Java哪個(gè)培訓(xùn)班靠譜?[java培訓(xùn)推薦]
2020-03-22