一:理论铺垫
tomcat是java的一个中间件,浏览器发出HTTP请求后经过tomcat中间件,转发到目的服务器,目的服务器返回响应消息,通过tomcat返回给浏览器。tomcat的使用很简单,但是作为合格的程序员,光会用可不行,接下来就通过手写一个tomcat彻底搞懂tomcat。
在手写Tomcat之前,首先重温一下http和servlet,http协议分为请求协议和响应协议:
GET /user HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
第一部分:请求行:请求类型,资源路径以及http版本(上述第一行)
第二部分:请求头:紧接在请求行之后,用于说明服务器需要使用的附加信息(第二到第八行)
HTTP/1.1 200
Content-Type:text/html
OK
第一部分:状态行,http版本,状态码,状态信息(第一行)
第二部分:响应报文头部,说明服务器需要用到的附加信息(第二行)
当收到客户端请求信息时,调用service方法处理客户端请求,service会根据不同的请求类型,调用不同的doXXX()方法
二:理解客户端和服务器的通信
客户端和服务器的通信,说到底就是两个数据的传输,客户端发送inputStream给服务器,服务器回复outputStream给客户端。
public class TomcatServerV1 {
public static void main(String[] args)throws IOException{
//开启ServerSocket服务,设置端口号为8080
ServerSocket serverSocket=new ServerSocket(8080);
System.out.println("======服务启动成功========");
//当服务没有关闭时
while(!serverSocket.isClosed()){
//使用socket进行通信
Socket socket=serverSocket.accept();
//收到客户端发出的inputstream
InputStream inputStream=socket.getInputStream();
System.out.println("执行客户请求:"+Thread.currentThread());
System.out.println("收到客户请求");
//读取inputstream的内容
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
String msg=null;
while((msg=reader.readLine())!=null){
if(msg.length()==0) break;
System.out.println(msg);
}
//返回outputstream,主体内容是OK
String resp="OK";
OutputStream outputStream=socket.getOutputStream();
System.out.println(resp);
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
socket.close();
}
}
}
上面的代码把客户端和服务器的通信简单了模拟了一遍,我们可以执行一遍,运行项目后,在浏览器中输入http://localhost:8080/hello,观察控制台信息,收到了客户的请求,这里的信息正是http请求协议的内容。
但是我们会发现客户端出现报错,为什么我们返回了一个OK过去,但是客户端没有显示呢?原因就在于客户端只能识别符合HTTP响应协议的数据,我们必须把outputstream的数据让客户端能看懂,其实也很简单,只需要把返回的数据加上HTTP响应协议的报文头部就行,即我们上面复习的HTTP协议,新建一个response类,封装请求信息:
public class Response {
public OutputStream outputStream;
public static final String responsebody="HTTP/1.1 200+\r\n"+"Content-Type:text/html+\r\n"
+"\r\n";
public Response(OutputStream outputStream){
this.outputStream=outputStream;
}
}
Response类定义了responsebody,包括了http响应协议的头部信息,修改前面的代码
将第22行的
String resp="OK";
修改为
String resp= Response.responsebody+"OK";
再次执行代码,客户端出现了我们自定义的“OK”
上面的代码虽然做到了服务器和客户端的通信,但是有个弊端,服务器一次只能连接一个客户端。tomcat在解决这个问题时使用了BIO模型,简单来讲就是每个连接一个线程,下面就来实现BIO:
public class TomcatServerV2 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(8080);
System.out.println("====服务启动====");
while(!serverSocket.isClosed()){
Socket socket=serverSocket.accept();
//对于每个连接,都开启一个线程
RequestHandler requestHandler=new RequestHandler(socket);
new Thread(requestHandler).start();
}
}
}
public class RequestHandler implements Runnable{
public Socket socket;
public RequestHandler(Socket socket)
{
this.socket=socket;
}
//继承Runnable接口,实现run方法
public void run() {
InputStream inputStream=null;
try{
inputStream=socket.getInputStream();
System.out.println("执行客户请求"+Thread.currentThread());
System.out.println("====收到客户端请求====");
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
String msg=null;
while((msg=reader.readLine())!=null){
if(msg.length()==0){
break;
}
System.out.println(msg);
}
String resp= Response.responsebody + "OK";
OutputStream outputStream=socket.getOutputStream();
System.out.println(resp);
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
上述的代码和第一版本没有太多差别,但是通过几句代码的添加实现了每个连接给一个线程。
三:手写Tomcat
在pom.xml中添加两个jar包的依赖:
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.1</version>
</dependency>
我们之前对Response响应进行了封装,同样对于Request也需要封装:
public class Request {
//获取uri,如 /user
private String uri;
//获取请求方法,这里只写get和post GET or POST
private String method;
public Request(InputStream inputStream){
try {
//获取inputStream
BufferedReader read=new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
//取HTTP请求响应的第一行,GET /user HTTP/1.1,按空格隔开
String[] data=read.readLine().split(" ");
//取uri和method
this.uri=data[1];
this.method=data[0];
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//省略uri和method的getter和setter方法
}
tomcat使用servlet进行请求处理,我们平常使用servlet时,会继承HttpServlet这个抽象类,然后重写里面的doGet,doPost等方法,最源头还会继承一个Servlet接口,该接口主要提供init,service等方法,我们可以看Servlet接口的代码
我们也用同样的方式来实现,首先建一个MyServlet接口,然后创建一个抽象类MyHttpServlet继承接口,最后建一个UserServlet实现具体的doGet,doPost等方法
public interface MyServlet {
void init() throws Exception;
void service(Request request, Response response) throws Exception;
void destory();
}
public abstract class MyHttpServlet implements MyServlet {
//如果有请求过来,就会调用这个方法,然后再根据请求类型来调用不同的doXXX()方法
public void service(Request request, Response response) throws Exception {
if("get".equalsIgnoreCase(request.getMethod())){
this.doGet(request,response);
}else {
this.doPost(request,response);
}
}
public abstract void doGet(Request request,Response response);
public abstract void doPost(Request request,Response response);
}
public class UserServlet extends MyHttpServlet {
@Override
public void doGet(Request request, Response response) {
this.doPost(request,response);
}
@Override
public void doPost(Request request, Response response) {
try {
//省略业务调用的代码,tomcat会根据request对象里面的inputStream拿到对应的参数进行业务调用
//模拟业务层调用后的返回
OutputStream outputStream=response.outputStream;
String result=Response.responsebody+"user handle successful";
outputStream.write(result.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void init() throws Exception { }
public void destory() { }
}
UserServlet中原本应该还需要写对业务代码的调用,但是这次项目因为模拟简单的Tomcat实现就不写业务调用了。
Servlet写完后还需要做很重要的一步,解析web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.sdxb.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/user</url-pattern>
</servlet-mapping>
</web-app>
这是我为这个项目写的web.xml,包含了两个映射,第一个是ServletName和Servlet实现类的映射,第二个是ServletName和uri的映射,意思就是如果检测到uri是/user,就能映射给UserServlet类。解析web.xml用的是最初导入的dom4j,在socket包下建一个MyTomcat类:
public class MyTomcat {
//自定义端口号为1314
public static final int port =1314;
//定义web.xml中的两个映射
public static final HashMap<String, MyHttpServlet> servletMapping=new HashMap<String, MyHttpServlet>();
public static final HashMap<String,String> urlmapping=new HashMap<String, String>();
public static void main(String[] args){
MyTomcat myTomcat=new MyTomcat();
myTomcat.init();
myTomcat.run();
}
//初始化,加载web.xml里面配置的servlet信息
private void init(){
try {
//获取web.xml目录地址
String path=MyTomcat.class.getResource("/").getPath();
//实例化SAXReader对象
SAXReader reader=new SAXReader();
//读取web.xml文件
Document document=reader.read(new File(path+"web.xml"));
//获取根标签(servlet和servlet-mapping),放在一个List中
Element rootelement=document.getRootElement();
List<Element> elements=rootelement.elements();
//循环将映射写进map映射里
for(Element element:elements){
if ("servlet".equalsIgnoreCase(element.getName())){
Element servletname=element.element("servlet-name");
Element servletclass=element.element("servlet-class");
System.out.println(servletname.getText()+"==>"+servletclass.getText());
//需要注意的是servletMapping映射的第二个参数,要通过反射的方式进行实例化
servletMapping.put(servletname.getText(),
(MyHttpServlet) Class.forName(servletclass.getText().trim()).newInstance());
}else if ("servlet-mapping".equalsIgnoreCase(element.getName())){
Element servletname=element.element("servlet-name");
Element urlpattern=element.element("url-pattern");
System.out.println(servletname.getText()+"==>"+urlpattern.getText());
urlmapping.put(urlpattern.getText(),servletname.getText());
}
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
//负责启动容器
private void run(){
ServerSocket serverSocket= null;
try {
serverSocket = new ServerSocket(port);
System.out.println("====服务启动====");
while(!serverSocket.isClosed()){
Socket socket=serverSocket.accept();
RequestHandler requestHandler=new RequestHandler(socket);
new Thread(requestHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
线程处理类也和最初的demo类似,不同的是这里需要根据不同的映射结果返回不同的数据给客户端
public class RequestHandler implements Runnable{
public Socket socket;
public RequestHandler(Socket socket)
{
this.socket=socket;
}
public void run() {
try{
//将inputstream封装成我们自己的request,用来获取uri,method等信息
Request request=new Request(socket.getInputStream());
//将outputstream封装成我们的response对象
Response response=new Response(socket.getOutputStream());
String uri=request.getUri();
System.out.println(uri);
//根据uri得到servletname
String servletname=MyTomcat.urlmapping.get(uri);
//根据servletname得到Servlet对象,如果web.xml文件中有映射就不为空
MyHttpServlet servlet= MyTomcat.servletMapping.get(servletname);
if(servlet!=null){
//不为空执行service方法,即跳转到doGet和doPost方法
servlet.service(request,response);
}else{
String resp=Response.responsebody+"can not find servlet";
OutputStream outputStream=socket.getOutputStream();
System.out.println(resp);
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
至此,Tomcat运行流程就写完了,最好的学习办法是自己敲一遍代码,并理解代码每一步在干什么,下一步去到了哪里。运行结果如下:
最后提供github源码:github源码