翻譯自:Introduction to Protocol Buffers on iOSpython
對大多數的應用來講,後臺服務、傳輸和存儲數據都是個重要的模塊。開發者在給一個 web service 寫接口時,一般使用 JSON 或者 XML 來發送和接收數據,而後根據這些數據生成結構並解析。ios
儘管有大量的 API 和框架幫助咱們序列化和反序列化,來支持一些後臺接口開發的平常工做,好比說更新代碼或者解析器來支持後臺的模型變化。git
可是若是你真的想提高你的新項目的健壯性的話 ,考慮下用 protocol buffers,它是由 Google 開發用來序列化數據結構的一種跨語言的方法。在不少狀況下,它比傳統的 JSON 和 XML 更加靈活有效。其中一個關鍵的特色就是,你只須要在其支持的任何語言和編譯器下,定義一次數據結構——包括 Swift! 建立的類文件就能夠很輕鬆的讀寫成對象。github
在這篇教程中,會使用一個 Python 服務端與一個 iOS 程序交互。你會學到 protocol buffers 是如何工做,如何配置環境,最後怎樣使用 protocol buffers 傳輸數據。web
怎麼,仍是不相信 protocol buffers 就是你所須要的東西?接着往下讀吧。json
注意:這篇教程是基於你已經有了必定的 iOS 和 Swift 經驗,同時有必定的基本的服務端和 terminal 基礎。 同時,確保你使用的是蘋果的 Xcode 8.2或之後的版本.flask
##準備開始 RWCards這個APP能夠用來查看你的會議門票和演講者名單。下載Starter Project並打開根目錄Starter。先熟悉一下這下面這三部分: #####The Client 在 Starter/RWCards下,打開 RWCards.xcworkspace,咱們來看看這幾個主要的文件:swift
整個工程使用 CocoaPods 來拉取這兩個框架:後端
注意:這篇教程中你會用到 Swift Protobuf 0.9.24 和 Google’s Protoc Compiler 3.1.0. 它們已經打包在項目裏了,因此你不須要再作別的。api
開始使用 protocol buffers 前,首先要定義一個 .proto 文件。在這個文件中指定了你的數據結構信息。下面是一個 .proto 文件的示例:
syntax = "proto3";
message Contact {
enum ContactType {
SPEAKER = 0;
ATTENDANT = 1;
VOLUNTEER = 2;
}
string first_name = 1;
string last_name = 2;
string twitter_name = 3;
string email = 4;
string github_link = 5;
ContactType type = 6;
string imageName = 7;
};
複製代碼
這個文件裏定義了一個 Contact 的 message 和它的相關屬性。
.proto 文件定義好了後,你只須要把這個文件交給 protocol buffer 的編譯器,編譯器會用你選擇的語言建立好一個數據類(Swift 中的 結構)。你能夠直接在項目中使用這個類/結構,很是簡單!
JSON 和 XML 多是目前開發者們用來存儲和傳輸數據的標準方案,而 protocol buffers 與之相比有如下優點:
Protocol buffers 雖然有着諸多優點,可是它也不是萬能的:
儘管並非適合於全部的狀況,但 protocol buffers 確確實實有着不少的優點。 把程序運行起來試試看吧。
Head back to Finder and look inside Starter/ProtoSchema. You’ll see the following files: 打開 Starter/ProtoSchema 目錄,你會看到這些文件:
Starter/Server 目錄下包括:
RWServer.py 是放在Flask上的一個 Python 服務。包含兩個 GET 請求:
RWDict.py 包含了 RWServer 將要讀取的演講者列表數據.
如今是時候配置環境來運行 protocol buffers 了。在下面的章節中,你會建立好運行 Google 的 protocol buffer編譯器環境,Swift 的 Protobuf 插件,並安裝 Flask 來運行你的 Python 服務。
在使用 protocol buffers 以前須要安裝許多的工具和庫。starter 項目中包含了一個名爲 protoInstallation.sh 的腳本幫你搞定了這些。它會在安裝以前檢查是否已經安裝過這些庫。 這個腳本須要花一點時間來安裝,尤爲是安裝 Google 的 protocol buffer 庫。打開你的終端,cd 命令進入到 Starter 目錄執行下面這個命令:
$ ./protoInstallation.sh
複製代碼
注意:執行的過程當中你可能會被要求輸入管理員密碼。
腳本執行完成後,再運行一次以確保的到如下輸出結果:
若是你看到這些,那表示腳本已經執行完畢。若是腳本執行失敗了,那檢查下你是否是輸入了錯誤的管理員密碼。並從新運行腳本;它不會從新安裝那些已經成功的庫。 這個腳本作了這些事:
注意:你能夠用編輯器打開 protoInstallation.sh 文件來了解這個腳本是如何工做的。這須要必定的 bash 基礎。
好了,如今你已經作好了使用 protocol buffers 的全部準備工做。
.proto 文件定義了 protocol buffer 描述你的數據結構的 message。把這個文件中的內容傳遞給 protocol buffer 編譯器後,編譯器會生成你的數據結構。
注意:在這篇教程中,你將使用 proto3 來定義 message,這是 protocol buffer 語言的最新版本。能夠訪問Google’s guidelines以獲取更多的 proto3 的信息。
用你最習慣的編輯器打開 ProtoSchema/contact.proto ,這裏已經定義好了演講者的 message:
syntax = "proto3";
message Contact { // 1
enum ContactType { // 2
SPEAKER = 0;
ATTENDANT = 1;
VOLUNTEER = 2;
}
string first_name = 1; //3
string last_name = 2;
string twitter_name = 3;
string email = 4;
string github_link = 5;
ContactType type = 6;
string imageName = 7;
};
message Speakers { // 4
repeated Contact contacts = 1;
};
複製代碼
咱們來看一下這裏麪包含了哪些內容:
The Contact model describes a person’s contact information. This will be displayed on their badges in the app.
##生成 Swift 結構 把 contact.proto 傳遞給 protoc 程序,proto 文件中的 message 將會被轉化生成 Swift 的結構。這些結構會遵循 ProtobufMessage.protoc 並提供 Swift 中構造、方法來序列化和反序列化數據的途徑。
注意:想了解更多關於 Swift 的 protobuf API, 訪問蘋果的 Protobuf API documentation.
在終端中,進入** Starter/ProtoSchema **目錄,用編輯器打開 protoScript.sh,你會看到:
#!/bin/bash
echo 'Running ProtoBuf Compiler to convert .proto schema to Swift'
protoc --swift_out=. contact.proto // 1
echo 'Running Protobuf Compiler to convert .proto schema to Python'
protoc -I=. --python_out=. ./contact.proto // 2
複製代碼
這個腳本對 contact.proto 文件執行了兩次 protoc 命令,分別建立了 Swift 和 Python 的源文件。 回到終端,執行下面的命令:
$ ./protoScript.sh
複製代碼
你會看到如下輸出結果:
Running ProtoBuf Compiler to convert .proto schema to Swift
protoc-gen-swift: Generating Swift for contact.proto
Running Protobuf Compiler to convert .proto schema to Python
複製代碼
你已經建立好了 Swift 和 Python 的源文件。 在 ** ProtoSchema** 目錄下,你會看到一個 Swift 和一個 Python 文件。同時分別還有一個對應的 .pb.swift 和 .pb.py. pb 前綴表示這是 protocol buffer 生成的類。
把 contact.pb.swift 拖到 Xcode 的 project navigator 下的 Protocol Buffer Objects 組. 勾上「Copy items if needed」選項。同時將 contact_pb2.py 拷貝到 Starter/Server 目錄。 看一眼 ** contact.pb.swift** 和 contact_pb2.py中的內容,看看 proto message 是如何轉換成目標語言的。 如今你已經有了生成好的模型對象了,能夠開始集成了! ##運行本地服務器 示例代碼中包含了一個 Python 服務。這個服務提供了兩個 GET 請求:一個用來獲取參會者的名牌信息,另外一個用來列出演講者。 這個教程不會深刻講解服務端的代碼。儘管如此,你須要瞭解到它用到了由 protocol buffer 編譯器生成的 contact_pb2.py 模型文件。若是你感興趣,能夠看一看 RWServer.py 中的代碼,不看也無妨(手動滑稽)。 打開終端並 cd 至 Starter/Server 目錄,運行下面的命令:
$ python RWServer.py
複製代碼
運行結果以下:
經過在瀏覽器中發起 HTTP 請求,你能夠看到 protocol buffer 的原數據。 在瀏覽器中打開 http://127.0.0.1:5000/currentUser 你會看到:
再試試演講者的接口,http://127.0.0.1:5000/speakers:
注意:測試 RWCards app的過程當中你能夠退出、停止和重啓本地服務以便調試。
如今你已經運行了本地服務器,它使用的是由 proto 文件生成的模型,是否是很cooool?
如今你已經把本地服務器跑起來了,是時候在 app 中發起服務請求了。**RWService.swift **文件中將 RWService 類替換成下面的代碼:
class RWService {
static let shared = RWService() // 1
let url = "http://127.0.0.1:5000"
private init() { }
func getCurrentUser(_ completion: @escaping (Contact?) -> ()) { // 2
let path = "/currentUser"
Alamofire.request("\(url)\(path)").responseData { response in
if let data = response.result.value { // 3
let contact = try? Contact(protobuf: data) // 4
completion(contact)
}
completion(nil)
}
}
}
複製代碼
這個類將用來與你的 Python 服務器進行交互。你已經實現了獲取當前用戶的請求:
解碼數據只須要把 protocol buffer 的數據傳遞給對象的構造器便可,不須要其餘的解析。 Swift 的 protocol buffer 庫幫你處理了全部的事情。 如今請求已經完成,能夠展現數據了。
打開 CardViewController.swift 文件並在 viewWillAppear(_:) 以後添加下面這些代碼:
func fetchCurrentUser() { // 1
RWService.shared.getCurrentUser { contact in
if let contact = contact {
self.configure(contact)
}
}
}
func configure(_ contact: Contact) { // 2
self.attendeeNameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)
self.twitterLabel.text = contact.twitterName
self.emailLabel.text = contact.email
self.githubLabel.text = contact.githubLink
self.profileImageView.image = UIImage(named: contact.imageName)
}
複製代碼
這些方法會幫你取得服務端傳過來的數據,並用來配置名片:
用起來很簡單,可是還須要拿到一個 ContactType 枚舉用來區分參會者的類型。
你須要添加一個方法來把枚舉類型轉換成 string, 這樣名片頁面才能顯示 SPEAKER 而不是一個數字0. 可是這有個問題,若是不從新生成 .proto 文件來更新 message,怎樣才能往模型裏添加新功能呢?
extension Contact {
func contactTypeToString() -> String {
switch type {
case .speaker:
return "SPEAKER"
case .attendant:
return "ATTENDEE"
case .volunteer:
return "VOLUNTEER"
default:
return "UNKNOWN"
}
}
}
複製代碼
contactTypeToString() 方法將 ContactType
映射成了一個對應的顯示用的字符串。 打開 CardViewController.swift 並添加下面的代碼到 configure(_:):
self.attendeeTypeLabel.text = contact.contactTypeToString()
複製代碼
將表明contact type的字符串傳遞給了 * attendeeTypeLabel*。 最後在 viewWillAppear(_:) 中,applyBusinessCardAppearance() 以後添加下面代碼:
if isCurrentUser {
fetchCurrentUser()
} else {
// TODO: handle speaker
}
複製代碼
My Badge 選項卡完成後,咱們來看看 Speakers 選項卡。 打開 RWService.swift 並添加下面的代碼:
func getSpeakers(_ completion: @escaping (Speakers?) -> ()) { // 1
let path = "/speakers"
Alamofire.request("\(url)\(path)").responseData { response in
if let data = response.result.value { // 2
let speakers = try? Speakers(protobuf: data) // 3
completion(speakers)
}
}
completion(nil)
}
複製代碼
看上去很熟悉是吧,它和 getCurrentUser(_:) 相似,不過他獲取的是 Speakers 對象,包含了一個 contact 的數組,用於表示回憶的演講者。 打開 SpeakersViewModel.swift 並將代碼替換爲:
class SpeakersViewModel {
var speakers: Speakers!
var selectedSpeaker: Contact?
init(speakers: Speakers) {
self.speakers = speakers
}
func numberOfRows() -> Int {
return speakers.contacts.count
}
func numberOfSections() -> Int {
return 1
}
func getSpeaker(for indexPath: IndexPath) -> Contact {
return speakers.contacts[indexPath.item]
}
func selectSpeaker(for indexPath: IndexPath) {
selectedSpeaker = getSpeaker(for: indexPath)
}
}
複製代碼
SpeakersListViewController 顯示了一個參會者的列表,SpeakersViewModel中包含了這些數據:從 /speakers 接口中獲取的contact對象組成的數組。 SpeakersListViewController將在每一行中顯示一個speaker。 viewmodel建立好了以後,就該配置cell了。打開 SpeakerCell.swift,添加下面的代碼到 SpeakerCell:
func configure(with contact: Contact) {
profileImageView.image = UIImage(named: contact.imageName)
nameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)
}
複製代碼
傳入了一個contact對象而且經過其屬性來配置cell的 image 和 label。這個cell會顯示演講者的照片,和他的名字。 接下來,打開 SpeakersListViewController.swift
並添加下面的代碼到 *viewWillAppear(_:)*中:
RWService.shared.getSpeakers { [unowned self] speakers in
if let speakers = speakers {
self.speakersModel = SpeakersViewModel(speakers: speakers)
self.tableView.reloadData()
}
}
複製代碼
getSpeakers(_:)發起了一個請求去獲取演講者列表的數據,建立了一個 * SpeakersViewModel 的對象,並返回 speakers。 tableview 接下來會更新這些獲取到的數據。 你須要給 tableview 的每一行指定一個speaker用於顯示。替換tableView(_:cellForRowAt:)*的代碼:
let cell = tableView.dequeueReusableCell(withIdentifier: "SpeakerCell", for: indexPath) as! SpeakerCell
if let speaker = speakersModel?.getSpeaker(for: indexPath) {
cell.configure(with: speaker)
}
return cell
複製代碼
getSpeaker(for:) 根據當前列表的 indexPath返回 speaker數據,經過cell的*configure(with:)*配置cell。 當點擊列表中的一個cell時,你須要跳轉到 CardViewController 展現選擇的演講者信息,打開 CardViewController.swift 並在類中添加這些屬性:
var speaker: Contact?
複製代碼
後面會用到這個屬性用來傳遞選擇的演講者。將*// TODO: handle speaker*替換爲:
if let speaker = speaker {
configure(speaker)
}
複製代碼
這個判斷用來肯定 speaker 是否已經填充過了,若是是,調用 configure(),在名片上更新演講者的信息。 回到 SpeakersListViewController.swift 傳遞選擇的 speaker。在 *tableView(_:didSelectRowAt:)*中, performSegue(withIdentifier:sender:) 上方添加:
speakersModel?.selectSpeaker(for: indexPath)
複製代碼
將 speakersModel 中的對應 speaker 標記爲選中。 接下來,在*prepare(for:sender:)*的 vc.isCurrentUser = false: 以後添加下面的代碼:
vc.speaker = speakersModel?.selectedSpeaker
複製代碼
這裏講 selectedSpeaker 傳遞給了 * CardViewController* 來顯示。 確保你的本地服務還在運行當中,build & run Xcode。你會看到 app 已經集成了用戶名片,同時顯示了演講者的信息。
你能夠從 這裏下載到完成的工程。 在這篇教程中,你已經學習到了 protocol buffer 的基本特徵, 怎樣定義一個 .proto 文件並經過編譯器生成 Swift 文件。還學習瞭如何使用Flask 建立一個簡單的本地服務器,並使用這個服務發送 protocol buffer 的二進制數據給客戶端,以及如何輕鬆地去反序列化數據。 protocol buffers 還有更多的特性,好比說在 message 中定義映射和處理向後兼容。若是你對這些感興趣,能夠查看 Google 的文檔。
最後值得一提的是,Remote Procedure Calls這個項目使用了 protocol buffers 而且看起來很是不錯,訪問GRPC瞭解更多吧。