從零手寫實現 tomcat-03-基本的 socket 實現

老马啸西风發表於2024-05-08

創作緣由

平時使用 tomcat 等 web 伺服器不可謂不多,但是一直一知半解。

於是想著自己實現一個簡單版本,學習一下 tomcat 的精髓。

系列教程

從零手寫實現 apache Tomcat-01-入門介紹

從零手寫實現 apache Tomcat-02-web.xml 入門詳細介紹

從零手寫實現 tomcat-03-基本的 socket 實現

從零手寫實現 tomcat-04-請求和響應的抽象

從零手寫實現 tomcat-05-servlet 處理支援

從零手寫實現 tomcat-06-servlet bio/thread/nio/netty 池化處理

從零手寫實現 tomcat-07-war 如何解析處理三方的 war 包?

從零手寫實現 tomcat-08-tomcat 如何與 springboot 整合?

從零手寫實現 tomcat-09-servlet 處理類

從零手寫實現 tomcat-10-static resource 靜態資原始檔

從零手寫實現 tomcat-11-filter 過濾器

從零手寫實現 tomcat-12-listener 監聽器

整體思路

我們透過 socket 套接字,實現最簡單的服務監聽。

然後直接輸出一個固定的響應到頁面。

套接字是個啥?

Java套接字(Socket)可以想象成一個網路通訊的“管道”。就像你用水管道把水從一個地方輸送到另一個地方,Java套接字則是用來在網路中傳輸資料的。

它允許你的Java程式和網路中的其他程式進行通訊,無論是在同一臺機器上還是在世界的另一端。

在Java中,套接字主要分為兩大類:

  1. 伺服器套接字(ServerSocket):它的作用是監聽網路上的連線請求。你可以把它想象成一個電話總機,它不主動打給別人,而是等著別人打進來。當有請求進來時,伺服器套接字就會建立一個新的通訊“管道”(也就是另一個套接字),專門用來和請求者進行資料交換。

  2. 客戶端套接字(Socket):它的作用是主動去連線伺服器套接字。就像你用電話撥打別人一樣,客戶端套接字會指定一個伺服器的地址和埠,然後嘗試建立連線。一旦連線成功,它也可以建立一個通訊“管道”來傳送和接收資料。

和 tomcat 有啥關係?

Tomcat作為一個Web伺服器,需要和大量的客戶端進行通訊,比如瀏覽器。當瀏覽器請求一個網頁時,Tomcat需要接收這個請求,並返回相應的網頁資料。這個過程就需要用到Java套接字:

  • 監聽連線:Tomcat使用ServerSocket來監聽指定埠上的HTTP請求。當瀏覽器傳送請求時,Tomcat的ServerSocket就會接受這個請求,並建立一個新的套接字來處理它。

  • 資料交換:一旦連線建立,Tomcat就會透過這個套接字和瀏覽器進行資料交換。瀏覽器透過這個“管道”傳送請求,Tomcat接收請求後,處理它,並把響應資料透過同一個“管道”傳送回瀏覽器。

  • 多執行緒處理:由於可能有成千上萬的客戶端同時請求,Tomcat會為每個連線建立一個新的執行緒,這樣每個請求就可以並行處理,而不會互相干擾。

簡而言之,Java套接字是Tomcat實現網路通訊的核心,它允許Tomcat接收客戶端的請求,併傳送響應,從而實現Web服務的功能。

v1-基本程式碼

核心實現

package com.github.houbb.minicat.bs;

import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.minicat.exception.MiniCatException;

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

/**
 * @author 老馬嘯西風
 * @since 0.1.0
 */
public class MiniCatBootstrap {

    private static final Log logger = LogFactory.getLog(MiniCatBootstrap.class);

    /**
     * 啟動埠號
     */
    private final int port;

    public MiniCatBootstrap(int port) {
        this.port = port;
    }

    public MiniCatBootstrap() {
        this(8080);
    }

    public void start() {
        logger.info("[MiniCat] start listen on port {}", port);
        logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);

        try {
            ServerSocket serverSocket = new ServerSocket(port);

            while(true){
                Socket socket = serverSocket.accept();
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write("Hello miniCat!".getBytes());
                socket.close();
            }

        } catch (IOException e) {
            logger.error("[MiniCat] meet ex", e);
            throw new MiniCatException(e);
        }
    }

}

啟動測試

MiniCatBootstrap bootstrap = new MiniCatBootstrap();
bootstrap.start();

日誌:

[INFO] [2024-04-01 16:55:56.705] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] start listen on port 8080
[INFO] [2024-04-01 16:55:56.705] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] visit url http://127.0.0.1:8080

我們瀏覽器訪問 http://127.0.0.1:8080,卻報錯了

該網頁無法正常運作127.0.0.1 傳送的響應無效。
ERR_INVALID_HTTP_RESPONSE

為什麼會報錯呢?

在這個 MiniCatBootstrap 類中,伺服器接收到請求後,直接向客戶端傳送了 "Hello miniCat!" 字串。

