使用java寫一個(gè)簡易的tomcat

該項(xiàng)目源碼地址:https://github.com/lastwhispers/mimic/tree/master/tomcat-mini

1. 項(xiàng)目簡介

一個(gè)極簡的tomcat。使用java實(shí)現(xiàn)最基本的tomcat的功能,能夠接收Http請(qǐng)求、處理Http請(qǐng)求(提供Servlet接口)、響應(yīng)Http請(qǐng)求。

項(xiàng)目結(jié)構(gòu):

項(xiàng)目結(jié)構(gòu)

項(xiàng)目演示:

項(xiàng)目運(yùn)行

2. 涉及的技術(shù)

3. 各部分設(shè)計(jì)

3.1 servlet接口設(shè)計(jì)

/**
 * @author lastwhisper
 * @desc 服務(wù)器小腳本接口
 */
public abstract class Servlet {
    /**
     * 
     * @author lastwhisper
     * @desc 處理GET請(qǐng)求
     * @param request
     * @param response
     */
    public abstract void doGet(Request request, Response response);

    /**
     * 
     * @author lastwhisper
     * @desc 處理POST請(qǐng)求
     * @param request
     * @param response
     */
    public abstract void doPost(Request request, Response response);

    /**
     * 
     * @author lastwhisper
     * @desc 處理請(qǐng)求
     * @param request
     * @param response
     */
    public void service(Request request, Response response) {
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            doGet(request, response);
        } else if ("POST".equalsIgnoreCase(request.getMethod())) {
            doPost(request, response);
        }
    }
}

3.2 解析web.xml、反射創(chuàng)建對(duì)象

使用過servlet的朋友都知道,tomcat通過web.xml的配置找到url與對(duì)應(yīng)的servlet全名,然后通過反射創(chuàng)建servlet。

比如有如下web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>cn.lastwhisper.server.basic.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>reg</servlet-name>
        <servlet-class>cn.lastwhisper.server.basic.servlet.RegisterServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
        <url-pattern>/g</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>reg</servlet-name>
        <url-pattern>/reg</url-pattern>
    </servlet-mapping>
</web-app>

使用java解析xml,并通過反射創(chuàng)建對(duì)象。

import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

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

/**
 * 
 * @author lastwhisper
 *
 */
public class XmlConvertion {

    public static void main(String[] args) throws Exception{
        // SAX解析
        // 1、獲取解析工廠
        SAXParserFactory factory = SAXParserFactory.newInstance();
        // 2、從解析工廠獲取解析器
        SAXParser parse = factory.newSAXParser();
        // 3、編寫處理器
        // 4、加載文檔 Document 注冊(cè)處理器
        WebHandler handler = new WebHandler();
        // 5、解析
        parse.parse(Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("cn/lastwhisper/server/basic/servlet/web.xml"), handler);

        // 獲取數(shù)據(jù)
        WebContext context = new WebContext(handler.getEntitys(), handler.getMappings());
        // 假設(shè)你輸入了 /login
        String className = context.getClazz("/g");
        Class clz = Class.forName(className);
        Servlet servlet = (Servlet) clz.getConstructor().newInstance();
        servlet.service();
    }

}

class WebHandler extends DefaultHandler {
    private List<Entity> entitys;
    private List<Mapping> mappings;
    private Entity entity;
    private Mapping mapping;
    private String tag; // 存儲(chǔ)操作標(biāo)簽
    private boolean isMapping = false;

