老规矩,先看效果:
Demo奉上:
socket服务端
socket客户端
一.写作目的:
1.使用socket实现简单的群聊
2.利用TCP建立的连接,模拟苹果远程推送
二.写作声明:
socket是C语言写的,所以不必担心平台的问题,本文主要使用语言---OC,介绍一个非常好用的库CocoaAsyncSocket,是谷歌的开发者,基于BSD-Socket写的一个IM框架,它给Mac和iOS提供了易于使用的、强大的异步套接字库,向上封装出简单易用OC接口。省去了我们面向Socket以及数据流Stream等繁琐复杂的编程。整个库其实就包含两个类,一个是基于TCP,一个是基于UDP的,其中基于TCP是GCDAsyncSocket,本文主要介绍就是他的用法。那既然说到socket了,那就先聊聊这玩意到底是个啥。首先声明,本人是计算机科学与技术出身,也不是正宗的网络专业,虽然大学时开过计算机网络课程(但是大家懂得,大学嘛。。。),所以对这个也只是知道点皮毛,够用就行。
CocoaAsyncSocket安装:
1.使用cocoapods安装
在podfile文件加上 pod 'CocoaAsyncSocket' 然后执行pod install
2.直接拖入CocoaAsyncSocket文件夹
从CocoaAsyncSocket网站上下载,然后拖入工程即可
三.言归正传:
1.socket
(1)socket又称套接字(一般不这么说,太别扭,直接叫它英文名就好)。
(2)网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接
的一端称为一个socket。
(3)应用程序通过socket向网络发出请求或者应答请求。
ps:提到网络通讯,那些基础的概念肯定都会涉及到,这里只讲一下什么是socket,至于什么TCP/UDP等等,网上一大堆,大神们讲的比我好多了,不清楚的可以搜一下,我就不做班门弄斧了(言多必失啊)。
2.socket服务端实现:
整个网络通讯过程服务端要做的事大概分为以下几点:
(1)创建一个服务端socket对象。
//既然是网络程序,那肯定要在子线程咯
dispatch_queue_t queue=dispatch_get_global_queue(0, 0);
GCDAsyncSocket *serverSocket=[[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:(queue)];
(2)绑定服务器的端口,并开始监听,此时服务器已经开启(端口号是表示应用程序的逻辑地址,每个应用程序会有一个唯一的端口号,范围是065559,其中01024被系统占用,开发当中建议使用1024以上的端口)。
NSError *error=nil;
[serverSocket acceptOnPort:serverPort error:&error];
if (!error) {
NSLog(@"服务器开启成功");
}
else{
//失败的原因可能是端口号被其他程序占用,或者计算机内存不足等
NSLog(@"服务器开启失败---%@",error);
}
(3)其实到此为止服务器已经启动了,并且在监听客户端连接,就是这么简单啊。那怎么知道有客户端连接或者断开连接?怎么知道客户端发来的消息呢?大家有没有注意到在创建服务端socket的时候设置了一个代理呢!这些都是通过它的代理方法知道的。
客户端的socket连接到服务器
#pragma mark ----------------当有客户端的socket连接到服务器---------------
-(void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
//服务端需要持有一个数组,将新连接上的客户端socket保存在数组里面,否则新连上的客户端一连上就会被立马释放,从而断开连接
[self.clientArray addObject:newSocket];
//监听客户端有没有上传-1表示不超时 0表示客户端的标识符
[newSocket readDataWithTimeout:-1 tag:0];
NSLog(@"%@已连接,其IP地址:%@,端口号:%d",newSocket,newSocket.connectedHost,newSocket.connectedPort);
NSLog(@"当前共有%ld客户连接服务器",self.clientArray.count);
//如果想要在客户端一连上就返回一条消息在这里写
NSString *answer=[NSString stringWithFormat:@"欢迎来到人工服务:\n输入数字1:普通服务\n输入数字2:特殊服务\n输入数字3:退出服务\n"];
NSData *answerData=[answer dataUsingEncoding:NSUTF8StringEncoding];
//处理请求返回数据给客户端
[newSocket writeData:answerData withTimeout:-1 tag:0];
}
客户端的socket断开连接
#pragma mark ----------------当有客户端的socket断开连接---------------
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
NSLog(@"%@断开连接,其IP地址:%@,端口号:%d",sock,sock.connectedHost,sock.connectedPort);
//断开连接的客户端需要将其从数组中移除
[self.clientArray removeObject:sock];
}
接收到有客户端发来消息
#pragma mark ----------------当接收到有客户端发来消息---------------
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSString *receive=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSData *answerData=[receive dataUsingEncoding:NSUTF8StringEncoding];
//把当前客户端发送的数据转发给其他客户端,从而实现群聊功能
for (GCDAsyncSocket *socket in self.clientArray) {
if (socket!=sock) {
[socket writeData:answerData withTimeout:-1 tag:0];
}
}
#warning 每次读完数据后都要调用一次监听数据的方法,否则读取不到客户端下一次的数据,这是监听机制问题
[sock readDataWithTimeout:-1 tag:0];
//给客户端写一个退出登录的选项只需将该客户端从数组中移除即可
if ([receive isEqualToString:@"quit"]) {
//移除客户端
[self.clientArray removeObject:sock];
}
}
服务端向所有客户端发送消息
//服务端向所有客户端发送消息,如果想向指定客户端发送可以用tag区别
- (void)senMessageToAllClient:(NSString *)message{
NSData *answerData=[message dataUsingEncoding:NSUTF8StringEncoding];
//处理请求返回数据给客户端
for (GCDAsyncSocket *socket in self.clientArray) {
[socket writeData:answerData withTimeout:-1 tag:0];
}
}
OK,到此为止,服务端的代码差不多搞定了,so easy!有没有。下面看看客户端的实现。。。
socket客户端实现:
(1)创建一个客户端scoket对象。
//这段跟创建服务端的一样,因为用的是同一个类啊,对象属性一样
dispatch_queue_t queue=dispatch_get_global_queue(0, 0);
GCDAsyncSocket *serverSocket=[[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:(queue)];
(2)客户端socket发送连接请求,必须知道服务端的IP地址和端口号,才能准确的连接到相应的服务器。这里只发送连接请求,成功与否在代理方法里面看到。
NSError *error=nil;
[clientScoket connectToHost:serverIp onPort:serverPort error:&error];
if (!error) {
NSLog(@"error--%@",error);
}
(3)客户端实现代理方法
客户端socket成功连接到服务器
#pragma mark ----------------客户端socket成功连接到服务器---------------
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
NSLog(@"与服务器连接成功");
//连接成功后开始监听服务端发来的消息
[sock readDataWithTimeout:-1 tag:0];
}
客户端socket与服务器断开连接
#pragma mark ----------------客户端socket与服务器断开连接---------------
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
NSLog(@"与服务器断开连接");
//如果是由于网络原因非主动断开连接的可以尝试重新连接
[self reConnect:sock];
}
//重连机制,每个一段时间尝试连接一次
- (void)reConnect:(GCDAsyncSocket *)sock{
if (self.isConnectting == YES) {
return;
}
self.isConnectting = YES;
//开启定时任务,每隔段时间尝试重新连接一次
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(3.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"正在努力尝试重新链接。。。");
//重新连接
NSError *error=nil;
[sock connectToHost:serverIp onPort:serverPort error:&error];
if (!error) {
NSLog(@"error--%@",error);
}else{
NSLog(@"重连成功");
// 取消定时器
dispatch_cancel(self.timer);
self.timer = nil;
self.isConnectting = NO;
}
});
// 启动定时器
dispatch_resume(self.timer);
}
客户端接收到服务器发来的消息
#pragma mark ----------------客户端接收到服务器发来的消息---------------
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSString *receive=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
//此处进行UI处理,比如将获取到的数据显示到页面上,注意这里是在子线程,进行UI操作需回到主线程刷新
if (receive) {
MessageModel *model=[[MessageModel alloc]init];
model.text=receive;
model.type=@"other";
model.time=[NSDate date];
[self.dataArray insertObject:model atIndex:0];
//回到主线程刷新表格
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
//发送本地通知,接收到服务端消息
[self sendNotiWithModel:model];
[self.tableView reloadData];
}];
}
//与服务端代码一样,读完一次数据后需继续调用一次监听方法监听读取数据
[sock readDataWithTimeout:-1 tag:0];
}
-(void)sendNotiWithModel:(MessageModel *)model{
//发送本地通知
UILocalNotification *localNoti=[[UILocalNotification alloc]init];
localNoti.alertBody=model.text;
localNoti.soundName=UILocalNotificationDefaultSoundName;
localNoti.fireDate=[NSDate dateWithTimeIntervalSinceNow:1.0];
[[UIApplication sharedApplication]scheduleLocalNotification:localNoti];
}
PS:OK,至此一个简单的群聊功能实现完成,客户端剩下的代码都是一些UI处理,至于文章前面提到的模拟远程推送功能其实是客户端接收到服务端消息后发送的一个本地通知,这样做有一个弊端,就是想收到推送必须是在app活跃的前提下才可以,所以当app处于杀死情况还是老老实实的走苹果的apns吧,哈哈。iOS目前还不知道有什么办法在app杀死的情况能绕过apns与客户端通信,有知道的大佬麻烦私下我哈,感激不尽!完整的demo我会传到github上,有什么问题欢迎评论哦!
番外篇
介绍一下CocoaAsyncSocket这个库,这个库包只包含两个类文件,GCDAsyncSocket和GCDAsyncUdpSocket,前者基于TCP后者基于UDP,服务端代码和客户端代码都在里面。
TCP
GCDAsyncSocket 是一个 tcp/ip套接字网络库,大概8000多行代码,在大型中央调度中构建。 以下是可用的主要功能:
- 本机 objective-c,完全自包含在一个类中。
不需要muck套接字或者流。 此类为你处理所有内容。 - 完全委托支持
错误。连接。读完成。写入完成。进程和断开所有导致对委托方法的调用。 - 排队非阻塞读取和写入,带有可选超时。
你告诉它读或者写什么,它处理你的一切。 队列中的排队。缓冲和搜索终止序列- 所有操作都自动处理。 - 自动套接字接受。
启动服务器套接字,告诉它接受连接,它会为每个连接调用自己的新实例。 - 对IPv4和IPv6上的TCP流的支持。
自动连接到IPv4或者IPv6主机。 通过此类的单个实例自动接受IPv4和IPv6传入的连接。 不再担心多个套接字。 - 支持 tls/ssl
使用单一方法调用轻松地保护你的套接字。 客户端和服务器套接字均可用。 - 完全基于服务器和线程安全
然而,它完全运行在自己的GCD中,而且完全是线程安全的。 而且,委托方法都是异步调用到你所选择的dispatch_queue。 这意味着套接字代码的并行操作,以及委托/处理代码。 - 最新技术&性能优化
库内部利用了诸如 这样的技术来限制系统调用,并优化缓冲分配。 换句话说,峰值性能。
UDP
GCDAsyncUdpSocket 是在大型中央调度中构建的udp/ip套接字网络库。 以下是可用的主要功能:
- 本机 objective-c,完全自包含在一个类中。
不需要在低级别套接字周围进行垃圾处理。 此类为你处理所有内容。 - 完全委托支持。
错误,发送完成,接收完成和中断所有导致对委托方法的调用。 - 排队非阻塞发送和接收操作,具有可选超时。
你告诉它发送或者接收什么,它会处理你的一切。 排队。缓冲。等待和检查 errno - 所有操作都自动处理。 - 支持IPv4和 IPv6.
使用IPv4和/或者IPv6自动发送/接收。 不再担心多个套接字。 - 完全基于服务器和线程安全
然而,它完全运行在自己的GCD中,而且完全是线程安全的。 而且,委托方法都是异步调用到你所选择的dispatch_queue。 这意味着套接字代码的并行操作,以及委托/处理代码。
作者简介: 就职于甜橙金融信息技术部,负责iOS前端开发工作。对于业内的新技术比较感兴趣,在我看来,新的东西必然是在旧的基础上优化而来,这对我们提高开发效率很有帮助。
参考文献:CocoaAsyncSocket, 用于Mac和iOS的异步套接字网络库
如需转载,请注明出处,谢谢~~~