咱们开发中用得最多的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;
复制代码