想做长连接,要知道几个基本问题:
1.运营商网络是否稳定,他们的ip是否变化。大家知道一切TCP/IP协议都要涉及到IP地址。一个移动设备在连网使用时,运营商会给你分配一个动态IP。若运营商发现你一段时间不使用就回收你的动态IP,分配给其他人,毕竟运营商的动态IP是有限的。每个省份的运营商给你的这段时间都不相同,有的是15分钟,有的是30分钟,甚至更久。所以你的长连接要保证这个间隔至少发送一次报文数据。
解决办法:我做push service时设置的是9分钟通过socket长连接发送一次报文。可以看到它很稳定,并且很省流量,很省电。它大部分处于select函数的侦听网络数据阶段。
2.遇到通而不达的受限网络,如何处理?有的网络是自动连接成功wifi网络,但是你需要输入用户/密码等信息才能使用。不然你只能发送数据,但是收不到数据。对于这样的网络,你的长连接很可能一直连接一直快速失败,导致程序跑飞。
解决办法:每一秒重连一次,连续连接失败9次,第十次暂停一分钟重新连接。
3.僵尸网络。当手机设备在车上进入网络盲区(山脚下,隧道中)或在各个蜂窝通信网络网络基站的边界快速通过。你的手机可能无法及时的连接到实时的可用网络或没有网络。如你从隧道出来,你发现手机上显示2G网络,上网很慢,甚至上不了网。你可以通过硬件开关网络,实时获取网络,来实时连接网络。在开车时,你不可能人工实时识别这种异常并手动操作,太不安全。这种手机网络都不正常了,socket当然也不正常了,这种情况socket的select可能也不能实时侦别出这种网络异常。打车软件是对网络和gps都严重依赖的应用。CocoaAsyncSocket采用的是起一个定时器来发送心跳消息的。
解决方案:我们每7秒向服务器通过socket发送一次nop报文或位置信息报文(长连接内部计算心跳发送时机,若刚发送消息就把心跳消息的发送时间更新为7秒以后),服务器发现15秒收不到任何报文就强制断开连接,客户端的select立即侦听到这种连接断开异常。select的超时时间设置的是7秒。
4.僵尸线程。苹果app在不开启后台定位等后台服务的情况下,应用切换到后题应用切换后台,应用被刮起,线程也被挂起,也就是苹果的墓碑式解决应用切换到后台的问题。长连接也是一个普通的线程(异步起的线程:dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{)。当你把应用再切换到前台时,你的这个长连接线程永远醒不来。
解决方案:把长连接放在一个单例里。通过单例的变量记录线程号。应用切换到前台立即重新建立一个新长连接线程。在线程函数中保证只有最新线程号的线程可以运行,其它的线程立即结束(函数返回)。
//设置线程名称 [[NSThread currentThread] setName:@"socket thread"]; //获得线程号 -(NSString *)getSocketThreadSno { NSString * socketThreadDescription = [NSThread currentThread].description; if(socketThreadDescription.length <= 0) { return nil; } NSRange range = [socketThreadDescription rangeOfString:@"number = "]; // FLDDLogVerbose(@"socketThreadDescription:%@", socketThreadDescription); if(range.length > 0) { NSRange range1 = [socketThreadDescription rangeOfString:@","]; if((range.length <= 0) || ((range1.length <= 0))) { return nil; } else { range = NSMakeRange (range.location + range.length, range1.location - range.location - range.length); if((range1.location > socketThreadDescription.length) || (range.location + range.length > socketThreadDescription.length)) { return nil; } NSString *number = [socketThreadDescription substringWithRange : range]; return number; } } else { //兼容ios8.0以后版本 NSRange range = [socketThreadDescription rangeOfString:@"num = "]; NSRange range1 = [socketThreadDescription rangeOfString:@"}"]; if((range.length <= 0) || ((range1.length <= 0))) { return nil; } else { range = NSMakeRange (range.location + range.length, range1.location - range.location - range.length); if((range1.location > socketThreadDescription.length) || (range.location + range.length > socketThreadDescription.length)) { return nil; } NSString *number = [socketThreadDescription substringWithRange : range]; return number; } } }
5.管道或socket另一端没有进程接收数据,导致管道破裂而崩溃。
解决方案: 见文章《Signal 13 was raised(SIGPIPE管道破裂)》
6.在手机上我没有遇到过,但是在华为赛门铁克子公司的路由器上遇到过网卡满了写不进去的罕见情况。
解决方案:判断socket的写文件描述符是否可写。
//判断是否可写,若可写,并且有消息可以发送就像服务器推送消息。 if (FD_ISSET(_fd, &client_fd_set)) { LogDebug(@"判断长连接可写\n"); }
7.如何解决socket的connect函数长期不返回问题。根据window的官方文档,windows系统的socket最大超时时间是30分钟。经过测试苹果手机的socket的创建连接最大超时时间是75秒。
解决方案:设置定时器处理这种连接超时情况,识别出来直接重新建立长连接就可以了。注意要是后台运行的应用,前后台都能起线程。若不时,只有前台运行时才能起线程,别到时候在前后台切换那个临界几秒你起个线程也没有用的。
8.如何设置异步socket呢?当然大家大都希望设置的socket是异步的吧,在创建socket是,大家可以做一些其它准备,在做push service时发现创建连接线程,循环两遍就能建立连接。在做app时发现循环第一遍就建立了socket。
解决方案:只需要setsockopt设置时,设置第三个参数是SO_SNDTIMEO就可以了。linux现在和mac os是一家亲,他们的很多方法是通用的。
int nNetTimeout=1000;//1秒 //发送时限 setsockopt(socket,SOL_S0CKET, SO_SNDTIMEO, (char *)&nNetTimeout,sizeof(int)); //接收时限 setsockopt(socket,SOL_S0CKET, SO_RCVTIMEO, (char *)&nNetTimeout,sizeof(int)); 这样做在Linux环境下是不会产生效果的,须如下定义:struct timeval timeout = {3,0}; //设置发送超时 setsockopt(socket,SOL_SOCKET,SO_SNDTIMEO,(char *)&timeout,sizeof(struct timeval)); //设置接收超时 setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval));
有两点注意就是:
1)recv ()的第四个参数需为MSG_WAITALL,在阻塞模式下不等到指定数目的数据不会返回,除非超时时间到。还要注意的是只要设置了接收超时,在没有MSG_WAITALL时也是有效的。说到底超时就是不让你的程序老在那儿等,到一定时间进行一次返回而已。
2)即使等待超时时间值未到,但对方已经关闭了socket, 则此时recv()会立即返回,并收到多少数据返回多少数据。
9.粘包问题,就是服务器通过socket发送过快或连续发送多条消息或socket客户端读取消息过慢,socket客户端一次收到多条消息。只要是自己socket就几乎无法避免粘包问题。
解决方法:简单的处理,限制消息长度,增加接受缓冲区的长度,支持一次解析多条消息。对处理可能缓慢的消息,起线程处理,做到不影响消息接受速度。
if (FD_ISSET(self.fd, read_fd_set)) { FLDDLogVerbose(@"ret2= %ld\n", ret); bzero(recv_msg, BUFFER_SIZE); long byte_num = recv(self.fd,recv_msg,BUFFER_SIZE,0); if(byte_num > 0) { //解析正常的消息,可能一次解析多条消息 } else{ //收到的消息长度为0说明是socket已经断开,需要重新建立socket FLDDLogVerbose(@"select 出错!\n"); //close(server_sock_fd); if(self.fd > 0) { setsockopt(self.fd, SOL_SOCKET, SO_NOSIGPIPE, SIG_IGN, sizeof(int)); close(self.fd); self.fd = -1; } self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } }
10.端口冲突问题,一台电脑:端口号的理论值范围是从0到65535,公共端口是0-1023 ,注册端口是1024-49152,还有随机动态端口是49152-65535,共是65536个端口。我在做push service时,华为赛门铁克老板说他们把他们的防火墙路由器电脑芯片可以用端口号从1万增加到2万是很大的成功。一台服务器,当端口号被申请到1万以后,再申请端口号后就经常遇到端口冲突。解决方案是采用动态虚拟端口的方式(我不知道怎么实现,这个是服务器方面的问题,理论上和我们客户端无关),其次是增加长连接服务器集群。虽然你们的应用很多,但是真正在使用的用户可能并不是很多,特别是不支持后台运行的应用,实际在线的用户可能更少。我上家公司的长连接服务器基于netty框架开发的。
11.当发送消息时,如何立即把消息通过socket发送出去。CocoaAsyncSocket是通过轮询的方式来实现及时发送,不过它的轮询间隔很短,所以用户没有太大的延迟感觉。不好的影响是耗电亮太大(手机耗电量和在运行的app线程循环速度正相关,跑飞的程序,手机一小会儿就发烫了),再小的延迟也是延迟,没有达到百分之百既发即走。
解决方案:设置为socket为异步socket,用select侦听消息。建立本地管道,把文件描述符加入select函数的监控参数中。当使用管道发送消息时,select立即结束等待按照文件描述符来识别是本地管道消息还是socket消息,然后按照消息内容进行处理。
注意:等需要长期通过select侦听消息时,不能把可写文件描述符加入fd_set由select函数监控。因为只要网卡不满,可写文件描述符号都处于可写状态,select立即结束等待,打不到等待消息的效果,当然你想发送消息时想判断网卡是否满了,就可以通过把可写文件描述符加入fd_set由select函数结合FD_ISSET来判断出来是否可写。
//监听socket和管道处理函数;服务器的消息会通过socket发送给客户端,被select函数监听到,客户端内部消息会通过管道发送过来,停止select的监听; //线程大部分时间处于该select函数,直到超时,所以它绝大部分时间起到10秒间隔的定时器的作用。 -(void)socketPipeSelectWithRet : (long *)ret processResult : (PROCESS_RESULT *)processResult timeval : (struct timeval *)tv nowTime : (long long *)nowTime readFdSet : (fd_set *)read_fd_set waitTimeInterval : (long *)waitTimeInterval max_fd : (int *)max_fd { LogDebug(@"函数"); [self processRequestTimeOut]; //当不处于登录中或登录成功的状态都结束长连接线程 if(!((LOGIN_ORDER_STATE_SUCESS == [[Singleton sharedInstance] getLoginOrderStat]) || (LOGIN_ORDER_STATE_LONGINING == [[Singleton sharedInstance] getLoginOrderStat]))) { [self endSocket]; *processResult = PROCESS_RESULT_ABORT; return; } if(SOCKECT_CONNECT_TIMEOUT == self.socketConnectStat) { //发送上线命令超时,可能socket异常,需要重新建立socket // close(server_sock_fd); if(self.fd > 0) { setsockopt(self.fd, SOL_SOCKET, SO_NOSIGPIPE, SIG_IGN, sizeof(int)); close(self.fd); self.fd = -1; } self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } if(self.fd <= 0) { self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } if(self.fdReadPipe <= 0) { self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; [self processSocketPipeClosed]; return; } self.fdWriteFlag = YES; FD_ZERO(read_fd_set); FD_SET(self.fd, read_fd_set); FD_SET(self.fdReadPipe, read_fd_set); self.bServiceActiveSend = NO; _bSendingFlag = YES; if(![self.messagesArray isKindOfClass:[NSMutableArray class]]) { self.messagesArray = [NSMutableArray array]; } if(![self.sendMessagesArray isKindOfClass:[NSMutableArray class]]) { self.sendMessagesArray = [NSMutableArray array]; } if(![self.processedSendMsgArray isKindOfClass:[NSMutableArray class]]) { self.processedSendMsgArray = [NSMutableArray array]; } if((SOCKECT_CONNECT_SUCESS != self.socketConnectStat)) { *waitTimeInterval = CONNECT_WAIT_TIME; } else if(self.hitOnLineTime > 0) { *waitTimeInterval = CONNECT_WAIT_TIME; } else if((0 == self.messagesArray.count) && (0 == self.sendMessagesArray.count) && (0 == self.processedSendMsgArray.count)) { *waitTimeInterval = SELECT_INTERVAL_TIME; } else { *waitTimeInterval = CONNECT_WAIT_TIME; } _bSendingFlag = NO; (*tv).tv_sec = *waitTimeInterval; (*tv).tv_usec = 0; //防止管道fd或socket的fd变更,需要计算出最大的fd *max_fd = self.fd; if(*max_fd < self.fdReadPipe) { *max_fd = self.fdReadPipe; } if(![self judgeConnectAfterNewestOnlySocketThread]) { //已经重新建立长连接线程,这个线程不是最新建立的线程,为了长连接线程唯一性,需要立刻结束该线程,防止出现两个长连接线程 LogDebug(@"关闭连接时间超过20秒的线程, Thread Sno: %@\n", [self getSocketThreadSno]); *processResult = PROCESS_RESULT_ABORT; return; } LogDebug(@"judgeConnectAfterNewestOnlySocketThread after"); int set = 1; setsockopt(self.fd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int)); int write = 1; setsockopt(self.fdReadPipe, SOL_SOCKET, SO_NOSIGPIPE, (void *)&write, sizeof(int)); //整个socket大部分时间会阻塞这这里,起到定时器的作用,能大大减少耗电量,收到管道消息或服务器发过来订单消息,select会立刻响应 //有订单时心跳为10秒,无订单时为60秒,若上次心跳和本次心跳间有其它消息发送就不发心跳消息了 // @try { *ret = select(*max_fd + 1, read_fd_set, NULL, NULL, tv); LogDebug(@"errno:%d\n", errno); // } @catch (NSException *exception) { // FLDDLogInfo(@"exception:%@", exception); // self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; // *processResult = PROCESS_RESULT_ABORT; // [self processSocketPipeClosed]; // return; // } @finally { // // } // FLDDLogDebug(@"select after"); LogDebug(@"ret2= %ld\n", *ret); if(self.fd <= 0) { self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } if(self.fdReadPipe <= 0) { self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; [self processSocketPipeClosed]; return; } if(![self judgeConnectAfterNewestOnlySocketThread]) { //已经重新建立长连接线程,这个线程不是最新建立的线程,为了长连接线程唯一性,需要立刻结束该线程,防止出现两个长连接线程 LogDebug(@"关闭连接时间超过20秒的线程, Thread Sno: %@\n", [self getSocketThreadSno]); _connectTime = (long long)[[NSDate date] timeIntervalSince1970]; *processResult = PROCESS_RESULT_ABORT; return; } *nowTime = (long long)[[NSDate date] timeIntervalSince1970]; if((NotReachable == [self getNetworkStatus]) || (ReachableUnknown == [self getNetworkStatus]) || (EhostUnreach == [self getNetworkStatus])) { // close(server_sock_fd); if(self.fd > 0) { setsockopt(self.fd, SOL_SOCKET, SO_NOSIGPIPE, SIG_IGN, sizeof(int)); close(self.fd); self.fd = -1; } self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } [self processRequestTimeOut]; *processResult = PROCESS_RESULT_NORMAL; _einProgressCount++; return; } //监听socket处理函数;主要是为了判定是否网卡缓冲是否已经满了,若网卡缓冲区满了,那么就不能通过长连接发送消息,或者可能出现数据丢失。 //一般select都会立刻响应,当然要处理监听期间有服务器的发送过来的消息和户端内部消息会通过管道发送过来的消息,当socket或管道中有数据没有读和网卡缓冲没有满可以写,select会立刻响应。 -(void)processReadWriteSelectNormalResultWithRet : (long)ret processResult : (PROCESS_RESULT *)processResult readFdSet : (fd_set *)read_fd_set recv_msg : (unsigned char *)recv_msg frontHearTime : (long long *)frontHearTime frontSendMidPoitTime : (long long *)frontSendMidPoitTime frontUpdatelocationTime : (long long *)frontUpdatelocationTime nowTime : (long long *)nowTime closeFlg : (BOOL *)closeFlg handleFlag : (BOOL *)handleFlag { if(self.fd <= 0) { self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } if(self.fdReadPipe <= 0) { self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; [self processSocketPipeClosed]; return; } //先把消息接收过来,后面再处理,防止再处理消息时,连接异常。 if (FD_ISSET(self.fd, read_fd_set)) { LogDebug(@"ret2= %ld\n", ret); bzero(recv_msg, BUFFER_SIZE); long byte_num = recv(self.fd,recv_msg,BUFFER_SIZE,0); if (byte_num > 0) { LogDebug(@"服务器:%s\n",recv_msg); LogDebug(@"byte_num= %ld\n, recv = %s\n", byte_num, recv_msg); // [self decodeMessageWithRecvMsg:recv_msg msgByteNum:byte_num]; [self decodeMessageWithRecvMsg:recv_msg msgByteNum:byte_num frontHearTime:frontHearTime frontSendMidPoitTime:frontSendMidPoitTime frontUpdatelocationTime:frontUpdatelocationTime]; *handleFlag = YES; } else{ //收到的消息长度为0说明是socket已经断开,需要重新建立socket LogDebug(@"select 出错!\n"); //close(server_sock_fd); if(self.fd > 0) { setsockopt(self.fd, SOL_SOCKET, SO_NOSIGPIPE, SIG_IGN, sizeof(int)); close(self.fd); self.fd = -1; } self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } } *closeFlg = NO; LogDebug(@"判断管道里是否有数据\n"); if (FD_ISSET(self.fdReadPipe, read_fd_set)) { LogDebug(@"ret2= %ld\n", ret); long nbytes; unsigned char readBuffer[PIPE_MESSAGE_MAX_BUFFER_LEN] = {0}; nbytes = read(self.fdReadPipe, readBuffer, PIPE_MESSAGE_MAX_BUFFER_LEN); if((nbytes < 0) && (errno == EINTR)) { //Interrupted system call,进程在一个慢系统调用(slow system call)中阻塞 *processResult = PROCESS_RESULT_CONTINUE; return; } else if (nbytes <= 0) { LogDebug(@"no data."); } else { readBuffer[PIPE_MESSAGE_MAX_BUFFER_LEN - 1] = '\n'; NSString *str = [[NSString alloc] initWithString:[NSString stringWithFormat:@"%s", readBuffer]]; LogDebug(@"data:%@", str); NSRange range1=[str rangeOfString:@",cancel quit" options:NSBackwardsSearch]; NSRange range2=[str rangeOfString:@",quit" options:NSBackwardsSearch]; NSRange range22=[str rangeOfString:receiveLocation options:NSBackwardsSearch]; NSRange range3=[str rangeOfString:@",send" options:NSBackwardsSearch]; NSRange range4=[str rangeOfString:@",net change" options:NSBackwardsSearch]; if(range1.length > 0) { self.isCloseSocket = NO; LogDebug(@"收到取消关闭socket管道消息:%@", str); } else if(range2.length > 0) { //收到下线指令,发送了下线消息,立刻结束长连接线程 *closeFlg = YES; } else if(range22.length > 0) { [self immediatelySendMidPointUpdatelocationWthFrontHeatTime:frontHearTime frontSendMidPoitTime:frontSendMidPoitTime frontUpdatelocationTime:frontUpdatelocationTime]; } else if(range3.length > 0) { LogDebug(@"收到发送消息管道消息:%@", str); } else if(range4.length > 0) { LogDebug(@"收到发送消息管道消息:%@", str); //close(server_sock_fd); if(self.fd > 0) { setsockopt(self.fd, SOL_SOCKET, SO_NOSIGPIPE, SIG_IGN, sizeof(int)); close(self.fd); self.fd = -1; } self.socketConnectStat = SOCKECT_CONNECT_ABNORMAL; *processResult = PROCESS_RESULT_SKIP; return; } } } *nowTime = (long long)[[NSDate date] timeIntervalSince1970]; LogDebug(@"nowTime - frontHearTime : %lld", *nowTime - *frontHearTime); if(_bServiceActiveSend) { //发送心跳消息,收到响应消息直接删除就可以,客户端不需要根据心跳判断socket断连,直接通过读取字节数为0直接判断socket断连 if([self socketHeart]) { *frontHearTime = *nowTime; _bServiceActiveSend = NO; } } *processResult = PROCESS_RESULT_NORMAL; return; } //通过长连接的监控管道发送char *格式的消息 -(BOOL)sendMessageBySocketMonitorPipeWithCharArr : (const char *)message { NSUInteger len = 0; len = strlen(message); if(0 == len) { return YES; } unsigned char input_msg[MESSAGE_MAX_LEN] = {0}; memcpy(input_msg, message, len);; @try { if(self.fdWritePipe <= 0) { //守护线程管道已经关闭 [self processSocketPipeClosed]; return NO; } setsockopt([SingleAsyncSocket sharedInstance].fdWritePipe, SOL_SOCKET, SO_NOSIGPIPE, SIG_IGN, sizeof(int)); long ret = write(self.fdWritePipe, input_msg, len); if((ret < 0) && (errno == EINTR)) { if(self.fdWritePipe <= 0) { //守护线程管道已经关闭 [self processSocketPipeClosed]; return NO; } setsockopt([SingleAsyncSocket sharedInstance].fdWritePipe, SOL_SOCKET, SO_NOSIGPIPE, SIG_IGN, sizeof(int)); //Interrupted system call,进程在一个慢系统调用(slow system call)中阻塞 long ret = write(self.fdWritePipe, input_msg, len); if(ret != len) { [self processSocketPipeClosed]; return NO; } else { return YES; } } else if(ret != len) { [self processSocketPipeClosed]; return NO; } else { return YES; } } @catch (NSException *exception) { FLDDLogInfo(@"exception:%@", exception); [self processSocketPipeClosed]; return NO; } @finally { } }