在 Linux 中使用 Swift 进行 TCP Sockets 编程

作者:Joe,原文链接,原文日期:2016-01-03
译者:shanks;校对:numbbbbb;定稿:Cee

在远古时代,程序员们使用 TCP/IP 套接字(sockets)来编写客户端-服务器(client-server)应用。这事发生在黑暗时代 HTTP 诞生之前。

当然,我只是开了个玩笑。HTTP 的出现给客户端-服务器(client-server)应用带来更多的变化,当然它也是 REST 应用的基础。HTTP 带给我们的不仅是将数据在网络中打包传输,还包括一个一致认可的包协议架构(从某种程度上来讲,是一个在特定端口下使用的标准)。可以进行的动作有:GET,POST,PUT 等。HTTP 头部本身也使得 HTTP 协议对于开发客户端-服务器应用变得更加友好。

接下来,在栈的底层,字节和字符都会被你操作系统的套接字接口处理和传输。网络套接字编程的 API 已经很强大了,很多教程书籍都和这个知识点有关。用 C 来处理 IP 网络目前看来很繁琐,但是一开始只能用它来做。之后我们使用 C++ 面向对象的思想来包装这些 API,从而使网络编程变得更加容易。接着,出现了苹果 Foundation 中的 CFStream 类,然后就是我们要用到的 swiftysockets API。

Swiftychat

为了说明如何使用 Swift 来调用 TCP/IP 网络套接字,我们开发了 一个简单的聊天应用:Swiftychat。不过这只是一个很初级的应用,功能有限,不能在真实环境中使用。但是,它可以作为一个案例,让我们学习如何使用 Swift 来调用 TCP/IP 网络套接字发送和接收字符串。

swiftysockets

Swiftychat 需要用到 swiftysockets,一个由 Zewo 团队基于 Swift 开发的 TCP/IP 套接字的包。但是由于包的限制,我们不得不首先安装一个 C 库──Tide。那么我们现在就搞起吧。

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 了。

开始编码!

main.swift 文件中的代码比较简单:创建一个 ChatterServer 并启动它。

//main.swift
if let server = ChatterServer() {
  server.start()
}

可以看到,main.swift 相当简单,只做了一件事情,入侵……抱歉,我刚才跑偏了,这不是星球大战……

简洁的 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,649评论 18 139
  • 参考:http://www.2cto.com/net/201611/569006.html TCP HTTP UD...
    F麦子阅读 2,947评论 0 14
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,089评论 4 62
  • 我来了
    朋程阅读 159评论 0 0
  • 转眼2014已经过去,回顾过去,我自己觉得,还是有进步的。展望2015,我也还是有期待的。 2014年从年初到...
    sleepy_cat阅读 248评论 0 1