利用Java手写简单的httpserver

前言:

在看完尚学堂JAVA300中讲解如何实现一个最简单的httpserver部分的视频之后,

一、前置知识

1.HTTP协议

  当前互联网网页访问主要采用了B/S的模式,既一个浏览器,一个服务器,浏览器向服务器请求资源,服务器回应请求,浏览器再将接收到的回应解析出来展现给用户。这一问一答的过程可以抽象成浏览器向服务器发送一个Request然后服务器返回一个Response的过程
  其中Request和Reponse在HTTP中有有具体的格式要求

  • 一个Request的例子
Method Path-to-resource Http/Version-number
User-agent 浏览器的类型Accept-charset 用户首选的编码字符集……
Accept 浏览器接受的MIME类型
Accept language 用户选择的接受语言
Accept-charset 用户首选的编码字符集
空行
Option Request Body

如表格所示,第一行首先是请求方式,后跟请求资源路径(url)如果请求方式是GET,则请求参数跟在请求路径里,以?分开,然后一个空格,后跟HTTP版本。后几行为固定格式内容。如果请求方式为POST,则隔一个空行后,跟的请求体的内容,里面有请求参数。

  • 一个Response内容
Http/Version-number Statuscode message
Server 服务器的类型信息
Content-type 响应的MIME类型信息
Content-length 被包含在相应类型中的字符数量
空行
Option Response Body

和Request类似,同样包含响应头和响应体两部分。第一行的Statuscode标识了状态参数,404表示请求资源没有找到,500表示服务器错误,200表示成功。响应体里面包含的是响应内容

该部分具体可以参考博文:

2.JAVA网络编程

在Java中提供了两种网络传输方式的实现,面向数据的UDP传输方式和面向连接的TCP传输方式,这里选用TCP方式。
在TCP方式中,服务端的编写主要依靠类Socket和类ServerSocket,通过这两个类可以建立一个TCP连接。

具体方法是:

  1. 首先新建一个ServerSocket对象server,指明端口号信息
  2. 然后使用server.accept()函数监听端口,监听到连接以后返回一个Socket对象
  3. 通过这个Socket对象,以及里面的输入流和输出流,我们就可以获得传过来的信息以及返回信息

二、具体实现

1.服务器类

首先我们需要建立一个服务器类,负责不断监听端口,获得Socket,然后再利用获得Socket新建一个分发器(Dispatcher),该分发器支持多线程,所以一个分发器专门负责处理一个连接,而服务器只负责不断接收连接,建立分发器。
具体代码如下:

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
package top.dlkkill.httpserver;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;



public class Server {

private boolean flag=false;
private ServerSocket server;

public static void main(String[] args) throws IOException {
Server myserver=new Server(8888);
myserver.start();
}

public Server(int port) {
try {
server=new ServerSocket(port);
} catch (IOException e) {
this.stop();
}
}

public void start() throws IOException {
this.flag=true;
this.revice(server);
}

public void revice(ServerSocket server) throws IOException {
while(flag) {
Socket client=server.accept();
new Thread(new Dispatcher(client)).start();
}
}

public void stop() {
flag=false;

}
}

2.封装Request和Response

为了方便解析Request和返回Response,我们需要抽象出两个对象(Request类和Response对象)。

首先封装Request

  Request对象的作用是解析请求信息,将请求方式,请求资源路径,请求参数解析分离出来,构建一个Request对象我们需要传入一个参数——>输入流。这样我们就可以从输入流中读入请求信息然后开始解析。

1
2


然后我们进行封装Response.
  Response主要分为两部分(响应头和响应体)
  构建Response需要传入通过Socket获得的输出流,利用这个输出流我们才可以写回信息
响应头格式较为固定,所有我们在Response中应该有一个私有方法根据响应码自动构建响应头信息。
  然后我们还需要一个共有方法void println(String msg),其他类利用该方法写入对应的响应体部分。
最后需要有一个send()方法,自动整合响应体和响应体内容,并将所有内容发送出去。

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
package top.dlkkill.httpserver;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Date;

