在 Linux 中使用 Swift 進(jìn)行 TCP Sockets 編程

作者: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 中的 IPTCPServerSocket 類有可能拋出錯誤冗美。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.swiftChatterServer.swift 放進(jìn)去渠退。

運(yùn)行 swift build忙迁,它會下載和編譯 2 個(gè)依賴的庫(Tideswiftysockets),接著就會編譯我們的應(yīng)用代碼碎乃。如果編譯成功姊扔,你就可以在 .build/debug/ 目錄下找到一個(gè)可執(zhí)行的二進(jìn)制文件:chatterserver

測試

我們的下一個(gè)教程將會編寫一個(gè)簡單使用的聊天客戶端程序梅誓,在這里我們就使用 ncnetcat)命令來測試我們的服務(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赫粥,一起剝皮案震驚了整個(gè)濱河市观话,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌越平,老刑警劉巖频蛔,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喧笔,居然都是意外死亡帽驯,警方通過查閱死者的電腦和手機(jī)龟再,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門书闸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人利凑,你說我怎么就攤上這事浆劲。” “怎么了哀澈?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵牌借,是天一觀的道長。 經(jīng)常有香客問我割按,道長膨报,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮现柠,結(jié)果婚禮上院领,老公的妹妹穿的比我還像新娘。我一直安慰自己够吩,他們只是感情好比然,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著周循,像睡著了一般强法。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上湾笛,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天饮怯,我揣著相機(jī)與錄音,去河邊找鬼嚎研。 笑死硕淑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嘉赎。 我是一名探鬼主播置媳,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼公条!你這毒婦竟也來了拇囊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤靶橱,失蹤者是張志新(化名)和其女友劉穎寥袭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體关霸,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡传黄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了队寇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膘掰。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖佳遣,靈堂內(nèi)的尸體忽然破棺而出识埋,到底是詐尸還是另有隱情,我是刑警寧澤零渐,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布窒舟,位于F島的核電站,受9級特大地震影響诵盼,放射性物質(zhì)發(fā)生泄漏惠豺。R本人自食惡果不足惜银还,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洁墙。 院中可真熱鬧见剩,春花似錦、人聲如沸扫俺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狼纬。三九已至羹呵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疗琉,已是汗流浹背冈欢。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盈简,地道東北人凑耻。 一個(gè)月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像柠贤,于是被迫代替她去往敵國和親香浩。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理臼勉,服務(wù)發(fā)現(xiàn)邻吭,斷路器,智...
    卡卡羅2017閱讀 134,637評論 18 139
  • 參考:http://www.2cto.com/net/201611/569006.html TCP HTTP UD...
    F麥子閱讀 2,945評論 0 14
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫畸写、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,066評論 4 62
  • 我來了
    朋程閱讀 159評論 0 0
  • 轉(zhuǎn)眼2014已經(jīng)過去氓扛,回顧過去枯芬,我自己覺得,還是有進(jìn)步的幢尚。展望2015破停,我也還是有期待的翅楼。 2014年從年初到...
    sleepy_cat閱讀 244評論 0 1