Netty UDP协议栈开发

  • 概念介绍
  • 相对路径
  • 绝对路径
  • 开发
  • 业务流程图如下:
  • FileChannel 介绍
  • jar 依赖
  • 服务端启动类 FileServer
  • 服务端业务处理类 FileServerHandler
  • 测试
  • 测试步骤
  • CMD客户端截图打印
  • 测试结果说明
  • 总结


概念介绍

文件是最常见的数据源之一,程序经常需要在文件中读取数据,也要将数据保存在文件中,进行持久化。
文件是计算机中一种基本的数据存储形式。即使计算机关机,文件的数据还是存在的,但是内存的数据就会丢失。

相对路径

从当前路径开始: linux 中: …/root/Demo.java。 当前目录的上一级目录中的 root文件夹的Demo.java文件。

绝对路径

从根节点开始,例如:windows 中的 D:\java\nio\netty\Demo.java
linux 中 /root/temp/Demo.java

开发

业务流程图如下:

netty转发端口springboot netty 传输文件_java

FileChannel 介绍

Java NIO 中的FileChannel是一个连接到文件的通道,可以通过这个文件通道读写文件。JDK1.7 之前NIO 的FileChannel是同步阻塞的。JDK1.7对NIO进行了升级,升级后的NIO提供了异步文件通道AsynchronousFileChannel。它支持异步非阻塞文件操作(AIO)。

在使用FileChannel之前必须先打开它,需要有InputStream,OutputSream或者RandomAccessFile来构造FileChannel实例。

RandomAccessFile tempFile=new RandomAccessFile("/home/temp/xxx.java");
FileChannel channel=tempFile.getChannle();

如果要从FileChannel中读取数据,要申请一个ByteBuffer,将数据从FileChannel中读取到字节缓冲中。read()方法返回的Int值表示有多少字节被读到了字节缓冲区中。如果返回-1,表示读到了文件末尾。
反之,如果需要通过FileChannel向文件中写入数据,需要将数据复制或者直接存放到ByteBuffer中,然后调用FileChannel.writer()方法进行写操作。

String content="echo,welcome to File world.";
ByteBuffer wrieteBuffer=ByteBuffer.allocate(128);//缓冲字节分配128 字节大小
//内容放入缓冲字节中
writeBuffer.put(content.getBytes());
writeBuffer.flip();
//将字节内容写入FileChannel 对应的文件中去。
channel.write(buf);

jar 依赖

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId> <!-- Use 'netty5-all' for 5.0-->
            <version>5.0.0.Alpha1</version>
            <scope>compile</scope>
        </dependency>

服务端启动类 FileServer

public class FileServer {
    public void run(int port){
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        EventLoopGroup workGroup=new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap=new ServerBootstrap();
            bootstrap.group(bossGroup,workGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,100)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
                                    .addLast(
                                            //编码  编码器从下至上。 顺序是  FileServerHandler >>>>StringEncoder
                                            //解码 是解码器从上到下。 每一个的返回值是下一个的入参。
                                            //     LineBasedFrameDecoder  >> StringDecoder >>  FileServerHandler
                                            //到 FileServerHandler 拿到的就是String了。

                                            //将文件内容编码为字符串
                                            new StringEncoder(CharsetUtil.UTF_8),
                                            //按照回车换行符对数据报报进行解码
                                            new LineBasedFrameDecoder(1024),
                                            //将数据报解码为字符串
                                            new StringDecoder(CharsetUtil.UTF_8),
                                            //业务处理类
                                            new FileServerHandler());


                        }
                    });
            ChannelFuture future=bootstrap.bind(port).sync();
            System.out.println("Start file server at port : "+port);
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new FileServer().run(8080);
    }

}

服务端业务处理类 FileServerHandler

public class FileServerHandler extends SimpleChannelInboundHandler<String> {

    private static final String CR=System.getProperty("line.separator");
    @Override
    protected void messageReceived(ChannelHandlerContext context, String s) throws Exception {
        //通过路径来构造文件
        File file=new File(s);
        //如果文件存在
        if (file.exists()){
            //如果不是文件,是文件夹
            if(!file.isFile()){
                context.writeAndFlush("Not a file : "+file+CR);
                return;
            }
            context.write(file+" "+file.length()+CR);
            //构造只读文件
            RandomAccessFile randomAccessFile=new RandomAccessFile(s,"r");
            //构造netty 的FileRegion对象
            FileRegion region=new DefaultFileRegion(
                    //FileChannel 文件通道,用于对文件进行读写操作
                    randomAccessFile.getChannel(),
                    //0(Position)文件操作的指针位置,读取或写入的起始点。
                    0,
                    //操作的总字节数
                    randomAccessFile.length());

            //实现对文件的发送。由于Netty 底层对文件写入进行了封装,我们不用关心发送的细节。
            context.write(region);
            //写入分割符告诉CMD 控制台,文件传输结束
            context.writeAndFlush(CR);
            randomAccessFile.close();
        }else {
            //文件不存在
            context.writeAndFlush("File not found : "+file+CR);
        }
    }

    public void exceptionCaught(ChannelHandlerContext context,Throwable cause){
        cause.printStackTrace();
        context.close();
    }
}

测试

测试步骤

(1)启动服务端
(2)打开cmd 窗口。输入 telnet 127.0.0.1 8080 (注意ip和端口中间有空格)
(3)连接上了后,复制粘贴文件的绝对路径。 这个输入操作比较麻烦,会有空格等字符串,导致服务器接收的文件路径不正确。所以测试失败了,看下服务器那边接收的路径是否正确。

CMD客户端截图打印

输入的绝对路径是: F:\dubbo_leaning\DoSpring\src\com\echo\service\echo.txt

netty转发端口springboot netty 传输文件_服务端_02


文件内容截图如下:

netty转发端口springboot netty 传输文件_java_03

测试结果说明

通过 这个结果看出。客户端打印了文件名称,说明服务端接收数据正确。
客户端接收了文件内容,说明服务器那边能正确获取文件,并能正确发送。
好了。我们的功能正确的实现 了。没有出现丢包和粘包现象。
如果 CMD客户端输入绝对路径很麻烦。大家可以参考用html 来模拟连接和输入绝对路径。
可以参考这篇博客

小伙伴们可以debugger 跟下服务端业务处理类逻辑。能更好的理解本章内容。

总结

本章节介绍了如何利用Netty进行文件传输。由于Netty对文件传输进行了封装,上层应用不需要感知文件操作的细节,Netty提供了多种编码类库,通过组合可以灵活地处理各种文件。
其实 Netty有多种方式实现文件的传输,本章只是实现了比较通用的方式。
比如 Netty 还提供了ChunkedWriteHandler 来解决大文件或者码流传输过程中可能发生的内存溢出问题。总的来说,Netty 的文件传输无论在功能还是可靠性方面,相比较于传统的I/O类库或者其他一些第三方文件传输类库,都有较大的优势。