iOS-網絡編程基礎

知 識 點 / 超 人


文章結構:
1.網絡七層
2.TCP/IP
3.UDP
4.socket
5.webSocket
6.MQTT
7.XMPP
8.HTTP


網絡七層
借用網上找到的一張圖

網絡七層

  • 應用層:當負責傳送數據發送請求時:把需要發送的數據,按照應用的格式標準協議等封裝成對應數據。當負責接收數據響應請求時:把數據按照應用的標準格式進行解析。例如在HTTP協議中,發送請求前要封裝請求頭,而接收數據時要解析響應頭,這些解析過程方式是根據應用層的協議格式而定的,每個協議格式式不一樣。如果不同應用標準格式不一樣,會解析失敗,無法正確顯示數據內容。例如:A電腦是 Mac電腦用的pages軟件寫一個文本,然后傳給B windows電腦,B電腦接收到的是A電腦的數據,數據格式完全跟A電腦一樣。但是B電腦上沒有能解析A電腦pages軟件數據的工具。所以B電腦用戶無法讀取該文件。文件之所以無法解析不是因為電腦端不一樣,而是因為沒有軟件解析對應數據格式的應用。 實際上數據格式的基本封裝是應用自身完成的,還沒有到達應用層這一步。拿HTTP說,當應用封裝好需要傳輸的數據進行傳輸時(例如我們平時應用中把數據封裝在字典里然后轉成json字符串,這一步驟就是應用本身完成的封裝),應用層會根據HTTP協議對數據進行處理(封裝成HTTP的請求頭),該協議會在傳輸的數據前段附加一個首部標簽,該首部標簽標明了發送數據的內容和發送的地址
  • 表示層:數據的轉換層。當負責傳送數據發送請求時:會將應用層封裝的數據轉換成網絡通用的標準數據格式進行傳遞(格式化編碼)。當負責接收數據響應請求時:會將會話層傳入的網絡通用標準格式數據轉換為對應設備的數據。不同設備對同一比特流數據的解析可能會有不同的結果。表示層與表示層之間為了識別編碼格式,也會附加首部信息。
  • 會話層:負責網絡通信的建立和斷開,選擇網絡通信的連接方式,是GET、POST、長連接,短連接。當負責傳送數據發送請求時:把表示層的數據按照一定規律和標準拆分成數據塊(每個數據庫都有一個單獨的附加首部信息,標記接收端和發送端ip)。當負責接收數據響應請求時:負責把比特流數據根據數據的每個節點拼接成完整的數據。會話層會在接收到的數據前端附加首部信息,記錄數據的傳輸順序信息。
  • 傳輸層:主要是用戶負責建立兩端節點的通信關系,保證數據的傳輸安全,傳輸是直接連接雙方節點ip地址,不經過路由處理。如果A發送給B信息,傳輸過程中出現數據丟失或者網絡出現異常只發送了部分信息到達B端,B端會反饋消息,告訴A,只收到了部分消息,A會將未發送的消息重新發送到B,比如迅雷下載資源的時候,可以暫停,下次繼續按照上一次的進度下載。(猜測可能是傳輸結束后會話層中根據附加首部信息判斷到數據傳輸順序并不完整,會把不完整的順序反饋給發送端,讓附送端把未接收到的數據重新發送過來)
  • 網絡層:將數據傳輸到目標地址,目標地址可以是由多個網絡或路由連接而成的某個地址,因此改層主要是尋找地址和路由選擇。
  • 數據鏈路層:負責物理層上的通信傳輸,把0、1序列化為有意義的數據幀傳給對端。通過Mac地址,目的是識別連接到同一傳輸介質上的設備。因此,這一把分層中將包含Mac地址信息的首部附加到網絡層轉發過來的數據上,發送到網絡。、
  • 物理層:負責將機器語言的0、1轉換為電壓高低、脈沖光的閃滅輸出給物理的傳輸介質(光纖)。
  • 帶寬:數據的物理傳輸一般用光纖,雙絞線電纜等來進行媒介傳輸,在這些媒介傳輸過程中,傳輸的速度是恒定的。如果要增加傳輸速度,只有增加傳輸媒介的通道數量,一般低速數據鏈路是指傳輸媒介通道較少,同一時間通過的數據少。高速數據鏈路是指傳輸媒介通道較多,同一時間可通過的數據多。傳輸數量又稱只為帶寬。傳輸媒介通道越多帶寬越大傳輸的能力越強。帶寬單位為bps(bits<比特> Per second<每秒>,每秒的比特流傳輸速度)

  • 吞吐量:吞吐量的單位與帶寬一樣都是bps。吞吐量的大小不僅衡量帶寬,同時也是衡量主機CPU處理能力,網絡擁堵情況,報文中數據字段的占有份額(不計算報文首部,僅計算數據字段本身)等信息

其實現在基本上把會話層、表示層、應用層統一為應用層了。

對于我們而言理解上也就主要是4個層,數據鏈路層,網絡層,傳輸層,應用層

數據鏈路層就是高速公路,TCP、UDP就是能在這個高速公路行駛的運貨車,而IP就像是這個運貨車的司機決定了貨車從哪一個站出發到達哪一個站,而HTTP,FTP就是這個車上運載的貨物。貨物是由客戶端與服務端協商定義的,由兩端定義好使用同一套協議去識別驗收貨物,而貨物的傳輸過程是由TCP/IP定義的。


基礎補充

bit:位,比特位,是計算機中表示數據的最小單位。通常bit用來作為數據傳輸的基本單位,數據鏈路層的傳輸是基于二進制的傳輸。
byte:字節,1byte = 8bit,1KB = 1024byte即1024B,1M = 1024KB

  • 8bit(位)最多表示0-255,因為在二進制中最多表示0000 0000-1111 1111,2的8次方-1
  • 16bit(位)最多表示0-65535 ,因為在二進制中最多表示0000 0000 0000 0000 - 1111 1111 1111 1111 ,2的16次方-1
  • 32bit(位)最多表示0- 4 294 967 295,2的32次方-1

英文字母和中文漢字在不同字符編碼下的字節數不一樣。
英文字母:
編碼類型:GB2312時字節數為 1byte
編碼類型:GBK時字節數為 1byte
編碼類型:GB18030時字節數為 1byte
編碼類型:ISO-8859-1時字節數為 1byte
編碼類型:UTF-8時字節數為 1byte
編碼類型:UTF-16時字節數為 3byte
編碼類型:UTF-16BE時字節數為 2byte
編碼類型:UTF-16LE時字節數為 2byte

中文漢字(包含繁體):
編碼類型:GB2312時字節數為 2byte
編碼類型:GBK時字節數為 2byte
編碼類型:GB18030時字節數為 2byte
編碼類型:ISO-8859-1時字節數為 1byte
編碼類型:UTF-8時字節數為 3byte
編碼類型:UTF-16時字節數為 4byte
編碼類型:UTF-16BE時字節數為 2byte
編碼類型:UTF-16LE時字節數為 2byte


TCP/IP

傳輸控制協議,TCP/IP是互聯網協議,Internet互聯網的基礎,由網絡層的IP協議與傳輸層的TCP協議組成.TCP/IP定義了電子設備如何連入Internet互聯網

TCP又叫傳輸控制層協議位于傳輸層,基于字節流,數據在互聯網之間傳輸的標準.它可以提供可靠的、較為安全的、面向連接點到點的網絡數據傳遞服務。

TCP報文格式

