本文翻译自Real-Time Communication with Streams Tutorial for iOS
翻译的不对的地方还请多多包涵指正,谢谢~html
从时间初始,人们就已开始梦想着更好地跟遥远的兄弟通信的方式。从信鸽到无线电波,咱们一直在努力将通信变得更清晰更高效。ios
在现代中,一种技术已成为咱们寻求相互理解的重要的工具:简易网络套接字。git
现代网络基础结构的第四层,套接字是任何从文本编辑到游戏在线通信的核心。github
你可能会奇怪,“为何不优先使用URLSession
而选择低级API?”。若是你没以为奇怪,能够伪装你以为......web
好问题^_^ URLSession
通信是基于HTTP
网络协议。使用HTTP
,通信是以【请求-响应】方式进行。这意味着在大部分App大多数网络代码都遵循如下模式:shell
server
端请求JSON
数据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
,使用上述启动服务器的代码。
下一步,打开DogeChat
工程,编译并运行,你会看到已经帮你写好的界面:
如上图所示,DogeChat
已经写好能够容许用户输入名字后进入到聊天室。不幸的是,前一个工程师不知道怎么写聊天App所以他写完了全部的界面和基本的跳转,留下了网络层部分给你。
在开始编码前,切到 ChatRoomViewController.swift 文件。你能够看到你有了一个界面处理器,它能接收来自输入栏的信息,也能够经过使用Message
对象配置cell的TableView
来展现消息。
既然你已经有了ViewController
,那么你只须要建立一个ChatRoom
来处理繁重的工做。
开始写新类前,我想快速列举下新类的功能。对于它,咱们但愿能处理这些事情:
如今你知道你该作什么啦,点击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
类,并声明了为使沟通更高效的属性。
server
的套接字。天然地,你会经过输出流来发送消息,输出流接收消息。username
变量用于存储当前用户的名字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)
}
复制代码
这里发生了:
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
,你须要想到这些烦人的动做:Get
,PUT
和PATCH
。须要构造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) }
}) } }
复制代码
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
}
}
复制代码
read(_:maxLength:)
方法读取流中的字节并将它放入传进来的缓冲区中该方法须要在输入流有字节可用的时候调用,所以在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)
}
复制代码
String
对象。设置该对象是ASCII
编码,并告诉对象在使用完缓冲区的时候释放它,并使用:
符号来分割消息,所以你就能够分别得到名字和消息。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(_:)
内也添加上述代码。
这样,就大功告成了~
想下完整代码,请点击这里
目前你已经掌握(至少是看过一个简单的例子)关于套接字网络的基础,还有几种方法来扩展你的眼界。
本教程是关于TCP
通信的例子,TCP
会创建一个链接并尽量保证数据包可达。做为选择,你可使用UDP
,或者数据包套接字通信。这些套接字并无如此的传输保证,这意味着他们更加快速且更小的开销。在游戏领域他们很实用。体验过延迟吗?那样意味着你遇到了糟糕的链接,许多应该收到的包被丢弃了。
另外一种想这样给应用使用HTTP
的技术叫WebSockets。不像传统的TCP
套接字,WebSockets
至少保持与HTTP的关系,而且能够用于实现与传统套接字相同的实时通讯目标,全部这一切都来自浏览器的温馨性和安全性。固然WebSockets
也能够在iOS上使用,咱们恰好有这篇教程若是你想学习更多内容的话。
最后,若是你真的想深刻了解网络,看看免费的在线书籍--Beej的网络编程指南。抛开奇怪的昵称,这本书提供了很是详尽且写的很好的套接字编程。若是你惧怕C语言,那么这本书确实有点“恐怖”,但说不定今天是你面对恐惧的时候呢:]
但愿你能享受这篇流教程,像往常同样,若是你有任何问题请毫无顾忌的让我知道或者在下方留言~