Since some time I've been playing with GameKit, but now I'm facing really bad difficulties.
I'm going to send through Bluetooth bigger files - 1-2MB. I've already prepared a packets (about 8kB each).
My app works as described on following scheme:
iPhone - sending header: file divided into 25 parts
iPod - received header: OK I got it waiting for 25 parts
iPhone - sending part #1
iPod - received part #1 send next
iPhone - sending part #2
iPod - received part #2 send next
...
iPhone - sending part #24
iPod - received part #24 send next
iPhone - sending part #25
iPod receiving part #25 processing file
I send both file parts and messages (confirmation of delivery) using:
[mSession sendData:data toPeers:mPeers withDataMode:GKSendDataReliable error:nil];
and receiving data:
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context
I would like to know how do you deal with some problems that may occur during Bluetooth transmission. Browsing the documentations GKSessionDelegate doesn't give me any info if the data was delivered or not.
In 90% cases the transfer works fine, but sometimes it suddenly stops and doesn't continue without reconnection/restart the app.
I tried to invent a easy solution to set the data again if I won't get the response within 1sec:
-(void)sendAgain {
[self sendData:bufor];
}
-(void)sendData:(NSData *)data {
bufor = [data retain];
timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(sendAgain) userInfo:nil repeats:NO];
[mSession sendData:data toPeers:mPeers withDataMode:GKSendDataReliable error:nil];
}
timeOutTimer is invalidated if sender received confirmation of successful delivery of file part. But in fact when I implement this solution there are even more problems with this.
Devices are next to each other on the desk.
How do you deal with problems of "undelivered" data between devices? It's just a tool, but how it could be annoying while developing games?
By the way, sending short chats messages never caused any problem and I'm using the same methods.
In fact the connection get lost very rarely, just the data likes to be lost in the air. I'm already dividing the parts so the size of the data is about 8kb, what really makes the transfer of images really really slow.
The GameKit framework isn't very reliable at this point, even for simple exchange of data for games I work on. I wouldn't use it to transmit large data, you're just asking for headaches.
I agree with both "it" and "refulgentis". Doing this over GK is asking for unreliable execution. You are better off setting this up over Bonjour and wiFi or having each user download the content from some central offline source. If your design requires that the big files move from one device to another, you might want to upload them on one side and download them on the other, rather than trying to device-to-device transfer files that large.
Related
There are a handful of posts discussing how Game Center's push notifications were fairly unreliable in the sandbox. However, the sandbox is obfuscated with iOS 9 so, I'm not sure why my Game Center push notifications are so unreliable.
When I reply to the active exchange, the sender is rarely notified.
[exchange replyWithLocalizableMessageKey:#"EXCHANGE_REPLY" arguments:#[] data:data completionHandler:^(NSError *error) {
if (error)
{
NSLog(#"");
}
}];
On the senders device, if I refresh the match data, I'll see a pending reply. If I process the reply, everything works.
The same goes for this method:
- (void)sendExchangeToParticipants:(NSArray<GKTurnBasedParticipant *> *)participants
data:(NSData *)data
localizableMessageKey:(NSString *)key
arguments:(NSArray<NSString *> *)arguments
timeout:(NSTimeInterval)timeout
completionHandler:(void(^__nullable)(GKTurnBasedExchange *exchange, NSError *error))completionHandler
At this point, I'm thinking my best option is to run my own push notification logic to trigger updating match data. That or I've read that sending reminders is more reliable though I believe there are throttling limits around that.
Update
I've tried using only devices and not the simulator. Same issue. Looks like it's a pretty well known problem though. It's even noted in this book on page 766.
Update
Sending reminders didn't help.
Update
Often when replying to an exchange, I'll get this error from GameKit.
The connection to service named com.apple.gamed was interrupted, but the message was sent over an additional proxy and therefore this proxy has become invalid.
Exchanges has until Oct 2020 never actually worked as needed, nor as specified, due to a bug in the Apple backend. Now however, an Apple engineer seem to suggest it has been fixed - asking me to verify that it works. Which I intend to do ASAP (I just need to update Xcode) using my public project: https://github.com/Gatada/TurnBasedGameFlow
FURTHER DETAIL
A turn based exchange relies on the turn holder being notified when the exchange is completed, so the turn holder can resolve it (submit it to Game Center). This notification however, was never pushed to the turn holder.
As a result of this bug, the games we made had to rely on the turn holder re-loading the game after the exchange completes, and our code had to gracefully handle the turn submission failing due to game data being out-of-sync (caused by the completed exchange).
I had a one-on-one Game Center session with Apple during WWDC 2020, where I reported this issue with hard evidence (after all, this bug had been around since 2010) which convinced the Apple engineer. It took them 3 months to get back to me, and another 3 months for me to get back to them - hehe, bringing us to now.
Most of time, this works normally in my application. Unfortunately, sometimes it never been triggered after discoverservice is called.
My code is:
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(#"Did connect to peripheral: %#", peripheral);
[self.delegate statusMessage:[NSString stringWithFormat:#"Did connect to peripheral: %#\n", peripheral]];
peripheral.Delegate = self;
NSArray *serviceArray = [NSArray arrayWithObject:_uuid_tpms_sensor_service];
[_peripheral discoverServices:serviceArray];
[peripheral discoverServices:serviceArray ];
}
Some posts relative with this are
CoreBluetooth never calls didDiscoverServices on iPhone5S
iOS CoreBluetooth not scanning for services in iPad Air
iPhone does not discover Services on a Bluetooth LE tag on reconnection
The final conclusion should be an issue in iOS. My question is that, given that it's iOS problem, how to work around this?
Many thanks to Larme and henrik as I got many ideas from your reply.
After three days verification, it seems I have find a work around for this problem (the problem is more likely a limitation of Bluetooth stack of iOS rather than an issue)
I'd like to summarize my findings and work around here:
[root cause]
Bluetooth stack in iOS is not robust enough, it's therefore the internal state machine become corrupted after some unexpected API calling.
As the BTLE radio follows a certain pattern where it interacts with one device connection query at a time, BLE application should follow the API sequence of connectperiperal-->discover-->read-->disconnect (trigger by local, peer device or supervision timeout on link layer).
Apple admitted that this was an issue in iOS.
[resolution]
follow the API calling sequence described in the above
Hope the summary is useful for other person.
Too many rogue app's not handling the BLE stack properly can cause it to crash. Then the phone needs to be restarted. iOS7 and 8 are much more robust than in the early life of iOS BLE.
Android are still less robust. Programmers forget to release resources or might try to write new data before the old has been transmitted.
This can happen a lot if you use X-code and you stop your app at a point before it releases resources etc. This can still crash iOS BLE stack.
I'm creating an online-shop-style app where users can browse different products on their iPad and order these products. The ordering process consists of creating an xml-file with the user's data and the relevant products he would like to order. But sometimes there might be the case, that users don't have an internet connection right now and I would like to create some mechanism, which checks every x minutes for an active internet connection and then tries to deliver the order-xml. It should repeat this step until it gets connected to the web and then just stop it, when all offline carts have been sent.
I have already been searching the web but only found ways to do this on iOS 7 (with UIBackgroundModes - fetch). But I don't want to use iOS 7 because the app is already done and I'm not planning to redesign it for iOS 7 (it's an Enterprise App). As far as I know, the current Background Execution time on iOS 6 is limited to something like 15 minutes, is that correct?
Any ideas on how to solve that?
Thanks.
EDIT:
I have tried the following in - (void)applicationDidEnterBackground:(UIApplication *)application
self.queue = [[NSOperationQueue alloc] init];
[self.queue addOperationWithBlock:^{
[[InstanceHolder getInstance] startNetworkTimer];
}];
and here is what should happen next:
- (void) startNetworkTimer{
if ([CommonCode getAllOfflineCartsForClient:nil].count > 0){
NSTimer *pauseTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:#selector(offlineCartLoop:) userInfo:nil repeats:YES];
}
}
- (void) offlineCartLoop:(id)sender{
if([CommonCode isInternetConnectionAvailable]){
[self sendOfflineCarts];
[sender invalidate];
}
}
startNetworkTimer gets called as it should, but then it doesn't call the offlineCartLoop function :-(
EDIT 2:
I think the timer-thing was the problem. I'm now calling the offlineCartLoop function like this:
self.queue = [[NSOperationQueue alloc] init];
[self.queue addOperationWithBlock:^{
[[InstanceHolder getInstance] offlineCartLoop:nil];
}];
and changed the offlineCartLoop function to this:
- (void) offlineCartLoop:(id)sender{
if([CommonCode isInternetConnectionAvailable]){
[self sendOfflineCarts];
}else{
[NSThread sleepForTimeInterval:10.0];
[self offlineCartLoop:nil];
}
}
Seems to work, but will this run forever? Is there anything else I need to take care of?
There is no solution to what you want - there is no such thing as being able to periodically check every N minutes in the background unless it is within the time window granted by beginBackgroundTaskWithExpirationHandler.
However that only permits 10 minutes of execution time for iOS6 and earlier, or approximately 3 minutes for iOS7.
You cannot cheat and try and use a background mode if your app does not need it, and even the background modes do not permit you to freely run whenever you want.
Even the new background modes in iOS 7 do not permit you to run on a scheduled basis.
Your best best actually is iOS7 even though you don't want to migrate to iOS7 - the background fetch being the relevant mode (even though you are pushing not fetching). With that background mode you will be able to have the opportunity to execute but not when you decide, only when the OS decides - and the frequency of that depends upon how the user uses your app.
With iOS6 your options are even less restricted.
See iOS: Keep an app running like a service
Basically there just is no such thing as continuous background execution, nor periodic background execution, nor the app deciding when it wants to run when in the background.
If the user does not have an internet connection at the time they use your app to place the order then you should be notifying them of that anyway (if you don't then your app risks rejection from the app store) and maybe tell them to try again later.
If they are in flight mode the user will know they are in flight mode, if there is a temporary interruption (such as the phone is in an elevator or tunnel) then your app could keep on trying for as long as it is able - keep trying every minute while in the foreground, then when you switch to the background you know you have 10 minutes left, keep trying until the 10 minutes has nearly expired then post a local notification to the user notifying them that the app was unable to place the order due to lack of connectivity. If the user clicks on the notification and your app launches then the app will have the chance to retry again at that point.
If you still cannot make a connection then so be it, but you will have the chance to start the retry algorithm again. But at least you have notified the user their order has not gone through.
If what you need to know is if and when a data connection is available, I recommend inverting the process: rather then querying for a data connection, let your app be notified when a data connection is available. It's more efficient.
On this subject, I suggest using Reachability: you can make a call to know if a specific URL is accessible, and execute a block of code as soon as a connection is available.
Reachability *reach = [Reachability reacabilityWithHostName:#"www.myservice.com"];
...
reach.reachableBlock = ^(Reachability *reach) {
// Process the requests queue
// You should implement the method below
[self processQueue];
}
...
if ([reach isReachable]) {
// Upload the XML file to the server
// You should implement the method below
[self uploadToServer:myRequest];
} else {
// Enqueue your request somewhere, for example into an NSArray
// You should implement the method below
[self addToQueue:myRequest];
}
The above code is meant to be a showcase (it doesn't work as is), use it as reference. I can just say that the reach variable should be a class property or data member, and that it should be initialized once.
Also, if you enqueue your requests into an NSArray, be sure to do it in thread safe mode
Alternatively, Reachability can also notify via NSNotification when a connection is available - a different way to achieve the same result. Up to you to decide which one better fits with your needs.
Is it possible to handle a request timeout with a UIAlert? I would like to inform the user that there has been a time out. I set 0.0 just for testing to see if it would occur. The log does not print out so i do not believe i am handling correcting
request.timeoutInterval=0.0;
and to handle it:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if(error.code == NSURLErrorTimedOut){
NSLog(#"Time out");
}
}
I am using NSURLConnectionDelegate and NSURLConnectionDownloadDelegate
Thanks.
You can find a list of all the relevant error codes here. Note that -1009 is kCFURLErrorNotConnectedToInternet. To force a timeout, you need to have the ability to suppress the actual sending of the URL, or find one that just goes into a black hole.
Apple bundles Network Link Conditioner with Xcode (its in the networking tools I believe). There is a great article on this tool on NSHipster.
Another way (I believe) to get a timeout is to immediately after sending a request is to switch to another app, putting yours in the background. Wait 5 minutes, switch back, and look at the log. What you can do is in a demo app continually send out NSURLConnections - that is, once the first returns, send another. so there is always one outstanding. Now switch your app out - switch to another app in the simulator - wait, then return. You should be able to catch the condition this way, you can see the affect of changing the timeoutinterval value.
There was a long thread about timeouts in the private Apple forums for iOS networking - you may be able to find it. In the end, the system enforces a reasonably long minimum (as I recall), and you may be able to make it longer but not shorter. This hidden behavior has for sure baffled many people. The responder to the question was Quinn "The Eskimo", one of Apple's most experienced networking engineers.
I have been using GKMatch for quite a while successfully in an app. I have been chasing down and issue with the game occasionally stopping and have tracked it down to packets being sent but not received. This happens only occasionally but I can't seem to track down why it happens.
All messages are sent using GKSendDataReliable.
Logging has shown that the packet is being sent from one device successfully, but it is never received at the target device.
//Code sample of sending method....
//self.model.match is a GKMatch instance
-(BOOL) sendDataToAllPlayers:(NSData *)data error:(NSError **)error {
[self.model.debugger addToLog:#"GKManager - sending data"];
return [self.model.match sendDataToAllPlayers:data withDataMode:GKSendDataReliable error:error];
}
...
//Code sample of receiving method....
// The match received data sent from the player.
-(void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
[self.model.debugger addToLog:#"GKManager - received data"];
[super didReceiveData:data fromPlayer:playerID];
}
What I see happen is that periodically (maybe 1 in 100 messages) is sent without error from the 'sendDataToAllPlayers' method, but the receiving device never hits the 'didReceiveData' method. My understanding is that using GKSendDataReliable should send messages and then won't send another until it receives an acknowledgement. Messages aren't received but new messages are sent from the same device.
The sending method returns 'YES' and error is nil, but the didReceiveData is never hit...!
Has anyone ever seen this? Does anyone have any ideas what this could be? I don't know what else I could do to debug this.
I confirm the bug.
I made an example project consistently reproducing the issue: https://github.com/rabovik/GKMatchPacketLostExample. I tested it on a weak internet connection (iPad 3 with Wi-Fi and iPhone 4S with EDGE; both on iOS 6.1.3), and some packets regularly get lost without any error from Game Center API. Moreover sometimes device stops receiving any data, while another still sends and receives messages successfully.
So if we are sure the bug exists, the only possible workaround is to add an extra transport layer for truly reliable delivery.
I wrote a simple lib for this purpose: https://github.com/rabovik/RoUTP. It saves all sent messages until acknowledgement for each received, resends lost and buffers received messages in case of broken sequence.
In my tests combination "RoUTP + GKMatchSendDataUnreliable" works even beter than "RoUTP + GKMatchSendDataReliable" (and of course better than pure GKMatchSendDataReliable which is not really reliable).
[Edit: RoUTP no longer seems to work properly in iOS9]
I did some testing yesterday at the edge of my wifi's range where packet loss was occuring. What happens is that when packets are lost using GKMatchSendDataReliable the player is abruptly disconnected from the GKMatch session. match:player:didChangeState is called with GKPlayerStateDisconnected and the player's ID is removed from the playerIDs dictionary. This happens with only slight packet loss. I can still browse the internet from this connection for instance.
Now, if I switch to sending packets unreliably, then match:player:didChangeState never fires and the match keeps going without a problem (except losing the occasional packet which might be important). It will only disconnect if the packet loss becomes substantial. Now this is where Yan's RoUTP library is handy, since we can keep track of and retry these important messages without having our players disconnected when they encounter slight packet loss.
Also, data sending using GKMatchSendDataReliable will only return YES if the message has been successfully queued for delivery. It does not tell you whether or not the message was successfully delivered. How could it? It returns right away.