Netty4实战第十三章:使用UDP
来源:互联网 发布:java类适配器 编辑:程序博客网 时间:2024/06/06 08:24
本章主要内容:
- 学习UDP协议
- 学习Netty对UDP的支持
- 启动UDP协议的Netty应用
由于Netty提供了统一的API,所以无论使用的是TCP还是UDP,大部分API都是一样的。你也可以重用你实现的ChannelHandler或者其他基于Netty的实现。
学习完本章之后,你会对无连接协议会有一个基本的了解,特别是UDP,并且你也能知道什么样的场景适合使用UDP协议。
本章的大部分知识比较独立,也就是说你没有学习前面的章节,也能看懂大部分本章的内容。因为本章已经涵盖了基于Netty的UDP的应用的大部分知识,而且不会对前面提到过的知识过多的深入讲解。只要知道一些基本的Netty API,学习本章就是没问题的。
一、了解UDP
在实际开发应用之前,我们需要先花一些时间来学习UDP协议,因为在使用它之前,我们至少得了解它是什么,它能干什么以及它的优点和缺点。
前面说过,UDP是无连接的协议,也就是说在客户端和服务端使用UDP通信的过程中是没有连接的,所以很难说这个消息属于哪个连接。
UDP的一个限制就是没有像TCP那样的纠错能力。TCP协议中发送端发送一个包之后,接收端会返回一个消息表示我收到了这个包,当发送端没收到接收端的确认信息时发送端就会重发这个包。UDP就不是这样的,UDP发送包之后立即就不管了,继续发送下面的包,所以UDP的发送端并不知道接收端有没有收到包,也就是说UDP只管发,接收端收没收到就是不是它的事了。
所以,UDP很适合那些数据量大但是丢包又对应用无太大影响的应用情况,例如视频类应用。当然,如果数据准确性很高,消息不能丢失类型的应用是绝对不能使用UDP的,这种情况应该使用TCP类协议,例如金钱交易类型的应用。
二、设计UDP应用
接下来的章节我们会开发一个UDP应用,主要逻辑就是通过UDP将数据转发给其他客户端。这个应用的主要功能就是监测一个文件,通过UDP转发新增的行内容。
如果你很数据类UNIX系统,则会很熟悉这个功能,和Syslog的功能很像。
UDP协议很适合这种类型应用,因为你即使丢失几行数据没有转发,也不会对应用产生致命影响。最重要的是,日志量很大时,需要靠UDP这种高性能的协议来达到应用需求。
这个应用的另一个特点是,新增一个文件监视器不需要额外的操作。只需要启动一个实例,然后指定端口就可以看到文件内容了。不过有时候这也不一定是好事,只要能启动实例就能获取到内容了,相对来说有一些不安全。所以一般UDP广播数据时都是在安全的内网环境中使用。另外一般广播数据的服务端和客户端都在同一个网络环境中,一旦夸网络经过路由,可能就会被拦截掉,防火墙会误判。这种模式一般被成为“发布-订阅”模式,服务端发布数据,一个或多个客户端订阅数据。
写代码之前,先看看应用的基本结构。
这个应用主要由两部分组成。其中一个用来监测文件内容,一般就是一个实例,类似服务端;另一个事件监测器用来获取服务端转发的文件内容,由一个或多个实例组成。
为了节省时间,实际项目中的安全验证、过滤等功能就不再这里说了。
如果你觉得这个应用还不错,某些代码很适合你,你可以适当调整满足自己的需求。得益于Netty的设计,逻辑分散,统一的API,所以修改起来还是很容易的。
后面还会介绍一些UDP的基础知识,来看看它和基于TCP的应用的不同。
三、EventLog对象
大家如果编写过其他类型的应用,特别是使用面向对象的语言的时候,如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是如何工作的。
四、编写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<LogEvent> { private final InetSocketAddress remoteAddress; public LogEventEncoder(InetSocketAddress remoteAddress) { this.remoteAddress = remoteAddress; } @Override protected void encode(ChannelHandlerContext channelHandlerContext, LogEvent logEvent, List<Object> out) throws Exception { ByteBuf buf = channelHandlerContext.alloc().buffer(); //文件名称写到ByteBuf中 buf.writeBytes(logEvent.getLogfile().getBytes(CharsetUtil.UTF_8)); //分割字符 buf.writeByte(LogEvent.SEPARATOR); //实际文件内容 buf.writeBytes(logEvent.getMsg().getBytes(CharsetUtil.UTF_8)); //创建DatagramPacket实例并添加到结果列表 out.add(new DatagramPacket(buf, remoteAddress)); }}
接下来就可以编写启动器,包括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主要干以下几件事情:
- 从LogEventBroadcaster那里收到DatagramPacket数据包
- 将DatagramPacket解码成LogEvent消息
- 打印LogEvent里面的内容
从上图可以看出,我们需要实现两个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<DatagramPacket> { @Override protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket, List<Object> out) throws Exception { //得到DatagramPacket数据内容 ByteBuf data = datagramPacket.content(); //获取分隔符位置 int i = data.indexOf(0, data.readableBytes() - 1, LogEvent.SEPARATOR); if (i < 0) { //没找到分隔符,脏数据或者空行 return; } //分隔符前面是文件内容 String filename = data.slice(0, i).toString(CharsetUtil.UTF_8); //分隔符后面是实际消息数据 String logMsg = data.slice(i + 1, data.readableBytes() - i - 1).toString(CharsetUtil.UTF_8); //创建LogEvent实例 LogEvent event = new LogEvent(datagramPacket.sender(), System.currentTimeMillis(), filename, logMsg); out.add(event); }}
将收到的数据解码成LogEvent后,我们还需要一个ChannelHandler去处理LogEvent。这个例子中我们只是简单的将数据打印到标准输出中。在实际项目中,一般会将数据存储到数据库,保存到文件中,或者其他方式处理。
package com.nan.netty.udp;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;public class LogEventHandler extends SimpleChannelInboundHandler<LogEvent> { @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<Channel>() { @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内部,所以大家不要错过哦。
- Netty4实战第十三章:使用UDP
- netty4 UDP的使用
- Netty4实战第四章:Transports
- Netty4实战第五章:Buffers
- Netty4实战第六章:ChannelHandler
- Netty4实战第七章:编解码器
- Netty4实战第十一章:WebSockets
- Netty4实战第十二章:SPDY
- Netty4实战
- Netty4实战第三章:Netty基础
- Netty4实战第九章:启动Netty应用
- Netty4实战第十四章:自定义编解码器
- Netty4实战第十六章:注销/注册EventLoop
- R语言实战第十三章
- Netty4实战第二章:第一个Netty应用
- Netty4实战第八章:Netty提供的ChannelHandler和编解码器
- Netty4实战第十章:Netty应用的单元测试
- Netty4实战第十五章:选择正确的线程模型
- java 关键字assert 断言
- 小之的架构之路——Android MVVM 面向接口型框架封装和单元测试
- 第一篇文章
- PyCharm使用技巧:Shift + Enter(快速换行)
- 4.3填空题。
- Netty4实战第十三章:使用UDP
- Haskell语言学习笔记(30)MonadCont, Cont, ContT
- 2-java学习笔记
- python 2.7 numpy、 scipy 、matplotlib、 sklearn、pandas的安装
- HDU 5253:连接的管道
- yii2 场景运用
- 数据结构:自平衡二叉查找树(AVL树)
- openwrt关闭串口打印信息
- C# PictureBox 显示单通道灰度图