转自http://www.open-open.com/lib/view/open1392881249795.html
主要用于手机端,网络不稳定时上传视频文件,服务端支持断点上传,一是提升速度,二是节省流量。使用netty4 实现。
本来以为文件断点续传功能很简单,不就是提供2个方法:
一个返回已经上传的文件的长度;另外一个负责上传文件呗(请求带上content-range 指明本次上传的内容在整个文件中的位置),然后根据请求提供的位置写呗,太简单了。
但是实际情况还是比较复杂的,关键问题是,上面的描述现在想想只能称作为文件分段上传,而不是断点续传。
断点意味着网络会断,然后断了之后,服务端根本获取不到本次上传的内容,于是下次又只能从头开始传文件。一种解决办法是客户端将文件分成很小的片段(单个片段丢了就整个片段重传),这个方案要求客户端做很多工作,服务端还得根据片段的编号组织文件,总之客户端和服务端都挺麻烦。
于是就想到用netty在写一个服务filestoreApdapterServer,文件上传提交给这个代理服务。这个做法有个前提就是,客户端上传的文件名称保证唯一,并且在请求头里面带着这个名字,以便服务端定位文件。利用的原理是一般长度比较大的消息体,netty会使用chunk传输,我们取得chunk写入临时文件,这样即使网络断了,服务端已经获取的文件内容还是保留在临时文件里面。
流程如下:
1. filestoreApdapterServer将请求的消息体写到临时文件(网络断了也不要紧,读到多少写多少)。
2. 客户端下次传之前先调用getSize获取上传传递的文件长度,我们就在这个getSize方法里面偷偷的将第一步保存的临时文件追加到正式文件里面,然后返回文件长度。
3. 客户端根据获取的服务端文件长度,定位未传的文件位置,读取上传。重复1,2步骤。直到文件上传完成。
看代码:FilestoreAdaptorServerInitializer
01 | public class FilestoreAdaptorServerInitializer extends |
02 | ChannelInitializer<SocketChannel> { |
05 | protected void initChannel(SocketChannel ch) throws Exception { |
06 | ChannelPipeline pipeline = ch.pipeline(); |
07 | pipeline.addLast( "decoder" , new HttpRequestDecoder()); |
08 | pipeline.addLast( "aggregator" , new StreamChunkAggregator(- 1 )); |
09 | pipeline.addLast( "encoder" , new HttpResponseEncoder()); |
10 | pipeline.addLast( "handler" , new FileUploadAdaptorHandler()); |
StreamChunkAggregator就是获取上传文件,写临时文件的:
01 | public class StreamChunkAggregator extends MessageToMessageDecoder<HttpObject> { |
02 | private static final Logger log = LoggerFactory.getLogger(StreamChunkAggregator. class ); |
04 | private volatile FullHttpMessage currentMessage; |
05 | private volatile OutputStream out; |
06 | private final int maxContentLength; |
07 | private volatile File file; |
09 | private ChannelHandlerContext ctx; |
11 | public static final int DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS = 1024 ; |
12 | private int maxCumulationBufferComponents = DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS; |
15 | * Creates a new instance. |
17 | public StreamChunkAggregator( int maxContentLength) { |
18 | this .maxContentLength = maxContentLength; |
22 | protected void decode(ChannelHandlerContext ctx, HttpObject msg, |
23 | List<Object> out) throws Exception { |
24 | FullHttpMessage currentMessage = this .currentMessage; |
26 | if (msg instanceof HttpMessage) { |
27 | HttpMessage m = (HttpMessage) msg; |
28 | if (msg instanceof HttpRequest) { |
29 | HttpRequest header = (HttpRequest) msg; |
30 | this .currentMessage = currentMessage = new DefaultFullHttpRequest(header.getProtocolVersion(), |
31 | header.getMethod(), header.getUri(), Unpooled.compositeBuffer(maxCumulationBufferComponents)); |
33 | final String localName = m.headers().get( "file" ); |
34 | log.debug( "upload file name is {}" , localName); |
35 | if ( null == localName || "" .equals(localName.trim())) { |
36 | ctx.fireChannelRead(m); |
38 | File dir = new File(ServerHelper.getDestDir().getAbsolutePath() + File.separator + ServerHelper.getStorePath(localName)); |
41 | log.debug( "upload file path is {}" , dir.getAbsolutePath()); |
42 | File tempFile = new File(dir, localName + ".utmp" ); |
43 | if (tempFile.exists()) { |
47 | this .out = new FileOutputStream(file, true ); |
52 | currentMessage.headers().set(m.headers()); |
53 | } else if (msg instanceof HttpContent) { |
54 | assert currentMessage != null ; |
55 | HttpContent chunk = (HttpContent) msg; |
57 | if (chunk.content().isReadable()) { |
59 | IOUtils.copyLarge( new ByteBufInputStream(chunk.content()), this .out); |
63 | if (!chunk.getDecoderResult().isSuccess()) { |
64 | currentMessage.setDecoderResult( |
65 | DecoderResult.failure(chunk.getDecoderResult().cause())); |
68 | last = chunk instanceof LastHttpContent; |
76 | this .currentMessage = null ; |
78 | out.add(currentMessage); |
FileUploadAdaptorHandler 这个是最后传成功后通知真正的服务端,并且获取服务的返回,给客户端:
01 | public class FileUploadAdaptorHandler extends SimpleChannelInboundHandler<DefaultFullHttpRequest> { |
02 | private static final Logger log = LoggerFactory.getLogger(FileUploadAdaptorHandler. class ); |
05 | protected void channelRead0( final ChannelHandlerContext ctx, DefaultFullHttpRequest msg) throws Exception { |
06 | if (log.isDebugEnabled()) { |
07 | log.debug( "message received: begin" ); |
10 | final String filename = msg.headers().get( "file" ); |
11 | if (filename == null || "" .equals(filename.trim())) { |
12 | String responseBody = "{\"result_code\": 4001,\"result_msg\": \"请求参数错误\"}" ; |
13 | response(responseBody.getBytes(), HttpResponseStatus.BAD_REQUEST, ctx); |
17 | final CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault(); |
20 | HttpGet request1 = new HttpGet(ServerHelper.getPlayServer()); |
21 | request1.setHeader( "Client-Session" , msg.headers().get( "client-session" )); |
22 | request1.setHeader( "Content-Range" , msg.headers().get( "content-range" )); |
23 | request1.setHeader( "file" , msg.headers().get( "file" )); |
25 | httpclient.execute(request1, new FutureCallback<org.apache.http.HttpResponse>() { |
27 | public void failed(Exception e) { |
30 | } catch (IOException e1) { |
31 | log.error(e1.getMessage(), e1); |
33 | serve500(ctx, filename); |
37 | public void completed(org.apache.http.HttpResponse playResonse) { |
38 | log.debug( "HttpAsyncClient callback" ); |
39 | int status = playResonse.getStatusLine().getStatusCode(); |
40 | log.debug( "HttpAsyncClient callback playResonse status is {}" , status); |
42 | ServerHelper.deleteTmpFile(filename); |
44 | HttpEntity entity = playResonse.getEntity(); |
45 | byte [] bytes = new byte [( int ) entity.getContentLength()]; |
47 | IOUtils.read(entity.getContent(), bytes); |
49 | response(bytes, new HttpResponseStatus(status, "" ), ctx); |
50 | } catch (Exception e) { |
51 | log.error(e.getMessage(), e); |
52 | serve500(ctx, filename); |
56 | } catch (IOException e1) { |
57 | log.error(e1.getMessage(), e1); |
63 | public void cancelled() { |
66 | } catch (IOException e1) { |
67 | log.error(e1.getMessage(), e1); |
69 | serve500(ctx, filename); |
72 | } catch (Exception e) { |
74 | log.error(e.getMessage(), e); |
75 | serve500(ctx, filename); |
79 | if (log.isDebugEnabled()) { |
80 | log.debug( "message received: end" ); |
真正服务提供2个方法,一个是获取长度,一个是接收filestoreAapterServer请求的方法:
01 | public static void getFileLength(String name) { |
02 | Logger.debug( "getFileLength path is " + FileHelper.getStorgePath(name)); |
03 | File file = new File(FileHelper.getStorgePath(name)); |
04 | long length = file.length(); |
05 | response.status = StatusCode.OK; |
06 | response.setHeader( "Content-Size" , String.valueOf(length)); |
07 | LocalFile file = LocalFile .find(。。。).first(); |
09 | Logger.debug( "getFileLength file has been in database" ); |
10 | FileResult result = new FileResult(); |
12 | throw new CustomJsonResult(result); |
15 | File fileTmp = new File(FileHelper.getStorgePath(name) + FileHelper.TMP_SUFFIX); |
16 | if (Logger.isDebugEnabled()) |
17 | Logger.debug( "getFileLength temp path is " + fileTmp.getAbsolutePath() + ", existed is: " + fileTmp.exists()); |
18 | if (fileTmp.exists()) { |
20 | Logger.debug( "getFileLength save tmp file" ); |
22 | FileHelper.saveFileFromTmp(fileTmp, file); |
23 | } catch (IOException ingore) { |
24 | Logger.error(ingore.getMessage(), ingore); |
26 | length = file.length(); |
28 | response.setHeader( "Content-Size" , String.valueOf(length)); |
01 | public static void saveUploadFile() { |
02 | String filename = getFileName(); |
03 | Logger.debug( "saveUploadFile name is %s" , filename); |
04 | long total = getFileTotal(); |
05 | File tempFile = new File(FileHelper.getStorgePath(filename) + FileHelper.TMP_SUFFIX); |
06 | if (Logger.isDebugEnabled()) { |
07 | Logger.debug( "saveUploadFile upload tmp file is: " + tempFile.getAbsolutePath()); |
09 | if (!tempFile.exists()) { |
10 | ApiResult result = new ApiResult(); |
11 | result.resultCode = ApiResultCode.UPLOAD_FILE_FAIL; |
12 | response.status = Http.StatusCode.INTERNAL_ERROR; |
13 | throw new CustomJsonResult(result); |
15 | File destFile = new File(FileHelper.getStorgePath(filename)); |
16 | if (destFile.length() >= total) { |
18 | FileUtils.deleteQuietly(tempFile); |
20 | if (Logger.isDebugEnabled()) { |
21 | Logger.debug( "saveUploadFile video has upload completely" ); |
26 | FileResult result = new FileResult(); |
27 | result.resultCode = ApiResultCode.SUCCESS; |
28 | result.videoUrl = video.videoUrl; |
29 | result.shortUrl = video.shortUrl; |
30 | throw new CustomJsonResult(result); |
33 | FileHelper.saveFileFromTmp(tempFile, destFile); |
34 | } catch (IOException e) { |
35 | Logger.error( "saveUploadFile " + e.getMessage(), e); |
36 | ApiResult result = new ApiResult(); |
37 | result.resultCode = ApiResultCode.UPLOAD_FILE_FAIL; |
38 | response.status = Http.StatusCode.INTERNAL_ERROR; |
39 | throw new CustomJsonResult(result); |
42 | afterWrite(filename, destFile, total); |
这个解决方法,和我们的服务绑定的比较紧,不能解决较为通用的问题 只是提出一种思路。