    @Override
    public void startDocument() throws SAXException {
        entitys = new ArrayList<Entity>();
        mappings = new ArrayList<Mapping>();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        tag = qName;
        if ("servlet".equals(tag)) {
            entity = new Entity();
            isMapping = false;
        } else if ("servlet-mapping".equals(tag)) {
            mapping = new Mapping();
            isMapping = true;
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String contents = new String(ch, start, length).trim();
        if (!isMapping) { // 操作servlet
            if ("servlet-name".equals(tag)) {
                entity.setName(contents);
            } else if ("servlet-class".equals(tag)) {
                entity.setClazz(contents);
            }
        } else { // 操作servlet-mapping
            if ("servlet-name".equals(tag)) {
                mapping.setName(contents);
            } else if ("url-pattern".equals(tag)) {
                mapping.addPatterns(contents);
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("servlet".equals(qName)) {
            entitys.add(entity);
        } else if ("servlet-mapping".equals(qName)) {
            mappings.add(mapping);
        }
        tag = null; // tag丟棄了
    }

    @Override
    public void endDocument() throws SAXException {
    }

    public List<Entity> getEntitys() {
        return entitys;
    }

    public List<Mapping> getMappings() {
        return mappings;
    }

}

3.3 接收Http請(qǐng)求

接收Http請(qǐng)求的步驟:

  1. 創(chuàng)建serversocket
  2. 建立連接獲取socket
  3. 通過輸入流獲取請(qǐng)求協(xié)議
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author lastwhisper
 * @desc 使用ServerSocket建立與瀏覽器的連接,獲取請(qǐng)求協(xié)議
 */
public class Server01 {
    private ServerSocket serverSocket;

    public static void main(String[] args) {
        Server01 server = new Server01();
        server.start();
    }

    // 啟動(dòng)服務(wù)
    public void start() {
        try {
            serverSocket = new ServerSocket(8888);
            revice();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服務(wù)器啟動(dòng)失敗....");
        }
    }

    // 接受連接處理
    public void revice() {
        try {
            // 接受連接
            Socket socket = serverSocket.accept();
            // 獲取請(qǐng)求協(xié)議
            InputStream inputStream = socket.getInputStream();
            byte[] datas = new byte[1024 * 1024];
            int len = inputStream.read(datas);
            String request = new String(datas, 0, len);
            System.out.println(request);
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客戶端錯(cuò)誤");
        }
    }

    // 停止服務(wù)
    public void stop() {

    }
}

運(yùn)行main方法,使用瀏覽器訪問 http://localhost:8888/ 可以看到控制臺(tái)輸出了瀏覽器發(fā)起的Http請(qǐng)求

GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Purpose: prefetch
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: Hm_lvt_8875c662941dbf07e39c556c8d97615f=1554520109,1555680733

3.4 響應(yīng)Http請(qǐng)求

響應(yīng)Http請(qǐng)求的步驟:

  1. 準(zhǔn)備響應(yīng)內(nèi)容
  2. 獲取字節(jié)數(shù)的長度
  3. 拼接響應(yīng)協(xié)議(注意空格與換行)
  4. 使用輸入輸出流
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

/**
 * @author lastwhisper
 * @desc 返回響應(yīng)協(xié)議
 */
public class Server02 {
    private ServerSocket serverSocket;

    public static void main(String[] args) {
        Server02 server = new Server02();
        server.start();
    }

    // 啟動(dòng)服務(wù)
    public void start() {
        try {
            serverSocket = new ServerSocket(8888);
            revice();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服務(wù)器啟動(dòng)失敗....");
        }
    }

    // 接受連接請(qǐng)求,響應(yīng)請(qǐng)求
    public void revice() {
        try {
            // 1. 接受連接請(qǐng)求
            Socket client = serverSocket.accept();
            // 2. 獲取http請(qǐng)求內(nèi)容
            InputStream inputStream = client.getInputStream();
            byte[] datas = new byte[1024 * 1024];
            int len = inputStream.read(datas);
            String request = new String(datas, 0, len);
            System.out.println(request);

            // 3. 構(gòu)造響應(yīng)請(qǐng)求
            StringBuilder content = new StringBuilder();
            content.append("<html>");
            content.append("<head>");
            content.append("<title>");
            content.append("服務(wù)器響應(yīng)成功");
            content.append("</title>");
            content.append("</head>");
            content.append("<body>");
            content.append("tomcat server終于回來了。。。。");
            content.append("</body>");
            content.append("</html>");
            int size = content.toString().getBytes().length; // 必須獲取字節(jié)長度

            StringBuilder response = new StringBuilder();
            String blank = " ";
            String CRLF = "\r\n";
            // 3.1 響應(yīng)行: HTTP/1.1 200 OK
            response.append("HTTP/1.1").append(blank);
            response.append(200).append(blank);
            response.append("OK").append(CRLF);
            // 3.2 響應(yīng)頭(最后一行存在空行):
            /*
             * Date:Mon,31Dec209904:25:57GMT 
             * Server:tomcat Server/0.0.1;charset=GBK
             * Content-type:text/html 
             * Content-length:39725426
             */
            response.append("Date:").append(new Date()).append(CRLF);
            response.append("Server:").append("tomcat Server/0.0.1;charset=GBK").append(CRLF);
            response.append("Content-type:text/html").append(CRLF);
            response.append("Content-length:").append(size).append(CRLF);
            response.append(CRLF);
            // 3.3 正文
            response.append(content.toString());
            // 4. 響應(yīng)到到客戶端
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
            bw.write(response.toString());
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客戶端錯(cuò)誤");
        }
    }

    // 停止服務(wù)
    public void stop() {

    }
}

運(yùn)行main方法,在瀏覽器訪問 http://localhost:8888/ ,即可看到自己構(gòu)造的響應(yīng)。

3.5 封裝響應(yīng)Response

由于每次構(gòu)造的響應(yīng)只有響應(yīng)內(nèi)容和響應(yīng)狀態(tài)碼是不固定的,所以可用封裝響應(yīng)。

  1. 動(dòng)態(tài)添加內(nèi)容
  2. 根據(jù)狀態(tài)碼拼接響應(yīng)頭協(xié)議
  3. 根據(jù)狀態(tài)碼響應(yīng)請(qǐng)求
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Date;

/**
 * @author lastwhisper
 * @desc 封裝響應(yīng)實(shí)體
 */
public class Response {
    private BufferedWriter bw;

    // 響應(yīng)正文
    private StringBuilder content;
    // 協(xié)議頭(響應(yīng)行、響應(yīng)頭、回車)信息
    private StringBuilder headInfo;
    // 正文的字節(jié)數(shù)
    private int len;

    private final String BLANK = " ";
    private final String CRLF = "\r\n";

    private Response() {
        content = new StringBuilder();
        headInfo = new StringBuilder();
        len = 0;
    }

    public Response(Socket client) throws IOException {
        this(client.getOutputStream());
    }

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

    // 動(dòng)態(tài)添加內(nèi)容(無換行)
    public Response print(String info) {
        content.append(info);
        len += info.getBytes().length;
        return this;
    }

    // 動(dòng)態(tài)添加內(nèi)容(有換行)
    public Response println(String info) {
        content.append(info).append(CRLF);
        len += (info + CRLF).getBytes().length;
        return this;
    }

    // 構(gòu)建響應(yīng)行、響應(yīng)頭信息
    private void createHeadInfo(int code) {
        // 1.響應(yīng)行 HTTP/1.1 200 OK
        headInfo.append("HTTP/1.1").append(BLANK);
        headInfo.append(code).append(BLANK);
        switch (code) {
        case 200:
            headInfo.append("OK").append(CRLF);
            break;
        case 404:
            headInfo.append("NOT FOUND").append(CRLF);
            break;
        case 505:
            headInfo.append("SERVER ERROR").append(CRLF);
            break;
        }

        // 2.響應(yīng)頭(最后一行存在空行)
        /*
         * Date:Mon,31Dec209904:25:57GMT 
         * Server:tomcat Server/0.0.1;charset=GBK
         * Content-type:text/html 
         * Content-length:39725426
         */
        headInfo.append("Date:").append(new Date()).append(CRLF);
        headInfo.append("Server:").append("tomcat Server/0.0.1;charset=GBK").append(CRLF);
        headInfo.append("Content-type:text/html").append(CRLF);
        headInfo.append("Content-length:").append(len).append(CRLF);
        headInfo.append(CRLF);
    }

    // 推送響應(yīng)信息
    public void pushToBrowser(int code) throws IOException {
        if (null == headInfo) {
            code = 505;
        }
        createHeadInfo(code);
        bw.append(headInfo);
        bw.append(content);
        bw.flush();
    }
}

3.6 封裝請(qǐng)求request

http的請(qǐng)求消息如下:

GET /hello HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

我們需要將請(qǐng)求類型、url、以及請(qǐng)求參數(shù)提取出來,用來映射到servlet上。

  1. 通過分解字符串獲取method、url、get參數(shù)、post請(qǐng)求體
  2. 通過Map封裝請(qǐng)求參數(shù)
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author lastwhisper
 * @desc 封裝請(qǐng)求協(xié)議: 獲取 method uri以及請(qǐng)求參數(shù)
 */
public class Request2 {
    // http請(qǐng)求信息
    private String requestInfo;
    // 請(qǐng)求方式
    private String method;
    // 請(qǐng)求url
    private String url;
    // 請(qǐng)求參數(shù)字符串username=zhangsan&pwd=123456
    private String queryParamStr;
    // 存儲(chǔ)請(qǐng)求參數(shù)
    private Map<String, List<String>> parameterMap;
    // 回車
    private final String CRLF = "\r\n";

    public Request2(Socket client) throws IOException {
        this(client.getInputStream());
    }

    public Request2(InputStream is) {
        parameterMap = new HashMap<String, List<String>>();
        byte[] datas = new byte[1024 * 1024];
        int len;
        try {
            len = is.read(datas);
            requestInfo = new String(datas, 0, len);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        // 分解字符串
        parseRequestInfo();
        // 轉(zhuǎn)成Map uname=張三&hobby=籃球&hobby=讀書&other=
        convertMap();
    }

    /**
     * 
     * @author lastwhisper
     * @desc 解析http請(qǐng)求信息
     */
    private void parseRequestInfo() {
        // 1. 獲取請(qǐng)求方式: 開頭到第一個(gè)/
        method = requestInfo.substring(0, requestInfo.indexOf("/")).toLowerCase().trim();
        // 2. 獲取請(qǐng)求url: 第一個(gè)/ 到 HTTP/;獲取get參數(shù)
        // 2.1 獲取/的位置
        int startIdx = requestInfo.indexOf("/") + 1;
        // 2.2 獲取 HTTP/的位置
        int endIdx = requestInfo.indexOf("HTTP/");
        // 2.3 分割字符串
        url = requestInfo.substring(startIdx, endIdx);
        // 2.4 獲取?的位置
        int queryIdx = url.indexOf("?");
        // 表示存在get請(qǐng)求參數(shù)
        if (queryIdx >= 0) {
            String[] urlParam = url.split("\\?");
            url = urlParam[0];
            queryParamStr = urlParam[1].trim();
        }

        // 3. 獲取post請(qǐng)求參數(shù)
        if ("post".equals(method)) {
            String requestBody = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
            if (null == queryParamStr) {
                queryParamStr = requestBody;
            } else {
                queryParamStr += "&" + requestBody;
            }
        }
        queryParamStr = null == queryParamStr ? "" : queryParamStr;
//      System.out.println(method + "-->" + url + "-->" + queryParamStr);
    }

    /**
     * @author lastwhisper
     * @desc 將請(qǐng)求參數(shù)轉(zhuǎn)為Map uname=張三&hobby=籃球&hobby=讀書&other=
     */
    private void convertMap() {
        // 1. 分割字符串 &
        String[] keyValues = this.queryParamStr.split("&");
        for (String queryStr : keyValues) {
            // 2. 再次分割字符串 =
            String[] kv = queryStr.split("=");
            // 防止other= ,value沒有值導(dǎo)致kv[1]數(shù)組越界,  
            kv = Arrays.copyOf(kv, 2);
            // 3. 獲取key和value
            String key = kv[0];
            String value = kv[1] == null ? null : decode(kv[1], "utf-8");
            // 4. 存儲(chǔ)到Map中
            if (!parameterMap.containsKey(key)) { // 第一次出現(xiàn)key(name)
                parameterMap.put(key, new ArrayList<String>());
            }
            parameterMap.get(key).add(value);
        }
    }

    /**
     * 
     * @author lastwhisper
     * @desc 處理中文
     * @param value 待處理字符串
     * @param enc   解碼后的編碼
     * @return
     */
    private String decode(String value, String enc) {
        try {
            return java.net.URLDecoder.decode(value, enc);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 
     * @author lastwhisper
     * @desc 通過name獲取對(duì)應(yīng)的多個(gè)值
     * @param key 參數(shù)名
     * @return
     */
    public String[] getParameterValues(String key) {
        List<String> values = this.parameterMap.get(key);
        if (null == values || values.size() < 1) {
            return null;
        }
        return values.toArray(new String[0]);
    }

    /**
     * 
     * @author lastwhisper
     * @desc 通過name獲取對(duì)應(yīng)的一個(gè)值
     * @param key 參數(shù)名
     * @return
     */
    public String getParameter(String key) {
        String[] values = getParameterValues(key);
        return values == null ? null : values[0];
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getQueryParamStr() {
        return queryParamStr;
    }
}

3.7 增加內(nèi)容分發(fā)器

使用內(nèi)容分發(fā)器,可以同時(shí)處理多個(gè)請(qǐng)求,并區(qū)分靜態(tài)頁面請(qǐng)求和動(dòng)態(tài)請(qǐng)求,使用的是短連接。

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

/**
 * @author lastwhisper
 * @desc 分發(fā)器 加入狀態(tài)內(nèi)容處理 404 505 以及首頁
 */
public class Dispatcher implements Runnable {
    private Socket client;
    private Request request;
    private Response response;

    public Dispatcher(Socket client) {
        this.client = client;
        try {
            // 獲取請(qǐng)求
            // 獲取響應(yīng)
            request = new Request(client);
            response = new Response(client);
        } catch (IOException e) {
            e.printStackTrace();
            IOUtils.closeSocket(client);
        }

    }

    @Override
    public void run() {
        try {
            if (request.getUrl() == null || request.getUrl().equals("")) {
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("index.html");
                response.print(IOUtils.InputStreamToString(is));
                response.pushToBrowser(200);
                is.close();
            }
            if (request.getUrl().endsWith("html")) {
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(request.getUrl());
                response.print(IOUtils.InputStreamToString(is));
                response.pushToBrowser(200);
                is.close();
            }
            Servlet servlet = WebApp.getServletFromUrl(request.getUrl());
            if (servlet != null) {
                servlet.service(request, response);
                response.pushToBrowser(200);
            } else {
                // 錯(cuò)誤....
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("error.html");
                response.print(IOUtils.InputStreamToString(is));
                response.pushToBrowser(404);
                is.close();
            }
        } catch (IOException e) {
            try {
                response.println("服務(wù)器端錯(cuò)誤");
                response.pushToBrowser(505);
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
        IOUtils.closeSocket(client);
    }

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)自陳明乾的博客,可能有一定更新。 轉(zhuǎn)原文聲明:原創(chuàng)作品,允許轉(zhuǎn)載,轉(zhuǎn)載時(shí)請(qǐng)務(wù)必以超鏈接形式標(biāo)明文章 原始出處 、...
    C86guli閱讀 4,885評(píng)論 6 72
  • 0 系列目錄# WEB請(qǐng)求處理 WEB請(qǐng)求處理一:瀏覽器請(qǐng)求發(fā)起處理 WEB請(qǐng)求處理二:Nginx請(qǐng)求反向代理 本...
    七寸知架構(gòu)閱讀 14,250評(píng)論 22 189
  • 1.內(nèi)存泄露 內(nèi)存泄漏兩種情況: 在堆中申請(qǐng)的空間沒有被釋放(虛擬機(jī)gc可以解決) 對(duì)象已不在使用,但仍然在內(nèi)存中...
    Aimerwhy閱讀 710評(píng)論 0 0
  • 這部分主要是與Java Web和Web Service相關(guān)的面試題。 96、闡述Servlet和CGI的區(qū)別? 答...
    雜貨鋪老板閱讀 1,507評(píng)論 0 10
  • IOC 控制反轉(zhuǎn)容器控制程序?qū)ο笾g的關(guān)系,而不是傳統(tǒng)實(shí)現(xiàn)中,有程序代碼之間控制,又名依賴注入。All 類的創(chuàng)建,...
    irckwk1閱讀 1,101評(píng)論 0 0

友情鏈接更多精彩內(nèi)容