h

hly

V1

2022/12/07阅读:27主题:默认主题

iOS面试- 0x01 IM的实现

使用Socket基于TCP协议实现IM

一、创建一个socket连接网络对象

// 基本内容: 1)socket对象 2)存储接收数据的列表
// 动作: 1)连接 2)写(发送)数据 3)读取数据 4)断开连接
// 状态:0)未连接 1)连接成功 2)连接中 3)断开连接 4)已经断开

@interface ChatNetwork()<GCDAsyncSocketDelegate>
{
    GCDAsyncSocket *_socket;
    
    NSMutableData *_data;
    NSString *_host;
    NSInteger _port;
}
@end

@implementation ChatNetwork

- (instancetype)init {
    self = [super init];
    if (self) {
//         初始化一个socket对象
        _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
        _data = [NSMutableData new];
    }
    return self;
}

- (void)connect {
    if (_status != ChatNetworkStatusDisconnected) {
        return;
    }
    [self setStatus:ChatNetworkStatusConnecting];
    
    NSError *error = nil;
    
    _host = OBTAIN_CHAT_HOST;
    _port = OBTAIN_CHAT_PORT;
    
//    配置主机 和 端口 以及过时时间
    if (![_socket connectToHost:_host onPort:_port withTimeout:CHAT_NETWORK_CONNECTION_TIMEOUT error:&error]) {
        [self setStatus:ChatNetworkStatusDisconnected];
        return;
    }
}

- (void)send:(NSData *)data {
    if (_status != ChatNetworkStatusConnected) {
        return;
    }
//     发送数据,即为写
    [_socket writeData:data withTimeout:CHAT_NETWORK_WRITE_TIMEOUT tag:0];
}

- (void)disconnect {
    if (_status != ChatNetworkStatusDisconnected) {
//         断开
        [_socket disconnect];
        [self setStatus:ChatNetworkStatusDisconnected];
    }
}

#pragma mark - Delegate Functions with Socket
// 连接成功回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    [sock readDataWithTimeout:-1 tag:0];
    [self setStatus:ChatNetworkStatusConnected];
}

// socket读取数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    if (data && data.length > 0) {
        [_data appendData:data];
    }
    [sock readDataWithTimeout:-1 tag:0];
    
    if ((_data.length > 0) && ([self.delegate respondsToSelector:@selector(network:onDataArrived:)])) {
        [self.delegate network:self onDataArrived:_data];
    }
    
}

// 断开连接代理
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    [self disconnect];
    if (_data.length > 0) {
        [_data replaceBytesInRange:NSMakeRange(0, _data.length) withBytes:NULL length:0];
    }
}


#pragma mark - Private Functions
- (void)setStatus:(ChatNetworkStatus)status {
    _status = status;
    switch (_status) {
        case ChatNetworkStatusDisconnected:
            if ([self.delegate respondsToSelector:@selector(networkDisconnected:)]) {
                [self.delegate networkDisconnected:self];
            }
            break;
        case ChatNetworkStatusConnecting:
            if ([self.delegate respondsToSelector:@selector(networkConnecting:)]) {
                [self.delegate networkConnecting:self];
            }
            
            break;
        case ChatNetworkStatusConnected:
            if ([self.delegate respondsToSelector:@selector(networkConnected:)]) {
                [self.delegate networkConnected:self];
            }
            break;
        default:
            NSLog(@"unknown chat socket status = %d", (int)_status);
            break;
    }
}

@end

二、根据不同的需求实现不同的IM代码

直播中的业务:

连接、断开连接
发送信息:根据业务定义类型:文本、点赞、收藏、离线
接收信息: 业务的信息类型

IM常规聊天业务

常规的IM功能,就需要对应的心跳机制。TCP的保活机制是保持TCP连接,心跳机制不仅确定TCP连接,同时也确保可以传输数据。

主要过程:

  1. 连接 和断开连接
  2. 判断如果是文本类型,直接添加到发送队列中发送,如果是音频和图片,添加到准备队列中,处理了之后再添加到发送队列中。
  3. 发送: 发送队列中的数据进行发送
  4. 注意IM中需要心跳机制。

这个过程涉及到数据的解析使用了google的proto buffer 协议。 参考:https://www.jianshu.com/p/4741a75997c9


