Java大文件传输(Netty应用一)

说明

基于Netty的FileRegion模式和ChunkedFile模式实现的大文件传输demo,其中ChunkedFile使用了SSL。

由于最近想在两台不同操作系统的电脑之间传输较大的(3G左右)单个大文件的需要,于是用netty自己写个文件传输的完整demo。(当然可以通过U盘或移动硬盘可以轻松实现这个需求)

从netty的官方文件传输的example中参考了server端的实现,但是没有找到客户端的例子来运行程序,于是自己写了个发到gitee上(https://gitee.com/bbstone101/pisces.git)。

Netty源码中的文件传输example的路径:/netty-4.1.48.Final/example/src/main/java/io/netty/example/file

Bootstrap编码解码过程说明

Server Bootstrap使用的channel handler说明:

ServerBootstrap b = new ServerBootstrap();
  b.group(bossGroup, workerGroup)
   .channel(NioServerSocketChannel.class)
   .option(ChannelOption.SO_BACKLOG, 100)
   .handler(new LoggingHandler(LogLevel.INFO))
   .childHandler(new ChannelInitializer<SocketChannel>() {
      @Override
      public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline p = ch.pipeline();
        if (sslCtx != null) {
          p.addLast(sslCtx.newHandler(ch.alloc()));
        }
        // outbound (default ByteBuf)
        // no encoder, direct send ByteBuf
        // if os not support zero-copy, used ChunkedWriteHandler
        p.addLast(new ChunkedWriteHandler());
        // inbound(decode by the delimiter, then forward to protobuf decoder, last forward to handler)
        ByteBuf delimiter = Unpooled.copiedBuffer(ConstUtil.delimiter.getBytes(CharsetUtil.UTF_8));
        p.addLast(new DelimiterBasedFrameDecoder(8192, delimiter)); // frameLen = BFileReq bytes
        p.addLast(new ProtobufDecoder(BFileMsg.BFileReq.getDefaultInstance()));
        p.addLast(new FileServerHandler());
      }
});

Server端outbound(发送出去)使用了ChunkedWriteHandler,在chunkedFile 模式下用到(FileRegion模式会跳过此handler),ChunkedFile会经过ChunkedWriteHandler来一块一块发送文件数据。

Inbound(接收传入)的数据流经过自定义的delimiter解码,

然后再经过protobuf解码后,

最后传递给FileServerHandler读取请求的文件或目录,返回文件BFileInfo列表给客户端。

Client Bootstrap使用的channel handler说明:

Bootstrap b = new Bootstrap();
b.group(group)
 .channel(NioSocketChannel.class)
 .option(ChannelOption.TCP_NODELAY, true)
 .handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
      ChannelPipeline p = ch.pipeline();
      if (sslCtx != null) {
        p.addLast(sslCtx.newHandler(ch.alloc()));
      }
      // outbound(BFileReq)
      p.addLast(new ProtobufEncoder());

      // --- inbound
      // if os not support zero-copy/sslEnabled, used this, must be the first inbound handler
      p.addLast(new ChunkedReadHandler());

      // ----- decode and handle (BFileRsp + FileRegion) data stream
      ByteBuf delimiter = Unpooled.copiedBuffer(ConstUtil.delimiter.getBytes(CharsetUtil.UTF_8));
      // inbound frameLen = chunkSize[default: 8192] + BFileRsp header)
//    p.addLast(new DelimiterBasedFrameDecoder(10240, delimiter));
      p.addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, delimiter));
      p.addLast(new FileClientHandler());
   }
});

请求头和响应头

请求头消息格式-protobuf(由client端编码,server端解码)

message BFileReq{
    string id = 1;
    string cmd = 2;
    string filepath = 3;
    
    uint64 ts = 4;
}

响应头消息格式-protobuf(由server端编码,client端解码)

message BFileRsp{
    string id = 1;
    string cmd = 2;
    string filepath = 3;

    uint64 fileSize = 4;
    string checksum = 5;
    string rspData = 6;
    bytes chunkData = 7;

    uint64 reqTs = 8;
    uint64 rspTs = 9;
}

消息格式说明:

文件请求的消息格式

REQ_FILE 请求指令的消息格式

----------------------+
| BFileReq| delimiter |
----------------------+

文件响应的消息格式(FileRegion模式)

RSP_FILE 响应指令的消息格式

------------------------------------+
| BFileRsp | chunk_data | delimiter |
------------------------------------+

文件响应的消息格式(ChunkedFile模式)

