本章主要内容:
在实际开发应用之前,我们需要先花一些时间来学习UDP协议,因为在使用它之前,我们至少得了解它是什么,它能干什么以及它的优点和缺点。
前面说过,UDP是无连接的协议,也就是说在客户端和服务端使用UDP通信的过程中是没有连接的,所以很难说这个消息属于哪个连接。
UDP的一个限制就是没有像TCP那样的纠错能力。TCP协议中发送端发送一个包之后,接收端会返回一个消息表示我收到了这个包,当发送端没收到接收端的确认信息时发送端就会重发这个包。UDP就不是这样的,UDP发送包之后立即就不管了,继续发送下面的包,所以UDP的发送端并不知道接收端有没有收到包,也就是说UDP只管发,接收端收没收到就是不是它的事了。
所以,UDP很适合那些数据量大但是丢包又对应用无太大影响的应用情况,例如视频类应用。当然,如果数据准确性很高,消息不能丢失类型的应用是绝对不能使用UDP的,这种情况应该使用TCP类协议,例如金钱交易类型的应用。
接下来的章节我们会开发一个UDP应用,主要逻辑就是通过UDP将数据转发给其他客户端。这个应用的主要功能就是监测一个文件,通过UDP转发新增的行内容。
如果你很数据类UNIX系统,则会很熟悉这个功能,和Syslog的功能很像。
UDP协议很适合这种类型应用,因为你即使丢失几行数据没有转发,也不会对应用产生致命影响。最重要的是,日志量很大时,需要靠UDP这种高性能的协议来达到应用需求。
这个应用的另一个特点是,新增一个文件监视器不需要额外的操作。只需要启动一个实例,然后指定端口就可以看到文件内容了。不过有时候这也不一定是好事,只要能启动实例就能获取到内容了,相对来说有一些不安全。所以一般UDP广播数据时都是在安全的内网环境中使用。另外一般广播数据的服务端和客户端都在同一个网络环境中,一旦夸网络经过路由,可能就会被拦截掉,防火墙会误判。这种模式一般被成为“发布-订阅”模式,服务端发布数据,一个或多个客户端订阅数据。
写代码之前,先看看应用的基本结构。
这个应用主要由两部分组成。其中一个用来监测文件内容,一般就是一个实例,类似服务端;另一个事件监测器用来获取服务端转发的文件内容,由一个或多个实例组成。
为了节省时间,实际项目中的安全验证、过滤等功能就不再这里说了。
如果你觉得这个应用还不错,某些代码很适合你,你可以适当调整满足自己的需求。得益于Netty的设计,逻辑分散,统一的API,所以修改起来还是很容易的。
后面还会介绍一些UDP的基础知识,来看看它和基于TCP的应用的不同。
大家如果编写过其他类型的应用,特别是使用面向对象的语言的时候,如C++、JAVA,会定义很多对象来保存传输的消息类容。所以这个应用的EventLog也可以理解成消息对象。它会在客户端与服务端之间分享,存储数据,并生成日志文件。
package com.nan.netty.udp;
import java.net.InetSocketAddress;
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() {
return source;
}
public String getLogfile() {
return logfile;
}
public String getMsg() {
return msg;
}
public long getReceivedTimestamp() {
return received;
}
}
定义好了这个消息实体,下一步该实现具体的逻辑了。下一小节我们就开始编写Broadcaster,并了解这个Broadcaster是如何工作的。
这个应用的核心就是广播消息,所以我们的重点就是广播。而广播的消息就是通过DatagramPacket包装。
从上图可以看出,每一条消息都对应一个DatagramPacket。基本上所有基于Netty的应用都会包含一个或多个ChannelHandler通过启动器配置到Channel中。我们来看看这个应用的ChannelPipeline的基本结构。
LogEventBroadcaster将数据放到LogEvent中并通过Channel发送出去。也就是说消息都封装在了LogEvent中,然后通过Channel发送给客户端。
在ChannelPipeline中,LogEvent消息会被编码成DatagramPacket消息,最终通过UDP发送出去的就是这个DatagramPacket。
所以这里我们需要自定义一个ChannelHandler,用来将LogEvent编码成DatagramPacket,通过前面的知识,这个编码还是很容易编写的。因为这里其实是两个对象转换,所以使用MessageToMessageEncoder比较适合。
package com.nan.netty.udp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.util.CharsetUtil;
import java.net.InetSocketAddress;
import java.util.List;
public class LogEventEncoder extends MessageToMessageEncoder {
private final InetSocketAddress remoteAddress;
public LogEventEncoder(InetSocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, LogEvent logEvent, List
接下来就可以编写启动器,包括Channel的配置,连接的配置等。
package com.nan.netty.udp;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
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();
//这里使用NioDatagramChannel,因为是广播地址所以下面的SO_BROADCAST属性要设置为true
bootstrap.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new LogEventEncoder(address));
this.file = file;
}
public void run() throws IOException {
Channel ch = bootstrap.bind(0).syncUninterruptibly().channel();
long pointer = 0;
while (true) {
long len = file.length();
if (len < pointer) {
pointer = len;
} else if (len > pointer) {
//长度大于上一次读取的位置,则说明文件增加了内容
RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(pointer);
String line;
while ((line = raf.readLine()) != null) {
ch.writeAndFlush(new LogEvent(null, -1, file.getName(), line));
}
pointer = raf.getFilePointer();
raf.close();
}
try {
//每个1秒检查一次文件内容
Thread.sleep(1000);
} catch (Exception e) {
Thread.interrupted();
break;
}
}
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] args) throws Exception {
int port = 9090;
LogEventBroadcaster broadcaster = new LogEventBroadcaster(new InetSocketAddress("255.255.255.255", port),
new File("C:/Users/wangj/Desktop/udp.txt"));
try {
broadcaster.run();
} finally {
broadcaster.stop();
}
}
}
这里我们完成了我们应用的第一部分。虽然没完成全部应用,但是我们还是有办法看看我们第一部分的结果是否正确。这里我们需要用到一个工具Netcat。一般的类UNIX系统都自带有此工具,没有的话也可以很容易安装。因为我这里用的是Windows系统,所以我这里使用的是Windows版的Netcat。需要此工具的可以访问这个地址下载,解压就可以直接使用了。
解压Netcat工具后,在命令行窗口使用如下命令。
nc -l -u -p 9090
意思是使用UDP协议监听9090端口,然后启动我们的LogEventBroadcaster,在监测文件里面输入内容并保存,就可以在命令行窗口看到转发的内容,例如我的实验结果如下图。
上面我们完成了我们应用的第一部分,并且使用Netcat工具验证了结果。接下来我们开始开发监视器端程序EventLogMonitor,也可以理解成客户端。
EventLogMonitor主要干以下几件事情:
从上图可以看出,我们需要实现两个ChannelHandler。第一个需要实现的是LogEventDecoder,它的作用是将收到的DatagramPacket包转成LogEvent对象。大部分Netty应用收到数据收都会有这个转化ChannelHandler。
package com.nan.netty.udp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.DatagramPacket;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.util.CharsetUtil;
import java.util.List;
public class LogEventDecoder extends MessageToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket, List
将收到的数据解码成LogEvent后,我们还需要一个ChannelHandler去处理LogEvent。这个例子中我们只是简单的将数据打印到标准输出中。在实际项目中,一般会将数据存储到数据库,保存到文件中,或者其他方式处理。
package com.nan.netty.udp;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class LogEventHandler extends SimpleChannelInboundHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelRead0(ChannelHandlerContext ctx, LogEvent event) throws Exception {
StringBuilder builder = new 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.err.println(builder.toString());
}
}
这里主要打印了消息接收时间、发送端地址、文件名称以及实际内容。
ChannelHandler都已经开发完成了,现在就可以写启动器来配置ChannelPipeline。
package com.nan.netty.udp;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import java.net.InetSocketAddress;
public class LogEventMonitor {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
public LogEventMonitor(InetSocketAddress address) {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new LogEventDecoder());
pipeline.addLast(new LogEventHandler());
}
}).localAddress(address);
}
public Channel bind() {
return bootstrap.bind().syncUninterruptibly().channel();
}
public void stop() {
group.shutdownGracefully();
}
public static void main(String[] main) throws Exception {
LogEventMonitor monitor = new LogEventMonitor(new InetSocketAddress(9090));
try {
Channel channel = monitor.bind();
System.out.println("LogEventMonitor running");
channel.closeFuture().await();
} finally {
monitor.stop();
}
}
}
现在不使用Netcat工具,直接使用我们编写的LogEventBroadcaster和LogEventMonitor配合验证,也可以得到想要的结果。
本章我们主要学习了如何编写基于Netty的UDP协议程序,使用起来和TCP的程序没多大区别,因为它们使用了统一的API。本章还进一步学习了如何实现自己的ChannelHandler,并添加到ChannelPipeline。并且我们划分了逻辑,将传送对象转成我们应用对象分成一部分,处理应用对象又是一部分。
这一章我们也了解了什么是无连接协议,例如UDP协议,知道了它的优缺点,我们就知道在实际工作中改如何选择了。
后面的章节我们会学习Netty一些高级知识和特性,算是深入到Netty内部,所以大家不要错过哦。