使用Socket進行HTTP請求與報文講解

猛猛的小盆友發表於2019-01-05

目錄
一、前言
二、什麼是Socket
三、如何使用Socket進行http請求
1、建立socket連線
2、http協議請求和響應格式解析
3、進行http請求
四、寫在最後

一、前言

本篇文章是為講述okhttp原始碼做一個鋪墊,主要是簡單講述一下socket的使用,因為在okhttp中網路通訊使用的便是socket。但這篇文章不會涉及okhttp,會簡單闡述下socket,然後用程式碼進行連線後http通訊,話不多說,開始幹!

二、什麼是Socket

回答這個問題前我們要先看下TCP/IP四層模型,想必這個圖大家都有見過,下面就解釋下這四層分別的表現形式是什麼(理論解釋比較讓人摸不著頭腦,所以這裡以其表現形式來闡述)

  1. 網路介面層:主要表現為識別mac間位元流的傳輸
  2. 網路層:表現為IP協議
  3. 傳輸層:表現為TCP、UDP
  4. 應用層:表現為Http、Https、RTSP等(這裡的協議比較多,我們經常使用的http協議就屬於應用層)

Tip:順便說下TCP和UDP的區別。TCP提供可靠的通訊傳輸,類似於打電話,需要等待另一方的接聽,才能進行真正的通訊;而UDP則不是可靠的,類似發簡訊,只將資訊發出,至於對方有沒收到,這個就不關心了。

TCP/IP四層模型

而我們關心的socket是什麼呢?socket其實是TCP連線的抽象,利用socket進行TCP的連線(這個解釋可能比較片面,但個人覺得是最為直觀的解釋,畢竟全面的解釋比較晦澀難懂)

二、如何使用Socket進行http請求

1、建立socket連線

在java中使用socket,其實非常的簡單。如果只是需要一個普通的socket,只需通過如下程式碼,便可以建立一個socket連線

Socket socket = new Socket(“ip或域名”, 埠);
複製程式碼

如果想建立一個sslSocket,用於https的通訊(例如:www.baidu.com)只需要通過sslSocketFactory進行建立sslSocket即可。程式碼如下:

Socket socket = SSLSocketFactory.getDefault().createSocket("www.baidu.com", 443);
複製程式碼

2、http協議請求和響應格式解析

在使用socket進行發起請求前,我們要先來了解下http協議。簡單一點的理解,http協議其實就是發起一個按照格式約定的字串,伺服器響應一串按格式組裝的資料。 這裡不使用教科書式的資料格式,我們使用從"Restlet Client"發起一次請求,觀察其請求報文和響應報文來進行講解。

Tip:Restlet Client是一個api請求工具,日常開發中也可以用來向伺服器發起請求,獲取資料結構方便除錯。可以在chrome的應用商店下載。

這裡使用的api是高德的天氣預報介面,點選了“send”後,獲取到請求報文和響應報文,如下圖所示

使用Socket進行HTTP請求與報文講解

我們先單獨說下這次請求的請求報文(第二個紅框中內容,如下所示)

GET /v3/weather/weatherInfo?city=%E9%95%BF%E6%B2%99&key=13cb58f5884f9749287abbead9c658f2 HTTP/1.1
Host: restapi.amap.com

複製程式碼

(1)第一行為發起請求資訊,其格式為:

  1. 發起的請求形式(這裡使用的是GET,如果為POST的話,這裡便為POST),即這裡的“GET”
  2. 一個空格,即“ ”
  3. 請求的路徑(不包括域名,因為域名已經在建立socket連線時確定,如果為GET請求,則引數追加在後面以“?”隔開;如果為POST請求,則請求引數會在body中增加,具體見第四小點),即這裡的“/v3/weather/weatherInfo?city=%E9%95%BF%E6%B2%99&key=13cb58f5884f9749287abbead9c658f2”
  4. 一個空格,即“ ”
  5. http請求的版本,即“HTTP/1.1”
  6. \r\n,此處沒有顯示出來,但是自己在組裝報文時,需要增加這個表示一行已經結束

(2)第二行的格式為:

  1. host欄位名,即“Host”
  2. 一個冒號加一個空格,即“: ”(敲黑板!!冒號後面有一個空格,這個在組裝請求報文時,尤為重要)
  3. host的內容,即“restapi.amap.com
  4. \r\n,此處沒有顯示出來,但是自己在組裝報文時,需要增加這個表示一行已經結束

tip:這裡其實是請求頭部,如果頭部引數有多個的話,就按照這種格式進行拼裝。例如還有一個“Connection為keep-alive”的頭部引數,則以“Connection: keep-alive\r\n”的形式寫入輸出流中,具體會在後面的例子中展示。

(3)第三行的格式為:(沒想到吧!!!這裡有第三行)

  1. \r\n,此處沒有顯示出來,但是自己在組裝報文時,需要增加這個表示頭部引數已寫完

(4)如果為POST請求,接下來還需要進行body引數的拼裝,這裡以form表單為例,拼接上面介面的引數。規則就是“鍵=值”,鍵值對間用“&”隔開。

