[转]GCDAsyncSocket类库,IOS下TCP通讯使用心
关于在IOS下使用Socket进行通讯的技术文章也许诺很久了,今日又是一个还债的日子,网上虽然很多介绍过AsyncSocket
或GCDAsyncSocket
的文章,但其实就那么一两篇大部分都是转载,于是我义正言辞、慷慨激昂的批判他们这种不负责任的态度,学习,不是给自己学的,是要和大家分享的。技术的共享有利于整体行业的进步,也可以使自身更深入全面的了解。
之前的文章中我们讲到过TCP通讯协议,并且也对其进行了较为详细的介绍和描述,关于TCP通讯的原理此处我们不再赘述,如有需要的看官可自行翻阅本人所写的《IOS、安卓IM语音聊天开发初探部分心得——网络基础篇》一文。
正如名称一样GCDAsyncSocket
开源类库是以苹果的GCD多任务处理机制完成的一个异步交互套接字通讯。使用方法其实并不复杂,主要说的是在使用这个类库的时候我的一些心得和理解,若有不妥之处望看官指点。首先,每一个** GCDAsyncSocket
对象(以下简称GCDSocket
**对象)都可以理解为一个socket套接字,我们的操作都是针对于这个socket执行的各种命令,可以打开一个端口侦听,同样也可以连接其他计算机的端口进行数据通讯等等等等。
首先我们来创建一个socket。当然这之前先要将CGDAsyncSocket
的.h文件及.m文件加入到我们的项目,并且在需要使用socket连接的地方将.h头文件包含,这些废话我觉得不需要复述了应该(那你还嘚吧嘚的说半天干嘛啊喂!)。具体代码如下
GCDAsyncSocket socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
代码并不复杂,我们只需要给出一个委托对象也就是第一个参数中的self,以及一个委托运行的GCD队列即可创建一个GCDAsyncSocket,当前代码中我们是使用静态全局函数取得的主消息队列。当然也可以使用其他方法获得其他的GCD队列,比如:dispatch_get_global_queue()
创建了Socket对象我们即可以立即为,当前我们的socket已经进入程序以供操作。但如果你想和服务器进行通讯,那么我们还需要和服务器进行连接。可能有的使用习惯了http协议的人会问,初始化函数中我们为何不直接指定服务器以及端口号?
其实这些肯定都是需要的,但是你要理解到,你的socket对象功能不只是可以用来连接服务器,换而言之我们的socket对象一样可以侦听某端口来等待他人连接,所以在通过套接字编程使用TCP协议的时候是我们从http协议过度到TCP协议的一个转变(虽然本文并不会教你如何在IOS上构架服务器。),但并不是第一个,第一个转变是要记得,我们要使用的是协议,并非某个类,所以我上述说明中都是说从http协议过度到TCP而不是跟大家说现在我们将从NSURLRequest
和NSURLConnection
过度到GCDAsyncSocket
。
好了接下来我们看看如何连接服务器。源代码如下:
NSError *err;
[socket connectToHost:@“192.168.10.111” onPort@”60000″ error:&err];
if (err != nil)
{
NSLog(@”%@”,err);
}
代码比前面稍微长了一点,不过实质上完全不复杂,我们只是先声明了一个错误信息的指针,然后使用之前创建的对象调用他的连接方法,第一个参数不难看出是一个IP,第二个参数则是一个端口,如果这里还不理解何为IP和端口的话,就先去看看在开头就提到的我之前写过的那边网络基础篇文章吧…最后一个是出参,如果连接的过程中出现了错误,该方法会把这根指针指向一个具体的错误信息,最后我们再判断一下之前我们创建错误信息的指针是否还是指向空,如果并非指向空那么代表我们连接的过程中出现了错误,将错误信息打印一下吧~不过请切记,此处的错误信息并非你创建连接时所有的错误都会在此处得到反映。
说到这里我们该说一点真正有用的了,GCDAsyncSocket
具有一系列完整的委托机制,我们所做的一切处理基本都是异步处理的状态,换句话说,连接之后是否连接成功,连接成功要执行什么懂并非应该写在此处而应该写在相应的委托之中,同样的道理一样适用于发送、读取数据等等。也就是说我们在此处读出的错误只是同步执行的代码处理一些连接时会发生的错误,而更多的处理我们应在相对应的委托中进行处理。首先请看下面这个方法:
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
这个方法就是在成功连接服务器之后的委托方法。关于委托该如何使用我在此处就不赘述了和本文的关系实在不大,不过给诸位看管一个建议,也是我才刚刚纠正的一个编码错误习惯,之前碰到所有委托的地方我都会将直接将当前的类对象设置成委托处理对象,并且遵循委托协议扩充代码,这么做的坏处显而易见,显示层与逻辑层的混淆是一方面,另一方面是一旦需要使用过多的委托,将造成大量不必要的代码都堆积在一个类中,并且我们很容易直接在委托方法中直接使用一些类内成员属性或者甚至是私有成员,而实际上这种做法是很不好的,因为这种最发会使得逻辑出现混乱,处理委托应当是单独处于后台的逻辑,如果需要一些必要的数据传递也应该采取属性侦听、甚至是通知等方式来实现而并非直接在显示层中编写逻辑代码来实现。使得代码耦合性大增的同时也使得很多时候在切换操作对象时对委托对象的处理变得复杂,甚至可能完全相同的代码要难免的复制粘贴。
所以我给大家的建议是单独编写一个委托类,在每个类中设置一个该类类型的成员指针,将委托设置到专门的委托对象上去处理,这样不仅效率更高,代码可读性更强,更便于维护,同时也更符合面向对象的编程思想。
回到对GCDAsyncSocket
使用的讲解上来,在这个委托方法中,我们可以取到一个socket对象一个服务器IP和一个端口号,你可以处理一切在连接建立之后应该马上执行的事情,比如与服务器进行通信确认连接端以免出现其他人通过IP及端口随意的和你的服务器通信,再比如开启心跳包的发送,让服务器一直可以确认你的存在。不管做什么,都是你和服务器的编写者事前约定好的,就像数据传输格式什么的,如果没有当面约定我坚信他也一定要给你出个文档什么的,否则你的工作接下来将举步维艰。但是不管你要在此处都做什么工作,都要处理哪些事宜,请务必记得,在此处你必须要在函数的最后加上一句:
[socket readDataWithTimeout:-1 tag:0];
这是什么?别慌,按照你看到这个函数的第一反应取理解,没错他就是读取数据的方法,两个参数也略显简单,一个超时时间,如果你设置成-1则认为永不超时,而第二参数则是区别该次读取与其他读取的标志,通常我们在设计视图上的控件时也会有这样的一个属性就是tag。如果你做过web开发,那你应该知道Http标签上的id,如果你做过一些桌面级开发,你的控件或许有个id或者是index再或者是tag的属性来区别这些控件,没错此tag和彼tag功效基本一样。
我们可以这样理解,socket在开启之前是一个巨大门,开启这道门之后(也就是连接之后)就是一个宽敞的通道,通过这条通道所达到的地点就是我们连接的目标服务器,或者是连接过来的客户端,两面都是一样的。我们现在不论是发送数据还是读取数据都是往返于这个大门之中的一个个门卫与邮递员,我们可以把读取数据的方法看作是门卫,而发送数据的人看做是邮递员,没错服务器与客户端都一样,我们都会派出一个个邮递员去我们连接的另一端送信,但是如果你没有命令你的门卫去吧门口邮箱中的信拿过来,那么你的邮递员就会假装看不见邮递员,然后呼呼睡大觉,好吧看起来这些门卫实在没什么责任心不是么,其实他们也是有苦衷的,因为这是最初设计者给他们的命令,不接到命令绝对不要出门,万一收到的是金刚葫芦娃高清全集的种子怎么办!好的就这样,为了避免我们的邮件不被错过,所以建立连接之后就让一个门卫跑去门口等着吧~慢着,万一我需要派出很多个门卫我分不清他们该怎么办,其实他们已经被你分配了工号,这个工号就是tag。
现在我们的连接动作算是完整的做完了,接下来我们要做的就只有两件事,第一个在需要发送数据的时候派出邮递员,以及当门卫接到消息的时候在我们的手机端上根据门卫的消息做出反应。等等,好像少了点什么,没错 少点委托,我们来看一下读取和写入的委托,读取的委托即是门卫接到信息的报告,写入的委托就是邮递员将邮件送完的回复:
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
好的让我们来看看这些委托中我们都能得到什么,首先是读取的委托,是一个socket对象,一个读取到的数据以及一个“门卫的工号”,嗯,大概也就这些,我们还能要什么的,没错这些足够了,别抱怨第二个参数的数据类型,要知道其实最开始接到数据的时候只是字节数组啊,已经给你转换成NSData对象了你就要学会感恩啊,谁让你要用套接字传输了,这就是活该的,所以改怎么读取转换解析这些数据你需要好好的和服务器编写者沟通。除此之外你还要详细的了解如何将NSData转换成各种各样的数据或者文件如果你还不知道该怎么做我这里实在帮不了你,因为我总不能吧多如牛毛的情况都列举在这一篇文章中吧,要知道我每篇文章的篇幅都够长了。。。不过也别因此而气馁百度和谷歌肯定可以帮到你~
接下来我们再看看“邮递员的委托”,嗯一个socket的对象,一个tag嗯,没错,哎哎,慢着,好像哪里不太对啊,我来看看,哪里不对呢,哦对了!发送的数据呢!怎么没有!哎也不对。。。明明是我自己发的数据我还要来干嘛,有了工号我不就知道发送的是什么了么。那是哪里不太对呢。。。哦!是名字!我们的数据传输来说接受可以是读取read而发送通常我们应该写成Send一类的单词,为何这里是Write?写入?没错就是写入,向TCP的通讯流之中写入数据。
TCP通讯协议是一个基于字节流的运输层通信协议,其数据传输的形式也是以流的形式提现,而我感觉在使用GCDAsyncSocket
的过程中我们可以很好的体会到流的概念,首先来说为什么这种TCP的这种传输形式要叫流而不像UDP中的那样叫做包?流之中又写入和读出的概念,我们可以把整个TCP通讯的连接看作为一条无水的河流,当然因为他没水所以你可以称它为沟,而向其写入数据即是向河流注入水,被写入的数据会向水一样流向连接的另一端。读即是从河流中取水,只要读得动作在继续,并且河流之中有水,那么我们就可以不停的取到数据,不论是河流之中有水你确没有去读亦或者是你去读了而河流之中没有水都会引发看起来完全相同的反应就是没有数据返回,所以在很多时候我们要处理更多的关于接收数据的逻辑的处理。正如我们目前使用的方法就是一种比较粗暴有效的方法——一旦开启连接读取的动作就永不停歇。
接下来我们还要记住使用TCP流式传输数据时的一个关键性问题,数据是不会自己分段的。没错,就如一次次倒入河流中的水一样,数据也同样会向水一样融合为一个整体,换句话说,数据在TCP中传输本身是没有起始或结尾之分,如果我先向数据流中写入两个人的聊天记录,第一句是“你好”,对方回复了一句“不好”,结果发到了服务器,服务器读取出的信息是“你好不好”,同样类似的情况会发生很多,比我举出的这个例子要常见的多比如我先发了一段音频,又发了一段图片,又发了一段文字,最后服务器接收到了一个带语音和字母的静态图片。实际情况上比我说的要遭的多,因为由于字节之间并没有边界,所以字符、文字、音频,我们根本无法确定他们各有多长,胡乱截取,只会导致无法编码解析成图片、文字及音频,所以如何界定数据之间的边界是你开始使用TCP协议之后又一个问题。你可以使用一个固定的字节数组组合来区分开头以及结尾,也可以将所有的字符串都添加一个特殊的界定字符来区分不同的命令与操作。
如果看到这里的看官有心使用GCDAsyncSocket
去编写了一个服务器端,并且使用它来接受客户端的数据,比如传输了一些音频,图片等从字节单位看来将会不小的长串数据的话就会发现,服务器端接到的程序是一段一段的,没错,但我没有欺骗你,TCP协议并不会区分你发送数据的头尾,被划分为段知识GCDAsyncSocket
为了保证在并不通常的移动互联网之中一样可以安全的传输数据,于是将你所有写入到流的数据都一分割为一段一段的内容,所以请正确理解我在上一段开头所说的“数据是不会自己分段的。”这句话,不要较真哦亲~
写到这里,GCDAsyncSocket
的基本操作及其核心思想就全部写完了,对于思想部分皆为笔者本人个人理解,若有缺少或意见不同之处,欢迎交换意见相互学习,感谢您的阅读。