GCDAsyncSocket源码阅读

1、 连接源码 【GCDAsyncSocket】

1》连接之前的检查 preConnectWithInterface

1、代理必须有
2、代理队列必须有
3、判断当前是否已经连接;连接之前必须保证没有俩接
4、我们的配置config是否会吃IPv4/IPv6
5、检查网卡: 检查IPv4、IPv6的网卡是否支持
6、清空读写的队列,因为上面通过了,读写队列中不应该有数据。

2》获取对应主机的信息 getInterfaceAddress4

1)获取对应你的网卡地址和端口【如果有传值的话】
2) interface没有传值,直接设置为标准的,即为默认值。
3) 判断是不是本地的,包括localhost ,loopback
4) iterface 有值,获取对一个的网卡地址和端口。 ①通过名字获取 ②通过ip地址来获取

2》查找主机的信息 lookupHost
  1. 过getaddrinfo 函数获取对应的信息,可能获取的信息数目不止一个。
  2. 如果获取失败,处理本地错误裸机价
  3. 如果获取成功,执行查找成功之后的逻辑:lookup:didSucceedWithAddress4
  1. 判断stateIndex(一个记录索引),判断ipv4/6是否符合,如果有符合,就调用connectWithAddress4;
  2. 执行连接方法,进入连接逻辑
    3)如果返回连接失败
  1. createSocket函数创建socket描述符,创建一个tcpsocket,并且设置
  2. 通过socket的描述符进行连接connectSocket ,里面调用了socket的API的方法conect。

小结: 1)判断是否符合当前连接 2)获取主机的地址信息 3)创建socket进行连接

#通过主机域名和端口以及网卡进行TCP连接。
// host: 域名或ip字符串地址,也可能是localhost、loopback 连接本地机器。
// port: 端口
// viaInterface: 可能是一个名字(eg:en1, lo0)或者ip字符串地址
// timeout 超时时间
// errPtr 错误的信息地址
// 如果有错误,赋值给errPtr,返回NO,如果真确,并且启动一个后台连接操作,返回YES,并执行有关的delegate方法。
// 因为这个类支持队列读和写,可以立即启动读写。所有的读写操作将会按照队列顺序操作。
- (BOOL)connectToHost:(NSString *)inHost
               onPort:(uint16_t)port
         viaInterface:(NSString *)inInterface
          withTimeout:(NSTimeInterval)timeout
                error:(NSError **)errPtr
{
 LogTrace();
 
 // Just in case immutable objects were passed
 NSString *host = [inHost copy];
 NSString *interface = [inInterface copy];
 
 __block BOOL result = NO;
 __block NSError *preConnectErr = nil;
 
 dispatch_block_t block = ^{ @autoreleasepool {
  
  // Check for problems with host parameter
  
  if ([host length] == 0)
  {
   NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
   preConnectErr = [self badParamError:msg];
   
   return_from_block;
  }
  
  // Run through standard pre-connect checks
  
  if (![self preConnectWithInterface:interface error:&preConnectErr])
  {
   return_from_block;
  }
  
  // We've made it past all the checks.
  // It'
s time to start the connection process.
  
        self->flags |= kSocketStarted;
  
  LogVerbose(@"Dispatching DNS lookup...");
  
  // It's possible that the given host parameter is actually a NSMutableString.
  // So we want to copy it now, within this block that will be executed synchronously.
  // This way the asynchronous lookup block below doesn'
t have to worry about it changing.
  
  NSString *hostCpy = [host copy];
  
        int aStateIndex = self->stateIndex;
  __weak GCDAsyncSocket *weakSelf = self;
  
  dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
  #pragma clang diagnostic push
  #pragma clang diagnostic warning "-Wimplicit-retain-self"
   
   NSError *lookupErr = nil;
   NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
   
   __strong GCDAsyncSocket *strongSelf = weakSelf;
   if (strongSelf == nil) return_from_block;
   
   if (lookupErr)
   {
    dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
     
     [strongSelf lookup:aStateIndex didFail:lookupErr];
    }});
   }
   else
   {
    NSData *address4 = nil;
    NSData *address6 = nil;
    
    for (NSData *address in addresses)
    {
     if (!address4 && [[self class] isIPv4Address:address])
     {
      address4 = address;
     }
     else if (!address6 && [[self class] isIPv6Address:address])
     {
      address6 = address;
     }
    }
    
    dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
     
     [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
    }});
   }
   
  #pragma clang diagnostic pop
  }});
  
  [self startConnectTimeout:timeout];
  
  result = YES;
 }};
 
 if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
  block();
 else
  dispatch_sync(socketQueue, block);
 
 
 if (errPtr) *errPtr = preConnectErr;
 return result;
}

写(发送)的源码 【GCDAsyncWritePacket】

  1. 外层调用writeData: 创建GCDAsyncWritePacket对象添加到writeQueue队列中, 然后调用maybeDequeueWrite
  2. maybeDequeueWrite: 判断当前没有写的操作,然后调用doWriteData写谁的处理
  3. doWriteData: 通过socket描述进行写,
  1. 如果完成全部写:标识写完成,继续写队列中的写一个
  2. 如果写了一部分,调用代理,告诉用户写了哪些
  3. 如果失败,处理socket的错误方法close
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
{
 if ([data length] == 0) return;
 
    //构建写包对象
 GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag];
 
 dispatch_async(socketQueue, ^{ @autoreleasepool {
  
  LogTrace();
  
        if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites))
  {
            // 写队列中添加包
            [self->writeQueue addObject:packet];
            // 队列中写谁
   [self maybeDequeueWrite];
  }
 }});
 
 // Do not rely on the block being run in order to release the packet,
 // as the queue might get released without the block completing.
}
// 原生的socket写的操作关键代码
else
 {
  // 
  // Writing data directly over raw socket
  // 进入到这里:socket写基本的原生数据
  
  int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
  
  const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;
  
  NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;
  
  if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
  {
   bytesToWrite = SIZE_MAX;
  }
  
  ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); // 传入socketFD写缓存的数据,返回已经完成写的数据的长度。
  LogVerbose(@"wrote to socket = %zd", result);
  
  // Check results
  if (result < 0)
  {
   if (errno == EWOULDBLOCK)
   {
    waiting = YES;
   }
   else
   {
    error = [self errorWithErrno:errno reason:@"Error in write() function"];
   }
  }
  else
  {
   bytesWritten = result;
  }
 }
  
  
  if (done) // 当前的数据写完,处理写完的操作
 {
  [self completeCurrentWrite]; // 完成当前操作
  
  if (!error)
  {
   dispatch_async(socketQueue, ^{ @autoreleasepool{
    [self maybeDequeueWrite]; // 重新写下一个数据
   }});
  }
 }
 else
  // 没有写完, 调代理回去给调用者,告诉用户已经写了多少数据。
  // 很可能我们的数据比较大,没有完全 传输玩完成,这个返回去给用户处理
 {
  if (bytesWritten > 0)
  {
   // We're not done with the entire write, but we have written some bytes
   
   __strong id<GCDAsyncSocketDelegate> theDelegate = delegate;

   if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)])
   {
    long theWriteTag = currentWrite->tag;
    
    dispatch_async(delegateQueue, ^{ @autoreleasepool {
     
     [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag];
    }});
   }
  }
 }

读取数据 【GCDAsyncReadPacket】

  1. 连接之后,开始触发读书数据readDataWithTimeout
  2. didReadData,这里获取到数据之后,读取下一个数据

readDataWithTimeout

1)添加到读队列中 2) 在读队列中开始读取

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    [sock readDataWithTimeout:-1 tag:0]; // 读取数据调用
    [self setStatus:ChatNetworkStatusConnected];
}

// socket读取数据返回的代理
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    if (data && data.length > 0) {
        [_data appendData:data];
    }
    [sock readDataWithTimeout:-1 tag:0]; // 读取下一个
    
    if ((_data.length > 0) && ([self.delegate respondsToSelector:@selector(network:onDataArrived:)])) {
        [self.delegate network:self onDataArrived:_data];
    }
}
// 继续读取数据,让它返回下一个数据
- (void)readDataWithTimeout:(NSTimeInterval)timeout
                     buffer:(NSMutableData *)buffer
               bufferOffset:(NSUInteger)offset
                  maxLength:(NSUInteger)length
                        tag:(long)tag
{
 if (offset > [buffer length]) {
  LogWarn(@"Cannot read: offset > [buffer length]");
  return;
 }
 
 GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
                                                           startOffset:offset
                                                             maxLength:length
                                                               timeout:timeout
                                                            readLength:0
                                                            terminator:nil
                                                                   tag:tag];
 
 dispatch_async(socketQueue, ^{ @autoreleasepool {
  
        if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites))
  {
            [self->readQueue addObject:packet]; // 读取的packet
   [self maybeDequeueRead]; // 读取队列中读取
  }
 }});
 
 // Do not rely on the block being run in order to release the packet,
 // as the queue might get released without the block completing.
}