public class Response {

public static final String CRLF="\r\n";
public static final String BANK=" ";

private StringBuilder headerinfo;
private StringBuilder content;
private BufferedWriter wr;
private int len;

public Response() {
// TODO Auto-generated constructor stub
headerinfo=new StringBuilder();
content=new StringBuilder();
if(content==null)
System.out.println("error");
len=0;
}

public Response(OutputStream os){
this();
wr=new BufferedWriter(new OutputStreamWriter(os));
}

public void createHeaderinfo(int code) {
System.out.println("code is "+code);
headerinfo.append("HTTP/1.1").append(BANK);
switch (code) {
case 200:
headerinfo.append(code).append(BANK).append("OK");
break;
case 404:
headerinfo.append(code).append(BANK).append("404 not found");
break;
case 500:
headerinfo.append(code).append(BANK).append("error");
break;
default:
headerinfo.append(code).append(BANK).append("error");
break;
}
headerinfo.append(CRLF);
headerinfo.append("Server:dlkkill server/0.1").append(CRLF);
headerinfo.append("Date:").append(new Date()).append(CRLF);
headerinfo.append("Content-type:text/html;charset=GBK").append(CRLF);
//正文长度,字节长度
headerinfo.append("Content-Length:").append(len).append(CRLF);
//空行分隔符
headerinfo.append(CRLF);
//System.out.println(headerinfo.toString());
}

public Response println(String msg) {
//System.out.println(msg);
if(content==null)
System.out.println(msg);
content.append(msg);
len+=msg.getBytes().length;
return this;
}

public void pushToClient(int code) throws IOException {
if(wr==null) {
code=500;
}
createHeaderinfo(code);
wr.write(headerinfo.toString());
wr.write(content.toString());
wr.flush();
}
}

3.创建分发器

  我们需要有一个类专门一对一处理一个连接,并且该类要支持多线程。所以我们抽象出来一个分发器类。该类负责专门一对一处理一个连接
  该类拥有一个私有属性Socket client。利用该属性,该类可以创建一个Request和一个Response,然后该类再根据请求的url,利用Webapp类(该类用于生成处理不同请求的不同的类)获得对应的类,启动该类进行处理。
最后该类再调用Response提供的pushToClient方法将所有信息推送给浏览器,然后关闭连接。

具体代码如下:

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
package top.dlkkill.httpserver;

import java.io.IOException;
import java.net.Socket;

public class Dispatcher implements Runnable {
private Socket client;
private Request req;
private Response rep;
private int code=200;

public Dispatcher(Socket client) {
this.client=client;
try {
req=new Request(client.getInputStream());
rep=new Response(client.getOutputStream());
} catch (IOException e) {
code=500;
}
}

@Override
public void run() {
System.out.println(req.getUrl()+" ***");
Servlet servlet=Webapp.getServlet(req.getUrl());
if(servlet!=null)
servlet.service(req, rep);
else
code=404;
try {
rep.pushToClient(code);
} catch (IOException e) {
code=500;
}
try {
rep.pushToClient(code);
} catch (IOException e) {

}
CloseUtil.closeAll(client);
}
}

4.抽象处理类Servlet

首先我们将该处理类抽象成一个abstract Servlet类,该类负责根据不同的请求进行处理
该抽象类提供多个抽象方法doGet、doPost方法等分别处理不同的请求,传入参数为(Request,Response)这两个参数,在该方法内进行处理。
提供一个service方法根据不同的请求调用不同的方法
具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package top.dlkkill.httpserver;

import java.net.Socket;

public abstract class Servlet {


public Servlet() {

}

public void service(Request req,Response rep) {
if(req.getMethod().equalsIgnoreCase("get")) {
this.doGet(req, rep);
}else {
this.doPost(req, rep);
}
}

public abstract void doGet(Request req,Response rep);

public abstract void doPost(Request req,Response rep);
}

一个实例:

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
package top.dlkkill.httpserver;

public class loginServlet extends Servlet {


@Override
public void doGet(Request req, Response rep) {
rep.println("<head>" +
" <title>test</title>" +
"</head>" +
"<body>" +
"<p>hellow</p>"+
"<form action=\"http://localhost:8888/index\" method=\"POST\">" +
"name: <input type=\"text\" name=\"name\">" +
"password: <input type=\"password\" name=\"pwd\">" +
"<input type=\"submit\" value=\"submit\">" +
"</form>" +
"</body>");
}

@Override
public void doPost(Request req, Response rep) {
this.doGet(req, rep);
}

}

5.处理类生成工厂

为了编程的灵活性,我们将该httpserver写出可以根据一个xml配置文件知道有多少种分别处理什么url请求的类,该xml就负责记录这种映射关系
首先需要一个ServletContext类,该类有两个属性private Map<String,String> servlet和private Map<String,String> map,分别用来记录名称到类存储地址之间的映射和url到名称之间的映射

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
package top.dlkkill.httpserver;

import java.util.HashMap;
import java.util.Map;

public class ServletContext {

//名称到类存储地址之间的映射
private Map<String,String> servlet;
//url到名称之间的映射
private Map<String,String> map;


public void setServlet(Map<String, String> servlet) {
this.servlet = servlet;
}

public void setMap(Map<String, String> map) {
this.map = map;
}
public ServletContext() {
servlet=new HashMap<String, String>();
map=new HashMap<String, String>();
}

public Map<String, String> getServlet() {
return servlet;
}

public Map<String, String> getMap() {
return map;
}

}

然后需要一个Webapp类
该类负责读入xml文件并且进行解析,根据xml文件配置的内容,为分发器生成不同的servlet处理类。
生成不同的类利用的Java的类加载机制,可以在代码中获取class信息然后new一个类出来
解析xml文件我们使用的是SAXParser解析器,为了利用该解析器,我们还需要实现一个继承于DefaultHandler的类

实现代码:

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
package top.dlkkill.httpserver;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.SAXException;

public class Webapp {
private static ServletContext servletcontext;
static{
servletcontext=new ServletContext();
Map<String,String> servlet=servletcontext.getServlet();
Map<String,String> map=servletcontext.getMap();
// servlet.put("index", "top.dlkkill.httpserver.indexServlet");
// servlet.put("login", "top.dlkkill.httpserver.loginServlet");
// map.put("/login", "login");
// map.put("/index", "index");
SAXParserFactory parserfactor=SAXParserFactory.newInstance();
WebHandler hd=new WebHandler();
SAXParser parser;
try {
parser=parserfactor.newSAXParser();
if(null==Thread.currentThread().getContextClassLoader().getResourceAsStream("top/dlkkill/httpserver/web.xml"))
System.out.println("error");
parser.parse(
Thread.currentThread().getContextClassLoader()
.getResourceAsStream("top/dlkkill/httpserver/web.xml"),
hd);
List<Entity> entityList=hd.getEntityList();
List<Mapping> mappingList=hd.getMappingList();
for (Mapping mapping : mappingList) {
String name=mapping.getName();
List<String> urlList=mapping.getUrl();
for (String url:urlList) {
map.put(url, name);
}
}
for (Entity entity:entityList) {
String servletname=entity.getName();
String clz=entity.getClz();
servlet.put(servletname, clz);
}
} catch (ParserConfigurationException | SAXException |IOException e) {

}


}
public static Servlet getServlet(String url) {
Map<String,String> servlet=servletcontext.getServlet();
Map<String,String> map=servletcontext.getMap();
String className=servlet.get(map.get(url));
Servlet temp=null;
Class<?> clz=null;
try {
System.out.println("classname:"+className);
if(className!=null)
clz=Class.forName(className);
} catch (ClassNotFoundException e) {
return null;
}
try {
if(clz!=null)
temp=(Servlet)clz.newInstance();
} catch (InstantiationException e) {
return null;
} catch (IllegalAccessException e) {
return null;
}
return temp;
}
}
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
package top.dlkkill.httpserver;

import java.util.ArrayList;
import java.util.List;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class WebHandler extends DefaultHandler {

private List<Entity> entityList;


private List<Mapping> mappingList;

private Entity entity;
private Mapping mapping;

private String tag;
private boolean isMap;

public WebHandler() {

}
public List<Entity> getEntityList() {
return entityList;
}

public List<Mapping> getMappingList() {
return mappingList;
}
@Override
public void startDocument() throws SAXException {
entityList=new ArrayList<Entity>();
mappingList=new ArrayList<Mapping>();
}

@Override
public void endDocument() throws SAXException {
// for (Mapping mapping : mappingList) {
// if(mapping==null)
// continue;
// String name;
// if(mapping.getName()!=null)
// name=mapping.getName();
// else
// name="null";
// List<String> urlList=mapping.getUrl();
// for (String url:urlList) {
// System.out.println(name+"---->"+url);
// }
// }
// for (Entity entity:entityList) {
// String servletname=entity.getName();
// String clz=entity.getClz();
// System.out.println(servletname+"---->"+clz);
// }
}

@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
//System.out.println("开始处理"+"--->"+qName);
if(null!=qName) {
if(qName.equals("servlet")) {
isMap=false;
entity=new Entity();
}else if(qName.equals("servlet-mapping")){
isMap=true;
mapping=new Mapping();
}
}
tag=qName;
}

@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
//System.out.println("结束处理"+"--->"+qName);
if(null!=qName) {
if(qName.equals("servlet")) {
entityList.add(entity);
}else if(qName.equals("servlet-mapping")){
mappingList.add(mapping);
}
}
tag=null;
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String str=new String(ch, start, length);
//System.out.println("处理中"+"--->"+str);
if(tag!=null&&str!=null&&!str.trim().equals("")) {
if(!isMap) {
if(tag.equals("servlet-name"))
entity.setName(str);
else if(tag.equals("servlet-class"))
entity.setClz(str);
}else {
if(tag.equals("servlet-name"))
mapping.setName(str);
else if(tag.equals("url"))
mapping.getUrl().add(str);
}
}
}

}

6.一个工具类

该类负责关闭连接,连接关闭了那与该连接有关的流也就关闭了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package top.dlkkill.httpserver;

import java.io.Closeable;

public class CloseUtil {
public static void closeAll(Closeable ...io) {
for (Closeable closeable : io) {
try {
if(closeable!=null)
closeable.close();
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
}