天天看點

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

期刊清單

關于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

首行也叫請求行,是用來告訴伺服器,用戶端調用的請求類型,請求資源路徑,請求協定類型

請求類型也就是我們常說的(面試官總問的)GET,POST等等發送的位置,它位于請求的最開始

請求資源路徑是提供給伺服器内部的尋址路徑,用來告訴伺服器用戶端希望通路什麼資源,在浏覽器中通路 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:8056和127.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, )

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

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

var result = Darwin.connect(socketFD, pointer, socklen_t(MemoryLayout.size(ofValue: sock4)))
guard result != - 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($)})

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

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

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

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

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

}
複制代碼
           

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

總結

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

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

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

參考:

菜鳥教程

百度百科