Websocket

原文:微信、QQ这类 IM app 怎么做--谈谈Websocket微信开源的跨平台终端组件

目录

  1. WebSocket 使用场景
  2. WebSocket 诞生由来
  3. WebSocket 协议原理
  4. WebSocketSocket 的区别与联系
  5. iOS 平台有哪些 WebSocket 和 Socket 的开源框架
  6. iOS 平台如何实现 WebSocket 协议
  7. 总结

 

一、WebSocket 的使用场景

1、社交聊天

这类聊天 app 的特点是低延迟,高即时。即时性是要求最高的。

2、弹幕

发弹幕需要实时显示,也需要和聊天一样,需要即时。

3、多玩家游戏

4、协同编辑

现在很多开源项目都是分散在世界各地的开发者一起协同开发,此时就会用到版本控制系统,比如 GitSVN 去合并冲突。但是如果有一份文档,支持多人实时在线协同编辑,那么此时就会用到比如 WebSocket 了,它可以保证各个编辑者都在编辑同一个文档,此时不需要用到 GitSVN 这些版本控制,因为在协同编辑界面就会实时看到对方编辑了什么。

5、股票基金实时报价

如果采用的网络架构无法满足实时性,那么就会给客户带来巨大的损失。

6、体育实况更新

7、视频会议/聊天

8、基于位置的应用

越来越多的开发者借用移动设备的 GPS 功能来实现他们基于位置的网络应用。如果你一直记录用户的位置(比如运行应用来记录运动轨迹),你可以收集到更加细致化的数据。

9、在线教育

Websocket 是个不错的选择,可以视频聊天、即时聊天以及其与别人合作一起在网上讨论问题。

10、智能家居

总结:从上面我列举的这些场景来看,一个共同点就是,高实时性!

 

二、WebSocket 诞生由来

1、最开始的轮询 Polling 阶段

image

这种方式是不适合获取实时信息的,客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。客户端会轮询,有没有新消息。这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有 HttpHeader,会很耗流量,也会消耗 CPU 的利用率。

 

2、改进版的长轮询 Long polling 阶段

2

长轮询是对轮询的改进版,客户端发送 HTTP 给服务器之后,有没有新消息。如果没有新消息,就一直等待;当有新消息的时候,才会返回给客户端。在某种程度上减小了网络带宽和 CPU 利用率等问题。

