Netty4实战第十三章:使用UDP

本章主要内容:

  • 学习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广播数据时都是在安全的内网环境中使用。另外一般广播数据的服务端和客户端都在同一个网络环境中,一旦夸网络经过路由,可能就会被拦截掉,防火墙会误判。这种模式一般被成为“发布-订阅”模式,服务端发布数据,一个或多个客户端订阅数据。

  写代码之前,先看看应用的基本结构。

Netty4实战第十三章:使用UDP_第1张图片

  这个应用主要由两部分组成。其中一个用来监测文件内容,一般就是一个实例,类似服务端;另一个事件监测器用来获取服务端转发的文件内容,由一个或多个实例组成。

  为了节省时间,实际项目中的安全验证、过滤等功能就不再这里说了。

  如果你觉得这个应用还不错,某些代码很适合你,你可以适当调整满足自己的需求。得益于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包装。

Netty4实战第十三章:使用UDP_第2张图片

  从上图可以看出,每一条消息都对应一个DatagramPacket。基本上所有基于Netty的应用都会包含一个或多个ChannelHandler通过启动器配置到Channel中。我们来看看这个应用的ChannelPipeline的基本结构。

Netty4实战第十三章:使用UDP_第3张图片

  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 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的结构。

Netty4实战第十三章:使用UDP_第4张图片

  从上图可以看出,我们需要实现两个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 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 {

    @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配合验证,也可以得到想要的结果。

Netty4实战第十三章:使用UDP_第5张图片

六、总结

  本章我们主要学习了如何编写基于Netty的UDP协议程序,使用起来和TCP的程序没多大区别,因为它们使用了统一的API。本章还进一步学习了如何实现自己的ChannelHandler,并添加到ChannelPipeline。并且我们划分了逻辑,将传送对象转成我们应用对象分成一部分,处理应用对象又是一部分。

  这一章我们也了解了什么是无连接协议,例如UDP协议,知道了它的优缺点,我们就知道在实际工作中改如何选择了。

  后面的章节我们会学习Netty一些高级知识和特性,算是深入到Netty内部,所以大家不要错过哦。

  


你可能感兴趣的:(netty学习)