iOS 實作 UDP Connectionless
目錄
1. Socket
1-1 TCP
1-2 UDP
2. CocoaAsyncSocket
2-1 程式碼
2-2 Read
2-3 Write
3. 成果
⦿ Socket
⦿ TCP
⦿ UDP
⦿ CocoaAsyncSocket
⦿ 程式碼
⦿ Read
⦿ Write
⦿ 成果
Socket
在現代網路 TCP/IP 的架構下,Socket 代表著使用 TCP、UDP 等傳輸層協議做網路溝通時的介面,是基於 BSD(Berkeley Software Distribution)中的 UNIX Socket 發展出的 API。
Socket 在 TCP/IP 架構下分兩類型:
- Datagram:Connectionless,基於
非連接
的資料傳輸(UDP) - Stream:Connection oriented,基於
連接
的資料傳輸(TCP)
TCP
以 TCP 來說,Socket 分為 Server 與 Client,參照之前兩篇文章。
這兩篇文章採用 TCP Socket 做溝通,以 ESP32 當作 Server,iOS Device 當作 Client。在 Server 端必須監聽自己
Port 的動態,在 Client 端必須綁定 Server IP 的 Port
,接著是 Connect、Read、Write、Close 來做到雙向溝通的流程。
Connect
即是連接,Close
是關閉連接,Read
為當 Soket 連接後,接收對方傳來的訊息,Write
為當 Socket 連接後,傳送給對方的訊息,這表示使用 TCP 連接是一對一的,讀寫僅在於兩方接收與傳遞。
CocoaAsyncSocket
在 iOS 中,我們可以使用 cocoapods 來管理套件,pod init
=> open Podfile => 加入 pod ‘CocoaAsyncSocket’
=> pod install => 開啟 Project.xcworkspace
。
接著,在 Storyboard 加入下面的 UI。
由於 UDP Socket 是基於非連接,所以我們並不需要擔心其中一方的 IP,只需要綁定 Port 即可,接著要有一欄是用來傳送訊息,一欄是用來接收訊息,最後再加個清除訊息,基本功能就差不多了。
程式碼
首先在 VC 的 viewDidLoad 中建立 udpSocket 實例,在主線程使用,設置 delegate 為 self。
import CocoaAsyncSocket
var udpSocket: GCDAsyncUdpSocket!
override func viewDidLoad() {
super.viewDidLoad()
udpSocket = GCDAsyncUdpSocket(delegate: self,
delegateQueue: .main)
}
就可以在 extension 中使用 delegate function。
extension UDPClientViewController: GCDAsyncUdpSocketDelegate {
func udpSocket(_ sock: GCDAsyncUdpSocket, didNotConnect error: Error?) {
if let error = error {
print("didNotConnect, because of: \(error)")
}
}
func udpSocketDidClose(_ sock: GCDAsyncUdpSocket, withError error: Error?) {
if let error = error {
print("關閉連接,因為: \(error)")
}
}
func udpSocket(_ sock: GCDAsyncUdpSocket,
didReceive data: Data,
fromAddress address: Data,
withFilterContext filterContext: Any?) {
print("didReceiveData")
var host: NSString?
var port: UInt16 = 0
GCDAsyncUdpSocket.getHost(&host, port: &port, fromAddress: address)
let text = String(data: data,encoding: String.Encoding.utf8) ?? "Can't be identified"
showMessage(dateString() + "\n\"\(host! as String)\" sent: \(text)\n")
}
func udpSocket(_ sock: GCDAsyncUdpSocket, didSendDataWithTag tag: Int) {
print("didSendDataWithTag")
}
func udpSocket(_ sock: GCDAsyncUdpSocket,
didNotSendDataWithTag tag: Int,
dueToError error: Error?) {
print("didNotSendDataWithTag")
}
}
當綁定 Port 後即可開始廣播,所以只需要注意這幾個 function,didNotConnect 跟 didClose,沒辦法建立 Socket 以及 Socket 關閉。
didReceive、didSendDataWithTag、didNotSendDataWithTag,接收、傳送與傳送失敗,就可完成基本的除錯與透過 Socket 溝通。
其實在 GCDAsyncUdpSocketDelegate
中還有一個 function 如下:
func udpSocket(_ sock: GCDAsyncUdpSocket, didConnectToAddress address: Data) {
print("didConnectToAddress \(address)")
}
不過這個例子中我們不會使用到它,暫且忽略吧。
這還沒完,我們還缺少綁定 Port 以及建立 Socket、關閉 Socket 的 function。
@IBAction func bindBtnTapped(_ sender: Any) {
bindBtn.isSelected = !bindBtn.isSelected
if bindBtn.isSelected {
bindBtn.setTitle("Unbind", for: .normal)
bindBtn.setTitleColor(.black, for: .selected)
bindSocket()
} else {
bindBtn.setTitle("Bind", for: .normal)
bindBtn.setTitleColor(.white, for: .selected)
unbindSocket()
}
}
當按下 bindBtn,我們要去改變 isSelected,而在之前的文章中,我們說過 isSelected 是一個很好辨識狀態的素材,因為如果使用預設的 Button 不對它操作,它將永遠是 false。
改變 isSelected 後,也跟著改變操作,因為在這個套件中我們並不能同時綁定兩個 Port,所以關於操作的生命週期
需要相當注意,綁定後不得再綁定,所以綁定後,Button 變成需解綁的狀態。
以及如果 View Controller 要消失時,我們必須要解綁,所以在 viewWillDisappear 中加入如下:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
unbindSocket()
}
接著,我們先來看看 bindSocket()
。
guard let text = portTextField.text,
let port = UInt16(text) else { return }
do {
try udpSocket.bind(toPort: port)
showMessage(dateString())
showMessage("已經綁定 Port: \(port)")
showMessage(separateLine)
} catch(let error) {
print(error)
}
do {
try udpSocket.enableBroadcast(true)
print("Broadcast")
} catch {
print("can't broadcast")
}
do {
try udpSocket.beginReceiving()
print("begin receiving...")
} catch {
print("receiving is not in process.")
}
view.endEditing(true)
確保 Port 欄位是能夠合法轉換成 UInt16 後,將 Socket 綁定這個 Port,接著讓 Socket 可以廣播,以及可以開始接收訊息。
然後是與 bind 相對的 unbindSocket()
。
func unbindSocket() {
if udpSocket != nil && udpSocket.isClosed() == false {
udpSocket.close()
}
}
如果 Socket 不是 nil,並且尚未 closed,我們才讓它 close。
幾乎快完成了!再加把勁。
Read
Read 我們在 delegate function 裡面做了。
Write
Write 我們必須在 Send Button 裡做。
@IBAction func sendBtnTapped(_ sender: Any) {
let data = messageTextField.text?.data(using: .utf8)
udpSocket.send(data!,
toHost: ip,
port: UInt16(portTextField.text!)!,
withTimeout: -1,
tag: 0)
view.endEditing(true)
}
因為是廣播,所以這個 ip 位址是 “255.255.255.255”,Port 當然是前面綁定的 Port,這裡直接 umwarp 沒關係,因為前面是 Port 合法才做綁定。
完成了!
繼續閱讀|回目錄
成果
當我們綁定 8088 Port,就可以開始向內網綁定這個 Port 的其他人發送廣播訊息。
而實際上也會發給自己,這邊看到 IP 是 192.168.1.186,不過卻有兩則訊息,一個是 ::ffff:192.168.1.186,一個是 192.168.1.186,後者是 IPv4,前者是 IPv4 的映射,即是 IPv6(IPv4-mapped IPv6
),這表示說這個廣播是 IPv4 與 IPv6 皆適用,並不能算是廣播兩次。
如果我們使用另外一個設備廣播,會接到如下訊息。
這是從我的 MBA(192.168.1.168)傳來的廣播。