TCP報文格式

  • 源端口號(sourec port):數據發送源的應用進程端口號,占16位(2個字節),端口號最小為1024,最大為65535。因為該字段占16位(16bit),而16bit在十進制中最大表示為65535最小表示為0,所以端口號最大為65535,而由于0-1023是知名端口號一般由系統占用。所以端口號最小從1024開始。
    -目的端口號(destination port):標明接收數據的端口號。
    -順序號(sequence number):這里的順序號就是TCP的握手和揮手的seq,它占32位,標識了TCP報文中第一個字節在傳輸方向中對應的字節序號。當SYN時,SN(sequence number)=ISN(隨機值)單位是byte。比如發送端發送的一個TCP包凈荷(不包含TCP頭)為12byte,SN為5,則發送端接著發送的下一個數據包的時候,SN應該設置為5+12=17。通過序列號,TCP接收端可以識別出重復接收到的TCP包,從而丟棄重復包,同時對于亂序數據包也可以依靠序列號進行重排序,進而對高層提供有序的數據流。另外如果接收的包中包含SYN或FIN標志位,邏輯上也占用1個byte,應答號需加1
  • TCP報頭長度(Header Length):僅表示報頭的長度,占4位(這里按照32位字長位單位計算。 占4位標示4bit,而4bit最大二進制表示為1111,最大顯示的值為15, 而單位是32位字長。表示每個單位按照32來計算。每個單位也就是4字節。最大能表示15個單位,就是60字節,TCP報頭內容最多60個字節)。它指出TCP報文段從數據起始處 距離 TCP報文段的起始處有多遠。[這就是為什么上圖中 會分為前16位 后16位,因為單位是按照32位字長計算的]
  • 保留位(Resy):占6位,保留給以后使用,目前必須設置為0
  • 編碼位:
    URG:緊急數據標志位
    ACK:確認序號有效
    PSH:請求推送位,接收端應盡快把數據傳送給應用層
    RST:重置連接,通常,如果TCP收到的一個分段明顯不屬于該主機的任何一個連接,則向遠程發送一個復位包
    SYN:建立連接,讓連接雙方同步序列號
    FIN:釋放連接
  • 窗口大小(Window Size):標示從確認號開始,報文的發送源可以接受的字節數為多少。
  • 校驗和(check sum):奇偶校驗,此校驗和是對整個TCP報文段以16位字進行計算所得,服務端接收后進行驗證。
    -緊急指針(urgent poiner):主要是用作發送端向另一端發送緊急數據的一種方式,只有在URG標志值為1的時候才有效。它是一個正的偏移量,跟順序號字段中的值相加表示緊急數據最后一個字節的序號。
    -選擇(options):至少1字節的可變長字段,標識哪個選項(如果有的話)有效。如果沒有選項,這個字節等于0,說明選項的結束。這個字節等于1表示無需再有操作;等于2表示下四個字節包括源機器的最大長度,最常見的可選字段是最長報文大小,又稱為MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段(為建立連接而設置SYN標志為1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。注:MSS=TCP報文段長度-TCP首部長度
  • 填充(Padding):因為選擇(options)項長度不一定是32位的整數倍,所以需要填充位,在這個字段中加入額外的0,來保證該字段的位數是32的倍數,保證TCP頭是32的整數倍。
  • 數據(data):TCP所傳輸的數據,在三次握手和四次揮手時僅有TCP首部,沒有該data字段。

TCP粘包拆包
1.從上面的TCP報文來看,我們知道TCP報文中是沒有字段表示數據長度的。
2.TCP是以流動的方式傳輸數據,傳輸的最小單位為一個報文段(segment)。TCP的Header中有Options標識位,常見的標識為mss(Maximum Segment Size),指的是連接層每次傳輸的數據有個最大限制MTU(Maximum Transmission Unit),一般是1500比特,超過這個量要分成多個報文段,MSS則是這個最大限制減去TCP的header,光是要傳輸的數據的大小,一般為1460比特。換算成字節,也就是180多字節。當需要傳輸的數據大小超過一個TCP段能接收的大小時,TCP就會把數據進行分段傳輸。這段內容摘取之該文章

而什么是粘包拆包呢。假如一個數據被TCP分段成packet1和packet2兩段時??赡軙霈F以下幾種情況:

  • 1.接收端正常收到packet1和packet2,即沒有發生拆包和粘包的現象

  • 2.接收端只收到一個數據包packet,由于TCP是不會出現丟包的(不是絕對,但TCP自帶丟包重發機制,所以基本不丟包),所以這一個數據包是由packet1和packet粘合在一起的,這種現象即為粘包。這種情況由于接收端不知道這兩個數據包的界限,所以對于接收端來說很難處理。

  • 3.接收端收到了兩個數據包,但由于某種原因packet2被分離成packet2_1和packet2_2,packet1的尾部和packet2_1的首部連在一起,合成一個大的報文,首先到達接收端端,過后packet2_2才到達,這種情況既發生了粘包,同時也發生了拆包。

TCP為提高性能,發送端會將需要發送的數據發送到緩沖區,等待緩沖區滿了之后,再將緩沖中的數據發送到接收方。同理,接收方也有緩沖區這樣的機制,來接收數據。
1.當要發送的數據大于TCP發送緩沖區剩余空間大小,將會發生拆包。
2.當待發送數據大于MSS(最大報文長度),TCP在傳輸前將進行拆包。
3.當要發送的數據小于TCP發送緩沖區的大小,TCP會將多次寫入到緩沖區的數據一次發送出去,將會發生粘包。
4.當接收數據端的應用層沒有及時讀取接收緩沖區中的數據,將發生粘包。

TCP的連接建立過程又稱為TCP的三次握手

  • 首先客戶端向服務端發起一個建立連接的同步(SYN)請求;
  • 服務端在收到這個請求后向客戶端回復一個同步/確認(SYN/ACK)的應答;
  • 客戶端收到應答回應后再向服務端發送一個確認(ACK),此時TCP連接成功建立.


    三次握手

TCP的連接斷開過程又稱之為TCP的四次揮手

  • 首先客戶端發送一個FIN消息給服務端,客戶端進入FIN_WAIT_1狀態。

  • 接著服務端收到FIN后,發送一個ACK給客戶端,確認序號為收到序號+1(與SYN相同,一個FIN占用一個序號),服務端進入CLOSE_WAIT狀態。

  • 服務端在回復完客戶端的TCP斷開請求后,不會馬上進行TCP連接的斷開,服務端會先確保斷開前,所有傳輸的數據是否已經傳輸完畢,一旦確認數據傳輸完成,服務端發送一個FIN消息給客戶端,服務端進入LAST_ACK狀態。

  • 最后客戶端收到FIN消息后,進入TIME_WAIT狀態,接著發送一個ACK給Server,確認序號為收到序號+1,服務端進入CLOSED狀態,完成四次揮手。

    四次揮手

    這里要單獨說下TIME_WAIT狀態,它比較特殊,因為處于TIME_WAIT狀態的一方還沒有釋放TCP連接。如果某一方主動發出關閉TCP的請求,那么這一方最后要較長時間(2倍MSL時間)的處于TIME_WAIT狀態,因為在最后一個ACK發送出去后,ACK可能會丟失,如果發生丟失,非主動關閉TCP請求的一方就會再次發送FIN請求,而此時主動關閉TCP請求這一方需要再次發送ACK。以確保正常中斷雙方數據通道,所以必須要讓四次ACK都正常接收。
    TCP不允許新建立的連接復用TIME_WAIT狀態下的。處于TIME_WAIT狀態的連接通道,在等待兩倍的MSL時間以后將會轉變為CLOSED狀態。之所以是兩倍的MSL,是由于MSL(最長報文段壽命)是一個數據報在網絡中單向發出認定丟失的時間,一個數據報有可能在發送途中或是其響應過程中成為殘余數據報,確認一個數據報及其響應的丟棄的需要兩倍的MSL,這就意味著,一個成功建立的連接,必然使得先前網絡中殘余的數據報都丟失了。

TCP將數據流拆分成多個部分,這些部分叫做TCP的數據段,利用IP協議進行數據段的傳輸。當TCP將把數據段分成多個數據報在IP中進行傳輸時,由于IP并不能保證接收的數據報的順序相一致,TCP會在收信端裝配TCP數據段并形成一個不間斷的數據流

為了確保IP數據報的成功傳遞TCP主要完成以下工作

  • 發送時對大塊數據進行分段,接收時對數據段重組。
  • 確保正確排序及按順序傳遞分段的數據。
  • 通過算法校驗傳輸數據的完整性。
  • 根據數據段是否接收成功,發送確認消息。通過使用選擇性確認,也對沒有收到的數據發送否定確認。從而確保數據段拼合后的完整性

IP
IP確保計算機之間通信數據的正確傳輸,位于網絡層
通過IP,將TCP分割的數據段通過因特網在計算機之間傳遞.IP 負責將每個數據包,路由至它的目的地.
IP路由器,當一個IP數據包從一臺計算機被發送,它會到達一個IP路由器.IP路由器負責將這個數據包,路由至它的目的地,直接地或者通過其他路由器的再次傳輸.
在一個相同的通信中,每個數據包經由的路徑可能都不同,因為數據包的傳輸過程是由IP路由器決定的。路由器負責根據通信量,網絡中的錯誤或者其他參數來進行正確地尋址。

計算機必須有一個IP地址才能夠正確的連接互聯網中.每個IP數據包必須有一個明確的地址才能夠準確發送到目標計算機.
在計算機中IP分為4段,每段的大小都是1個字節,而1個字節是8位(bit)二進制數(即0或1的8位數組合),那么最大的二進制數就是:11111111,化為十進制得:255。最小的就是:00000000,化為十進制得:0,所以每個IP地址由4組0到255之間的數字組合而成,并由點號隔開.例如:192.168.1.192。每個IP地址32bit


UDP

UDP被稱之為用戶數據報協議位于傳輸層。提供非面向連接的網絡服務,傳送數據不需要和服務端建立連接,只需要知道數據需要發送到哪一個IP地址和監聽端口即可,該服務傳輸的數據是不可靠的、可以由一點發送到到多點。這意味著它不保證數據報的到達只負責發送,也不保證所傳送數據包的順序是否正確,正因為UDP協議的控制選項較少,在數據傳輸過程中延遲較小所以數據傳輸效率很高,一般用于對可靠性要求不高的功能上,因為它不提供數據包分組、組裝和不能對數據包進行排,舍棄了TCP的建立連接等開銷較大的步驟。簡單的說UDP只管發送數據,過程與結果并不在乎。一般網絡質量較差時,采用UDP會造成數據的丟失。從UDP的結構來說,UDP首部采用了16bit大小的內容來標識UDP數據報文的長度,因此在應用層能很好的將不同的數據報文區分開,從而避免粘包和拆包的問題。

UDP報文格式

UDP報文格式

這里UDP的校驗和有點特殊。會加一個偽首部進去計算。

UDP長度(Length):由于UDP首部由4個字段組成,每個字段16bit即2字節。所以UDP長度最小8字節。UDP長度占16bit,最大表示值65535,而UDP報文占8個字節,IP包頭占20個字節。所以UDP最大能表示65535-8-20=65507字節。但由于MTU的限制,實際在以太網中UDP最大可以表示1500-8-20=1472字節。

MTU是什么?1500從哪里來的?
MTU通信術語 最大傳輸單元(Maximum Transmission Unit,MTU)是指一種通信協議的某一層上面所能通過的最大數據包大?。ㄒ宰止潪閱挝?。
在以太網中,每個以太網幀都有最小的大小64Bytes最大不能超過1518Bytes,對于小于或者大于這個限制的以太網幀我們都可以視之為錯誤的數據幀,一般的以太網轉發設備會丟棄這些數據幀。
由于以太網EthernetII最大的數據幀是1518Bytes這樣,刨去以太網幀的幀頭(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和幀尾CRC校驗部分4Bytes那么剩下承載上層協議的地方也就是Data域最大就只能有1500Bytes這個值我們就把它稱之為MTU

UDP丟包和無序
丟包原因:
接收端處理包時間過長,接收端處理上一個包的時間過長,導致下個包來的時候并沒有去調用recv來接收包。從而導致丟包。 在TCP中有緩沖區,2次調用recv中間收到的包會放在緩沖區中,只有在緩沖區放不下中間發送的包時,會發生丟包,而且丟包TCP會自動重發。但UDP沒有緩沖區。

解決方案:
1.可以自己寫一個緩沖區去做處理
2.發送端與接收端協議 緩沖區大小和數據包的最大大小,包超過大小需要切割包,包超過緩沖區大小,需要延時發送。
3.也可以增加處理包的效率
4.可以寫一個丟包重發機制
這些方法也是TCP之所以不丟包的原因,也是UDP快的原因之一,個人感覺這樣做后就跟TCP差不多了,可能唯一不同的就是TCP自行處理的,而UDP需要我們自己寫,自己寫可操作空間才更大吧。

無序原因:
因為UDP傳送數據不需要和服務端建立連接,而網絡的穩定是不確定性,所以多個包到達接收端的順序是無法控制的。因此會造成無序。

沒有處理過這種情況,只能猜測解決辦法。在數據包中增加順序標識和完整數據包大小或個數標識


socket

socket又稱之為套接字,因為它本身不是一種協議,是位于應用層和傳輸控制層之間的一組接口。它包含進行網絡通信必須的五種信息:
1.連接使用的協議
2.本地主機的IP地址
3.本地進程的協議端口
4.遠地主機的IP地址
5.遠地進程的協議端口
通過Socket接口,可以讓應用層和傳輸層區分來自不同應用程序進程(端口)或網絡連接的通信。建立一個有效的Socket連接需要至少配對兩個套接字,一個運行于客戶端的套接字,稱為ClientSocket,另一個運行于服務器端的套接字,稱為ServerSocket。套接字之間的連接過程分為三個步驟:
1.服務器的事件監聽
2.客戶端數據請求
3.一條雙向通道的連接
Socket可以支持不同的傳輸層協議(TCP或UDP),當使用TCP協議進行連接時,該Socket連接就是一個TCP連接,UDP連接同理。

TCP/IP與Socket的關系
TCP/IP只是一個協議,就像操作系統的運行機制一樣,它是一套規則,必須要把規則具體實現,同時還要對外提供操作接口。像操作系統會提供標準的編程接口,TCP/IP提供給程序員做網絡開發所用的接口,這就是Socket編程接口

網絡這一塊主要是理論所以要耐著性子看,慢慢消化,當你看到這里的時候你就里成功理解網絡編程又進一步了,廢話不多說直接上代碼,從代碼里理解socket

客戶端

/**
 連接socket

 @param host 域名ip
 @param port 端口號
 */
- (void)connectSocketWithHost:(NSString *)host port:(NSUInteger)port
{
    __weak typeof(self) weakSelf = self;
    dispatch_async(_socketQueue, ^{//創建一個單獨隊列來執行連接
        //用來接收獲取地址信息是否成功的值
        int error;
        /*
         memset作用是在一段內存塊中填充某個給定的值,它是對較大的結構體或數組進行清零操作的一種最快方法
         第一個參數為指針地址,第二個為設置值,第三個為連續設置的長度(大?。?         分配一個hints結構體,把它清零后填寫需要的字段,再調用getaddrinfo,然后遍歷一個鏈表逐個嘗試每個返回地址
         */
        memset(&hints, 0, sizeof(hints));
        
        
        /*如果指定AF_INET,那么函數九不能返回任何IPV6相關的地址信息;
         如果僅指定了AF_INET6,則就不能返回任何IPV4地址信息。
         AF_UNSPEC則意味著函數返回的是適用于指定主機名和服務名且適合任何協議族的地址。
         如果某個主機既有AAAA記錄(IPV6)地址,同時又有A記錄(IPV4)地址,那么AAAA記錄將作為sockaddr_in6結構返回,而A記錄則作為sockaddr_in結構返回*/
        hints.ai_family = PF_UNSPEC;//AF_UNSPEC,ai_family設置網絡返回的地址信息類型
        
        
        /*
         AI_V4MAPPED:如果需要dns的host沒有v6地址的情況下,getaddinfo會把v4地址轉換成v4-mapped ipv6 address,如果有v6地址返回就不會做任何動作
         AI_ADDRCONFIG:這個是一個很有用的特性,這個flags表示getaddrinfo會根據本地網絡情況,去掉不支持的IP協議地址
         AI_DEFAULT:其實就是AI_V4MAPPED|AI_ADDRCONFIG_CFG,也是apple推薦的flags設置方式
         */
        hints.ai_flags = AI_DEFAULT;//ai_flags對返回的地址信息處理方式
        
        
        /*
         如果服務支持多個套接口類型,那么每個套接口類型都可能返回一個對應的結構,具體取決于hints結構的ai_socktype成員
         指定的服務既可支持TCP也可支持UDP,所以調用者可以把hints結構中的ai_socktype成員設置成自己需要的服務
         常用的Socket類型有兩種:
         流式Socket(SOCK_STREAM)流式是一種面向連接的Socket,針對于面向連接的TCP服務應用;
         數據報式Socket(SOCK_DGRAM)數據報式Socket是一種無連接的Socket,對應于無連接的UDP服務應用。
         */
        hints.ai_socktype = SOCK_STREAM;//ai_socktype設置連接服務類型
        
        
        /* getaddrinfo解決了把主機名和服務名轉換成套接口地址結構的問題
         hostname:一個主機名或者地址串(IPv4的點分十進制串或者IPv6的16進制串)
         service:服務名可以是十進制的端口號,也可以是已定義的服務名稱,如ftp、http等
         hints:可以是一個空指針,也可以是一個指向某個addrinfo結構體的指針,調用者在這個結構中填入關于期望返回的信息類型的暗示。舉例來說:如果指定的服務既支持TCP也支持UDP,那么調用者可以把hints結構中的ai_socktype成員設置成SOCK_DGRAM使得返回的僅僅是適用于數據報套接口的信息。
         result:本函數通過result指針參數返回一個指向addrinfo結構體鏈表的指針。
         返回值:0——成功,非0——出錯
         */
        error = getaddrinfo([host UTF8String], NULL, &hints, &res0);
        
        
        /*
         如果getaddrinfo函數返回成功,那么由res0參數指向的變量已被填入一個指針,它指向的是由其中的ai_next成員串聯起來的addrinfo結構鏈表??梢詫е路祷囟鄠€addrinfo結構的情形有以下2個:
         1.    如果與hostname參數關聯的地址有多個,那么適用于所請求地址簇的每個地址都返回一個對應的結構。
         2.    如果service參數指定的服務支持多個套接口類型,那么每個套接口類型都可能返回一個對應的結構,具體取決于hints結構的ai_socktype成員。
         
         遍歷一個鏈表逐個嘗試每個返回地址。
         
         */
        
        //判斷是否解析成功
        if (error != 0) {
            NSLog(@"getaddrinfo失敗,原因:%s",gai_strerror(error));
            [weakSelf sendConnectStatus:HYJSocketStatus_failed];
            return;
        }
        weakSelf.sockerStatus = -1;
        for (struct addrinfo *res = res0; res; res= res->ai_next) {
            
            
            /*
             socket() 創建socket
             參數1:ai_family網絡返回的地址信息類型
             參數2:ai_socktype設置連接服務類型
             參數3:ai_protocol協議類型。取的值取決于ai_address和ai_socktype的值.一般設置為0
             返回值非負描述符成功,返回一個新的套接字描述,出錯返回-1
             */
            weakSelf.sockerStatus = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
            if (weakSelf.sockerStatus < 0) {//解析失敗
                continue;
            }
            
            
            /*
             ai_addr是一個sockaddr類型的結構體
             sa_family是返回的連接類型是IPv4還是IPv6
             */
            switch (res->ai_addr->sa_family) {
                case AF_INET://IPv4
                {
                    /* sockaddr_in必須在.mm文件中使用 該類型為C++ */
                    struct sockaddr_in *v4sa = (struct sockaddr_in*)res->ai_addr;//會將struct sockaddr 變量強制轉化為struct sockaddr_in 類型
                    
                    /*
                     把主機字節序轉化為網絡字節序 (必須把port和host這些參數轉化為網絡字節序)
                     ntohs() 參數16位 short類型 網絡字節序轉主機字節序
                     ntohl() 參數32位 long類型 網絡字節序轉主機字節序
                     ntohll() 參數64位 long類型 網絡字節序轉主機字節序
                     
                     htons() 參數16位 short類型 主機字節序轉網絡字節序
                     htonl() 參數32位 long類型 主機字節序轉網絡字節序
                     htonll() 參數64位 long類型 主機字節序轉網絡字節序
                     以上聲明中 n代表netwrok, h代表host ,s代表short,l代表long,ll代表longlong
                     如果數據是單字節的話,則其沒有字節序的說法了
                     
                     
                     網絡字節順序NBO(Network Byte Order)
                     按從高到低的順序存儲,在網絡上使用同一的網絡字節順序,可避免兼容性問題;
                     主機字節順序HBO(Host Byte Order)
                     不同的機器HBO不相同,與CPU的設計有關,數據的順序是由CPU決定的,而與操作系統無關;
                     如Intel x86結構下,short型數0x1234表示為34 12,int型數0x12345678表示為78 56 34 12;
                     如IBM power PC結構下,short型數0x1234表示為 12 34,int型數0x12345678表示為 12 34 56 78.
                     由于這個原因,不同體系結構的機器之間不能直接通信,所以要轉換成一種約定的順序,也就是網絡字節順序,其實就是如同power pc那樣的順序。在PC開發中有ntohl和htonl函數可以用來進行網絡字節和主機字節的轉換
                     */
                    v4sa->sin_port = htons(port);//注意port的數據類型,sin_port是in_port_t類型,而in_port_t是__uint16_t類型。如果一個高位轉低位就會出現數據丟失,編譯器會報錯
                    /*
                     注意點
                     在iOS編程中,經常會使用C或者C++混編的情況。在C和C++編程中,經常將指針轉化為int類型。
                     在32位的系統中,指針占有4個字節(當然有2個字節的指針,別鉆牛角)的空間;int也占有4個字節的空間。這樣,轉化是沒有精度損耗的。OK。
                     在64位操作系統下占有8個字節的空間,而int占有4個字節的空間;這時候,就出問題了,將8字節的指針轉化位4個字節的int,當然會發生精度的損耗。
                     */
                }
                    break;
                case AF_INET6://IPv6
                {
                    struct sockaddr_in6 *v6sa = (struct sockaddr_in6*)res->ai_addr;//會將struct sockaddr 變量強制轉化為struct sockaddr_in6 類型,注意類型與IPv4不同
                    v6sa->sin6_port = htons(port);//配置port
                }
                    break;
            }
            
            
            /*
             connect()用來將參數sockfd的socket連至參數serv_addr指定的網絡地址.結構sockaddr請參考bind().參數addrlen為sockaddr的結構長度.
             返回值:成功則返回0,失敗返回-1,錯誤原因存于errno中.
             錯誤代碼:
             1、EBADF參數sockfd非合法socket處理代碼
             2、EFAULT參數serv_addr指針指向無法存取的內存空間
             3、ENOTSOCK參數sockfd為一文件描述詞,非socket.
             4、EISCONN參數sockfd的socket已是連線狀態
             5、ETIMEDOUT企圖連線的操作超過限定時間仍未有響應.
             6、ENETUNREACH無法傳送數據包至指定的主機.
             7、EAFNOSUPPORT sockaddr結構的sa_family不正確.
             8、EALREADY socket為不可阻斷且先前的連線操作還未完成.
             */
            if (connect(weakSelf.sockerStatus, res->ai_addr, res->ai_addrlen) < 0) {
                close(weakSelf.sockerStatus);//關閉套接字描述
                //socket 設置成非阻塞狀態
                
                
                /*
                 int fcntl(int fd, int cmd, long arg);
                 fcntl()針對(文件)描述符提供控制。參數fd是被參數cmd操作(如下面的描述)的描述符。針對cmd的值,fcntl能夠接受第三個參數int arg。
                 
                 fcntl()的返回值與命令有關。如果出錯,所有命令都返回-1,如果成功則返回某個其他值。下列三個命令有特定返回值:F_DUPFD , F_GETFD , F_GETFL以及F_GETOWN。
                 F_DUPFD   返回新的文件描述符
                 F_GETFD   返回相應標志
                 F_GETFL , F_GETOWN   返回一個正的進程ID或負的進程組ID
                 
                 fcntl函數有5種功能:
                 1. 復制一個現有的描述符(cmd=F_DUPFD).
                 2. 獲得/設置文件描述符標記(cmd=F_GETFD或F_SETFD).
                 3. 獲得/設置文件狀態標記(cmd=F_GETFL或F_SETFL).
                 4. 獲得/設置異步I/O所有權(cmd=F_GETOWN或F_SETOWN).
                 5. 獲得/設置記錄鎖(cmd=F_GETLK , F_SETLK或F_SETLKW).
                 */
                flags = fcntl(weakSelf.sockerStatus,F_GETFL,0);
                fcntl(weakSelf.sockerStatus,F_SETFL, flags|O_NONBLOCK);
                
                //socket 設置成非阻塞狀態
                weakSelf.sockerStatus = -1;
                continue;
            }
            break;/* 連接成功跳出循環 */
        }
        
        if (weakSelf.sockerStatus < 0) {
            NSLog(@"連接失敗");//小于0時連接失敗
        }else
        {
            NSLog(@"連接成功");
        }
        
        dispatch_async(weakSelf.socketListenQueue, ^{//需要在單獨創建一個隊列去不斷的接收消息,如果不無線循環的去接收消息,那么就只能客戶端發一條消息,服務端回一條消息。因為沒有新的消息接收器
            while (weakSelf.sockerStatus >= 0) {
                [weakSelf listenMessage];
            }
        });
        freeaddrinfo(res0);//釋放對象
    });
}

- (void)listenMessage
{
    char *buf[1024];
    ssize_t recvLen = recv(_sockerStatus, buf, sizeof(buf), 0);
    NSString *recvStr = [[NSString alloc] initWithBytes:buf length:recvLen encoding:NSUTF8StringEncoding];
    NSLog(@"收到的消息:%@",recvStr);
}
Socket的Demo演示

上面只是Socket客戶端的部分代碼。建議要更深入了解socket的讀者,下載我Git上的Demo,里面有更加詳細的注釋,并且通過每個流程去深入仔細的了解socket的每個環節,包含客戶端和服務端兩個工程的代碼
socket接口說明

//socket 創建并初始化 socket,返回該 socket 的文件描述符,如果描述符為 -1 表示創建失敗。
int socket(int addressFamily, int type,int protocol)
//關閉socket連接
int close(int socketFileDescriptor)
//將 socket 與特定主機地址與端口號綁定,成功綁定返回0,失敗返回 -1。
int bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength)
//接受客戶端連接請求并將客戶端的網絡地址信息保存到 clientAddress 中。
int accept(int socketFileDescriptor,sockaddr *clientAddress, int clientAddressStructLength)
//客戶端向特定網絡地址的服務器發送連接請求,連接成功返回0,失敗返回 -1。
int connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength)
//使用 DNS 查找特定主機名字對應的 IP 地址。如果找不到對應的 IP 地址則返回 NULL。
hostent* gethostbyname(char *hostname)
//通過 socket 發送數據,發送成功返回成功發送的字節數,否則返回 -1。
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)
//從 socket 中讀取數據,讀取成功返回成功讀取的字節數,否則返回 -1。
int receive(int socketFileDescriptor,char *buffer, int bufferLength, int flags)
//通過UDP socket 發送數據到特定的網絡地址,發送成功返回成功發送的字節數,否則返回 -1。
int sendto(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)
//從UDP socket 中讀取數據,并保存發送者的網絡地址信息,讀取成功返回成功讀取的字節數,否則返回 -1 。
int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)

