本文主要内容
1. 使用netty结合http协议进行零拷贝文件传输
2. 解决HttpContentCompressor和文件传输无法结合的问题
注:项目里用到的http不是非常标准,是结合了项目实际应用做的改动,仅供参考
项目背景
- 实际需求简化一下就是:客户端连上服务器之后,就向服务器发送文件;服务器收到请求之后,保存传入文件。
整体思路
- 文件内容放进http报文的content传输,其余http的header结合项目实际添加
- 由于有ssl时不能使用零拷贝,所以在传输之前做了判断:无ssl时使用
DefaultFileRegion
,有ssl时使用ChunkedFile
。 - 又由于在
pipeline
中同时使用了ChunkedFile
和HttpContentCompressor
,发现传输无法成功,后来发现是因为HttpContentCompressor
不支持ByteBuf
,所以自定义一个类HttpChunkedContentCompressor
继承了HttpContentCompressor
,用来处理ByteBuf
。
完整代码
github:https://github.com/StanAugust/NettyFileTransfer-zerocopy-http
主要代码
1. 客户端(文件发送方)
public class ClientSendFile extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(ClientSendFile.class.getName());
// TODO 指定待传文件路径
private String filePath = "test2.txt";
private RandomAccessFile file;
private long fileLen = -1;
private ChannelHandlerContext ctx;
/**
* @Description: 连接一激活就向服务器发送文件,发送文件的函数在这里修改
*
* @param ctx
* @throws Exception
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelActive(io.netty.channel.ChannelHandlerContext)
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
try {
file = new RandomAccessFile(filePath, "r");
fileLen = file.length();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fileLen < 0 && file != null) {
file.close();
}
}
// 具体处理发送
send0();
}
/**
* @Description: 具体处理发送
* @throws IOException
*/
private void send0() throws IOException {
// 构建http请求
HttpRequest request = initHttpRequest(new File(filePath));
// 写入http request首行和头部
ctx.write(request);
// 写入http content
ChannelFuture sendFileFuture;
ChannelFuture lastContentFuture;
// SSL not enabled - can use zero-copy file transfer.
if (ctx.pipeline().get(SslHandler.class) == null) {
sendFileFuture = ctx.write(new DefaultFileRegion(file.getChannel(), 0, fileLen),
ctx.newProgressivePromise());
// 写入结束符
lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
// SSL enabled - cannot use zero-copy file transfer.
} else {
// 如果pipeline中无{@link HttpContentCompressor},只需添加 {@link ChunkedWriteHandler} 用来传输 ChunkedFile
ctx.pipeline().addBefore("sender", "chunked writer", new ChunkedWriteHandler());
// 使用 {@link HttpChunkedInput},会自动写入 LastHttpContent
sendFileFuture = ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(file)),
ctx.newProgressivePromise());
lastContentFuture = sendFileFuture;
}
// 监听请求是否传输成功
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationComplete(ChannelProgressiveFuture future) throws Exception {
if (future.isSuccess()) {
logger.info("文件上传完毕>>>>>>>>>");
file.close();
}
}
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total)
throws Exception {
if (total < 0) { // total unknown
logger.info(future.channel() + " Transfer progress:" + progress);
} else {
logger.info(future.channel() + " Transfer progress:" + progress + "/" + total);
}
}
});
}
/**
* @Description: 初始化一个http request,设置基本信息
* @return
*/
private HttpRequest initHttpRequest(File file) {
HttpRequest request = new DefaultHttpRequest(HttpVersion.valueOf("HTTP/2.0"),
HttpMethod.valueOf("TRANSFER"),
"/");
request.headers().set(HttpHeaderNames.DATE, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
request.headers().set(HttpHeaderNames.CONTENT_TYPE, new MimetypesFileTypeMap().getContentType(file.getPath()));
HttpUtil.setContentLength(request, fileLen);
return request;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
2. 服务器(文件接收方)
public class ServerReceiveFile extends ChannelInboundHandlerAdapter{
private static final Logger logger = Logger.getLogger(ServerReceiveFile.class.getName());
// TODO 存储路径需指定
private String path = "server_receive.txt";
private RandomAccessFile file;
/**
* @Description: 接收文件
* @param ctx
* @param msg
* @throws Exception
* @see io.netty.channel.ChannelInboundHandlerAdapter#channelRead(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest)msg;
file = new RandomAccessFile(path, "rw");
file.seek(0);
file.write(ByteBufUtil.getBytes(request.content()));
file.close();
logger.info("server receive all>>>>>>>>>>>>>>>");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
3. 关于压缩
/**
* @ClassName: HttpChunkedContentCompressor
* @Description: 将 {@link Bytebuf}封装到 {@link DefaultHttpContent} 中
* @author Stan
* @date: 2021年3月11日
*/
public class HttpChunkedContentCompressor extends HttpContentCompressor {
public HttpChunkedContentCompressor(int compressionLevel) {
super(compressionLevel);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf) {
/*
* 将ByteBuf转换为HttpContent,使其能够使用HttpContentCompressor
*
* 如果pipeline中有 HttpContentCompressor,那么使用ChunkedWriteHandler发送文件就需要添加这一步
*/
ByteBuf buf = (ByteBuf) msg;
if (buf.isReadable()) {
/*
* 只编码非空缓冲区,因为空缓冲区可用于确定内容何时被刷新
*/
msg = new DefaultHttpContent(buf);
}
}
super.write(ctx, msg, promise);
}
}
4. 关于如何调用 HttpChunkedContentCompressor
// SSL not enabled - can use zero-copy file transfer.
if (ctx.pipeline().get(SslHandler.class) == null) {
sendFileFuture = ctx.write(new DefaultFileRegion(file.getChannel(), 0, fileLen),
ctx.newProgressivePromise());
// 写入结束符
lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
// SSL enabled - cannot use zero-copy file transfer.
} else {
/*
* TODO 如果要往pipeline中添加{@link HttpContentCompressor}
*
* 不能使用{@link HttpContentCompressor}来压缩ChunkedFile,因为它不支持ByteBuf
* 使用{@link HttpChunkedContentCompressor}来处理ByteBuf实例,它扩展了HttpContentCompressor。
*
* 实际上{@link HttpContentCompressor}是用来压缩{@link HttpResponse}的,所以这里在客户端添加只是举个例子
*/
// 顺序很重要!
ctx.pipeline().addBefore("sender", "chunked compressor", new HttpChunkedContentCompressor(6));
ctx.pipeline().addBefore("chunked compressor", "chunked writer", new ChunkedWriteHandler());
// 使用 {@link HttpChunkedInput},会自动写入 LastHttpContent
sendFileFuture = ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(file)),
ctx.newProgressivePromise());
lastContentFuture = sendFileFuture;
}