玩轉socket之字節流操做--拼包、拆包

玩轉socket之字節流操做--拼包、拆包

咱們開發中用得最多的HTTP協議及超文本傳輸協議,是一種基於TCP/IP的文本傳輸協議。基本不多碰到字節流操做。html

可是我過咱們要用socket,實現一套基本TCP/IP協議的自定義協議,那麼,對於字節流的操做,數據包的拼接、拆解,是繞不開的。git

本文的全部示例代碼在這裏github

字節流的表示方式
NSData、Data

在iOS,對於字節流,大多數狀況下咱們要打交道的是NSData類型數據。在swift中它叫Dataswift

字節數組

在OC中它能夠表示爲Byte類型的數組api

Byte bytes[256];
複製代碼

Byte等同於UInt8unsigned char數組

typedef UInt8            Byte;
typedef unsigned char    UInt8;
複製代碼

與NSData相互轉換:bash

// bytes轉Data
Byte bytes[256] = {0xff,0xaa,0x33,0xe4};
NSData *data = [[NSData alloc] initWithBytes:bytes length:256];

// Data轉Bytes
const Byte *nBytes = [data bytes];
// 或者
Byte byte[4] = {0};
[cdata getBytes:byte length:4];
複製代碼

swift中,沒有Byte類型,他叫[UInt8]。轉化爲Data服務器

var bytes : [UInt8] = [0x22,0xef,0xee,0xb3]
let data = Data(bytes)
複製代碼

DataUInt8時,沒有像OC同樣的bytes方法網絡

咱們也能夠跟OC中相似的方法app

var nBytes = [UInt8]()
data.copyBytes(to:&nBytes, count:4)
複製代碼

固然,最簡單的方式是這樣

let bytes = [UInt8](data)
複製代碼

若是你喜歡,也能夠這樣

let bytes : [UInt8] = data0.withUnsafeBytes({$0.map({$0})})
複製代碼
十六進制字符串

咱們都知道,計算機存儲和傳輸的數據都是二進制的。而二進制不易與閱讀。當咱們要查看字節流進行調試時,每每會將其轉化爲16進制的字符串。

OC的NSData對象打印默認獲得的是帶*<>*及空格的十六進制字符串。若是想讓其根容易閱讀些,能夠在NSData的category中增添:

- (NSString *)hexString
{
    NSMutableString *mstr = [NSMutableString new];
    const Byte *bytes = [self bytes];
    for (int i = 0; i < self.length; i++) {
        [mstr appendFormat:@"0x%02x ",bytes[i]];
    }
    return mstr;
}
複製代碼

swift中的Data只能打印出有多少個字節。須要一個生成十六進制串的方法:

extension UInt8 {
    var hexString : String {
        let str = String(format:"0x%02x",self)
        return str
    }
}

extension Data {
    var bytes : [UInt8] {
        return [UInt8](self)
    }
    var hexString : String {
        var str = ""
        for byte in self.bytes {
            str += byte.hexString
            str += " "
        }
        return str
    }
}
複製代碼
字節序

在進行字節流的拼接和解析以前,咱們必須先了解網絡傳輸中,一個關鍵的概念字節序

什麼叫字節序

當一個整數的值大於255時,必須用多個字節表示。那麼就產生一個問題,這些字節是從左到右仍是從右到左稱爲字節順序。

大端字節序Vs小端字節序

若是咱們有一個16進制值0x0025,那麼它包含兩個字節0x00、0x25。在傳輸時,咱們指望的是,在字節流中,先看到0x00而0x25緊隨其後。

若是是大端字節序,一切將會是咱們預期的。 可是若是是小端字節序,咱們看到的是將是0x2五、0x00。

在網絡傳輸時,TCP/IP中規定必須採用網絡字節順,也就是大端字節序

而不一樣的CPU和操做系統下的主機字節序是不一樣的。

咱們用的是什麼字節序

咱們用簡單代碼測試一下。

int16_t i = 0x0025;
NSData *data = [[NSData alloc] initWithBytes:&i length:2];
NSLog(@"%@",[data hexString]);

// 輸出:0x25 0x00
複製代碼

swift中

var value : UInt16 = 0x0025
let data = Data(bytes:&value, count:2)
print([UInt8](data))
// 輸出:0x25 0x00
複製代碼

