前言

大家好,我是老马。很高兴遇到你。

我们为 java 开发者实现了 java 版本的 nginx

https://github.com/houbb/nginx4j

如果你想知道 servlet 如何处理的,可以参考我的另一个项目:

手写从零实现简易版 tomcat minicat

手写 nginx 系列

如果你对 nginx 原理感兴趣,可以阅读:

从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?

从零手写实现 nginx-02-nginx 的核心能力

从零手写实现 nginx-03-nginx 基于 Netty 实现

从零手写实现 nginx-04-基于 netty http 出入参优化处理

从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)

从零手写实现 nginx-06-文件夹自动索引

从零手写实现 nginx-07-大文件下载

从零手写实现 nginx-08-范围查询

从零手写实现 nginx-09-文件压缩

从零手写实现 nginx-10-sendfile 零拷贝

从零手写实现 nginx-11-file+range 合并

从零手写实现 nginx-12-keep-alive 连接复用

从零手写实现 nginx-13-nginx.conf 配置文件介绍

从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?

从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?

从零手写实现 nginx-16-nginx 支持配置多个 server

从零手写实现 nginx-17-nginx 默认配置优化

从零手写实现 nginx-18-nginx 请求头+响应头操作

从零手写实现 nginx-19-nginx cors

从零手写实现 nginx-20-nginx 占位符 placeholder

从零手写实现 nginx-21-nginx modules 模块信息概览

从零手写实现 nginx-22-nginx modules 分模块加载优化

从零手写实现 nginx-23-nginx cookie 的操作处理

从零手写实现 nginx-24-nginx IF 指令

从零手写实现 nginx-25-nginx map 指令

从零手写实现 nginx-26-nginx rewrite 指令

从零手写实现 nginx-27-nginx return 指令

从零手写实现 nginx-28-nginx error_pages 指令

从零手写实现 nginx-29-nginx try_files 指令

从零手写实现 nginx-30-nginx proxy_pass upstream 指令

从零手写实现 nginx-31-nginx load-balance 负载均衡

从零手写实现 nginx-32-nginx load-balance 算法 java 实现

从零手写实现 nginx-33-nginx http proxy_pass 测试验证

从零手写实现 nginx-34-proxy_pass 配置加载处理

从零手写实现 nginx-35-proxy_pass netty 如何实现?

背景

最初感觉范围处理和文件的处理不是相同的逻辑,所以做了拆分。

但是后来发现有很多公共的逻辑。

主要两种优化方式:

  1. 把范围+文件合并到同一个文件中处理。添加各种判断代码

  2. 采用模板方法,便于后续拓展修改。

这里主要尝试下第 2 种,便于后续的拓展。

代码的相似之处

首先,我们要找到二者的相同之处。

range 主要其实是开始位置和长度,和普通的处理存在差异。

基础文件实现

我们对常见的部分抽象出来,便于子类拓展

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/** * 文件 * * @since 0.10.0 * @author 老马笑西风 */ public class AbstractNginxRequestDispatchFile extends AbstractNginxRequestDispatch { /** * 获取长度 * @param context 上下文 * @return 结果 */ protected long getActualLength(final NginxRequestDispatchContext context) { final File targetFile = context.getFile(); return targetFile.length(); } /** * 获取开始位置 * @param context 上下文 * @return 结果 */ 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); } /** * 填充响应头 * @param context 上下文 * @param response 响应 * @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() + "\""); } // 如果请求中有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 结果 */ protected boolean isZipEnable(NginxRequestDispatchContext context) { return InnerGzipUtil.isMatchGzip(context); } /** * gzip 的提前预处理 * @param context 上下文 * @param response 响应 */ protected void beforeZip(NginxRequestDispatchContext context, HttpResponse response) { File compressFile = InnerGzipUtil.prepareGzip(context, response); context.setFile(compressFile); } /** * gzip 的提前预处理 * @param context 上下文 * @param response 响应 */ 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) { // 传输完毕,发送最后一个空内容,标志传输结束 ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); // 如果不支持keep-Alive,服务器端主动关闭请求 if (!HttpUtil.isKeepAlive(context.getRequest())) { lastContentFuture.addListener(ChannelFutureListener.CLOSE); } } @Override public 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); // 响应 HttpResponse response = buildHttpResponse(context); // 添加请求头 fillRespHeaders(context, request, response); //gzip boolean zipFlag = isZipEnable(context); try { if(zipFlag) { beforeZip(context, response); } // 写基本信息 ctx.write(response); // 零拷贝 boolean isZeroCopyEnable = isZeroCopyEnable(context); if(isZeroCopyEnable) { //zero-copy dispatchByZeroCopy(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(); // 分块传输文件内容 final long actualStart = context.getActualStart(); final long actualFileLength = context.getActualFileLength(); try { RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r"); FileChannel fileChannel = randomAccessFile.getChannel(); // 使用DefaultFileRegion进行零拷贝传输 DefaultFileRegion fileRegion = new DefaultFileRegion(fileChannel, actualStart, actualFileLength); ChannelFuture transferFuture = ctx.writeAndFlush(fileRegion); // 监听传输完成事件 transferFuture.addListener(new ChannelFutureListener() { @Override public 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 { // 确保在所有操作完成之后再关闭文件通道和RandomAccessFile try { fileChannel.close(); randomAccessFile.close(); } catch (Exception e) { logger.error("[Nginx] error closing file channel", e); } } } }); // 记录传输进度(如果需要,可以通过监听器或其他方式实现) logger.info("[Nginx] file process >>>>>>>>>>> {}", actualFileLength); } catch (Exception e) { logger.error("[Nginx] file meet ex", e); throw new Nginx4jException(e); } } // 分块传输文件内容 /** * 分块传输-普通方式 * @param context 上下文 */ protected void dispatchByRandomAccessFile(NginxRequestDispatchContext context) { final ChannelHandlerContext ctx = context.getCtx(); final File targetFile = context.getFile(); // 分块传输文件内容 long actualFileLength = context.getActualFileLength(); // 分块传输文件内容 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); // 写入分块数据 ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer))); buffer.clear(); // 清空缓冲区以供下次使用 // process 可以考虑加一个 listener totalRead += 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); } } }

这样原来的普通文件类只需要直接继承。

范围类重置如下方法即可:

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/** * 文件范围查询 * * @since 0.7.0 * @author 老马啸西风 */ public class NginxRequestDispatchFileRange extends AbstractNginxRequestDispatchFile { private static final Log logger = LogFactory.getLog(AbstractNginxRequestDispatchFullResp.class); @Override protected HttpResponse buildHttpResponse(NginxRequestDispatchContext context) { long start = context.getActualStart(); // 构造HTTP响应 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, start < 0 ? HttpResponseStatus.OK : HttpResponseStatus.PARTIAL_CONTENT); return response; } @Override protected 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}; // 表示无效的范围请求 } }

小结

模板方法对于代码的复用好处还是很大的,不然后续拓展特性,很多地方都需要修改多次。

下一节,我们考虑实现一下 HTTP keep-alive 的支持。

我是老马,期待与你的下次重逢。

开源地址

为了便于大家学习,已经将 nginx 开源

https://github.com/houbb/nginx4j