本文是介紹Socket系列的第一篇html
做爲半路出家的非CS(Computer Science)專業的iOS程序猿,對於計算機網絡的相關知識很是薄弱。linux
緣由也是很簡單,若是不更深刻的瞭解網絡,而是隻知道如何使用AFNetworking
、Alamofire
等等的三方網絡庫,那麼咱們如何才能成長?git
socket 是一種抽象的定義,咱們廣義上的計算機網絡系統有一個7層模型github
層 | OSI定義 |
---|---|
7 | 應用層 |
6 | 表示層 |
5 | 會話層 |
4 | 傳輸層 |
3 | 網絡層 |
2 | 數據鏈路層 |
1 | 物理層 |
這裏,socket 表明的是其中的五、6層,也就是會話層、表示層面試
其中會話層負責創建客戶端和服務端(通常稱主動發起鏈接的一方爲客戶端,另外一方爲服務端)的鏈接objective-c
表示層負責數據格式的轉化、加密解密等操做緩存
看到這裏,就有可能產生疑惑了,socket到底是什麼呢?服務器
在廣義的定義下(全部操做系統),socket就是一個普通的c語言的頭文件(.h),這個頭文件中定義了一些數據結構體,一些操做函數(建立、鏈接...)。 不一樣的廠商,不一樣的系統對於這個頭文件的實現細節(.c或者用objective-c中的表示.m)均不相同。網絡
在類Unix系統下,socket抽象類的具象實現通常封裝了一些底層的鏈接協議如(TCP/IP,UDP/IP等)數據結構
可是做爲上層程序猿的咱們沒必要去關心他們的是如何實現的,只須要知道,這個頭文件中的函數的功能,就能夠藉助這個抽象實現網絡通訊的功能。
socket是很抽象的定義,在linux下,咱們可使用netstat -anp
查看 socket鏈接。通常來講,socket是成對出現的(客戶端和服務端)。
一個例子:
socket在現實中最像的就是打電話了,電話想要撥通,雙方都得有一部電話(socket端口號),電話線(物理層鏈接)。socket鏈接就像是打電話
常常會有面試官問,有沒有使用過socket長鏈接?
這個時候可能就會有疑問了,socket不是一個抽象的定義麼,怎麼能和長鏈接結合起來?
一般狀況下,若是說到了socket長鏈接,他們通常特指TCP/IP鏈接協議,這個協議是面向鏈接的可靠的數據流服務,它能夠維持長時間,持續的交換數據包。於是咱們所說的長鏈接即是基於這個協議實現的,同時socket又爲咱們實現了這個功能,所以,這個鏈接協議的長時間維持,也被稱爲socket長鏈接
與長鏈接是長時間維持住鏈接以方便持續發送數據,可是不少時候咱們並不須要長時間發送,咱們只須要確認咱們發送了,服務器處理了,就能夠了。因此,短鏈接就產生了,它是TCP/IP鏈接協議的使用完即關閉。
咱們的http協議即是基於此而來
1. socket測試工具的準備
用法:
須要安裝Java jdk, 安裝完成以後直接點擊jar包便可
我是分割線--------------------------------------
如今的iOS系統是類Unix,所以它的socket是BSD socket,咱們介紹的也將是BSD socket的使用
注: 接下來的全部代碼演示都是Swift
本文的前言部分: 關於Swift指針的那些事
2. socket的建立
let socketFD = Darwin.socket(domain, SOCK_STREAM, 0)
複製代碼
由於全部的socket相關操做都是Module:Darwin
提供的,咱們爲了防止函數命名衝突,所以在函數前加上Module名稱
domain: 地址描述
AF_INET
: 表明IPv4協議(例:192.0.0.1)AF_INET6
: 表明IPv6協議(例:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789)SOCK_STREAM: 表明着創建鏈接的類型
SOCK_STREAM:
表明創建流鏈接,基於TCP,這個鏈接創建後對於數據傳輸會有保障,當咱們但願傳輸文件或者須要作一些確保數據能被接收的操做的時候,咱們會使用這個。
SOCK_DGRAM:
創建的數據鏈接(對方是否接收數據包無保證),基於UDP,鏈接創建後,沒法確保數據包準確送達
0: 表明着協議類型,若是未肯定填0便可
這個字段並不進行特殊說明(由於我也沒有太清楚這個參數的含義-_-)
返回值: Int32
返回值爲一個Int32
的整數,須要記錄下來,它是操做socket的句柄。
這裏,我對這個返回值的理解是,是有可能,有一個全局的鏈表,專門記錄着對應須要操做的socket,返回給咱們的就是這個socket所對應的index,咱們接下來全部的操做都是基於這個index的
若返回值爲-1,則表示socket建立失敗,具體的錯誤碼能夠經過Darwin.errno
獲取
2. socket的設置
socket在創建的時候是有一系列默認的設置的,好比默認socket的操做(讀寫)是阻塞的(須要等待操做完成,函數纔有返回值),可是做爲服務器的咱們並不但願被阻塞,因此socket提供了對應的修改方法
// 設置非阻塞
// var status = Darwin.fcntl(socketFD, F_SETFL, O_NONBLOCK)
// 設置socket重用
// var resultOn = 1
//status = setsockopt(socketFD,
// SOL_SOCKET,
// SO_REUSEADDR,
// &resultOn,
// socklen_t(MemoryLayout.size(ofValue: resultOn)))
複製代碼
具體的setsockopt()操做細節:
請參照百度百科:baike.baidu.com/item/setsoc…
3. socket鏈接(客戶端)
在進行鏈接前,咱們須要作一個場外操做,就是打開咱們的socket測試工具,而且開啓TCP server 設置端口爲9090
// 這個函數的做用是將 "127.0.0.1" 轉化爲 socket 所需的UInt32 整形
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)))
}
var sock4: sockaddr_in = sockaddr_in()
sock4.sin_len = __uint8_t(MemoryLayout.size(ofValue: sock4))
// 將ip轉換成UInt32
sock4.sin_addr = converIPToUInt32(a: 127, b: 0, c: 0, d: 1)
// 因內存字節和網絡通信字節相反,顧咱們須要交換大小端 咱們鏈接的端口是9090
sock4.sin_port = CFSwapInt16HostToBig(9090)
// 設置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)")
}
複製代碼
這裏主要使用到的函數是Darwin.connect()
代碼中咱們須要的流程是
1). 找出以前得到的socket操做句柄
2). 組裝出sockaddr_in結構體,並賦值
3). 將socketaddr_in強轉爲sockaddr(由於connect須要此類型指針)
4). 調用connect()函數並傳參
5). 根據返回值判斷鏈接成功(返回值不能是-1,通常成功即返回0)
這陣咱們從測試軟件上已經能看到socket已經鏈接上了
4. 發送數據包
在步驟3中,咱們已經完成了socket的鏈接創建,操做結束後咱們已經經歷了TCP的3次握手,可是做爲上層的調用者,咱們對此並沒有感知,在此我特意單獨說一下
接下來就是發送或者接收數據了
發送數據
// 這裏在"你好"前面增長一個空格,是爲了不SocketTest3 識別文字亂碼
let data = " 你好,我是阿帕奇".data(using: .utf8) ?? Data()
// 咱們將data轉爲rawPointer指針 也就是c語言中的 (void *) 指針
let rawPointer = data.withUnsafeBytes({UnsafeRawPointer($0)})
// 咱們須要提供指針(數據的首地址) 和 數據的長度給函數
var resultWrite = Darwin.write(socketFD, rawPointer, data.count)
guard resultWrite != -1 else {
fatalError("Error in write() function code is \(errno)")
}
複製代碼
寫數據仍是很是簡單的,可是對於Swift來講,就費事在了數據轉指針的地方
接收數據
// 初始化數據接收區
let readData = Data(count: 500)
// 咱們將data轉爲rawPointer指針
let readRawPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)})
// 最後的長度爲 緩存區的長度
let resultRead = Darwin.read(socketFD, readRawPointer, readData.count)
// 打印socket的返回值
print("\(String(data: readData, encoding: .utf8) ?? "")")
複製代碼
接收數據須要咱們在socketTest上輸入信息並點擊send,隨後咱們就能夠在打印控制檯中看到對應的返回值了
其實socket並不神祕,咱們對socket感受神祕,只是由於咱們一直用的都是socket的高級用法,對socket的一些高級封裝,使咱們感覺不到socket的存在,神祕感由此而生。
咱們在真正的使用過程當中,Objective-c有一個庫CocoaAsyncSocket是對socket的高級封裝;
我基於CocoaAsyncSocket重寫的SwiftAsyncSocket庫在歷時一個多月的時間裏,用純Swift語言的形式重寫完成,重構部分邏輯使得代碼更容易閱讀,使用方法目前和CocoaAsyncSocket同樣。
寫這個庫的緣由也很簡單,學習Swift同時學習Socket。
但願你們也能試試個人這個庫,若是有問題,也請多交流。
SwiftAsyncSocket開源倉庫地址:
若是以爲還好用,但願能給star哦,若是發現不足,請在github中提issue
其實在網絡的模型中,socket層已經算是很高級的封裝了,咱們在使用socket的過程當中,已經對於網絡ip尋址、數據丟失重發等等的操做無感知了。
咱們如今前行的每一步,都是前人爲咱們鋪好的道路。
文章中若是有錯誤,還請各位評論指出
本文首發於,本人博客與公衆號(見下圖),若是但願轉載到公衆號,請聯繫本人開通權限。
最後,祝你們新年快樂,心想事成