【iOS】—— Implement WebSocket to send messages (use and analysis of SocketRocket third-party library)


By accident, I learned that WebSocket is used to send messages between the backend and the frontend, so I checked how to write this thing in iOS, and used the interface written by my roommate to simply realize the communication between the two users.

WebSocket

  • WebSocketIs a protocol that enables two-way communication in web applications. It allows a persistent connection to be established between a client and a server so that bidirectional communication can take place at any point in time.
  • The traditional HTTPrequest -response mode only supports the client to initiate a request and the server to respond. That is, when a page is loaded, it usually initiates a HTTPrequest for certain resources, such as HTML, CSSand JavaScriptfiles . Once these resources are sent to the browser, the connection is closed unless another request is made.
  • However, for some Webapplications , more real-time interaction is required. This is the advantage offered WebSocketby : they allow an open connection between a client and a server so that both parties can send messages at any time.
  • WebSocketHandshake using HTTPthe protocol , then upgrade to WebSocketthe protocol. Once this upgrade is complete, the client and server will communicate using WebSocketthe protocol . WebSocketThis is achieved by using standard TCP/IP sockets to send and receive data over the network.
  • WebSocketIt is very practical in many scenarios, such as online games, real-time chat, stock market push, etc., which can greatly improve the interaction effect and user experience of the application.

Features of WebSocket

1. 建立在 TCP 协议之上,服务器端的实现比较容易。
2. 与 HTTP 协议有着良好的兼容性。默认端口也是80443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
3. 数据格式比较轻量,性能开销小,通信高效。
4. 可以发送文本,也可以发送二进制数据。
5. 没有同源限制,客户端可以与任意服务器通信。
6. 协议标识符是ws(如果加密,则为wss),服务器网址就是URL。`ws://example.com:80/some/path`
7. 全双工通信。

SocketRocket

The method of use is the same as the previous third-party library, using cocoapods to guide the library, the specific operation is not described in detail, and it was also written in the previous blog.

Import the header file to set the proxy

Import #import <SocketRocket/SRWebSocket.h> where needed
and follow the proxy SRWebSocketDelegate
to declare a property SRWebSocket *webSocket;

SRWebSocket initialization and connection establishment

    self.socket = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://101.43.193.62:9090/ws?id=%d", self.senderSubscriberNumber]]]];
    self.socket.delegate = self;
    NSLog(@"Opening Connection...");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    dispatch_async(queue, ^{
    
    
        [self.socket open];
    });

See here why I put [self.socket open];this code in an asynchronous concurrent queue. Before I called openthe method directly on the main thread, the compiler reported a bunch of strange things to me. After searching, it told me that it was the thread priority Caused by inversion:
In iOS, each thread has a Quality of Service (QoS) level associated with it, which is used to specify its relative priority. QOS_CLASS_USER_INTERACTIVE indicates that the thread needs to respond immediately and has the highest priority, while QOS_CLASS_DEFAULT indicates that the thread has the default priority.

The reason for this problem should be the bottom layer of this third-party library. I read some source code and found that many GCD things are called, but I haven't found the reason yet.

SRWebSocketDelegate proxy method implementation

//socket 连接成功
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
    
    
    NSLog(@"Websocket Connected");
}

//socket 连接失败
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
    
    
    NSLog(@":( Websocket Failed With Error %@", error);
    self.socket = nil;
    // 断开连接后每过1s重新建立一次连接
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        [self setSocket];
    });
}

//socket连接断开
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
    
    
    NSLog(@"WebSocket closed");
    self.socket = nil;
}

//收到消息
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
    
    
    NSLog(@"Received \"%@\"", message);
}

The four most commonly used methods respectively indicate that the connection is successful, the connection fails, the connection is disconnected, and the message is received. We can do what we should do in each protocol function.