根據簡單的測試。很顯然,咱們用的是小端字節序

如何轉換

咱們的主機字節序與網絡字節序是不一致。那麼,在字節流的編碼和解碼過程當中,就須要進行字節序的轉化。

swift中全部整形,都有bigEndian屬性,能夠很容易進行大小端字節序之間的轉化

let v : UInt32 = 78
let bv = v.bigEndian
複製代碼

OC中,將轉化方法分爲兩種,主機序轉大字節序、大字節序轉主機序。其實只用其中之一就能夠了。由於兩種方法實現都是同樣,都是字節序的反轉。但爲了代碼可讀性,能夠在編碼和解析時候區分一下,使用不一樣方法。

// 大端字節序轉主機字節序(小端字節序)
uint16_t CFSwapInt16BigToHost(uint16_t arg)
uint32_t CFSwapInt32BigToHost(uint32_t arg)

// 大端字節序轉主機字節序(小端字節序)
uint16_t CFSwapInt16HostToBig(uint16_t arg)
uint32_t CFSwapInt32HostToBig(uint32_t arg)
複製代碼
double需不須要處理

swift中只對所用整形類型提供轉化方法,對於浮點型卻沒有。那麼若是碰到浮點數,咱們須要若是處理呢。

通常而言,編譯器是按照IEEE標準對浮點型解釋的。只要編譯器是支持IEEE浮點標準的,就不須要考慮字節順序。而目前主流編譯器都是支持IEEE的。

因此浮點型不用考慮字節序問題

編碼方式(拼包)

編碼方式即,咱們根據事先約定好的格式,從低位到高位,依次拼接相應數據類型及字節長度的數據。最終造成數據包。

下面咱們來看一下,針對不一樣的數據類型的處理方式

整型

OC中拼接方式很簡單,只要注意大端字節序的轉化就好了。

// 以整形初始化
int a = -25;
int biga = CFSwapInt32HostToBig(a);
NSMutableData *data = [[NSMutableData alloc] initWithBytes:&biga length:sizeof(biga)];
    
// 整形的拼接
uint16_t b = 8;
uint16_t bigb = CFSwapInt16HostToBig(b);
[data appendBytes:&bigb length:sizeof(bigb)];

複製代碼

須要補充一點:OC中int固定佔4個字節32位;NSInteger與swift中Int類型同樣,根據不一樣的平臺會差生差別。目前iOS都是64位系統,他們都佔8個字節64位至關於int64_tInt64。因此上述代碼中int類型的aCFSwapInt32HostToBig()轉化

若是你喜歡byte數組,也能夠。

uint32_t value = 0x1234;
Byte byteData[4] = {0};
byteData[0] =(Byte)((value & 0xFF000000)>>24);
byteData[1] =(Byte)((value & 0x00FF0000)>>16);
byteData[2] =(Byte)((value & 0x0000FF00)>>8);
byteData[3] =(Byte)((value & 0x000000FF));

// 輸出 byteData:0x00 0x00 0x12 0x34
複製代碼

swift當中

var a = 3.bigEndian
var b  = UInt16(23).bigEndian
var data = Data(bytes:&a, count:a.bitWidth/8)
let bpoint = UnsafeBufferPointer(start:&b, count:1)
data.append(bpoint)
複製代碼

咱們也能夠轉化爲UIn8字節數組

extension FixedWidthInteger {
    var bytes : [UInt8] {
        let size = MemoryLayout<Self>.size
        if size == 1 {
            return [UInt8(self)]
        }
        var bytes = [UInt8]()
        for i in 0..<size {
            let distance = (size - 1 - i) * 8;
            let sub  = self >> distance
            let value = UInt8(sub & 0xff)
            bytes.append(value)
        }
        return bytes
    }
}
複製代碼

如此獲得的字節數組自己就是大端字節序,咱們能夠直接這樣用

let c : Int8 = 6
let d : UInt16 = 0x1234
var abytes = c.bytes
abytes += d.bytes
let data0 = Data(abytes)
複製代碼
浮點型

浮點型的編碼方式與整型類似,只是少了大字節序的轉化過程。可是上述轉成字節數組的方式只適用於整型,對於浮點型並不奏效。

而在swift中,轉化字節數組,對於任意類型都有效的方式:

