有些东西学起来当时嗯嗯哦就过去了,听着还像是那回事儿,人问起来也能答出个五六七八,但真要理解,还真得自己上手摆弄一遍。
不把原理弄清楚了,哪怕知道怎么个操作法儿,实际上心里还是特别虚。把基础弄明白了,哪怕发展再快,万变不离其宗,还能蹦跶出什么来。
再说个题外话,《计算机网络》这门课这是门神课,可惜可惜。。。
写在前面
- 为了加深对Http协议的理解,现在目标是实现一个小型的Http文件服务器。
- HTTP是一个属于应用层的面向对象的协议,它基于TCP/IP通信协议来传递数据。针对这个,服务器端采取传统的阻塞型Java SocketServer处理TCP连接。
- 因为每一个TCP连接都会占用一个线程资源,故而采取了一定的补救措施,即监控所有Socket,关闭长时间未关闭的连接。
- 因为是文件服务器,故而更关注Get方法的实现;
- 为了断点续传功能,关注range头域;
- 为了授权功能,关注authorization头域+cookie;
- 增加了缓存文件校对,支持If-Modified-Since头域;
- 另外,也试验性地实现了Chunked传输方式;
- 参考了以下链接(可能漏了一些,😓):
写在中间
常见请求/回复示例
普通Get请求示例:
GET / HTTP/1.1
Host: 127.0.0.1:7778
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8
//注意Header结束后Body之前(如果有的话)有一个\r\n
普通Response 200OK示例:
...
String html = "<html><head><title>test</title></head><body><h1>测试</h1></body></html>";
writer.write("HTTP/1.1 200 OK\r\n");
writer.write("Date: "+ HttpResource.GMTDateFormat.format(System.currentTimeMillis()));
writer.write("\r\nContent-Type: text/html; charset=UTF-8\r\n");
writer.write("Content-Length: "+ html.length()+ "\r\n");
writer.write("\r\n");
//上面Header结束,以下是内容,Content-Length很关键
writer.write(html);
writer.flush();
授权认证示例
客户端访问服务器,服务器关注Cookies和Authorization标签,
- Cookies通过,正常处理;
- Authorization通过,返回
Set-Cookie
新建会话时长有效期的Session cookie,其它正常处理; - 未通过认证,返回
WWW-Authenticate
头域告知需要鉴权
未通过回复示例:
httpResponse.dataLength = HttpResource.PAGE_401.length;
httpResponse.headers.put("WWW-Authenticate", "Basic realm=\"NiceLee's Site\"");
headerTrans.transferCommonHeader(httpResponse, out);
// out date-length & data
out.write("Content-Length: ".getBytes());
out.write(("" + httpResponse.dataLength).getBytes());
out.write(HttpResource.BREAK_LINE);
out.write(HttpResource.BREAK_LINE);
out.write(HttpResource.PAGE_401);
授权Get请求示例:
GET /sources HTTP/1.1
Host: 127.0.0.1:7777
Connection: keep-alive
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8
//注意Header结束后Body之前(如果有的话)有一个\r\n
断点续传示例
断点续传Get请求示例,重点在Range:
GET /sources/test.mp4 HTTP/1.1
Accept:*/*
Accept-Encoding:identity;q=1, *;q=0
Accept-Language:zh-CN,zh;q=0.8
Authorization:Basic YWRtaW46YWRtaW4=
Cache-Control:no-cache
Connection:keep-alive
Cookie:Hm_lvt_5e8b566b55a65225efb0997910ade81f=1547108968,1547616967; Hm_lvt_11f780e4e4ccd4e99b101eac776e93e4=1548511001,1548560945,1548578471,1550039650; Hm_lpvt_11f780e4e4ccd4e99b101eac776e93e4=1550047568
Host:127.0.0.1:7777
Pragma:no-cache
Range:bytes=225935360-
Referer:http://127.0.0.1:7777/sources/test.mp4
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0
断点续传回复示例。若不从头开始,返回206;注意Content-Range
HTTP/1.1 206 Partial Content
Accept-Ranges:bytes
Connection:keep-alive
Content-Length:1729656
Content-Range:bytes 225935360-227665015/227665016
Content-Type:video/mp4
Date:Wed, 13 Feb 2019 16:54:15 GMT
Last-Modified:Sat, 19 Jan 2019 11:42:33 GMT
部分代码:
String range;
if ((range = httpRequest.headers.get("range")) != null) {
// System.out.println("Range Required: " +range);
Pattern patternFileRange = Pattern.compile("^bytes=([0-9]+)-([0-9]*)$");
Matcher matcher = patternFileRange.matcher(range);
if (matcher.find()) {
long begin = Long.parseLong(matcher.group(1));
long end = file.length() - 1;
try {
end = Long.parseLong(matcher.group(2));
end = end < (file.length() - 1) ? end : (file.length() - 1);
} catch (Exception e) {
}
if (begin > 0) {
httpResponse.do206();
}
headerTrans.transferCommonHeader(httpResponse, out);
dataTrans.transferFileWithRange(begin, end, out, file);
} else {
headerTrans.transferCommonHeader(httpResponse, out);
dataTrans.transferFileCommon(out, file);
}
}
缓存文件校对
比较简单,直接上码:
String cacheTime = httpRequest.headers.get("If-Modified-Since");
//System.out.println("headers 缓存时间: " + cacheTime);
if (cacheTime != null) {
try {
//System.out.println("对方本地浏览器缓存时间" + HttpResource.GMTDateFormat.parse(cacheTime).getTime() );
//System.out.println("服务器文件时间" + file.lastModified() );
if (HttpResource.GMTDateFormat.parse(cacheTime).getTime() + 1000 >= file.lastModified()) {
doResponseWithFileNoChange(httpResponse, out);
//System.out.println("对方本地浏览器已有缓存, 返回304");
return;
}
//System.out.println("对方本地浏览器已有缓存, 但已过时");
} catch (Exception e) {
e.printStackTrace();
}
}
返回304方法:
/**
* 若URL对应的文件缓存未过时, 使用该方法返回
*
* @param httpResponse
* @param writer
* @throws IOException
*/
public static void doResponseWithFileNoChange(HttpResponse httpResponse, BufferedOutputStream out)
throws IOException {
HttpHeaderTransfer headerTrans = new HttpHeaderTransfer();
// 304
httpResponse.do304();
httpResponse.dataLength = 0;
headerTrans.transferCommonHeader(httpResponse, out);
// out date-length & data
out.write("Content-Length: 0".getBytes());
out.write(HttpResource.BREAK_LINE);
out.write(HttpResource.BREAK_LINE);
out.flush();
}
Chunked方式实现
/**
* 使用Chunked 方式传输文件
*
* @param out 面向客户端的输出流
* @param file 文件
* @throws IOException
*/
public void transferFileChunked(BufferedOutputStream out, File file) throws IOException {
RandomAccessFile raf = new RandomAccessFile(file, "r");
try {
System.out.println("准备传输 + ");
out.write("Transfer-Encoding: chunked".getBytes());
out.write(BREAK_LINE);
out.write(BREAK_LINE);
int sizeRead = raf.read(data);
while (sizeRead > 0) {
System.out.println("准备传输 + " + String.format("%x", sizeRead));
out.write(String.format("%x", sizeRead).getBytes());
out.write(BREAK_LINE);
out.write(data, 0, sizeRead);
out.write(BREAK_LINE);
sizeRead = raf.read(data);
out.flush();
}
out.write(48);
out.write(BREAK_LINE);
out.write(BREAK_LINE);
out.write(BREAK_LINE);
out.flush();
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
try {
raf.close();
} catch (Exception e) {
}
}
}