現(xiàn)在訪問網(wǎng)站和Web應(yīng)用大多采用“瀏覽器--服務(wù)器”也就是Browser/Server模式,簡稱BS架構(gòu),它的特點是,客戶只需要瀏覽器,應(yīng)用程序的邏輯和數(shù)據(jù)都存儲在服務(wù)器端??蛻糁恍枰@取Web頁面,并在web頁面上完成各種操作。
Web頁面具有極強(qiáng)的交互性。由于Web頁面是用HTML編寫的,而HTML具備超強(qiáng)的表現(xiàn)力,并且,服務(wù)器端升級后,客戶端無需任何部署就可以使用到新的版本,因此,BS架構(gòu)升級非常容易。
HTTP協(xié)議
在Web應(yīng)用中,瀏覽器請求一個URL,服務(wù)器就把生成的HTML網(wǎng)頁發(fā)送給瀏覽器,而瀏覽器和服務(wù)器之間的傳輸協(xié)議是HTTP,所以:
HTML是一種用來定義網(wǎng)頁的文本,會HTML,就可以編寫網(wǎng)頁;
HTTP是在網(wǎng)絡(luò)上傳輸HTML的協(xié)議,用于瀏覽器和服務(wù)器的通信。
對于Browser來說,請求頁面的流程如下:
- 與服務(wù)器建立TCP連接;
- 發(fā)送HTTP請求;
- 收取HTTP響應(yīng),然后把網(wǎng)頁在瀏覽器中顯示出來。
瀏覽器發(fā)送的HTTP請求如下:
GET / HTTP/1.1
Host: www.sina.com.cn
User-Agent: Mozilla/5.0 xxx
Accept: */*
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8
其中,第一行表示使用GET
請求獲取路徑為/
的資源,并使用HTTP/1.1
協(xié)議,從第二行開始,每行都是以Header: Value
形式表示的HTTP頭,比較常用的HTTP Header包括:
- Host: 表示請求的主機(jī)名,因為一個服務(wù)器上可能運(yùn)行著多個網(wǎng)站,因此,Host表示瀏覽器正在請求的域名;
- User-Agent: 標(biāo)識客戶端本身,例如Chrome瀏覽器的標(biāo)識類似
Mozilla/5.0 ... Chrome/79
,IE瀏覽器的標(biāo)識類似Mozilla/5.0 (Windows NT ...) like Gecko
; - Accept:表示瀏覽器能接收的資源類型,如
text/*
,image/*
或者*/*
表示所有; - Accept-Language:表示瀏覽器偏好的語言,服務(wù)器可以據(jù)此返回不同語言的網(wǎng)頁;
- Accept-Encoding:表示瀏覽器可以支持的壓縮類型,例如
gzip, deflate, br
。
服務(wù)器的響應(yīng)如下:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 21932
Content-Encoding: gzip
Cache-Control: max-age=300
<html>
...網(wǎng)頁數(shù)據(jù)...
</html>
服務(wù)器響應(yīng)的第一行總是版本號+空格+數(shù)字+空格+文本,數(shù)字表示響應(yīng)代碼,其中2xx
表示成功,3xx
表示重定向,4xx
表示客戶端引發(fā)的錯誤,5xx
表示服務(wù)器端引發(fā)的錯誤。數(shù)字是給程序識別,文本則是給開發(fā)者調(diào)試使用的。常見的響應(yīng)代碼有:
- 200 OK:表示成功;
- 301 Moved Permanently:表示該URL已經(jīng)永久重定向;
- 302 Found:表示該URL需要臨時重定向;
- 304 Not Modified:表示該資源沒有修改,客戶端可以使用本地緩存的版本;
- 400 Bad Request:表示客戶端發(fā)送了一個錯誤的請求,例如參數(shù)無效;
- 401 Unauthorized:表示客戶端因為身份未驗證而不允許訪問該URL;
- 403 Forbidden:表示服務(wù)器因為權(quán)限問題拒絕了客戶端的請求;
- 404 Not Found:表示客戶端請求了一個不存在的資源;
- 500 Internal Server Error:表示服務(wù)器處理時內(nèi)部出錯,例如因為無法連接數(shù)據(jù)庫;
- 503 Service Unavailable:表示服務(wù)器此刻暫時無法處理請求。
從第二行開始,服務(wù)器每一行均返回一個HTTP頭。服務(wù)器經(jīng)常返回的HTTP Header包括:
- Content-Type:表示該響應(yīng)內(nèi)容的類型,例如
text/html
,image/jpeg
; - Content-Length:表示該響應(yīng)內(nèi)容的長度(字節(jié)數(shù));
- Content-Encoding:表示該響應(yīng)壓縮算法,例如
gzip
; - Cache-Control:指示客戶端應(yīng)如何緩存,例如
max-age=300
表示可以最多緩存300秒。
HTTP請求和響應(yīng)都由HTTP Header和HTTP Body構(gòu)成,其中HTTP Header每行都以\r\n
結(jié)束。如果遇到兩個連續(xù)的\r\n
,那么后面就是HTTP Body。瀏覽器讀取HTTP Body,并根據(jù)Header信息中指示的Content-Type
、Content-Encoding
等解壓后顯示網(wǎng)頁、圖像或其他內(nèi)容。
通常瀏覽器獲取的第一個資源是HTML網(wǎng)頁,在網(wǎng)頁中,如果嵌入了JavaScript、CSS、圖片、視頻等其他資源,瀏覽器會根據(jù)資源的URL再次向服務(wù)器請求對應(yīng)的資源。
編寫HTTP Server 回應(yīng)客戶端請求
一個HTTP Server本質(zhì)上是一個TCP服務(wù)器,如下為多線程實現(xiàn)的服務(wù)器:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8080); // 監(jiān)聽指定端口
System.out.println("server is running...");
for (;;) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
t.start();
}
}
}
class Handler extends Thread {
Socket sock;
public Handler(Socket sock) {
this.sock = sock;
}
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}
private void handle(InputStream input, OutputStream output) throws IOException {
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
// TODO: 處理HTTP請求
}
}
只需要在handle()
方法中,用Reader讀取HTTP請求,用Writer發(fā)送HTTP響應(yīng),即可實現(xiàn)一個最簡單的HTTP服務(wù)器。編寫代碼如下:
private void handle(InputStream input, OutputStream output) throws IOException {
System.out.println("Process new http request...");
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
// 讀取HTTP請求:
boolean requestOk = false;
String first = reader.readLine();
if (first.startsWith("GET / HTTP/1.")) {
requestOk = true;
}
for (;;) {
String header = reader.readLine();
if (header.isEmpty()) { // 讀取到空行時, HTTP Header讀取完畢
break;
}
System.out.println(header);
}
System.out.println(requestOk ? "Response OK" : "Response Error");
if (!requestOk) {
// 發(fā)送錯誤響應(yīng):
writer.write("HTTP/1.0 404 Not Found\r\n");
writer.write("Content-Length: 0\r\n");
writer.write("\r\n");
writer.flush();
} else {
// 發(fā)送成功響應(yīng):
String data = "<html><body><h1>Hello, world!</h1></body></html>";
int length = data.getBytes(StandardCharsets.UTF_8).length;
writer.write("HTTP/1.0 200 OK\r\n");
writer.write("Connection: close\r\n");
writer.write("Content-Type: text/html\r\n");
writer.write("Content-Length: " + length + "\r\n");
writer.write("\r\n"); // 空行標(biāo)識Header和Body的分隔
writer.write(data);
writer.flush();
}
}
這里的核心代碼是,先讀取HTTP請求,這里我們只處理GET /
的請求。當(dāng)讀取到空行時,表示已讀到連續(xù)兩個\r\n
,說明請求結(jié)束,可以發(fā)送響應(yīng)。發(fā)送響應(yīng)的時候,首先發(fā)送響應(yīng)代碼HTTP/1.0 200 OK
表示一個成功的200響應(yīng),使用HTTP/1.0
協(xié)議,然后,依次發(fā)送Header,發(fā)送完Header后,再發(fā)送一個空行標(biāo)識Header結(jié)束,緊接著發(fā)送HTTP Body。
HTTP目前有多個版本,1.0
是早期版本,瀏覽器每次建立TCP連接后,只發(fā)送一個HTTP請求并接收一個HTTP響應(yīng),然后就關(guān)閉TCP連接。由于創(chuàng)建TCP連接本身就需要消耗一定的時間,因此,HTTP 1.1允許瀏覽器和服務(wù)器在同一個TCP連接上反復(fù)發(fā)送、接收多個HTTP請求和響應(yīng),這樣就大大提高了傳輸效率。
我們注意到HTTP協(xié)議是一個請求-響應(yīng)協(xié)議,它總是發(fā)送一個請求,然后接收一個響應(yīng)。能不能一次性發(fā)送多個請求,然后再接收多個響應(yīng)呢?HTTP 2.0可以支持瀏覽器同時發(fā)出多個請求,但每個請求需要唯一標(biāo)識,服務(wù)器可以不按請求的順序返回多個響應(yīng),由瀏覽器自己把收到的響應(yīng)和請求對應(yīng)起來??梢?,HTTP 2.0進(jìn)一步提高了傳輸效率,因為瀏覽器發(fā)出一個請求后,不必等待響應(yīng),就可以繼續(xù)發(fā)下一個請求。
HTTP 3.0為了進(jìn)一步提高速度,將拋棄TCP協(xié)議,改為使用無需創(chuàng)建連接的UDP協(xié)議,目前HTTP 3.0仍然處于實驗階段。
小結(jié)
使用B/S架構(gòu)時,總是通過HTTP協(xié)議實現(xiàn)通信;
Web開發(fā)通常是指開發(fā)服務(wù)器端的Web應(yīng)用程序。