在遠古時代,程序員們使用 TCP/IP 套接字(sockets)來編寫客戶端-服務器(client-server)應用。這事發生在黑暗時代 HTTP 誕生以前。linux
固然,我只是開了個玩笑。HTTP 的出現給客戶端-服務器(client-server)應用帶來更多的變化,固然它也是 REST 應用的基礎。HTTP 帶給咱們的不只是將數據在網絡中打包傳輸,還包括一個一致承認的包協議架構(從某種程度上來說,是一個在特定端口下使用的標準)。能夠進行的動做有:GET,POST,PUT 等。HTTP 頭部自己也使得 HTTP 協議對於開發客戶端-服務器應用變得更加友好。ios
接下來,在棧的底層,字節和字符都會被你操做系統的套接字接口處理和傳輸。網絡套接字編程的 API 已經很強大了,不少教程和書籍都和這個知識點有關。用 C 來處理 IP 網絡目前看來很繁瑣,可是一開始只能用它來作。以後咱們使用 C++ 面向對象的思想來包裝這些 API,從而使網絡編程變得更加容易。接着,出現了蘋果 Foundation 中的 CFStream
類,而後就是咱們要用到的 swiftysockets
API。git
爲了說明如何使用 Swift 來調用 TCP/IP 網絡套接字,咱們開發了 一個簡單的聊天應用:Swiftychat。不過這只是一個很初級的應用,功能有限,不能在真實環境中使用。可是,它能夠做爲一個案例,讓咱們學習如何使用 Swift 來調用 TCP/IP 網絡套接字發送和接收字符串。程序員
Swiftychat 須要用到 swiftysockets,一個由 Zewo 團隊基於 Swift 開發的 TCP/IP 套接字的包。可是因爲包的限制,咱們不得不首先安裝一個 C 庫──Tide。那麼咱們如今就搞起吧。github
bash $ git clone https://github.com/iachievedit/Tide Cloning into 'Tide'... ... $ cd Tide $ sudo make install clang -c Tide/tcp.c Tide/ip.c Tide/utils.c ar -rcs libtide.a *.o rm *.o mkdir -p tide/usr/local/lib mkdir -p tide/usr/local/include/tide cp Tide/tcp.h Tide/ip.h Tide/utils.h Tide/tide_swift.h tide/usr/local/include/tide # copy .a cp libtide.a tide/usr/local/lib/ mkdir -p /usr/local cp -r tide/usr/local/* /usr/local/
小道消息稱將來 Swift 包管理會支持編譯 C 庫,能夠和你寫的包一塊兒進行編譯。可是在這以前,咱們必須安裝 C 庫。編程
安裝好 Tide 之後,咱們就能夠在 Swiftychat 應用中愉快的使用 swiftysockets 了。swift
main.swift
文件中的代碼比較簡單:建立一個 ChatterServer
並啓動它。數組
//main.swift if let server = ChatterServer() { server.start() }
能夠看到,main.swift
至關簡單,只作了一件事情,入侵……抱歉,我剛纔跑偏了,這不是星球大戰……bash
簡潔的 main.swift
意味着咱們全部的實現都在 ChatterServer
類中,代碼以下:
import swiftysockets import Foundation class ChatterServer { private let ip:IP? private let server:TCPServerSocket? init?() { do { self.ip = try IP(port:5555) self.server = try TCPServerSocket(ip:self.ip!) } catch let error { print(error) return nil } } func start() { while true { do { let client = try server!.accept() self.addClient(client) } catch let error { print(error) } } } private var connectedClients:[TCPClientSocket] = [] private var connectionCount = 0 private func addClient(client:TCPClientSocket) { self.connectionCount += 1 let handlerThread = NSThread(){ let clientId = self.connectionCount print("Client \(clientId) connected") while true { do { if let s = try client.receiveString(untilDelimiter: "\n") { print("Received from client \(clientId): \(s)", terminator:"") self.broadcastMessage(s, except:client) } } catch let error { print ("Client \(clientId) disconnected: \(error)") self.removeClient(client) return } } } handlerThread.start() connectedClients.append(client) } private func removeClient(client:TCPClientSocket) { connectedClients = connectedClients.filter(){$0 !== client} } private func broadcastMessage(message:String, except:TCPClientSocket) { for client in connectedClients where client !== except { do { try client.sendString(message) try client.flush() } catch { // } } } }
咱們的服務器分解爲如下幾部分代碼:
1. 初始化
咱們使用可選的構造器 init?
,這表示有可能返回 nil
,由於調用 swiftysockets
中的 IP
和 TCPServerSocket
類有可能拋出錯誤。IP
類封裝好了 IP
地址和端口,提供給 TCPServerSocket
類構造器一個 IP
類實例。若是初始化成功,咱們就能夠獲得一個指定端口上的 TCP
套接字,爲下一步的鏈接作好準備。
2. 主循環
咱們不關心主循環的名字,你叫它 startListening
、start
或者 main
均可以。主循環的任務是接收新的客戶端鏈接,而後把它們加入到已鏈接的客戶端列表中。server!.accept()
是一個阻塞方法,它會掛起並等待新鏈接到來。這是標準作法。
3. 客戶端管理
ChatterServer
類剩餘的部分包含了全部對於客戶端管理的方法,包括一些變量和三個動做。
變量包括:
一個包含已鏈接客戶端的數組: [TCPClientSocket]
鏈接計數用來處理客戶端鏈接的標識符
同時有如下 3 個方法:
addClient
接收一個 TCPClientSocket
對象,增長鏈接計數,而後創建一個新的 NSThread
來獨立處理當前得到的客戶端鏈接。接收到新鏈接時,它會建立新的 NSThread
來處理它們。咱們在後面會介紹 NSThread
的方法。當線程啓動後,addClient
會把這個傳入的 TCPClientSocket
實例加入已鏈接客戶端列表的末尾。
removeClient
使用 filter
方法從已鏈接客戶端列表刪除指定的客戶端鏈接。注意咱們這裏使用了 !==
操做符。
broadcastMessage
方法把咱們的 ChatterServer
變成了一個聊天服務器。方法中使用 where
語句建立一個過濾後的數組,而後把一個客戶端的消息廣播給全部已鏈接的客戶端。在這裏,咱們再次使用了 !==
操做符。
再回顧一下,線程是一個在主過程當中單獨的執行路徑。服務器端建立一個單獨的線程來處理每一個鏈接的客戶端。你可能會質疑,這樣作是否合適?若是咱們的服務器最終要處理成千上萬的客戶端請求,那我也認爲這不是一個好的作法。可是對於這篇教學文章來講,我認爲已經夠啦。
讓咱們再看一眼線程代碼:
let handlerThread = NSThread(){ let clientId = self.connectionCount print("Client \(clientId) connected") while true { do { if let s = try client.receiveString(untilDelimiter: "\n") { print("Received from client \(clientId): \(s)", terminator:"") self.broadcastMessage(s, except:client) } } catch let error { print ("Client \(clientId) disconnected: \(error)") self.removeClient(client) return } } } handlerThread.start()
客戶端處理線程時會進入一個循環,等待 TCPClientSocket
中的 receiveString
方法獲取客戶端的輸入。當服務器端接收到一個字符串後,服務器端會打印到終端,而後廣播這個消息,若是 try
語句拋出了錯誤(斷開鏈接),服務器端會刪除這個客戶端鏈接。
咱們的目標是使用 Swift 包管理來編譯咱們的應用,關於 swiftpm
的介紹,請查看咱們的相關教程。
如下是 Package.swift
的代碼:
import PackageDescription let package = Package( name: "chatterserver", dependencies: [ .Package(url: "https://github.com/iachievedit/swiftysockets", majorVersion: 0), ] )
而後建立一個 Sources
文件夾,把 main.swift
和 ChatterServer.swift
放進去。
運行 swift build
,它會下載和編譯 2 個依賴的庫(Tide
和 swiftysockets
),接着就會編譯咱們的應用代碼。若是編譯成功,你就能夠在 .build/debug/
目錄下找到一個可執行的二進制文件:chatterserver
。
咱們的下一個教程將會編寫一個簡單使用的聊天客戶端程序,在這裏咱們就使用 nc
(netcat)命令來測試咱們的服務器。啓動服務器,在另一個終端窗口中輸入 nc localhost 5555
,你能夠在服務器的終端窗口中看到 Client 1 connected
。若是你用 CTRL-C 關掉客戶端窗口,服務器端會打印一個斷開鏈接信息,而且會打印出說明信息(好比:Connection reset by peer
)。
下面進行更加真實的測試,咱們將啓用一個服務器端和三個客戶端,見下圖:
看圖中左邊的終端,咱們的聊天服務器正在運行。右邊終端有 3 個客戶端,每個都使用命令 nc localhost 5555
來啓動。每一個客戶端鏈接服務器的時候,都會在服務器端打印出鏈接信息。
回想一下,咱們的 broadcastMessage
方法中排除了廣播的發起方,這樣就避免了客戶端收到本身發送的消息(仔細看 where
語句,你就知道我在說啥了)。
使用 nc
命令做爲咱們的客戶端有點無聊。咱們不能使用暱稱,消息也沒有結構可言,並且沒有時間戳之類的信息。在上面的例子中,服務器端徹底不知道咱們傳過來是啥。swiftysockets
有一個 TCPClientSocket
類,爲啥咱們不可使用它去建立一個更加健壯的聊天客戶端呢?
咱們將聊天服務器代碼上傳到了 GitHub 上。這其中也包括目前暫時未實現的 chatterclient
項目。下載完成後,你能夠在根目錄下使用 make
指令編譯服務器端和客戶端。
牢記:你必須提早安裝好 libtide.a
和對應的頭文件,由於 swiftysockets
會用到它!
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。