WebSocket STOMP协议iOS端实现,SocketRocket,StompKit,StompClientLib

一 : Stomp

HTTP处在应用层,而WebSocket处在TCP上,并且内容不多,是一个消息架构,不包含特定的解释协议,所以还得有专门的协议来解释消息,有很多,Stomp是其中之一.

stomp以帧来封装消息,一个帧由一个命令,加上header(可以是多个),再加上body(文本或二进制),组装出来的是一段字符串.

命令的类型:
CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT

例如发送一个消息

SEND
destination:/queue/a
content-type:text/plain

hello queue a
^@

订阅消息

SUBSCRIBE
id:0
destination:/queue/foo
ack:client

^@

二 : SocketRocket

SocketRocket是Facebook维护的iOS和mac os 上的webSocket库,是OC实现的,是比较推荐的一个.
1.建立连接
Springboot基于STOMP实现的webSocket可以将http模拟成Socket,所以建立连接的url可能是一个"http://"

在iOS端SocketRocket库也可以支持STOMP.

pod 'SocketRocket'

var request = URLRequest.init(url: .init(string: "")!, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
//给request header添加一些key-value
request.setValue("", forHTTPHeaderField: "")
socket = SRWebSocket.init(urlRequest: request)

url可以是http(或者ws;wss)://域名(或者IP):端口/路径(/websocket)
最后可以加上"/websocket"强制使用websocket协议
例如http://test.com:9090/test/websocket

2.但是SocketRocket并没有实现Stomp协议的相关API,所以如果需要发送帧,就需要手动拼写frame;
就是命令 + 换行 + headerkey + : + headerValue + ... + 换行 + body + 终止字符
同样接受到的消息也是一个Frame,也需要解析

    private func sendFrame(command: String?, header: [String: String]?, body: AnyObject?) {
        var frameString = ""
        if command != nil {
            frameString = command! + "\n"
        }
        if let header = header {
            for (key, value) in header {
                frameString += key
                frameString += ":"
                frameString += value
                frameString += "\n"
            }
        }
        if let body = body as? String {
            frameString += "\n"
            frameString += body
        } else if let _ = body as? NSData {
            
        }
        if body == nil {
            frameString += "\n"
        }
        frameString += String(format: "%C", arguments: [0x00])
        if socket?.readyState == .OPEN {
            do{
                try self.socket?.send(string: frameString)
            }catch{}
        }
    }

三:StompKit

StompKit提供了封装和解析Frame的方法;以及CONNECT SUBSCRIBE ACK等命令的方法;
StompKit本身是基于CocoaAsyncSocket的,是纯OC的

1.预定义


#define kCommandAbort       @"ABORT"
#define kCommandAck         @"ACK"
#define kCommandBegin       @"BEGIN"
#define kCommandCommit      @"COMMIT"
#define kCommandConnect     @"CONNECT"
#define kCommandConnected   @"CONNECTED"
#define kCommandDisconnect  @"DISCONNECT"
#define kCommandError       @"ERROR"
#define kCommandMessage     @"MESSAGE"
#define kCommandNack        @"NACK"
#define kCommandReceipt     @"RECEIPT"
#define kCommandSend        @"SEND"
#define kCommandSubscribe   @"SUBSCRIBE"
#define kCommandUnsubscribe @"UNSUBSCRIBE"

#pragma mark Control characters

#define kLineFeed @"\x0A"
#define kNullChar @"\x00"
#define kHeaderSeparator @":"

2.构造Frame

- (NSString *)toString {
    NSMutableString *frame = [NSMutableString stringWithString: [self.command stringByAppendingString:kLineFeed]];
    for (id key in self.headers) {
        [frame appendString:[NSString stringWithFormat:@"%@%@%@%@", key, kHeaderSeparator, self.headers[key], kLineFeed]];
    }
    [frame appendString:kLineFeed];
    if (self.body) {
        [frame appendString:self.body];
    }
    [frame appendString:kNullChar];
    return frame;
}

解析Frame

+ (STOMPFrame *) STOMPFrameFromData:(NSData *)data {
    NSData *strData = [data subdataWithRange:NSMakeRange(0, [data length])];
    NSString *msg = [[NSString alloc] initWithData:strData encoding:NSUTF8StringEncoding];
    LogDebug(@"<<< %@", msg);
    NSMutableArray *contents = (NSMutableArray *)[[msg componentsSeparatedByString:kLineFeed] mutableCopy];
    while ([contents count] > 0 && [contents[0] isEqual:@""]) {
        [contents removeObjectAtIndex:0];
    }
    NSString *command = [[contents objectAtIndex:0] copy];
    NSMutableDictionary *headers = [[NSMutableDictionary alloc] init];
    NSMutableString *body = [[NSMutableString alloc] init];
    BOOL hasHeaders = NO;
    [contents removeObjectAtIndex:0];
    for(NSString *line in contents) {
        if(hasHeaders) {
            for (int i=0; i < [line length]; i++) {
                unichar c = [line characterAtIndex:i];
                if (c != '\x00') {
                    [body appendString:[NSString stringWithFormat:@"%c", c]];
                }
            }
        } else {
            if ([line isEqual:@""]) {
                hasHeaders = YES;
            } else {
                NSMutableArray *parts = [NSMutableArray arrayWithArray:[line componentsSeparatedByString:kHeaderSeparator]];
                // key ist the first part
                NSString *key = parts[0];
                [parts removeObjectAtIndex:0];
                headers[key] = [parts componentsJoinedByString:kHeaderSeparator];
            }
        }
    }
    return [[STOMPFrame alloc] initWithCommand:command headers:headers body:body];
}

3.订阅的同时维护一个字典(subscriptions)来存储频道和收到消息的回调block

- (STOMPSubscription *)subscribeTo:(NSString *)destination
                           headers:(NSDictionary *)headers
                    messageHandler:(STOMPMessageHandler)handler {
    NSMutableDictionary *subHeaders = [[NSMutableDictionary alloc] initWithDictionary:headers];
    subHeaders[kHeaderDestination] = destination;
    NSString *identifier = subHeaders[kHeaderID];
    if (!identifier) {
        identifier = [NSString stringWithFormat:@"sub-%d", idGenerator++];
        subHeaders[kHeaderID] = identifier;
    }
    self.subscriptions[identifier] = handler;
    [self sendFrameWithCommand:kCommandSubscribe
                       headers:subHeaders
                          body:nil];
    return [[STOMPSubscription alloc] initWithClient:self identifier:identifier];
}

四 : WebsocketStompKit

WebsocketStompKit是用Jetfire为基础,然后再结合StompKit的思路来封装Frame,和connect,subscribe等操作
Jetfire还有一个swift版本叫starscream,不过Jetfire用的比较少

五 : StompClientLib

基于socketRocket然后封装了stomp协议相关操作的库,并且stomp部分是用swift实现的.
不过代码比较旧,而且个人认为有很多不太好的逻辑;
不过实质就是对STOMP协议的封装和解析,没有很多代码,这个库就一个文件;
所以建议直接放到项目里,根据实际业务直接魔改.

连接

var socketClient = StompClientLib()
let url = NSURL(string: "your-socket-url-is-here")!
socketClient.openSocketWithURLRequest(request: NSURLRequest(url: url as URL) , delegate: self)

订阅

let destination = "/topic/your_topic"
let ack = destination
let id = destination
let header = ["destination": destination, "ack": ack, "id": id]

// subscribe
socketClient?.subscribeWithHeader(destination: destination, withHeader: header)

// unsubscribe
socketClient?.unsubscribe(destination: subsId)

在实际使用的时候发现几个问题:

  1. func openSocket()方法判断了socketRocket在非.CLOSED的状态进入open();但是webSocket还有一个.CLOSING状态,如果做了多服务器支持,其中一台挂掉的时候,可能会长期处理这个状态,所以我也加上了;
    另外现在iOS废弃了SSL免认证的API ,所以certificateCheckEnabled我也删了
  private func openSocket() {
        if socket == nil || socket?.readyState == .CLOSED || socket?.readyState == .CLOSING{
            self.socket = SRWebSocket(urlRequest: urlRequest! as URLRequest)
            socket!.delegate = self
            socket!.open()
        }
    }

2.我觉着connection属性没有很好的发挥作用,我修改了下,在webSocketDidOpen()时设置为true;
在webSocket(_ webSocket: SRWebSocket, didCloseWithCode code: Int, reason: String?, wasClean: Bool) 设置为false;其他地方都不需要赋值;

3.在func closeSocket()中,self.socket!.close()之后,设置delegate=nil和socket=nil;
这个也是有有问题的,这样在主动断开连接之后,收不到func webSocket(_ webSocket: SRWebSocket, didCloseWithCode code: Int, reason: String?, wasClean: Bool)代理方法的回调;
所以我也修改了,结合上面的第2点.

  private func closeSocket(){
        if let delegate = delegate {
            DispatchQueue.main.async(execute: {
                delegate.stompClientDidDisconnect(client: self)
                if self.socket != nil {
                    // Close the socket
                    self.socket!.close()
                }
            })
        }
    }

4.reconnectTimer在调用stopReconnect()之后还是在执行,我直接换成了DispatchSourceTimer

     public func reconnect(request: NSURLRequest, delegate: StompClientLibDelegate, connectionHeaders:   [String: String] = [String: String](), time: Double = 1.0, exponentialBackoff: Bool = true){
        reconnectTimer = DispatchSource.makeTimerSource(flags: .strict, queue: .main)
        reconnectTimer?.schedule(deadline: .now(), repeating: time)
        reconnectTimer?.setEventHandler(handler: {
            self.reconnectLogic(request: request, delegate: delegate
                                , connectionHeaders: connectionHeaders)
        })
        reconnectTimer?.resume()
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 本片我们说下WebSocket,之前项目中有几个轮询的情况,使用基于http协议的接口,每隔几秒调用一下,感觉有点...
    Miaoz0070阅读 15,309评论 42 27
  • 1. webSocket介绍1.1. 轮询1.2. 长链接1.3. websocket 2.STOMP传输协议介绍...
    JimmyOu阅读 31,685评论 2 12
  • 长轮询与短轮询 短轮询 其实就是普通的轮询,在特定的时间间隔内,由浏览器向服务器发出HTTP请求,然后服务器返回最...
    hellomyshadow阅读 930评论 0 0
  • 以前有遇到一些服务端客户端交互问题,有时希望交互是异步的,服务器的响应是非即时的,但是http协议显然不符合我的需...
    冯行洲阅读 271评论 0 0
  • 项目工程中需要对服务端的一些硬件操作,之后等待服务端回调,思来想去只能使用websocket了。 什么是webso...
    后浪普拉斯阅读 9,264评论 1 28