然而,HTTP 協議規定了一定的格式要求,而 "Hello miniCat!" 並不符合這些格式要求,因此客戶端無法正確解析這個響應,導致出現 "ERR_INVALID_HTTP_RESPONSE" 錯誤。

要修復這個問題,你需要修改 MiniCatBootstrap 類,以便生成符合 HTTP 格式的響應。

例如,你可以將 "Hello miniCat!" 包裝在一個合法的 HTTP 響應中,如下所示:

String response = "HTTP/1.1 200 OK\r\n" +
                  "Content-Type: text/plain\r\n" +
                  "\r\n" +
                  "Hello miniCat!";

outputStream.write(response.getBytes());

這個響應包括了 HTTP 狀態行("HTTP/1.1 200 OK")、Content-Type 頭部("Content-Type: text/plain")和一個空行("\r\n"),然後是 "Hello miniCat!" 字串。

這樣生成的響應就符合了 HTTP 協議的要求,客戶端應該能夠正確解析它。

程式碼調整

我們把原來的原始字串調整下:

outputStream.write(InnerHttpUtil.httpResp("Hello miniCat!").getBytes());

工具類如下:

    /**
     * 符合 http 標準的字串
     * @param rawText 原始文字
     * @return 結果
     */
    public static String httpResp(String rawText) {
        String format = "HTTP/1.1 200 OK\r\n" +
                "Content-Type: text/plain\r\n" +
                "\r\n" +
                "%s";

        return String.format(format, rawText);
    }

再次訪問,就一切都正常了。

v2-程式碼最佳化+支援stop

上面的方法不支援 stop,這有點不夠優雅。

程式碼調整

調整後的程式碼實現如下:

package com.github.houbb.minicat.bs;

import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.minicat.exception.MiniCatException;
import com.github.houbb.minicat.util.InnerHttpUtil;

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

/**
 * @author 老馬嘯西風
 * @since 0.1.0
 */
public class MiniCatBootstrap {

    private static final Log logger = LogFactory.getLog(MiniCatBootstrap.class);

    /**
     * 啟動埠號
     */
    private final int port;

    /**
     * 是否執行的標識
     */
    private volatile boolean runningFlag = false;

    /**
     * 服務端 socket
     */
    private ServerSocket serverSocket;

    public MiniCatBootstrap(int port) {
        this.port = port;
    }

    public MiniCatBootstrap() {
        this(8080);
    }

    /**
     * 服務的啟動
     */
    public synchronized void start() {
        if(runningFlag) {
            logger.warn("[MiniCat] server is already start!");
            return;
        }

        logger.info("[MiniCat] start listen on port {}", port);
        logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);

        try {
            this.serverSocket = new ServerSocket(port);
            runningFlag = true;

            while(runningFlag){
                Socket socket = serverSocket.accept();
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write(InnerHttpUtil.httpResp("Hello miniCat!").getBytes());
                socket.close();
            }

            logger.info("[MiniCat] end listen on port {}", port);
        } catch (IOException e) {
            logger.error("[MiniCat] start meet ex", e);
            throw new MiniCatException(e);
        }
    }

    /**
     * 服務的啟動
     */
    public synchronized void stop() {
        if(!runningFlag) {
            logger.warn("[MiniCat] server is not start!");
            return;
        }

        try {
            if(this.serverSocket != null) {
                serverSocket.close();
            }
            this.runningFlag = false;

            logger.info("[MiniCat] stop listen on port {}", port);
        } catch (IOException e) {
            logger.error("[MiniCat] stop meet ex", e);
            throw new MiniCatException(e);
        }
    }

}

我們定義一個 runingFlag 變數標識,stop 之後就可以根據這個屬性判斷是否繼續執行。

測試程式碼

我們預期服務啟動 30S 之後,然後關閉。

程式碼如下:

MiniCatBootstrap bootstrap = new MiniCatBootstrap();
bootstrap.start();

TimeUnit.SECONDS.sleep(30);
bootstrap.stop();

這裡會按照我們預期執行嗎?為什麼?

測試結果

測試日誌:

[DEBUG] [2024-04-01 17:23:55.012] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2024-04-01 17:23:55.014] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] start listen on port 8080
[INFO] [2024-04-01 17:23:55.015] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] visit url http://127.0.0.1:8080

我們等待很久,也並沒有等到服務關閉。

為什麼?

即使在修改後的程式碼中新增了 stop() 方法來停止伺服器,但是 start() 方法仍然會在一個無限迴圈中監聽連線請求,導致主執行緒被阻塞。

這是因為 start() 方法中的 while 迴圈會一直執行,直到 stop() 方法被呼叫將 runningFlag 設定為 false。

要解決這個問題,可以將伺服器的監聽邏輯放在一個單獨的執行緒中執行,這樣 start() 方法就可以立即返回,不會阻塞主執行緒。

v3-解決主執行緒阻塞問題

思路

我們把主執行緒執行放到一個非同步執行緒,不去阻塞主線存。

實現

package com.github.houbb.minicat.bs;

