本系列文章將帶領大家使用 Java 從頭編寫一個簡易的 Web 伺服器。Web 伺服器與客戶端之間的通信基於 HTTP 協議,而 HTTP 協議本質上是建立在 TCP 協議之上的請求-應答標準。因此,我們將利用 Java 中的 Socket API 來實現這個簡易伺服器。關於 HTTP 協議的更多細節,讀者可自行查閱相關資料深入了解。
在編寫伺服器之前,我們先透過實際代碼觀察瀏覽器發送的請求內容。使用 ServerSocket 監聽 80 埠,接受連接並讀取輸入流:
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import org.junit.Test;
public class HQHttpProtocolTest {
@Test
public void server() throws Exception {
ServerSocket serverSocket = new ServerSocket(80);
Socket socket = serverSocket.accept();
InputStream stream = socket.getInputStream();
int r = -1;
while ((r = stream.read()) != -1) {
System.out.print((char) r);
}
}
}
啟動測試後,在瀏覽器訪問 https://127.0.0.1,控制台將輸出類似如下的請求報文:
GET / HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
為了更全面地分析,我們構造一個包含表單提交的 HTML 頁面,再次觀察請求:
POST /?test=123 HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Content-Length: 8
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: null
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
name=bob
名稱: 值,以 \r\n 分隔。name=bob)。注意:以上僅模擬最簡單的表單提交(application/x-www-form-urlencoded),複雜的文件上傳(multipart/form-data)格式有所不同,本文不展開討論。
同樣地,我們編寫客戶端代碼向一個真實的 Web 伺服器發送請求,捕獲響應報文:
@Test
public void client() throws Exception {
Socket socket = new Socket("127.0.0.1", 80);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("GET /Servlet/test.html HTTP/1.1\r\n");
writer.write("Host: 127.0.0.1\r\n");
writer.write("Connection: keep-alive\r\n");
writer.write("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n");
writer.write("User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36\r\n");
writer.write("Accept-Encoding: gzip,deflate,sdch\r\n");
writer.write("Accept-Language: zh-CN,zh;q=0.8\r\n");
writer.write("\r\n");
writer.flush();
InputStream stream = socket.getInputStream();
int r = -1;
while ((r = stream.read()) != -1) {
System.out.print((char) r);
}
}
伺服器返回的響應內容如下:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Accept-Ranges: bytes
ETag: W/"129-1456125361109"
Last-Modified: Mon, 22 Feb 2016 07:16:01 GMT
Content-Type: text/html
Content-Length: 129
Date: Mon, 22 Feb 2016 08:08:32 GMT
this is test page.
注意:除此類標準響應外,還存在 Transfer-Encoding: chunked 等分塊傳輸方式,本文不深入探討。
至此,我們已詳細分析了 HTTP 請求與響應的報文格式。下一篇將開始逐步實現伺服器端的核心邏輯,敬請期待。