如果想了解心跳機制或更深入了解socket可以看下面這篇文章
iOS即時通訊,從入門到“放棄”?


WebSocket

WebSocket是一種HTML5的協議,與Socket不一樣,Socket是位于傳輸層和應用層之間的抽象接口,主要是用于方便對TCP/IP協議等使用,而WebSocket是位于應用層的協議用于對數據的包裝解析。
WebSocket 是一個基于 TCP 的協議。建立一個 WebSocket 連接,首先需要客戶端向服務器發起一個 HTTP 請求,請求頭中包含了一些WebSocket特有的頭信息,其中附加頭信息Upgrade:WebSocket表示這是一個特殊的 HTTP 請求,請求的目的就是向服務端申請,將客戶端和服務器端的通訊協議從 HTTP 協議升級到 WebSocket 協議。請求頭中的Sec-WebSocket-Key1Sec-WebSocket-Key2[8-byte securitykey]是客戶端向服務器端提供的握手信息,服務器端解析這些信息,并在握手的過程中依據這些信息生成一 個 16 位的安全密鑰并返回給客戶端,以表明服務器端獲取了客戶端的請求,同意創建 WebSocket 連接。一旦連接建立,客戶端和服務器端就可以通過這個通道雙向傳輸數據了,并且這個連接會持續存在直到客戶端或者服務器端的某一方主動的關閉連接。