The information sent to me by the backend is as follows:
Please add a picture description
I wanted to use a dictionary to store the message when I received it, but there was always a problem at the beginning of the conversion, and the conversion was successful after many attempts.

    NSString *messageString = (NSString *)message;
    NSData *messageData = [messageString dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *messageDictionary = [NSJSONSerialization JSONObjectWithData:messageData options:kNilOptions error:nil];

Plus simple UI for simple communication between two users

Please add a picture description

Please add a picture description

I looked at a little bit of source code (I don’t understand deeply)

1. Basic initialization method
- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates

_SR_commonInitThe underlying method is called

//初始化
- (void)_SR_commonInit;
{
    
    
    //得到url schem小写
    NSString *scheme = _url.scheme.lowercaseString;
        //如果不是这几种,则断言错误
    assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);

    _readyState = SR_CONNECTING;
    _webSocketVersion = 13;
     //初始化工作的队列,串行
    _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    
    //给队列设置一个标识,标识为指向自己的,上下文对象为这个队列
    dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
    
    //设置代理queue为主队列
    _delegateDispatchQueue = dispatch_get_main_queue();
    
    //retain主队列?
    sr_dispatch_retain(_delegateDispatchQueue);
    
    //读Buffer
    _readBuffer = [[NSMutableData alloc] init];
    //输出Buffer
    _outputBuffer = [[NSMutableData alloc] init];
    //当前数据帧
    _currentFrameData = [[NSMutableData alloc] init];
    //消费者数据帧的对象
    _consumers = [[NSMutableArray alloc] init];
    
    _consumerPool = [[SRIOConsumerPool alloc] init];
    //注册的runloop
    _scheduledRunloops = [[NSMutableSet alloc] init];
    ....省略了一部分代码
}

the whole process:

  • Determine the protocol type
  • Initialized _workQueue, this GCD queue is used to handle the main business logic, including handling errors, sending content, closing connections, etc.
  • Initialize _delegateDispatchQueue, which is used to send out notifications. We can customize this queue by - (void)setDelegateOperationQueue:(NSOperationQueue*) queueor .- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue

2. Create input and output streams

//初始化流
- (void)_initializeStreams;
{
    
    
    //断言 port值小于UINT32_MAX
    assert(_url.port.unsignedIntValue <= UINT32_MAX);
    //拿到端口
    uint32_t port = _url.port.unsignedIntValue;
    //如果端口号为0,给个默认值,http 80 https 443;
    if (port == 0) {
    
    
        if (!_secure) {
    
    
            port = 80;
        } else {
    
    
            port = 443;
        }
    }
    NSString *host = _url.host;
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    //用host创建读写stream,Host和port就绑定在一起了
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);
    
    //绑定生命周期给ARC  _outputStream = __bridge transfer
    _outputStream = CFBridgingRelease(writeStream);
    _inputStream = CFBridgingRelease(readStream);
    
    //代理设为自己
    _inputStream.delegate = self;
    _outputStream.delegate = self;
}

3. openMethod

//开始连接
- (void)open;
{
    
    
    assert(_url);
    //如果状态是正在连接,直接断言出错
    NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");

    //自己持有自己
    _selfRetain = self;
    //判断超时时长
    if (_urlRequest.timeoutInterval > 0)
    {
    
    
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, _urlRequest.timeoutInterval * NSEC_PER_SEC);
        //在超时时间执行
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    
    
            //如果还在连接,报错
            if (self.readyState == SR_CONNECTING)
                [self _failWithError:[NSError errorWithDomain:@"com.squareup.SocketRocket" code:504 userInfo:@{
    
    NSLocalizedDescriptionKey: @"Timeout Connecting to Server"}]];
        });
    }
    //开始建立连接
    [self openConnection];
}
//开始连接
- (void)openConnection;
{
    
    
    //更新安全、流配置
    [self _updateSecureStreamOptions];
    
    //判断有没有runloop
    if (!_scheduledRunloops.count) {
    
    
        //SR_networkRunLoop会创建一个带runloop的常驻线程,模式为NSDefaultRunLoopMode。
        [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];
    }
    
    //开启输入输出流
    [_outputStream open];
    [_inputStream open];
}

- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
{
    
    
    [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
    [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
    
    //添加到集合里,数组
    [_scheduledRunloops addObject:@[aRunLoop, mode]];
}

I haven’t read it since then, but I have briefly read these two or three methods, and the methods that will be adjusted later:

  • didConnect
    • Build HTTP Headers
    • Send HTTP Header
    • Register a listener that receives the header information returned by the server, and perform corresponding processing in the callback
  • - (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
    • This is the callback method of NSStream, the common callback of input and output streams
    • NSStreamEventOpenCompletedConnection open; NSStreamEventHasBytesAvailablereadable; NSStreamEventHasSpaceAvailabledata can be written
    • NSStreamEventOpenCompletedIt is used [self _pumpScanner];to trigger 3 in Article 5 to process the handshake Header information returned by the server

Guess you like

Origin blog.csdn.net/m0_62386635/article/details/130623637