關於Socket,看我這幾篇就夠了(二)之HTTP

chouheiwa發表於2019-01-29

期刊列表

關於Socket,看我這幾篇就夠了(一)


在上一篇中,我們初步的講述了socket的定義,以及socket中的TCP的簡單用法。

這篇我們主要講的是HTTP相關的東西。

什麼是HTTP

HTTP -> Hyper Text Transfer Protocol(超文字傳輸協議),它是基於TCP/IP協議的一種無狀態連線

特性

無狀態

無狀態是指,在標準情況下,客戶端的發出每一次請求,都是獨立的,伺服器並不能直接通過標準http協議本身獲得使用者對話的上下文。

這裡,可能很多人會有疑問,我們平時使用的http不是這樣的啊,伺服器能識別我們請求的身份啊,要不免登入怎麼做啊?

所以額外解釋下,我們說的這些狀態,如cookie/session是由伺服器與客戶端雙方約定好,每次請求的時候,客戶端填寫,伺服器獲取到後查詢自身記錄(資料庫、記憶體),為客戶端確定身份,並返回對應的值。

從另一方面也可說,這個特性和http協議本身無關,因為伺服器不是從這個協議本身獲取對應的狀態

無狀態也可這樣理解: 從同一客戶端連續發出兩次http請求到伺服器,伺服器無法從http協議本身上獲取兩次請求之間的關係

無連線

無連線指的是,伺服器在響應客戶端的請求後,就主動斷開連線,不繼續維持連線

結構

http 是超文字傳輸協議,顧名思義,傳輸的是一定格式的文字,所以,我們接下來講述一下這個協議的格式

在http中,一個很重要的分割符就是 CRLF(Carriage-Return Line-Feed) 也就是 \r 回車符 + \n 換行符,它是用來作為識別的字元

請求 Request

請求格式

上圖為請求格式

請求行

GET / HTTP/1.1\r\n

首行也叫請求行,是用來告訴伺服器,客戶端呼叫的請求型別請求資源路徑請求協議型別

請求型別也就是我們常說的(面試官總問的)GETPOST等等傳送的位置,它位於請求的最開始

請求資源路徑是提供給伺服器內部的定址路徑,用來告訴伺服器客戶端希望訪問什麼資源,在瀏覽器中訪問 www.jianshu.com/p/6cfbc63f3… (用簡書做一波示範了),則我們請求的就是 /p/6cfbc63f3a2b

請求協議型別目前使用最多的是HTTP/1.1說不定在不遠的未來,將會被HTTP/2.0所取代

注:

  1. 所使用連結為https連結,但是其內容與http一樣,因此使用該連結做為例子,ssl 將會在接下來的幾篇文章中講述

  2. 請求行的不同內容需要用 " "空格符 來做分割

  3. 請求行的結尾需要新增CRLF分割符

請求頭Request Headers

請求行之後,一直到請求體(body),之間的部分,被我們成為請求頭。

請求頭的長度並不固定,我們可以放置無限多的內容到請求頭中。

但是請求頭的格式是固定的,我們可以把它看做是鍵值對。

格式:

key: value\r\n
複製程式碼

我們通常所說的cookie便是請求頭中的一項

一些常用的http頭的定義與作用: blog.csdn.net/philos3/art…

注:

當所有請求頭都已經結束(即我們要傳送body)的時候,我們需要額外增加一個空行(CRLF) 告訴伺服器請求頭已經結束

請求體Request Body

如果說header我們沒有那麼多的使用機會的話,那麼body則是幾乎每個開發人員都必須接觸的了。

通常,當我們進行 POST 請求的時候,我們上傳的引數就在這裡了。

伺服器是如何獲得我們上傳的完整Body呢?換句話說,就是伺服器怎麼知道我們的body已經傳輸完畢了呢?

我們想一下,如果我們在需要實現這個協議的時候,我們會怎麼做?

  • 可以約定特殊位元組作為終止字元,當讀取到指定字元時,即認為讀取完畢

  • 傳送方肯定知道要傳送的資料的大小,直接告訴接收方,接收方只需要在收到指定大小的資料的時候就可以停止接收了

  • 傳送方也不知道資料的大小(或者他需要花很大成本才能知道資料的大小),就先告訴接收方,我現在也不知道有多少,等傳送的時候看,真正傳送的時候告訴接收方,"我這次要傳送多少",最後告訴接收方,"我發完了",接收方以此停止接收。‘

也許你會有別的想法,那恭喜你,你可以自己實現類似的接收方法了。

目前,伺服器是依靠上述三種方法接收的:

  • 約定特殊位元組:

客戶端在傳送完資料後,就呼叫關閉socket連線,伺服器在收到關閉請求後開始解析資料,並返回結果,最後關閉連線

  • 確定資料大小:

