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

期刊列表

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


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

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

什麼是HTTP

HTTP -> Hyper Text Transfer Protocol(超文本傳輸協議),它是基於TCP/IP協議的一種無狀態鏈接程序員

特性

無狀態

無狀態是指,在標準狀況下,客戶端的發出每一次請求,都是獨立的,服務器並不能直接經過標準http協議自己得到用戶對話的上下文。github

這裏,可能不少人會有疑問,咱們平時使用的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畫的-_-

注: 本文原創,若但願轉載請聯繫做者

參考:

菜鳥教程

百度百科

相關文章
相關標籤/搜索