目前iOS最成熟的WebSocket工具是Facebook開源的SocketRocket。
下面是本人基于SocketRocket寫的一套客戶端代碼

@interface HYJWebSocketClientManager()<SRWebSocketDelegate>
{
    /* 心跳時間 */
    dispatch_source_t webSocketHeartbeatTimer;
    /* 重連時間 */
    int reConnectTime;
    /* 當前需要連接的host */
    NSString *_host;
    /* 當前需要連接的port */
    NSString *_port;
}
/* webSocket管理者 */
@property (nonatomic, strong)SRWebSocket *webSocket;
/* webSocket執行隊列 */
@property (nonatomic, strong)NSOperationQueue *webSocketQueue;
/* 心跳隊列 */
@property (nonatomic, strong)dispatch_queue_t webSocketHeartbeatQueue;

@end

@implementation HYJWebSocketClientManager

//單例對象
+ (instancetype)sharedManager
{
    static HYJWebSocketClientManager *object = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        object = [HYJWebSocketClientManager new];
        object.status = HYJWebSocketStatus_unInit;
    });
    return object;
}

#pragma mark - PublicMethod

- (void)connectWebSocketWithHost:(NSString *_Nonnull)host Port:(NSString *_Nonnull)port
{
    if (_webSocket) {
        [_webSocket close];
        _webSocket = nil;
        [self sendWebSocketStatus:HYJWebSocketStatus_unInit];
    }
    if ([self isBlankString:host] || [self isBlankString:port]) {
        return;
    }
    _host = host;
    _port = port;
    NSURL *url = [[NSURL alloc] initWithString:[NSString stringWithFormat:@"ws://%@:%@",host,port]];
    _webSocket = [[SRWebSocket alloc] initWithURL:url];
    _webSocket.delegate = self;
    
    _webSocketQueue = [NSOperationQueue new];
    [_webSocket setDelegateOperationQueue:_webSocketQueue];
    [self sendWebSocketStatus:HYJWebSocketStatus_ing];
    [_webSocket open];
    
}

