WebSocket STOMP協(xié)議iOS端實現(xiàn),SocketRocket,StompKit,StompClientLib

一 : Stomp

HTTP處在應(yīng)用層,而WebSocket處在TCP上,并且內(nèi)容不多,是一個消息架構(gòu),不包含特定的解釋協(xié)議,所以還得有專門的協(xié)議來解釋消息,有很多,Stomp是其中之一.

stomp以幀來封裝消息,一個幀由一個命令,加上header(可以是多個),再加上body(文本或二進(jìn)制),組裝出來的是一段字符串.

命令的類型:
CONNECT簇抵、SEND吉殃、SUBSCRIBE偎血、UNSUBSCRIBE哪亿、BEGIN、COMMIT、ABORT、ACK、NACK确买、DISCONNECT

例如發(fā)送一個消息

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

hello queue a
^@

訂閱消息

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

^@

二 : SocketRocket

SocketRocket是Facebook維護(hù)的iOS和mac os 上的webSocket庫,是OC實現(xiàn)的,是比較推薦的一個.
1.建立連接
Springboot基于STOMP實現(xiàn)的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協(xié)議
例如http://test.com:9090/test/websocket

2.但是SocketRocket并沒有實現(xiàn)Stomp協(xié)議的相關(guān)API,所以如果需要發(fā)送幀,就需要手動拼寫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.預(yù)定義


#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.構(gòu)造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.訂閱的同時維護(hù)一個字典(subscriptions)來存儲頻道和收到消息的回調(diào)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為基礎(chǔ),然后再結(jié)合StompKit的思路來封裝Frame,和connect,subscribe等操作
Jetfire還有一個swift版本叫starscream,不過Jetfire用的比較少

五 : StompClientLib

基于socketRocket然后封裝了stomp協(xié)議相關(guān)操作的庫,并且stomp部分是用swift實現(xiàn)的.
不過代碼比較舊,而且個人認(rèn)為有很多不太好的邏輯;
不過實質(zhì)就是對STOMP協(xié)議的封裝和解析,沒有很多代碼,這個庫就一個文件;
所以建議直接放到項目里,根據(jù)實際業(yè)務(wù)直接魔改.

連接

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)

在實際使用的時候發(fā)現(xiàn)幾個問題:

  1. func openSocket()方法判斷了socketRocket在非.CLOSED的狀態(tài)進(jìn)入open();但是webSocket還有一個.CLOSING狀態(tài),如果做了多服務(wù)器支持,其中一臺掛掉的時候,可能會長期處理這個狀態(tài),所以我也加上了;
    另外現(xiàn)在iOS廢棄了SSL免認(rèn)證的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屬性沒有很好的發(fā)揮作用,我修改了下,在webSocketDidOpen()時設(shè)置為true;
在webSocket(_ webSocket: SRWebSocket, didCloseWithCode code: Int, reason: String?, wasClean: Bool) 設(shè)置為false;其他地方都不需要賦值;

3.在func closeSocket()中,self.socket!.close()之后,設(shè)置delegate=nil和socket=nil;
這個也是有有問題的,這樣在主動斷開連接之后,收不到func webSocket(_ webSocket: SRWebSocket, didCloseWithCode code: Int, reason: String?, wasClean: Bool)代理方法的回調(diào);
所以我也修改了,結(jié)合上面的第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在調(diào)用stopReconnect()之后還是在執(zhí)行,我直接換成了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()
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市纱皆,隨后出現(xiàn)的幾起案子湾趾,更是在濱河造成了極大的恐慌,老刑警劉巖派草,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搀缠,死亡現(xiàn)場離奇詭異,居然都是意外死亡近迁,警方通過查閱死者的電腦和手機艺普,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鉴竭,“玉大人歧譬,你說我怎么就攤上這事〔妫” “怎么了瑰步?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長璧眠。 經(jīng)常有香客問我面氓,道長,這世上最難降的妖魔是什么蛆橡? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮掘譬,結(jié)果婚禮上泰演,老公的妹妹穿的比我還像新娘。我一直安慰自己葱轩,他們只是感情好睦焕,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布藐握。 她就那樣靜靜地躺著,像睡著了一般垃喊。 火紅的嫁衣襯著肌膚如雪猾普。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天本谜,我揣著相機與錄音初家,去河邊找鬼。 笑死乌助,一個胖子當(dāng)著我的面吹牛溜在,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播他托,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼掖肋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了赏参?” 一聲冷哼從身側(cè)響起志笼,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎把篓,沒想到半個月后纫溃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡纸俭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年皇耗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揍很。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡郎楼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出窒悔,到底是詐尸還是另有隱情呜袁,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布简珠,位于F島的核電站阶界,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏聋庵。R本人自食惡果不足惜膘融,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望祭玉。 院中可真熱鬧氧映,春花似錦、人聲如沸脱货。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至臼疫,卻和暖如春择份,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背烫堤。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工荣赶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塔逃。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓讯壶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親湾盗。 傳聞我的和親對象是個殘疾皇子伏蚊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 本片我們說下WebSocket,之前項目中有幾個輪詢的情況格粪,使用基于http協(xié)議的接口躏吊,每隔幾秒調(diào)用一下,感覺有點...
    Miaoz0070閱讀 15,313評論 42 27
  • 1. webSocket介紹1.1. 輪詢1.2. 長鏈接1.3. websocket 2.STOMP傳輸協(xié)議介紹...
    JimmyOu閱讀 31,714評論 2 12
  • 長輪詢與短輪詢 短輪詢 其實就是普通的輪詢帐萎,在特定的時間間隔內(nèi)比伏,由瀏覽器向服務(wù)器發(fā)出HTTP請求,然后服務(wù)器返回最...
    hellomyshadow閱讀 936評論 0 0
  • 以前有遇到一些服務(wù)端客戶端交互問題疆导,有時希望交互是異步的赁项,服務(wù)器的響應(yīng)是非即時的,但是http協(xié)議顯然不符合我的需...
    馮行洲閱讀 272評論 0 0
  • 項目工程中需要對服務(wù)端的一些硬件操作澈段,之后等待服務(wù)端回調(diào)悠菜,思來想去只能使用websocket了。 什么是webso...
    后浪普拉斯閱讀 9,274評論 1 28