简介在上一篇文章中,我们学习了如何在netty中搭建一个HTTP服务器,讨论了如何处理和响应客户端发送的请求。今天我们就来讨论一下如何在netty中搭建文件服务器进行文件传输时应该注意的问题。文件的content-type客户端向服务器请求一个文件,服务器会在返回的HTTP头中包含一个content-type,这个content-type表示返回的文件类型。这种类型应该如何确认?一般来说,文件类型是根据文件的扩展名来确定的。根据RFC4288的规范,所有网络媒体类型都必须注册。Apache还提供了文件MIME类型和扩展名的映射表。因为文件类型比较多,我们先看几个常用的类型如下:.spreadsheetml.sheetxlsxapplication/msworddocapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentdocxapplication/vnd.openxmlformats-officedocument.presentationml.presentationpptxapplication/vnd.ms-powerpointpptapplication/pdfpdfJDK提供了一个MimetypesFileContentMap类,可以根据请求的文件提供一个TypeTypeMap方法路径信息,推断其MIME类型类型:privatestaticvoidsetContentTypeHeader(HttpResponseresponse,Filefile){MimetypesFileTypeMapmimeTypesMap=newMimetypesFileTypeMap();response.headers().set(HttpHeaderNames.CONTENT_TYPE,mimeTypes(Map.getContentfile.getPath()));}客户端缓存文件对于HTTP文件请求,为了保证请求的速度,会使用客户端缓存机制。例如,客户端向服务器请求一个文件A.txt。服务端收到请求后,会将A.txt文件发送给客户端。请求流程如下:第一步:客户端向服务器端请求文件====================GET/file1.txtHTTP/1.1第二步:服务器返回文件,并附加文件时间信息:====================HTTP/1.1200OKDate:Mon,23Aug202117:52:30GMT+08:00最后修改时间:2021年8月10日星期二18:05:35GMT+08:00过期时间:2021年8月23日星期一17:53:30GMT+08:00缓存控制:私有、最大年龄=60一般来说,如果客户端是现代浏览器的话,A.txt会被缓存。在接下来的调用中,只需要在头部加上If-Modified-Since,向服务器询问文件是否被修改过。如果文件没有被修改,服务器将返回304NotModified。客户端得到这个状态后,就会使用本地的缓存文件。第3步:客户端再次请求文件====================GET/file1.txtHTTP/1.1If-Modified-Since:Mon,23Aug202117:55:30GMT+08:00第4步:服务器响应请求====================HTTP/1.1304未修改日期:2021年8月23日星期一17:55:32GMT+08:00在服务端的代码层面,我们首先需要返回一个日期字段,响应中通常需要返回一个日期字段,比如Date、Last-Modified、Expires、Cache-Control等。:SimpleDateFormatdateFormatter=newSimpleDateFormat(HTTP_DATE_FORMAT,Locale.US);dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));//日期标题Calendartime=newGregorianCalendar();log.info(dateFormatter.format(time.getTime()));response.headers().set(HttpHeaderNames.DATE,dateFormatter.format(time.getTime()));//缓存头time.add(Calendar.SECOND,HTTP_CACHE_SECONDS);response.headers().set(HttpHeaderNames.EXPIRES,dateFormatter.format(time.getTime()));response.headers().set(HttpHeaderNames.CACHE_CONTROL,"private,max-age="+HTTP_CACHE_SECONDS);response.headers().set(HttpHeaderNames.LAST_MODIFIED,dateFormatter.format(newDate(fileToCache.lastModified())));然后在收到客户端第二次请求后,需要比较文件的最后修改时间和If-Modified-Since自带的时间,如果没有变化,则发送304状态:FullHttpResponseresponse=newDefaultFullHttpResponse(HTTP_1_1,NOT_MODIFIED,Unpooled.EMPTY_BUFFER);setDateHeader(响应);我们在HTTPFiletypesandcaching中讨论了其他常见的处理,对于一个通用的HTTP服务器,还有很多其他常见的处理需要考虑,比如异常、重定向和Keep-Alive设置对于异常,我们需要根据异常代码构造一个DefaultFullHttpResponse,并设置对应的CONTENT_TYPE头,如下所示:\n",CharsetUtil.UTF_8));response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain;charset=UTF-8");重定向还需要构建一个DefaultFullHttpResponse,状态为302Found,并在响应头中设置location为要重定向的URL地址:FullHttpResponseresponse=newDefaultFullHttpResponse(HTTP_1_1,FOUND,Unpooled.EMPTY_BUFFER);response.headers().set(HttpHeaderNames.LOCATION,newUri);Keep-Alive是HTTP中的一种优化方法,避免每次请求都建立连接。HTTP/1.0中默认的keep-alive为false,HTTP/1.1中默认的keep-alive为true。如果header中手动设置了connection:false,则服务端请求返回也需要设置connection:false。另外,由于HTTP/1.1中默认keep-alive为true,如果通过HttpUtil.isKeepAlive判断,需要判断是否为HTTP/1.0,将keep-alive设置为true。finalbooleankeepAlive=HttpUtil.isKeepAlive(请求);HttpUtil.setContentLength(响应,response.content().readableBytes());如果(!keepAlive){response.headers().set(HttpHeaderNames.CONNECTION,HttpHeaderValues.CLOSE);}elseif(request.protocolVersion().equals(HTTP_1_0)){response.headers().set(HttpHeaderNames.CONNECTION,HttpHeaderValues.KEEP_ALIVE);}文件内容显示处理文件内容显示处理是http服务器的核心,也是比较难理解的地方。首先要设置的是ContentLength,也就是响应文件的长度,可以使用file:RandomAccessFileraf的length方法获取;raf=newRandomAccessFile(file,"r");longfileLength=raf.length();HttpUtil。设置内容长度(响应,文件长度);然后我们需要根据文件扩展名设置相应的CONTENT_TYPE,这在第一节已经介绍过了。然后设置日期和缓存属性。这样我们就得到了一个只包含响应头的DefaultHttpResponse。我们先把只包含响应头的respose写到ctx中。写完HTTPheader之后,下一步就是写HTTPContent。对于HTTP传递的文件,有两种处理方式。第一种方法,如果知道整个响应的内容大小,可以直接在后台复制传输整个文件。如果服务器本身支持零拷贝,可以使用DefaultFileRegion的transferTo方法来传输File或Channel文件。sendFileFuture=ctx.write(newDefaultFileRegion(raf.getChannel(),0,fileLength),ctx.newProgressivePromise());//结束部分lastContentFuture=ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);如果您不知道整个响应大小的上下文,您可以将大文件拆分成块,并在响应标头中将传输编码设置为分块。Netty提供了HttpChunkedInput和ChunkedFile将大文件分割成块进行传输。sendFileFuture=ctx.writeAndFlush(newHttpChunkedInput(newChunkedFile(raf,0,fileLength,8192)),ctx.newProgressivePromise());如果将ChunkedFile写入通道,则需要添加相应的ChunkedWriteHandler来处理分块文件。pipeline.addLast(newChunkedWriteHandler());注意,如果是完整的文件传输,需要手动添加最后的内容部分:lastContentFuture=ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);如果是ChunkedFile,最后的内容部分已经包含在chunkedFile中,不需要手动添加。文件传输进度ChannelFuture可以添加相应的listener来监听文件传输的进度。Netty提供了一个ChannelProgressiveFutureListener来监听文件的进度。可以重写operationProgressed和operationComplete方法来自定义进度监听:sendFileFuture.addListener(newChannelProgressiveFutureListener(){@OverridepublicvoidoperationProgressed(ChannelProgressiveFuturefuture,longprogressive,longtotal){if(total<0){log.info(future.channel()+"传输进度:"+progress);}else{log.info(future.channel()+"传输进度:"+progress+"/"+total);}}@OverridepublicvoidoperationComplete(ChannelProgressiveFuturefuture){log.info(future.channel()+"transmissioncomplete.");}});总结我们已经考虑了一个HTTP文件服务器的一些最基本的注意事项,现在我们可以使用这个文件服务器来提供服务了!本文实例可参考:learn-netty4本文已收录于http://www.flydean.com/20-netty-fileserver/最流行的解读,最深刻的干货,最简洁的教程,很多你不知道的小技巧等你来发现!欢迎关注我的公众号:《程序那些事儿》,懂技术,更懂你!
