創作緣由
平時使用 tomcat 等 web 服務器不可謂不多,但是一直一知半解。
於是想着自己實現一個簡單版本,學習一下 tomcat 的精髓。
系列教程
從零手寫實現 apache Tomcat-02-web.xml 入門詳細介紹
從零手寫實現 tomcat-03-基本的 socket 實現
從零手寫實現 tomcat-06-servlet bio/thread/nio/netty 池化處理
從零手寫實現 tomcat-07-war 如何解析處理三方的 war 包?
從零手寫實現 tomcat-08-tomcat 如何與 springboot 集成?
從零手寫實現 tomcat-10-static resource 靜態資源文件
整體思路
我們通過 socket 套接字,實現最簡單的服務監聽。
然後直接輸出一個固定的響應到頁面。
套接字是個啥?
Java套接字(Socket)可以想象成一個網絡通信的“管道”。就像你用水管道把水從一個地方輸送到另一個地方,Java套接字則是用來在網絡中傳輸數據的。
它允許你的Java程序和網絡中的其他程序進行通信,無論是在同一臺機器上還是在世界的另一端。
在Java中,套接字主要分爲兩大類:
-
服務器套接字(ServerSocket):它的作用是監聽網絡上的連接請求。你可以把它想象成一個電話總機,它不主動打給別人,而是等着別人打進來。當有請求進來時,服務器套接字就會創建一個新的通信“管道”(也就是另一個套接字),專門用來和請求者進行數據交換。
-
客戶端套接字(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 實現。別稱【嗅虎】(心有猛虎,輕嗅薔薇。)