本文翻譯自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語言,那麼這本書確實有點「恐怖」,但說不定今天是你面對恐懼的時候呢:]
但願你能享受這篇流教程,像往常同樣,若是你有任何問題請毫無顧忌的讓我知道或者在下方留言~