作者:Joe,原文鏈接技矮,原文日期:2016-01-03
譯者:shanks抖誉;校對:numbbbbb;定稿:Cee
在遠(yuǎn)古時(shí)代衰倦,程序員們使用 TCP/IP 套接字(sockets)來編寫客戶端-服務(wù)器(client-server)應(yīng)用袒炉。這事發(fā)生在黑暗時(shí)代 HTTP 誕生之前。
當(dāng)然樊零,我只是開了個(gè)玩笑我磁。HTTP 的出現(xiàn)給客戶端-服務(wù)器(client-server)應(yīng)用帶來更多的變化,當(dāng)然它也是 REST 應(yīng)用的基礎(chǔ)驻襟。HTTP 帶給我們的不僅是將數(shù)據(jù)在網(wǎng)絡(luò)中打包傳輸夺艰,還包括一個(gè)一致認(rèn)可的包協(xié)議架構(gòu)(從某種程度上來講,是一個(gè)在特定端口下使用的標(biāo)準(zhǔn))沉衣∮舾保可以進(jìn)行的動作有:GET,POST豌习,PUT 等霞势。HTTP 頭部本身也使得 HTTP 協(xié)議對于開發(fā)客戶端-服務(wù)器應(yīng)用變得更加友好烹植。
接下來,在棧的底層愕贡,字節(jié)和字符都會被你操作系統(tǒng)的套接字接口處理和傳輸草雕。網(wǎng)絡(luò)套接字編程的 API 已經(jīng)很強(qiáng)大了,很多教程和書籍都和這個(gè)知識點(diǎn)有關(guān)固以。用 C 來處理 IP 網(wǎng)絡(luò)目前看來很繁瑣墩虹,但是一開始只能用它來做。之后我們使用 C++ 面向?qū)ο蟮乃枷雭戆b這些 API憨琳,從而使網(wǎng)絡(luò)編程變得更加容易诫钓。接著,出現(xiàn)了蘋果 Foundation 中的 CFStream
類篙螟,然后就是我們要用到的 swiftysockets
API菌湃。
Swiftychat
為了說明如何使用 Swift 來調(diào)用 TCP/IP 網(wǎng)絡(luò)套接字,我們開發(fā)了 一個(gè)簡單的聊天應(yīng)用:Swiftychat遍略。不過這只是一個(gè)很初級的應(yīng)用惧所,功能有限,不能在真實(shí)環(huán)境中使用绪杏。但是下愈,它可以作為一個(gè)案例,讓我們學(xué)習(xí)如何使用 Swift 來調(diào)用 TCP/IP 網(wǎng)絡(luò)套接字發(fā)送和接收字符串蕾久。
swiftysockets
Swiftychat 需要用到 swiftysockets势似,一個(gè)由 Zewo 團(tuán)隊(duì)基于 Swift 開發(fā)的 TCP/IP 套接字的包。但是由于包的限制僧著,我們不得不首先安裝一個(gè) C 庫──Tide履因。那么我們現(xiàn)在就搞起吧。
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 庫盹愚,可以和你寫的包一起進(jìn)行編譯搓逾。但是在這之前,我們必須安裝 C 庫杯拐。
安裝好 Tide 以后霞篡,我們就可以在 Swiftychat 應(yīng)用中愉快的使用 swiftysockets 了。
開始編碼端逼!
main.swift
文件中的代碼比較簡單:創(chuàng)建一個(gè) ChatterServer
并啟動它朗兵。
//main.swift
if let server = ChatterServer() {
server.start()
}
可以看到,main.swift
相當(dāng)簡單顶滩,只做了一件事情余掖,入侵……抱歉,我剛才跑偏了礁鲁,這不是星球大戰(zhàn)……
簡潔的 main.swift
意味著我們所有的實(shí)現(xiàn)都在 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 {
//
}
}
}
}
我們的服務(wù)器分解為以下幾部分代碼:
1. 初始化
我們使用可選的構(gòu)造器 init?
赁豆,這表示有可能返回 nil
,因?yàn)檎{(diào)用 swiftysockets
中的 IP
和 TCPServerSocket
類有可能拋出錯誤冗美。IP
類封裝好了 IP
地址和端口魔种,提供給 TCPServerSocket
類構(gòu)造器一個(gè) IP
類實(shí)例。如果初始化成功粉洼,我們就可以得到一個(gè)指定端口上的 TCP
套接字节预,為下一步的連接做好準(zhǔn)備。
2. 主循環(huán)
我們不關(guān)心主循環(huán)的名字属韧,你叫它 startListening
安拟、start
或者 main
都可以。主循環(huán)的任務(wù)是接收新的客戶端連接宵喂,然后把它們加入到已連接的客戶端列表中糠赦。server!.accept()
是一個(gè)阻塞方法,它會掛起并等待新連接到來锅棕。這是標(biāo)準(zhǔn)做法拙泽。
3. 客戶端管理
ChatterServer
類剩余的部分包含了所有對于客戶端管理的方法,包括一些變量和三個(gè)動作哲戚。
變量包括:
* 一個(gè)包含已連接客戶端的數(shù)組: `[TCPClientSocket]`
* 連接計(jì)數(shù)用來處理客戶端連接的標(biāo)識符
同時(shí)有以下 3 個(gè)方法:
addClient
接收一個(gè)TCPClientSocket
對象奔滑,增加連接計(jì)數(shù)艾岂,然后建立一個(gè)新的NSThread
來獨(dú)立處理當(dāng)前獲得的客戶端連接顺少。接收到新連接時(shí),它會創(chuàng)建新的NSThread
來處理它們王浴。我們在后面會介紹NSThread
的方法脆炎。當(dāng)線程啟動后,addClient
會把這個(gè)傳入的TCPClientSocket
實(shí)例加入已連接客戶端列表的末尾氓辣。removeClient
使用filter
方法從已連接客戶端列表刪除指定的客戶端連接秒裕。注意我們這里使用了!==
操作符。broadcastMessage
方法把我們的ChatterServer
變成了一個(gè)聊天服務(wù)器钞啸。方法中使用where
語句創(chuàng)建一個(gè)過濾后的數(shù)組几蜻,然后把一個(gè)客戶端的消息廣播給所有已連接的客戶端。在這里体斩,我們再次使用了!==
操作符梭稚。
再回顧一下,線程是一個(gè)在主過程中單獨(dú)的執(zhí)行路徑絮吵。服務(wù)器端創(chuàng)建一個(gè)單獨(dú)的線程來處理每個(gè)連接的客戶端弧烤。你可能會質(zhì)疑,這樣做是否合適蹬敲?如果我們的服務(wù)器最終要處理成千上萬的客戶端請求暇昂,那我也認(rèn)為這不是一個(gè)好的做法莺戒。但是對于這篇教學(xué)文章來說,我認(rèn)為已經(jīng)夠啦急波。
讓我們再看一眼線程代碼:
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()
客戶端處理線程時(shí)會進(jìn)入一個(gè)循環(huán)从铲,等待 TCPClientSocket
中的 receiveString
方法獲取客戶端的輸入。當(dāng)服務(wù)器端接收到一個(gè)字符串后幔崖,服務(wù)器端會打印到終端食店,然后廣播這個(gè)消息,如果 try
語句拋出了錯誤(斷開連接)赏寇,服務(wù)器端會刪除這個(gè)客戶端連接吉嫩。
整合所有內(nèi)容
我們的目標(biāo)是使用 Swift 包管理來編譯我們的應(yīng)用,關(guān)于 swiftpm
的介紹嗅定,請查看我們的相關(guān)教程自娩。
以下是 Package.swift
的代碼:
import PackageDescription
let package = Package(
name: "chatterserver",
dependencies: [
.Package(url: "https://github.com/iachievedit/swiftysockets", majorVersion: 0),
]
)
然后創(chuàng)建一個(gè) Sources
文件夾,把 main.swift
和 ChatterServer.swift
放進(jìn)去渠退。
運(yùn)行 swift build
忙迁,它會下載和編譯 2 個(gè)依賴的庫(Tide
和 swiftysockets
),接著就會編譯我們的應(yīng)用代碼碎乃。如果編譯成功姊扔,你就可以在 .build/debug/
目錄下找到一個(gè)可執(zhí)行的二進(jìn)制文件:chatterserver
。
測試
我們的下一個(gè)教程將會編寫一個(gè)簡單使用的聊天客戶端程序梅誓,在這里我們就使用 nc
(netcat)命令來測試我們的服務(wù)器恰梢。啟動服務(wù)器,在另外一個(gè)終端窗口中輸入 nc localhost 5555
梗掰,你可以在服務(wù)器的終端窗口中看到 Client 1 connected
嵌言。如果你用 CTRL-C 關(guān)掉客戶端窗口,服務(wù)器端會打印一個(gè)斷開連接信息及穗,并且會打印出說明信息(比如:Connection reset by peer
)摧茴。
下面進(jìn)行更加真實(shí)的測試,我們將啟用一個(gè)服務(wù)器端和三個(gè)客戶端埂陆,見下圖:
看圖中左邊的終端苛白,我們的聊天服務(wù)器正在運(yùn)行。右邊終端有 3 個(gè)客戶端焚虱,每一個(gè)都使用命令 nc localhost 5555
來啟動购裙。每個(gè)客戶端連接服務(wù)器的時(shí)候,都會在服務(wù)器端打印出連接信息著摔。
回想一下缓窜,我們的 broadcastMessage
方法中排除了廣播的發(fā)起方,這樣就避免了客戶端收到自己發(fā)送的消息(仔細(xì)看 where
語句,你就知道我在說啥了)禾锤。
下回分解
使用 nc
命令作為我們的客戶端有點(diǎn)無聊私股。我們不能使用昵稱,消息也沒有結(jié)構(gòu)可言恩掷,而且沒有時(shí)間戳之類的信息倡鲸。在上面的例子中,服務(wù)器端完全不知道我們傳過來是啥黄娘。swiftysockets
有一個(gè) TCPClientSocket
類峭状,為啥我們不可以使用它去創(chuàng)建一個(gè)更加健壯的聊天客戶端呢?
獲取代碼
我們將聊天服務(wù)器代碼上傳到了 GitHub 上逼争。這其中也包括目前暫時(shí)未實(shí)現(xiàn)的 chatterclient
項(xiàng)目优床。下載完成后,你可以在根目錄下使用 make
指令編譯服務(wù)器端和客戶端誓焦。
牢記:你必須提前安裝好 libtide.a
和對應(yīng)的頭文件胆敞,因?yàn)?swiftysockets
會用到它!
本文由 SwiftGG 翻譯組翻譯杂伟,已經(jīng)獲得作者翻譯授權(quán)移层,最新文章請?jiān)L問 http://swift.gg。