App開(kāi)發(fā)中當(dāng)遇到行情價(jià)格、聊天場(chǎng)景中, 為了讓消息及時(shí)到達(dá), App 端往往采用 Socket 的方式和服務(wù)器實(shí)現(xiàn)長(zhǎng)連接. 服務(wù)器最常見(jiàn)的就是部署在云端, 從根本上來(lái)說(shuō)就是一臺(tái)主機(jī)或者虛擬機(jī). 從互聯(lián)網(wǎng)的組成結(jié)構(gòu)來(lái)看, 任何一個(gè)節(jié)點(diǎn)上的計(jì)算機(jī), 都可以作為服務(wù)器來(lái)使用. iPhone 智能手機(jī)作為連接在網(wǎng)絡(luò)上的硬件, 從理論上來(lái)說(shuō)是可以作為服務(wù)器使用的.
對(duì)于上面所述, 以前只是感性上的調(diào)侃, 這兩天查找資料, 竟然發(fā)現(xiàn)了早就有大神實(shí)現(xiàn)了該功能: CocoaAsyncSocket , 趕快下載下來(lái)體驗(yàn)一番.
1 Socket
Socket 翻譯成中文稱為"套接字", 有人將 Socket 等價(jià)于 TCP/IP, 個(gè)人認(rèn)為這樣不是太準(zhǔn)確, 因?yàn)槭褂?Socket 還可以實(shí)現(xiàn) UDP 協(xié)議的發(fā)送. 可以把 Socket 看做比 TCP/IP 更高級(jí)的接口層, 可以用下圖形象的理解:

使用Socket進(jìn)行客戶端和服務(wù)端的鏈接過(guò)程如下:

2 Server
使用 GCDAsyncSocket 創(chuàng)建服務(wù)端, 監(jiān)聽(tīng)端口 5858:
- (GCDAsyncSocket *)serverSocket {
if (!_serverSocket) {
_serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return _serverSocket;
}
NSError *error;
BOOL result = [self.serverSocket acceptOnPort:port error:&error];
delegateQueue 要求是順序隊(duì)列, 保證Socket中傳輸?shù)臄?shù)據(jù)按順序讀取或者存儲(chǔ).
從 Socket 鏈接過(guò)程圖中, 能夠看出分為鏈接成功/接收數(shù)據(jù)/斷開(kāi)鏈接 3 個(gè)過(guò)程, GCDAsyncSocket 通過(guò)代理方法來(lái)實(shí)現(xiàn):
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
NSLog(@"收到鏈接:%@: %d", newSocket.localAddress, newSocket.localPort);
[newSocket readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSString *clientStr = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
NSLog(@"收到內(nèi)容: %@ 長(zhǎng)度:%zd", clientStr, clientStr.length);
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err {
NSLog(@"斷開(kāi)鏈接: %@; error: %@", sock.connectedHost, err);
}
readDataWithTimeout 中設(shè)置為-1, 表示超時(shí)時(shí)間無(wú)限大, 保證數(shù)據(jù)寫入到Socket中的dataBuffer. tag值為數(shù)據(jù)包的標(biāo)識(shí), 過(guò)程中可以通過(guò)該標(biāo)識(shí)識(shí)別數(shù)據(jù)包, 比如傳輸失敗的代理方法/服務(wù)端接收到數(shù)據(jù)的代理方法中.
3 Client
客戶端通過(guò)GCDAsyncSocket鏈接到主機(jī)端口:
- (GCDAsyncSocket *)clientSocket {
if (!_clientSocket) {
_clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.sockeQueue];
return _clientSocket;
}
NSError *error = nil;
[self.clientSocket connectToHost:host onPort:port error:&error];
if (error) {
return NO;
}else {
return YES;
}
在鏈接成功后使用 socket 向服務(wù)端口發(fā)送數(shù)據(jù)包:
- (void)socket:(GCDAsyncSocket *)socket didConnectToHost:(NSString *)host port:(uint16_t)port {
NSData *contentData = [@"client: 123" dataUsingEncoding:(NSUTF8StringEncoding)];
[socket writeData:data withTimeout:-1 tag:0];
}
4 粘包
手動(dòng)點(diǎn)擊button發(fā)送小數(shù)據(jù), 不會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂的問(wèn)題, 當(dāng)大量數(shù)據(jù)通過(guò)Socket同時(shí)發(fā)送時(shí), 在服務(wù)端會(huì)出現(xiàn)數(shù)據(jù)鏈接錯(cuò)亂的情況. 鏈接成功后, 向服務(wù)端發(fā)送 100 個(gè)數(shù)據(jù):
- (void)sendData:(GCDAsyncSocket *)socket {
for (int i = 0; i < 100; i++) {
NSString *str = [NSString stringWithFormat:@"%d", i];
NSData *contentData = [str dataUsingEncoding:(NSUTF8StringEncoding)];
[socket writeData:data withTimeout:-1 tag:0];
}
}
在服務(wù)端接收的數(shù)據(jù)如下:
SocketServer[70565:1560844] 收到內(nèi)容: 0 長(zhǎng)度:1
SocketServer[70565:1560844] 收到內(nèi)容: 1234 長(zhǎng)度:4
SocketServer[70565:1560844] 收到內(nèi)容: 5678910111213141516171819202122 長(zhǎng)度:31
SocketServer[70565:1560844] 收到內(nèi)容: 2324252627282930313233343536 長(zhǎng)度:28
SocketServer[70565:1560844] 收到內(nèi)容: 373839404142434445464748495051 長(zhǎng)度:30
SocketServer[70565:1560844] 收到內(nèi)容: 5253545556575859606162636465666768697071727374 長(zhǎng)
SocketServer[70565:1560844] 收到內(nèi)容: 7576777879808182838485868788899091929394959697 長(zhǎng)
SocketServer[70565:1560844] 收到內(nèi)容: 9899 長(zhǎng)度:4
能夠看出接收的數(shù)據(jù), 保持了客戶端發(fā)送123...的順序, 但是并沒(méi)有按照 Socket 發(fā)送的 1, 2, 3...相互分割的方式去讀取, 這就是Socket的粘包現(xiàn)象. 粘包導(dǎo)致服務(wù)端接收數(shù)據(jù)后, 無(wú)法分辨出客戶端發(fā)送數(shù)據(jù)的起始和終止位置, 會(huì)導(dǎo)致解析出錯(cuò)誤數(shù)據(jù).
粘包的原因
TCP是面向連接的,面向流的,提供高可靠性服務(wù)。收發(fā)兩端(客戶端和服務(wù)器端)都要有一一成對(duì)的socket,因此,發(fā)送端為了將多個(gè)發(fā)往接收端的包,更有效的發(fā)到對(duì)方,使用了優(yōu)化方法(Nagle算法),將多次間隔較小且數(shù)據(jù)量小的數(shù)據(jù),合并成一個(gè)大的數(shù)據(jù)塊,然后進(jìn)行封包。
粘包的解決方式:
- 禁用
Nagle算法; - 當(dāng)填入數(shù)據(jù)后調(diào)用push操作指令強(qiáng)制數(shù)據(jù)立即傳送,而不必等待發(fā)送緩沖區(qū)填充;
- 數(shù)據(jù)包中加頭,頭部信息為整個(gè)數(shù)據(jù)的長(zhǎng)度(最廣泛最常用);
發(fā)送數(shù)據(jù)
- (void)sendData:(NSData *)contentData {
NSInteger dataLength = contentData.length;
NSData *lengthData = [NSData dataWithBytes:&dataLength length:sizeof(dataLength)];
NSData *headData = [lengthData subdataWithRange:NSMakeRange(0, 4)];
NSMutableData *data = [NSMutableData dataWithData:headData];
[data appendData:contentData];
[self.clientSocket writeData:data withTimeout:-1 tag:0];
}
接收數(shù)據(jù)
- (NSMutableData *)dataBuffer {
if (!_dataBuffer) {
_dataBuffer = [NSMutableData data];
}
return _dataBuffer;
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
[self.dataBuffer appendData:data];
while (self.dataBuffer.length >= 4) {
NSInteger dataLength = 0;
//獲取數(shù)據(jù)長(zhǎng)度
[[self.dataBuffer subdataWithRange:(NSMakeRange(0, 4))] getBytes:&dataLength length:sizeof(dataLength)];
if (self.dataBuffer.length >= (dataLength+4)) {
NSData *realData = [self.dataBuffer subdataWithRange:NSMakeRange(4, dataLength)];
NSString *clientStr = [[NSString alloc] initWithData:realData encoding:(NSUTF8StringEncoding)];
NSLog(@"收到內(nèi)容: %@ 長(zhǎng)度:%zd", clientStr, clientStr.length);
self.dataBuffer = [[self.dataBuffer subdataWithRange:NSMakeRange(4+dataLength, self.dataBuffer.length-4-dataLength)] mutableCopy];
} else {
break;
}
}
[sock readDataWithTimeout:-1 tag:0];
}
注:
data 的getBytes: length:方法, 將data的length部分拷貝到getBytes的容器中, 當(dāng)length大于data長(zhǎng)度時(shí), 拷貝data的全部. 初始化NSInteger dataLength = 0, 將data頭部的4個(gè)長(zhǎng)度數(shù)據(jù)的字節(jié), 存儲(chǔ)到長(zhǎng)度為8 的dataLength 空間.
5 Demo
實(shí)現(xiàn)了Client和Server的工程: Demo
喜歡和關(guān)注都是對(duì)我的鼓勵(lì)和支持~
參考:
1 CocoaAsyncSocket使用
2 CocoaAsyncSocket介紹與使用
3 CocoaAsyncSocket 讀/寫操作以及粘包處理