//發送消息
- (void)sendMessage:(NSString *)message
{
    NSString *messageString = [self packageMessageBag:message messageType:@"user"];
    [self.webSocket send:messageString];
}

- (void)closeWebSocket
{
    if (_webSocket) {
        [_webSocket close];
        _webSocket = nil;
        [self sendWebSocketStatus:HYJWebSocketStatus_unInit];
    }
}

#pragma mark - PrivateMethod
//啟動心跳時間
- (void)initHeartBeatEvent
{
    
    [self cancelHeartbeatEvent];

    dispatch_time_t startTimer = dispatch_time(DISPATCH_TIME_NOW, 3*60*NSEC_PER_SEC);
    uint64_t interval = 3*60*NSEC_PER_SEC;
    
    dispatch_source_set_timer(webSocketHeartbeatTimer, startTimer, interval, 0);
    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(webSocketHeartbeatTimer, ^{
        NSString *heartString = [self packageMessageBag:@"" messageType:@"user"];
        [weakSelf.webSocket send:heartString];
    });
    
    dispatch_resume(webSocketHeartbeatTimer);
}

//取消心跳時間
- (void)cancelHeartbeatEvent
{
    if (webSocketHeartbeatTimer) {
        dispatch_source_cancel(webSocketHeartbeatTimer);
        webSocketHeartbeatTimer = nil;
    }
}
//封裝消息
- (NSString *)packageMessageBag:(NSString *)message messageType:(NSString *)type
{
    NSMutableDictionary *messageDic = [NSMutableDictionary new];
    [messageDic setValue:message forKey:@"message"];
    [messageDic setValue:type forKey:@"messageType"];
    NSError *error;
    NSData *data = [NSJSONSerialization dataWithJSONObject:messageDic options:NSJSONWritingPrettyPrinted error:&error];
    if (!data || error) {
        return nil;
    }else
    {
        return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    }
}
//解析數據包
- (void)analyticalMessage:(NSString *)message
{
    if ([self isBlankString:message]) {
        //空消息不處理
        return ;
    }
    
    NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
    NSError *error;
    NSMutableDictionary *messageDic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
    if (!messageDic && error) {
        [HYJToast showToastWithText:@"收到一條消息,但解析失敗" withToastType:HYJToastType_KeyWindow withToastPointType:HYJToastPointType_Center ForView:nil];
        return;
    }
    
    NSString *messageString = [messageDic objectForKey:@"message"];
    if ([self isBlankString:messageString]) {
        [HYJToast showToastWithText:@"收到空消息" withToastType:HYJToastType_KeyWindow withToastPointType:HYJToastPointType_Center ForView:nil];
        return;
    }else
    {
        [self backMessage:messageString];
    }
}
//是否為空字符串判斷 YES表示為空字符串
- (BOOL)isBlankString:(NSString *)string
{
    if (string) {
        if ([string isKindOfClass:[NSString class]]) {
            if ([string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]].length > 0) {
                return NO;
            }else
            {
                return YES;
            }
        }else
        {
            return YES;
        }
    }else
    {
        return YES;
    }
}
//修改并更新socket連接狀態
- (void)sendWebSocketStatus:(HYJWebSocketStatus)status
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(webSocketConnetStatt:)]) {
        self.status = status;
        [self.delegate webSocketConnetStatt:status];
    }else
    {
        self.status = status;
    }
}
//通過代理返回接收到的數據
- (void)backMessage:(NSString *)message
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveMessage:)]) {
        [self.delegate didReceiveMessage:message];
    }
}

