Netty實踐入門-編寫簡單服務器
按照以前的套路,一般學習一項新的IT技術,首先得來個 ‘Hello,World!‘之類的,以向世界宣告你要學習某項技術了。但在世界上最簡單的協議不是’Hello,World!‘而是 DISCARD。它是一種丟棄任何接收到的數據而沒有任何響應的協議(暫時就叫它」裝死協議」吧)。
要實現DISCARD協議,只需要忽略所有接收到的數據。讓我們從處理程序實現直接開始,這個處理程序實現處理Netty生成的I/O事件。先擼下面幾串代碼吧 -
package com.yiibai.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* 處理服務器端通道
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
// 以靜默方式丟棄接收的數據
((ByteBuf) msg).release(); // (3)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// 出現異常時關閉連接。
cause.printStackTrace();
ctx.close();
}
}
一些重要的解釋:
DiscardServerHandler
擴展了ChannelInboundHandlerAdapter
,它是ChannelInboundHandler
的一個實現。ChannelInboundHandler
提供了可以覆蓋的各種事件處理程序方法。 現在,它只是擴展了ChannelInboundHandlerAdapter
,而不是自己實現處理程序接口。我們在這裏覆蓋
channelRead()
事件處理程序方法。每當從客戶端接收到新數據時,使用該方法來接收客戶端的消息。 在此示例中,接收到的消息的類型爲ByteBuf
。要實現
DISCARD
協議,處理程序必須忽略接收到的消息。ByteBuf
是引用計數的對象,必須通過release()
方法顯式釋放。請記住,處理程序負責釋放傳遞給處理程序的引用計數對象。 通常,channelRead()
處理程序方法實現如下:@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { try { // Do something with msg } finally { ReferenceCountUtil.release(msg); } }
當Netty由於I/O錯誤或由處理事件時拋出的異常而導致的處理程序實現引發異常時,使用
Throwable
調用exceptionCaught()
事件處理程序方法。 在大多數情況下,捕獲的異常會被記錄,並且其相關的通道應該在這裏關閉,這種方法的實現可以根據想要什麼樣的方式來處理異常情況而有所不同。 例如,您可能希望在關閉連接之前發送帶有錯誤代碼的響應消息。
到現在如果沒有問題,我們已經實現了DISCARD
服務器的前半部分。 現在剩下的就是編寫main()
方法,並使用DiscardServerHandler
啓動服務器。
package com.yiibai.netty;
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;
/**
* Discards any incoming data.
*/
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)
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync(); // (7)
// Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
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();
}
}
NioEventLoopGroup
是處理I/O
操作的多線程事件循環。 Netty爲不同類型的傳輸提供了各種EventLoopGroup
實現。 在此示例中,實現的是服務器端應用程序,因此將使用兩個NioEventLoopGroup
。 第一個通常稱爲「boss
」,接受傳入連接。 第二個通常稱爲「worker
」,當「boss
」接受連接並且向「worker
」註冊接受連接,則「worker
」處理所接受連接的流量。 使用多少個線程以及如何將它們映射到創建的通道取決於EventLoopGroup
實現,甚至可以通過構造函數進行配置。ServerBootstrap
是一個用於設置服務器的助手類。 您可以直接使用通道設置服務器。 但是,請注意,這是一個冗長的過程,在大多數情況下不需要這樣做。在這裏,我們指定使用
NioServerSocketChannel
類,該類用於實例化新的通道以接受傳入連接。此處指定的處理程序將始終由新接受的通道計算。
ChannelInitializer
是一個特殊的處理程序,用於幫助用戶配置新的通道。 很可能要通過添加一些處理程序(例如DiscardServerHandler
)來配置新通道的ChannelPipeline
來實現您的網絡應用程序。 隨着應用程序變得複雜,可能會向管道中添加更多處理程序,並最終將此匿名類提取到頂級類中。還可以設置指定
Channel
實現的參數。這裏編寫的是一個TCP/IP
服務器,所以我們允許設置套接字選項,如tcpNoDelay
和keepAlive
。 請參閱ChannelOption的apidocs和指定的ChannelConfig實現,以瞭解關於ChannelOptions。你注意到
option()
和childOption()
沒有?option()
用於接受傳入連接的NioServerSocketChannel
。childOption()
用於由父ServerChannel
接受的通道,在這個示例中爲NioServerSocketChannel
。現在準備好了。剩下的是綁定到端口和啓動服務器。 這裏,我們綁定到機器中所有NIC(網絡接口卡)的端口:
8080
。 現在可以根據需要多次調用bind()
方法(使用不同的綁定地址)。
恭喜!這就完成了一個基於 Netty 的第一個服務器。
查看接收的數據
現在我們已經編寫了第一個服務器,還需要測試它是否真的有效地運行工作。測試它的最簡單的方法是使用telnet
命令。 例如,可以在命令行中輸入telnet localhost 8080
並鍵入內容。
但是,能驗證服務器工作正常嗎? 其實不能真正知道,因爲它是一個」丟棄「(什麼也不處理)服務器。所以發送什麼請求根本不會得到任何反應。 爲了證明它是真的在運行工作,我們還修改一點服務器端上的代碼 - 打印它收到了什麼東西。
前面我們已經知道,只要接收到數據,就調用channelRead()
方法。現在把一些代碼放到DiscardServerHandler
的channelRead()
方法中,如下所示:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
while (in.isReadable()) { // (1)
System.out.print((char) in.readByte());
System.out.flush();
}
} finally {
ReferenceCountUtil.release(msg); // (2)
}
// 或者直接打印
System.out.println("Yes, A new client in = " + ctx.name());
}
這個低效的循環實際上可以簡化爲:
System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
或者,可以在這裏寫上:in.release()
。
最後看一看項目的文件結構,如下所示 -
運行 DiscardServer
,輸出結果如下 -
三月 01, 2017 00:02:07 上午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x4f99602f] REGISTERED
三月 01, 2017 00:02:07 上午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x4f99602f] BIND: 0.0.0.0/0.0.0.0:8080
三月 01, 2017 00:02:07 上午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x4f99602f, L:/0:0:0:0:0:0:0:0:8080] ACTIVE
如果再次運行telnet localhost 8080
命令,將會看到服務器打印接收到的內容。
三月 01, 2017 2:14:14 上午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0xd63646da] REGISTERED
三月 01, 2017 2:14:14 上午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0xd63646da] BIND: 0.0.0.0/0.0.0.0:8080
三月 01, 2017 2:14:14 上午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0xd63646da, L:/0:0:0:0:0:0:0:0:8080] ACTIVE
三月 01, 2017 2:14:32 上午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xd63646da, L:/0:0:0:0:0:0:0:0:8080] RECEIVED: [id: 0x452d2ebd, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:58248]
Yes, A new client in = DiscardServerHandler#0
Yes, A new client in = DiscardServerHandler#0
上面的輸出證明,這個服務器程序是可以正常運行工作的。