本教學中實現的協定是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