使用socket實(shí)現(xiàn)http服務(wù)器和發(fā)送http請(qǐng)求

注:本文并沒有實(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è)問題

  1. 為什么服務(wù)端返回響應(yīng)后沒有關(guān)閉socket連接?
  2. 客戶端讀取響應(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功能避免了建立或者重新建立連接。

normal和keep-alive對(duì)比

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)閉的.

P:引用自HTTP長(zhǎng)連接 && keep-alive

啟用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)

響應(yīng)結(jié)構(gòu)

HTTP響應(yīng)有4個(gè)部分,從上到下分別是狀態(tài)行、響應(yīng)頭、空白行響應(yīng)體

1.狀態(tài)行:描述了響應(yīng)的狀態(tài)。

  1. 響應(yīng)頭:它們包含了更多關(guān)于響應(yīng)的信息。比如:頭部可以指定認(rèn)為響應(yīng)過期的過期日期,或者是指定用來(lái)給用戶安全的傳輸實(shí)體內(nèi)容的編碼格式。
  2. 空白行:一般文章都沒有提到這個(gè)空白行,但是實(shí)際上不可缺少,標(biāo)志著響應(yīng)頭的結(jié)束。
  3. 響應(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)文是這樣的:

分塊編碼報(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容