func toByteArray<T>(value: T) -> [UInt8] {
    var value = value
    let bytes = withUnsafeBytes(of: &value,{$0.map({$0})})
    return bytes
}
複製代碼

雖然上述方法是範型的,理論上任意類型均可以調用。但準確來講上述方法,只適用於整型與浮點型。

字符串

一般咱們使用和傳輸的字符串都是utf-8編碼的。

字符串轉NSData很簡單。

NSString *str = @"temp";
NSData *datas = [str dataUsingEncoding:NSUTF8StringEncoding];
複製代碼

swift中相似的

var data = "buf".data(using:.utf8)
複製代碼
字符串轉字節數組

若是咱們要使用字節數組,咱們每每會將字符串轉化爲c字符串。由於c字符串自己就是字符數組,而每一個字符正好是一個字節。

NSString *string = @"一個字符串";
Byte *cbytes = (Byte *)[string cStringUsingEncoding:NSUTF8StringEncoding];
複製代碼

須要注意的是,這樣轉化以後咱們並不知道字節數組的長度,它與NSStringlength大相徑庭。須要根據字符串末尾的\0標識符來肯定

int ci = 0;
while (*(cbytes+ci) != '\0') {
    ci++;
}

NSLog(@"string.length=%lu cstring.lenth=%d",(unsigned long)string.length,ci);
// 輸出:string.length=5 cstring.lenth=15
複製代碼

若是在swift中這要作:

var sarr  = "一個字符串".cString(using:.utf8)
複製代碼

能夠直接獲得[CChar][Int8],而且能夠直接獲得數組長度。可是要注意的是,swift是強類型,離咱們的[UInt8]還差一步。注意若是CChar是負數及首位上是1,直接轉化成UInt8直接拋出異常。

但咱們能夠先去掉首位,等轉化完成再加上

extension String {
    var bytes : [UInt8] {
       var bytes = self.cString(using:.utf8)?.map({ char -> UInt8 in
            if char < 0 {
                let b = char & 0b01111111
                let c = UInt8(b) | 0b10000000
                return c
            }else{
                return UInt8(char)
            }
        })
        bytes = bytes?.dropLast()
        return bytes ?? [UInt8]()
    }
}
複製代碼

須要注意的是,轉成[CChar]以後,其末尾的\0 也會帶上。這裏咱們贊成把它去掉

字節數組如何拼接

貌似漏了字符數組的拼接方式。下面咱們來看看

先說swift,基於以前定義的擴展方法,它拼接起來很簡單

var mbytes = [UInt8]()
mbytes += 5.bytes
mbytes += toByteArray(value:3.14)
mbytes += "a string".bytes
let mdata = Data(mbytes)
複製代碼
數據包中添加字符串

須要注意的是,若是咱們在二進制數據包中加入字符串,那麼必須指定字符串的長度。要麼在字符串以前添加指定長度的字段,要麼指定字符串的固定最大長度。否則將會對數據的解析形成困擾。

解碼的方式(拆包)
用OC實現
int16_t ri;
UInt8   rj;
double  rk;
[data0 getBytes:&ri range:NSMakeRange(0,2)];
int16_t rri = CFSwapInt16BigToHost(ri);
[data0 getBytes:&rj range:NSMakeRange(2,1)];
[data0 getBytes:&rk range:NSMakeRange(3,8)];
NSData *rsData = [data0 subdataWithRange:NSMakeRange(8,8)];
NSString *rs = [[NSString alloc] initWithData:rsData encoding:NSUTF8StringEncoding];
複製代碼
用swift實現

基於字節數組如何拼接一節中的swift代碼片斷中的mdata。在swift中能夠這樣拆包、解析:

let mi : Int   =  mdata[0..<8].withUnsafeBytes {$0.pointee}
let rmi : Int = mi.bigEndian
let md : Double = mdata[8..<16].withUnsafeBytes {$0.pointee}
let ms  = String(data:mdata[16..<mdata.count], encoding:.utf8)
複製代碼
碰到的坑

可是Data的下列方法在swift5中廢棄了。

public func withUnsafeBytes<ResultType, ContentType>(_ body: (UnsafePointer<ContentType>) throws -> ResultType) rethrows -> ResultType
複製代碼

換成了同名但參數類型變化了的函數

