Соединение с WebSocket не закрывается с помощью SocketRocket
Я использую библиотеку SocketRocket для Objective-C для подключения к 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];
}
Открытие соединения работает отлично. Делегат вызывается после установления соединения.
-(void)webSocketDidOpen:(SRWebSocket *)webSocket {
NSLog(@"WebSocket is open");
}
Но когда я хочу закрыть соединение, ничего не происходит.
-(void)close {
if( !self.webSocket )
return;
[self.webSocket close];
self.webSocket.delegate = nil;
}
Делегат для успешного закрытия соединения не вызывается. Может ли кто-нибудь сказать мне, почему это происходит?
Спасибо, что прочитали мой вопрос.
Ответы
Ответ 1
Я понял, что делегат никогда не называется, потому что веб-сайт никогда не закрывается. Закрытие websocket в SRWebSocket происходит в методе pumpWriting следующим образом:
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 ");
}
Все потоки и объект для сохранения websocket закрыты или удалены там. Пока они все еще открыты, сокет не будет закрыт соответствующим образом. Но закрытие никогда не происходило в моей программе, потому что когда я пытался закрыть websocket, _closeWhenFinishedWriting всегда был НЕТ.
Это логическое значение устанавливается только один раз в методе отключить.
- (void)_disconnect;
{
assert(dispatch_get_current_queue() == _workQueue);
SRFastLog(@"Trying to disconnect");
_closeWhenFinishedWriting = YES;
[self _pumpWriting];
}
Но при вызове метода closeWithCode в SRWebSocket отключить вызывается только в одном случае, и это значит, что websocket находится в состоянии соединения.
BOOL wasConnecting = self.readyState == SR_CONNECTING;
SRFastLog(@"Closing with code %d reason %@", code, reason);
dispatch_async(_workQueue, ^{
if (wasConnecting) {
[self _disconnect];
return;
}
Это означает, что если сокет находится в другом состоянии, websocket никогда не будет закрываться. Один способ обхода - всегда вызывать метод отключить. По крайней мере, это сработало для меня, и все, кажется, в порядке.
Если у кого-то есть идея, почему SRWebSocket реализована так, пожалуйста, оставьте комментарий для этого ответа и помогите мне.
Ответ 2
Я думаю, что это ошибка.
При вызове close сервер возвращает сообщение "закрыть".
Он получен SRWebSocket, однако _selfRetain никогда не устанавливается в nil, и сокет остается открытым (потоки не закрыты), и у нас есть утечка памяти.
Я проверил и наблюдал это в приложении тестового чата.
Я сделал следующее изменение:
-(BOOL)_innerPumpScanner {
BOOL didWork = NO;
if (self.readyState >= SR_CLOSING) {
[self _disconnect]; // <--- Added call to disconnect which releases _selfRetain
return didWork;
}
Теперь сокет закрывается, экземпляр освобождается, а утечка памяти исчезает.
Единственное, что я не уверен в том, должен ли делегат быть вызван при закрытии таким образом. Посмотрите на это.
Ответ 3
Как только конечная точка отправила и получила контрольный кадр Close, эта конечная точка ДОЛЖНА закрыть соединение WebSocket, как определено в разделе 7.1.1. (RFC 6455 7.1.2)
Экземпляр SRWebSocket здесь не _disconnect
, потому что это закроет TCP-соединение с сервером до того, как клиент получит ответный флажок Close. Фактически, _disconnect
ing здесь будет разорвать TCP-сокет, прежде чем клиент сможет даже отправить собственный сервер Close на сервер, потому что _disconnect
в конечном итоге вызывает _pumpWriting
до closeWithCode:
. Вероятно, сервер будет достаточно изящным образом реагировать, но он несоответствует, и вы не сможете отправлять уникальные коды с уникальными условиями, когда все будет настроено таким образом.
Это правильно рассмотрено в handleCloseWithData:
if (self.readyState == SR_OPEN) {
[self closeWithCode:1000 reason:nil];
}
dispatch_async(_workQueue, ^{
[self _disconnect];
});
Этот блок обрабатывает запросы "Закрыть", инициированные как клиентом, так и сервером. Если сервер отправляет первый закрытый фрейм, метод выполняется в соответствии с выложенной вами последовательностью, в конечном итоге заканчивается в _pumpWriting
через closeWithCode:
, где клиент будет отвечать своим собственным фреймом Close. Затем он продолжает срывать соединение с этим _disconnect
.
Когда клиент сначала отправляет кадр, closeWithCode:
запускается один раз, не закрывая TCP-соединение, поскольку _closeWhenFinishedWriting
по-прежнему является ложным. Это позволяет серверному времени отвечать собственным фреймом Close, что обычно приводит к запуску closeWithCode:
снова, но для следующего блока вверху этого метода:
if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) {
return;
}
Поскольку readyState изменяется на первой итерации closeWithCode:
, на этот раз он просто не будет работать.
исправление ошибки emp необходимо для того, чтобы сделать эту работу по назначению, однако: в противном случае закрывающий кадр с сервера ничего не делает. Соединение будет по-прежнему завершено, но грязно, потому что сервер (имеющий как отправленные, так и полученные его фреймы) сломает сокет на своем конце, и клиент ответит с помощью NSStreamEventEndEncountered:
, который обычно зарезервирован для ошибок потока, вызванных внезапные потери связи. Лучшим подходом было бы определить, почему кадр никогда не выходит из _innerPumpScanner
в handleCloseWithData:
. Еще одна проблема, которая стоит иметь в виду, заключается в том, что по умолчанию close
просто вызывает closeWithCode:
с кодом несоответствия RFC -1. Это отбросило ошибки на моем сервере, пока я не изменил его, чтобы отправить одно из принятых значений.
Все, что сказал: ваш метод делегирования не работает, потому что вы отключите делегат сразу после вызова close
. Все в close
находится внутри асинхронного блока; не будет делегата, которого нужно вызывать к моменту вызова didCloseWithCode:
независимо от того, что вы здесь делаете.