Game Center GKMatch GKSendDataReliable packet lost - ios

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.

Related

How do I reply to a GKTurnBasedExchange? GKLocalPlayerListener delegate receivedExchangeReplies is called intermittently

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.

cancel file uploading with NSURLConnection

I've got NSURLConnection with timeout = 30s which is uploading an image on server.
If connection is horrible and call delegate method didFailWithError: then i need to cancel current connection.
But if i just call the [myConnection cancel] connection will still alive but will not call delegates methods (apple docs say it - NSURLConnection cancel method). And i want to abort connection but not only remove delegate methods. How i can do what?
upd:
My problem is if connection is fails by timeout - in business logic i must recreate connection with similar request. If i have got horrible connection for 1 min and after that connection will be good - server will get a lot of (about 3 times retry count) photos. But first 2 connections is canceled. –
At the moment i make "dirty hack" like "if it's photo uploading request" - do not retry recreate connection.
I do a ton of network stuff and don't recall a scenario where everything was successfully received but the iOS app timed out. I'm trying to grok the scenario you describe, where you're seeing this happen a lot and I'm not seeing how that would happen. We might need to see some of your code.
Regardless, when you cancel a NSURLConnection, it not only stops the delegate methods from being called, but it stops the upload, too. I just did a test:
I attempting to upload a 20mb file (non-chunked request);
At the 1mb mark (as identified by didSendBodyData), I canceled the connection (by calling [connection cancel]);
I immediately stopped receiving any delegate messages at that point;
Looking at Charles, I'm only seeing 1.3mb of data in the hex log of the request. When I look at the "Network" tab of the Mac OS "Activity Monitor" and looking at by "Sent Bytes", it's at 2.1mb uploaded.
So canceling a connection will stop further data from being sent. Perhaps if there is some transmission in progress that still gets out (that's the asynchronous world we live it), but the it's not true to conclude that canceled connections will routinely send their full HTTP request. There must be something about the nature of the timeout that is unique to your environment.
In terms of your immediate problem, I might suggest that when uploading a file that the iOS app assign some unique identifier to the upload so that the server code can immediately recognize duplicate requests and handle them appropriately. But the question is why you are seeing so many time-outs and notably ones where the request appears to be successfully received in toto, but the response is not. That's very curious.
You cannot forcefully abort an ongoing connection.
In case if connection is not yet started cancel and unscheduleFromRunLoop for the NSURLConnection will work.
Try with following step
[myConnection cancel];
myConnection = nil;
Might be helpful in your case and If this step is not working then also try with myConnection.delegate = nil;

iOS socket based chat, how to receive messages that were sent when app is in the background?

For fun I've built a little socket based chat application. It is speaking over tcp socket with a nodejs server. Its successfully sending messages back and forth. However, the problem occurs when i press the home button and put my app in the background - at this point the socket seems to be "paused" and all messages that are sent during this time are lost.
I've got push notifications setup for when the app is in the background but since the socket is not active in the app the message never really reaches the app.
How should i handle this? What are my options?
Ive got a few ideas but id love some input here on how people are handling this.
My ideas:
Add a "loadHistory" method on my nodejs server that sends all the history of a channel. So that my app can call a certain URL and get a
JSON formated response with all the messages. Maybe this should be done each time a table item is touched?
Make use of the new iOS7 background running features? (Im not to sure its possible to keep the socket alive with that)
Implement a way for the server to know if a message has been delivered, if it could not be delivered it stores the message on the
database. As soon as the client goes online again it sends the
messages over tcp. This would require a lot of added functionality to
the server so id like to avoid this if possible.
if you want offline message , the third way is needed, for every message you get, you need to send a notify to the server to tell it the message is delivered , and when you get connected , you should fetch the lost message.
you could use voip, but your app can not receive message if it was killed.
(if you add voip mode but your app is not about voice, apple will block your app)
I have also done some work over TCP socket. I gave background support by these steps :
Add "Required background modes" key in your plist file.
Add this code in your AppDelegate.m file
- (void)applicationDidEnterBackground:(UIApplication *)application
{
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000
if([[UIDevice currentDevice] respondsToSelector:#selector(isMultitaskingSupported)] && [[UIDevice currentDevice] isMultitaskingSupported])
{
NSLog(#"Keep timeout alive");
[application setKeepAliveTimeout:600 handler: ^{
NSLog(#"applicationDidEnterBackground:: setKeepAliveTimeout:handler^");//task as you want to do
}];
}
#else
LogInfo(#"applicationDidEnterBackground (Not supported)");
#endif
}
When you get your read and write CFStream socket object after a successful TCP connection, add this code
CFReadStreamSetProperty(yourCFReadStreamObj, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
CFWriteStreamSetProperty(yourCFWriteStreamObj, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
After flowwing these steps your socket should receive message in background too and connection will be active till the time you give in "applicationDidEnterBackground".
I hope this will help. Thanks

iOS GameCenter connection from different networks

I have made an iOS multiplayer GameCenter game, but right before publishing found an issue I don't know how to solve. In coding process I used Ray Wenderlich tutorial http://www.raywenderlich.com/3276/how-to-make-a-simple-multiplayer-game-with-game-center-tutorial-part-12
GameCenter view controller is shown, connection creates and game can be played until both devices are on the same Wifi network.
If I turn off Wifi on my phone and use 3G network, then try to start new game - in that case connection isn't made anymore. Both devices find each other, but hangs on "Connecting..." screen. It looks like that
- (void)match:(GKMatch *)theMatch player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
is not called. Any ideas how to solve it or at least understand, where exactly is the problem?
I think that in your particular case the problem is that your 3G ISP restricts connections from necessary ports. The Apple docs say:
To use Game Center ... port forwarding must be enabled for ports 443 (TCP), 3478-3497 (UDP), 4398 (UDP), 5223 (TCP), 16384-16387 (UDP), and 16393-16472 (UDP)
I faced this issue too when trying to play on iPad connected via bluetooth to iPhone: there was "Connecting..." screen on each device.
But when I use built-in iPad 3G (with a different tariff plan) everything goes fine.
Just remind, in a normal match-making scenario match:player:didChangeState: may not be called. You should also check match.expectedPlayerCount:
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)theMatch {
//...
if (theMatch.expectedPlayerCount == 0) {
NSLog(#"Ready to start match!");
}
}
Also I expected a similar problem with "Connecting..." screen, but on Wifi network.
And it was reproduced only on iOS6 and after I tried rematch via -[GKMatch rematchWithCompletionHandler:^(GKMatch *match, NSError *error) {}] before.
One device hanged on "Connecting..." screen but on the other matchmakerViewController:didFindMatch: was successfully called, but what is interesting is that match.expectedPlayerCount was 0 and match.playerIDs array was empty at the same time.
I think such error occurred because I tried to find a new match while previous match tried to reconnect on background thread in the same time. And because of that new match was obtained corrupted.
The decision is to wait for rematchCompletion being called and only then try to find new match. There is no interface in GKMatch to cancel rematch, so I use [[GKMatchmaker sharedMatchmaker] cancel] and after several seconds rematchCompletion is called with error and we are ready to start finding new match.
Also I figured out that old unsued GKMatch instances are not deallocated and continue to live somewhere in GameKit framework. And they may probably cause problems if the work with them is not finished correctly (i.e. not disconnected, or rematch is not canceled in my case ). So do not forget to call -[GKMatch disconnect] and finish any other kind of work before removing the last strong reference to the match object .

iPhone SDK: GameKit and large files + connection lost

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.

Resources