使用 Java ServerSocket 的簡單 HTTP 伺服器
1. 概述
HTTP 伺服器通常會向請求的客戶端提供資源。 Java 中有一系列生產級的 Web 伺服器。
但是,我們可以透過使用ServerSocket類別實作 HTTP 伺服器來了解 HTTP 伺服器的工作原理。這個類別允許我們建立一個伺服器,用 IP 位址和連接埠號碼來監聽 TCP 連線。
在本教程中,我們將學習如何使用ServerSocket類別建立一個簡單的伺服器。此外,我們將使用簡單的 HTTP 伺服器執行 GET 請求。值得注意的是,該伺服器僅用於教育目的,不適合生產。
2. 使用ServerSocket的 Web 伺服器基礎知識
首先,伺服器監聽來自客戶端應用程式的連線。客戶端應用程式可以是瀏覽器、其他程式、API 工具等等。連接成功後,伺服器透過向客戶端提供資源來回應客戶端連線。
ServerSocket類別提供在指定連接埠上建立伺服器的方法。它使用accept()方法監聽定義連接埠上的傳入連線。
accept()方法會阻塞直到建立連線為止,並傳回一個Socket實例。 Socket實例提供對伺服器和客戶端之間通訊的輸入和輸出流的存取。
3.建立ServerSocket實例
首先,讓我們建立一個具有指定連接埠的ServerSocket物件:
int port = 8080;
ServerSocket serverSocket = new ServerSocket(port);
接下來,讓我們使用accept()方法來接受傳入的連線:
while (true) {
Socket clientSocket = serverSocket.accept();
// ...
}
在上面的程式碼中,我們使用while循環不斷等待連線。然後,我們呼叫ServerSocket物件上的accept()方法來監聽並接受連線。
當建立連線時,此方法傳回一個Socket對象,允許伺服器和客戶端透過建立的網路進行通訊。
4.處理輸入和輸出
通常,伺服器接收來自客戶端的輸入並發送適當的回應。我們可以使用Socket類別的getInputStream()和getOutputStream()方法透過提供流來向客戶端讀取和寫入數據,從而促進通訊。
讓我們擴展範例來讀取和寫入流:
while (true) {
// ...
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream())
);
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(clientSocket.getOutputStream())
);
// ...
}
在上面的程式碼中,我們使用clientSocket物件上的getInputStream()方法來檢索與客戶端和伺服器之間的活動連線相關的輸入流。該流被包裝在BufferedReader中,以便更有效地讀取文字資料。
類似地, getOutputStream()被包裝在BufferedWriter中,這使得伺服器可以方便地將回應傳送到客戶端。
在我們的範例中,輸入包含一個 HTTP 請求,例如對 URL 的 GET 請求 – http://localhost:8080 。
接下來,讓我們透過呼叫BufferedWriter()物件上的write()方法來編寫伺服器回應。典型的 HTTP 回應具有標頭和正文。
首先我們來寫出回應主體:
String body = """
<html>
<head>
<title>Baeldung Home</title>
</head>
<body>
<h1>Baeldung Home Page</h1>
<p>Java Tutorials</p>
<ul>
<li>
<a href="/get-started-with-java-series"> Java </a>
</li>
<li>
<a href="/spring-boot"> Spring </a>
</li>
<li>
<a href="/learn-jpa-hibernate"> Hibernate </a>
</li>
</ul>
</body>
</html>
""";
在上面的程式碼中,我們建立一個簡單的 HTML 頁面作為回應主體。接下來,讓我們計算內容長度以將其添加到標題中:
int length = body.length();
接下來,讓我們將 HTTP 標頭和內文寫入輸出流:
while (true) {
// ...
String clientInputLine;
while ((clientInputLine = in.readLine()) != null) {
if (clientInputLine.isEmpty()) {
break;
}
out.write("HTTP/1.0 200 OK\r\n");
out.write("Date: " + now + "\r\n");
out.write("Server: Custom Server\r\n");
out.write("Content-Type: text/html\r\n");
out.write("Content-Length: " + length + "\r\n");
out.write("\r\n");
out.write(body);
}
}
在上面的程式碼中,我們使用write()方法定義 HTTP 標頭和正文。值得注意的是,我們使用\r\n (空白行)將標題與正文分開,以表示標題的結束。
5. 多執行緒伺服器
我們的簡單伺服器僅在單一執行緒上處理請求,這會影響效能。伺服器必須能夠同時處理多個請求。
讓我們重構最初的例子,用單獨的執行緒來處理每個請求。首先,讓我們建立一個名為SimpleHttpServerMultiThreaded的類別:
class SimpleHttpServerMultiThreaded {
private final int port;
private static final int THREAD_POOL_SIZE = 10;
public SimpleHttpServerMultiThreaded(int port) {
this.port = port;
}
}
在上面的類別中,我們定義了兩個欄位分別表示連接埠號碼和執行緒池大小。在建立伺服器物件時,連接埠號碼會透過建構函式傳遞。
接下來,讓我們定義一個方法來處理客戶端通訊:
void handleClient(Socket clientSocket) {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(clientSocket.getOutputStream()))
) {
String clientInputLine;
while ((clientInputLine = in.readLine()) != null) {
if (clientInputLine.isEmpty()) {
break;
}
}
LocalDateTime now = LocalDateTime.now();
out.write("HTTP/1.0 200 OK\r\n");
out.write("Date: " + now + "\r\n");
out.write("Server: Custom Server\r\n");
out.write("Content-Type: text/html\r\n");
out.write("Content-Length: " + length + "\r\n");
out.write("\r\n");
out.write(body);
} catch (IOException e) {
// ...
} finally {
try {
clientSocket.close();
} catch (IOException e) {
// ...
}
}
}
上述方法示範如何處理與客戶端的輸入和輸出通訊。 body和length與上一節的範例相同。
接下來,讓我們建立另一個名為start()的方法來在單獨的執行緒上建立每個連線:
void start() throws IOException {
try (ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
Socket clientSocket = serverSocket.accept();
threadPool.execute(() -> handleClient(clientSocket));
}
}
}
在上面的程式碼中,我們透過實例化ExecutorService來建立線程池。接下來,我們呼叫threadPool物件上的execute()方法為每個客戶端連線提交一個任務。
透過將客戶端連線分配給線程池中的線程,伺服器可以同時處理多個請求,從而顯著提高效能。
此外,每次客戶端連線時, accept()方法都會建立一個新的Socket實例。此Socket特定於客戶端連接,並在伺服器和客戶端之間提供專用的通訊通道。
6.測試伺服器
讓我們透過在main方法中實例化來執行我們的伺服器:
static void main(String[] args) throws IOException {
int port = 8080;
SimpleHttpServerMultiThreaded server = new SimpleHttpServerMultiThreaded(port);
server.start();
}
接下來我們在瀏覽器中開啟http://localhost:8080來測試伺服器:
7. 結論
在本文中,我們學習如何使用ServerSocket類別建立一個簡單的伺服器。此外,我們還看到瞭如何使用此類創建單線程和多線程伺服器的範例。
與往常一樣,範例的完整原始程式碼可在 GitHub 上找到。