在上一篇中,我們初步的講述了socket的定義馒索,以及socket中的TCP的簡單用法莹妒。
這篇我們主要講的是HTTP相關(guān)的東西。
什么是HTTP
HTTP -> Hyper Text Transfer Protocol(超文本傳輸協(xié)議)绰上,它是基于TCP/IP協(xié)議的一種無狀態(tài)連接
特性
無狀態(tài)
無狀態(tài)是指旨怠,在標(biāo)準(zhǔn)情況下,客戶端的發(fā)出每一次請求蜈块,都是獨立的鉴腻,服務(wù)器并不能直接通過標(biāo)準(zhǔn)http協(xié)議本身獲得用戶對話的上下文。
這里百揭,可能很多人會有疑問爽哎,我們平時使用的http不是這樣的啊,服務(wù)器能識別我們請求的身份啊器一,要不免登錄怎么做啊?
所以額外解釋下课锌,我們說的這些狀態(tài),如cookie/session是由服務(wù)器與客戶端雙方約定好盹舞,每次請求的時候产镐,客戶端填寫,服務(wù)器獲取到后查詢自身記錄(數(shù)據(jù)庫踢步、內(nèi)存)癣亚,為客戶端確定身份,并返回對應(yīng)的值获印。
從另一方面也可說述雾,這個特性和http協(xié)議本身無關(guān),因為服務(wù)器不是從這個協(xié)議本身獲取對應(yīng)的狀態(tài)兼丰。
無狀態(tài)也可這樣理解: 從同一客戶端連續(xù)發(fā)出兩次http請求到服務(wù)器玻孟,服務(wù)器無法從http協(xié)議本身上獲取兩次請求之間的關(guān)系
無連接
無連接指的是,服務(wù)器在響應(yīng)客戶端的請求后鳍征,就主動斷開連接黍翎,不繼續(xù)維持連接
結(jié)構(gòu)
http 是超文本傳輸協(xié)議,顧名思義艳丛,傳輸?shù)氖且欢ǜ袷降奈谋鞠坏В裕覀兘酉聛碇v述一下這個協(xié)議的格式
在http中氮双,一個很重要的分割符就是 CRLF(Carriage-Return Line-Feed) 也就是 \r 回車符 + \n 換行符碰酝,它是用來作為識別的字符
請求 Request
上圖為請求格式
請求行
GET / HTTP/1.1\r\n
首行也叫請求行,是用來告訴服務(wù)器戴差,客戶端調(diào)用的請求類型送爸,請求資源路徑,請求協(xié)議類型
請求類型也就是我們常說的(面試官總問的)GET暖释,POST等等發(fā)送的位置袭厂,它位于請求的最開始
請求資源路徑是提供給服務(wù)器內(nèi)部的尋址路徑,用來告訴服務(wù)器客戶端希望訪問什么資源饭入,在瀏覽器中訪問 http://www.reibang.com/p/6cfbc63f3a2b (用簡書做一波示范了)嵌器,則我們請求的就是 /p/6cfbc63f3a2b
請求協(xié)議類型目前使用最多的是HTTP/1.1說不定在不遠(yuǎn)的未來,將會被HTTP/2.0所取代
注:
所使用鏈接為https鏈接谐丢,但是其內(nèi)容與http一樣爽航,因此使用該鏈接做為例子,ssl 將會在接下來的幾篇文章中講述
請求行的不同內(nèi)容需要用 " "空格符 來做分割
請求行的結(jié)尾需要添加CRLF分割符
請求頭Request Headers
請求行之后乾忱,一直到請求體(body)讥珍,之間的部分,被我們成為請求頭窄瘟。
請求頭的長度并不固定衷佃,我們可以放置無限多的內(nèi)容到請求頭中。
但是請求頭的格式是固定的蹄葱,我們可以把它看做是鍵值對氏义。
格式:
key: value\r\n
我們通常所說的cookie便是請求頭中的一項
一些常用的http頭的定義與作用: https://blog.csdn.net/philos3/article/details/76946029
注:
當(dāng)所有請求頭都已經(jīng)結(jié)束(即我們要發(fā)送body)的時候锄列,我們需要額外增加一個空行(CRLF) 告訴服務(wù)器請求頭已經(jīng)結(jié)束
請求體Request Body
如果說header我們沒有那么多的使用機(jī)會的話,那么body則是幾乎每個開發(fā)人員都必須接觸的了惯悠。
通常邻邮,當(dāng)我們進(jìn)行 POST 請求的時候,我們上傳的參數(shù)就在這里了克婶。
服務(wù)器是如何獲得我們上傳的完整Body呢?換句話說筒严,就是服務(wù)器怎么知道我們的body已經(jīng)傳輸完畢了呢?
我們想一下,如果我們在需要實現(xiàn)這個協(xié)議的時候情萤,我們會怎么做?
可以約定特殊字節(jié)作為終止字符鸭蛙,當(dāng)讀取到指定字符時,即認(rèn)為讀取完畢
發(fā)送方肯定知道要發(fā)送的數(shù)據(jù)的大小筋岛,直接告訴接收方娶视,接收方只需要在收到指定大小的數(shù)據(jù)的時候就可以停止接收了
發(fā)送方也不知道數(shù)據(jù)的大小(或者他需要花很大成本才能知道數(shù)據(jù)的大小),就先告訴接收方睁宰,我現(xiàn)在也不知道有多少歇万,等發(fā)送的時候看,真正發(fā)送的時候告訴接收方勋陪,"我這次要發(fā)送多少"贪磺,最后告訴接收方,"我發(fā)完了"诅愚,接收方以此停止接收寒锚。‘
也許你會有別的想法违孝,那恭喜你刹前,你可以自己實現(xiàn)類似的接收方法了。
目前雌桑,服務(wù)器是依靠上述三種方法接收的:
- 約定特殊字節(jié):
客戶端在發(fā)送完數(shù)據(jù)后喇喉,就調(diào)用關(guān)閉socket連接,服務(wù)器在收到關(guān)閉請求后開始解析數(shù)據(jù)校坑,并返回結(jié)果拣技,最后關(guān)閉連接
- 確定數(shù)據(jù)大小:
客戶端在請求頭中給定字段 Content-Length
,服務(wù)器解析到對應(yīng)數(shù)據(jù)后接受body耍目,當(dāng)body數(shù)據(jù)達(dá)到指定長度后膏斤,服務(wù)器開始解析數(shù)據(jù),并返回結(jié)果
- 不確定數(shù)據(jù)大小(Http/1.1 可用)
客戶端在請求頭中給定頭 Transfer-Encoding: chunked
邪驮,隨后開始準(zhǔn)備發(fā)送數(shù)據(jù)
發(fā)送的每段數(shù)據(jù)都有特定的格式莫辨,
格式為:
- 長度行:
每段數(shù)據(jù)的開頭的文本為該段真實發(fā)送的數(shù)據(jù)的16進(jìn)制長度加CRLF分割符
- 數(shù)據(jù)行:
真實發(fā)送的數(shù)據(jù)加CRLF分割符
例:
12\r\n // 長度行 16進(jìn)制下的12就是10進(jìn)制下的 18
It is a chunk data\r\n // 數(shù)據(jù)行 CRLF 為分割符
結(jié)尾段:
用以告訴服務(wù)器數(shù)據(jù)發(fā)送完成,開始解析或存儲數(shù)據(jù)。
結(jié)尾段格式固定
0\r\n
\r\n
目前沮榜,客戶端使用這種方法的不多盘榨。
到這里,如何告訴服務(wù)器應(yīng)該接收多少數(shù)據(jù)的部分已經(jīng)完成了
接下來就到了蟆融,告訴服務(wù)器较曼,數(shù)據(jù)究竟是什么了
同樣也是頭部定義:Content-Type
Content-Type介紹:
https://blog.csdn.net/qq_23994787/article/details/79044908
到這里,Request的基本格式已經(jīng)講完
響應(yīng) Response
相應(yīng)結(jié)構(gòu)
其實Response 和 Request 從協(xié)議上分析振愿,他們是一樣的,但是他們是對Http協(xié)議中文本協(xié)議的不同的實現(xiàn)弛饭。
響應(yīng)行
HTTP/1.1 200 OK\r\n
首行也叫響應(yīng)行冕末,是用來告訴客戶端當(dāng)前請求的處理狀況的,由請求協(xié)議類型侣颂,服務(wù)器狀態(tài)碼档桃,對應(yīng)狀態(tài)描述構(gòu)成
請求協(xié)議類型 是用來告訴客戶端,服務(wù)器采用的協(xié)議是什么冗澈,以便于客戶端接下來的處理福压。
服務(wù)器狀態(tài)碼 是一個很重要的返回值缰雇,它是用來通知服務(wù)器對本次客戶端請求的處理結(jié)果。
狀態(tài)碼非常多嘹屯,但是對于我們開發(fā)一般用到的是如下幾個狀態(tài)碼
狀態(tài)碼 | 對應(yīng)狀態(tài)描述 | 含義 | 客戶對應(yīng)操作 |
---|---|---|---|
200 | OK | 標(biāo)志著請求被服務(wù)器成功處理 | 無 |
400 | Bad Request | 標(biāo)志著客戶端請求出現(xiàn)了問題,服務(wù)器無法識別从撼,客戶端修改后服務(wù)器才能進(jìn)行處理 | 修改request參數(shù) |
401 | Unauthorized | 當(dāng)前請求需要校驗權(quán)限州弟,客戶端需要在下次請求頭部提交對應(yīng)權(quán)限信息 | 修改Header頭并提交對應(yīng)信息 |
403 | Forbidden | 當(dāng)前請求被服務(wù)器拒絕執(zhí)行(防火墻阻止或其他原因) | 等待一段時間后再次發(fā)起,無其他解決辦法 |
404 | Not Found | 服務(wù)無法找到對應(yīng)資源(最為常見的錯誤碼) | 修改Request中的資源請求路徑 |
405 | Method Not Allowed | 客戶端當(dāng)前請求方法不被允許 | 修改請求方法 |
408 | Request Timeout | 客戶端請求超時(服務(wù)器沒有在允許的時間內(nèi)解析出全部的Request) | 重新發(fā)起請求 |
500 | Internal Server Error | 服務(wù)器自身錯誤(可能是未對操作過程中的異常進(jìn)行處理) | 聯(lián)系后臺開發(fā)人員解決(誰要是說這是客戶端問題就去找他理論) |
完整錯誤碼請參照網(wǎng)址:
https://baike.baidu.com/item/HTTP%E7%8A%B6%E6%80%81%E7%A0%81/5053660?fr=aladdin
響應(yīng)頭Response Headers 及 響應(yīng)體Response Body
這些內(nèi)容與Request中對應(yīng)部分并無區(qū)別低零,顧不贅述了
我們已經(jīng)從特性與結(jié)構(gòu)兩部分講述了Http相關(guān)的屬性婆翔,到這里這篇文章的主要內(nèi)容基本上算是結(jié)束了,接下來我要講講一些其他的http相關(guān)的知識
跨域
作為移動端開發(fā)人員掏婶,我們對這個的了解不是很多啃奴,也幾乎用不到,但是我這里還是需要說明雄妥。因為現(xiàn)在已經(jīng)到了前端的時代最蕾,萬一我們以后需要踏足前端,了解跨域老厌,至少能為我們解決不少事情揖膜。
這篇文章不會詳細(xì)講解如何解決跨域,只會講解跨域形成的原因
什么是 跨域
在講跨域的時候梅桩,需要先講什么是域
什么是域
在上一課講解socket的過程中壹粟,我們已經(jīng)發(fā)現(xiàn)了,想建立一個TCP/IP的連接需要知道至少兩個事情
- 對方的地址(host)
- 對方的門牌號(port)
我們只有依靠這兩個才能建立TCP/IP 的連接,其中host標(biāo)明我們該怎么找到對方趁仙,port表示洪添,我們應(yīng)該連接具體的那個端口。
服務(wù)器應(yīng)用是一直在監(jiān)聽著這個端口的雀费,這樣才能保證在有連接進(jìn)入的時候干奢,服務(wù)器直接響應(yīng)對應(yīng)的信息
向上聊聊吧,我們通常講的服務(wù)器指的是服務(wù)器應(yīng)用盏袄,比如常說Tomcat忿峻,Apache 等等,他們啟動的時候一般會綁定好一個指定的端口(通常不會同時綁定兩個端口)辕羽。所以呢逛尚,作為客戶端,就可以用host+port來確定一個指定的服務(wù)器應(yīng)用
由此刁愿,域的概念就此生成绰寞,就是host + port
舉個例子: http://127.0.0.1:8056/
這個網(wǎng)址所屬的域就是127.0.0.1+8056 也可以寫成127.0.0.1:8056
這時候有人就會問了,那localhost:8056和127.0.0.1:8056是同一域么铣口,他們實際是等價的啊滤钱。
他們不屬于同一域,規(guī)定的很死脑题,因為他們的host的表示不同件缸,所以不是。
跨域
我們已經(jīng)知道域了叔遂,跨域也就出現(xiàn)了停团,就是一個域訪問另一個域。
我們從http協(xié)議中可以發(fā)現(xiàn)掏熬,服務(wù)器并不任何強(qiáng)制規(guī)定域佑稠,也就是說,服務(wù)器并不在乎這個訪問是從哪個域訪問過來的旗芬,同時舌胶,作為客戶端,我們也并沒有域這么一說疮丛。
那么跨域究竟是什么呢?
這就要說跨域的來源了幔嫂,我們?nèi)粘TL問的網(wǎng)站,它實際上就是html代碼誊薄,服務(wù)器將代碼下發(fā)到了瀏覽器履恩,由瀏覽器渲染并展示給我們。
開發(fā)瀏覽器的程序員在開發(fā)的時候呢蔫,也不知道這個網(wǎng)頁究竟要做什么切心,但是他們?yōu)榱税踩腱荒芙o網(wǎng)頁和客戶端(socket)同樣的權(quán)限,因此他們限制了某些操作绽昏,在本域的網(wǎng)頁的某些請求操作在對方的服務(wù)器沒有添加允許該域的訪問權(quán)限的時候协屡,訪問操作將不會被執(zhí)行,這些操作會對瀏覽器的安全性有很大到的影響全谤。
所以跨域就此產(chǎn)生肤晓。
跨域從頭到尾都只是一個客戶端的操作行為,從某種角度上說认然,它與服務(wù)器毫無關(guān)系补憾,因為服務(wù)器無法得知某次請求是否來自于某一網(wǎng)頁(在客戶端不配合的情況下),也就無從禁止了
對于我們移動端卷员,了解跨域后我們至少可以說盈匾,跨域與我們無關(guān)-_-
socket實現(xiàn)簡單的http請求
事實上,一篇文章如果沒有代碼上的支撐子刮,只是純理念上的闡述,終究還是感覺缺點什么窑睁,本文將在上篇文章代碼的基礎(chǔ)上做些小的改進(jìn)挺峡。
這里就以菜鳥教程網(wǎng)的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轉(zhuǎn)換成UInt32
sock4.sin_addr = converIPToUInt32(a: 47, b: 246, c: 3, d: 228)
// 因內(nèi)存字節(jié)和網(wǎng)絡(luò)通訊字節(jié)相反,顧我們需要交換大小端 我們連接的端口是80
sock4.sin_port = CFSwapInt16HostToBig(80)
// 設(shè)置sin_family 為 AF_INET表示著這個為IPv4 連接
sock4.sin_family = sa_family_t(AF_INET)
// Swift 中指針強(qiáng)轉(zhuǎn)比OC要復(fù)雜
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"
//轉(zhuǎn)換成二進(jìn)制
guard let data = sendMessage.data(using: .utf8) else {
fatalError("Error occur when transfer to data")
}
// 轉(zhuǎn)換指針
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)")
}
// 設(shè)置32Kb字節(jié)存儲防止溢出
let readData = Data(count: 64 * 1024)
let readPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)})
// 記錄當(dāng)前讀取多少字節(jié)
var currentRead = 0
while true {
// 讀取socket數(shù)據(jù)
let result = Darwin.read(socketFD, readPointer + currentRead, readData.count - currentRead)
guard result >= 0 else {
fatalError("Error in read() function code is \(errno)")
}
// 這里睡眠是減少調(diào)用頻率
sleep(2)
if result == 0 {
print("無新數(shù)據(jù)")
continue
}
// 記錄最新讀取數(shù)據(jù)
currentRead += result
// 打印
print(String(data: readData, encoding: .utf8) ?? "")
}
對應(yīng)代碼例子已經(jīng)放在github上担钮,地址:https://github.com/chouheiwa/SocketTestExample
總結(jié)
越學(xué)習(xí)越覺得自己懂得越少橱赠,我們現(xiàn)在走的每一步,都是在學(xué)習(xí)箫津。
題外話:畫圖好費勁啊狭姨,都是用PPT畫的-_-
注: 本文原創(chuàng),若希望轉(zhuǎn)載請聯(lián)系作者
參考: