文章目录
Tomcat应用服务器的简单实现
简单来说就是实现一个基于TCP的HTTP服务器,可以完成静态资源和动态资源的访问。
1 Tomcat的职责:
1)正确监听TCP端口并处理客户端发起的TCP连接请求。
2)在已经建立TCP连接的基础上,可以正确的从TCP连接中读取数据。
3)可以按照HTTP协议,解析数据(HTTP请求),对方发过来的不一定是HTTP请求。
4)把请求封装成HttpServletRequest请求对象。
5)根据请求中的URL信息,决定交给哪个 Web应用处理。
6)根据请求中的URL信息,决定交给哪个 Servlet处理。
7)得到一个含有内容的HttpServletResponse响应对象。
8)把HttpServletResponse响应对象,构建成HTTP响应,并发送。
Tomcat负责建立TCP连接,根据Http协议解析数据,封装请求,交给内部的Servlet来处理,得到HttpServletResponse响应,再构建成HTTP响应,通过socket发送出去。
2 实际的Servlet代码有三部分构成:
- Servlet标准(一组接口)
- Tomcat实现Servlet
- 自己的Servlet应用
3 类设计:
- 仿照Tomcat 写一个Servlet , HttpServletRequest,HttpServletResponse接口和
HttpServlet(抽象类) - Server (程序入口,用线程池的方式处理多个客户端的连接,负责前台事务)
- TransactionTask(线程池任务:读取请求,发送响应)
- HttpServletRequestImpl(封装请求内容对象)
- HttpServletResponseImp(封装响应内容对象)
- StaticResourceServlet(用来处理静态资源)
- NotFoundServlet(用来处理404无法找到资源)
- 自定义一些继承HttpServlet抽象类的Servlet,来根据请求做出响应。
- 仿照web.xml,用WebXML类实现动态资源路径的匹配,根据路径找到对应的自定义Servlet.
TransactionTask具体的任务主要分为三大块:
- 读取请求
- 发送响应
- 关闭连接
整体流程:
(1)用HttpServletRequest中的方法将Socket输入字节流中的请求进行解析,并封装成一个HttpServletRequest对象。
(2)在HttpServletResponse类中建立一个空的response对象。
(3)根据请求方法及路径后缀确定要寻找静态资源还是动态资源。
(4)无论是静态资源还是动态资源或者是查找不到(404),都能指向一个对应的servlet对象。
(5)调用Servlet中的doGet/doPost封装该响应对象,然后向HTTP写入响应,先将响应头部分写入,再写入响应体部分。
Ⅰ Servlet模块
编写Servlet接口:
public interface Servlet {
void init();
void service(HttpServletRequest req, HttpServletResponse resp) throws IOException;
void destroy();
}
编写实现了Servlet接口的抽象类:
public abstract class HttpServlet implements Servlet {
@Override
public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
switch (req.getMethod()) {
case "GET":
doGet(req, resp);
break;
case "POST":
doPost(req, resp);
break;
default:
resp.setStatus(400);
}
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException{
// Method Not Allow
resp.setStatus(405);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// Method Not Allow
resp.setStatus(405);
}
@Override
public void init() {
}
@Override
public void destroy() {
}
}
编写HttpServletRequest请求接口:
public interface HttpServletRequest {
String getMethod();
String getPath();
String getParameter(String name);
String getHeader(String name);
}
编写HttpServletResponse响应接口:
public interface HttpServletResponse {
void setStatus(int status);
void setHeader(String name, String value);
void setContentType(String contentType);
PrintWriter getWriter() throws IOException;
OutputStream getOutputStream() throws IOException;
}
Ⅱ 编写tomcat模块
(1) Server
TCP 服务器
- 创建 Socket
- 绑定本地 ip + port
- 对 socket 进行 Listen
- 通过调用 accept 等待三次握手成功的客户端
TCP建立连接后, 接下来所有和该客户端通信的过程,全部封装成 TransactionTask 任务
public class Server {
private static final int PORT = 8080;
// 主线程只处理前台的事务
public static void main(String[] args) throws IOException {
/**
* 通过使用线程池的方式,支持客户端并发处理
* TODO: NIO 的方式支持并发
*/
ExecutorService pool = Executors.newFixedThreadPool(8);
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
Socket socket = serverSocket.accept();
// 应用层取到一个 ESTABLISHED 状态连接对应的 socket
Runnable task = new TransactionTask(socket);
// 交给线程池去处理
pool.execute(task);
}
}
}
}
(2) TransactionTask.Java
- 解析请求并封装;
- 初始化响应;
- 判断是否是静态文件,如果是静态文件,就将servlet对象指向staticResourceServlet对象
- 如果不是静态文件,则调用
WebXML.map(request.getPath());
拿到对应的servlet;
// <servlet>
servlet.put("Hello", new HelloServlet());
servlet.put("Login", new LoginServlet());
// <servlet-mapping>
servletMapping.put("/login", "Login");
servletMapping.put("/hello", "Hello");
- 如果servlet==null就让
servlet = notFoundServlet;
(3)开始解析请求:
//请求行
GET /homebd/egf.gif?authorization=bce&name=glp HTTP/1.1
//请求头
Host: static.home.baidu.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
HttpServletRequestImpl request =
HttpServletRequestImpl.readAndParse(socket.getInputStream());
通过socket.getInputStream()
获得TCP输入流,使用 Scanner scanner = new Scanner(inputStream, "UTF-8");
一行一行的读取输入流(HTTP 协议中大多使用 CRLF 进行分割,需要一个方便读取一行的类进行处理 —— Scanner). 每读取一行就调用一下 String line = scanner.nextLine();
1)解析请求行
String[] group = line.split(" ");
request.method = group[0]; //方法
String url = group[1]; //url部分
请求行以空格分割后,可以轻易拿到请求方法,然后再解析 url 部分,HTTP/1.1 版本协议我们省略,分出 url 的 path 和 parameter 部分
注:
解析请求时我们不考虑完整的 URL(http://www.baidu.com/index.html), 只考虑相对的 URL(/index.html)
划分url的路径及参数请求部分:
///homebd/egf.gif?authorization=bce&name=glp
String[] group = url.split("\\?");
request.path = URLDecoder.decode(group[0], "UTF-8");
//authorization=bce&name=glp
//当有多个参数时
String[] fragments = group[1].split("&");
2) 解析请求头
//请求头
Host: static.home.baidu.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
使用 readAndParseRequestHeaders(request, scanner);
解析请求头;
以key-Value的形式划分请求头,使用Scanner一行一行的读,当读到空行时,请求头读取结束:
line = scanner.nextLine();
// Key: Value
String[] group = line.split(":");
到此为止:请求封装完成。
(4) 构建一个空的 HttpServletResponseImpl 对象(进行初始化)
调用:HttpServletResponseImpl.build(socket.getOutputStream());
public static HttpServletResponseImpl build(OutputStream outputStream) throws UnsupportedEncodingException {
HttpServletResponseImpl response = new HttpServletResponseImpl();
// 基本的初始化功能
response.outputStream = outputStream;
return response;
}
(5) 根据请求路径,寻找对应的servlet
对应的servlet即为继承了HttpServlet的类,public class StaticResourceServlet extends HttpServlet
。 如果对应路径为静态资源,则让servlet指向StaticResourceServlet(解析静态资源是tomcat的工作),如果对应路径为我们编写的servlet类,则让servlet指向对应的自定义servlet类,如果路径不匹配则让servlet指向notFoundServlet;
采用url优先匹配静态方法,根据路径和方法(GET/POST),确定Servlet对象:
(6)调用servlet中的service方法
会根据不同的 method,调用 doGet或doPost, response 被填充了
servlet.service(request, response);
注意:
- 这里的request是HttpServletRequest的实现类对象
,HttpServletRequestImpl request =new HttpServletRequestImpl();
- 这里的response是HttpServletResponse的实现类对象
,HttpServletResponseImpl response =new HttpServletResponseImpl();
(7)封装HTTP响应并发送
要先写入相应行,再写入响应头,最后写入响应体
response.send();
Ⅲ 自定义servlet
public class LoginServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
// TODO: 验证用户名密码是否正确
// TODO: 设置 Session 信息
resp.setContentType("text/plain; charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("登录成功");
}
}
Ⅳ 流程及注意事项
1 大体流程:
(1)根据读到的数据,构建请求对象;
(2)构建一个空的响应对象;
(3)选择合适的Servlet对象;
(4)调用service()
方法,会填充响应对象的状态、头信息、正文;
(5)拿到完整的响应对象;
(6)根据完整的响应对象,按照HTTP协议标准,调用send()发送数据到socket()中;
(7)关闭socket();
2 具体需要实现:
1.TCP Server(程序入口,用线程池的方式处理多个客户端的连接,负责前台事务,具体的任务交给TransactionTask去做)
2.接收HTTP请求,解析HTTP请求,并将其封装为HttpServletRequest对象。
3.实例化HTTPServletResponse对象
4.区分静态资源,动态资源,资源访问不到的情况
(1)静态资源:读取静态资源,并返回一个Servlet对象。
(2)动态资源:把request/response交给Servlet中的doGet/doPost进行处理,并返回一个Servlet对象。
(3)404无法找到资源:返回一个NotFoundServlet对象。
5. 把HTTPServletResponse对象转换为HTTP响应并发送。
注意:
(1)利用service方法执行对应的doGet或doPost方法(这里用到动态绑定),完成对response的封装。其中response响应体的内容都是写在bodyOutputStream(一个输出到内存数组的输出流)中, 在向HTTP写入响应时,需要先将响应内容刷新到bodyOutputStream中,然后才能在bodyOutputStream中拿到内容,写入到HTTP响应中。
因为标准的HTTP响应,应该先写入相应行,再写入响应头,最后才是正文,所以
在写入响应时,不能直接将响应写入到socket中,因为响应必须先写响应行再写响应头,最后写正文。所以我们需要先将响应体部分写入到bodyOutputStream中,经过组合后,再将响应写入到socket中。
3 接口动态绑定及刷新缓冲区
HttpServletResponse的实现类,HttpServletResponseImpl implements HttpServletResponse:
private OutputStream outputStream;
private ByteArrayOutputStream bodyOutputStream = new ByteArrayOutputStream(8192);
private PrintWriter bodyPrintWriter;
public HttpServletResponseImpl() throws UnsupportedEncodingException {
bodyPrintWriter = new PrintWriter(new OutputStreamWriter(bodyOutputStream, "UTF-8"));
}
@Override
public PrintWriter getWriter() throws IOException {
return bodyPrintWriter;
}
@Override
public OutputStream getOutputStream() {
return bodyOutputStream;
}
因为调用service时 servlet.service(request, response);
,传入的是HttpServletResponse的实现类,以及HttpServletRequest的实现类。所以在HttpServlet实现类中(自定义的servlet,静态servlet,notfundServlet),调用doGet(HttpServletRequest req, HttpServletResponse resp)
时,HttpServletRequest req,以及HttpServletResponse resp都发生了动态绑定:
即自定义servlet中调用的PrintWriter writer = resp.getWriter();
方法实际是HttpServletResponseImpl 中的方法。即响应内容都输出到了bodyOutputStream 中;
public void send() throws IOException {
// 1. 强制把所有 body 的内容都刷新到最终的目的 buffer(缓冲区) 中
bodyPrintWriter.flush();
// 3. 最后写响应体
sendResponseBody(outputStream);
}
private void sendResponseBody(OutputStream outputStream) throws IOException {
outputStream.write(bodyOutputStream.toByteArray());
}
注:
什么是缓冲区:
缓冲区(buffer),它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来 缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的。
为什么需要刷新缓冲区:
举实例来说,比如设定缓冲长度是50字符。也就是说,每次cout输送50字符就强制输出一次。如果最后一次是个hello world,那么缓冲没有填满,就不会立即打印,而等待下一次缓冲满一并输出。 如果在此之前程序结束了,那么这行输出很可能就被吞掉了。如果输出hello world之后发送一个endl到stdout,那么此时这句hello world就被强行冲出缓冲区直接进行打印,也就没有输出被吞没的问题了。