最近在弄一些比较底层的东西,所以不可避免的要弄一下网络协议相关的内容。
正好项目转测有些时间,所以就琢磨着写了这个简易的类似apache的容器。
目前这个web容器只支持静态文件的请求处理,后续如果有时间,会加入动态请求的处理。
这个项目的源码目前托管在github上,地址为:
https://github.com/geeeeeeeeeeeeeeeek/NemoWebServer
在开始之前,需要一些基础的知识:
1、NIO。
2、多线程。
3、文件流处理。
4、字符串处理。
先来看一个比较正经的网络请求体:
POST /aaa.html?a=1 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Cookie: optimizelyEndUserId=oeu1508152867688r0.8182557986067157; _ga=GA1.1.162119193.1508152868; Hm_lvt_9a85ad9e95bce2dc91797e2441dc1245=1511238155; pgv_pvid=2314526233; token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMzMwMDAwMDAwMCIsImF1ZGllbmNlIjoiV0VCIiwicG9zaXRpb25JZCI6MywiY3JlYXRlZCI6MTUxMjk4MTY0MjcwNiwicG9zaXRpb25Db2RlIjoiUE9TSVRJT05fSU5WRVNUX01BSk9SIiwiZXhwIjoxNTEzNTg2NDQyLCJvcmdJZCI6MzV9.PhiZB-eaABspm40bbXZVz0rKPoh5bxvjSkVnCyCjbIIzmnKUklZyyNZbVAdfeFNJkd8WzAafYOiXgdwRqsB4lg
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0, no-cache
Pragma: no-cache
Content-Length: 8
b=aaaaaa
需要分析下这个请求体的内容:
第一行:
POST /aaa.html?a=1 HTTP/1.1
不难看出,这里定义了请求的类型(GET\POST\DELETE\PUT),请求的资源文件/aaa.html,地址栏参数?a=1,以及使用的协议版本:HTTP/1.1
余下的第二行到底下,b=aaaaa之前:
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Cookie: optimizelyEndUserId=oeu1508152867688r0.8182557986067157; _ga=GA1.1.162119193.1508152868; Hm_lvt_9a85ad9e95bce2dc91797e2441dc1245=1511238155; pgv_pvid=2314526233; token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMzMwMDAwMDAwMCIsImF1ZGllbmNlIjoiV0VCIiwicG9zaXRpb25JZCI6MywiY3JlYXRlZCI6MTUxMjk4MTY0MjcwNiwicG9zaXRpb25Db2RlIjoiUE9TSVRJT05fSU5WRVNUX01BSk9SIiwiZXhwIjoxNTEzNTg2NDQyLCJvcmdJZCI6MzV9.PhiZB-eaABspm40bbXZVz0rKPoh5bxvjSkVnCyCjbIIzmnKUklZyyNZbVAdfeFNJkd8WzAafYOiXgdwRqsB4lg
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0, no-cache
Pragma: no-cache
Content-Length: 8
这一部分猜测应该是请求头的内容。
而其中有个起头为Cookie的内容,证明这是本次请求带的Cookie。
Cookie: optimizelyEndUserId=oeu1508152867688r0.8182557986067157; _ga=GA1.1.162119193.1508152868; Hm_lvt_9a85ad9e95bce2dc91797e2441dc1245=1511238155; pgv_pvid=2314526233; token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMzMwMDAwMDAwMCIsImF1ZGllbmNlIjoiV0VCIiwicG9zaXRpb25JZCI6MywiY3JlYXRlZCI6MTUxMjk4MTY0MjcwNiwicG9zaXRpb25Db2RlIjoiUE9TSVRJT05fSU5WRVNUX01BSk9SIiwiZXhwIjoxNTEzNTg2NDQyLCJvcmdJZCI6MzV9.PhiZB-eaABspm40bbXZVz0rKPoh5bxvjSkVnCyCjbIIzmnKUklZyyNZbVAdfeFNJkd8WzAafYOiXgdwRqsB4lg
而最后,一行空行之后的b=aaaaaa则是本次请求的请求体参数,对应HttpServletRequest中的request.getAttribute取得的参数。
分析完成了之后,那么就可以做一些编码了:
1、首先,咱们的服务器需要监听一个端口,用来通过这个端口接收一些请求。
java的net包中,提供了相应的方法,用来监听端口,处理请求:
ServerSocket serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
这里监听了8080端口。
2、连接 - 这里使用的是nio堵塞等待模式,据说aio较nio有更好的性能,但是这里先不涉及了。
Socket socket = serverSocket.accept();
开启了堵塞等待,主线程会堵塞在这里,一直等到有连接才继续执行。
3、得到本次请求的输入输出流,用来方便得到请求体和向客户端输出响应内容:
//本次请求的输入流
InputStream input = socket.getInputStream();
//对于本次请求的输出流
OutputStream output = socket.getOutputStream();
4、先从输入流中得到本次请求的请求体:
/**
* 得到请求数据
* @return
*/
private String getRequestData(){
StringBuffer request = new StringBuffer();
byte[] buffer = new byte[2048];
int i = 0;
try {
i = input.read(buffer);
} catch (IOException e) {
e.printStackTrace();
i = -1;
}
for(int k = 0; k < i; k++) {
request.append((char)buffer[k]);
}
return request.toString();
}
这里返回的就是一早咱们看到的就是那一坨网络请求体的东西了。
5、然后即可以从请求体中获取一些请求地址、地址栏参数、请求体参数、请求头、cookie的解析获取。
拿到了这一些常用的参数,那么就可以做很多处理了。
这里直接贴上代码:
/**
* 开始解析
* @param requestData
*/
private void parser(String requestData){
parserUri(requestData);
}
/**
* 解析请求地址
* @param requestData
* @return
*/
private void parserUri(String requestData) {
int index1 = requestData.indexOf(' ');
if(index1 != -1) {
int index2 = requestData.indexOf(' ', index1 + 1);
if(index2 > index1) {
uri = requestData.substring(index1 + 1, index2);
url = uri;
if(uri!=null){
String tempStr[] = uri.split("\\?");
//提取到地址
if(tempStr.length>0){
uri = tempStr[0];
}
//含有参数,开始提取
if(tempStr.length>1){
parserParameters(tempStr[1]);
}
}
}
}
//请求头处理
parserHeader(requestData);
}
/**
* 解析地址栏参数
* @param paramsStr
*/
private void parserParameters(String paramsStr){
if(paramsStr==null){
return;
}
//多个参数使用&作为分割
String params[] = paramsStr.split("&");
if(params == null || params.length<=0){
return;
}
for(String param : params){
String paramDetail[] = param.split("=");
if(paramDetail.length<=1){
//不存在,赋值null
requestParameters.put(param,null);
}else{
requestParameters.put(paramDetail[0],paramDetail[1]);
}
}
}
/**
* 解析请求头
* @param requestData
* @return
*/
private void parserHeader(String requestData){
try {
StringReader reader = new StringReader(requestData);
BufferedReader bufferedReader = new BufferedReader(reader);
String str;
//标志第一行不需要读取
boolean doNext = false;
boolean isAttributes = false;
StringBuffer arrtibutesStrBuffer = new StringBuffer();
while((str=bufferedReader.readLine())!=null){
if(doNext && !isAttributes){
String tempStrs[] = str.split(": ");
String name = tempStrs[0];
if(!name.equals("")) { //这部分是请求头
String value = str.replaceFirst(tempStrs[0] + ": ", "");
if(name.equals("Cookie")) {
//Cookie解析
parserCookie(value);
}else{
header.put(name, value);
}
}else{ //下面这部分是请求体参数
isAttributes = true;
}
}else if(isAttributes){
arrtibutesStrBuffer.append(str);
}
doNext = true;
}
//开始解析请求体参数
parserAttributes(arrtibutesStrBuffer.toString());
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 解析cookie
* @param cookieStr
*/
private void parserCookie(String cookieStr){
if(cookieStr==null){
return;
}
String cookies[] = cookieStr.split("; ");
for(String str : cookies){
if(str == null){
continue;
}
String cookieArr[] = str.split("=");
if(cookieArr.length>1) {
cookie.put(cookieArr[0], cookieArr[1]);
}
}
}
/**
* 解析请求头参数
* @param requestData
*/
private void parserAttributes(String requestData){
if(requestData==null || requestData.equals("")){
return;
}
try {
StringReader reader = new StringReader(requestData);
BufferedReader bufferedReader = new BufferedReader(reader);
String str;
while ((str = bufferedReader.readLine()) != null) {
if(str!=null){
String[] split = str.split("&");
if(str!=null){
for(String tempStr : split){
if(tempStr!=null) {
String tempSplit[] = tempStr.split("=");
if(tempSplit.length>0) {
requestAttributes.put(tempSplit[0], tempStr.replaceFirst(tempSplit[0] + "=", ""));
}
}
}
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
6、拿到了请求参数后,按正常来说,后端的运用服务就可以根据这些参数来做很多不同的处理,但是目前这个web容器暂时还没实现对动态程序的支持,所以这里就先对静态资源进行读取,然后返回给请求的客户端即可。
这里可以分为几个简单的步骤:
读取服务器文件,判断文件是否存在,如果存在则读取文件流内容通过输出流返回给客户端;如果不存在,那么直接返回给客户端一些提示信息便是。
/**
* 发送一个静态资源给客户端,若本地服务器有对应的文件则返回,否则返回404页面
*/
public void output() {
byte[] buffer = new byte[BUFFER_SIZE];
int ch;
FileInputStream fis = null;
try {
//读取磁盘文件
File file = new File(GlobalParams.WEB_ROOT, request.getUri());
if(file.exists()) { //如果文件存在,则返回文件
fis = new FileInputStream(file);
ch = fis.read(buffer);
while(ch != -1) {
output.write(buffer, 0, ch);
ch = fis.read(buffer, 0, BUFFER_SIZE);
}
} else { //文件不存在,则返回404 提示
String errorMessage = "HTTP/1.1 404 File Not Found \r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 24\r\n" +
"\r\n" +
"<h1>File Not Found!</h1>";
output.write(errorMessage.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
7、这样就完成了一个简单请求响应的web容器的基本实现。
不过有一点还需要考虑:对于某一个web容器,可能每时每刻都会有新的请求过来,如果所有的请求都放在主线程处理,那么势必会造成很多阻塞。所以这里需要用多线程处理这个问题。
这里把请求参数的解析和响应给客户端内容的步骤都放到单独的线程里面处理。
/**
* 处理请求的线程,每个请求都应由单独的线程来处理
* Created by Nemo on 2017/12/14.
*/
public class DealRequestThread implements Runnable {
private Socket socket;
public DealRequestThread(Socket socket) {
this.socket = socket;
}
public void run() {
try {
Thread current = Thread.currentThread();
System.out.println("当前处理线程:" + current.getName());
//本次请求的输入流
InputStream input = socket.getInputStream();
//对于本次请求的输出流
OutputStream output = socket.getOutputStream();
//接收请求
Request request = new Request(input);
request.parser();
//处理请求并返回结果
Response response = new Response(output);
response.setRequest(request);
response.output();
socket.close();
} catch (IOException e) {
//错误只做异常堆栈输出,不向外抛出
e.printStackTrace();
}
}
}
然后在主线程,每次有新的请求进来以后,直接把处理放到单独的线程里机型,不对主线程造成堵塞:
try {
//创建socket,等待请求
Socket socket = serverSocket.accept();
//如果有请求,则直接放到线程中直接处理即可
Thread thread = new Thread(new DealRequestThread(socket));
thread.start();
} catch (IOException e) {
e.printStackTrace();
//异常不终止所有进程,应立即继续下一个请求
continue;
}
这样一个简单的web容器就实现了。
后续如果有更多的时间,届时再添加动态内容的支持。