##doReadData## 关键代码

1) 读取缓存中的数据
// 
 // STEP 1 - READ FROM PREBUFFER
 // 
 
 if ([preBuffer availableBytes] > 0)
 {
  // There are 3 types of read packets:
  // 
  // 1) Read all available data. 读取有效的书
  // 2) Read a specific length of data.  读取指定长度的数据
  // 3) Read up to a particular terminator. //  读取到终止的数据
  
  NSUInteger bytesToCopy;
  
  if (currentRead->term != nil)
  {
   // Read type #3 - read up to a terminator
   
   bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
  }
  else
  {
   // Read type #1 or #2
   
   bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]];
  }
  
  // Make sure we have enough room in the buffer for our read.
  
  [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy];
  
  // Copy bytes from prebuffer into packet buffer
  
  uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset +
                                                                    currentRead->bytesDone;
  
  memcpy(buffer, [preBuffer readBuffer], bytesToCopy);
  
  // Remove the copied bytes from the preBuffer
  [preBuffer didRead:bytesToCopy];
  
  LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]);
  
  // Update totals
  
  currentRead->bytesDone += bytesToCopy;
  totalBytesReadForCurrentRead += bytesToCopy;
  
  // Check to see if the read operation is done
  
  if (currentRead->readLength > 0)
  {
   // Read type #2 - read a specific length of data
   
   done = (currentRead->bytesDone == currentRead->readLength);
  }
  else if (currentRead->term != nil)
  {
   // Read type #3 - read up to a terminator
   
   // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method
   
   if (!done && currentRead->maxLength > 0)
   {
    // We're not done and there's a set maxLength.
    // Have we reached that maxLength yet?
    
    if (currentRead->bytesDone >= currentRead->maxLength)
    {
     error = [self readMaxedOutError];
    }
   }
  }
  else
  {
   // Read type #1 - read all available data
   // 
   // We're done as soon as
   // - we'
ve read all available data (in prebuffer and socket)
   // - we've read the maxLength of read packet.
   
   done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength));
  }
  
 }
 
2) 从socket中读取数据 —— 因为在缓存中没能够完全读取

读取数据的三种情况:
1> 读取所有可用数据。 —— 默认是所有有效的数据【常用】
2> 读取指定长度的数据。—— 通过传进来的bufferOffset 和 maxLength 来确定指定长度
3> 读取特定的终止符 —— 这个终止符是穿进来的

常规的返回来就读取的 1)将 prebuffer中读取数据到buffer中。 2)从socket中读取数据写到prebuffer中,让后将prebuffer中的数据写到buffer中,设置读取的数据,读取完成

关闭连接

  1. disconnect 调用内层
  2. closeWithError:nil 真实关闭连接的方法

关闭步骤:

  1. 终止连接超时定时器
  2. 终止 读、写、 重置预先缓存、释放读写流
  3. 置空Socket对象 等
  4. 调用关闭的代理

基于Websocket协议实现

WebSocket RFC : https://www.rfc-editor.org/rfc/rfc6455

1、官方的iOS13新增的websocket API


 An NSURLSessionWebSocketTask is a task that allows clients to connect to servers supporting
 WebSocket. The task will perform the HTTP handshake to upgrade the connection
 and once the WebSocket handshake is successful, the client can read and write
 messages that will be framed using the WebSocket protocol by the framework.
 
 @class NSURLSessionWebSocketTask;           /* WebSocket objects perform a WebSocket handshake with the server and can be used to send and receive WebSocket messages */


// 创建一个websocket task 对象
 // 1、url : 链接协议 ws/wss 
// 2、protocols 支持的协议
// 3、request 一个url的request
/* Creates a WebSocket task given the url. The given url must have a ws or wss scheme.
 */
- (NSURLSessionWebSocketTask *)webSocketTaskWithURL:(NSURL *)url API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/* Creates a WebSocket task given the url and an array of protocols. The protocols will be used in the WebSocket handshake to
 * negotiate a preferred protocol with the server
 * Note - The protocol will not affect the WebSocket framing. More details on the protocol can be found by reading the WebSocket RFC
 */
- (NSURLSessionWebSocketTask *)webSocketTaskWithURL:(NSURL *)url protocols:(NSArray<NSString *>*)protocols API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/* Creates a WebSocket task given the request. The request properties can be modified and will be used by the task during the HTTP handshake phase.
 * Clients who want to add custom protocols can do so by directly adding headers with the key Sec-WebSocket-Protocol
 * and a comma separated list of protocols they wish to negotiate with the server. The custom HTTP headers provided by the client will remain unchanged for the handshake with the server.
 */
- (NSURLSessionWebSocketTask *)webSocketTaskWithRequest:(NSURLRequest *)request API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));


typedef NS_ENUM(NSInteger, NSURLSessionWebSocketMessageType) { // websocket传递数据的类型
    NSURLSessionWebSocketMessageTypeData = 0, // data数据传输,除了字符串
    NSURLSessionWebSocketMessageTypeString = 1, // 字符串
} API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/* The client can create a WebSocket message object that will be passed to the send calls
 * and will be delivered from the receive calls. The message can be initialized with data or string.
 * If initialized with data, the string property will be nil and vice versa.
 */
 // websocket发送的对象
NS_SWIFT_SENDABLE
API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0))
@interface NSURLSessionWebSocketMessage : NSObject

/* Create a message with data type
 */
- (instancetype)initWithData:(NSData *)data NS_DESIGNATED_INITIALIZER;

/* Create a message with string type
 */
- (instancetype)initWithString:(NSString *)string NS_DESIGNATED_INITIALIZER;

@property (readonly) NSURLSessionWebSocketMessageType type;
@property (nullable, readonly, copy) NSData *data;
@property (nullable, readonly, copy) NSString *string;

@end

/* The WebSocket close codes follow the close codes given in the RFC
 */
 // websocket 关闭的代码
typedef NS_ENUM(NSInteger, NSURLSessionWebSocketCloseCode)
{
    NSURLSessionWebSocketCloseCodeInvalid =                             0,
    NSURLSessionWebSocketCloseCodeNormalClosure =                    1000,
    NSURLSessionWebSocketCloseCodeGoingAway =                        1001,
    NSURLSessionWebSocketCloseCodeProtocolError =                    1002,
    NSURLSessionWebSocketCloseCodeUnsupportedData =                  1003,
    NSURLSessionWebSocketCloseCodeNoStatusReceived =                 1005,
    NSURLSessionWebSocketCloseCodeAbnormalClosure =                  1006,
    NSURLSessionWebSocketCloseCodeInvalidFramePayloadData =          1007,
    NSURLSessionWebSocketCloseCodePolicyViolation =                  1008,
    NSURLSessionWebSocketCloseCodeMessageTooBig =                    1009,
    NSURLSessionWebSocketCloseCodeMandatoryExtensionMissing =        1010,
    NSURLSessionWebSocketCloseCodeInternalServerError =              1011,
    NSURLSessionWebSocketCloseCodeTLSHandshakeFailure =              1015,
} API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));


/*
 * A WebSocket task can be created with a ws or wss url. A client can also provide
 * a list of protocols it wishes to advertise during the WebSocket handshake phase.
 * Once the handshake is successfully completed the client will be notified through an optional delegate.
 * All reads and writes enqueued before the completion of the handshake will be queued up and
 * executed once the handshake succeeds. Before the handshake completes, the client can be called to handle
 * redirection or authentication using the same delegates as NSURLSessionTask. WebSocket task will also provide
 * support for cookies and will store cookies to the cookie storage on the session and will attach cookies to
 * outgoing HTTP handshake requests.
 */
 // 主要的方法,用来发送和接收信息
NS_SWIFT_SENDABLE
API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0))
@interface NSURLSessionWebSocketTask : NSURLSessionTask

