在上一篇中,我們初步的講述了socket的定義,以及socket中的TCP的簡單用法。
這篇我們主要講的是HTTP相關的東西。
什么是HTTP
HTTP -> Hyper Text Transfer Protocol(超文本傳輸協(xié)議),它是基于TCP/IP協(xié)議的一種無狀態(tài)連接
特性
無狀態(tài)
無狀態(tài)是指,在標準情況下,客戶端的發(fā)出每一次請求,都是獨立的,服務器并不能直接通過標準http協(xié)議本身獲得用戶對話的上下文。
這里,可能很多人會有疑問,我們平時使用的http不是這樣的啊,服務器能識別我們請求的身份啊,要不免登錄怎么做啊?
所以額外解釋下,我們說的這些狀態(tài),如cookie/session是由服務器與客戶端雙方約定好,每次請求的時候,客戶端填寫,服務器獲取到后查詢自身記錄(數據庫、內存),為客戶端確定身份,并返回對應的值。
從另一方面也可說,這個特性和http協(xié)議本身無關,因為服務器不是從這個協(xié)議本身獲取對應的狀態(tài)。
無狀態(tài)也可這樣理解: 從同一客戶端連續(xù)發(fā)出兩次http請求到服務器,服務器無法從http協(xié)議本身上獲取兩次請求之間的關系
無連接
無連接指的是,服務器在響應客戶端的請求后,就主動斷開連接,不繼續(xù)維持連接
結構
http 是超文本傳輸協(xié)議,顧名思義,傳輸的是一定格式的文本,所以,我們接下來講述一下這個協(xié)議的格式
在http中,一個很重要的分割符就是 CRLF(Carriage-Return Line-Feed) 也就是 \r 回車符 + \n 換行符,它是用來作為識別的字符
請求 Request
上圖為請求格式
請求行
GET / HTTP/1.1\r\n
首行也叫請求行,是用來告訴服務器,客戶端調用的請求類型,請求資源路徑,請求協(xié)議類型
請求類型也就是我們常說的(面試官總問的)GET,POST等等發(fā)送的位置,它位于請求的最開始
請求資源路徑是提供給服務器內部的尋址路徑,用來告訴服務器客戶端希望訪問什么資源,在瀏覽器中訪問 http://www.itdecent.cn/p/6cfbc63f3a2b (用簡書做一波示范了),則我們請求的就是 /p/6cfbc63f3a2b
請求協(xié)議類型目前使用最多的是HTTP/1.1說不定在不遠的未來,將會被HTTP/2.0所取代
注:
所使用鏈接為https鏈接,但是其內容與http一樣,因此使用該鏈接做為例子,ssl 將會在接下來的幾篇文章中講述
請求行的不同內容需要用 " "空格符 來做分割
請求行的結尾需要添加CRLF分割符
請求頭Request Headers
請求行之后,一直到請求體(body),之間的部分,被我們成為請求頭。
請求頭的長度并不固定,我們可以放置無限多的內容到請求頭中。
但是請求頭的格式是固定的,我們可以把它看做是鍵值對。
格式:
key: value\r\n
我們通常所說的cookie便是請求頭中的一項
一些常用的http頭的定義與作用: https://blog.csdn.net/philos3/article/details/76946029
注:
當所有請求頭都已經結束(即我們要發(fā)送body)的時候,我們需要額外增加一個空行(CRLF) 告訴服務器請求頭已經結束
請求體Request Body
如果說header我們沒有那么多的使用機會的話,那么body則是幾乎每個開發(fā)人員都必須接觸的了。
通常,當我們進行 POST 請求的時候,我們上傳的參數就在這里了。
服務器是如何獲得我們上傳的完整Body呢?換句話說,就是服務器怎么知道我們的body已經傳輸完畢了呢?
我們想一下,如果我們在需要實現這個協(xié)議的時候,我們會怎么做?
可以約定特殊字節(jié)作為終止字符,當讀取到指定字符時,即認為讀取完畢
發(fā)送方肯定知道要發(fā)送的數據的大小,直接告訴接收方,接收方只需要在收到指定大小的數據的時候就可以停止接收了
發(fā)送方也不知道數據的大小(或者他需要花很大成本才能知道數據的大小),就先告訴接收方,我現在也不知道有多少,等發(fā)送的時候看,真正發(fā)送的時候告訴接收方,"我這次要發(fā)送多少",最后告訴接收方,"我發(fā)完了",接收方以此停止接收?!?/p>
也許你會有別的想法,那恭喜你,你可以自己實現類似的接收方法了。
目前,服務器是依靠上述三種方法接收的:
- 約定特殊字節(jié):
客戶端在發(fā)送完數據后,就調用關閉socket連接,服務器在收到關閉請求后開始解析數據,并返回結果,最后關閉連接
- 確定數據大小:
客戶端在請求頭中給定字段 Content-Length,服務器解析到對應數據后接受body,當body數據達到指定長度后,服務器開始解析數據,并返回結果
- 不確定數據大小(Http/1.1 可用)
客戶端在請求頭中給定頭 Transfer-Encoding: chunked,隨后開始準備發(fā)送數據
發(fā)送的每段數據都有特定的格式,
格式為:
- 長度行:
每段數據的開頭的文本為該段真實發(fā)送的數據的16進制長度加CRLF分割符
- 數據行:
真實發(fā)送的數據加CRLF分割符
例:
12\r\n // 長度行 16進制下的12就是10進制下的 18
It is a chunk data\r\n // 數據行 CRLF 為分割符
結尾段:
用以告訴服務器數據發(fā)送完成,開始解析或存儲數據。
結尾段格式固定
0\r\n
\r\n
目前,客戶端使用這種方法的不多。
到這里,如何告訴服務器應該接收多少數據的部分已經完成了
接下來就到了,告訴服務器,數據究竟是什么了
同樣也是頭部定義:Content-Type
Content-Type介紹:
https://blog.csdn.net/qq_23994787/article/details/79044908
到這里,Request的基本格式已經講完
響應 Response
相應結構
其實Response 和 Request 從協(xié)議上分析,他們是一樣的,但是他們是對Http協(xié)議中文本協(xié)議的不同的實現。
響應行
HTTP/1.1 200 OK\r\n
首行也叫響應行,是用來告訴客戶端當前請求的處理狀況的,由請求協(xié)議類型,服務器狀態(tài)碼,對應狀態(tài)描述構成
請求協(xié)議類型 是用來告訴客戶端,服務器采用的協(xié)議是什么,以便于客戶端接下來的處理。
服務器狀態(tài)碼 是一個很重要的返回值,它是用來通知服務器對本次客戶端請求的處理結果。
狀態(tài)碼非常多,但是對于我們開發(fā)一般用到的是如下幾個狀態(tài)碼
| 狀態(tài)碼 | 對應狀態(tài)描述 | 含義 | 客戶對應操作 |
|---|---|---|---|
| 200 | OK | 標志著請求被服務器成功處理 | 無 |
| 400 | Bad Request | 標志著客戶端請求出現了問題,服務器無法識別,客戶端修改后服務器才能進行處理 | 修改request參數 |
| 401 | Unauthorized | 當前請求需要校驗權限,客戶端需要在下次請求頭部提交對應權限信息 | 修改Header頭并提交對應信息 |
| 403 | Forbidden | 當前請求被服務器拒絕執(zhí)行(防火墻阻止或其他原因) | 等待一段時間后再次發(fā)起,無其他解決辦法 |
| 404 | Not Found | 服務無法找到對應資源(最為常見的錯誤碼) | 修改Request中的資源請求路徑 |
| 405 | Method Not Allowed | 客戶端當前請求方法不被允許 | 修改請求方法 |
| 408 | Request Timeout | 客戶端請求超時(服務器沒有在允許的時間內解析出全部的Request) | 重新發(fā)起請求 |
| 500 | Internal Server Error | 服務器自身錯誤(可能是未對操作過程中的異常進行處理) | 聯(lián)系后臺開發(fā)人員解決(誰要是說這是客戶端問題就去找他理論) |
完整錯誤碼請參照網址:
https://baike.baidu.com/item/HTTP%E7%8A%B6%E6%80%81%E7%A0%81/5053660?fr=aladdin
響應頭Response Headers 及 響應體Response Body
這些內容與Request中對應部分并無區(qū)別,顧不贅述了
我們已經從特性與結構兩部分講述了Http相關的屬性,到這里這篇文章的主要內容基本上算是結束了,接下來我要講講一些其他的http相關的知識
跨域
作為移動端開發(fā)人員,我們對這個的了解不是很多,也幾乎用不到,但是我這里還是需要說明。因為現在已經到了前端的時代,萬一我們以后需要踏足前端,了解跨域,至少能為我們解決不少事情。
這篇文章不會詳細講解如何解決跨域,只會講解跨域形成的原因
什么是 跨域
在講跨域的時候,需要先講什么是域
什么是域
在上一課講解socket的過程中,我們已經發(fā)現了,想建立一個TCP/IP的連接需要知道至少兩個事情
- 對方的地址(host)
- 對方的門牌號(port)
我們只有依靠這兩個才能建立TCP/IP 的連接,其中host標明我們該怎么找到對方,port表示,我們應該連接具體的那個端口。
服務器應用是一直在監(jiān)聽著這個端口的,這樣才能保證在有連接進入的時候,服務器直接響應對應的信息
向上聊聊吧,我們通常講的服務器指的是服務器應用,比如常說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是同一域么,他們實際是等價的啊。
他們不屬于同一域,規(guī)定的很死,因為他們的host的表示不同,所以不是。
跨域
我們已經知道域了,跨域也就出現了,就是一個域訪問另一個域。
我們從http協(xié)議中可以發(fā)現,服務器并不任何強制規(guī)定域,也就是說,服務器并不在乎這個訪問是從哪個域訪問過來的,同時,作為客戶端,我們也并沒有域這么一說。
那么跨域究竟是什么呢?
這就要說跨域的來源了,我們日常訪問的網站,它實際上就是html代碼,服務器將代碼下發(fā)到了瀏覽器,由瀏覽器渲染并展示給我們。
開發(fā)瀏覽器的程序員在開發(fā)的時候,也不知道這個網頁究竟要做什么,但是他們?yōu)榱税踩?,不能給網頁和客戶端(socket)同樣的權限,因此他們限制了某些操作,在本域的網頁的某些請求操作在對方的服務器沒有添加允許該域的訪問權限的時候,訪問操作將不會被執(zhí)行,這些操作會對瀏覽器的安全性有很大到的影響。
所以跨域就此產生。
跨域從頭到尾都只是一個客戶端的操作行為,從某種角度上說,它與服務器毫無關系,因為服務器無法得知某次請求是否來自于某一網頁(在客戶端不配合的情況下),也就無從禁止了
對于我們移動端,了解跨域后我們至少可以說,跨域與我們無關-_-
socket實現簡單的http請求
事實上,一篇文章如果沒有代碼上的支撐,只是純理念上的闡述,終究還是感覺缺點什么,本文將在上篇文章代碼的基礎上做些小的改進。
這里就以菜鳥教程網的http教程作為本篇文章的測試(http://www.runoob.com/http/http-tutorial.html)(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)
// 因內存字節(jié)和網絡通訊字節(jié)相反,顧我們需要交換大小端 我們連接的端口是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)")
}
// 組裝文本協(xié)議 訪問 菜鳥教程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字節(jié)存儲防止溢出
let readData = Data(count: 64 * 1024)
let readPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)})
// 記錄當前讀取多少字節(jié)
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上,地址:https://github.com/chouheiwa/SocketTestExample
總結
越學習越覺得自己懂得越少,我們現在走的每一步,都是在學習。
題外話:畫圖好費勁啊,都是用PPT畫的-_-
注: 本文原創(chuàng),若希望轉載請聯(lián)系作者
參考: