一室叉、socket
1.網(wǎng)絡(luò)體系結(jié)構(gòu)和網(wǎng)絡(luò)協(xié)議
在說socket之前凿可,先要簡單說一說網(wǎng)絡(luò)體系結(jié)構(gòu)揪胃。OSI(Open System Interconnection Reference艾君, 開放式系統(tǒng)互聯(lián)通信參考)將計(jì)算機(jī)網(wǎng)絡(luò)體系結(jié)構(gòu)劃分為以下七層:
其中媒體層是網(wǎng)絡(luò)工程師所研究的對象署海,主機(jī)層則是用戶所面向和關(guān)心的內(nèi)容吗购。
常應(yīng)用到的傳輸協(xié)議有http協(xié)議、tcp/udp協(xié)議砸狞、ip協(xié)議分別對應(yīng)于應(yīng)用層捻勉、傳輸層、網(wǎng)絡(luò)層刀森。TCP/IP是傳輸層協(xié)議踱启,主要解決數(shù)據(jù)如何在網(wǎng)絡(luò)中傳輸;而HTTP是應(yīng)用層協(xié)議研底,主要解決如何包裝數(shù)據(jù)埠偿。
我們在傳輸數(shù)據(jù)時(shí),可以只使用傳輸層(TCP/IP)榜晦,那樣的話由于沒有應(yīng)用層冠蒋,便無法識別數(shù)據(jù)內(nèi)容,如果想要使傳輸?shù)臄?shù)據(jù)有意義乾胶,則必須使用應(yīng)用層協(xié)議抖剿,應(yīng)用層協(xié)議很多,有HTTP识窿、FTP斩郎、TELNET等等,也可以自己定義應(yīng)用層協(xié)議喻频。WEB使用HTTP作傳輸層協(xié)議缩宜,以封裝HTTP文本信息,然 后使用TCP/IP做傳輸層協(xié)議將它發(fā)送到網(wǎng)絡(luò)上甥温。Socket是對TCP/IP協(xié)議的封裝脓恕,Socket本身并不是協(xié)議膜宋,而是一個(gè)調(diào)用接口(API)窿侈,通過Socket炼幔,我們才能使用TCP/IP協(xié)議。
2.Http和Socket連接區(qū)別
2.1 socket連接:
建立起一個(gè)TCP連接需要經(jīng)過“三次握手”:
第一次握手:客戶端發(fā)送syn包(syn=j)到服務(wù)器史简,并進(jìn)入SYN_SEND狀態(tài)乃秀,等待服務(wù)器確認(rèn);
第二次握手:服務(wù)器收到syn包圆兵,必須確認(rèn)客戶的SYN(ack=j+1)跺讯,同時(shí)自己也發(fā)送一個(gè)SYN包(syn=k),即SYN+ACK包殉农,此時(shí)服務(wù)器進(jìn)入SYN_RECV狀態(tài)刀脏;
第三次握手:客戶端收到服務(wù)器的SYN+ACK包,向服務(wù)器發(fā)送確認(rèn)包ACK(ack=k+1)超凳,此包發(fā)送完畢愈污,客戶端和服務(wù)器進(jìn)入ESTABLISHED狀態(tài),完成三次握手轮傍。
握手過程中傳送的包里不包含數(shù)據(jù)暂雹,三次握手完畢后,客戶端與服務(wù)器才正式開始傳送數(shù)據(jù)创夜。斷開連接時(shí)服務(wù)器和客戶端均可以主動發(fā)起斷開TCP連接的請求杭跪,斷開過程需要經(jīng)過“四次握手”。
socket連接就是所謂的長連接驰吓,理論上客戶端和服務(wù)器端一旦加你其連接將不會主動斷掉涧尿;但是由于各種環(huán)境因素可能會連接斷開,比如說:服務(wù)器端或客戶端主機(jī)down了檬贰,網(wǎng)絡(luò)故障姑廉,或者兩者之間長時(shí)間沒有傳輸數(shù)據(jù),網(wǎng)絡(luò)防火墻可能會斷開該連接以釋放網(wǎng)絡(luò)資源偎蘸。所以當(dāng)一個(gè)socket連接中沒有數(shù)據(jù)傳輸?shù)臅r(shí)候庄蹋,那么為了維持連接需要發(fā)送心跳消息,具體心跳消息格式是開發(fā)者自己定義的迷雪。
2.2 HTTP連接
HTTP協(xié)議即超文本傳送協(xié)議(HypertextTransfer Protocol )限书,是Web聯(lián)網(wǎng)的基礎(chǔ),也是手機(jī)聯(lián)網(wǎng)常用的協(xié)議之一章咧,HTTP協(xié)議是建立在TCP協(xié)議之上的一種應(yīng)用倦西。
HTTP連接最顯著的特點(diǎn)是客戶端發(fā)送的每次請求都需要服務(wù)器回送響應(yīng),在請求結(jié)束后赁严,會主動釋放連接扰柠。從建立連接到關(guān)閉連接的過程稱為“一次連接”粉铐。
1)在HTTP 1.0中,客戶端的每次請求都要求建立一次單獨(dú)的連接卤档,在處理完本次請求后蝙泼,就自動釋放連接。
2)在HTTP 1.1中則可以在一次連接中處理多個(gè)請求劝枣,并且多個(gè)請求可以重疊進(jìn)行汤踏,不需要等待一個(gè)請求結(jié)束后再發(fā)送下一個(gè)請求。
由于HTTP在每次請求結(jié)束后都會主動釋放連接舔腾,因此HTTP連接是一種“短連接”溪胶,要保持客戶端程序的在線狀態(tài),需要不斷地向服務(wù)器發(fā)起連接請求稳诚。通常的 做法是即時(shí)不需要獲得任何數(shù)據(jù)哗脖,客戶端也保持每隔一段固定的時(shí)間向服務(wù)器發(fā)送一次“保持連接”的請求,服務(wù)器在收到該請求后對客戶端進(jìn)行回復(fù)扳还,表明知道客戶端“在線”才避。若服務(wù)器長時(shí)間無法收到客戶端的請求,則認(rèn)為客戶端“下線”普办,若客戶端長時(shí)間無法收到服務(wù)器的回復(fù)工扎,則認(rèn)為網(wǎng)絡(luò)已經(jīng)斷開。
二.GCDAsyncSocket
在iOS開發(fā)中使用socket衔蹲,一般都是用第三方庫GCDAsyncSocket(雖然也有原生CFSocket)肢娘。
GCDAsyncSocket 下載地址: GCDAsyncSocket
使用之前需要先在項(xiàng)目引入ASyncSocket庫:
- 把ASyncSocket庫源碼加入項(xiàng)目:只需要增加RunLoop目錄中的AsyncSocket.h、AsyncSocket.m舆驶、AsyncUdpSocket.h和AsyncUdpSocket.m四個(gè)文件橱健。
- 在項(xiàng)目增加CFNetwork框架:在Framework目錄右健,選擇Add-->Existing Files... , 選擇 CFNetwork.framework
下面開始介紹一下如何使用ASyncSocket:
一般來說,一個(gè)用戶只需要建立一個(gè)socket長連接拘荡,所以可以用單例類方便使用。
單例方法
// 創(chuàng)建單例
+ (Singleton *) sharedInstance
{
static Singleton *sharedInstace = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstace = [[self alloc] initPrivate];
});
return sharedInstace;
}
// 私有創(chuàng)建方法珊皿,不公開
- (instancetype)initPrivate {
if (self = [super init]) {
_lockStr = @"1234";
}
return self;
}
// 廢除init創(chuàng)建方法
- (instancetype)init {
@throw [NSException exceptionWithName:@"初始化異常" reason:@"不允許通過init方法創(chuàng)建對象" userInfo:nil];
}
建立socket長連接
#define TIME_OUT 20
// 建立socket連接
-(void)socketConnectHost{
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
NSLog(@"連接服務(wù)器");
NSError *error = nil;
[_socket connectToHost:_socketHost onPort:_socketPort withTimeout:TIME_OUT error:&error];
}
// socket成功連接回調(diào)
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"成功連接到%@:%d",host,port);
_bufferData = [[NSMutableData alloc] init]; // 存儲接收數(shù)據(jù)的緩存區(qū)
[_socket readDataWithTimeout:-1 tag:99];
}
心跳###
@property (nonatomic, retain) NSTimer *heartTimer; // 心跳計(jì)時(shí)器
在連接成功的回調(diào)方法里巨税,啟動定時(shí)器,每隔2秒向服務(wù)器發(fā)送固定的消息來檢測長連接草添。
// 心跳連接
-(void)longConnectToSocket{
根據(jù)服務(wù)器要求發(fā)送固定格式的數(shù)據(jù),假設(shè)為指令@"longConnect",但是一般不會是這么簡單的指令
NSString *longConnect = @"longConnect";
NSData *dataStream = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[_socket writeData:dataStream withTimeout:1 tag:1];
}
斷開連接###
- 主動斷開
- (void)cutOffSocket {
[_socket disconnect];
_socket.userData = @(SocketOfflineByUser);
NSLog(@"斷開連接");
}
- 被動斷開
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
if (err.code == 57) {
_socket.userData = @(SocketOfflineByWifiCut); // wifi斷開
}
else {
_socket.userData = @(SocketOfflineByServer); // 服務(wù)器掉線
}
NSLog(@"斷開連接屠凶,錯(cuò)誤:%@",err);
}
錯(cuò)誤碼請見 sys/errno.h
發(fā)送消息
// 發(fā)消息
- (void)sendMessage:(NSData *)data {
[_socket writeData:data withTimeout:TIME_OUT tag:10];
}
// wirte成功
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
// 持續(xù)接收數(shù)據(jù)
// 超時(shí)設(shè)置為附屬肆资,表示不會使用超時(shí)
[_socket readDataWithTimeout:-1 tag:tag];
}
接收消息
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 在這里處理消息
[self disposeBufferData:data];
//持續(xù)接收服務(wù)端的數(shù)據(jù)
[sock readDataWithTimeout:-1 tag:tag];
}
粘包
最后說說戰(zhàn)報(bào)的問題和相關(guān)處理。粘包是指發(fā)送方發(fā)送的若干包數(shù)據(jù)到接收方接收時(shí)粘成一包迅耘,TCP傳輸往往會出現(xiàn)粘包贱枣。出現(xiàn)粘包現(xiàn)象的原因是多方面的,它既可能由發(fā)送方造成颤专,也可能由接收方造成。發(fā)送方引起的粘包是由TCP協(xié)議本身造成的钠乏,TCP為提高傳輸效率栖秕,發(fā)送方往往要收集到足夠多的數(shù)據(jù)后才發(fā)送一包數(shù)據(jù)。若連續(xù)幾次發(fā)送的數(shù)據(jù)都很少晓避,通常TCP會根據(jù)優(yōu)化算法把這些數(shù)據(jù)合成一包后一次發(fā)送出去簇捍,這樣接收方就收到了粘包數(shù)據(jù)。接收方引起的粘包是由于接收方用戶進(jìn)程不及時(shí)接收數(shù)據(jù)俏拱,從而導(dǎo)致粘包現(xiàn)象暑塑。這是因?yàn)榻邮辗较劝咽盏降臄?shù)據(jù)放在系統(tǒng)接收緩沖區(qū),用戶進(jìn)程從該緩沖區(qū)取數(shù)據(jù)锅必,若下一包數(shù)據(jù)到達(dá)時(shí)前一包數(shù)據(jù)尚未被用戶進(jìn)程取走事格,則下一包數(shù)據(jù)放到系統(tǒng)接收緩沖區(qū)時(shí)就接到前一包數(shù)據(jù)之后,而用戶進(jìn)程根據(jù)預(yù)先設(shè)定的緩沖區(qū)大小從系統(tǒng)接收緩沖區(qū)取數(shù)據(jù)搞隐,這樣就一次取到了多包數(shù)據(jù)驹愚。
為了避免粘包現(xiàn)象,可采取以下幾種措施劣纲。一是對于發(fā)送方引起的粘包現(xiàn)象逢捺,用戶可通過編程設(shè)置來避免,TCP提供了強(qiáng)制數(shù)據(jù)立即傳送的操作指令push癞季,TCP軟件收到該操作指令后劫瞳,就立即將本段數(shù)據(jù)發(fā)送出去,而不必等待發(fā)送緩沖區(qū)滿绷柒;二是對于接收方引起的粘包志于,則可通過優(yōu)化程序設(shè)計(jì)、精簡接收進(jìn)程工作量辉巡、提高接收進(jìn)程優(yōu)先級等措施恨憎,使其及時(shí)接收數(shù)據(jù),從而盡量避免出現(xiàn)粘包現(xiàn)象;三是由接收方控制憔恳,將一包數(shù)據(jù)按結(jié)構(gòu)字段瓤荔,人為控制分多次接收,然后合并钥组,通過這種手段來避免粘包输硝。
以上提到的三種措施,都有其不足之處程梦。第一種編程設(shè)置方法雖然可以避免發(fā)送方引起的粘包点把,但它關(guān)閉了優(yōu)化算法郎逃,降低了網(wǎng)絡(luò)發(fā)送效率褒翰,影響應(yīng)用程序的性能优训,一般不建議使用各聘。第二種方法只能減少出現(xiàn)粘包的可能性躲因,但并不能完全避免粘包,當(dāng)發(fā)送頻率較高時(shí)搁嗓,或由于網(wǎng)絡(luò)突發(fā)可能使某個(gè)時(shí)間段數(shù)據(jù)包到達(dá)接收方較快箱靴,接收方還是有可能來不及接收衡怀,從而導(dǎo)致粘包。第三種方法雖然避免了粘包够委,但應(yīng)用程序的效率較低茁帽,對實(shí)時(shí)應(yīng)用的場合不適合。
一種比較周全的對策是:接收方創(chuàng)建一預(yù)處理線程吊输,對接收到的數(shù)據(jù)包進(jìn)行預(yù)處理铁追,將粘連的包分開。具體的方法就是在發(fā)送數(shù)據(jù)是在數(shù)據(jù)前加入包頭扭屁,接收時(shí)首先將待處理的接收數(shù)據(jù)流(長度為m)強(qiáng)行轉(zhuǎn)換成預(yù)定的結(jié)構(gòu)數(shù)據(jù)形式料滥,并從中取出結(jié)構(gòu)數(shù)據(jù)長度字段n埋泵,而后根據(jù)n計(jì)算得到第一包數(shù)據(jù)長度丽声。
1)若n<m雁社,則表明數(shù)據(jù)流內(nèi)容超過一段完整的數(shù)據(jù)結(jié)構(gòu)晒骇,將前n長度的數(shù)據(jù)截取并進(jìn)行處理洪囤,對于剩下的m-n長度數(shù)據(jù)重復(fù)上述解析和判斷瘤缩。
2)若n=m,則表明數(shù)據(jù)流內(nèi)容恰好是一完整結(jié)構(gòu)數(shù)據(jù)锦溪,直接將其存入臨時(shí)緩沖區(qū)即可刻诊。
3)若n>m牺丙,則表明數(shù)據(jù)流內(nèi)容尚不夠構(gòu)成一完整結(jié)構(gòu)數(shù)據(jù),需留待與下一包數(shù)據(jù)合并后再行處理亿昏。
下面是我和服務(wù)器約定好的包頭和包的類型
// 定義包頭
typedef struct tagNetPacketHead
{
int version; //版本
int eMainType; //包類型主協(xié)議
int eSubType; //包類型子協(xié)議
unsigned int nLen; //包體長度
} NetPacketHead;
// 定義發(fā)包類型
typedef struct tagNetPacket
{
NetPacketHead netPacketHead; //包頭
unsigned char *packetBody; //包體
} NetPacket;
收到數(shù)據(jù)時(shí)先將收到的數(shù)據(jù)放到緩存中龙优,然后進(jìn)行上述判斷事秀。
- (void)disposeBufferData:(NSData *)data {
@synchronized (self.lockStr) {
[_bufferData appendData:data];
while (_bufferData.length >= 16) {
struct tagNetPacketHead head;
[_bufferData getBytes:&head range:NSMakeRange(0, 16)];
while (_bufferData.length >= 16 && !(head.version == 1 && head.eMainType > -10 && head.eMainType < 1000 && head.eSubType > - 10 && head.eSubType < 1000)) {
int a = (int)_bufferData.length - 1;
_bufferData = [_bufferData subdataWithRange:NSMakeRange(1, a)].mutableCopy;
if (_bufferData.length >= 16) {
[_bufferData getBytes:&head range:NSMakeRange(0, 16)];
}
}
BOOL isIn = !(head.nLen > (_bufferData.length - 16));
if (isIn && _bufferData.length >= 16) {
NSMutableData *pendingData = [NSMutableData data];
if (head.eSubType == -1) {
pendingData = [_bufferData subdataWithRange:NSMakeRange(4, 4)].mutableCopy;
[pendingData appendData:[_bufferData subdataWithRange:NSMakeRange(16, head.nLen)]];
}
else {
pendingData = [_bufferData subdataWithRange:NSMakeRange(4, 8)].mutableCopy;
NSLog(@"%d", head.nLen);
[pendingData appendData:[_bufferData subdataWithRange:NSMakeRange(16, head.nLen)]];
}
[DisposeManager disposeData:pendingData num:head.eMainType];
int totalLen = _bufferData.length;
_bufferData = [_bufferData subdataWithRange:NSMakeRange(16 + head.nLen, totalLen - 16 - head.nLen)].mutableCopy;
}
}
}
}
注意:在這里加入線程鎖@synchronized (self.lockStr)宰衙,防止緩沖區(qū)同時(shí)被多個(gè)線程訪問發(fā)生緩沖區(qū)數(shù)據(jù)混亂供炼。
發(fā)送數(shù)據(jù)時(shí)則需要將數(shù)據(jù)按照約定的結(jié)構(gòu)進(jìn)行處理袋哼,在前邊加上包頭涛贯。
- (NSMutableData *)linkDataWithVersion:(NSData *)versionData mainType:(int)mainType subType:(int)subType packetBody:(NSData *)packetBody{
if (!versionData) {
int version = 1;
versionData = [NSMutableData dataWithBytes:&version length:sizeof(version)];
}
NSMutableData *mainTypeData = [NSMutableData dataWithBytes:&mainType length:sizeof(mainType)];
NSMutableData *subTypeData = [NSMutableData dataWithBytes:&subType length:sizeof(subType)];
unsigned int len;
if (packetBody) {
len = packetBody.length;
}
else {
len = 0;
}
NSMutableData *lenData = [NSMutableData dataWithBytes:&len length:sizeof(len)];
NSMutableData *sendData = [[NSMutableData alloc] init];
[sendData appendData:versionData];
[sendData appendData:mainTypeData];
[sendData appendData:subTypeData];
[sendData appendData:lenData];
[sendData appendData:packetBody];
return sendData.mutableCopy;
}
注:這里的mainTypeData和subTypeData只是和本工程相關(guān)的主協(xié)議和子協(xié)議弟翘,并不具備普遍性稀余。
注:version和服務(wù)器端約定好是1趋翻。