说明
基于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的文件信息。
已知问题
- 断点续传功能未实现
- client端接收文件的目录如果已经有一样的文件,会直接覆盖,不会跳过。
- server端下载文件的目录和client端接收文件的目录只能通过config.propertis预先配置好,还不支持通过命令交互方式输入源文件路径和保存的目标路径。
附录:SSL中使用的数字证书创建过程
基本流程
- 搞一个虚拟的CA机构,生成一个证书
- 生成一个自己的密钥,然后填写证书认证申请,拿给上面的CA机构去签名
- 于是就得到了自(自建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