注:本文并沒有實(shí)現(xiàn)完整的http服務(wù)器和http網(wǎng)絡(luò)請(qǐng)求,主要是提供思路
先上一個(gè)簡(jiǎn)單的get請(qǐng)求和響應(yīng)的代碼,代碼使用idea測(cè)試過,使用時(shí)先運(yùn)行服務(wù)端,然后再運(yùn)行客戶端測(cè)試,服務(wù)端測(cè)試也可以通過使用瀏覽器輸入localhost:5050進(jìn)行測(cè)試
服務(wù)端
public class LiteHttpServerTest {
public static void main(String[] args) {
listen(5050);
}
public static void listen(int port) {
new Thread(() -> {
try {
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
Socket accept = serverSocket.accept();
new ServerSocketHandler(accept).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
public static class ServerSocketHandler extends Thread {
private final Socket socket;
public ServerSocketHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
super.run();
try {
InputStream is = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
String line = bufferedReader.readLine();
while (line != null && !"".equals(line)) {
System.out.println(line);
line = bufferedReader.readLine();
}
String body = "hello word";
StringBuilder response = new StringBuilder();
response.append("HTTP/1.1 200 OK\r\n")
.append("Content-Length: ").append(body.getBytes().length).append("\r\n")
.append("Content-Type: text/plain; charset-utf-8\r\n")
.append("\r\n")
.append(body).append("\r\n");
OutputStream outputStream = socket.getOutputStream();
outputStream.write(response.toString().getBytes());
outputStream.flush();
//注意這里并沒有將socket關(guān)閉
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客戶端
public class LiteHttpClientTest {
public static void main(String[] args) {
sendGet(5050);
}
public static void sendGet(int port) {
new Thread(() -> {
Socket socket = new Socket();
try {
socket.connect(new InetSocketAddress("localhost", port));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("GET / HTTP/1.1\r\n");
bw.write("Host: 127.0.0.1\r\n");
bw.write("\r\n");
bw.flush();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while ((line = br.readLine()) != null) {//由于服務(wù)器端沒有關(guān)閉socket,會(huì)一直阻塞在這里
System.out.println(line);
}
br.close();
bw.close();
socket.close();
System.out.println("close");//發(fā)現(xiàn)代碼沒有走到這里
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
好了,代碼很簡(jiǎn)單,但是這里有兩個(gè)問題
- 為什么服務(wù)端返回響應(yīng)后沒有關(guān)閉socket連接?
- 客戶端讀取響應(yīng)一直阻塞住的問題怎么解決(如何知道響應(yīng)結(jié)束)?
1、為什么服務(wù)端返回響應(yīng)后沒有關(guān)閉socket連接?
我們學(xué)http協(xié)議的時(shí)候是不是記得http協(xié)議是無(wú)連接、無(wú)狀態(tài)的,所以總是以為http服務(wù)器響應(yīng)完后就會(huì)關(guān)閉連接.
無(wú)連接:無(wú)連接是限制每個(gè)連接只有一個(gè)請(qǐng)求的意思。在服務(wù)器處理完客戶的請(qǐng)求,并收到客戶的反應(yīng),即斷開。通過這種方式可以節(jié)省傳輸時(shí)間。
在HTTP1.0默認(rèn)確實(shí)是這樣的,但是HTTP請(qǐng)求實(shí)際上是有Keep-Alive模式的
什么是Keep-Alive模式?
我們知道HTTP協(xié)議采用“請(qǐng)求-應(yīng)答”模式,當(dāng)使用普通模式,即非Keep-Alive模式時(shí),每個(gè)請(qǐng)求/應(yīng)答客戶和服務(wù)器都要新建一個(gè)連接,完成之后立即斷開連接(HTTP協(xié)議為無(wú)連接的協(xié)議);當(dāng)使用Keep-Alive模式(又稱持久連接、連接重用)時(shí),Keep-Alive功能使客戶端到服務(wù)器端的連接持續(xù)有效,當(dāng)出現(xiàn)對(duì)服務(wù)器的后繼請(qǐng)求時(shí),Keep-Alive功能避免了建立或者重新建立連接。

http 1.0中默認(rèn)是關(guān)閉的,需要在http頭加入"Connection: Keep-Alive",才能啟用Keep-Alive;http 1.1中默認(rèn)啟用Keep-Alive,如果加入"Connection: close ",才關(guān)閉。目前大部分瀏覽器都是用http1.1協(xié)議,也就是說默認(rèn)都會(huì)發(fā)起Keep-Alive的連接請(qǐng)求了,所以我提供的服務(wù)端代碼是沒有立馬關(guān)閉的.
啟用Keep-Alive模式的有點(diǎn)很明顯,假設(shè)一個(gè)網(wǎng)頁(yè)有100張小圖標(biāo),那么用普通模式就要建立100+個(gè)連接,而Keep-Alive只要一個(gè),效率提升明顯.
Keep-Alive模式下服務(wù)端什么時(shí)候關(guān)閉socket?
雖然是Keep-Alive模式,但是服務(wù)端也不會(huì)一直保持長(zhǎng)連接,它會(huì)有超時(shí)機(jī)制和請(qǐng)求次數(shù)限制.在HTTP首部的Connection: Keep-alive中,Keep-Alive: timeout=20,表示這個(gè)TCP通道可以保持20秒。max=XXX,表示這個(gè)長(zhǎng)連接最多接收XXX次請(qǐng)求就斷開。如果在客戶端,即發(fā)請(qǐng)求的時(shí)候,沒有定義超時(shí)時(shí)間。服務(wù)端也會(huì)發(fā)起四次揮手的。TCP還有心跳檢查機(jī)制來(lái)當(dāng)前連接是否活著。
2、客戶端讀取響應(yīng)一直阻塞住的問題怎么解決(如何知道響應(yīng)結(jié)束)?
在解決這個(gè)問題前我們先來(lái)看看HTTP響應(yīng)的結(jié)構(gòu)

HTTP響應(yīng)有4個(gè)部分,從上到下分別是
狀態(tài)行、響應(yīng)頭、空白行、響應(yīng)體
1.狀態(tài)行:描述了響應(yīng)的狀態(tài)。
- 響應(yīng)頭:它們包含了更多關(guān)于響應(yīng)的信息。比如:頭部可以指定認(rèn)為響應(yīng)過期的過期日期,或者是指定用來(lái)給用戶安全的傳輸實(shí)體內(nèi)容的編碼格式。
- 空白行:一般文章都沒有提到這個(gè)空白行,但是實(shí)際上不可缺少,標(biāo)志著響應(yīng)頭的結(jié)束。
- 響應(yīng)體:它包含了響應(yīng)的內(nèi)容。它可以包含HTML代碼、圖片等等。
回到我們的問題,如何知道響應(yīng)結(jié)束了?這里分為兩種情況
1.Content-Length
我們可以通過響應(yīng)頭中的Content-Length來(lái)獲取響應(yīng)體的長(zhǎng)度,當(dāng)我們讀取到空白行后讀取Content-Length個(gè)字節(jié)長(zhǎng)度就說明讀完了響應(yīng)了。
2.分塊編碼
當(dāng)響應(yīng)頭里面沒有寶行Content-Length時(shí)怎么辦?這個(gè)時(shí)候就要采取分塊編碼了,服務(wù)器必須返回兩種之一,當(dāng)響應(yīng)頭中包含Transfer-Encoding: chunked即表示是分塊編碼。
分塊編碼的報(bào)文是這樣的:

每個(gè)分塊包含一個(gè)長(zhǎng)度值(十六進(jìn)制,字節(jié)數(shù))和該分塊的數(shù)據(jù)。 <CR><LF>用于區(qū)隔長(zhǎng)度值和數(shù)據(jù)。長(zhǎng)度值不包含分塊中的任何 <CR><LF>序列。最后一個(gè)分塊,用長(zhǎng)度值0來(lái)表示結(jié)束。注意報(bào)文首部包含一個(gè) Trailer:Content-MD5, 所以在緊跟著最后一個(gè)報(bào)文結(jié)束之后,就是一個(gè)拖掛。其他如, Content-Length, Trailer, Transfer-Encoding也可以作為拖掛。
好了,最后推薦一個(gè)http服務(wù)器和http請(qǐng)求實(shí)現(xiàn)的庫(kù)AndroidAsync