客戶端在請求頭中給定欄位 Content-Length,伺服器解析到對應資料後接受body,當body資料達到指定長度後,伺服器開始解析資料,並返回結果

  • 不確定資料大小(Http/1.1 可用)

客戶端在請求頭中給定頭 Transfer-Encoding: chunked,隨後開始準備傳送資料

傳送的每段資料都有特定的格式,

格式為:

  1. 長度行:

每段資料的開頭的文字為該段真實傳送的資料的16進位制長度CRLF分割符

  1. 資料行:

真實傳送的資料CRLF分割符

例:

12\r\n // 長度行 16進位制下的12就是10進位制下的 18
It is a chunk data\r\n // 資料行 CRLF 為分割符
複製程式碼

結尾段:

用以告訴伺服器資料傳送完成,開始解析或儲存資料。

結尾段格式固定

0\r\n
\r\n 
複製程式碼

目前,客戶端使用這種方法的不多。

到這裡,如何告訴伺服器應該接收多少資料的部分已經完成了

接下來就到了,告訴伺服器,資料究竟是什麼了

同樣也是頭部定義:Content-Type

Content-Type介紹: blog.csdn.net/qq_23994787…

到這裡,Request的基本格式已經講完

響應 Response

響應格式

相應結構

其實Response 和 Request 從協議上分析,他們是一樣的,但是他們是對Http協議中文字協議的不同的實現。

響應行

HTTP/1.1 200 OK\r\n

首行也叫響應行,是用來告訴客戶端當前請求的處理狀況的,由請求協議型別伺服器狀態碼對應狀態描述構成

請求協議型別 是用來告訴客戶端,伺服器採用的協議是什麼,以便於客戶端接下來的處理。

伺服器狀態碼 是一個很重要的返回值,它是用來通知伺服器對本次客戶端請求的處理結果。

狀態碼非常多,但是對於我們開發一般用到的是如下幾個狀態碼

狀態碼 對應狀態描述 含義 客戶對應操作
200 OK 標誌著請求被伺服器成功處理
400 Bad Request 標誌著客戶端請求出現了問題,伺服器無法識別,客戶端修改後伺服器才能進行處理 修改request引數
401 Unauthorized 當前請求需要校驗許可權,客戶端需要在下次請求頭部提交對應許可權資訊 修改Header頭並提交對應資訊
403 Forbidden 當前請求被伺服器拒絕執行(防火牆阻止或其他原因) 等待一段時間後再次發起,無其他解決辦法
404 Not Found 服務無法找到對應資源(最為常見的錯誤碼) 修改Request中的資源請求路徑
405 Method Not Allowed 客戶端當前請求方法不被允許 修改請求方法
408 Request Timeout 客戶端請求超時(伺服器沒有在允許的時間內解析出全部的Request) 重新發起請求
500 Internal Server Error 伺服器自身錯誤(可能是未對操作過程中的異常進行處理) 聯絡後臺開發人員解決(誰要是說這是客戶端問題就去找他理論)

完整錯誤碼請參照網址: baike.baidu.com/item/HTTP狀態…

響應頭Response Headers響應體Response Body

這些內容與Request中對應部分並無區別,顧不贅述了


我們已經從特性與結構兩部分講述了Http相關的屬性,到這裡這篇文章的主要內容基本上算是結束了,接下來我要講講一些其他的http相關的知識

跨域

作為移動端開發人員,我們對這個的瞭解不是很多,也幾乎用不到,但是我這裡還是需要說明。因為現在已經到了前端的時代,萬一我們以後需要踏足前端,瞭解跨域,至少能為我們解決不少事情。

這篇文章不會詳細講解如何解決跨域,只會講解跨域形成的原因

什麼是 跨域

在講跨域的時候,需要先講什麼是

什麼是域

在上一課講解socket的過程中,我們已經發現了,想建立一個TCP/IP的連線需要知道至少兩個事情

  1. 對方的地址(host)
  2. 對方的門牌號(port)

我們只有依靠這兩個才能建立TCP/IP 的連線,其中host標明我們該怎麼找到對方,port表示,我們應該連線具體的那個埠。

伺服器應用是一直在監聽著這個埠的,這樣才能保證在有連線進入的時候,伺服器直接響應對應的資訊

向上聊聊吧,我們通常講的伺服器指的是伺服器應用,比如常說Tomcat,Apache 等等,他們啟動的時候一般會繫結好一個指定的埠(通常不會同時繫結兩個埠)。所以呢,作為客戶端,就可以用host+port來確定一個指定的伺服器應用

由此,的概念就此生成,就是host + port

舉個例子: http://127.0.0.1:8056/

這個網址所屬的域就是127.0.0.1+8056 也可以寫成127.0.0.1:8056

這時候有人就會問了,那localhost:8056127.0.0.1:8056是同一域麼,他們實際是等價的啊。

