本章包含
- UDP 预览
- 广播应用示例
到目前为止,您看到的大多数示例都使用了基于连接的协议,例如TCP。 在本章中,我们将重点介绍无连接协议,即用户数据报协议(UDP),它通常在性能至关重要且可以容忍某些数据包丢失时使用。
我们首先概述UDP,其特性和局限性。 接下来我们将描述本章的示例应用程序,它将演示如何使用UDP的广播功能。 我们还将利用编码器和解码器来处理POJO作为广播消息格式。 到本章结束时,您将准备好在自己的应用程序中使用UDP。
注: 基于UDP协议最出名的就是域名服务了(Domain Name Service).将完全限定名称映射到数字IP地址。
13.1 UDP basics
面向连接的传输(如TCP)管理两个网络端点之间的连接建立,在连接的生命周期内确保发送的消息的有序和可靠传输,最后,有序地终止连接。 相比之下,在像UDP这样的无连接协议中,没有持久连接的概念,每个消息(UDP数据报)都是独立的传输。
此外,UDP没有TCP的纠错机制,其中每个对等体确认它接收的分组,并且发送方重新发送未确认的分组。
通过类比,TCP连接就像电话会话,其中一系列有序消息在两个方向上流动。 相反,UDP类似于往邮箱中塞一堆明信片。 您无法知道他们到达目的地的顺序,也无法确保他们都将到达。
UDP的这些方面可能会给您带来严重的限制,但它们也解释了为什么它比TCP快得多:所有的握手和消息管理开销都已被消除。 显然,与处理金融交易的应用程序不同,UDP非常适合处理或容忍丢失消息的应用程序。
13.2 UDP broadcast
到目前为止,我们的所有示例都使用了称为单播的传输模式 unicast,定义为将消息发送到由唯一地址标识的单个网络目标。 连接和无连接协议都支持此模式。
UDP提供了额外的传输模式,用于向多个收件人发送 message:
- Multicast —— 传输到定义的一组 hosts
- Broadcast —— 传输到一个网络中的所有 hosts(或者一个 subnet)
本章中的示例应用程序将通过发送可由同一网络上的所有主机接收的消息来演示UDP广播的使用。 为此,我们将使用特殊的有限广播或零网络地址255.255.255.255。 发送到此地址的消息发往本地网络上的所有主机(0.0.0.0),并且永远不会被路由器转发到其他网络。
下面我们来讨论如何设计这个程序
13.3 The UDP sample application
我们的示例应用程序将打开一个文件,并通过UDP将每一行作为消息广播到指定的端口。 如果您熟悉类UNIX操作系统,您可能会认为这是标准syslog实用程序的一个非常简化的版本。 UDP非常适合这样的应用程序,因为只要文件本身存储在文件系统中,就可以容忍偶尔丢失一行日志文件。 此外,该应用程序提供了有效处理大量数据的非常有价值的能力。
接收器怎么样? 使用UDP广播,您可以创建一个事件监视器,只需通过在指定端口上启动侦听程序即可接收日志消息。 请注意,这种易于访问会引起潜在的安全问题,这是UDP广播不会在不安全的环境中使用的一个原因。 出于同样的原因,路由器经常阻止广播消息,将它们限制在它们发起的网络中。
Publish/Subscribe syslog等应用程序通常归类为publish / subscribe:生产者或服务发布事件,多个客户订阅接收它们。
图14.1显示了整个系统的高级视图,它由一个广播公司和一个或多个事件监视器组成。 广播公司监听要出现的新内容,当它出现时,通过UDP将其作为广播消息发送。
监听UDP端口的所有事件监视器都接收广播消息。
为简单起见,我们不会在示例应用程序中添加身份验证,验证或加密。 但要结合这些功能以使其成为一个强大,可用的实用程序并不困难。
在下一节中,我们将开始探索广播组件的设计和实现细节。
13.4 The message POJO: LogEvent
在消息传递应用程序中,数据通常由POJO表示,POJO除了实际的消息内容之外还可以保存配置或处理信息。 在这个应用程序中,我们将消息作为事件处理,并且因为数据来自日志文件,我们将其称为LogEvent。 清单14.1显示了这个简单POJO的细节。
// LogEvent message
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;
// Constructor for an outgoing message
public LogEvent(String logfile, String msg) {
this(null, -1, logfile, msg);
}
// Constructor for an incoming message
public LogEvent(InetSocketAddress source, long received,
String logfile, String msg) {
this.source = source;
this.logfile = logfile;
this.msg = msg;
this.received = received;
}
// Returns the InetSocketAddress of the source that sent the LogEvent
public InetSocketAddress getSource() {
return source;
}
// Returns the name if the log file for which the LogEvent was sent
public String getLogfile() {
return logfile;
}
// Returns the message contents
public String getMsg() {
return msg;
}
// Returns the time at which the logEvent was received
public long getReceivedTimestamp() {
return received;
}
}
通过定义消息组件,我们可以实现应用程序的广播逻辑。 在下一节中,我们将研究用于编码和传输LogEvent消息的Netty框架类。
13.5 Writing the broadcaster
Netty提供了许多类来支持UDP应用程序的编写。 我们将使用的主要是表14.1中列出的消息容器和通道类型。
名称 | 描述 |
---|---|
interface AddressedEnvelope<M, A extends SocketAddress> extends ReferenceCounted | 定义包含发件人和收件人地址的 message。 M是消息类型; A是地址类型。 |
class DefaultAddressedEnvelope<M, A extends SocketAddress> implements AddressedEnvelope<M, A> | 提供了接口 AddressedEnvelope 的一种默认实现。 |
class DatagramPacket extendsDefaultAddressedEnvelope<ByteBuf, InetSocketAddress> implements ByteBufHolder | 扩展DefaultAddressedEnvelope以使用ByteBuf作为消息数据容器。 |
interface DatagramChannel extends Channel | 扩展Netty的Channel 抽象类以支持UDP组播组管理。 |
class NioDatagramChannel extends AbstractNioMessageChannel implements DatagramChannel | 定义可以发送和接收AddressedEnvelope消息的Channel类型。 |
Netty的DatagramPacket是一个简单的消息容器,由DatagramChannel实现用于与远程对等体通信。 就像我们在之前的类比中提到的明信片一样,它带有收件人的地址(以及可选的发件人)以及消息有效负载本身。
要将EventLog消息转换为DatagramPacket,我们需要一个编码器。 但是没有必要从头开始编写我们自己的。 我们将扩展Netty的MessageToMessageEncoder,我们在第9章和第10章中使用了它。
图14.2显示了三个日志条目的广播,每个日志条目都通过专用的DatagramPacket进行。
图14.3表示LogEventBroadcaster的ChannelPipeline的高级视图,显示了LogEvent如何流经它。
如您所见,所有要传输的数据都封装在LogEvent消息中。 LogEventBroadcaster将这些文件写入通道,通过ChannelPipeline将它们转换(编码)到DatagramPacket消息中。 最后,它们通过UDP广播并由远程对等体(监视器)拾取。
下一个清单显示了MessageToMessageEncoder的自定义版本,它执行刚刚描述的转换。
// LogEventEncoder
public class LogEventEncoder extends MessageToMessageEncoder<LogEvent> {
private final InetSocketAddress remoteAddress;
// LogEventEncoder creates DatagramPacket messages to be sent to the specified InetSocketAddress
public LogEventEncoder(InetSocketAddress remoteAddress) {
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);
// Writes the filename to the ByteBuf
buf.writeBytes(file);
// Adds a SEPARATOR
buf.writeBytes(LogEvent.SEPARATOR);
// Writes the log message to the ByteBuf
buf.writeBytes(msg);
// Adds a new DatagramPacket with the data and destination address to the list of outbound messages
out.add(new DatagramPacket(buf, remoteAddress));
}
}
通过实现LogEventEncoder,我们已准备好引导服务器,包括设置各种ChannelOption并在管道中安装所需的ChannelHandler。 这将由主要类LogEventBroadcaster完成,如下所示。
// 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();
// Bootstraps the NioDatagramChannel(connectionless)
// Sets the SO_BROADCAST socket option
bootstrap.group(group).channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new LogEventEncoder(address));
this.file = file;
}
public void run() throws Exception {
//Binds the channel
Channel ch = bootstrap.bind(0).sync().channel();
long pointer = 0;
// Starts the main processing loop
for(;;) {
long len = file.length();
if(len<pointer) {
//If necessary, sets the file pointer to the last bytes of the file
// file was reset
pointer = len;
} else if (len>pointer) {
// Content was added
RandomAccessFile raf = new RandomAccessFile(file, "r");
// Sets the current file pointer so nothing old is sent
raf.seek(pointer);
String line;
while((line = raf.readLine()) != null) {
// For each log entry, writes a LogEvent to the channel
ch.writeAndFlush(new LogEvent(null, -1, file.getAbsolutePath(), line));
file.getAbsolutePath(), line));
}
// Stores the current position within the file
pointer = raf.getFilePointer();
raf.close();
}
try {
// Sleeps for 1 second, If interrupted, exits the loop; else restarts it.
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.interrupted();
break;
}
}
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
if(args.length != 2) {
throw new IllegalArgumentException();
}
// Creates and starts a new LogEventBroadcaster instance
LogEventBroadcaster broadcaster = new 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获得。
netcat非常适合此应用程序的基本测试; 它只是侦听指定的端口并将收到的所有数据打印到标准输出。 将其设置为在端口9999上侦听UDP数据,如下所示:
nc -l -u 9999
现在我们需要启动LogEventBroadcaster。 清单14.4显示了如何使用mvn编译和运行广播器。 pom.xml中的配置指向经常更新的文件/ var / log / messages(假设是UNIX / L inux环境),并将端口设置为9999.文件中的条目将通过UDP广播到该端口,并打印到启动netcat的控制台。
要更改文件和端口值,请在调用mvn时将它们指定为系统属性。 下一个清单显示了如何将日志文件设置为/var/log/mail.log,将端口设置为8888。
当您看到LogEventBroadcaster正在运行时,您将知道它已成功启动。 如果有错误,将打印一条异常消息。 进程运行后,它将广播添加到日志文件中的所有新日志消息。
使用netcat足以用于测试目的,但它不适合生产系统。 这将我们带到我们的应用程序的第二部分 - 我们将在下一节中实现的广播监视器。
13.6 Writing the monitor
我们的目标是用更完整的事件使用者替换netcat,我们称之为EventLogMonitor。 这个程序会
- 接收LogEventBroadcaster广播的UDP DatagramPackets。
- 将它们解码为LogEvent消息。
- 将 LogEvent messages 写到 System.out
就像以前一样,逻辑将由自定义ChannelHandlers实现 —— 对于我们的解码器,我们将扩展MessageToMessageDecoder。 图14.4描述了LogEventMonitor的ChannelPipeline,并显示了LogEvent将如何流过它。
public class LogEventDecoder extends MessageToMessageDecoder<DatagramPacket> {
@Override
protected void decode(ChannelHandlerContext ctx,
DatagramPacket datagramPacket, List<Object> out) throws Exception {
//Gets a reference to the data in the DatagramPacket(a ByteBuf)
ByteBuf data = datagramPacket.data();
// Gets the index of the SEPARATOR
int idx = data.indexOf(0, data.readableBytes(), LogEvent.SEPARATOR);
// Extracts the filename
String filename = data.slice(0, idx)
.toString(CharsetUtil.UTF_8);
// Extracts the log message
String logMsg = data.slice(idx + 1, data.readableBytes()).toString(CharsetUtil.UTF_8);
// Constructs a new LogEvent object and adds it to the list.
LogEvent event = new LogEvent(datagramPacket.remoteAddress(),
System.currentTimeMillis(), filename, logMsg);
out.add(event);
}
}
第二个ChannelHandler的工作是对第一个创建的LogEvent消息执行一些处理。 在这种情况下,它只是将它们写入System.out。 在实际应用程序中,您可以将它们与源自不同日志文件的事件聚合在一起,或将它们发布到数据库中。 此列表显示了LogEventHandler,说明了要遵循的基本步骤。
// Extends SimpleChannelInboundHandler to handleLogEvent messages
public class LogEventHandler
extends SimpleChannelInboundHandler<LogEvent> {
// On exception, prints the stack trace and closes the channel
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void chanelRead0(ChannelHandlerContext ctx,
LogEvent event) throws Exception {
// Creates a StringBuilder and builds up the output
StringBuilder builder = new StringBuilder();
builder.append(event.getReceivedTimestamp());
builder.append(" [");
builder.append(event.getSource().toString());
builder.append("] [");
builder.append(event.getLogfile());
builder.append("] : ");
builder.apend(event.getMsg());
// Prints out the LogEvent data
System.out.println(builder.toString());
}
}
LogEventHandler以易于阅读的格式打印LogEvent,其格式如下:
- 收到的时间戳,以毫秒(milliseconds)为单位
- 发送方的InetSocketAddress,由IP地址和端口组成
- 生成LogEvent的文件的绝对名称
- 实际的日志消息,表示日志文件中的一行
现在我们需要在ChannelPipeline中安装我们的处理程序,如图14.4所示。 此列表显示了LogEventMonitor主类如何完成它。
// LogEventMonitor
public class LogEventMonitor {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
public LogEventMonitor(InetSocketAddress address) {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
// Bootstraps the NioDatagramChannel
bootstrap.group(group)
.channel(NioDatagramChannel.class)
// sets the SO_BROADCAST socket options
.option(ChannelOption.SO_BROADCAST, true)
.handler( new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channle channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline();
// Adds the ChannelHanders to the ChannelPipeline
pipeline.addLast(new LogEventDecoder());
pipeline.addLast(new LogEventHandler());
}
}).localAddress(address);
}
public Channel bind() {
//Binds the channel.Note that DatagramChannel is connectionless.
return bootstrap.bind().sync().channel();
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] main) throws Exception {
if(args.length != 1) {
throw new IllegalArgumentException("Useage: LogEventMonitor <port>");
}
// Constructs a new LogEventMonitor
LogEventMonitor monitor = new LogEventMonitor(new InetSocketAddress(args[0]));
try {
Channel channel = monitor.bind();
System.out.println("LogEventMonitor running");
channel.closeFuture().sync();
} finally {
monitor.stop();
}
}
}
13.7 Running the LogEventBroadcaster and LogEventMonitor
和以前一样,我们将使用Maven来运行应用程序。 这次你需要打开两个控制台窗口,每个窗口对应一个程序。 每个都将继续运行,直到您使用Ctrl-C停止它。
首先,您需要启动LogEventBroadcaster。 因为您已经构建了项目,所以以下命令就足够了(使用默认值):
$ chapter14> mvn exec:exec -PLogEventBroadcaster
和以前一样,这将通过UDP广播日志消息。
现在,在新窗口中,构建并启动LogEventMonitor以接收和显示广播消息。
当您看到LogEventMonitor正在运行时,您将知道它已成功启动。 如果出现错误,将打印一条异常消息。
控制台将显示添加到日志文件中的任何事件,如下所示。 消息的格式是由LogEventHandler创建的。
如果您无权访问UNIX syslog,则可以创建自定义文件并手动提供内容以查看应用程序的运行情况。 接下来显示的步骤使用UNIX命令,从touch开始创建一个空文件。
$ touch ~/mylog.log
现在再次启动LogEventBroadcaster并通过设置系统属性将其指向该文件:
$ chapter14> mvn exec:exec -PLogEventBroadcaster -Dlogfile=~/mylog.log
LogEventBroadcaster运行后,您可以手动将消息添加到文件中,以在LogEventMonitor控制台中查看广播输出。 使用echo并将输出重定向到文件,如下所示:
# echo 'Test log entry' >> ~/mylog.log
您可以根据需要启动任意数量的监视器实例; 每个人都会收到并显示相同的消息。
13.8 Summary
在本章中,我们以UDP为例介绍了无连接协议。 我们构建了一个示例应用程序,它将日志条目转换为UDP数据报并广播它们以供订阅的监视器客户端选取。 我们的实现使用POJO来表示日志数据,并使用自定义编码器将此消息格式转换为Netty的DatagramPacket。 该示例说明了可以轻松地开发和扩展Netty UDP应用程序以支持专门用途。
在接下来的两章中,我们将介绍使用Netty构建工业级应用程序的知名公司用户提供的案例研究。