咱們開發中用得最多的HTTP協議及超文本傳輸協議,是一種基於TCP/IP的文本傳輸協議。基本不多碰到字節流操做。html
可是我過咱們要用socket,實現一套基本TCP/IP協議的自定義協議,那麼,對於字節流的操做,數據包的拼接、拆解,是繞不開的。git
本文的全部示例代碼在這裏github
在iOS,對於字節流,大多數狀況下咱們要打交道的是NSData
類型數據。在swift中它叫Data
swift
在OC中它能夠表示爲Byte
類型的數組api
Byte bytes[256];
複製代碼
Byte
等同於UInt8
及unsigned 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)
複製代碼
Data
轉UInt8
時,沒有像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時,必須用多個字節表示。那麼就產生一個問題,這些字節是從左到右仍是從右到左稱爲字節順序。
若是咱們有一個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)
複製代碼
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_t
或Int64
。因此上述代碼中int
類型的a
用CFSwapInt32HostToBig()
轉化
若是你喜歡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];
複製代碼
須要注意的是,這樣轉化以後咱們並不知道字節數組的長度,它與NSString
的length
大相徑庭。須要根據字符串末尾的\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)
複製代碼
須要注意的是,若是咱們在二進制數據包中加入字符串,那麼必須指定字符串的長度。要麼在字符串以前添加指定長度的字段,要麼指定字符串的固定最大長度。否則將會對數據的解析形成困擾。
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代碼片斷中的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指針很熟悉的話,天然會這樣作:
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++時,那麼咱們在數據傳遞時,有一種更爲高效的方式,直接傳遞結構體。
爲了有效傳輸和解析,須要保證
#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;
複製代碼