說明
基于Netty的FileRegion模式和ChunkedFile模式實(shí)現(xiàn)的大文件傳輸demo,其中ChunkedFile使用了SSL。
由于最近想在兩臺(tái)不同操作系統(tǒng)的電腦之間傳輸較大的(3G左右)單個(gè)大文件的需要,于是用netty自己寫個(gè)文件傳輸?shù)耐暾鹍emo。(當(dāng)然可以通過U盤或移動(dòng)硬盤可以輕松實(shí)現(xiàn)這個(gè)需求)
從netty的官方文件傳輸?shù)膃xample中參考了server端的實(shí)現(xiàn),但是沒有找到客戶端的例子來運(yùn)行程序,于是自己寫了個(gè)發(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模式會(huì)跳過此handler),ChunkedFile會(huì)經(jīng)過ChunkedWriteHandler來一塊一塊發(fā)送文件數(shù)據(jù)。
Inbound(接收傳入)的數(shù)據(jù)流經(jīng)過自定義的delimiter解碼,
然后再經(jīng)過protobuf解碼后,
最后傳遞給FileServerHandler讀取請(qǐng)求的文件或目錄,返回文件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());
}
});
請(qǐng)求頭和響應(yīng)頭
請(qǐ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;
}
消息格式說明:
文件請(qǐng)求的消息格式
REQ_FILE 請(qǐng)求指令的消息格式
----------------------+
| 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 |
-------------+
文件傳輸請(qǐng)求-響應(yīng) 過程說明
由client端觸發(fā)操作,client連上server后,發(fā)起查詢文件列表請(qǐng)求,server將指定的目錄下的所有文件BFileInfo列表返回給client端。
client端收到列表后,根據(jù)列表逐個(gè)發(fā)起文件下載請(qǐng)求。
FileRegion模式:
server端通過FileRegion將文件切割成8192 Byte大小的chunk,逐個(gè)寫到channel中。每個(gè)chunk都附加上BFileRsp的響應(yīng)頭信息。(詳細(xì)格式見 設(shè)計(jì)說明 RSP_FILE 指令碼格式 章節(jié))
client端收到消息后,進(jìn)行解碼,先解出BFileRsp的頭信息,然后根據(jù)頭信息的指令碼(cmd),選擇對(duì)應(yīng)的CmdHandler來處理消息。為了減少頻繁寫磁盤,client收到chunk后,先緩存起來,直到緩存滿4M或文件數(shù)據(jù)接收完畢后才寫一次文件數(shù)據(jù)到磁盤。
ChunkedFile模式:
server端首先發(fā)送一條BFileRsp結(jié)構(gòu)的文件信息(包括cmd,文件相對(duì)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的文件信息。
已知問題
斷點(diǎn)續(xù)傳功能未實(shí)現(xiàn)
client端接收文件的目錄如果已經(jīng)有一樣的文件,會(huì)直接覆蓋,不會(huì)跳過。
server端下載文件的目錄和client端接收文件的目錄只能通過config.propertis預(yù)先配置好,還不支持通過命令交互方式輸入源文件路徑和保存的目標(biāo)路徑。
附錄:SSL中使用的數(shù)字證書創(chuàng)建過程
基本流程
搞一個(gè)虛擬的CA機(jī)構(gòu),生成一個(gè)證書
生成一個(gè)自己的密鑰,然后填寫證書認(rèn)證申請(qǐng),拿給上面的CA機(jī)構(gòu)去簽名
于是就得到了自(自建CA機(jī)構(gòu)認(rèn)證的)簽名證書
Server/Client都用ca.crt來簽名
首先,虛構(gòu)一個(gè)CA認(rèn)證機(jī)構(gòu)出來
生成CA認(rèn)證機(jī)構(gòu)的證書密鑰key# 需要設(shè)置密碼,輸入兩次(123456)
openssl genrsa -des3 -out ca.key 1024
去除密鑰里的密碼(可選)# 這里需要再輸入一次原來設(shè)的密碼
openssl rsa -in ca.key -out ca.key
用私鑰ca.key生成CA認(rèn)證機(jī)構(gòu)的證書ca.crt# 其實(shí)就是相當(dāng)于用私鑰生成公鑰,再把公鑰包裝成證書
openssl req -new -x509 -key ca.key -out ca.crt -days 3650
這個(gè)證書ca.crt有的又稱為"根證書",因?yàn)榭梢杂脕碚J(rèn)證其他證書
其次,才是生成網(wǎng)站的證書
用上面那個(gè)虛構(gòu)出來的CA機(jī)構(gòu)來認(rèn)證,不收錢!
server 簽名
生成密鑰server.key,輸入秘密:123456
openssl genrsa -des3 -out server.key 1024
生成證書的請(qǐng)求文件
如果找外面的CA機(jī)構(gòu)認(rèn)證,也是發(fā)個(gè)請(qǐng)求文件給他們
這個(gè)私鑰就包含在請(qǐng)求文件中了,認(rèn)證機(jī)構(gòu)要用它來生成公鑰,然后包裝成一個(gè)證書
openssl req -new -key server.key -out server.csr
使用虛擬的CA認(rèn)證機(jī)構(gòu)的證書ca.crt,來對(duì)證書請(qǐng)求文件server.csr進(jìn)行處理,生成簽名后的證書server.crt
注意設(shè)置序列號(hào)和有效期(設(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
生成證書的請(qǐng)求文件
如果找外面的CA機(jī)構(gòu)認(rèn)證,也是發(fā)個(gè)請(qǐng)求文件給他們
這個(gè)私鑰就包含在請(qǐng)求文件中了,認(rèn)證機(jī)構(gòu)要用它來生成網(wǎng)站的公鑰,然后包裝成一個(gè)證書
openssl req -new -key client.key -out client.csr
使用虛擬的CA認(rèn)證機(jī)構(gòu)的證書ca.crt,來對(duì)證書請(qǐng)求文件client.csr進(jìn)行處理,生成簽名后的證書client.crt
注意設(shè)置序列號(hào)和有效期(設(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