這些天項目不是很急解幽,自己研究了一下socket,感覺收獲頗豐烘苹,真的很高興躲株。
- http :
它是超文本傳輸協(xié)議,對應(yīng)于應(yīng)用層镣衡,它主要強調(diào)的是對數(shù)據(jù)的封裝霜定。它是所謂的“短鏈接”档悠,一般都是客服端發(fā)送請求 到服務(wù)器 服務(wù)給了客戶端應(yīng)答過后即斷開鏈接,我們平時做的項目中 只要不是客戶端和服務(wù)器保持長期的鏈接的情況下 基本都用的是http - socket :
它是我們所謂的“長鏈接”望浩,它是一個套接字 實際上我覺得它是支持傳輸協(xié)議的的一個基本的單元辖所,它支持tcp/ip 協(xié)議 也支持udp協(xié)議。 -
tcp/ip :
我們一般使用的是socket的協(xié)議都是tcp/ip協(xié)議磨德,這種協(xié)議我們認為是絕對安全的為什么呢 因為他是確認建立完整的通道之后才進行傳輸數(shù)據(jù) 所以只要鏈接的通道在數(shù)據(jù)一定是能從A ->B 或者 B->A的 我們即時通訊的基本上都是這種協(xié)議缘回,它建立鏈接需要“三次握手”,斷開鏈接需要“四次握手”典挑。
“三次握手”:
1.客戶端發(fā)送syn(ack = j)包給服務(wù)端酥宴,并自己進入syn_send狀態(tài),等待服務(wù)端確認搔弄。
2.服務(wù)端收到客戶端的syn包幅虑,必須確認客戶端的包即(ack = j+1),同時自己也發(fā)送一個syn(ack = k)的一個包 ,它發(fā)給客戶端的是 自己的syn包和確認的客戶端的包顾犹,并且自己進入syn_recv狀態(tài)倒庵。
3.客戶端收到服務(wù)器給的包,并且發(fā)送服務(wù)端一個確認包及(ack = k+1)的包炫刷,這個包發(fā)送完畢 雙方都進入ESTABLISHED狀態(tài)擎宝,三次握手成功。
斷開的“四次握手”:
實話說這個我說不很清楚浑玛,但是我查了一些資料有些形象的比喻绍申,我基本明白了。
1. a 告訴b要斷開鏈接
2.b收到a的消息顾彰,并且告訴a等待极阅,等待自己發(fā)送未完成的數(shù)據(jù)包。
3.b告訴a發(fā)送數(shù)據(jù)完成涨享,a可以關(guān)閉了筋搏。
4.a知道b發(fā)送完成數(shù)據(jù)了,并且a等待一下(這個我不知道a為什么不馬上關(guān)閉)厕隧,a關(guān)閉鏈接奔脐。
我在網(wǎng)絡(luò)上找的一個圖片形象的描述了這一個過程。
image.png
其實我們正常寫代碼根本就是不知道這些的吁讨。因為我們基本不基于蘋果原聲的api 那個是純c的我們一般不用 用框架的話框架內(nèi)部都給我們做好了髓迎,我們只需要調(diào)用方法就好了,但是我們應(yīng)該明白原理建丧。
- udp:
說實話我工作中根本沒用用到這個排龄,但是它是非安全的,舉個簡單的例子 tcp/ip就是打電話茶鹃,雙方信號通了才能說話涣雕,udp是相當于別人給你說話艰亮,那你有可能聽到也有可能聽不到,只是知道說話的人說話了挣郭。但是udp的效率還是比tcp相對來說要高迄埃。我們一般用到的udp一般是廣播等等的吧。 - 做一個及時通訊的聊天
我們做一個及時通訊的聊天需要準備什么呢 兑障?對于我們客戶端而言我覺得需要準備數(shù)據(jù)格式侄非、協(xié)議、還有及時通訊的框架流译。
1.數(shù)據(jù)格式:
我理解的是我們是用json 逞怨、protobuf還是xml。
1.json:我們現(xiàn)在一般的公司有用到的因為它簡單福澡,直接將我們的數(shù)據(jù)轉(zhuǎn)換成json 在將 json轉(zhuǎn)換成data 完成我們數(shù)據(jù)的封裝叠赦。
2.protobuf 相信很多人可能第一次聽說,它是谷歌寫的一套框架用來我們封裝數(shù)據(jù)的 里面定義的都是模型 除秀,用起來非常的方便,不好的地方就是安裝環(huán)境不是很好安裝算利。
3.xml我沒用用過這種方式 缎患,如果有的公司用的話請查查資料吧 我這個不是很了解。
上面這三種方式最好的是protobuf 因為它壓縮包的大小大概是json的10分之一蚂蕴,大概是xml的20分之一。所以我重點介紹的是protobuf.為什么數(shù)據(jù)包越小越好俯邓,比如我們經(jīng)常玩的王者榮耀骡楼,那里面的幾乎每一個操作都是socket通信,如果數(shù)據(jù)包比較大在網(wǎng)絡(luò)不是很通暢的情況下稽鞭,那人家本來不來不該死的是不是就死了鸟整,人家該放出技能的情況下 是不是有可能放不出來了。所以數(shù)據(jù)包小 很重要朦蕴。 - protobuf :
1.安裝:這里用的是cocoapod的方式安裝篮条,因為這個是最簡單的弟头,如果自己手動倒入絕對是缺爹少娘的。
pod 'Protobuf', '~> 3.1.0'
2.創(chuàng)建一個proto的文件 涉茧,最簡單的辦法是我們 用終端命令 赴恨,touch xx.proto.
里面的內(nèi)容具體書寫:
syntax = "proto2";
message UserInfo {
required string name = 1;
required int64 level = 2;
}
message TextMessage {
required UserInfo user = 1;
required string text = 2;
}
message GiftMessage {
required UserInfo user = 1;
required string giftname = 2;
required string giftURL = 3;
required string giftCount = 4;
}
syntax = "proto2";好像說的是包名,這個我們不用糾結(jié)等下下面會說伴栓。
required 是必須要傳的參數(shù)伦连,如果這個不傳 會導(dǎo)致protobuf 沒有數(shù)據(jù)。
我們現(xiàn)在建立的protobuf的proto文件寫好了 下面將我們寫的proto文件倒入到我們的工程钳垮。順便說一下 我們不能這樣寫
message GiftMessage {
required UserInfo user = 1;
required string giftname = 1;
}
因為后面的那個數(shù)字是它的一個標示惑淳,標示不能重復(fù)。我給大家看一下我的proto文件的位置
到這一步是沒有這兩個文件的
下面我們用命令生成這兩個文件
首先我們用命令切換到我們proto所在的文件的位置 饺窿,比如我的文件的位置是:
這時候我們執(zhí)行這個命令:
protoc --objc_out=. *.proto
我們回到工程點擊我們的proto文件然后show in finder 會看到這時候生成兩個文件
將這兩個文件拖到我們的工程中 變成這個樣子:
我們編譯 發(fā)現(xiàn)會報錯歧焦,這是非arc的原因 我們只需要這樣操作 我用圖片來進行演示:
只時候進行編譯 發(fā)現(xiàn)成功了 我們這時候應(yīng)該默默的高興。
指的說明的是protobuf 安裝網(wǎng)上怎么說的都有 我也嘗試了 但是我不知道什么原因基本上都不能成功肚医,我現(xiàn)在這個是沒有問題的绢馍,大家可以試試。
下面我們就可以使用了忍宋,具體怎么使用:
這是一個protobuf付值并且轉(zhuǎn)換成data的例子痕貌。
GiftMessage *giftM = [[GiftMessage alloc] init];
giftM.user = self.userInfo;
giftM.giftname = giftName;
giftM.giftURL = imageUrl;
giftM.giftCount = giftCount;
NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];
將protobuf轉(zhuǎn)換成我們的模型
UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
我們平時用到的基本就兩個方法,還有其他的使用就到github上看下文檔就可以了糠排。注意服務(wù)端和我們的客戶端都用一套protobuf也就是proto文件是一樣的舵稠,而且proto支持多言語 什么java 、php入宦、 oc哺徊、swift、android等等的都支持乾闰。
- 協(xié)議:
其實我們做一個聊天的功能最重要的就是協(xié)議的定義和我們的封包和解包落追。
協(xié)議我們一般都是自定義的協(xié)議,這個協(xié)議是我們和我們的后端商量好的
我們正規(guī)公司做的協(xié)議大概是這樣的
version:版本(一般4個字節(jié))
type:類型(有的公司用一個也有的公司用 maintype 和 subtype共同決定涯肩,一般4個字節(jié))
lengthData: 消息體的長度(一般4個字節(jié))
messageData:消息體
salt:加密的鹽轿钠,需要加密才用,不加密的可能不用 具體根據(jù)公司來定病苗。(一般4個字節(jié))
具體說明:
1.version的作用 一般來說 比如 我們的qq 假如新用戶升級了一個版本疗垛,但是老用戶還沒有升級到最新的版本,那么我們新用戶給老用戶發(fā)送消息的時候老用戶可能就解析不了(比如你公司的加密升級)所以需要version硫朦。
2.type:顧名思義就是 比如我們的文本 圖片 音頻 視頻等等的 用來做區(qū)分的.
3.消息體的長度:這個需要客戶端提前計算好贷腕,因為你丟過去一個數(shù)據(jù)包 服務(wù)器怎么解呢 它怎么知道那塊是消息體的長度呢 有的人說去掉前面的就是消息體 那么這個包是不完整的呢 比如 完整的包是1024個字節(jié) 實際山服務(wù)端收到的是800字節(jié) 服務(wù)端怎么知道這個是不是完整的呢。所以消息體的長度很重要.
4.salt 顧名思義 這個不在多說 具體有你公司而定.
- 我們按照順序?qū)⑽覀冞@些每一個組成一個data 然后進行拼接 最后將一個整個的data發(fā)送出去。注意順序不能錯 公司怎么定義你需要怎么拼接 否則服務(wù)端解析會報錯泽裳。
- socket用什么框架 oc的話 我用的是CocoaAsyncSocket瞒斩,swift也有很多 我具體不說了,我用的oc 所以大家查一下就可以了涮总。
說實話這個框架很簡單 具體怎么使用大家具體百度一下 我覺得太簡單了 所以我就不說了胸囱。 - socket的封包:
我覺得從現(xiàn)在以后的都很重要 ,何謂封包就是我們按照我們的協(xié)議將我們的數(shù)據(jù)封裝起來妹卿,將一個完整的包發(fā)給服務(wù)端旺矾。我下面說一下我自己簡單定的協(xié)議
lengthData:消息體的長度 占4個字節(jié)
type:類型 占2個字節(jié)
data :消息體
因為服務(wù)器是我自己寫的(網(wǎng)上查了一點資料) 我以我的電腦當作服務(wù)器
下面是我具體的封包的代碼,我拿文本消息進行舉例 代碼寫了很詳細的注釋
GiftMessage *giftM = [[GiftMessage alloc] init];
giftM.user = self.userInfo;
giftM.giftname = giftName;
giftM.giftURL = imageUrl;
giftM.giftCount = giftCount;
NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeGift];
/**
這個方法是封包 組裝數(shù)據(jù)的格式是夺克。type + length + bodyData
@param type 類型
@param bodyData 要發(fā)送的數(shù)據(jù)
@param maxM 最大的發(fā)送數(shù)據(jù)M數(shù)量
@return 整理好的data
*/
- (NSMutableData *)calculatorData:(MessageType)type bodyData:(NSData *)bodyData maxM:(int)maxM{
// 需要返回的data
NSMutableData *needSendData = [[NSMutableData alloc] init];
// 類型的data 固定2個字節(jié)
int typeInt = (int)type;
NSMutableData *typeData = [NSMutableData dataWithBytes:&typeInt length:2];
// 發(fā)送數(shù)據(jù)的長度data 也是固定4個字節(jié)
int lenth = (int)bodyData.length;
NSMutableData *legthData = [NSMutableData dataWithBytes:&lenth length:4];
// 進行拼接,注意順序不能錯
[needSendData appendData:legthData];
[needSendData appendData:typeData];
[needSendData appendData:bodyData];
return needSendData;
}
再次強調(diào)順序不能錯
順便說一下 :我們封裝的包是完整的 但是我們服務(wù)器收到的包不一定就是完整的為什么因為tcp/ip 協(xié)議是有優(yōu)化的算法的它可能會分批發(fā)送也有可能是發(fā)送一個整個數(shù)據(jù)包箕宙,這樣的話就會導(dǎo)致粘包 服務(wù)器發(fā)送給我們的也有可能是這種情況,下面我舉一下導(dǎo)致沾包的原因
假設(shè)有兩個數(shù)據(jù)包
1.發(fā)送a是完整的b不完整
2.假設(shè)發(fā)送a是不完整的铺纽,b是完整的
3.假設(shè)a柬帕、b都是完整的
等等還有其他的情況 我大致就是據(jù)這個三個例子
其實第三個情況是沒有問題的,1狡门、2 等的不完整的數(shù)據(jù)包就會導(dǎo)致沾包問題
- socket的解包陷寝,下面是我的解包(很重要 ,解包不了導(dǎo)致直接數(shù)據(jù)解析失斊淞蟆)
/**
拆除包 防止沾包
@param data data
@param sock sock
*/
- (void)parseData:(NSData *)data socket:(GCDAsyncSocket *)sock{
// 首先付給要處理的data
[self.cacheParseData appendData:data];
// 找到我們當初存儲的長度
NSData *lengthData = [self.cacheParseData subdataWithRange:NSMakeRange(0, 4)];
int shouldLength = 0;
[lengthData getBytes:&shouldLength length:4];
shouldLength += 6;
while (self.cacheParseData.length > 6) {
if (shouldLength > self.cacheParseData.length) { // 說明這個包是不完整的
[sock readDataWithTimeout:TIME_OUT tag:0];
break;
}else{ // 說明這個包至少是大于等于一個完整包的長度
NSData *needParseData = [self.cacheParseData subdataWithRange:NSMakeRange(0, shouldLength)];
// 在這里開始正式解決這個包
[self parseData:needParseData dataLength:shouldLength - 6];
[self.cacheParseData replaceBytesInRange:NSMakeRange(0, shouldLength) withBytes:nil length:0];
[sock readDataWithTimeout:TIME_OUT tag:0];
}
}
}
/**
進入到這個方法說明是一個完整的包凤跑,并且是能夠解析的
@param data data
@param shouldHaveLength 消息的長度
*/
- (void)parseData:(NSData *)data dataLength:(int)shouldHaveLength{
// 類型
NSData *typeData = [data subdataWithRange:NSMakeRange(4, 2)];
int type = 0;
[typeData getBytes:&type length:2];
// 消息體
NSData *bodyData = [data subdataWithRange:NSMakeRange(6, shouldHaveLength)];
switch (type) {
case MessageTypeHeart:
{
LDGLog(@"心跳包的消息");
}
break;
case MessageTypeJoinRoom:
{
UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveJoinRoom:)]) {
[self.toolDelegate haveJoinRoom:userI];
}
}
break;
case MessageTypeLeaveRoom:
{
UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveLeaveRoom:)]) {
[self.toolDelegate haveLeaveRoom:userI];
}
}
break;
case MessageTypeText:
{
TextMessage *textM = [TextMessage parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveAcceptTextMessage:)]) {
[self.toolDelegate haveAcceptTextMessage:textM];
}
}
break;
case MessageTypeGift:
{
GiftMessage *giftM = [GiftMessage parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveAcceptGiftMessage:)]) {
[self.toolDelegate haveAcceptGiftMessage:giftM];
}
}
break;
default:
{
LDGLog(@"不知道是啥子消息");
}
break;
}
}
- 至此 我們socket的簡單的封包和解包就做完了,我們基本有一點基礎(chǔ)了叛复,但是我們現(xiàn)在做的遠遠不夠仔引,因為有的公司這個需求 就是我要求你客戶端每一次發(fā)送的包不能大于2M?我們該怎么辦?還有我們做socket 聽過心跳包 那個是怎么回事褐奥?好 下面我會針對不同的問題說幾點注意點咖耘。我覺得很重要。
- 個人覺得非常重要的幾點說明:
1.假如公司要求你每次發(fā)送的數(shù)據(jù)包不能大于2M,我覺得我們在封裝包的情況下應(yīng)該這樣寫撬码,因為我屬于自己寫的服務(wù)器 儿倒,沒有那么復(fù)雜,我下面的代碼我沒有驗證呜笑,但是我覺得大致思路是對的夫否。
/**
這個方法是封包 組裝數(shù)據(jù)的格式是。type + length + bodyData
@param type 類型
@param bodyData 要發(fā)送的數(shù)據(jù)
@param maxM 最大的發(fā)送數(shù)據(jù)M數(shù)量
@return 整理好的data
*/
- (NSMutableData *)calculatorData:(MessageType)type bodyData:(NSData *)bodyData maxM:(int)maxM{
// 需要返回的data
NSMutableData *needSendData = [[NSMutableData alloc] init];
// 類型的data 固定2個字節(jié)
int typeInt = (int)type;
NSMutableData *typeData = [NSMutableData dataWithBytes:&typeInt length:2];
// 發(fā)送數(shù)據(jù)的長度data 也是固定4個字節(jié)
int lenth = (int)bodyData.length;
NSMutableData *legthData = [NSMutableData dataWithBytes:&lenth length:4];
// 進行拼接,注意順序不能錯
[needSendData appendData:legthData];
[needSendData appendData:typeData];
[needSendData appendData:bodyData];
if (needSendData.length > 2 * 1024 *1024) { // 說明數(shù)據(jù)包大于2M
// 計算countNumber是一個小算法叫胁,就是計算我們的數(shù)據(jù)包有(2*1024*1024)慷吊,最后一個不足也加1
NSInteger countNumber = (needSendData.length - 1)/ (2*1024*1024) + 1;
for (NSInteger index = 0; index < countNumber; index ++) {
NSData *perData = [[NSData alloc] init];
if (index == countNumber -1 ) {
perData = [needSendData subdataWithRange:NSMakeRange(index *(2*1024*1024) , needSendData.length - index *(2*1024*1024))];
}else{
perData = [needSendData subdataWithRange:NSMakeRange(index *(2*1024*1024) , 2*1024*1024)];
}
[self.sectionArray addObject:perData];
}
}
return needSendData;
}
/**
發(fā)送禮物的消息
@param imageUrl 圖片的url
@param giftName 圖片的名字
@param giftCount 禮物的數(shù)量
*/
- (void)sendGiftMessage:(NSString *)imageUrl giftName:(NSString *)giftName giftCount:(NSString *)giftCount{
GiftMessage *giftM = [[GiftMessage alloc] init];
giftM.user = self.userInfo;
giftM.giftname = giftName;
giftM.giftURL = imageUrl;
giftM.giftCount = giftCount;
NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];
if (self.sectionArray.count) {
for (NSData *data in self.sectionArray) {
[self.socket writeData:data withTimeout:TIME_OUT tag:MessageTypeGift];
}
[self.sectionArray removeAllObjects];
}else{
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeGift];
}
}
- 解釋疑問:我開始可能會產(chǎn)生這樣的一個疑問,我現(xiàn)在是把一個完整的包分開了 那會不會出現(xiàn)這樣的一個問題呢 會不會導(dǎo)致我把一個漢字或者一個字母分成兩半了呢曹抬,后來我想了一下不會 因為我們在解包的情況下 我判斷只有完整的包才會解析,所以在界面顯示的時候不會出現(xiàn)。
2.什么心跳包谤民,心跳包有什么作用堰酿?
心跳包就是在間隔相同的時間內(nèi),客戶端像服務(wù)端發(fā)送你們規(guī)定好的一個數(shù)據(jù)包张足,用來判斷客戶端與服務(wù)端一直保持鏈接的触创。下面是我寫的心跳包,我在子線程開辟了一個定時器为牍。
/**
* Called when a socket connects and is ready for reading and writing.
* The host parameter will be an IP address, not a DNS name.
**/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
LDGLog(@"說明已經(jīng)鏈接成功了");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (!self.heartTimer) {
self.heartTimer = [NSTimer timerWithTimeInterval:HEART_TIME target:self selector:@selector(heartAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.heartTimer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
});
[sock readDataWithTimeout:TIME_OUT tag:0];
}
/**
心跳包的事件 為了保持客戶端和服務(wù)端長期的鏈接
*/
-(void)heartAction{
NSData *heartData = [@"my heart is very bad" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *needSendData = [self calculatorData:MessageTypeHeart bodyData:heartData maxM:2];
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeHeart];
}
解釋:其中為什么在[[NSRunLoop currentRunLoop] run];而沒有用[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]這是runloop相關(guān)的知識哼绑,我們不細說因為 runloop在子線程默認是不開啟的 在主線程默認是開啟的。順便說一句 每個心跳包的時間是不一樣的碉咆,一般根據(jù)公司規(guī)定抖韩, 一般來說10秒、20秒 疫铜、30秒茂浮。心跳包能接收到消息說明是鏈接著的。反之說明斷開鏈接壳咕。
3.說明一下 一般而言 根本不會寫服務(wù)器的代碼席揽,我也是在網(wǎng)上找的 自己修改了一丟丟,那我們?nèi)绻粫懛?wù)器代碼谓厘,我們自己做socket項目怎么演示呢幌羞,我想到一個不算太好的辦法,可以驗證我們解析包的時候是否解析出來
- CocoaAsyncSocket大致說一下:
4.1)#import "GCDAsyncSocket.h"
4.2)鏈接主機和端口號竟稳,端口號不能小于等于1024 因為這些端口都被系統(tǒng)或者什么的占領(lǐng)了属桦,為什么還需要端口號 因為一個ip地址下可以有多個服務(wù)器,怎樣找到我們的那臺服務(wù)器就是需要端口號住练。端口號和ip地址確定唯一的服務(wù)器地啰。
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
[self.socket connectToHost:@"192.168.100.193" onPort:7878 error:nil];
4.3)已經(jīng)鏈接成功的delegate回調(diào)
/**
* Called when a socket connects and is ready for reading and writing.
* The host parameter will be an IP address, not a DNS name.
**/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
4.3)已經(jīng)讀取成功的回調(diào)
/**
* Called when a socket has completed reading the requested data into memory.
* Not called if there is an error.
**/
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
4.4)斷開鏈接的回調(diào) 被動斷開
/**
已經(jīng)斷開鏈接了 被動斷開鏈接
@param sock socket
@param err 錯誤信息
*/
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
4.5) 客戶端主動斷開鏈接
[sock disconnect]
4.6)鏈接主機成功了
/**
* Called when a socket connects and is ready for reading and writing.
* The host parameter will be an IP address, not a DNS name.
**/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
大致就是這些常用的 還有需要的 我們看一下文檔就可以了說的很清楚,大家肯定也會讲逛。
- 如果公司用的json 封裝數(shù)據(jù)的怎么辦亏吝,我在網(wǎng)上找了一段很好的代碼,我直接粘貼了盏混,大家看一下就很明白了 很簡單
NSMutableDictionary *dictTemp = [NSMutableDictionary dictionary];
dictTemp[@"username"] = @"LD";
//先創(chuàng)建模型 --> 轉(zhuǎn)Json -->轉(zhuǎn)字符串
TestModel *model = [TestModel new];
model.type = 1;
model.userName = @"LD";
model.age = @"18";
model.message = @"Hellow";
model.Content = dictTemp;
//先將模型轉(zhuǎn)換成Json格式的數(shù)據(jù)這里根據(jù)自己項目情況來看是否需要轉(zhuǎn)成Json格式 使用到了MJExtension蔚鸥,
NSString * strJson = [[NSString alloc] initWithData :model.mj_JSONData encoding :NSUTF8StringEncoding];
Cs_Connect *connect = [Cs_Connect new];
connect.serverID = 1;
connect.message = strJson;
connect.length = (int)connect.message.length;
//將數(shù)據(jù)傳換成二進制數(shù)據(jù),轉(zhuǎn)換之后的數(shù)據(jù)和協(xié)議順序是一致的(為什么不需要調(diào)整順序我也不知道,有興趣的的同學(xué)自己去研究下這個方法)
NSMutableData *dataModel = [socket RequestSpliceAttribute:connect];
// 通過Socket發(fā)出去
[socket sendMessage:dataModel];
// 將模型數(shù)據(jù)轉(zhuǎn)換成二進制數(shù)據(jù)
-(NSMutableData *)RequestSpliceAttribute:(id)obj{
_data = nil;//記得清空不然數(shù)據(jù)包會越來越大
if (obj == nil) {
self.object = self.data;
NSLog(@"傳入需轉(zhuǎn)二進制的數(shù)據(jù)為空");
return nil;
}
unsigned int numIvars; //成員變量個數(shù)
objc_property_t *propertys = class_copyPropertyList(NSClassFromString([NSString stringWithUTF8String:object_getClassName(obj)]), &numIvars);
NSString *type = nil;
NSString *name = nil;
for (int i = 0; i < numIvars; i++) {
objc_property_t thisProperty = propertys[i];
name = [NSString stringWithUTF8String:property_getName(thisProperty)];
// NSLog(@"%d.name:%@",i,name);
type = [[[NSString stringWithUTF8String:property_getAttributes(thisProperty)] componentsSeparatedByString:@","] objectAtIndex:0]; //獲取成員變量的數(shù)據(jù)類型
// NSLog(@"%d.type:%@",i,type);
id propertyValue = [obj valueForKey:[(NSString *)name substringFromIndex:0]];
// NSLog(@"%d.propertyValue:%@",i,propertyValue);
if ([type isEqualToString:TYPE_UINT8]) {
uint8_t i = [propertyValue charValue];// 8位
[self.data appendData:[DLSocketDataUtils byteFromUInt8:i]];
}else if([type isEqualToString:TYPE_UINT16]){
uint16_t i = [propertyValue shortValue];// 16位
[self.data appendData:[DLSocketDataUtils bytesFromUInt16:i]];
}else if([type isEqualToString:TYPE_UINT32]){
uint32_t i = [propertyValue intValue];// 32位
[self.data appendData:[DLSocketDataUtils bytesFromUInt32:i]];
}else if([type isEqualToString:TYPE_UINT64]){
uint64_t i = [propertyValue longLongValue];// 64位
[self.data appendData:[DLSocketDataUtils bytesFromUInt64:i]];
}else if([type isEqualToString:TYPE_STRING]){
NSData *data = [(NSString*)propertyValue \
dataUsingEncoding:NSUTF8StringEncoding];// 通過utf-8轉(zhuǎn)為data
[self.data appendData:data];
}else {
NSLog(@"RequestSpliceAttribute:未知類型");
NSAssert(YES, @"RequestSpliceAttribute:未知類型");
}
}
// hy: 記得釋放C語言的結(jié)構(gòu)體指針
free(propertys);
self.object = _data;
return _data;
}
- 解釋:其中作者有一段 為什么不需要調(diào)整順序我也不知道许赃,有興趣的的同學(xué)自己去研究下這個方法這個寫的那個人不明白 止喷,其實很簡單,是不是運行時Cs_Connect這個模型的順序是一定的 所以順序不會錯對吧 很簡單的混聊。
6 .這是我封裝消息轉(zhuǎn)發(fā)的工具類
LDGMessageTransformTool.h 文件
//
// LDGMessageTransformTool.h
// ZhiBo
//
// Created by apple on 2018/5/23.
// Copyright ? 2018年 apple. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "GCDAsyncSocket.h"
typedef NS_ENUM(NSUInteger,MessageType){
MessageTypeJoinRoom = 0,
MessageTypeLeaveRoom = 1,
MessageTypeText = 2,
MessageTypeGift = 3,
MessageTypeHeart = 100
};
#define TIME_OUT 20
#define HEART_TIME 10
@protocol LDGMessageTransformToolDelegate<NSObject>
/**
已經(jīng)接收到進入到 房間消息了
@param userI userI
*/
- (void)haveJoinRoom:(UserInfo *)userI;
/**
已經(jīng)接收到推出到 房間消息了
@param userI userI
*/
- (void)haveLeaveRoom:(UserInfo *)userI;
/**
已經(jīng)接收到收到禮物的消息了
@param giftM giftM
*/
- (void)haveAcceptGiftMessage:(GiftMessage *)giftM;
/**
已經(jīng)接受到文本消息了弹谁。大師兄
@param textM 文本消息的model
*/
- (void)haveAcceptTextMessage:(TextMessage *)textM;
@end
@interface LDGMessageTransformTool : NSObject
@property (weak, nonatomic) id<LDGMessageTransformToolDelegate> toolDelegate;
/**
進入房間的消息是 : 0
離開房間的消息是 : 1
發(fā)送文本消息是 : 2
發(fā)送禮物消息是 : 3
*/
/**
鏈接上服務(wù)器
*/
- (instancetype)initWithConnectServer;
/**
進入房間
*/
- (void)joinRoom;
/**
離開房間
*/
- (void)leaveRoom;
/**
發(fā)送文本消息
@param text text
*/
- (void)sendTextMessage:(NSString *)text;
/**
發(fā)送禮物的消息
@param imageUrl 圖片的url
@param giftName 圖片的名字
@param giftCount 禮物的數(shù)量
*/
- (void)sendGiftMessage:(NSString *)imageUrl giftName:(NSString *)giftName giftCount:(NSString *)giftCount;
@end
LDGMessageTransformTool.m 文件
//
// LDGMessageTransformTool.m
// ZhiBo
//
// Created by apple on 2018/5/23.
// Copyright ? 2018年 apple. All rights reserved.
//
#import "LDGMessageTransformTool.h"
@interface LDGMessageTransformTool ()<GCDAsyncSocketDelegate>
@property (strong, nonatomic) GCDAsyncSocket *socket;
@property (strong, nonatomic) UserInfo *userInfo;
@property (strong, nonatomic) NSTimer *heartTimer;
@property (strong, nonatomic) NSMutableData *cacheParseData;
@end
@implementation LDGMessageTransformTool
-(NSMutableData *)cacheParseData {
if (!_cacheParseData) {
_cacheParseData = [[NSMutableData alloc] init];
}
return _cacheParseData;
}
/**
鏈接上服務(wù)器,創(chuàng)建
*/
- (instancetype)initWithConnectServer{
if (self = [super init]) {
UserInfo *userInfo = [[UserInfo alloc] init];
userInfo.name = @"liudiange";
userInfo.level = 1;
self.userInfo = userInfo;
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
[self.socket connectToHost:@"192.168.100.193" onPort:7878 error:nil];
}
return self;
}
#pragma mark - 發(fā)送消息的方法
/**
進入房間的消息是 : 0
離開房間的消息是 : 1
發(fā)送文本消息是 : 2
發(fā)送禮物消息是 : 3
*/
/**
進入房間
*/
- (void)joinRoom{
NSMutableData *needSendData = [self calculatorData:MessageTypeJoinRoom bodyData:self.userInfo.data maxM:2];
// 沒有超時時間 -1 代表沒有超時時間
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeJoinRoom];
}
/**
離開房間
*/
- (void)leaveRoom {
NSMutableData *needSendData = [self calculatorData:MessageTypeLeaveRoom bodyData:self.userInfo.data maxM:2];
// 沒有超時時間 -1 代表沒有超時時間
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeLeaveRoom];
}
/**
發(fā)送文本消息
@param text text
*/
- (void)sendTextMessage:(NSString *)text{
TextMessage *textM = [[TextMessage alloc] init];
textM.user = self.userInfo;
textM.text = text;
NSMutableData *needSendData = [self calculatorData:MessageTypeText bodyData:textM.data maxM:2];
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeText];
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// [self parseData:needSendData socket:self.socket];
// });
}
/**
發(fā)送禮物的消息
@param imageUrl 圖片的url
@param giftName 圖片的名字
@param giftCount 禮物的數(shù)量
*/
- (void)sendGiftMessage:(NSString *)imageUrl giftName:(NSString *)giftName giftCount:(NSString *)giftCount{
GiftMessage *giftM = [[GiftMessage alloc] init];
giftM.user = self.userInfo;
giftM.giftname = giftName;
giftM.giftURL = imageUrl;
giftM.giftCount = giftCount;
NSMutableData *needSendData = [self calculatorData:MessageTypeGift bodyData:giftM.data maxM:2];
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeGift];
}
/**
這個方法是封包 組裝數(shù)據(jù)的格式是。type + length + bodyData
@param type 類型
@param bodyData 要發(fā)送的數(shù)據(jù)
@param maxM 最大的發(fā)送數(shù)據(jù)M數(shù)量
@return 整理好的data
*/
- (NSMutableData *)calculatorData:(MessageType)type bodyData:(NSData *)bodyData maxM:(int)maxM{
// 需要返回的data
NSMutableData *needSendData = [[NSMutableData alloc] init];
// 類型的data 固定2個字節(jié)
int typeInt = (int)type;
NSMutableData *typeData = [NSMutableData dataWithBytes:&typeInt length:2];
// 發(fā)送數(shù)據(jù)的長度data 也是固定4個字節(jié)
int lenth = (int)bodyData.length;
NSMutableData *legthData = [NSMutableData dataWithBytes:&lenth length:4];
// 進行拼接,注意順序不能錯
[needSendData appendData:legthData];
[needSendData appendData:typeData];
[needSendData appendData:bodyData];
return needSendData;
}
/**
心跳包的事件 為了保持客戶端和服務(wù)端長期的鏈接
*/
-(void)heartAction{
NSData *heartData = [@"my heart is very bad" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *needSendData = [self calculatorData:MessageTypeHeart bodyData:heartData maxM:2];
[self.socket writeData:needSendData withTimeout:TIME_OUT tag:MessageTypeHeart];
}
#pragma mark - 接受到消息的方法
/**
* Called when a socket has completed writing the requested data. Not called if there is an error.
**/
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
[sock readDataWithTimeout:TIME_OUT tag:tag];
}
/**
* Called when a socket connects and is ready for reading and writing.
* The host parameter will be an IP address, not a DNS name.
**/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
LDGLog(@"說明已經(jīng)鏈接成功了");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (!self.heartTimer) {
self.heartTimer = [NSTimer timerWithTimeInterval:HEART_TIME target:self selector:@selector(heartAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.heartTimer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
});
[sock readDataWithTimeout:TIME_OUT tag:0];
}
/**
* Called when a socket has completed reading the requested data into memory.
* Not called if there is an error.
**/
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
[self parseData:data socket:sock];
}
/**
已經(jīng)斷開鏈接了 被動斷開鏈接
@param sock socket
@param err 錯誤信息
*/
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err{
if (self.heartTimer) {
[self.heartTimer invalidate];
self.heartTimer = nil;
}
}
/**
拆除包 防止沾包
@param data data
@param sock sock
*/
- (void)parseData:(NSData *)data socket:(GCDAsyncSocket *)sock{
// 首先付給要處理的data
[self.cacheParseData appendData:data];
// 找到我們當初存儲的長度
NSData *lengthData = [self.cacheParseData subdataWithRange:NSMakeRange(0, 4)];
int shouldLength = 0;
[lengthData getBytes:&shouldLength length:4];
shouldLength += 6;
while (self.cacheParseData.length > 6) {
if (shouldLength > self.cacheParseData.length) { // 說明這個包是不完整的
[sock readDataWithTimeout:TIME_OUT tag:0];
break;
}else{ // 說明這個包至少是大于等于一個完整包的長度
NSData *needParseData = [self.cacheParseData subdataWithRange:NSMakeRange(0, shouldLength)];
// 在這里開始正式解決這個包
[self parseData:needParseData dataLength:shouldLength - 6];
[self.cacheParseData replaceBytesInRange:NSMakeRange(0, shouldLength) withBytes:nil length:0];
[sock readDataWithTimeout:TIME_OUT tag:0];
}
}
}
/**
進入到這個方法說明是一個完整的包预愤,并且是能夠解析的
@param data data
@param shouldHaveLength 消息的長度
*/
- (void)parseData:(NSData *)data dataLength:(int)shouldHaveLength{
// 類型
NSData *typeData = [data subdataWithRange:NSMakeRange(4, 2)];
int type = 0;
[typeData getBytes:&type length:2];
// 消息體
NSData *bodyData = [data subdataWithRange:NSMakeRange(6, shouldHaveLength)];
switch (type) {
case MessageTypeHeart:
{
LDGLog(@"心跳包的消息");
}
break;
case MessageTypeJoinRoom:
{
UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveJoinRoom:)]) {
[self.toolDelegate haveJoinRoom:userI];
}
}
break;
case MessageTypeLeaveRoom:
{
UserInfo *userI = [UserInfo parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveLeaveRoom:)]) {
[self.toolDelegate haveLeaveRoom:userI];
}
}
break;
case MessageTypeText:
{
TextMessage *textM = [TextMessage parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveAcceptTextMessage:)]) {
[self.toolDelegate haveAcceptTextMessage:textM];
}
}
break;
case MessageTypeGift:
{
GiftMessage *giftM = [GiftMessage parseFromData:bodyData error:nil];
if ([self.toolDelegate respondsToSelector:@selector(haveAcceptGiftMessage:)]) {
[self.toolDelegate haveAcceptGiftMessage:giftM];
}
}
break;
default:
{
LDGLog(@"不知道是啥子消息");
}
break;
}
}
@end
- 最后我目前總結(jié)大概就是這么多沟于,其實到公司用到應(yīng)該比這還多,那時候我們只能現(xiàn)場發(fā)揮了見招拆招了植康,我寫到這的時候真的很興奮旷太,因為研究出來有成果我們正常人都會感到高興,還有我以后會更加深入的研究socket 销睁,我們作為一個程序員不能滿足現(xiàn)狀供璧,因為知識是無止境的。
- 最后為我寫的一個下載的框架做一個小小的宣傳冻记,大家覺得我寫的還行的話給個星睡毒。
https://github.com/liudiange/DGDownloadManager