本章主要內容
- UDP概述
- 一個示例廣播應用程序
到目前為止,你所見過的絕大多數的例子都使用了基於連接的協議,如TCP。在本章中,我們將會把重點放在一個無連接協議即用戶數據報協議(UDP)上,它通常用在性能至關重要並且能夠容忍一定的數據包丟失的情況下[1]。
我們將會首先概述UDP的特性以及它的局限性。在這之後,我們將描述本章的示例應用程序,其將演示如何使用UDP的廣播能力。我們還會使用一個編碼器和一個解碼器來處理作為廣播消息格式的POJO。在本章的結束時候,你將能夠在自己的應用程序中使用UDP。
13.1 UDP的基礎知識
面向連接的傳輸(如TCP)管理了兩個網絡端點之間的連接的建立,在連接的生命週期內的有序和可靠的消息傳輸,以及最後,連接的有序終止。相比之下,在類似於UDP這樣的無連接協議中,並沒有持久化連接這樣的概念,並且每個消息(一個UDP數據報)都是一個單獨的傳輸單元。
此外,UDP也沒有TCP的糾錯機制,其中每個節點都將確認它們所接收到的包,而沒有被確認的包將會被發送方重新傳輸。
通過類比,TCP連接就像打電話,其中一系列的有序消息將會在兩個方向上流動。相反,UDP則類似於往郵箱中投入一疊明信片。你無法知道它們將以何種順序到達它們的目的地,或者它們是否所有的都能夠到達它們的目的地。
UDP的這些方面可能會讓你感覺到嚴重的局限性,但是它們也解釋了為何它會比TCP快那麼多:所有的握手以及消息管理機制的開銷都已經被消除了。顯然,UDP很適合那些能夠處理或者容忍消息丟失的應用程序,但可能不適合那些處理金融交易的應用程序[2]。
13.2 UDP廣播
到目前為止,我們所有的例子採用的都是一種叫作單播[3]的傳輸模式,定義為發送消息給一個由唯一的地址所標識的單一的網絡目的地。面向連接的協議和無連接協議都支持這種模式。
UDP提供了向多個接收者發送消息的額外傳輸模式:
- 多播——傳輸到一個預定義的主機組;
- 廣播——傳輸到網絡(或者子網)上的所有主機。
本章中的示例應用程序將通過發送能夠被同一個網絡中的所有主機所接收的消息來演示UDP廣播的使用。為此,我們將使用特殊的受限廣播地址或者零網絡地址255.255.255.255
。發送到這個地址的消息都將會被定向給本地網絡(0.0.0.0
)上的所有主機,而不會被路由器轉發給其他的網絡。
接下來,我們將討論該應用程序的設計。
13.3 UDP示例應用程序
我們的示例程序將打開一個文件,隨後將會通過UDP把每一行都作為一個消息廣播到一個指定的端口。如果你熟悉類UNIX操作系統,你可能會認識到這是標準的syslog實用程序的一個非常簡化的版本。UDP非常適合於這樣的應用程序,因為考慮到日誌文件本身已經被存儲在了文件系統中,因此,偶爾丟失日誌文件中的一兩行是可以容忍的。此外,該應用程序還提供了極具價值的高效處理大量數據的能力。
接收方是怎麼樣的呢?通過UDP廣播,只需簡單地通過在指定的端口上啟動一個監聽程序,便可以創建一個事件監視器來接收日誌消息。需要注意的是,這樣的輕鬆訪問性也帶來了潛在的安全隱患,這也就是為何在不安全的環境中並不傾向於使用UDP廣播的原因之一。出於同樣的原因,路由器通常也會阻止廣播消息,並將它們限制在它們的來源網絡上。
發佈/訂閱模式 類似於syslog這樣的應用程序通常會被歸類為發佈/訂閱模式:一個生產者或者服務發佈事件,而多個客戶端進行訂閱以接收它們。
圖13-1展示了整個系統的一個高級別視圖,其由一個廣播者以及一個或者多個事件監視器所組成。廣播者將監聽新內容的出現,當它出現時,則通過UDP將它作為一個廣播消息進行傳輸。
圖13-1 廣播系統概覽
所有的在該UDP端口上監聽的事件監視器都將會接收到廣播消息。
為了簡單起見,我們將不會為我們的示例程序添加身份認證、驗證或者加密。但是,要加入這些功能並使得其成為一個健壯的、可用的實用程序應該也不難。
在下一節中,我們將開始探討該廣播者組件的設計以及實現細節。
13.4 消息POJO: LogEvent
在消息處理應用程序中,數據通常由POJO表示,除了實際上的消息內容,其還可以包含配置或處理信息。在這個應用程序中,我們將會把消息作為事件處理,並且由於該數據來自於日誌文件,所以我們將它稱為LogEvent
。代碼清單13-1展示了這個簡單的POJO的詳細信息。
代碼清單13-1 LogEvent
消息
public final class LogEvent {
public static final byte SEPARATOR = (byte) ':';
private final InetSocketAddress source;
private final String logfile;
private final String msg;
private final long received;
public LogEvent(String logfile, String msg) { ← -- 用於傳出消息的構造函數
this(null, -1, logfile, msg);
}
public LogEvent(InetSocketAddress source, long received, ← -- 用於傳入消息的構造函數
String logfile, String msg) {
this.source = source;
this.logfile = logfile;
this.msg = msg;
this.received = received;
}
public InetSocketAddress getSource { ← -- 返回發送LogEvent 的源的InetSocketAddress
return source;
}
public String getLogfile { ← -- 返回所發送的LogEvent的日誌文件的名稱
return logfile;
}
public String getMsg { ← -- 返回消息內容
return msg;
}
public long getReceivedTimestamp { ← -- 返回接收LogEvent的時間
return received;
}
}
定義好了消息組件,我們便可以實現該應用程序的廣播邏輯了。在下一節中,我們將研究用於編碼和傳輸LogEvent
消息的Netty框架類。
13.5 編寫廣播者
Netty提供了大量的類來支持UDP應用程序的編寫。表13-1列出了我們將要使用的主要的消息容器以及Channel
類型。
表13-1 在廣播者中使用的Netty的UDP相關類
名 稱
描 述
interface AddressedEnvelope
<M, A extends SocketAddress>
extends ReferenceCounted
定義一個消息,其包裝了另一個消息並帶有發送者和接收者地址。其中M
是消息類型;A
是地址類型
class DefaultAddressedEnvelope
<M, A extends SocketAddress>
implements AddressedEnvelope<M,A>
提供了interface AddressedEnvelope
的默認實現
class DatagramPacket
extends DefaultAddressedEnvelope
<ByteBuf, InetSocketAddress>
implements ByteBufHolder
擴展了DefaultAddressedEnvelope
以使用ByteBuf
作為消息數據容器
interface DatagramChannel
extends Channel
擴展了Netty的Channel
抽像以支持UDP的多播組管理
class NioDatagramChannnel
extends AbstractNioMessageChannel
implements DatagramChannel
定義了一個能夠發送和接收Addressed- Envelope
消息的Channel
類型
Netty的DatagramPacket
是一個簡單的消息容器,DatagramChannel
實現用它來和遠程節點通信。類似於在我們先前的類比中的明信片,它包含了接收者(和可選的發送者)的地址以及消息的有效負載本身。
要將LogEvent
消息轉換為DatagramPacket
,我們將需要一個編碼器。但是沒有必要從頭開始編寫我們自己的。我們將擴展Netty的MessageToMessageEncoder
,在第10章和第11章中我們已經使用過了。
圖13-2展示了正在廣播的3個日誌條目,每一個都將通過一個專門的DatagramPacket
進行廣播。
圖13-2 通過DatagramPacket
發送的日誌條目
圖13-3呈現了該LogEventBroadcaster
的ChannelPipeline
的一個高級別視圖,展示了LogEvent
消息是如何流經它的。
圖13-3 LogEventBroadcaster
:ChannelPipeline
和LogEvent
事件流
正如你所看到的,所有的將要被傳輸的數據都被封裝在了LogEvent
消息中。LogEvent-Broadcaster
將把這些寫入到Channel
中,並通過ChannelPipeline
發送它們,在那裡它們將會被轉換(編碼)為DatagramPacket
消息。最後,他們都將通過UDP被廣播,並由遠程節點(監視器)所捕獲。
代碼清單13-2展示了我們自定義版本的MessageToMessageEncoder
,其將執行剛才所描述的轉換。
代碼清單13-2 LogEventEncoder
public class LogEventEncoder extends MessageToMessageEncoder<LogEvent> {
private final InetSocketAddress remoteAddress;
public LogEventEncoder(InetSocketAddress remoteAddress) { ← -- LogEventEncoder 創建了即將被發送到指定的InetSocketAddress 的DatagramPacket 消息
this.remoteAddress = remoteAddress;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext,
LogEvent logEvent, List<Object> out) throws Exception {
byte file = logEvent.getLogfile.getBytes(CharsetUtil.UTF_8);
byte msg = logEvent.getMsg.getBytes(CharsetUtil.UTF_8);
ByteBuf buf = channelHandlerContext.alloc
.buffer(file.length + msg.length + 1);
buf.writeBytes(file); ← -- 將文件名寫入到ByteBuf 中
buf.writeByte(LogEvent.SEPARATOR); ← -- 添加一個SEPARATOR
buf.writeBytes(msg); ← -- 將日誌消息寫入ByteBuf 中
out.add(new DatagramPacket(buf, remoteAddress)); ← -- 將一個擁有數據和目的地地址的新DatagramPacket添加到出站的消息列表中
}
}
在LogEventEncoder
被實現之後,我們已經準備好了引導該服務器,其包括設置各種各樣的ChannelOption
,以及在ChannelPipeline
中安裝所需要的ChannelHandler
。這將通過主類LogEventBroadcaster
完成,如代碼清單13-3所示。
代碼清單13-3 LogEventBroadcaster
public class LogEventBroadcaster {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
private final File file;
public LogEventBroadcaster(InetSocketAddress address, File file) {
group = new NioEventLoopGroup;
bootstrap = new Bootstrap;
bootstrap.group(group).channel(NioDatagramChannel.class) ← -- 引導該NioDatagram-Channel(無連接的)
.option(ChannelOption.SO_BROADCAST, true) ← -- 設置SO_BROADCAST套接字選項
.handler(new LogEventEncoder(address));
this.file = file;
}
public void run throws Exception {
Channel ch = bootstrap.bind(0).sync.channel; ← -- 綁定Channel
long pointer = 0;
for (;;) { ← -- 啟動主處理循環
long len = file.length;
if (len < pointer) {
// file was reset
pointer = len; ← -- 如果有必要,將文件指針設置到該文件的最後一個字節
} else if (len > pointer) {
// Content was added
RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(pointer); ← -- 設置當前的文件指針,以確保沒有任何的舊日誌被發送
String line;
while ((line = raf.readLine) != null) {
ch.writeAndFlush(new LogEvent(null, -1, ← -- 對於每個日誌條目,寫入一個LogEvent到Channel 中
file.getAbsolutePath, line));
}
pointer = raf.getFilePointer; ← -- 存儲其在文件中的當前位置
raf.close;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) { ← -- 休眠1 秒,如果被中斷,則退出循環;否則重新處理它
Thread.interrupted;
break;
}
}
}
public void stop {
group.shutdownGracefully;
}
public static void main(String args) throws Exception {
if (args.length != 2) {
throw new IllegalArgumentException;
}
LogEventBroadcaster broadcaster = new LogEventBroadcaster( ← -- 創建並啟動一個新的LogEventBroadcaster的實例
new InetSocketAddress("255.255.255.255",
Integer.parseInt(args[0])), new File(args[1]));
try {
broadcaster.run;
}
finally {
broadcaster.stop;
}
}
}
這樣就完成了該應用程序的廣播者組件。對於初始測試,你可以使用netcat程序。在UNIX/Linux系統中,你能發現它已經作為nc被預裝了。用於Windows的版本可以從http://nmap.org/ncat獲取[4]。
netcat非常適合於對這個應用程序進行基本的測試;它只是監聽某個指定的端口,並且將所有接收到的數據打印到標準輸出。可以通過下面所示的方式,將其設置為監聽UDP端口9999上的數據:
$ nc -l -u -p 9999
現在我們需要啟動我們的LogEventBroadcaster
。代碼清單13-4展示了如何使用mvn
來編譯和運行該廣播者應用程序。pom.xml
文件中的配置指向了一個將被頻繁更新的文件,/var/log/messages
(假設是一個UNIX/Linux環境),並將端口設置為了9999。該文件中的條目將會通過UDP廣播到那個端口,並在你啟動了netcat的終端上打印出來。
代碼清單13-4 編譯和啟動LogEventBroadcaster
$ chapter13> mvn clean package exec:exec LogEventBroadcaster
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------------------------------------------------
[INFO] Building UDP Broadcast 1.0-SNAPSHOT
[INFO] --------------------------------------------------------------------
...
...
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
[INFO] Building jar: target/chapter13-1.0-SNAPSHOT.jar
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action –
LogEventBroadcaster running
要改變該日誌文件和端口值,可以在啟動mvn
的時候通過System
屬性來指定它們。代碼清單13-5展示了如何將日誌文件設置為/var/log/mail.log
,並將端口設置為8888
。
代碼清單13-5 編譯和啟動LogEventBroadcaster
$ chapter13> mvn clean package exec:exec -PLogEventBroadcaster /
-Dlogfile=/var/log/mail.log –Dport=8888 –....
....
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action –
LogEventBroadcaster running
當你看到LogEventBroadcaster running
時,你便知道它已經成功地啟動了。如果有錯誤發生,將會打印一個異常消息。一旦這個進程運行起來,它就會廣播任何新被添加到該日誌文件中的日誌消息。
使用netcat對於測試來說是足夠了,但是它並不適合於生產系統。這也就有了我們的應用程序的第二個部分——我們將在下一節中實現的廣播監視器。
13.6 編寫監視器
我們的目標是將netcat替換為一個更加完整的事件消費者,我們稱之為LogEventMonitor
。這個程序將:
(1)接收由LogEventBroadcaster
廣播的UDP DatagramPacket
;
(2)將它們解碼為LogEvent
消息;
(3)將LogEvent
消息寫出到System.out
。
和之前一樣,該邏輯由一組自定義的ChannelHandler
實現——對於我們的解碼器來說,我們將擴展MessageToMessageDecoder
。圖13-4描繪了LogEventMonitor
的Channel-Pipeline
,並且展示了LogEvent
是如何流經它的。
圖13-4 LogEventMonitor
ChannelPipeline
中的第一個解碼器LogEventDecoder
負責將傳入的DatagramPacket
解碼為LogEvent
消息(一個用於轉換入站數據的任何Netty應用程序的典型設置)。代碼清單13-6展示了該實現。
代碼清單13-6 LogEventDecoder
public class LogEventDecoder extends MessageToMessageDecoder<DatagramPacket> {
@Override
protected void decode(ChannelHandlerContext ctx,
DatagramPacket datagramPacket, List<Object> out) throws Exception { ← -- 獲取對DatagramPacket 中的數據(ByteBuf)的引用
ByteBuf data = datagramPacket.content;
int idx = data.indexOf(0, data.readableBytes, ← -- 獲取該SEPARATOR的索引
LogEvent.SEPARATOR);
String filename = data.slice(0, idx) ← -- 提取文件名
.toString(CharsetUtil.UTF_8);
String logMsg = data.slice(idx + 1, ← -- 提取日誌消息
data.readableBytes).toString(CharsetUtil.UTF_8);
LogEvent event = new LogEvent(datagramPacket.sender, ← -- 構建一個新的LogEvent 對象,並且將它添加到(已經解碼的消息的)列表中
System.currentTimeMillis, filename, logMsg);
out.add(event);
}
}
第二個ChannelHandler
的工作是對第一個ChannelHandler
所創建的LogEvent
消息執行一些處理。在這個場景下,它只是簡單地將它們寫出到System.out
。在真實世界的應用程序中,你可能需要聚合來源於不同日誌文件的事件,或者將它們發佈到數據庫中。代碼清單13-7展示了LogEventHandler
,其說明了需要遵循的基本步驟。
代碼清單13-7 LogEventHandler
public class LogEventHandler
extends SimpleChannelInboundHandler<LogEvent> { ← -- 擴展SimpleChannelInbound-Handler 以處理LogEvent 消息
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
cause.printStackTrace; ← -- 當異常發生時,打印棧跟蹤信息,並關閉對應的Channel
ctx.close;
}
@Override
public void channelRead0(ChannelHandlerContext ctx,
LogEvent event) throws Exception {
StringBuilder builder = new StringBuilder; ← -- 創建StringBuilder,並且構建輸出的字符串
builder.append(event.getReceivedTimestamp);
builder.append(" [");
builder.append(event.getSource.toString);
builder.append("] [");
builder.append(event.getLogfile);
builder.append("] : ");
builder.append(event.getMsg);
System.out.println(builder.toString); ← -- 打印LogEvent的數據
}
}
LogEventHandler
將以一種簡單易讀的格式打印LogEvent
消息,包括以下的各項:
- 以毫秒為單位的被接收的時間戳;
- 發送方的
InetSocketAddress
,其由IP地址和端口組成; - 生成
LogEvent
消息的日誌文件的絕對路徑名; - 實際上的日誌消息,其代表日誌文件中的一行。
現在我們需要將我們的LogEventDecoder
和LogEventHandler
安裝到ChannelPipeline
中,如圖13-4所示。代碼清單13-8展示了如何通過LogEventMonitor
主類來做到這一點。
代碼清單13-8 LogEventMonitor
public class LogEventMonitor {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
public LogEventMonitor(InetSocketAddress address) {
group = new NioEventLoopGroup;
bootstrap = new Bootstrap;
bootstrap.group(group) ← -- 引導該NioDatagramChannel
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true) ← -- 設置套接字選項SO_BROADCAST
.handler( new ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline;
pipeline.addLast(new LogEventDecoder); ← -- 將LogEventDecoder 和LogEventHandler 添加到ChannelPipeline 中
pipeline.addLast(new LogEventHandler);
}
} )
.localAddress(address);
}
public Channel bind {
return bootstrap.bind.syncUninterruptibly.channel; ← -- 綁定Channel。 注意,DatagramChannel 是無連接的
}
public void stop {
group.shutdownGracefully;
}
public static void main(String main) throws Exception {
if (args.length != 1) {
throw new IllegalArgumentException(
"Usage: LogEventMonitor <port>");
}
LogEventMonitor monitor = new LogEventMonitor( ← -- 構造一個新的LogEventMonitor
new InetSocketAddress(Integer.parseInt(args[0])));
try {
Channel channel = monitor.bind;
System.out.println("LogEventMonitor running");
channel.closeFuture.sync;
} finally {
monitor.stop;
}
}
}
13.7 運行LogEventBroadcaster和LogEventMonitor
和之前一樣,我們將使用Maven來運行該應用程序。這一次你將需要打開兩個控制台窗口,每個都將運行一個應用程序。每個應用程序都將會在直到你按下了Ctrl+C組合鍵來停止它之前一直保持運行。
首先,你需要啟動LogEventBroadcaster
,因為你已經構建了該工程,所以下面的命令應該就足夠了(使用默認值):
$ chapter13> mvn exec:exec -PLogEventBroadcaster
和之前一樣,這將通過UDP協議廣播日誌消息。
現在,在一個新窗口中,構建並且啟動LogEventMonitor
以接收和顯示廣播消息,如代碼清單13-9所示。
代碼清單13-9 編譯並啟動LogEventBroadcaster
$ chapter13> mvn clean package exec:exec -PLogEventMonitor
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------------------------------------------------
[INFO] Building UDP Broadcast 1.0-SNAPSHOT
[INFO] --------------------------------------------------------------------
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
[INFO] Building jar: target/chapter14-1.0-SNAPSHOT.jar
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action ---
LogEventMonitor running
當你看到LogEventMonitor running
時,你將知道它已經成功地啟動了。如果有錯誤發生,則將會打印異常信息。
如代碼清單13-10所示,當任何新的日誌事件被添加到該日誌文件中時,該終端都會顯示它們。消息的格式則是由LogEventHandler
創建的。
代碼清單13-10 LogEventMonitor
的輸出
1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:55:08
dev-linux dhclient: DHCPREQUEST of 192.168.0.50 on eth2 to 192.168.0.254
port 67
1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:55:08
dev-linux dhclient: DHCPACK of 192.168.0.50 from 192.168.0.254
1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:55:08
dev-linux dhclient: bound to 192.168.0.50 -- renewal in 270 seconds.
1364217299382 [/192.168.0.38:63182] [[/var/log/messages] : Mar 25 13:59:38
dev-linux dhclient: DHCPREQUEST of 192.168.0.50 on eth2 to 192.168.0.254
port 67
1364217299382 [/192.168.0.38:63182] [/[/var/log/messages] : Mar 25 13:59:38
dev-linux dhclient: DHCPACK of 192.168.0.50 from 192.168.0.254
1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:59:38
dev-linux dhclient: bound to 192.168.0.50 -- renewal in 259 seconds.
1364217299383 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 14:03:57
dev-linux dhclient: DHCPREQUEST of 192.168.0.50 on eth2 to 192.168.0.254
port 67
1364217299383 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 14:03:57
dev-linux dhclient: DHCPACK of 192.168.0.50 from 192.168.0.254
1364217299383 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 14:03:57
dev-linux dhclient: bound to 192.168.0.50 -- renewal in 285 seconds.
如果你不能訪問UNIX的syslog,那麼你可以創建一個自定義的文件,並手動提供內容以觀測該應用程序的反應。以使用touch
命令來創建一個空文件作為開始,下面所展示的步驟使用了UNIX命令。
$ touch ~/mylog.log
現在再次啟動LogEventBroadcaster
,並通過設置系統屬性來將其指向該文件:
$ chapter13> mvn exec:exec -PLogEventBroadcaster -Dlogfile=~/mylog.log
一旦LogEventBroadcaster
運行,你就可以手動將消息添加到該文件中,以在LogEventMonitor
終端中查看廣播輸出。使用echo
命令並將輸出重定向到該文件,如下所示:
$ echo 'Test log entry' >> ~/mylog.log
你可以根據需要啟動任意多的監視器實例,它們每一個都將接收並顯示相同的消息。
13.8 小結
在本章中,我們使用UDP作為例子介紹了無連接協議。我們構建了一個示例應用程序,其將日誌條目轉換為UDP數據報並廣播它們,隨後這些被廣播出去的消息將被訂閱的監視器客戶端所捕獲。我們的實現使用了一個POJO來表示日誌數據,並通過一個自定義的編碼器來將這個消息格式轉換為Netty的DatagramPacket
。這個例子說明了Netty的UDP應用程序可以很輕鬆地被開發和擴展用以支持專業化的用途。
在接下來的兩章中,我們將把目光投向由知名公司的用戶所提供的案例研究上,他們已使用Netty構建了工業級別的應用程序。
[1] 最有名的基於UDP的協議之一便是域名服務(DNS),其將完全限定的名稱映射為數字的IP地址。
[2] 基於UDP協議實現的一些可靠傳輸協議可能不在此範疇內,如Quic、Aeron和UDT。——譯者注
[3] 參見http://en.wikipedia.org/wiki/Unicast。
[4] 也可以使用scoop install netcat
。——譯者注