city=長沙&key=13cb58f5884f9749287abbead9c658f2
複製程式碼

至此一個請求報文便拼裝完畢,將其用輸出流寫出即可獲得伺服器的響應報文。

我們接著說下這次請求的響應報文

HTTP/1.1 200 OK
Server: Tengine
Date: Sun, 06 May 2018 08:22:10 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 445
Connection: close
X-Powered-By: ring/1.0.0
gsid: 010185222147152559493030300162313551811
sc: 0.013
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,key,x-biz,x-info,platinfo,encr,enginever,gzipped,poiid

{"status":"1","count":"2","info":"OK","infocode":"10000","lives":[{"province":"湖南","city":"長沙市","adcode":"430100","weather":"陣雨","temperature":"25","winddirection":"東北","windpower":"7","humidity":"78","reporttime":"2018-05-06 16:00:00"},{"province":"湖南","city":"長沙縣","adcode":"430121","weather":"陣雨","temperature":"25","winddirection":"東北","windpower":"7","humidity":"78","reporttime":"2018-05-06 16:00:00"}]}
複製程式碼

(1)第一行為響應狀態,格式為

  1. http的版本資訊,即“HTTP/1.1”
  2. 一個空格,即“ ”
  3. 狀態碼,即“200”
  4. 一個空格,即“ ”
  5. 狀態,即“OK”
  6. \r\n,此處沒有顯示出來,但是解析響應報文時,需要通過這兩個字元進行判斷是否一行結束

(2)第二行至第十二行為響應頭,每一行的格式為

  1. 頭名稱,即“Server”
  2. 一個冒號加一個空格,即“: ”
  3. 頭部引數值,即“Tengine”
  4. \r\n,此處沒有顯示出來,但是解析響應報文時,需要通過這兩個字元進行判斷是否一行結束

(3)第十三行,格式為

  1. \r\n,用於區分頭部引數和內容的區分

(4)第十四行為響應內容,格式為

這裡便是介面給我們的資料,即此處給到我們的天氣json資料,而此處json的長度為頭部中有一個引數為“Content-Length”決定的,例子中內容的長度為445。值得一提的是,有些介面返回的頭部引數並沒有“Content-Length”這一頭部引數,而是返回了“Transfer-Encoding: chunked”這樣的頭部引數,則表明是以塊的形式給到我們資料。 塊的形式會以如下格式,第一行的“10\r\n”表明接下來的一行會有10個位元組的內容,第二行便是10位元組的內容,同樣以“\r\n”結束一行(\r\n這兩個字元不算在內容長度中),每一塊的格式都按這樣的形式,如果遇到“0\r\n\r\n”就說明內容結束。

10\r\n      //(注意!!!這裡是10是16進位制,即如果進行內容讀取需要將其進行做10進位制的轉換)
10位元組長度的內容\r\n

//結束格式
0\r\n
\r\n
複製程式碼

至此響應報文解析完畢。

3、進行http請求

逼逼叨逼逼叨了這麼久,很多小夥伴已經很迫不及待的想知道怎麼請求和獲取響應了。我們這裡便直接上程式碼,程式碼很簡單,並沒有什麼知識難點。

public class MySocket {

    public static void main(String[] args) throws IOException {
        //如果需要進行https的請求只需要換成如下一句(https的預設埠為443,http預設埠為80)
        //Socket socket = SSLSocketFactory.getDefault().createSocket("xxx", 443);
        Socket socket = new Socket("restapi.amap.com", 80);

        //獲取輸入流,即從伺服器獲取的資料
        final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        //獲取輸出流,即我們寫出給伺服器的資料
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

        //使用一個執行緒來進行讀取伺服器的響應
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    String line = null;
                    try {
                        while ((line = bufferedReader.readLine()) != null) {
                            System.out.println("recv : " + line);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        bufferedWriter.write("GET /v3/weather/weatherInfo?city=%E9%95%BF%E6%B2%99&key=13cb58f5884f9749287abbead9c658f2 HTTP/1.1\r\n");
        bufferedWriter.write("Host: restapi.amap.com\r\n\r\n");
        bufferedWriter.flush();

    }

}
複製程式碼

跑起來後會看到控制檯輸出如下資訊,這個時候我們就可以按照第二小結中的格式進行解析到一個模型中,最終返回給UI或是邏輯層去使用。

執行結果

四、寫在最後

OkHttp中使用socket連線後,進行處理響應便是這樣的處理邏輯。只是它還有對socket的複用,連線進行限制之類的優化處理,這個在後面的文章中會進行剖析。如果您期待這樣的剖析之旅的話,給個“❤️”加個關注吧!文章中並沒有對頭部引數進行說明其含義,這裡也不打算給出,其實百度一下或google都有很多,需要的時候進行搜查一下即可。記住我,我是猛猛的小盆友?,如果我有理解錯誤或是寫的晦澀難懂的地方請與我聯絡討論,共同進步。

如果需要更多的交流與探討,可以通過以下微信二維碼加小盆友好友。

使用Socket進行HTTP請求與報文講解

相關文章