iOS流式即时通信教程

前言

本文翻译自Real-Time Communication with Streams Tutorial for iOS
翻译的不对的地方还请多多包涵指正,谢谢~html

iOS流式即时通信教程

从时间初始,人们就已开始梦想着更好地跟遥远的兄弟通信的方式。从信鸽到无线电波,咱们一直在努力将通信变得更清晰更高效。ios

在现代中,一种技术已成为咱们寻求相互理解的重要的工具:简易网络套接字。git

现代网络基础结构的第四层,套接字是任何从文本编辑到游戏在线通信的核心。github

为什么是套接字

你可能会奇怪,“为何不优先使用URLSession而选择低级API?”。若是你没以为奇怪,能够伪装你以为......web

好问题^_^ URLSession通信是基于HTTP网络协议。使用HTTP,通信是以【请求-响应】方式进行。这意味着在大部分App大多数网络代码都遵循如下模式:shell

  1. server端请求JSON数据
  2. 在代理方法内接收并使用JSON

但当你但愿server告诉App一些事情是怎么办嘞?对于这种事情HTTP确实处理的不太好。诚然,你能够经过不断请求server看是否有更新来实现,也叫轮询,或者你能够更狡猾点使用长轮询,但这些技术都感受不那么天然且都有本身的缺陷。最后,为何要限制本身必定要使用请求-响应的范式若是它不是一个合适的工具嘞?编程

注:长轮询 ---- 原文没有swift

长轮询是传统轮旋技术的变种,能够模拟信息从服务端推送到客户端。使用长轮询,客户端像普通的轮询同样请求服务端。但当服务端没有任何信息能够给到服务端时,server会持有这个请求等待可用的信息而不是发送一个空信息给客户端。一旦server有可发送的信息(或者超时),就发送一个响应给客户端。客户端一般会收到信息后当即在请求server,这样服务基本会一致有一个等待中的用于响应客户端的请求。在web/AJAX中,长链接被叫作Comet浏览器

长轮询自己并非一个推送技术,但能够用于在长链接不可能实现的状况下使用。安全

在这篇流式教程中,你将会学习如何使用套接字直接建立一个实时的聊天应用。

程序中不是每一个客户端都去检查服务端是否有更新,而是使用在聊天期间持续存在的输入输出流。

开始~

开始前,下载这个启动包,包含了聊天App和用Go语言写的server代码。你不用担忧本身须要写Go代码,只需启动server用来跟客户端交互。

启动并运行server

server代码是使用Go写完的而且已帮你编译好。假如你不相信从网上下载的已编译好的可执行文件,文件夹中有源代码,你能够本身编译。

为了运行已编译好的server,打开你的终端,切到下载的文件夹并输入如下命令,并接下来输入你的开机密码:

sudo ./server
复制代码

在你输入完密码后,应该能看到 Listening on 127.0.0.1:80。聊天server开始运行啦~ 如今你能够调到下个章节了。

假如你想本身编译Go代码,须要用Homebrew安装Go

没有Homebrew工具的话,须要先安装它。打开终端,复制以下命令贴到终端。

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)
复制代码

而后,使用以下命令安装Go

brew install go
复制代码

一旦完成安装,切到下载的代码位置并在终端使用以下编译命令:

go build server.go
复制代码

最终,你能够启动server,使用上述启动服务器的代码。

瞅瞅现有的App

下一步,打开DogeChat工程,编译并运行,你会看到已经帮你写好的界面:

如上图所示,DogeChat已经写好能够容许用户输入名字后进入到聊天室。不幸的是,前一个工程师不知道怎么写聊天App所以他写完了全部的界面和基本的跳转,留下了网络层部分给你。

建立聊天室

在开始编码前,切到 ChatRoomViewController.swift 文件。你能够看到你有了一个界面处理器,它能接收来自输入栏的信息,也能够经过使用Message对象配置cell的TableView来展现消息。

既然你已经有了ViewController,那么你只须要建立一个ChatRoom来处理繁重的工做。

开始写新类前,我想快速列举下新类的功能。对于它,咱们但愿能处理这些事情:

  1. 打开聊天室服务器的链接
  2. 容许经过提供名字来进入聊天室
  3. 用户可以收发信息
  4. 当时完成时关闭链接

如今你知道你该作什么啦,点击Command+N建立新的文件。选择Cocoa Touch Class并将它命名为ChatRoom

建立输入输出流

如今,继续并替换在文件内的内容以下:

import UIKit

class ChatRoom: NSObject {
  //1
  var inputStream: InputStream!
  var outputStream: OutputStream!
  
  //2
  var username = ""
  
  //3
  let maxReadLength = 4096
  
}
复制代码