//重連機制
- (void)reConnect
{
    [self closeWebSocket];
    if ([self isBlankString:_host] || [self isBlankString:_port]) {
        return;
    }
    //限制重連最長不超過64秒
    if (reConnectTime > 64) {
        return;
    }
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf connectWebSocketWithHost:_host Port:_port];
    });
    
    
    //重連時間2的指數級增長
    if (reConnectTime == 0) {
        reConnectTime = 2;
    }else{
        reConnectTime *= 2;
    }
}

#pragma mark - SRWebSocketDelegate
//收到消息的回調
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
    //沒收到一條消息就重新開啟心跳時間
    [self initHeartBeatEvent];
    [self analyticalMessage:message];
}

//連接成功的回調
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
    //連接成功后開啟心跳時間
    [self initHeartBeatEvent];
    [self sendWebSocketStatus:HYJWebSocketStatus_success];
}

//連接發生錯誤的回調
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
    //連接失敗,這里可以實現掉線自動重連,要注意以下幾點
    //1.判斷當前網絡環境,如果斷網了就不要連了,等待網絡到來,在發起重連
    //2.判斷調用層是否需要連接,例如用戶都沒在聊天界面,連接上去浪費流量
    //3.連接次數限制,如果連接失敗了,重試10次左右就可以了,不然就死循環了。
    [self sendWebSocketStatus:HYJWebSocketStatus_fail];
    [self reConnect];
    //[self cancelHeartbeatEvent];
}

//連接被關閉的回調
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
    [self sendWebSocketStatus:HYJWebSocketStatus_unInit];
    [self cancelHeartbeatEvent];
}

/*
 該函數是接收服務器發送的pong消息,其中最后一個是接受pong消息的,
 在這里就要提一下心跳包,一般情況下建立長連接都會建立一個心跳包,
 用于每隔一段時間通知一次服務端,客戶端還是在線,這個心跳包其實就是一個ping消息,
 我的理解就是建立一個定時器,每隔十秒或者十五秒向服務端發送一個ping消息,這個消息可是是空的
 */
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload
{
    
}

//將收到的消息,是否需要把data轉換為NSString,每次收到消息都會被調用,默認YES
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket
{
    return YES;
}

#pragma mark LazyLoad
- (dispatch_queue_t)webSocketHeartbeatQueue
{
    if (!_webSocketHeartbeatQueue) {
        _webSocketHeartbeatQueue = dispatch_queue_create("HYJWebSocketHeartbeatQueue", DISPATCH_QUEUE_SERIAL);
    }
    return _webSocketHeartbeatQueue;
}

@end

大家可以在我的Git中下載該Demo,熟悉流程。


MQTT

關于MQTT的介紹借用下別人的
沒辦法網上千篇一律
一、MQTT簡介
MQTT(Message Queuing Telemetry Transport,消息隊列遙測傳輸)是IBM開發的一個即時通訊協議,該協議支持所有平臺,也就是說不論什么平臺都可以使用集成此協議,MQTT被用來當做傳感器和致動器的通信協議。MQTT基于訂閱發布模式,客戶端之間要互相通信,必須都訂閱了同一個主題(topic),客戶端之間是不能直接單獨通訊的,必須通過服務端的發布。當群發消息的時候只需要把消息發布到對應群的主題(topic)中,通過服務端去發布到該主題中的每一個用戶,所有訂閱了這個topic的客戶端就可以接收到消息了。

一、MQTT特點
MQTT協議是為大量計算能力有限,且工作在低帶寬、不可靠的網絡的遠程傳感器和控制設備通訊而設計的協議(簡單的說就是數據的傳輸流量開銷很小,三種QOS質量服務能較好保證數據傳送),它具有以下主要的幾項特性:
1、使用發布/訂閱消息模式,提供一對多的消息發布,解除應用程序耦合;
2、對負載內容屏蔽的消息傳輸;(這句話啥意思?反正我是沒太懂)
3、使用 TCP/IP 提供網絡連接;
4、有三種QoS消息發布服務質量:
QoS 0 代表,Sender (發送方)最多向Receiver(接收方)發送一次消息,如果發送失敗了就依賴于TCP的重發機制,自身不會再次重發,不會關注Receiver是否真的收到消息。
QoS 1 代表,Sender 發送的一條消息,Receiver 至少能收到一次。也就是說 Sender 會確定 Receiver 是否都到消息,如果發送失敗,自身會重發,直到 Receiver 明確的告訴Sender收到消息為止。因為是自身重發,底層由基于TCP,TCP也有重發,因此Receiver 有可能會收到重復的消息。
QoS 2 代表,在QoS 1的基礎上增加了重發校驗機制,確保消息不會重復發送,Receiver能有且僅收到一次消息。
5、小型傳輸,開銷很?。ü潭ㄩL度的頭部是 2 字節),協議交換最小化,以降低網絡流量;

  1. 使用 Last Will 和 Testament 特性通知有關各方客戶端異常中斷的機制。

基本理論差不多就這些,對于客戶端來說已經受用。如果要更加深入的了解MQTT可以看看大佬翻譯的IBM文檔,想做相關優化而不僅僅是使用就耐著性子看完吧。

目前就iOS來說,一般使用MQTT都是用的MQTTKitMQTTClient這兩個開源庫。不過MQTTKit確實很久沒有更新了,最新一次更新都是2015年9月了。所以似乎大家都用的MQTTClient。