@inlinable public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
複製代碼

直接影響是用舊方法時,沒法聯想出pointee屬性,每次都得手敲。

那麼,咱們就換新方法試一下

let mi0 = mdata[0..<8].withUnsafeBytes {  $0.load(as:Int.self) }
let rmi0 = mi0.bigEndian
let md0 = mdata[8..<16].withUnsafeBytes {  $0.load(as: Double.self) }
let ms0 = String(data:mdata[16..<mdata.count], encoding:.utf8)
複製代碼

一切看起來都很正常。可是若是咱們在要處理的數據最前面加上一個UInt類型。依次方法解析,在獲取第二個變量時,會拋出

Fatal error: load from misaligned raw pointer
複製代碼

搜索一番後,在stack overflow。獲得的結果是除非支持unaligned loads,否則仍是用[UInt8]

By loading individual UInt8 values instead and performing bit shifting, we can avoid such alignment issues (however if/when UnsafeMutableRawPointer supports unaligned loads, this will no longer be an issue).

而後看到這個答案,知道原來它須要內存像C語言結構體那樣的對界方式,若是你取UInt32須要按4的倍數取,若是你取Int須要按8的倍數取

然而,咱們經過subscript獲得了新的Data確定是對齊的啊。

有可能經過subscript獲取到的Data和原始數據共享的相同內存。那麼咱們建立新的對象試試:

let mdata1 = Data(mdata[1..<9])
let mi0 = mdata1.withUnsafeBytes {$0.load(as:UInt64.self)}
複製代碼

果真跟咱們想的同樣,這回跑起來一塊兒正常。

用字節數組實現

咱們仍是以先以swift爲例

let bytes = [UInt8](mdata)
let ma : UInt8 = bytes[0]
let mb = bytes[1..<9].enumerated().reduce(0, { (result, arg) -> Int in
    let (offset, item) = arg
    let size = MemoryLayout<Int>.size
    let biteOffset = (size - offset - 1) * 8
    let temp =  Int(item) << biteOffset
    return result | temp
})
複製代碼

對於double類型,咱們沒辦法進行位運算。聰明的你若是想經過Int進行位運算再轉化爲double,可是就是在轉成Dobule那一步一切將前功盡棄。緣由很簡單,double遵循IEEE浮點標準,跟整型的編碼方式不同,在作類型轉化時,全部已經排好的字節將會按新規則從新生成。

仍是像當初將double轉成字節數組同樣

func byteArrayToType<T>(_ value:[UInt8]) -> T
{
    return value.withUnsafeBytes({$0.load(as:T.self)})
}
複製代碼

使用時

let mc : Double = byteArrayToType(bytes[9..<17].map({$0}))
複製代碼
用c指針實現

若是你對C指針很熟悉的話,天然會這樣作:

const void *bytes = [data0 bytes];
int16_t ci = *(int16_t*)(bytes);
uint8_t cj = *(uint8_t*)(bytes+2);
double  ck = *(double*)(bytes+3);
char *cstr = (char *)(bytes+11);
NSString *nstr = [NSString stringWithCString:cstr encoding:NSUTF8StringEncoding];
複製代碼
C結構體的傳輸

當咱們的服務器是C/C++時,那麼咱們在數據傳遞時,有一種更爲高效的方式,直接傳遞結構體。

爲了有效傳輸和解析,須要保證

  • 結構體的大小必須是固定的。這就意味着咱們在傳遞字符串或者數組時,必須定義其大小
  • 接受與發送端結構體定義必須一致。就是說,其一變量的順序一致;其二結構體的字節對齊方式一致,都是天然對界(按結構體的成員中size最大的成員對齊)或者#pragma pack (n)申明一致

如何知足上述兩個條件。咱們能夠很容易的完成數據包的生成及拆解。

數據包的生成:

struct Message msg = {};
msg.type = 1;
msg.seq  = 0x0102;
msg.timeTemp = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
memcpy(msg.content,cstr,16);

void *sent = &msg;
int length = sizeof(struct Message);
NSData *cdata = [[NSData alloc] initWithBytes:&sent length:length];
複製代碼

拆解:

const void *rec = [cdata bytes];
struct Message nmsg = *(struct Message *)rec;
複製代碼
參考資料
相關文章
相關標籤/搜索