在 Linux 中使用 Swift 進行 TCP Sockets 編程

做者:Joe,原文連接,原文日期:2016-01-03
譯者:shanks;校對:numbbbbb;定稿:Ceehtml

在遠古時代,程序員們使用 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

Swiftychat

爲了說明如何使用 Swift 來調用 TCP/IP 網絡套接字,咱們開發了 一個簡單的聊天應用:Swiftychat。不過這只是一個很初級的應用,功能有限,不能在真實環境中使用。可是,它能夠做爲一個案例,讓咱們學習如何使用 Swift 來調用 TCP/IP 網絡套接字發送和接收字符串。程序員

swiftysockets

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 中的 IPTCPServerSocket 類有可能拋出錯誤。IP 類封裝好了 IP 地址和端口,提供給 TCPServerSocket 類構造器一個 IP 類實例。若是初始化成功,咱們就能夠獲得一個指定端口上的 TCP 套接字,爲下一步的鏈接作好準備。

2. 主循環

咱們不關心主循環的名字,你叫它 startListeningstart 或者 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.swiftChatterServer.swift 放進去。

    運行 swift build,它會下載和編譯 2 個依賴的庫(Tideswiftysockets),接着就會編譯咱們的應用代碼。若是編譯成功,你就能夠在 .build/debug/ 目錄下找到一個可執行的二進制文件:chatterserver

    測試

    咱們的下一個教程將會編寫一個簡單使用的聊天客戶端程序,在這裏咱們就使用 ncnetcat)命令來測試咱們的服務器。啓動服務器,在另一個終端窗口中輸入 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

    相關文章
    相關標籤/搜索