但是这种方式还是有一种弊端:假设服务器端的数据更新速度很快,服务器在传送一个数据包给客户端后必须等待客户端的下一个 Get 请求到来,才能传递第二个更新的数据包给客户端,那么这样的话,客户端显示实时数据最快的时间为 2×RTT(往返时间),而且如果在网络拥塞的情况下,这个时间用户是不能接受的,比如在股市的的报价上。另外,由于 http 数据包的头部数据量往往很大(通常有 400 多个字节),但是真正被服务器需要的数据却很少(有时只有 10 个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费

 

3WebSocket 诞生

现在急需的需求是能支持客户端和服务器端的双向通信,而且协议的头部又没有 HTTPHeader 那么大。

3

上图就是 WebsocketPolling 的区别,从图中可以看到 Polling 里面客户端发送了好多 Request,而下图,只有一个Upgrade,非常简洁高效。消耗方面的比较:

4

蓝色的柱状图,是 Polling 轮询消耗的流量,这次测试,HTTP 请求和响应头信息开销总共包括 871 字节。当然每次测试不同的请求,头的开销不同。这次测试都以 871 字节的请求来测试。

**Use case A: **1,000 clients polling every second: Network throughput is (871 x 1,000) = 871,000 bytes = 6,968,000 bits per second (6.6 Mbps)
**Use case B: **10,000 clients polling every second: Network throughput is (871 x 10,000) = 8,710,000 bytes = 69,680,000 bits per second (66 Mbps)
**Use case C: **100,000 clients polling every 1 second: Network throughput is (871 x 100,000) = 87,100,000 bytes = 696,800,000 bits per second (665 Mbps)

WebsocketFramejust two bytes of overhead instead of 871,仅仅用 2 个字节就代替了轮询的 871 字节!

**Use case A: **1,000 clients receive 1 message per second: Network throughput is (2 x 1,000) = 2,000 bytes = 16,000 bits per second (0.015 Mbps)
**Use case B: **10,000 clients receive 1 message per second: Network throughput is (2 x 10,000) = 20,000 bytes = 160,000 bits per second (0.153 Mbps)
**Use case C: **100,000 clients receive 1 message per second: Network throughput is (2 x 100,000) = 200,000 bytes = 1,600,000 bits per second (1.526 Mbps)

相同的每秒客户端轮询的次数,当次数高达 10W/s 的高频率次数的时候,Polling 轮询需要消耗 665Mbps,而 Websocket 仅仅只花费了 1.526Mbps,将近 435 倍。

 

三、谈谈 WebSocket 协议原理

Websocket 是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。

“它必须依赖 HTTP 协议进行一次握手”

其他的 Socket 都是通过 ip + 端口连接,但是客户端写死 ip 和端口是很不明智的,通过域名解析在客户端与服务端之间建立桥梁,服务端就能更加灵活。WebSocket 里能通过 url 建立连接,http 一次握手有可能是用来域名解析用的。(待验证

Websocket 的数据传输是以 frame 形式传输的,比如会将一条消息分为几个 frame,按照先后顺序传输出去。这样做会有几个好处:

  1. 大数据的传输可以分片传输,不用考虑到数据大小导致的长度标志位不足够的情况。
  2. httpchunk 一样,可以边生成数据边传递消息,即提高传输效率。
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+



    FIN      1bit 表示信息的最后一帧,flag,也就是标记符
    RSV 1-3  1bit each 以后备用的 默认都为 0
    Opcode   4bit 帧类型,稍后细说
    Mask     1bit 掩码,是否加密数据,默认必须置为1 (这里很蛋疼)
    Payload  7bit 数据的长度
    Masking-key      1 or 4 bit 掩码
    Payload data     (x + y) bytes 数据
    Extension data   x bytes  扩展数据
    Application data y bytes  程序数据

具体的规范,还请看官网的RFC 6455文档给出的详细定义。这里还有一个翻译版本

 

四、WebSocket 和 Socket 的区别与联系

首先 Socket 其实并不是一个协议。它工作在 OSI 模型会话层(第 5 层),是为了方便大家直接使用更底层协议(一般是 TCPUDP )而存在的一个抽象层Socket 是对 TCP/IP 协议的封装,Socket本身并不是协议,而是一个调用接口(API

5

Socket 通常也称作”套接字”,用于描述 IP 地址和端口,是一个通信链的句柄

网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个 Socket,一个 Socket 由一个 IP 地址和一个端口号唯一确定。应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

Socket 在通讯过程中,服务端监听某个端口是否有连接请求,客户端向服务端发送连接请求,服务端收到连接请求向客户端发出接收消息,这样一个连接就建立起来了。客户端和服务端也都可以相互发送消息与对方进行通讯,直到双方连接断开。

所以基于 WebSocket 和基于 Socket 都可以开发出 IM 社交聊天类的 app

 

五、iOS 平台的 WebSocket 和 Socket 开源框架

Socket 开源框架有:CocoaAsyncSocketsocketio/socket.io-client-swift
WebSocket 开源框架有:facebook/SocketRockettidwall/SwiftWebSocket

 

六、iOS 平台如何实现 WebSocket 协议

来看看 facebook/SocketRocket 的实现方法。首先这是 SRWebSocket 定义的一些成员变量

@property (nonatomic, weak) id <SRWebSocketDelegate> delegate;
/**
 A dispatch queue for scheduling the delegate calls. The queue doesn't need be a serial queue.

 If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls.
 */
@property (nonatomic, strong) dispatch_queue_t delegateDispatchQueue;
/**
 An operation queue for scheduling the delegate calls.

 If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls.
 */
@property (nonatomic, strong) NSOperationQueue *delegateOperationQueue;
@property (nonatomic, readonly) SRReadyState readyState;
@property (nonatomic, readonly, retain) NSURL *url;
@property (nonatomic, readonly) CFHTTPMessageRef receivedHTTPHeaders;
// Optional array of cookies (NSHTTPCookie objects) to apply to the connections
@property (nonatomic, copy) NSArray<NSHTTPCookie *> *requestCookies;

// This returns the negotiated protocol.
// It will be nil until after the handshake completes.
@property (nonatomic, readonly, copy) NSString *protocol;

SRWebSocket 的一些方法:

// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol.
- (instancetype)initWithURLRequest:(NSURLRequest *)request;
- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray<NSString *> *)protocols;
- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray<NSString *> *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;

// Some helper constructors.
- (instancetype)initWithURL:(NSURL *)url;
- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray<NSString *> *)protocols;
- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray<NSString *> *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;

// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;

// SRWebSockets are intended for one-time-use only.  Open should be called once and only once.
- (void)open;
- (void)close;
- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;

///--------------------------------------
#pragma mark Send
///--------------------------------------

//下面是4个发送的方法
/**
 Send a UTF-8 string or binary data to the server.

 @param message UTF-8 String or Data to send.

 @deprecated Please use `sendString:` or `sendData` instead.
 */
- (void)send:(id)message __attribute__((deprecated("Please use `sendString:` or `sendData` instead.")));
- (void)sendString:(NSString *)string;
- (void)sendData:(NSData *)data;
- (void)sendPing:(NSData *)data;

@end

5 种状态的代理方法:

///--------------------------------------
#pragma mark - SRWebSocketDelegate
///--------------------------------------
@protocol SRWebSocketDelegate <NSObject>

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message;

@optional
- (void)webSocketDidOpen:(SRWebSocket *)webSocket;
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;

// Return YES to convert messages sent as Text to an NSString. Return NO to skip NSData -> NSString conversion for Text messages. Defaults to YES.
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket;
@end

didReceiveMessage 方法是必须实现的,用来接收消息的。下面 4did 方法分别对应着 OpenFailCloseReceivePong 不同状态的代理方法。

方法就上面这些了,实际来看看代码怎么写

{
    /* 先初始化 Websocket 连接,注意此处 ws:// 或者 wss:// 连接有且最多只能有一个,这个是 Websocket 协议规定的  */
    NSURL * url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%zd/ws", serverProto, serverIP, serverPort]];

    self.ws = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:url]];
    self.ws.delegate = delegate;
    [self.ws open];

    // 发送消息
    [self.ws send:message];
}