他們不屬於同一域,規定的很死,因為他們的host的表示不同,所以不是。

跨域

我們已經知道域了,跨域也就出現了,就是一個訪問另一個

我們從http協議中可以發現,伺服器並不任何強制規定域,也就是說,伺服器並不在乎這個訪問是從哪個域訪問過來的,同時,作為客戶端,我們也並沒有域這麼一說。

那麼跨域究竟是什麼呢?

這就要說跨域的來源了,我們日常訪問的網站,它實際上就是html程式碼,伺服器將程式碼下發到了瀏覽器,由瀏覽器渲染並展示給我們。

開發瀏覽器的程式設計師在開發的時候,也不知道這個網頁究竟要做什麼,但是他們為了安全著想,不能給網頁和客戶端(socket)同樣的許可權,因此他們限制了某些操作,在本的網頁的某些請求操作在對方的伺服器沒有新增允許該的訪問許可權的時候,訪問操作將不會被執行,這些操作會對瀏覽器的安全性有很大到的影響。

所以跨域就此產生。

跨域從頭到尾都只是一個客戶端的操作行為,從某種角度上說,它與伺服器毫無關係,因為伺服器無法得知某次請求是否來自於某一網頁(在客戶端不配合的情況下),也就無從禁止了

對於我們移動端,瞭解跨域後我們至少可以說,跨域與我們無關-_-

socket實現簡單的http請求

事實上,一篇文章如果沒有程式碼上的支撐,只是純理念上的闡述,終究還是感覺缺點什麼,本文將在上篇文章程式碼的基礎上做些小的改進。

這裡就以菜鳥教程網的http教程作為本篇文章的測試(www.runoob.com/http/http-t…)(ip:47.246.3.228:80)

// MARK: - Create 建立
let socketFD = Darwin.socket(AF_INET, SOCK_STREAM, 0)

func converIPToUInt32(a: Int, b: Int, c: Int, d: Int) -> in_addr {
    return Darwin.in_addr(s_addr: __uint32_t((a << 0) | (b << 8) | (c << 16) | (d << 24)))
}
// MARK: - Connect 連線
var sock4: sockaddr_in = sockaddr_in()

sock4.sin_len = __uint8_t(MemoryLayout.size(ofValue: sock4))
// 將ip轉換成UInt32
sock4.sin_addr = converIPToUInt32(a: 47, b: 246, c: 3, d: 228)
// 因記憶體位元組和網路通訊位元組相反,顧我們需要交換大小端 我們連線的埠是80
sock4.sin_port = CFSwapInt16HostToBig(80)
// 設定sin_family 為 AF_INET表示著這個為IPv4 連線
sock4.sin_family = sa_family_t(AF_INET)
// Swift 中指標強轉比OC要複雜
let pointer: UnsafePointer<sockaddr> = withUnsafePointer(to: &sock4, {$0.withMemoryRebound(to: sockaddr.self, capacity: 1, {$0})})

var result = Darwin.connect(socketFD, pointer, socklen_t(MemoryLayout.size(ofValue: sock4)))
guard result != -1 else {
    fatalError("Error in connect() function code is \(errno)")
}
// 組裝文字協議 訪問 菜鳥教程Http教程
let sendMessage = "GET /http/http-tutorial.html HTTP/1.1\r\n"
    + "Host: www.runoob.com\r\n"
    + "Connection: keep-alive\r\n"
    + "USer-Agent: Socket-Client\r\n\r\n"
//轉換成二進位制
guard let data = sendMessage.data(using: .utf8) else {
    fatalError("Error occur when transfer to data")
}
// 轉換指標
let dataPointer = data.withUnsafeBytes({UnsafeRawPointer($0)})

let status = Darwin.write(socketFD, dataPointer, data.count)

guard status != -1 else {
    fatalError("Error in write() function code is \(errno)")
}
// 設定32Kb位元組儲存防止溢位
let readData = Data(count: 64 * 1024)

let readPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)})
// 記錄當前讀取多少位元組
var currentRead = 0

while true {
    // 讀取socket資料
    let result = Darwin.read(socketFD, readPointer + currentRead, readData.count - currentRead)

    guard result >= 0 else {
        fatalError("Error in read() function code is \(errno)")
    }
    // 這裡睡眠是減少呼叫頻率
    sleep(2)
    if result == 0 {
        print("無新資料")
        continue
    }
    // 記錄最新讀取資料
    currentRead += result
    // 列印
    print(String(data: readData, encoding: .utf8) ?? "")

}
複製程式碼

對應程式碼例子已經放在github上,地址:github.com/chouheiwa/S…

總結

越學習越覺得自己懂得越少,我們現在走的每一步,都是在學習。

題外話:畫圖好費勁啊,都是用PPT畫的-_-

注: 本文原創,若希望轉載請聯絡作者

參考:

菜鳥教程

百度百科

相關文章