Netty4实战第十三章:使用UDP

来源:互联网 发布:java类适配器 编辑:程序博客网 时间:2024/06/06 08:24

本章主要内容:

  • 学习UDP协议
  • 学习Netty对UDP的支持
  • 启动UDP协议的Netty应用
  前面学习的例子都是基于连接的协议,如TCP。这一章我们重点学习UDP。UDP是一种无连接的协议,主要适用于高性能且丢部分包不是问题的场景。基于UDP协议的应用有一个著名的例子,就是DNS域名解析服务。
  由于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。我们先来设计下LogEventMonitor里面的ChannelPipeline的结构。


  从上图可以看出,我们需要实现两个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内部,所以大家不要错过哦。