/// 接收消息以及其他 3 个代理方法
/**
 * @brief  接受消息。这里接受服务器返回的数据,方法里面就应该写处理数据,存储数据的方法。
 */
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
    NSDictionary * data = [NetworkUtils decodeData:message];
    if (!data)
        return;
}

/**
 * @brief  Websocket 刚刚 Open
 * @discussion  就像微信刚刚连接中,会显示连接中,当连接上了,就不显示连接中了,取消显示连接的方法就应该写在这里面
 */
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
    // Open = silent ping
    [self.ws receivedPing];
}

/**
 *  @brief  关闭 Websocket
 */ 
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
    [self failedConnection:NSLS(Disconnected)];
}

/**
 * @brief  连接 Websocket 失败,这里面一般都会写重连的方法
 */
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
    [self failedConnection:NSLS(Disconnected)];
}

 

七、问题

对于 websocket 的优点上面解释得很清楚。

1、它的缺点呢?

其中一个:因为其连接的 persistent(保持)特点,必然占据大量的资源(服务器以及客户端),因此对于并发量大的情况下,资源的消耗是很可观的。这个需要根据实际情况抉择,损耗相对于优点来说,是否可以忽略。

2webSocket 心跳,怎么处理?

每隔几秒发一次,和 socket 差不多。

3Websocket 实现视频通话,语音通话,有什么好的解决方案吗?

可以看看 webRTC

4websocket 可以正常连接,不过连接成功两分钟后就会断掉,后台没有提供心跳接口,像这样的问题应该怎么处理?目前临时处理是断开后置空 websocket,然后重新连接,感觉这样太不好了,而且会有较低概率出现闪退,回复是由于 NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");

要搞清楚为什么会断开,是服务器的问题还是客户端的问题。

一般都是需要用心跳来维持的,直接置空确实是有闪退的危险的。

自定义了心跳机制,服务器会周期性的发心跳包,客户端收到心跳后,再把心跳发给服务器,服务器和客户端以此来判断是否处于链接状态,如果服务器在连续 3 个周期没有收到心跳包的话,可以认为客户端丢失,可以断开链接。

5、断网重连问题:在断网后 WebSocket 也会断开,然后用了一个监听网络状态的库,如果网络重新打开了,进行重连,但也会出现 NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");

WebSocket 记得是有重新连接的机制的。单独去监听的话,需要拿到之前那个连接,不然会出现这个错误。

SRWebSocketdidFailWithErrordidCloseWithCode 的回调,可以在这两个回调里边进行重连。

6、还有一种情况,如果网络正常,服务器崩了,我应该怎么处理呢?是直接断开,还是定时重连呢?

服务器崩了,应该先定时重连,尝试 3 次以后就断开。这样做才比较合理,QQ 和微信都是这么做。

 

You may also like...