Android和iOS中的socket超时问题

咱们牛逼的新产品——码表,在数据同步上使用蓝牙和Wi-Fi两种同步方式,其中Wi-Fi同步是让码表和手机处在同一局域网内,然后通过一个协议来传输数据。

很容易想到Socket。

Socket,提供了IP层的数据传输业务。让两个节点通信成为了可能。

Socket有TCP和UDP传输两种方式。据说现在最先进的是TCP,它是对UDP的升级。从技术角度说,UDP是一个过时的技术。那么,它真的没用了吗?

简单一搜索,发现现在还是有很多业务是使用UDP的,而且用更先进的TCP反而效果不理想。

TCP

之前水壶架硬件和服务器交互的时候,使用的是TCP来传输。TCP的优点就是,保证数据正确、完整、已送达。它有强大又复杂的重传校验等机制。保证了可靠性。

而且,由于TCP是一个长链接,所以实时性也非常的好。

所以我们二话没说,就开始使用TCP传输。

Android TCP

其实android直接用系统的接口就可以很方便的进行socket操作。

注意,Socket是必须在新线程中操作的

初始化一个socket实例,获取它的input和output的流,对流进行读写,就相当于用socket进行收发了。

不过这里有一个问题,那就是你很难知道自己是否还连着socket,除非用一个写操作,出现异常。

因为他的isConnected或者isClosed方法都是获取本地状态的,并不知道远程服务器是否还连接着。

所以stackoverflow上的解决方法是,发送心跳包…发送一个urgentData来确定服务器是否还可达。

说实在的,这个解决方法,真的垃圾。

Timeout

那么超时呢?

我觉得完全没法做。读取流的时候,当前线程就完全阻塞了。根本没有机会去检查等待时间,而且完全无法中断读取的操作…………

iOS TCP

iOS上用CocoaAsyncSocket这个网络库来实现socket通信。实在是方便。

他的读和写可以增加Tag,方便实用状态基。并且有读取指定长度这种操作,还可以设置超时。

实在是太方便了,所以我都不想多说了……

BUT

写到这里,是比较了Android原生TCP超精简实现和iOS超厉害的框架实现的TCP通信。

当我根据协议把一切的一切写好了之后,联调时候发现了严重的丢帧问题。

从TCP的理论来说,丢帧了,应该会自动重传。但是并没有收到新的帧。上下位机都停在了某一个状态。

经过调试,我们发现问题出在了下位机发送数据和接受数据的地方。下位机发送数据后会收到TCP的响应。而这时候手机也发了协议中的响应数据回来。这就导致了下位机串口收到的数据,出现了粘包情况。导致了状态异常。从上位机的角度来说,自己的发送的响应已经得到了响应,会认为下位机已经收到了响应……

对,状态乱了,也不同步了。

所以我们在上位机这里增加了延迟机制,保护下位机脆弱的心灵。但是这并没有从根本上解决这个问题,而且这会大大增加传输需要的时间。(经过测试,发现稳定的用了延迟方法传输一个文件的速率是蓝牙的3倍)

UDP

这肯定是不能被接受的!

虽然说Wi-Fi的理论速度是用MB做单位的,但是由于我们硬件的各种限制。这个数字会大大大大的缩小。但是也不至于缩小到3kb左右~

我们通过进一步调试,发现主要问题还是出现在下位机TCP响应和数据包的粘连情况。所以,为了解决这个棘手的问题,不如直接把这个响应去掉算了!

Android UDP

UDP在Android中也有很现成的接口。

使用DatagramSocket类来操作udp。

udp可以算是数据包了,它是不基于连接的。但是它还是需要端口来通信。所以对于下位机来说,不能直接将数据通过一个连接写入了。得确定目标节点的地址和端口才能将数据返回回去。

由于之前TCP版本抽象的比较好,所以转换到UDP比较轻松。

Timeout

关键的一点是,android的DatagramSocket类自带了timeout属性!在等待数据进入的时候,可以设置一个timeout。一旦超时,直接抛出异常。之后就可以进入重传流程!

iOS UDP

CocoaAsyncSocket这个库必然也是提供了UDP操作的,不过可气的是,这次他不提供timeout这个操作了!

Timeout

所以iOS的超时就需要自己写了……

其实想想还是比较简单的。只要在发送完毕之后启动一个定时器,在x秒后检查是否有数据即可。

其中的难点大概就是,如何确定是否有数据进入,以及如何确定进入的数据是不是之前开启定时器时候的数据的回复信息……

所以,特地引入了一个变量

NSDate *reading_timestamp;

读取数据的时间。

一个定时检查的方法

-(void)check_receive:(NSTimeInterval)timestamp{
    if (reading_timestamp.timeIntervalSince1970==timestamp) {
    //timeout!!
    NSLog(@"timeout !");
    }
//.......
}

然后在需要回馈的发送中添加了一些赋值操作。

-(void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag{
    isSending=NO;
    if(tag>0&&tag<FitFileTransmissionStatusDone){
        transmissionStatus=(FitFileTransmissionStatus)tag;
        reading_timestamp=[NSDate date];
        NSTimeInterval interval=reading_timestamp.timeIntervalSince1970;
        [sock receiveOnce:nil];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
            [self check_receive:interval];
        });
    }
}

这里要注意,interval这个中间变量是一定要写的,不写会死。具体原因,就和block的机制有关了,想通过我的笔记了解的话,可以看看之前的一片关于危险的block的记录。

在延迟的block中比较全局的开始读取的时间和执行block前的时间局部的时间,来确定全局的读取时间是否有改变。用这个思路成功的解决了这个问题!!

End

通过UDP,传输稳得很,而且速度是蓝牙的19倍左右!这才叫爽。

全文只有iOS的Timeout贴了代码,因为。别的代码实在是太简单了,而且网上一查一大片。只有iOS这一段是我原创的~哈哈