XMPP

XMPP是應用層的協議,是以XML對數據進行封裝,基于TCP以數據流方式進行傳輸的協議。
XMPP中主要有三大角色
客戶端:對信息的封裝發送和解析接收
服務端:對信息的接收記錄轉發,因為大多數時候客戶端發送數據給另一個客戶端不是直接告訴客戶端該用戶的IP地址,而是userId。而服務端根據userId去查找該用戶的IP。想當與一個路由功能
網關:這里的網關與計算機的網關不一樣,也是XMPP的特點之一,畢竟XMPP完全開源可擴展,例如郵件方式,假設A用戶想和B通信,他們兩人的帳號分別在QQ和谷歌的服務器上。A用戶客戶端將訊息傳送到QQ服務器。QQ服務器開啟與谷歌服務器的連接。谷歌服務器將訊息寄送給B。兩人的服務器是由兩家不同的業者所提供的,而他們彼此傳訊時,就是依靠網關處理完成發送

在iOS中一般使用XMPP都是用的XMPPFramework。由于本人沒有用過這個庫,所以代碼暫時就不加進去了。等以后對這個有一定深度的掌握后加入代碼。

后續會添加MQTT和XMPP的相關代碼和Demo。

HTTP

HTTP稱之為超文本傳輸協議(HTTP,HyperText Transfer Protocol),是最為常用的網絡協議。所有的WWW文件都必須遵守這個標準。HTTP協議采用了請求/響應模型??蛻舳讼蚍掌靼l送一個HTTP請求,HTTP會把內容封裝成請求頭,請求頭中包含請求的方法、URL、協議版本、以及包含請求修飾符、客戶信息和內容的類似于MIME的消息結構。服務器以一個狀態行作為響應,響應的內容包括消息協議的版本,成功或者錯誤編碼加上包含服務器信息、實體元信息以及可能的實體內容。

客戶端向服務端發送請求所包含的內容

一個請求中包含
請求行 -> 通用信息頭 -> 請求頭 - >實體頭 ->報文主體
除了主體,主要信息是 請求行請求頭

請求行
POST /api/tool/getAreaList HTTP/1.1
第一部分POST是請求方式 常見的就GET 和 POST(Method)
第二部分/api/tool/getAreaList是請求的資源路徑
第三部分HTTP/1.1是請求的協議版本 (Protocol)

