使用 socket 可以很方便地實現客服端和服務器的長連接,並進行雙向的數據通信。但有時我們發送的數據包長度並不是固定的(比如做一個聊天系統),通常的做法是在數據包前面添加個包頭信息,讓接收方知道整個發送包的長度。也就是說接收方先收這個固定長度的包頭信息,然後再根據包頭信息裡面定義的實際長度來接收包數據。
下面通過一個聊天室程序演示 socket 發送/讀取不定長消息包的幾種方法。socket 通信這裡我們使用了一個封裝好的 socket 庫(SwiftSocket)。
幾種方法 (以聊天室為例)
方法1:每條消息發送兩個包
第1個包長度固定為4個字節,裡邊記錄的是後面發送的實際消息包的長度。
第2個包才是實際的消息包。
接收方先接收第一個固定長度的數據包,通過第一個包知道了數據長度,從而進行讀取實際的消息包。這種方式我原來寫過相關的文章,地址是:Swift - 使用socket進行通信(附聊天室樣例)
(1)發送數據相關代碼如下:
//發送消息
func sendMessage(msgtosend:NSDictionary){
let msgdata=try? NSJSONSerialization.dataWithJSONObject(msgtosend,
options: .PrettyPrinted)
var len:Int32=Int32(msgdata!.length)
let data:NSMutableData=NSMutableData(bytes: &len, length: 4)
self.socketClient!.send(data: data)
self.socketClient!.send(data:msgdata!)
}
(2)接收數據相關代碼如下:
//解析收到的消息
func readMsg()->NSDictionary?{
//read 4 byte int as type
if let data=self.tcpClient!.read(4){
if data.count==4{
let ndata=NSData(bytes: data, length: data.count)
var len:Int32=0
ndata.getBytes(&len, length: data.count)
if let buff=self.tcpClient!.read(Int(len)){
let msgd:NSData=NSData(bytes: buff, length: buff.count)
let msgi:NSDictionary=(try!
NSJSONSerialization.JSONObjectWithData(msgd,
options: .MutableContainers)) as! NSDictionary
return msgi
}
}
}
return nil
}
方法2:每條消息只發送1個包。不過這個包增加了包頭信息。
這個其實是上面方式的整合。發送方將消息長度做為包頭信息(比如固定是4個字節大小)添加到消息包前面。
接收方先讀取前4個字節的內容,再根據獲取到的長度信息讀取剩余的消息。
(1)客戶端這邊(ViewController.swift)發送消息的時候將包頭拼在消息包前面,這樣每次只發一個數據包。(接收數據方式不變)
1
//發送消息
func sendMessage(msgtosend:NSDictionary){
//消息數據包
let msgdata = try? NSJSONSerialization.dataWithJSONObject(msgtosend,
options: .PrettyPrinted)
//消息數據包大小
var len:Int32=Int32(msgdata!.length)
//消息包頭數據
let data:NSMutableData=NSMutableData(bytes: &len, length: 4)
//包頭後面添加正真的消息數據包
data.appendData(msgdata!)
//發送合並後的數據包
self.socketClient!.send(data: data)
}
(2)服務端(MyTcpSocketServer.swift)同樣也是發送一個包含包頭信息的數據包。(接收數據方式不變)
//發送消息
func sendMsg(msg:NSDictionary){
//消息數據包
let jsondata=try? NSJSONSerialization.dataWithJSONObject(msg, options:
NSJSONWritingOptions.PrettyPrinted)
//消息數據包大小
var len:Int32=Int32(jsondata!.length)
//消息包頭數據
let data:NSMutableData=NSMutableData(bytes: &len, length: 4)
//包頭後面添加正真的消息數據包
data.appendData(jsondata!)
//發送合並後的數據包
self.tcpClient!.send(data: data)
}
方法3:每條消息只發送1個包。同時不增加包頭信息。
這種方式發送方直接將消息包發送出去,不再額外添加任何包頭信息。
接受方由於不知道消息的實際長度,便需要進行循環讀取。每次讀固定字節數(比如1024個字節),如果某次讀取到的字節數小於1024個字節,則表示讀取完畢。
(1)首先修改 ytcpsocket.swift,給 TCPClient 類添加 readAll() 方法,用來實現自動循環讀取全部的消息,而不用指定預期的數據長度。
/*
* 不指定長度來讀取數據
*/
public func readAll(timeout:Int = -1)->[UInt8]?{
//內容緩沖區
var data:[UInt8] = [UInt8]()
repeat{
if let tempData = read(1024, timeout: timeout) {
//將每次讀取到的數據添加到緩沖區
data = data + tempData
//讀取結束
if tempData.count < 1024 {
break
}
}else{
break
}
}while true
return data
}
(2)客戶端管理類 ChatUser(MyTcpSocketServer.swift)修改
接收數據的方式修改了,同時發送數據前不再需要提供消息長度,直接發消息包即可。
//客戶端管理類(便於服務端管理所有連接的客戶端)
class ChatUser:NSObject{
var tcpClient:TCPClient?
var username:String=""
var socketServer:MyTcpSocketServer?
//解析收到的消息
func readMsg()->NSDictionary?{
if let buff=self.tcpClient!.readAll(){
let msgd:NSData=NSData(bytes: buff, length: buff.count)
let msgi:NSDictionary=(try!
NSJSONSerialization.JSONObjectWithData(msgd,
options: .MutableContainers)) as! NSDictionary
return msgi
}
return nil
}
//循環接收消息
func messageloop(){
while true{
if let msg=self.readMsg(){
self.processMsg(msg)
}else{
self.removeme()
break
}
}
}
//處理收到的消息
func processMsg(msg:NSDictionary){
if msg["cmd"] as! String=="nickname"{
self.username=msg["nickname"] as! String
}
self.socketServer!.processUserMsg(user: self, msg: msg)
}
//發送消息
func sendMsg(msg:NSDictionary){
let jsondata=try? NSJSONSerialization.dataWithJSONObject(msg, options:
NSJSONWritingOptions.PrettyPrinted)
self.tcpClient!.send(data: jsondata!)
}
//移除該客戶端
func removeme(){
self.socketServer!.removeUser(self)
}
//關閉連接
func kill(){
self.tcpClient!.close()
}
}
(3)同樣的,客戶端代碼(ViewController.swift)這邊發送與接收數據也可以簡化。
import UIKit
class ViewController: UIViewController {
//消息輸入框
@IBOutlet weak var textFiled: UITextField!
//消息輸出列表
@IBOutlet weak var textView: UITextView!
//socket服務端封裝類對象
var socketServer:MyTcpSocketServer?
//socket客戶端類對象
var socketClient:TCPClient?
override func viewDidLoad() {
super.viewDidLoad()
//啟動服務器
socketServer = MyTcpSocketServer()
socketServer?.start()
//初始化客戶端,並連接服務器
processClientSocket()
}
//初始化客戶端,並連接服務器
func processClientSocket(){
socketClient=TCPClient(addr: "localhost", port: 8080)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
() -> Void in
//用於讀取並解析服務端發來的消息
func readmsg()->NSDictionary?{
if let buff=self.socketClient!.readAll(){
let msgd:NSData=NSData(bytes: buff, length: buff.count)
let msgi:NSDictionary =
(try! NSJSONSerialization.JSONObjectWithData(msgd,
options: .MutableContainers)) as! NSDictionary
return msgi
}
return nil
}
//連接服務器
let (success,msg)=self.socketClient!.connect(timeout: 5)
if success{
dispatch_async(dispatch_get_main_queue(), {
self.alert("connect success", after: {
})
})
//發送用戶名給服務器(這裡使用隨機生成的)
let msgtosend=["cmd":"nickname","nickname":"游客\(Int(arc4random()%1000))"]
self.sendMessage(msgtosend)
//不斷接收服務器發來的消息
while true{
if let msg=readmsg(){
dispatch_async(dispatch_get_main_queue(), {
self.processMessage(msg)
})
}else{
dispatch_async(dispatch_get_main_queue(), {
//self.disconnect()
})
break
}
}
}else{
dispatch_async(dispatch_get_main_queue(), {
self.alert(msg,after: {
})
})
}
})
}
//“發送消息”按鈕點擊
@IBAction func sendMsg(sender: AnyObject) {
let content=textFiled.text!
let message=["cmd":"msg","content":content]
self.sendMessage(message)
textFiled.text=nil
}
//發送消息
func sendMessage(msgtosend:NSDictionary){
let msgdata=try? NSJSONSerialization.dataWithJSONObject(msgtosend,
options: .PrettyPrinted)
self.socketClient!.send(data:msgdata!)
}
//處理服務器返回的消息
func processMessage(msg:NSDictionary){
let cmd:String=msg["cmd"] as! String
switch(cmd){
case "msg":
self.textView.text = self.textView.text +
(msg["from"] as! String) + ": " + (msg["content"] as! String) + "\n"
default:
print(msg)
}
}
//彈出消息框
func alert(msg:String,after:()->(Void)){
let alertController = UIAlertController(title: "",
message: msg,
preferredStyle: .Alert)
self.presentViewController(alertController, animated: true, completion: nil)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 7), dispatch_get_main_queue(),{
alertController.dismissViewControllerAnimated(false, completion: nil)
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}