網(wǎng)站如何調(diào)用手機(jī)淘寶做淘寶客最近新聞?wù)?0字
前言
大家好,我是老馬。很高興遇到你。
我們?yōu)?java 開發(fā)者實(shí)現(xiàn)了 java 版本的 nginx
https://github.com/houbb/nginx4j
如果你想知道 servlet 如何處理的,可以參考我的另一個(gè)項(xiàng)目:
手寫從零實(shí)現(xiàn)簡易版 tomcat minicat
手寫 nginx 系列
如果你對(duì) nginx 原理感興趣,可以閱讀:
從零手寫實(shí)現(xiàn) nginx-01-為什么不能有 java 版本的 nginx?
從零手寫實(shí)現(xiàn) nginx-02-nginx 的核心能力
從零手寫實(shí)現(xiàn) nginx-03-nginx 基于 Netty 實(shí)現(xiàn)
從零手寫實(shí)現(xiàn) nginx-04-基于 netty http 出入?yún)?yōu)化處理
從零手寫實(shí)現(xiàn) nginx-05-MIME類型(Multipurpose Internet Mail Extensions,多用途互聯(lián)網(wǎng)郵件擴(kuò)展類型)
從零手寫實(shí)現(xiàn) nginx-06-文件夾自動(dòng)索引
從零手寫實(shí)現(xiàn) nginx-07-大文件下載
從零手寫實(shí)現(xiàn) nginx-08-范圍查詢
從零手寫實(shí)現(xiàn) nginx-09-文件壓縮
從零手寫實(shí)現(xiàn) nginx-10-sendfile 零拷貝
從零手寫實(shí)現(xiàn) nginx-11-file+range 合并
從零手寫實(shí)現(xiàn) nginx-12-keep-alive 連接復(fù)用
從零手寫實(shí)現(xiàn) nginx-13-nginx.conf 配置文件介紹
從零手寫實(shí)現(xiàn) nginx-14-nginx.conf 和 hocon 格式有關(guān)系嗎?
從零手寫實(shí)現(xiàn) nginx-15-nginx.conf 如何通過 java 解析處理?
從零手寫實(shí)現(xiàn) nginx-16-nginx 支持配置多個(gè) server
從零手寫實(shí)現(xiàn) nginx-17-nginx 默認(rèn)配置優(yōu)化
從零手寫實(shí)現(xiàn) nginx-18-nginx 請(qǐng)求頭響應(yīng)頭的處理
背景
最初感覺范圍處理和文件的處理不是相同的邏輯,所以做了拆分。
但是后來發(fā)現(xiàn)有很多公共的邏輯。
主要兩種優(yōu)化方式:
把范圍+文件合并到同一個(gè)文件中處理。添加各種判斷代碼
采用模板方法,便于后續(xù)拓展修改。
這里主要嘗試下第 2 種,便于后續(xù)的拓展。
代碼的相似之處
首先,我們要找到二者的相同之處。
range 主要其實(shí)是開始位置和長度,和普通的處理存在差異。
基礎(chǔ)文件實(shí)現(xiàn)
我們對(duì)常見的部分抽象出來,便于子類拓展
/*** 文件** @since 0.10.0* @author 老馬笑西風(fēng)*/
public class AbstractNginxRequestDispatchFile extends AbstractNginxRequestDispatch {/*** 獲取長度* @param context 上下文* @return 結(jié)果*/protected long getActualLength(final NginxRequestDispatchContext context) {final File targetFile = context.getFile();return targetFile.length();}/*** 獲取開始位置* @param context 上下文* @return 結(jié)果*/protected long getActualStart(final NginxRequestDispatchContext context) {return 0L;}protected void fillContext(final NginxRequestDispatchContext context) {long actualLength = getActualLength(context);long actualStart = getActualStart(context);context.setActualStart(actualStart);context.setActualFileLength(actualLength);}/*** 填充響應(yīng)頭* @param context 上下文* @param response 響應(yīng)* @since 0.10.0*/protected void fillRespHeaders(final NginxRequestDispatchContext context,final HttpRequest request,final HttpResponse response) {final File targetFile = context.getFile();final long fileLength = context.getActualFileLength();// 文件比較大,直接下載處理if(fileLength > NginxConst.BIG_FILE_SIZE) {logger.warn("[Nginx] fileLength={} > BIG_FILE_SIZE={}", fileLength, NginxConst.BIG_FILE_SIZE);response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"" + targetFile.getName() + "\"");}// 如果請(qǐng)求中有KEEP ALIVE信息if (HttpUtil.isKeepAlive(request)) {response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);}response.headers().set(HttpHeaderNames.CONTENT_TYPE, InnerMimeUtil.getContentTypeWithCharset(targetFile, context.getNginxConfig().getCharset()));response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);}protected HttpResponse buildHttpResponse(NginxRequestDispatchContext context) {HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);return response;}/*** 是否需要壓縮處理* @param context 上下文* @return 結(jié)果*/protected boolean isZipEnable(NginxRequestDispatchContext context) {return InnerGzipUtil.isMatchGzip(context);}/*** gzip 的提前預(yù)處理* @param context 上下文* @param response 響應(yīng)*/protected void beforeZip(NginxRequestDispatchContext context, HttpResponse response) {File compressFile = InnerGzipUtil.prepareGzip(context, response);context.setFile(compressFile);}/*** gzip 的提前預(yù)處理* @param context 上下文* @param response 響應(yīng)*/protected void afterZip(NginxRequestDispatchContext context, HttpResponse response) {InnerGzipUtil.afterGzip(context, response);}protected boolean isZeroCopyEnable(NginxRequestDispatchContext context) {final NginxConfig nginxConfig = context.getNginxConfig();return EnableStatusEnum.isEnable(nginxConfig.getNginxSendFileConfig().getSendFile());}protected void writeAndFlushOnComplete(final ChannelHandlerContext ctx,final NginxRequestDispatchContext context) {// 傳輸完畢,發(fā)送最后一個(gè)空內(nèi)容,標(biāo)志傳輸結(jié)束ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);// 如果不支持keep-Alive,服務(wù)器端主動(dòng)關(guān)閉請(qǐng)求if (!HttpUtil.isKeepAlive(context.getRequest())) {lastContentFuture.addListener(ChannelFutureListener.CLOSE);}}@Overridepublic void doDispatch(NginxRequestDispatchContext context) {final FullHttpRequest request = context.getRequest();final File targetFile = context.getFile();final ChannelHandlerContext ctx = context.getCtx();logger.info("[Nginx] start dispatch, path={}", targetFile.getAbsolutePath());// 長度+開始等基本信息fillContext(context);// 響應(yīng)HttpResponse response = buildHttpResponse(context);// 添加請(qǐng)求頭fillRespHeaders(context, request, response);//gzipboolean zipFlag = isZipEnable(context);try {if(zipFlag) {beforeZip(context, response);}// 寫基本信息ctx.write(response);// 零拷貝boolean isZeroCopyEnable = isZeroCopyEnable(context);if(isZeroCopyEnable) {//zero-copydispatchByZeroCopy(context);} else {// 普通dispatchByRandomAccessFile(context);}} finally {// 最后處理if(zipFlag) {afterZip(context, response);}}}/*** Netty 之 FileRegion 文件傳輸: https://www.jianshu.com/p/447c2431ac32** @param context 上下文*/protected void dispatchByZeroCopy(NginxRequestDispatchContext context) {final ChannelHandlerContext ctx = context.getCtx();final File targetFile = context.getFile();// 分塊傳輸文件內(nèi)容final long actualStart = context.getActualStart();final long actualFileLength = context.getActualFileLength();try {RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r");FileChannel fileChannel = randomAccessFile.getChannel();// 使用DefaultFileRegion進(jìn)行零拷貝傳輸DefaultFileRegion fileRegion = new DefaultFileRegion(fileChannel, actualStart, actualFileLength);ChannelFuture transferFuture = ctx.writeAndFlush(fileRegion);// 監(jiān)聽傳輸完成事件transferFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) {try {if (future.isSuccess()) {writeAndFlushOnComplete(ctx, context);} else {// 處理傳輸失敗logger.error("[Nginx] file transfer failed", future.cause());throw new Nginx4jException(future.cause());}} finally {// 確保在所有操作完成之后再關(guān)閉文件通道和RandomAccessFiletry {fileChannel.close();randomAccessFile.close();} catch (Exception e) {logger.error("[Nginx] error closing file channel", e);}}}});// 記錄傳輸進(jìn)度(如果需要,可以通過監(jiān)聽器或其他方式實(shí)現(xiàn))logger.info("[Nginx] file process >>>>>>>>>>> {}", actualFileLength);} catch (Exception e) {logger.error("[Nginx] file meet ex", e);throw new Nginx4jException(e);}}// 分塊傳輸文件內(nèi)容/*** 分塊傳輸-普通方式* @param context 上下文*/protected void dispatchByRandomAccessFile(NginxRequestDispatchContext context) {final ChannelHandlerContext ctx = context.getCtx();final File targetFile = context.getFile();// 分塊傳輸文件內(nèi)容long actualFileLength = context.getActualFileLength();// 分塊傳輸文件內(nèi)容final long actualStart = context.getActualStart();long totalRead = 0;try(RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r")) {// 開始位置randomAccessFile.seek(actualStart);ByteBuffer buffer = ByteBuffer.allocate(NginxConst.CHUNK_SIZE);while (totalRead <= actualFileLength) {int bytesRead = randomAccessFile.read(buffer.array());if (bytesRead == -1) { // 文件讀取完畢logger.info("[Nginx] file read done.");break;}buffer.limit(bytesRead);// 寫入分塊數(shù)據(jù)ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));buffer.clear(); // 清空緩沖區(qū)以供下次使用// process 可以考慮加一個(gè) listenertotalRead += bytesRead;logger.info("[Nginx] file process >>>>>>>>>>> {}/{}", totalRead, actualFileLength);}// 最后的處理writeAndFlushOnComplete(ctx, context);} catch (Exception e) {logger.error("[Nginx] file meet ex", e);throw new Nginx4jException(e);}}}
這樣原來的普通文件類只需要直接繼承。
范圍類重置如下方法即可:
/*** 文件范圍查詢** @since 0.7.0* @author 老馬嘯西風(fēng)*/
public class NginxRequestDispatchFileRange extends AbstractNginxRequestDispatchFile {private static final Log logger = LogFactory.getLog(AbstractNginxRequestDispatchFullResp.class);@Overrideprotected HttpResponse buildHttpResponse(NginxRequestDispatchContext context) {long start = context.getActualStart();// 構(gòu)造HTTP響應(yīng)HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,start < 0 ? HttpResponseStatus.OK : HttpResponseStatus.PARTIAL_CONTENT);return response;}@Overrideprotected void fillContext(NginxRequestDispatchContext context) {final long fileLength = context.getFile().length();final HttpRequest httpRequest = context.getRequest();// 解析Range頭String rangeHeader = httpRequest.headers().get("Range");logger.info("[Nginx] fileRange start rangeHeader={}", rangeHeader);long[] range = parseRange(rangeHeader, fileLength);long start = range[0];long end = range[1];long actualLength = end - start + 1;context.setActualStart(start);context.setActualFileLength(actualLength);}protected long[] parseRange(String rangeHeader, long totalLength) {// 簡單解析Range頭,返回[start, end]// Range頭格式為: "bytes=startIndex-endIndex"if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {String range = rangeHeader.substring("bytes=".length());String[] parts = range.split("-");long start = parts[0].isEmpty() ? totalLength - 1 : Long.parseLong(parts[0]);long end = parts.length > 1 ? Long.parseLong(parts[1]) : totalLength - 1;return new long[]{start, end};}return new long[]{-1, -1}; // 表示無效的范圍請(qǐng)求}}
小結(jié)
模板方法對(duì)于代碼的復(fù)用好處還是很大的,不然后續(xù)拓展特性,很多地方都需要修改多次。
下一節(jié),我們考慮實(shí)現(xiàn)一下 HTTP keep-alive 的支持。
我是老馬,期待與你的下次重逢。
開源地址
為了便于大家學(xué)習(xí),已經(jīng)將 nginx 開源
https://github.com/houbb/nginx4j