import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.minicat.exception.MiniCatException;
import com.github.houbb.minicat.util.InnerHttpUtil;

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

/**
 * @since 0.1.0
 * @author 老馬嘯西風
 */
public class MiniCatBootstrap {

    private static final Log logger = LogFactory.getLog(MiniCatBootstrap.class);

    /**
     * 啟動埠號
     */
    private final int port;

    /**
     * 是否執行的標識
     */
    private volatile boolean runningFlag = false;

    /**
     * 服務端 socket
     */
    private ServerSocket serverSocket;

    public MiniCatBootstrap(int port) {
        this.port = port;
    }

    public MiniCatBootstrap() {
        this(8080);
    }

    public synchronized void start() {
        // 引入執行緒池
        Thread serverThread = new Thread(new Runnable() {
            @Override
            public void run() {
                startSync();
            }
        });

        // 啟動
        serverThread.start();
    }

    /**
     * 服務的啟動
     */
    public void startSync() {
        if(runningFlag) {
            logger.warn("[MiniCat] server is already start!");
            return;
        }

        logger.info("[MiniCat] start listen on port {}", port);
        logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);

        try {
            this.serverSocket = new ServerSocket(port);
            runningFlag = true;

            while(runningFlag && !serverSocket.isClosed()){
                Socket socket = serverSocket.accept();
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write(InnerHttpUtil.httpResp("Hello miniCat!").getBytes());
                socket.close();
            }

            logger.info("[MiniCat] end listen on port {}", port);
        } catch (IOException e) {
            logger.error("[MiniCat] start meet ex", e);
            throw new MiniCatException(e);
        }
    }

    /**
     * 服務的暫停
     */
    public void stop() {
        logger.info("[MiniCat] stop called!");

        if(!runningFlag) {
            logger.warn("[MiniCat] server is not start!");
            return;
        }

        try {
            if(this.serverSocket != null) {
                serverSocket.close();
            }
            this.runningFlag = false;

            logger.info("[MiniCat] stop listen on port {}", port);
        } catch (IOException e) {
            logger.error("[MiniCat] stop meet ex", e);
            throw new MiniCatException(e);
        }
    }

}

啟動測試

MiniCatBootstrap bootstrap = new MiniCatBootstrap();
bootstrap.start();

System.out.println("main START sleep");
TimeUnit.SECONDS.sleep(10);
System.out.println("main END sleep");

bootstrap.stop();

日誌如下:

main START sleep
[INFO] [2024-04-02 09:03:41.604] [Thread-0] [c.g.h.m.b.MiniCatBootstrap.startSync] - [MiniCat] start listen on port 8080
[INFO] [2024-04-02 09:03:41.604] [Thread-0] [c.g.h.m.b.MiniCatBootstrap.startSync] - [MiniCat] visit url http://127.0.0.1:8080
main END sleep
[INFO] [2024-04-02 09:03:51.592] [main] [c.g.h.m.b.MiniCatBootstrap.stop] - [MiniCat] stop called!
[ERROR] [2024-04-02 09:03:51.592] [Thread-0] [c.g.h.m.b.MiniCatBootstrap.startSync] - [MiniCat] start meet ex
java.net.SocketException: socket closed
	at java.net.DualStackPlainSocketImpl.accept0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:127)
	at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:535)
	at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:189)
	at java.net.ServerSocket.implAccept(ServerSocket.java:545)
	at java.net.ServerSocket.accept(ServerSocket.java:513)
	at com.github.houbb.minicat.bs.MiniCatBootstrap.startSync(MiniCatBootstrap.java:74)
	at com.github.houbb.minicat.bs.MiniCatBootstrap$1.run(MiniCatBootstrap.java:49)
	at java.lang.Thread.run(Thread.java:750)
Exception in thread "Thread-0" com.github.houbb.minicat.exception.MiniCatException: java.net.SocketException: socket closed
	at com.github.houbb.minicat.bs.MiniCatBootstrap.startSync(MiniCatBootstrap.java:83)
	at com.github.houbb.minicat.bs.MiniCatBootstrap$1.run(MiniCatBootstrap.java:49)
	at java.lang.Thread.run(Thread.java:750)
Caused by: java.net.SocketException: socket closed
	at java.net.DualStackPlainSocketImpl.accept0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:127)
	at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:535)
	at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:189)
	at java.net.ServerSocket.implAccept(ServerSocket.java:545)
	at java.net.ServerSocket.accept(ServerSocket.java:513)
	at com.github.houbb.minicat.bs.MiniCatBootstrap.startSync(MiniCatBootstrap.java:74)
	... 2 more
[INFO] [2024-04-02 09:03:51.613] [main] [c.g.h.m.b.MiniCatBootstrap.stop] - [MiniCat] stop listen on port 8080

Process finished with exit code 0

已經可以正常的關閉。

開源地址

 /\_/\  
( o.o ) 
 > ^ <

mini-cat 是簡易版本的 tomcat 實現。別稱【嗅虎】(心有猛虎,輕嗅薔薇。)

開源地址:https://github.com/houbb/minicat

相關文章