这里,你定义了ChatRoom类,并声明了为使沟通更高效的属性。

  1. 首先,你有了输入输出流。使用这对类可让你建立基于app和server的套接字。天然地,你会经过输出流来发送消息,输出流接收消息。
  2. 下一步,你定义了username变量用于存储当前用户的名字
  3. 最后定义了maxReadLength。该变量限制你单次发送信息的数据量

而后,切到ChatRoomViewController.swift并在类的内部商法添加ChatRoom属性:

let chatRoom = ChatRoom()
复制代码

目前你已经构建了类的基础结构,是时候开始你以前列举类功能的第一项了---打开server与App间的链接。

开启链接

返回到ChatRoom.swift文件在属性定义的下方,加入如下代码:

func setupNetworkCommunication() {
  // 1
  var readStream: Unmanaged<CFReadStream>?
  var writeStream: Unmanaged<CFWriteStream>?

  // 2
  CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
                                     "localhost" as CFString,
                                     80,
                                     &readStream,
                                     &writeStream)
}
复制代码

这里发生了:

  1. 第一段,建立了两个未初始化的且不会自动内存管理的套接字流
  2. 将读写套接字联系起来并将其连上主机的套接字,这里的端口号是80。
    这个函数传入四个参数,第一个是你要用来初始化流的分配类型。尽量地使用kCFAllocatorDefault,但若是遇到你但愿它有不一样表现的时候有其余的选项。

下一步,你指定了hostname。此时你只须要链接本地机器,但若是你有远程服务得指定IP,你能够在此使用它。

而后,你指定了链接经过80端口,这是在server端设定的一个端口号。

最后,你传入了读写的流指针,这个方法能使用已链接的内部的读写流来初始化它们。

如今你已得到了出过后的流,你能够经过添加如下两行代码存储它们的引用:

inputStream = readStream!.takeRetainedValue()
outputStream = writeStream!.takeRetainedValue()
复制代码

在不受管理的对象上调用takeRetainedValue()可让你同步得到一个保留的引用而且消除不平衡的保留(an unbalanced retain),所以以后内存不会泄露。如今当你须要流时你可使用它们啦。

下一步,为了让app可以合理地响应网络事件,这些流须要添加进runloop内。在setupNetworkCommunication函数内部最后添加如下两行代码:

inputStream.schedule(in: .current, forMode: .commonModes)
outputStream.schedule(in: .current, forMode: .commonModes)
复制代码

你已经准备好打开“洪流之门”了~ 开始吧,添加如下代码(还在setupNetworkCommunication函数内部最后):

inputStream.open()
outputStream.open()
复制代码

这就是所有啦。咱们回到ChatRoomViewController.swift类,在viewWillAppear函数内添加以下代码:

chatRoom.setupNetworkCommunication()
复制代码

在本地服务器上,如今你已打开了客户端和服务端链接。再次编译运行代码,将会看到跟你写代码以前如出一辙的界面。

参与聊天

如今你已连上了服务端,是时候发一些消息了~ 第一件事情你可能会说我究竟是谁。以后,你也但愿开始发送信息给其余人了。

这里提出了一个重要的问题:由于你有两种消息,须要想个办法来区分他们。

通讯协议

降到TCP层好处之一是你能够定义本身的协议来决定一个信息的有效与否。对于HTTP,你须要想到这些烦人的动做:GetPUTPATCH。须要构造URL并使用合适的头部和各类各样的事情。

这里咱们以后两种信息,你能够发送:

iam:Luke
复制代码

来进入聊天室并通知世界你的名字。你能够说:

msg:Hey, how goes it mang?
复制代码

来发送一个消息给任何一个在聊天室的人。

这样纯粹且简单。

这样显然不安全,所以不要在工做中使用它。

你知道了服务器的指望格式,能够在ChatRoom写一个方法来进入聊天室了。仅有的参数就是名字了。

为实现它,添加以下方法到刚添加的方法后面:

funcfunc  joinChatjoinChat(username: String)(username: String) {
   {   //1//1
     letlet data =  data = "iam:"iam:\(username)\(username)"".data(using: .ascii)!
  .data(using: .ascii)!   //2//2
     selfself.username = username
  
  .username = username      //3//3
     __ = data.withUnsafeBytes { outputStream.write($ = data.withUnsafeBytes { outputStream.write($00, maxLength: data., maxLength: data.countcount) }
}) } }
复制代码
  1. 首先,使用简单的聊天协议构造了消息
  2. 而后,保存了刚传进来的名字,以后能够在发送消息的时候使用它
  3. 最后,将消息写入输出流。这比你预想的要复杂一些,write(_:maxLength:)方法将一个不安全的指针引用做为第一个参数。withUnsafeBytes(of:_:)方法提供一个很是便利的方式在闭包的安全范围内处理一些数据的不安全指针。

