Netty實踐-時間服務器
本教程中實現的協議是TIME協議。 它與先前的示例不同,時間服務器只發送包含32
位整數的消息,而不接收任何請求,並在消息發送後關閉連接。 在本示例中,您將學習如何構造和發送消息,以及在完成時關閉連接。
因爲時間服務器將忽略任何接收到的數據,但是一旦建立連接就發送消息,所以我們不能使用channelRead()
方法。而是覆蓋channelActive()
方法。 以下是代碼的實現:
package com.yiibai.netty.time;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(final ChannelHandlerContext ctx) { // (1)
final ByteBuf time = ctx.alloc().buffer(4); // (2)
time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
final ChannelFuture f = ctx.writeAndFlush(time); // (3)
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
}); // (4)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
下面我們來看看上面代碼的一些解釋分析:
如上所述,當建立連接並準備好生成流量時,將調用
channelActive()
方法。現在在這個方法中編寫一個32
位的整數來表示當前的時間。要發送新消息,需要分配一個包含消息的新緩衝區。我們要寫入一個32位整數,因此需要一個ByteBuf,其容量至少爲
4
個字節。 通過ChannelHandlerContext.alloc()
獲取當前的ByteBufAllocator
並分配一個新的緩衝區。像之前一樣,編寫構造的消息。
但是,在NIO中發送消息之前,我們是否曾調用java.nio.ByteBuffer.flip()
? ByteBuf沒有這樣的方法,它只有兩個指針; 一個用於讀取操作,另一個用於寫入操作。 當您向ByteBuf
寫入內容時,寫入索引會增加,而讀取器索引不會更改。讀取器索引和寫入器索引分別表示消息的開始和結束位置。
相比之下,NIO緩衝區不提供一個乾淨的方式來確定消息內容開始和結束,而不用調用flip
方法。當您忘記翻轉緩衝區時,就將會遇到麻煩,因爲不會發送任何或發送不正確的數據。但是這樣的錯誤不會發生在Netty中,因爲不同的操作類型我們有不同的指針。
另一點要注意的是ChannelHandlerContext.write()
(和writeAndFlush()
)方法返回一個ChannelFuture
。ChannelFuture
表示尚未發生的I/O
操作。這意味着,任何請求的操作可能尚未執行,因爲所有操作在Netty中是異步的。 例如,以下代碼可能會在發送消息之前關閉連接:Channel ch = ...; ch.writeAndFlush(message); ch.close();
因此,需要在
ChannelFuture
完成後調用close()
方法,該方法由write()
方法返回,並在寫入操作完成時通知其監聽器。 請注意,close()
也可能不會立即關閉連接,並返回一個ChannelFuture。
當寫請求完成時,我們如何得到通知? 這就像向返回的ChannelFuture
添加ChannelFutureListener
一樣簡單。 在這裏,我們創建了一個新的匿名ChannelFutureListener
,當操作完成時關閉Channel
。
或者,可以使用預定義的偵聽器來簡化代碼:
f.addListener(ChannelFutureListener.CLOSE);
要測試我們的時間服務器是否按預期工作,可以使用UNIX rdate
命令:
$ rdate -o <port> -p <host>
其中<port>
是在main()
方法中指定的端口號,<host>
通常是localhost
或服務器的IP地址。
編寫時間客戶端
與DISCARD
和ECHO
服務器不同,我們需要一個用於TIME協議的客戶端,因爲我們無法將32
位二進制數據轉換爲日曆上的日期。 在本節中,我們討論如何確保服務器正常工作並學習如何使用Netty編寫客戶端。
Netty中服務器和客戶端之間最大的和唯一的區別是使用了不同的Bootstrap
和Channel
實現。 請看看下面的代碼:
package com.yiibai.netty.time;
public class TimeClient {
public static void main(String[] args) throws Exception {
String host = args[0];
int port = Integer.parseInt(args[1]);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); // (1)
b.group(workerGroup); // (2)
b.channel(NioSocketChannel.class); // (3)
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync(); // (5)
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
Bootstrap
與ServerBootstrap
類似,只是它用於非服務器通道,例如客戶端或無連接通道。如果只指定一個
EventLoopGroup
,它將同時用作boss
組和worker
組。boss
組和worker
組不是用於客戶端。不使用
NioServerSocketChannel
,而是使用NioSocketChannel
來創建客戶端通道。注意,這裏不像我們使用的
ServerBootstrap
,所以不使用childOption()
,因爲客戶端SocketChannel
沒有父類。應該調用
connect()
方法,而不是bind()
方法。
如上面所見,它與服務器端代碼沒有什麼不同。 ChannelHandler
實現又是怎麼樣的呢? 它應該從服務器接收一個32
位整數,將其轉換爲人類可讀的格式,打印轉換爲我們熟知的時間格式 ,並關閉連接:
package com.yiibai.netty.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg; // (1)
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
Date currentTime = new Date(currentTimeMillis);
System.out.println("Default Date Format:" + currentTime.toString());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = formatter.format(currentTime);
// 轉換一下成中國人的時間格式
System.out.println("Date Format:" + dateString);
ctx.close();
} finally {
m.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
(1). 在TCP/IP
中,Netty讀取從對端發送的ByteBuf
數據。
客戶端看起來很簡單,與服務器端示例沒什麼區別。 但是,這個處理程序有時會拒絕拋出IndexOutOfBoundsException
。 我們將在下一節討論爲什麼會發生這種情況。
先運行 TimeServer.java
程序,然後再運行 TimeClient.java
, 當運行 TimeClient.java
時就可以到有一個時間日期輸出,然後程序自動退出。輸出結果如下 -
Default Date Format:Thu Mar 02 20:50:23 CST 2017
Date Format:2017-03-02 20:50:23