WebSocket Connection is not closing using SocketRocket - objective-c

I use the SocketRocket library for Objective-C to connect to a websocket:
-(void)open {
if( self.webSocket ) {
[self.webSocket close];
self.webSocket.delegate = nil;
}
self.webSocket = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"ws://192.168.0.254:5864"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20]];
self.webSocket.delegate = self;
[self.webSocket open];
}
Opening the connection works totally fine. The delegate is called after the connection was established.
-(void)webSocketDidOpen:(SRWebSocket *)webSocket {
NSLog(#"WebSocket is open");
}
But when I want to close the connection, nothing happens.
-(void)close {
if( !self.webSocket )
return;
[self.webSocket close];
self.webSocket.delegate = nil;
}
The delegate for successfully closing the connection is not called. Can anyone tell me why this happens?
Thank you for reading my question.

I figured out that the delegate is never called, because the websocket is never really closed. The closing of the websocket in the SRWebSocket happens in the method pumpWriting like this:
if (_closeWhenFinishedWriting &&
_outputBuffer.length - _outputBufferOffset == 0 &&
(_inputStream.streamStatus != NSStreamStatusNotOpen &&
_inputStream.streamStatus != NSStreamStatusClosed) &&
!_sentClose) {
_sentClose = YES;
[_outputStream close];
[_inputStream close];
if (!_failed) {
dispatch_async(_callbackQueue, ^{
if ([self.delegate respondsToSelector:#selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
[self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES];
}
});
}
_selfRetain = nil;
NSLog(#" Is really closed and released ");
}
else {
NSLog(#" Is NOT closed and released ");
}
All streams and an object to retain the websocket are closed or deleted there. As long as they are still open, the socket won´t be closed appropriately. But the closing never happened in my program, because when I tried to close the websocket, _closeWhenFinishedWriting was always NO.
This boolean is only set once in the disconnect method.
- (void)_disconnect;
{
assert(dispatch_get_current_queue() == _workQueue);
SRFastLog(#"Trying to disconnect");
_closeWhenFinishedWriting = YES;
[self _pumpWriting];
}
But when calling the closeWithCode method in SRWebSocket, disconnect is only called in one case and that is, if the websocket is in the connecting state.
BOOL wasConnecting = self.readyState == SR_CONNECTING;
SRFastLog(#"Closing with code %d reason %#", code, reason);
dispatch_async(_workQueue, ^{
if (wasConnecting) {
[self _disconnect];
return;
}
This means, if the socket is in another state, the websocket will never really close. One workaround is to always call the disconnect method. At least it worked for me and everything seems to be alright.
If anyone has an idea, why SRWebSocket is implemented like that, please leave a comment for this answer and help me out.

I think this is a bug.
When calling close, the server echo's back the 'close' message.
It is received by SRWebSocket, however the _selfRetain is never set to nil, and the socket remains open (the streams are not closed) and we have a memory leak.
I have checked and observed this in the test chat app as well.
I made the following change:
-(BOOL)_innerPumpScanner {
BOOL didWork = NO;
if (self.readyState >= SR_CLOSING) {
[self _disconnect]; // <--- Added call to disconnect which releases _selfRetain
return didWork;
}
Now the socket closes, the instance is released, and the memory leak is gone.
The only thing that I am not sure of is if the delegate should be called when closing in this way. Will look into this.

Once an endpoint has both sent and received a Close control frame, that endpoint SHOULD Close the WebSocket Connection as defined in Section 7.1.1. (RFC 6455 7.1.2)
The SRWebSocket instance doesn't _disconnect here because that would close the TCP connection to the server before the client has received a Close control frame in response. In fact, _disconnecting here will tear down the TCP socket before the client can even send its own Close frame to the server, because _disconnect ultimately calls _pumpWriting before closeWithCode: can. The server will probably respond gracefully enough, but it's nonconforming, and you won't be able to send situation-unique close codes while things are set up this way.
This is properly dealt with in handleCloseWithData:
if (self.readyState == SR_OPEN) {
[self closeWithCode:1000 reason:nil];
}
dispatch_async(_workQueue, ^{
[self _disconnect];
});
This block handles Close requests initiated by both the client and the server. If the server sends the first Close frame, the method runs as per the sequence you laid out, ultimately ending up in _pumpWriting via closeWithCode:, where the client will respond with its own Close frame. It then goes on to tear down the connection with that _disconnect.
When the client sends the frame first, closeWithCode: runs once without closing the TCP connection because _closeWhenFinishedWriting is still false. This allows the server time to respond with its own Close frame, which would normally result in running closeWithCode: again, but for the following block at the top of that method:
if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) {
return;
}
Because the readyState is changed on the first iteration of closeWithCode:, this time it simply won't run.
emp's bug fix is necessary to make this work as intended, however: otherwise the Close frame from the server doesn't do anything. The connection will still end, but dirtily, because the server (having both sent and received its frames) will break down the socket on its end, and the client will respond with an NSStreamEventEndEncountered:, which is normally reserved for stream errors caused by sudden losses of connectivity. A better approach would be to determine why the frame never gets out of _innerPumpScanner to handleCloseWIthData:. Another issue to keep in mind is that by default, close just calls closeWithCode: with an RFC-nonconforming code of -1. This threw errors on my server until I changed it to send one of the accepted values.
All that said: your delegate method doesn't work because you're unsetting the delegate right after you call close. Everything in close is inside an async block; there won't be a delegate left to call by the time you invoke didCloseWithCode: regardless of what else you do here.

Related

ObjectiveC - displaying window through distributed objects

I try to display window in other process using Distributed objects.
Process A invoke remotely through Distributed Object method from process B which display dialog. Something wrong happens if I try to wait for results.
The method looks like that:
-(BOOL)showWindow //method invoked through distributed objects
{
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[object showDialog:^(BOOL result){ //this methods creates and display window
NSLog(#"Block called");
dispatch_semaphor_signal(sem);
}];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
return YES;
}
The function showWindow never ends. If I comment dispatch_semaphore_wait "Block called" is displayed and window is show.
I checked different variant synchronization, I tried to run this code using dispatch_sync or async but nothing helped.
I will be gratefull for help.
Kon
The problem is that DO requires that the run loop runs in order to process communication between the two sides. Basically, the code you showed sends a request to the remote side, the one which hosts the real object that object is a proxy for. The request carries a reference to your local block object. The remote side effectively gets a proxy for that.
Some time later, the remote side invokes its block proxy. That causes a request to be sent back to this process and thread. However, this thread can't receive that request because it is blocked in dispatch_semaphore_wait(). It is not attending to any communication channels.
So, both sides deadlock. The local side waits forever for a semaphore that will never be signaled and the remote side waits forever for its request to run the block to complete.
Thought of another way, the semaphore is not signaled by the remote process. You've asked for it to be signaled by this same thread which is waiting on it. It's just that, if it were possible, this thread would do so in response to an event sent by another process. Having a thread be responsible for signaling the semaphore that it's waiting on is a pretty clear, immediate self-deadlock.
Can you change -showWindow to not be synchronous? Why is it using a semaphore and waiting? Why can't it just fire off the request and then return back to the calling code, which should not assume the dialog has completed and instead return all the way back to the main event loop? Whatever work should happen "next" should be put into the completion block and will just be invoked asynchronously when the dialog completes.
If you really, really need this method to be synchronous, then you will have to use the run loop to wait instead of a dispatch semaphore. Something like:
-(BOOL)showWindow //method invoked through distributed objects
{
__block BOOL done = NO;
[object showDialog:^(BOOL result){ //this methods creates and display window
NSLog(#"Block called");
done = YES;
}];
NSString* mode = #"com.yourcompany.yourapp.privatemode";
NSConnection* conn = [(NSDistantObject*)object connectionForProxy];
[conn addRequestMode:mode];
while (!done)
[[NSRunLoop currentRunLoop] runMode:mode beforeDate:[NSDate distantFuture]];
[conn removeRequestMode:mode];
return YES;
}
Probably, you should actually add the private mode to the connection when it's first created and leave it there for its lifetime. You don't want to use any of the predefined modes for this because then unexpected things may fire while you're waiting and your code becomes unexpectedly re-entrant, etc.

NSConnection - how to properly do "unvending" of an object?

For Mac OSX, I'm trying to use an NSConnection to proxy access of an object from one application instance to another on the same host. The relevant code is below. I can provide more if needed. Assume when I say "server", I'm referring to the application that actually "vends" an object with an NSConnection. And "client" is the other instance of the same application that gets a proxy to it.
Everything works, except for two issues.
When an application acting as a server attempts to tear down the object it is vending, any client connected through a proxy still remains. That is, even after I call my stopLocalServer function below, any client app that previously connected and got a proxy object is still able to send messages and invoke code on the server app. I would have expected the client to throw an exception when passing a message after the server calls NSConnection:invalidate. How do I forcibly disconnect any client without requiring the server process to exit?
In the startClientConnection code below, if the server never vended the object in the first place with the expected registered name, then the call on the client to NSConnection:connectionWithRegisteredName:host will immediately return nil. This is good. But if the server had started vending an object via the startLocalServer code below, then later stops vending it with stopLocalServer, subsequent client attempts to connect will hang (block forever) until the server application process exits. The call to NSConnection:connectionWithRegisteredName returns a non-nil object, but the call to [_clientConnection rootProxy] hangs forever until the server application actually exits.
I suspect that I'm not properly tearing down the original NSConnection object or I'm missing something basic here.
Here is some relevant code for the platform that my user interface code sits on top of:
-(void)startLocalServer:(NSString*)str
{
[self stopLocalServer]; // clean up any previous instance that might be running
_serverConnection = [NSConnection new];
[_serverConnection setRootObject:self];
[_serverConnection registerName:str];
}
-(void)stopLocalServer
{
[_serverConnection registerName:nil];
[_serverConnection setRootObject:nil];
[_serverConnection invalidate];
_serverConnection = nil;
}
-(void)startClientConnection:(NSString*)str
{
[self stopClientConnection]; // tear down any previous connection
_clientConnection = [NSConnection connectionWithRegisteredName:str host:nil];
if ((_clientConnection == nil) || (_clientConnection.valid == NO))
{
LogEvent(#"ERROR - _clientConnection is nil or invalid!");
}
else
{
_proxy = [_clientConnection rootProxy];
}
}
-(void)stopClientConnection
{
_proxy = nil;
[_clientConnection invalidate];
_clientConnection = nil;
}
Answering my own question. I'll still hold out for a better answer, or if anyone can do a better job explaining the reasoning about why this is needed.
I believe the stopLocalServer function needs to call [[_serverConnection receivePort] invalidate] such that the port created with the connection is closed. Just adding that line to the original stopLocalServer function solves my problem. That prevents further connection attempts and messages from succeeding.
More appropriately, the application call can just own the port that the NSConnection uses. So this becomes a better solution for starting and stopping a distributed object listener:
-(void)startLocalServer:(NSString*)str
{
[self stopLocalServer]; // clean up any previous instance that might be running
_port = [NSPort port]; // _port is of type NSPort*
_serverConnection = [NSConnection connectionWithReceivePort:_port sendPort:nil];
[_serverConnection setRootObject:self];
[_serverConnection registerName:str];
}
-(void)stopLocalServer
{
[_serverConnection registerName:nil];
[_serverConnection setRootObject:nil];
[_serverConnection invalidate];
_serverConnection = nil;
[_port invalidate];
_port = nil;
}
That seems to solve both #1 and #2 above.

Multipeer Connectivity Disconnect

I'm having trouble with staying connected using the Multipeer Connectivity Framework in iOs7. Currently my app is programmatically handling the browsing and advertising using MCNearbyServiceAdvertiser and MCNearbyServiceBrowser. I have an alert view that asks the user if he is a browser or an advertiser. On the return from that view I instantiate either an MCNearbyServiceAdvertiser or Browser accordingly.
#pragma - Alert Delegate
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0)
{
_browser = [[MCNearbyServiceBrowser alloc]initWithPeer:_peerID serviceType:#"Context-xl"];
[_browser setDelegate:self];
[self.detailViewController setRemote:YES];
[_browser startBrowsingForPeers];
} else
{
_advertiser = [[MCNearbyServiceAdvertiser alloc]initWithPeer:_peerID discoveryInfo:nil serviceType:#"Context-xl"];
[_advertiser setDelegate:self];
[self.detailViewController setRemote:NO];
[_advertiser startAdvertisingPeer];
}
[self.detailViewController configureView];
}
My session delegate method peer:...DidChangeState... is getting called twice, once for the connect and again for the disconnect. I'm not stopping the advertiser or browser at all after the session is started. Should I stop browsing/advertising?
EDIT Used a support ticket with Apple and they confirmed that calling sendData with too much data or too often can cause disconnects.
EDIT My hypothesis is that Apple has a thread or queue that is polling to check if peers are connected. If this thread / queue stalls (i.e. a breakpoint is hit or the app pegs the CPU or does something that takes a while on the main thread) it appears that this causes a disconnect.
Creating my session without encryption seems to have helped performance and with the disconnects, although they still happen.
MCPeerID* peerId = [[MCPeerID alloc] initWithDisplayName:self.displayName];
self.peer = [[MultiPeerPeer alloc] initWithDisplayName:peerId.displayName andPeer:peerId];
self.session = [[MCSession alloc] initWithPeer:peerId securityIdentity:nil encryptionPreference:MCEncryptionNone];
In addition, I have found calling sendData too often (more than 30-60 times a second) can cause the framework to get in a bad state and cause freezes and disconnects.

Why does my MCSession peer disconnect randomly?

Im using MCNearbyServiceBrowser and MCNearbyServiceAdvertiser to join two peers to a MCSession. I am able to send data between them using MCSession's sendData method. All seems to be working as expected until I randomly (and not due to any event I control) receive a MCSessionStateNotConnected via the session's MCSessionDelegate didChangeState handler. Additionally, the MCSession's connectedPeers array no longer has my peers.
Two questions: Why? and How do i keep the MCSession from disconnecting?
This is a bug, which I just reported to Apple. The docs claim the didReceiveCertificate callback is optional, but it's not. Add this method to your MCSessionDelegate:
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
certificateHandler(YES);
}
The random disconnects should cease.
UPDATE After using a support ticket to Apple, they confirmed that calling sendData too often and with too much data can cause disconnects.
I have had disconnects when hitting break points and when backgrounding. Since the break points won't happen on the app store, you need to handle the backgrounding case by beginning a background task when your app is about to enter the background. Then end this task when your app comes back to the foreground. On iOS 7 this gives you about 3 background minutes which is better than nothing.
An additional strategy would be to schedule a local notification for maybe 15 seconds before your background time expires by using [[UIApplication sharedApplication] backgroundTimeRemaining], that way you can bring the user back into the app before it suspends and the multi peer framework has to be shutdown. Perhaps the local notification would warn them that their session will expire in 10 seconds or something...
If the background task expires and the app is still in the background, you have to tear down everything related to multi-peer connectivity, otherwise you will get crashes.
- (void) createExpireNotification
{
[self killExpireNotification];
if (self.connectedPeerCount != 0) // if peers connected, setup kill switch
{
NSTimeInterval gracePeriod = 20.0f;
// create notification that will get the user back into the app when the background process time is about to expire
NSTimeInterval msgTime = UIApplication.sharedApplication.backgroundTimeRemaining - gracePeriod;
UILocalNotification* n = [[UILocalNotification alloc] init];
self.expireNotification = n;
self.expireNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:msgTime];
self.expireNotification.alertBody = TR(#"Text_MultiPeerIsAboutToExpire");
self.expireNotification.soundName = UILocalNotificationDefaultSoundName;
self.expireNotification.applicationIconBadgeNumber = 1;
[UIApplication.sharedApplication scheduleLocalNotification:self.expireNotification];
}
}
- (void) killExpireNotification
{
if (self.expireNotification != nil)
{
[UIApplication.sharedApplication cancelLocalNotification:self.expireNotification];
self.expireNotification = nil;
}
}
- (void) applicationWillEnterBackground
{
self.taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
{
[self shutdownMultiPeerStuff];
[[UIApplication sharedApplication] endBackgroundTask:self.taskId];
self.taskId = UIBackgroundTaskInvalid;
}];
[self createExpireNotification];
}
- (void) applicationWillEnterForeground
{
[self killExpireNotification];
if (self.taskId != UIBackgroundTaskInvalid)
{
[[UIApplication sharedApplication] endBackgroundTask:self.taskId];
self.taskId = UIBackgroundTaskInvalid;
}
}
- (void) applicationWillTerminate
{
[self killExpireNotification];
[self stop]; // shutdown multi-peer
}
You'll also want this handler in your MCSession delegate due to Apple bug:
- (void) session:(MCSession*)session didReceiveCertificate:(NSArray*)certificate fromPeer:(MCPeerID*)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
if (certificateHandler != nil) { certificateHandler(YES); }
}
There are many causes of this, and the two answers thus far are both correct in my experience. Another which you'll find in other similar questions is this: Only one peer can accept another's invitation.
So, to clarify, if you set up an app where all devices are both advertisers and browsers, any devices can freely invite any others found to join a session. However, between any two given devices, only one device can actually accept the invitation and connect to the other device. If both devices accept each others' invitations they will disconnect within a minute or less.
Note that this limitation does not prevent the desired behavior because - unlike what my intuition stated before I built my multipeer implementation - when one device accepts an invitation and connects to another device they both become connected and receive connection delegate methods and can send each other messages.
Therefore, if you are connecting devices which both browse and advertise, send invitations freely but only accept one of a pair.
The problem of only accepting one of two invitations can be solved a myriad of ways. To begin, understand that you can pass any arbitrary object or dictionary (archived as data) as the context argument in an invitation. Therefore, both devices have access to any arbitrary information about the other (and of course itself). So, you could use at least these strategies:
simply compare: the display name of the peerID. But there's no guarantee these won't be equal.
store the date your multipeer controller was initialized and use that for comparison
give each peer a UUID and send this for comparison (my technique, in which each device - indeed each user of the app on a device - has a persistent UUID it employs).
etc - any object which supports both NSCoding and compare: will do fine.
I've been having similar problems. It seems though that if I have run my app on one iOS device, and connected to another, then quit and relaunch (say when I rerun from Xcode), then I am in a situation where I get a Connected message and then a Not Connected message a little later. This was throwing me off. But looking more carefully, I can see that the Not Connected message is actually meant for a different peerId than the one that has connected.
I think the problem here is that most samples I've seen just care about the displayName of the peerID, and neglect the fact that you can get multiple peerIDs for the same device/displayName.
I am now checking the displayName first and then verifying that the peerID is the same, by doing a compare of the pointers.
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
MyPlayer *player = _players[peerID.displayName];
if ((state == MCSessionStateNotConnected) &&
(peerID != player.peerID)) {
NSLog(#"remnant connection drop");
return; // note that I don't care if player is nil, since I don't want to
// add a dictionary object for a Not Connecting peer.
}
if (player == nil) {
player = [MyPlayer init];
player.peerID = peerID;
_players[peerID.displayName] = player;
}
player.state = state;
...
I was disconnecting immediately after I accepted the connection request. Observing the state, I saw it change from MCSessionStateConnected to MCSessionStateNotConnected.
I am creating my sessions with:
[[MCSession alloc] initWithPeer:peerID]
NOT the instantiation method dealing with security certificates:
- (instancetype)initWithPeer:(MCPeerID *)myPeerID securityIdentity:(NSArray *)identity encryptionPreference:(MCEncryptionPreference)encryptionPreference
Based on Andrew's tip above, I added the delegate method
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler {
certificateHandler(YES);
}
and the disconnects stopped.

asyncsocket "connectToHost" always succeeds and never returns a fail

I am creating a socket connection with objective C, using asyncsocket. I do this using the "connectToHost" method. I am trying to handle the case where the socket connection fails. "connectToHost" is supposed to return "YES" when it connects sucessfully, and a NO otherwise. For some reason, it is always returning a yes. I even supplied a blank string as the host and it still returns yes. Any thoughts?
Thanks,
Robin
BOOL connectStatus = NO; //used to check if connection attempt succeeded
testSocket = [[AsyncSocket alloc] initWithDelegate: self];
connectStatus = [testSocket connectToHost: #"" onPort: 5000 error: nil];
if(connectStatus == NO)
{
NSLog(#"Failed to connect to socket ");
}
else {
NSLog(#"Connected to socket sucessfully, connectStatus = %d", connectStatus);
}
Per the header file:
// Once one of the accept or connect methods are called, the AsyncSocket instance is locked in
// and the other accept/connect methods can't be called without disconnecting the socket first.
// If the attempt fails or times out, these methods either return NO or
// call "onSocket:willDisconnectWithError:" and "onSockedDidDisconnect:".
If you check the source – for the love of all things holy, when working with open source software, USE THE SOURCE! –, you will see that the method you are invoking returns NO only when it fails to start the connection process. A return value of YES just says, "Okay, I'm trying to connect:
"If things go wrong, I'll let you know by calling onSocket:willDisconnectWithError: and onSocketDidDisconnect:.
"If all goes well, you'll get a onSocket:didConnectToHost:port: message, mmkay?"
Don't expect synchronous behavior from an asynchronous socket library.