/* Sends a WebSocket message. If an error occurs, any outstanding work will also fail.
 * Note that invocation of the completion handler does not
 * guarantee that the remote side has received all the bytes, only
 * that they have been written to the kernel.
 */
 // 发送信息
- (void)sendMessage:(NSURLSessionWebSocketMessage *)message completionHandler:(void (NS_SWIFT_SENDABLE ^)(NSError * _Nullable error))completionHandler;

/* Reads a WebSocket message once all the frames of the message are available.
 * If the maximumMessage size is hit while buffering the frames, the receiveMessage call will error out
 * and all outstanding work will also fail resulting in the end of the task.
 */
 // 接受信息
- (void)receiveMessageWithCompletionHandler:(void (NS_SWIFT_SENDABLE ^)(NSURLSessionWebSocketMessage * _Nullable message, NSError * _Nullable error))completionHandler;

/* Sends a ping frame from the client side. The pongReceiveHandler is invoked when the client
 * receives a pong from the server endpoint. If a connection is lost or an error occurs before receiving
 * the pong from the endpoint, the pongReceiveHandler block will be invoked with an error.
 * Note - the pongReceiveHandler will always be called in the order in which the pings were sent.
 */
 // 发送心跳
- (void)sendPingWithPongReceiveHandler:(void (NS_SWIFT_SENDABLE ^)(NSError * _Nullable error))pongReceiveHandler;

/* Sends a close frame with the given closeCode. An optional reason can be provided while sending the close frame.
 * Simply calling cancel on the task will result in a cancellation frame being sent without any reason.
 */
 //取消连接通过一个关闭的code
- (void)cancelWithCloseCode:(NSURLSessionWebSocketCloseCode)closeCode reason:(nullable NSData *)reason;

//信息的最大数据
@property NSInteger maximumMessageSize; /* The maximum number of bytes to be buffered before erroring out. This includes the sum of all bytes from continuation frames. Receive calls will error out if this value is reached */
// 关闭码
@property (readonly) NSURLSessionWebSocketCloseCode closeCode; /* A task can be queried for it's close code at any point. When the task is not closed, it will be set to NSURLSessionWebSocketCloseCodeInvalid */
// 关闭的原因
@property (nullable, readonly, copy) NSData *closeReason; /* A task can be queried for it'
s close reason at any point. A nil value indicates no closeReason or that the task is still running */

@end

// websocket 的代理方法
PI_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0))
@protocol NSURLSessionWebSocketDelegate <NSURLSessionTaskDelegate>
@optional

/* Indicates that the WebSocket handshake was successful and the connection has been upgraded to webSockets.
 * It will also provide the protocol that is picked in the handshake. If the handshake fails, this delegate will not be invoked.
 */
 // 已经打开的代理回调
- (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask didOpenWithProtocol:(nullable NSString *) protocol;

/* Indicates that the WebSocket has received a close frame from the server endpoint.
 * The close code and the close reason may be provided by the delegate if the server elects to send
 * this information in the close frame
 */
 // 已经关闭关闭的回调
- (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask didCloseWithCode:(NSURLSessionWebSocketCloseCode)closeCode reason:(nullable NSData *)reason;

@end

提供的接口包括:

  1. 一个网络任务的对象,websocketTask, 通过URL的ws/wss开头的链接创建
  2. 提供方法:连接、(取消)断开、发送消息(string、data)、接受消息
  3. 心跳机制, pingpong
  4. 代理: 已经打开连接, 已经关闭连接

SocketRocket 源码解析

可以和websoocket RFC 一起阅读

使用了NSInputStream读取数据流, 使用NSOutputStream写数据流。


https://github.com/socketio/socket.io-client-swift 支持socket.io 服务、 支持二进制、支持轮询和websockets


公众号:`技术小难`
[简书](https://www.jianshu.com/u/1851ec413025)
[博客园](https://account.cnblogs.com/blog-apply) 链接需要替换
[CSDN](https://blog.csdn.net/u012496940?spm=1000.2115.3001.5343)
[知乎](https://www.zhihu.com/people/gu-han-90-61)
[掘金](https://juejin.cn/user/1943592286824333)
[segmentfault](https://segmentfault.com/u/natqeeak/articles)

分类:

前端

标签:

计算机网络

作者介绍

h
hly
V1