前言
大家好,我是老马。很高兴遇到你。
我们希望实现最简单的 http 服务信息,可以处理静态文件。
如果你想知道 servlet 如何处理的,可以参考我的另一个项目:
手写从零实现简易版 tomcat minicat
手写 nginx 系列
如果你对 nginx 原理感兴趣,可以阅读:
从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?
从零手写实现 nginx-03-nginx 基于 Netty 实现
从零手写实现 nginx-04-基于 netty http 出入参优化处理
从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)
从零手写实现 nginx-12-keep-alive 连接复用
从零手写实现 nginx-13-nginx.conf 配置文件介绍
从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?
从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?
从零手写实现 nginx-16-nginx 支持配置多个 server
什么是 http 范围查询?
HTTP范围请求(Range Requests)是一种让客户端可以请求资源(如文件)的一部分而不是全部的机制。
这在处理大文件时特别有用,例如,视频点播服务或大文件下载,用户可以请求文件的特定部分进行播放或下载。
范围请求通过在HTTP请求中添加Range
头来实现。以下是Range
头的一些关键点:
-
语法 :
Range
头的语法遵循以下格式:vbnetRange: bytes=<start-byte>-<end-byte>
其中
<start-byte>
和<end-byte>
指定了请求的字节范围(包含)。如果<end-byte>
被省略,服务器将从<start-byte>
发送到文件的末尾。 -
例子:
-
请求文件的前8192字节:
iniRange: bytes=0-8191
-
请求文件从第5120个字节开始到第10239个字节:
iniRange: bytes=5120-10239
-
请求文件从第2048个字节开始到文件末尾:
iniRange: bytes=2048-
-
-
响应 : 如果服务器支持范围请求,并且请求的范围有效,服务器将返回状态码
206 Partial Content
,并将请求范围内的数据发送给客户端。服务器还需要在响应中包含
Content-Range
头,指示实际发送的数据范围。 -
Content-Range
头 :Content-Range
头的格式如下:arduinoContent-Range: bytes <start-byte>-<end-byte>/<total-file-size>
<start-byte>
和<end-byte>
与请求中的范围对应。<total-file-size>
是资源的总大小。
-
不支持范围请求 : 如果服务器不支持范围请求,或者请求的范围无效(例如,开始字节大于文件大小),服务器将返回状态码
200 OK
,并发送资源的全部内容。 -
多范围请求 : HTTP协议也支持请求多个非连续的范围,但这需要特定的服务器支持。多范围请求的
Range
头会包含多个范围,用逗号分隔:iniRange: bytes=500-600,601-700
对于多范围请求,服务器可能返回多个部分,每个部分都有自己的
Content-Range
头,并且包装在multipart/byteranges
的Content-Type
中。 -
用例: 范围请求常用于以下场景:
- 恢复中断的下载。
- 视频点播服务中的"快进"功能。
- 大文件的增量更新或备份。
-
注意事项:
- 并非所有的服务器都支持范围请求,这取决于服务器的配置和能力。
- 对于不支持范围请求的资源,客户端仍然可以使用分块下载技术来实现类似的功能。
范围请求是HTTP协议中一个强大且灵活的特性,它为客户端提供了对资源访问的细粒度控制。
netty 实现
流程
在Netty中实现HTTP范围请求(Range Requests),你需要处理HTTP请求,解析Range头,并根据请求的范围发送相应的响应。以下是实现这一功能的步骤:
-
解析HTTP请求 :首先,你需要解析客户端发送的HTTP请求,特别是
Range
请求头。 -
处理Range头 :根据
Range
头指定的范围,确定要发送的字节区间。 -
构造HTTP响应 :创建HTTP响应,如果范围有效,设置状态码为
206 Partial Content
,否则使用200 OK
。 -
设置Content-Range头 :在响应中添加
Content-Range
头,指示实际发送的数据范围。 -
发送数据:使用适当的方式发送请求范围内的数据。
-
结束响应 :发送结束标记,如
LastHttpContent.EMPTY_LAST_CONTENT
。
核心实现
java
public void doDispatch(NginxRequestDispatchContext context) {
final HttpRequest request = context.getRequest();
final File file = context.getFile();
final ChannelHandlerContext ctx = context.getCtx();
// 解析Range头
String rangeHeader = request.headers().get("Range");
logger.info("[Nginx] fileRange start rangeHeader={}", rangeHeader);
long fileLength = file.length(); // 假设file是你要发送的File对象
long[] range = parseRange(rangeHeader, fileLength);
long start = range[0];
long end = range[1];
// 构造HTTP响应
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
start < 0 ? HttpResponseStatus.OK : HttpResponseStatus.PARTIAL_CONTENT);
// 设置Content-Type
response.headers().set(HttpHeaderNames.CONTENT_TYPE, InnerMimeUtil.getContentType(file));
if (start >= 0) {
// 设置Content-Range
if (end < 0) {
end = fileLength - 1;
}
response.headers().set(HttpHeaderNames.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileLength);
// 设置Content-Length
int contentLength = (int) (end - start + 1);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentLength);
// 发送响应头
ctx.write(response);
try (FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ)) {
fileChannel.position(start); // 设置文件通道的起始位置
ByteBuffer buffer = ByteBuffer.allocate(NginxConst.CHUNK_SIZE);
while (end >= start) {
// 读取文件到ByteBuffer
int bytesRead = fileChannel.read(buffer);
if (bytesRead == -1) { // 文件读取完毕
break;
}
buffer.flip(); // 切换到读模式
ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));
buffer.compact(); // 保留未读取的数据,并为下次读取腾出空间
start += bytesRead; // 更新下一个读取的起始位置
}
ctx.flush(); // 确保所有数据都被发送
// 发送结束标记
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
.addListener(ChannelFutureListener.CLOSE); // 如果连接断开,则关闭
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
范围处理如下:
java
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}; // 表示无效的范围请求
}
请求测试
yaml
>curl -i -H "Range: bytes=0-" http://192.168.1.12:8080/mime/1.css
HTTP/1.1 206 Partial Content
content-type: text/css
content-range: bytes 0-198/199
content-length: 199
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
h1 {
color: #333333;
}
.container {
width: 80%;
margin: auto;
}
>curl -i -H "Range: bytes=127-" http://192.168.1.12:8080/mime/1.css
HTTP/1.1 206 Partial Content
content-type: text/css
content-range: bytes 127-198/199
content-length: 72
}
.container {
width: 80%;
margin: auto;
}
或者过长的内容:
go
>curl -i -H "Range: bytes=255-" http://192.168.1.12:8080/mime/1.css
HTTP/1.1 206 Partial Content
content-type: text/css
content-range: bytes 255-198/199
curl: (8) Invalid Content-Length: value