使用 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 上找到。