說明
基于Netty的FileRegion模式和ChunkedFile模式實現(xiàn)的大文件傳輸demo战授,其中ChunkedFile使用了SSL。
由于最近想在兩臺不同操作系統(tǒng)的電腦之間傳輸較大的(3G左右)單個大文件的需要,于是用netty自己寫個文件傳輸?shù)耐暾鹍emo欺税。(當(dāng)然可以通過U盤或移動硬盤可以輕松實現(xiàn)這個需求)
從netty的官方文件傳輸?shù)膃xample中參考了server端的實現(xiàn)分唾,但是沒有找到客戶端的例子來運行程序,于是自己寫了個發(fā)到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(發(fā)送出去)使用了ChunkedWriteHandler,在chunkedFile 模式下用到(FileRegion模式會跳過此handler)推汽,ChunkedFile會經(jīng)過ChunkedWriteHandler來一塊一塊發(fā)送文件數(shù)據(jù)。
Inbound(接收傳入)的數(shù)據(jù)流經(jīng)過自定義的delimiter解碼歧沪,
然后再經(jīng)過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());
}
});
請求頭和響應(yīng)頭
請求頭消息格式-protobuf(由client端編碼暖夭,server端解碼)
message BFileReq{
string id = 1;
string cmd = 2;
string filepath = 3;
uint64 ts = 4;
}
響應(yīng)頭消息格式-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 |
----------------------+
文件響應(yīng)的消息格式(FileRegion模式)
RSP_FILE 響應(yīng)指令的消息格式
------------------------------------+
| BFileRsp | chunk_data | delimiter |
------------------------------------+
文件響應(yīng)的消息格式(ChunkedFile模式)
第一條是文件信息的消息撵孤,有界定符(解決粘包和拆包問題)
----------------------+
| BFileRsp| delimiter |
----------------------+
第二條是ChunkedFile經(jīng)過ChunkedWriteHandler按照一塊塊發(fā)送的數(shù)據(jù)迈着,沒有界定符。
-------------+
| chunk_data |
-------------+
文件傳輸請求-響應(yīng) 過程說明
由client端觸發(fā)操作邪码,client連上server后裕菠,發(fā)起查詢文件列表請求,server將指定的目錄下的所有文件BFileInfo列表返回給client端闭专。
client端收到列表后糕韧,根據(jù)列表逐個發(fā)起文件下載請求。
FileRegion模式:
server端通過FileRegion將文件切割成8192 Byte大小的chunk喻圃,逐個寫到channel中萤彩。每個chunk都附加上BFileRsp的響應(yīng)頭信息。(詳細格式見 設(shè)計說明 RSP_FILE 指令碼格式 章節(jié))
client端收到消息后斧拍,進行解碼雀扶,先解出BFileRsp的頭信息,然后根據(jù)頭信息的指令碼(cmd)肆汹,選擇對應(yīng)的CmdHandler來處理消息愚墓。為了減少頻繁寫磁盤,client收到chunk后昂勉,先緩存起來浪册,直到緩存滿4M或文件數(shù)據(jù)接收完畢后才寫一次文件數(shù)據(jù)到磁盤。
ChunkedFile模式:
server端首先發(fā)送一條BFileRsp結(jié)構(gòu)的文件信息(包括cmd岗照,文件相對server.dir的路徑村象,checksum等信息)笆环。然后接著發(fā)送ChunkedFile。
client端收到響應(yīng)后厚者,首先解析第一條BFileRsp的消息躁劣,解析后,如果是RSP_FILE命令库菲,就給ClientCache.recvFileKey賦值账忘,并保存接收到的BFileRsp信息。第二條消息開始就是chunked file的純文件數(shù)據(jù)(具體發(fā)送多少字節(jié)數(shù)據(jù)由ChunkedWriteHandler決定)熙宇。文件接收完成后鳖擒,重置recvFileKey為null,刪除第一條消息保存的BFileRsp的文件信息烫止。
已知問題
斷點續(xù)傳功能未實現(xiàn)
client端接收文件的目錄如果已經(jīng)有一樣的文件蒋荚,會直接覆蓋,不會跳過烈拒。
server端下載文件的目錄和client端接收文件的目錄只能通過config.propertis預(yù)先配置好圆裕,還不支持通過命令交互方式輸入源文件路徑和保存的目標(biāo)路徑。
附錄:SSL中使用的數(shù)字證書創(chuàng)建過程
基本流程
搞一個虛擬的CA機構(gòu)荆几,生成一個證書
生成一個自己的密鑰吓妆,然后填寫證書認證申請,拿給上面的CA機構(gòu)去簽名
于是就得到了自(自建CA機構(gòu)認證的)簽名證書
Server/Client都用ca.crt來簽名
首先吨铸,虛構(gòu)一個CA認證機構(gòu)出來
生成CA認證機構(gòu)的證書密鑰key# 需要設(shè)置密碼行拢,輸入兩次(123456)
openssl genrsa -des3 -out ca.key 1024
去除密鑰里的密碼(可選)# 這里需要再輸入一次原來設(shè)的密碼
openssl rsa -in ca.key -out ca.key
用私鑰ca.key生成CA認證機構(gòu)的證書ca.crt# 其實就是相當(dāng)于用私鑰生成公鑰,再把公鑰包裝成證書
openssl req -new -x509 -key ca.key -out ca.crt -days 3650
這個證書ca.crt有的又稱為"根證書",因為可以用來認證其他證書
其次诞吱,才是生成網(wǎng)站的證書
用上面那個虛構(gòu)出來的CA機構(gòu)來認證舟奠,不收錢!
server 簽名
生成密鑰server.key房维,輸入秘密:123456
openssl genrsa -des3 -out server.key 1024
生成證書的請求文件
如果找外面的CA機構(gòu)認證沼瘫,也是發(fā)個請求文件給他們
這個私鑰就包含在請求文件中了,認證機構(gòu)要用它來生成公鑰咙俩,然后包裝成一個證書
openssl req -new -key server.key -out server.csr
使用虛擬的CA認證機構(gòu)的證書ca.crt耿戚,來對證書請求文件server.csr進行處理,生成簽名后的證書server.crt
注意設(shè)置序列號和有效期(設(shè)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 轉(zhuǎn)換成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機構(gòu)認證膜蛔,也是發(fā)個請求文件給他們
這個私鑰就包含在請求文件中了,認證機構(gòu)要用它來生成網(wǎng)站的公鑰脖阵,然后包裝成一個證書
openssl req -new -key client.key -out client.csr
使用虛擬的CA認證機構(gòu)的證書ca.crt皂股,來對證書請求文件client.csr進行處理,生成簽名后的證書client.crt
注意設(shè)置序列號和有效期(設(shè)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 轉(zhuǎn)換成pkcs8 的private key
openssl pkcs8 -topk8 -in client.key -out pkcs8_client.key -nocrypt