請求頭
Host www.baidu.com
Content-Type application/x-www-form-urlencoded
Accept */*
User-Agent ETI/1.0.0 (iPhone; iOS 11.4; Scale/3.00)
Accept-Language zh-Hans-CN;q=1
Content-Length 75
Accept-Encoding gzip, deflate
Connection keep-alive
Host: 訪問的主機名
Content-Type:客戶端發送的數據類型
Accept: 客戶端所能接收的數據類型,/表示任意類型
User-Agent: 客戶端的類型,客戶端的軟件環境
Accept-Language: 客戶端的語言環境
Content-Length:請求中內容的大小。這個大小是包含了內容編碼的,比如上面請求是采用gzip壓縮,那么Content-Length就是壓縮后的大小
Accept-Encoding: gzip // 客戶端支持的數據壓縮格式
Connection:告訴服務器,請求完成后是關閉還是保持鏈接

Content-Type
Content-Type一般有這三種:
application/x-www-form-urlencoded:數據被編碼為名稱/值對。這是標準的編碼格式。它會將表單內的數據轉換為鍵值對,比如,name=12&age = 18
multipart/form-data: 它會將請求表表單中的每條數據處理為一條消息,以標簽為單元,用分隔符分開。既可以上傳鍵值對,也可以上傳文件。當上傳的字段是文件時,會有Content-Type來表名文件類型;content-disposition,用來說明字段的一些信息;由于有boundary隔離,所以multipart/form-data既可以上傳文件,也可以上傳鍵值對,它采用了鍵值對的方式,所以可以上傳多個文件。
text/plain: 數據以純文本形式(text/json/xml/html)進行編碼,其中不含任何控件或格式字符。(RAW)。

formenctype屬性為編碼方式,常用有兩種:application/x-www-form-urlencodedmultipart/form-data,默認為application/x-www-form-urlencoded。
當請求方式為GET時候,瀏覽器用x-www-form-urlencoded的編碼方式把form數據部分轉換拼接成一個字符串(例如:name1=value1&name2=value2...),然后把這個字符串追加到url后面,用?分割,加載這個新的url。
當請求方式為POST的時候,瀏覽器把form數據封裝到body中,發送到服務端。 如果沒有type=file的控件,用默認的application/x-www-form-urlencoded就可以了。 但是如果有type=file的話,就要用到multipart/form-data了。Content-Type類型是multipart/form-data,瀏覽器會把整個表單以控件為單位分割,并為每個部分加上Content-Disposition(form-data或者file),Content-Type(默認為text/plain),name(控件name)等信息,并加上分割符(boundary)。

Accept-Encoding
在Http中,可以采用gzip編碼方式,對body內容進行編碼,來從而達到對內容壓縮的方式。
首先客戶端發送request給服務端, request 中有Accept-Encoding,告訴服務端數據采用gzip編碼壓縮
接著服務端接到request后, 生成原始的Response, 其中有原始的Content-TypeContent-Length。
然后通過服務端通過Gzip,來對Response進行編碼, 編碼后header中有Content-Type和Content-Length(壓縮后的大小), 并且增加了Content-Encoding:gzip. 然后把Response發送給客戶端。
最后客戶端接到Response后,會根據Content-Encoding來對Response 進行解碼。 獲取到原始response后,再根據數據類型進行解析。

gzip:表明實體采用GNU zip編碼
compress:表明實體采用Unix的文件壓縮程序
deflate表明實體是用zlib的格式壓縮的
identity表明沒有對實體進行編碼。當沒有Content-Encoding header時, 就默認為這種情況
gzip, compress, 以及deflate編碼都是無損壓縮算法,用于減少傳輸報文的大小,不會導致信息損失。 其中gzip通常效率最高, 使用最為廣泛。

Connection
HTTP協議采用“請求(request)-應答(response)”模式,當Connection為非KeepAliv時,每次請求/應答客戶端和服務端都要新建一個連接,完成請求應答之后立即斷開連接(此時HTTP協議為無連接的協議);當使用Keep-Alive(又稱持久連接、連接重用)時,Keep-Alive功能使客戶端到服 務器端的連接具有持續有效性,當出現對服務端的后繼請求時,Keep-Alive功能避免了建立或者重新建立與服務端的連接,避免了建立/釋放連接的開銷,它更高效,性能更高。

http 1.0版本中Keep-Alive默認是關閉的,需要在http頭加入Connection: Keep-Alive,才能啟用Keep-Alive;http 1.1中默認啟用Keep-Alive,如果加入Connection: close,才關閉。目前大部分瀏覽器都是用http1.1協議,也就是說默認都會發起Keep-Alive的連接請求了

服務端向客戶端返回響應結果所包含的內容

一個響應內容有
狀態行 -> 通用信息頭 -> 響應頭 -> 實體頭 -> 報文主體
主要信息是響應行響應頭

響應行
HTTP/1.1 200 OK
第一部分HTTP/1.1 表示HTTP協議版本號
第二部分200 狀態碼
第三部分OK 狀態英文名稱
響應頭
Server: nginx/1.12.2
Date: Thu, 16 Aug 2018 06:26:21 GMT
Content-Type: application/json;charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.2.8
Content-Encoding: gzip
Server:服務器類型
Date:返回響應的時間
Content-Type:返回的數據類型
Transfer-Encoding:數據傳送的方式
Connection:連接方式
X-Powered-By:是用何種語言或框架編寫
Content-Encoding: gzip // 服務端對數據的壓縮格式

X-Powered-By
聲明服務端使用何種語言或者框架編寫的

PHP標準輸出是:X-Powered-By: PHP/7.2.8,可在php.ini中增加或修改 expose_php = Off關閉。
ThinkPHP標準輸出是:X-Powered-By: ThinkPHP 2.0,可修改相關類文件關閉
.net標準輸出是:X-Powered-By:ASP.NET,可修改web.config 刪除

Transfer-Encoding
服務端返回數據的方式

在HTTP1.1后數據傳送方式都采用的chunked,分塊編碼。
分塊編碼是一種HTTP超文本協議傳輸數據的一種機制,它表示服務端傳送給客戶端的數據可以拆分為多個部分進行傳輸。數據拆分成一系列數據塊,并以一個或多個塊發送,這樣服務器可以發送數據而不需要預先知道發送內容的總大小

每個分塊包含十六進制的長度值和數據,長度值獨占一行,長度不包括它結尾的 CRLF(\r\n),也不包括分塊數據結尾的 CRLF。
最后一個分塊長度值必須為 0,對應的分塊數據沒有內容,表示實體結束。

一般Content-EncodingTransfer-Encoding 二者經常會結合來用,其實就是針對 Transfer-Encoding 的分塊再進行 Content-Encoding壓縮

以上請求體和響應體只是常規的類型。有部分少見key沒有說明。HTTP/1.0 定義了三種請求方法: GET, POST 和 HEAD方法,HTTP/1.1 新增了五種請求方法:OPTIONS、 PUT、DELETE、 TRACE 和 CONNECT 方法。HTTP/2.0僅適用于HTTPS

GET POST區別

  1. GET使用URL或Cookie傳參。而POST將數據放在BODY中。
  2. GET的URL會有長度上的限制,而POST的數據則可以非常大。
  3. POST比GET安全,因為數據在地址欄上不可見。(相對而言,這句話并不準確。討論的點)
    HTTP協議明確地指出了,HTTP頭和Body都沒有長度的要求。而對于URL長度上的限制
    使用http協議的GET方式,所有參數必須拼接放在URL的請求中,這個URL請求最大長度限制是2048個字符

http1.1與 http1.0的區別:
1、http1.1添加更多的緩存控制策略(如:Entity tag,If-Match)
2、http1.1支持斷點續傳
3、http1.1錯誤狀態碼的增多
4、http1.1Host頭處理:支持Host頭域,不在以IP為請求方標志
5、http1.1新增長連接:減少了建立和關閉連接的消耗和延遲。
http2.0與 http1.1的區別:
1、http2.0 使用二進制格式,1.0依然使用基于文本格式
2、http2.0 多路復用:連接共享,不同的request可以使用同一個連接傳輸(最后根據每個request上的id號組合成正常的請求)
3、http2.0 壓縮header:由于1.X中header帶有大量的信息,并且得重復傳輸,2.0使用encoder來減少需要傳輸的hearder大小
4、http2.0新增服務端推送


HTTPS

HTTPS是基于TCP傳輸層與HTTP應用層之間加入了SSL。讓HTTP與TCP交互之間多了一層安全控制層。
SSL又叫安全套接字層,通過互相認證、使用數字簽名確保完整性、使用加密確保私密性來保證安全通信。SSL分為兩層,SSL記錄協議層,它建立在TCP之上為高層協議提供數據的封裝、壓縮、加密等基本功能。SSL握手協議層,它建立在SSL記錄層之上,主要是在傳遞數據之前對通信雙方的身份認真并協商加密算法交換加密密鑰。在SSL3.0之后,IETF指定了一種新協議,TLS,它是SSL3.0的后續版本。TLS與SSL算法不同,記錄的版本號值不同,相比之下,TLS比SSL更加安全。

這里通過一張網上找到的HTTPS抓包圖來讓大家直觀的了解下TLS與TCP的關系

HTTPS抓包圖

前三行:是TCP常規的三次握手,建立連接。
第四行到最后一行:是TLS的四次握手。之間穿插的TCP的ACK請求是接收方為了確認接收到TLS信息而發送的確認序列,這是TCP常識。

這里詳細說明下TLS的四次握手:

第一次握手:首先客戶端會發送一個client hello信息給服務端。
其中包含:
1.客戶端支持的最高TLS的版本號
2.加密套件 cipher suites 列表, 每個加密套件對應前面 TLS 原理中的四個功能的組合:認證算法 Au (身份驗證)、密鑰交換算法 KeyExchange(密鑰協商)、對稱加密算法 Enc (信息加密)和信息摘要 Mac(完整性校驗)
3.支持的壓縮算法 compression methods 列表,用于后續的信息壓縮傳輸。
4.一個用于生成密鑰的32字節的隨機數,
5.擴展字段 extensions,支持協議與算法的相關參數以及其它輔助信息等。

第二次握手:服務端會向客戶端發送一個ServerHello應答
其中包含:
1.基于客戶端支持的最高TLS版本號,而選擇的通信使用的TLS版本號。
2.一個用于生成密鑰的32字節隨機數
3.基于客戶端的加密套件列表,而選擇的加密套件。
4.基于客戶端的壓縮算法列表,選擇的壓縮算法
5.server_certificates, 服務器端配置對應的證書鏈,用于身份驗證與密鑰交換。
6.上圖第七行,服務端發送 certificateserver hello done 通知客戶端 server_hello 信息發送結束。

這里當客戶端收到了serverHello信息結束后,客戶端會去驗證證書的合法性,如果驗證通過才會進行后續通信,否則根據錯誤情況不同做出提示和操作,合法性驗證包括如下:
? 證書鏈的可信性 trusted certificate path
? 證書是否吊銷 revocation,有兩類方式離線 CRL 與在線 OCSP,不同的客戶端行為會不同;
? 有效期 expiry date,證書是否在有效時間范圍;
? 域名 domain,核查證書域名是否與當前的訪問域名匹配,匹配規則后續分析;
驗證通過后才會進行第三次握手

第三次握手:證書的合法性驗證通過后發送消息,其中client_key_exchange是,合法性驗證通過之后,客戶端計算產生隨機數字 Pre-master,并用證書公鑰加密,發送給服務器,此時客戶端已經獲取全部的計算協商密鑰需要的信息:兩個明文隨機數與自己計算產生的 Pre-master,計算得到協商密鑰, change_cipher_spec是,客戶端通知服務器后續的通信都采用協商的通信密鑰和加密算法進行加密通信,encrypted_handshake_message是,結合之前所有通信參數的 hash 值與其它相關信息生成一段數據,采用協商密鑰 session secret 與算法進行加密,然后發送給服務器用于數據與握手驗證。

第四次握手:服務端在發送一個響應。
此時服務器用私鑰解密加密的 Pre-master 數據,基于之前交換的兩個明文隨機數,計算得到協商密鑰。計算之前所有接收信息的 hash 值,然后解密客戶端發送的 encrypted_handshake_message是,驗證數據和密鑰正確性,change_cipher_spec是, 驗證通過之后,服務器同樣發送 change_cipher_spec 以告知客戶端后續的通信都采用協商的密鑰與算法進行加密通信,encrypted_handshake_message是, 服務器也結合所有當前的通信參數信息生成一段數據并采用協商密鑰 session secret 與算法加密并發送到客戶端。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容

  • 1、TCP為什么需要3次握手,4次斷開? “三次握手”的目的是“為了防止已失效的連接請求報文段突然又傳送到了服務端...
    杰倫哎呦哎呦閱讀 3,510評論 0 6
  • 網絡編程 網絡編程對于很多的初學者來說,都是很向往的一種編程技能,但是很多的初學者卻因為很長一段時間無法進入網絡編...
    程序員歐陽閱讀 2,040評論 1 37
  • 計算機網絡概述 網絡編程的實質就是兩個(或多個)設備(例如計算機)之間的數據傳輸。 按照計算機網絡的定義,通過一定...
    蛋炒飯_By閱讀 1,240評論 0 10
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,067評論 6 13
  • 一、什么是TCP/IP 網絡和協議 1. TCP/IP是一類協議系統,它是一套支持網絡通信的協議集合。網絡是計算機...
    karlon的馬甲閱讀 6,589評論 1 24