第一条是文件信息的消息,有界定符(解决粘包和拆包问题)

----------------------+
| BFileRsp| delimiter |
----------------------+

第二条是ChunkedFile经过ChunkedWriteHandler按照一块块发送的数据,没有界定符。

-------------+
| chunk_data |
-------------+

文件传输请求-响应 过程说明

由client端触发操作,client连上server后,发起查询文件列表请求,server将指定的目录下的所有文件BFileInfo列表返回给client端。

client端收到列表后,根据列表逐个发起文件下载请求。

FileRegion模式:

server端通过FileRegion将文件切割成8192 Byte大小的chunk,逐个写到channel中。每个chunk都附加上BFileRsp的响应头信息。(详细格式见 设计说明 RSP_FILE 指令码格式 章节)

client端收到消息后,进行解码,先解出BFileRsp的头信息,然后根据头信息的指令码(cmd),选择对应的CmdHandler来处理消息。为了减少频繁写磁盘,client收到chunk后,先缓存起来,直到缓存满4M或文件数据接收完毕后才写一次文件数据到磁盘。

ChunkedFile模式:

server端首先发送一条BFileRsp结构的文件信息(包括cmd,文件相对server.dir的路径,checksum等信息)。然后接着发送ChunkedFile。

client端收到响应后,首先解析第一条BFileRsp的消息,解析后,如果是RSP_FILE命令,就给ClientCache.recvFileKey赋值,并保存接收到的BFileRsp信息。第二条消息开始就是chunked file的纯文件数据(具体发送多少字节数据由ChunkedWriteHandler决定)。文件接收完成后,重置recvFileKey为null,删除第一条消息保存的BFileRsp的文件信息。

已知问题

  1. 断点续传功能未实现
  2. client端接收文件的目录如果已经有一样的文件,会直接覆盖,不会跳过。
  3. server端下载文件的目录和client端接收文件的目录只能通过config.propertis预先配置好,还不支持通过命令交互方式输入源文件路径和保存的目标路径。

附录:SSL中使用的数字证书创建过程

基本流程

  1. 搞一个虚拟的CA机构,生成一个证书
  2. 生成一个自己的密钥,然后填写证书认证申请,拿给上面的CA机构去签名
  3. 于是就得到了自(自建CA机构认证的)签名证书

Server/Client都用ca.crt来签名

首先,虚构一个CA认证机构出来

生成CA认证机构的证书密钥key# 需要设置密码,输入两次(123456)

openssl genrsa -des3 -out ca.key 1024

去除密钥里的密码(可选)# 这里需要再输入一次原来设的密码

openssl rsa -in ca.key -out ca.key

用私钥ca.key生成CA认证机构的证书ca.crt# 其实就是相当于用私钥生成公钥,再把公钥包装成证书

openssl req -new -x509 -key ca.key -out ca.crt -days 3650

这个证书ca.crt有的又称为”根证书”,因为可以用来认证其他证书

其次,才是生成网站的证书

用上面那个虚构出来的CA机构来认证,不收钱!

server 签名

生成密钥server.key,输入秘密:123456

openssl genrsa -des3 -out server.key 1024

生成证书的请求文件

如果找外面的CA机构认证,也是发个请求文件给他们

这个私钥就包含在请求文件中了,认证机构要用它来生成公钥,然后包装成一个证书

openssl req -new -key server.key -out server.csr

使用虚拟的CA认证机构的证书ca.crt,来对证书请求文件server.csr进行处理,生成签名后的证书server.crt

注意设置序列号和有效期(设10年)

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt -days 3650

将server.key RSA private key 转换成pkcs8 的private key

openssl pkcs8 -topk8 -in server.key -out pkcs8_server.key -nocrypt

Client签名

生成密钥client.key,输入秘密:123456

openssl genrsa -des3 -out client.key 1024

生成证书的请求文件

如果找外面的CA机构认证,也是发个请求文件给他们

这个私钥就包含在请求文件中了,认证机构要用它来生成网站的公钥,然后包装成一个证书

openssl req -new -key client.key -out client.csr

使用虚拟的CA认证机构的证书ca.crt,来对证书请求文件client.csr进行处理,生成签名后的证书client.crt

注意设置序列号和有效期(设10年)

openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt -days 3650

将server.key RSA private key 转换成pkcs8 的private key

openssl pkcs8 -topk8 -in client.key -out pkcs8_client.key -nocrypt

最早发布于:2022.09.30 09:28:38

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注