Netty4 文件传输零拷贝 使用Http协议

该博客介绍了如何使用Netty结合HTTP协议实现零拷贝文件传输,并解决了在使用HttpContentCompressor时与文件传输不兼容的问题。通过自定义HttpChunkedContentCompressor类,实现了对ByteBuf的支持,确保了在SSL环境下文件的压缩和传输。文中提供了完整的客户端和服务器端代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要内容

1. 使用netty结合http协议进行零拷贝文件传输
2. 解决HttpContentCompressor和文件传输无法结合的问题

注:项目里用到的http不是非常标准,是结合了项目实际应用做的改动,仅供参考

项目背景

  • 实际需求简化一下就是:客户端连上服务器之后,就向服务器发送文件;服务器收到请求之后,保存传入文件。

整体思路

  • 文件内容放进http报文的content传输,其余http的header结合项目实际添加
  • 由于有ssl时不能使用零拷贝,所以在传输之前做了判断:无ssl时使用 DefaultFileRegion,有ssl时使用 ChunkedFile
  • 又由于在 pipeline 中同时使用了 ChunkedFileHttpContentCompressor,发现传输无法成功,后来发现是因为 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;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值