方法已就绪,回到ChatRoomViewController.swift并在viewWillAppear(_:)方法内最后添加进入聊天室的方法调用。

chatRoom.joinChat(username: username)
复制代码

如今编译并运行,输入名字进入界面看看:

一样什么也没发生?

稍等,我来解释下~ 去看看终端程序。就在 Listening on 127.0.0.1:80 下方,你会看到 Luke has joined,或若是你的名字不是Luke的话就是其余的内容。

这是个好消息,但你确定更但愿看到在手机屏幕上成功的迹象。

响应即未来临的消息

幸运的是,服务器接收的消息就像你刚刚发送的同样,而且发送给在聊天的每一个人,包括你本身。更幸运的是,app本就已可在ChatRoomViewController的表格界面上展现即将要来的消息。

全部你要作的就是使用inputStream来捕捉这些消息,将其转换成Message对象,并将它传出去让表格作显示。

为响应消息,第一个须要作的事情是让ChatRoom成为输入流的代理。首先,到ChatRoom.swift最底部添加如下扩展:

extension ChatRoom: StreamDelegate {

}
复制代码

如今ChatRoom已经采用了StreamDelegate协议,能够申明为inputStream的代理了。

添加如下代码到setupNetworkCommunication()方法内,而且恰好在schedule(_:forMode:)方法以前。

inputStream.delegate = self
复制代码

下一步,在扩展中添加stream(_:handle:)的实现:

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case Stream.Event.hasBytesAvailable:
      print("new message received")
    case Stream.Event.endEncountered:
      print("new message received")
    case Stream.Event.errorOccurred:
      print("error occurred")
    case Stream.Event.hasSpaceAvailable:
      print("has space available")
    default:
      print("some other event...")
      break
    }
}
复制代码

这里你处理了即未来的可能在流上会发生的事件。你最感兴趣的一个应该是Stream.Event.hasBytesAvailable,由于这意味着有消息须要你读~

下一步,写一个处理即未来的消息的方法。在下面方法下添加:

private func readAvailableBytes(stream: InputStream) {
  //1
  let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength)
  
  //2
  while stream.hasBytesAvailable {
    //3
    let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength)
    
    //4
    if numberOfBytesRead < 0 {
      if let _ = stream.streamError {
        break
      }
    }

    //Construct the Message object
    
  }
}
复制代码
  1. 首先,建立一个缓冲区,能够用来读取消息字节
  2. 下一步,一直循环到输入流没有字节读取了为止
  3. 在每一步循环中,调用read(_:maxLength:)方法读取流中的字节并将它放入传进来的缓冲区中
  4. 若是读取的字节数小于0,说明错误发生并退出

该方法须要在输入流有字节可用的时候调用,所以在stream(_:handle:)内的Stream.Event.hasBytesAvailable中调用这个方法:

readAvailableBytes(stream: aStream as! InputStream)
复制代码

此时,你得到了一个充满字节的缓冲区!在完成这个方法前,你须要写另外一个辅助方法将缓冲区编程Message对象。

将以下代码放到readAvailableBytes(_:)后面:

private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>, length: Int) -> Message? {
  //1
  guard let stringArray = String(bytesNoCopy: buffer,
                                 length: length,
                                 encoding: .ascii,
                                 freeWhenDone: true)?.components(separatedBy: ":"),
    let name = stringArray.first,
    let message = stringArray.last else {
      return nil
  }
  //2
  let messageSender:MessageSender = (name == self.username) ? .ourself : .someoneElse
  //3
  return Message(message: message, messageSender: messageSender, username: name)
}
复制代码
  1. 首先,使用缓冲区和长度初始化一个String对象。设置该对象是ASCII编码,并告诉对象在使用完缓冲区的时候释放它,并使用:符号来分割消息,所以你就能够分别得到名字和消息。
  2. 下一步,你知道你或者其余人基于名字发送了一个消息。在真是的app中,可能会但愿用一个独特的令牌来区分不一样的人,但在这里这样就能够了。
  3. 最后,使用刚才得到的字符串构造Message对象并返回

readAvailableBytes(_:)方法的最后添加如下if-let代码来使用构造Message的方法:

if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) {
  //Notify interested parties
  
}
复制代码

此时,你已准备将Message发送给某人了,可是谁呢?

建立ChatRoomDelegate协议

OK,你确定但愿告诉ChatRoomViewController.swift新的消息来了,但你并无它的引用。由于它持有了ChatRoom的强引用,你不但愿显示地申明一个ChatRoomViewController属性来建立引用循环。

这是使用代理协议的绝佳时刻。ChatRoom不关系哪一个对象想知道新消息,它就是负责告诉某人就好。

ChatRoom.swift的顶部,添加下面简单的协议定义:

