手写Tomcat(ServerSocket、HTTP协议)

时间:2024-05-18 20:49:09

Tomcat本质上就一个请求+响应请求的JAVA程序,当我们从浏览器输入一个url时,我们将发送请求到Tomcat上,tomcat对该请求进行解析,并将响应的内容返回浏览器。

Tomcat通过Socket+HTTP协议进行实现,这里做了一个简单的流程图。
手写Tomcat(ServerSocket、HTTP协议)
下面简单介绍下HTTP协议:
HTTP协议

  1. HTTP协议运行于TCP协议之上,默认端口80,HTTP则是443。
  2. HTTP协议都是客户端发起请求,是一个无状态协议,这次请求和上次没有对应关系。
  3. HTTP请求(响应)报文包括:请求行(状态行)、首部、空行和实体。

手写Tomcat(ServerSocket、HTTP协议)
请求头(重点几个)

  • Host:本次请求主机的路径
  • User-Agent:告诉服务器本次请求客户端所在的平台以及本次请求采用的浏览器
  • Accept:用于指定客户端接收哪些类型的信息,*/*都接收
  • Accept-Language:语言
  • Accept-Encoding:压缩数据的格式,例如gzip,表示浏览器可以识别gzip类型的数据

响应头(重点几个)

  • Date:响应事件
  • content-Type:本次响应内容类型
  • content-Encoding:本次内容采用的压缩格式
  • content-length:本次内容长度

动手实现

手写Tomcat(ServerSocket、HTTP协议)
当请求是静态请求,即请求一个html界面的时候,我们只需要读取html文件的内容,并组织成http响应报文即可。这里给出两个html:
手写Tomcat(ServerSocket、HTTP协议)
手写Tomcat(ServerSocket、HTTP协议)
而如果是动态的请求,则就是servlet,通过java代码返回响应体,这里我们配置两个简单servlet在配置文件中:

aa=com.fty.tomcatv2.AAServlet
bb=com.fty.tomcatv2.BBServlet

并实现简单java代码:

package com.fty.tomcatv2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public interface Servlet {//所有服务端JAVA要实现的接口
    //初始化
    public void init();
    //服务
    public void Service(InputStream is, OutputStream os) throws IOException;
    //销毁
    public void destroy();
}

这是servlet的整体接口,而我们的service就是具体的业务逻辑

package com.fty.tomcatv2;	
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class AAServlet implements Servlet {


    public void init() {
        System.out.println("aaServlet...init");
    }	
    public void Service(InputStream is, OutputStream os) throws IOException {
        System.out.println("aaServlet...service");
        os.write("I'm from AAServlet".getBytes());
        os.flush();
    }	
    public void destroy() {
        System.out.println("aa...destroy...");
    }
}

package com.fty.tomcatv2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class BBServlet implements Servlet {	
    public void init() {
        System.out.println("bbServlet...init");
    }
    public void Service(InputStream is, OutputStream os) throws IOException {
        System.out.println("bbServlet...service");
        os.write("I'm from BBServlet".getBytes());
        os.flush();
    }
    public void destroy() {
        System.out.println("bb...destroy...");
    }
}

具体的servlet的实现类,每个servlet可以看成不同业务,即对不同的请求进行特定的响应。最后给出最重要的代码,已经进行很详细的注释:

package com.fty.tomcatv2;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
public class TestServer {

    //获取resources路径
    public static String WEB_ROOT = TestServer.class.getClassLoader().
            getResource("./").getPath();
    //请求的内容,域名后的地址
    private static String url = "";
    //定义一个静态类型的MAP,存储服务conf.properties中的配置信息
    private static Map<String, String> map = new HashMap<String, String>();

    static {
        //服务器启动之前将配置参数中的信息加载到MAP中
        //创建一个Properties对象
        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(WEB_ROOT + "conf.properties"));
            //将配置文件中的数据读取到Map中
            Set set = prop.keySet();
            Iterator iter = set.iterator();
            while (iter.hasNext()) {
                String key = (String) iter.next();
                String value = prop.getProperty(key);
                map.put(key, value);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        Socket socket = null;
        OutputStream outputStream = null;
        InputStream inputStream = null;

        try {
            //创建ServerSocket,监听本机80端口
            serverSocket = new ServerSocket(8080);
            while (true) {
                //获取到客户端对应的socket
                socket = serverSocket.accept();

                //获取输入输出流对象
                outputStream = socket.getOutputStream();
                inputStream = socket.getInputStream();

                //获取HTTP请求部分并解析请求
                //判断本次请求是静态demo.html还是运行在服务端的一段JAVA小程序
                parse(inputStream);
                if(null != url) {
                    if(url.indexOf(".") != -1) {
                        //发送静态资源文件
                        sendStaticResource(outputStream);
                    } else {
                        //发送动态资源
                        sendDynamicResource(outputStream, inputStream);
                    }
                }
                relaxStream(outputStream, inputStream);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放资源
            relaxStream(outputStream, inputStream);
            if(null != socket) {
                socket.close();
                socket = null;
            }
        }
    }

    private static void relaxStream(OutputStream outputStream, InputStream inputStream) throws IOException {
        if(null != outputStream) {
            outputStream.close();
            outputStream = null;
        }
        if(null != inputStream) {
            inputStream.close();
            inputStream = null;
        }
    }

    private static void sendDynamicResource(OutputStream outputStream, InputStream inputStream) throws Exception {
        //将HTTP协议的响应行和响应头发送到客户端
        outputStream.write("HTTP/1.1 200 OK\n".getBytes());
        outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
        outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
        outputStream.write("\n".getBytes());

        //判断map中key是否和本次待请求的资源路径一致
        if(map.containsKey(url)) {
            //如果包含指定的key,获取到map中key对应的value部分
            String value = map.get(url);
            //通过反射将对应的java程序加载到内存
            Class clazz = Class.forName(value);
            //执行init方法
            Servlet servlet = (Servlet) clazz.newInstance();
            //执行service方法
            servlet.init();
            servlet.Service(inputStream, outputStream);
        }
    }

    private static void sendStaticResource(OutputStream outputStream) throws IOException {
        //发送静态资源

        //定义一个文件输入流,用户获取静态资源demo01.html中的内容
        byte[] bytes = new byte[2048];

        FileInputStream fileInputStream = null;
        try {
            //创建文件对象File,代表本次要请求的资源demo01.html
            File file = new File(WEB_ROOT, url);
            //如果文件存在
            if(file.exists()) {
                //向客户端输出HTTP协议的响应行/头
                outputStream.write("HTTP/1.1 200 OK\n".getBytes());
                outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
                outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
                outputStream.write("\n".getBytes());
                //获取到文件输入流对象
                fileInputStream = new FileInputStream(file);
                //读取静态资源demo01.html中的内容到数组中
                int ch = fileInputStream.read(bytes);
                while (ch!=-1) {
                    //将读取到数组中的内容通过输出流发送到客户端
                    outputStream.write(bytes, 0, ch);
                    ch = fileInputStream.read(bytes);
                }

            } else {
                //如果文件不存在
                //向客户端响应文件不存在消息
                outputStream.write("HTTP/1.1 404 not found\n".getBytes());
                outputStream.write("Server:apache-Coyote/1.1\n".getBytes());
                outputStream.write("Content-Type:text/html;charset=utf-8\n".getBytes());
                outputStream.write("\n".getBytes());
                String errorMessage = "file not found";
                outputStream.write(errorMessage.getBytes());
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(null != null) {
                fileInputStream.close();
                fileInputStream = null;
            }
        }

    }

    private static void parse(InputStream inputStream) throws IOException {
        //读取请求部分,截取请求资源名称
        //定义一个变量,存放HTTP协议请求部分的数据
        StringBuffer content = new StringBuffer(2048);
        //定义一个数组,存放HTTP协议请求部分数据
        byte[] buffer = new byte[2048];
        //定义一个变量i,代表读取数据到数组中之后,数据量的大小
        int i = -1;
        //读取客户端发送过来的数据,将数据读取到字节组buffer中,i表示读取数据量的大小
        i = inputStream.read(buffer);
        //遍历字节数组,将数组中的数据追加到content中
        for(int j=0; j<i; j++) {
            content.append((char) buffer[j]);
        }
        //System.out.println(content.toString());
        //截取客户端请求的资源路径
        parseUrl(content.toString());
    }
    //截取客户端请求的资源路径
    private static void parseUrl(String content) {
        //定义2个变量,存放请求后2个空格位置
        int index1, index2;
        index1 = content.indexOf(" ");
        if(index1 != -1) {
            index2 = content.indexOf(" ", index1+1);
            if(index2 > index1) {
                url = content.substring(index1+2, index2);
            }
        }
        System.out.println(url);
    }
}

主要的就是main函数中的逻辑,通过SocketServer绑定一个特定的端口从而可以接收请求,接收请求后判断该请求是动态还是静态请求,然后根据请求的内容调用特定的html或者java代码,得到需要响应的内容,组织成http响应报文,返回浏览器即可。