protocol ChatRoomDelegate: class {
  func receivedMessage(message: Message)
}
复制代码

下一步,添加weak可选属性来保留一个任何想成为ChatRoom代理的对象引用。

weak var delegate: ChatRoomDelegate?
复制代码

如今,回到readAvailableBytes(_:)方法并在if-let内添加下面的代码:

delegate?.receivedMessage(message: message)
复制代码

为完成它,回到ChatRoomViewController.swift并在MessageInputDelegate代理扩展下面添加对ChatRoomDelegate的扩展

extension ChatRoomViewController: ChatRoomDelegate {
  func receivedMessage(message: Message) {
    insertNewMessageCell(message)
  }
}
复制代码

就像我以前说的,其他的工做都已经帮你作好了,insertNewMessageCell(_:)方法会接收你的消息并妥善地添加合适的cell到表格上。

如今,在viewWillAppear(_:)内调用它的super代码后将界面控制器设置为ChatRoom的代理。

chatRoom.delegate = self
复制代码

再一次编译运行,输入你的名字进入到聊天页面:

聊天室如今成功展现了一个代表你进入聊天室的cell。你正式地发送了一条消息并接收了来自基于套接字TCP服务器的消息。

发送消息

是时候容许用户发送真正的文本消息啦~

回到ChatRoom.swift并在类定义的底部添加以下代码:

func sendMessage(message: String) {
  let data = "msg:\(message)".data(using: .ascii)!
  
  _ = data.withUnsafeBytes { outputStream.write($0, maxLength: data.count) }
}
复制代码

该方法就像以前写的joinChat(_:)方法,将你发送的msg转成做为真正消息的文本。

由于你但愿在inputBar告诉ChatRoomViewController用户已点击Send按钮时发送消息,回到ChatRoomViewController.swift并找到MessageInputDelegate的扩展。

这里,你会找到一个叫sendWasTapped(_:)的空方法。为了真正来发送消息,直接就将它传给chatRoom

chatRoom.sendMessage(message: message)
复制代码

这就是发送功能的所有啦~ server将会收到消息并将其转发给任何人,ChatRoom将会与以加入房间的方式被通知到消息。

再次运行并发送消息:

若你想看到别人在这里聊天,打开一个新的终端,并输入:

telnet localhost 80
复制代码

这样容许你用命令行的方式链接到TCP服务器。如今那里能够发送跟app相同的命令:

iam:gregg
复制代码

而后,发送一条消息:

msg:Ay mang, wut's good?
复制代码

恭喜你,已成功建立了聊天客户端~

清理工做

若是你以前有写过任何关于文件的编程,你应该知道当文件使用完时的良好习惯。事实证实,像在Unix中的任何其余事情同样,开着的套接字链接是使用文件句柄来表示的,这意味着像其余文件同样,在使用完毕后,你须要关闭它。

sendMessage(_:)方法后面添加以下方法

func stopChatSession() {
  inputStream.close()
  outputStream.close()
}
复制代码

你可能已猜到,该方法会关闭流并使得消息不能被接收或者发送出去。这也会将流从以前添加的runloop中移除掉。

为最终完成它,在Stream.Event.endEncountered代码分支下添加调用该方法的代码:

stopChatSession()
复制代码

而后,回到ChatRoomViewController.swift并在viewWillDisappear(_:)内也添加上述代码。

这样,就大功告成了~

何去何从

想下完整代码,请点击这里

目前你已经掌握(至少是看过一个简单的例子)关于套接字网络的基础,还有几种方法来扩展你的眼界。

UDP 套接字

本教程是关于TCP通信的例子,TCP会创建一个链接并尽量保证数据包可达。做为选择,你可使用UDP,或者数据包套接字通信。这些套接字并无如此的传输保证,这意味着他们更加快速且更小的开销。在游戏领域他们很实用。体验过延迟吗?那样意味着你遇到了糟糕的链接,许多应该收到的包被丢弃了。

WebSockets

另外一种想这样给应用使用HTTP的技术叫WebSockets。不像传统的TCP套接字,WebSockets至少保持与HTTP的关系,而且能够用于实现与传统套接字相同的实时通讯目标,全部这一切都来自浏览器的温馨性和安全性。固然WebSockets也能够在iOS上使用,咱们恰好有这篇教程若是你想学习更多内容的话。

Beej的网络编程指南

最后,若是你真的想深刻了解网络,看看免费的在线书籍--Beej的网络编程指南。抛开奇怪的昵称,这本书提供了很是详尽且写的很好的套接字编程。若是你惧怕C语言,那么这本书确实有点“恐怖”,但说不定今天是你面对恐惧的时候呢:]

但愿你能享受这篇流教程,像往常同样,若是你有任何问题请毫无顾忌的让我知道或者在下方留言~

相关文章
相关标签/搜索