Socket搭建即時通信服務器

webSecket

即時通信

  • 相關代碼Demo地址, 內附服務端代碼和iOS端聊天室測試Demo
  • 原文地址: Socket搭建即時通信服務器
  • 即時通信(Instant messaging,簡稱IM)是一個終端服務,容許兩人或多人使用網路即時的傳遞文字訊息、檔案、語音與視頻交流
  • 即時通信按使用用途分爲企業即時通信和網站即時通信
  • 根據裝載的對象又可分爲手機即時通信和PC即時通信,手機即時通信表明是短信,網站、視頻即時通信

IM通訊原理

  • 客戶端A與客戶端B如何產生通訊?客戶端A不能直接和客戶端B,由於二者相距太遠。
  • 這時就須要經過IM服務器,讓二者產生通訊.
  • 客戶端A經過socket與IM服務器產生鏈接,客戶端B也經過socket與IM服務器產生鏈接
  • A先把信息發送給IM應用服務器,而且指定發送給B,服務器根據A信息中描述的接收者將它轉發給B,一樣B到A也是這樣。
  • 通信問題: 服務器是不能主動鏈接客戶端的,只能客戶端主動鏈接服務器

即時通信鏈接原理

  • 即時通信都是長鏈接,基本上都是HTTP1.1協議,設置Connectionkeep-alive便可實現長鏈接,而HTTP1.1默認是長鏈接,也就是默認Connection的值就是keep-alive
  • HTTP分爲長鏈接和短鏈接,其實本質上是TCP鏈接,HTTP協議是應用層的協議,而TCP纔是真正的傳輸層協議, IP是網絡層協議,只有負責傳輸的這一層才須要創建鏈接
  • 例如: 急送一個快遞,HTTP協議指的那個快遞單,你寄件的時候填的單子就像是發了一個HTTP請求。而TCP協議就是中間運貨的運輸工具,它是負責運輸的,而運輸工具所行駛的路就是所謂的TCP鏈接
  • HTTP短鏈接(非持久鏈接)是指,客戶端和服務端進行一次HTTP請求/響應以後,就關閉鏈接。因此,下一次的HTTP請求/響應操做就須要從新創建鏈接。
  • HTTP長鏈接(持久鏈接)是指,客戶端和服務端創建一次鏈接以後,能夠在這條鏈接上進行屢次請求/響應操做。持久鏈接能夠設置過時時間,也能夠不設置

即時通信數據傳遞方式

目前實現即時通信的有四種方式(短輪詢、長輪詢、SSE、Websocketphp

短輪詢:

  • 每隔一小段時間就發送一個請求到服務器,服務器返回最新數據,而後客戶端根據得到的數據來更新界面,這樣就間接實現了即時通訊
  • 優勢是簡單,缺點是對服務器壓力較大,浪費帶寬流量(一般狀況下數據都是沒有發生改變的)。
  • 主要是客戶端人員寫代碼,服務器人員比較簡單,適於小型應用

長輪詢:

  • 客戶端發送一個請求到服務器,服務器查看客戶端請求的數據(服務器中數據)是否發生了變化(是否有最新數據),若是發生變化則當即響應返回,不然保持這個鏈接並按期檢查最新數據,直到發生了數據更新或鏈接超時
  • 同時客戶端鏈接一旦斷開,則再次發出請求,這樣在相同時間內大大減小了客戶端請求服務器的次數.
  • 弊端:服務器長時間鏈接會消耗資源,返回數據順序無保證,難於管理維護
  • 底層實現:在服務器的程序中加入一個死循環,在循環中監測數據的變更。當發現新數據時,當即將其輸出給瀏覽器並斷開鏈接,瀏覽器在收到數據後,再次發起請求以進入下一個週期

SSE

  • Server-sent Events服務器推送事件):爲了解決瀏覽器只可以單向傳輸數據到服務端,HTML5提供了一種新的技術叫作服務器推送事件SSE
  • SSE技術提供的是從服務器單向推送數據給瀏覽器的功能,加上配合瀏覽器主動HTTP請求,二者結合起來,實際上就實現了客戶端和服務器的雙向通訊.

WebSocket

  • 以上提到的這些解決方案中,都是利用瀏覽器單向請求服務器或者服務器單向推送數據到瀏覽器
  • 而在HTML5中,爲了增強web的功能,提供了websocket技術,它不只是一種web通訊方式,也是一種應用層協議
  • 它提供了瀏覽器和服務器之間原生的全雙工跨域通訊,經過瀏覽器和服務器之間創建websocket鏈接,在同一時刻可以實現客戶端到服務器和服務器到客戶端的數據發送

WebSocket

  • WebSocket 是一種網絡通訊協議。RFC6455 定義了它的通訊標準
  • WebSocket是一種雙向通訊協議,在創建鏈接後,WebSocket 服務器和客戶端都能主動的向對方發送或接收數據
  • WebSocket是基於HTTP協議的,或者說借用了HTTP協議來完成一部分握手(鏈接),在握手(鏈接)階段與HTTP是相同的,只不過HTTP不能服務器給客戶端推送,而WebSocket能夠

WebSocket如何工做

  • Web瀏覽器和服務器都必須實現WebSockets協議來創建和維護鏈接。
  • 因爲WebSockets鏈接長期存在,與典型的HTTP鏈接不一樣,對服務器有重要的影響
  • 基於多線程或多進程的服務器沒法適用於 WebSockets,由於它旨在打開鏈接,儘量快地處理請求,而後關閉鏈接
  • 任何實際的WebSockets服務器端實現都須要一個異步服務器

webServer

Websocket協議

協議頭: ws, 服務器根據協議頭判斷是Http仍是websockethtml

// 請求頭
     GET ws://localhost:12345/websocket/test.html HTTP/1.1
     Origin: http://localhost
     Connection: Upgrade
     Host: localhost:12345
     Sec-WebSocket-Key: JspZdPxs9MrWCt3j6h7KdQ==  
     Upgrade: websocket 
     Sec-WebSocket-Version: 13
    // Sec-WebSocket-Key: 叫「夢幻字符串」是個密鑰,只有有這個密鑰 服務器才能經過解碼認出來,這是個WB的請求,要創建TCP鏈接了!!!若是這個字符串沒有按照加密規則加密,那服務端就認不出來,就會認爲這整個協議就是個HTTP請求。更不會開TCP。其餘的字段均可以隨便設置,可是這個字段是最重要的字段,標識WB協議的一個字段
     

// 響應頭
     HTTP/1.1 101 Web Socket Protocol Handshake
     WebSocket-Location: ws://localhost:12345/websocket/test.php
     Connection: Upgrade
     Upgrade: websocket
     Sec-WebSocket-Accept: zUyzbJdkVJjhhu8KiAUCDmHtY/o= 
     WebSocket-Origin: http://localhost
     
    // Sec-WebSocket-Accept: 叫「夢幻字符串」,和上面那個夢幻字符串做用同樣。不一樣的是,這個字符串是要讓客戶端辨認的,客戶端拿到後自動解碼。而且辨認是否是一個WB請求。而後進行相應的操做。這個字段也是重中之重,不可隨便修改的。加密規則,依然是有規則的
複製代碼

WebSocket客戶端

在客戶端,沒有必要爲WebSockets使用JavaScript庫。實現WebSocketsWeb 瀏覽器將經過WebSockets對象公開全部必需的客戶端功能(主要指支持HTML5的瀏覽器)git

客戶端 API

如下 API 用於建立WebSocket對象。github

var Socket = new WebSocket(url, [protocol] );
複製代碼
  • 以上代碼中的第一個參數url, 指定鏈接的URL
  • 第二個參數protocol是可選的,指定了可接受的子協議

WebSocket屬性

如下是WebSocket對象的屬性。假定咱們使用了以上代碼建立了Socket對象web

  • Socket.readyState: 只讀屬性readyState表示鏈接狀態, 能夠是如下值
    • 0 : 表示鏈接還沒有創建
    • 1 : 表示鏈接已創建,能夠進行通訊
    • 2 : 表示鏈接正在進行關閉
    • 3 : 表示鏈接已經關閉或者鏈接不能打開。
  • Socket.bufferedAmount: 只讀屬性bufferedAmount
    • 表示已被send() 放入正在隊列中等待傳輸,可是尚未發出的UTF-8文本字節數

WebSocket事件

如下是WebSocket對象的相關事件。假定咱們使用了以上代碼建立了Socket 對象:express

事件 事件處理程序 描述
open Socket.onopen 鏈接創建時觸發
message Socket.onmessage 客戶端接收服務端數據時觸發
error Socket.onerror 通訊發生錯誤時觸發
close Socket.onclose 鏈接關閉時觸發

WebSocket方法

如下是WebSocket對象的相關方法。假定咱們使用了以上代碼建立了Socket對象:npm

方法 描述
Socket.send() 使用鏈接發送數據
Socket.close() 關閉鏈接

示例

// 客戶端
var socket = new WebSocket("ws://localhost:9090")

// 創建 web socket 鏈接成功觸發事件
socket.onopen = function () {
    // 使用send發送數據
    socket.send("發送數據")
    console.log(socket.bufferedAmount)
    alert('數據發送中')
}

// 接受服務端數據是觸發事件
socket.onmessage = function (evt) {
    var received_msg = evt.data
    alert('數據已經接受..')
}

// 斷開 websocket 鏈接成功觸發事件
socket.onclose = function () {
    alert('連接已經關閉')
    console.log(socket.readyState)
}

複製代碼

WebSocket服務端

WebSocket在服務端的實現很是豐富。Node.jsJavaC++Python 等多種語言都有本身的解決方案, 其中Node.js經常使用的有如下三種json

下面就着重研究一下Socket.IO吧, 由於別的我也不會, 哈哈哈哈......swift

Socket.IO

  • Socket.IO是一個庫,能夠在瀏覽器和服務器之間實現實時,雙向和基於事件的通訊
  • Socket.IO是一個徹底由JavaScript實現、基於Node.js、支持WebSocket的協議用於實時通訊、跨平臺的開源框架
  • Socket.IO包括了客戶端(iOS,Android)和服務器端(Node.js)的代碼,能夠很好的實現iOS即時通信技術
  • Socket.IO支持及時、雙向、基於事件的交流,可在不一樣平臺、瀏覽器、設備上工做,可靠性和速度穩定
  • Socket.IO其實是WebSocket的父集,Socket.io封裝了WebSocket和輪詢等方法,會根據狀況選擇方法來進行通信
  • 典型的應用場景如:
    • 實時分析:將數據推送到客戶端,客戶端表現爲實時計數器、圖表、日誌客戶
    • 實時通信:聊天應用
    • 二進制流傳輸:socket.io支持任何形式的二進制文件傳輸,例如圖片、視頻、音頻等
    • 文檔合併:容許多個用戶同時編輯一個文檔,並可以看到每一個用戶作出的修改

Socket.IO服務端

  • Socket.IO實質是一個庫, 因此在使用以前必須先導入Socket.IO
  • Node.js導入庫和iOS導入第三方庫性質同樣, 只不過iOS使用的是pods管理, Node.js使用npm

導入Socket.IO

// 1. 進入噹噹前文件夾
cd ...

// 2. 建立package.json文件
npm init

/// 3. 導入庫
npm install socket.io --sava
npm install express --sava
複製代碼

建立socket

  • socket本質仍是http協議,因此須要綁定http服務器,才能啓動socket服務.
  • 並且須要經過web服務器監聽端口,socket不能監聽端口,有人訪問端口才能創建鏈接,因此先建立web服務器
// 引入http模塊
var http = require('http')

// 面向express框架開發,加載express框架,方便處理get,post請求
var express = require('express')

// 建立web服務器
var server = http.Server(express)

// 引入socket.io模塊
var socketio = require('socket.io')

// 建立愛你socket服務器
var serverSocket = socketio(server)


server.listen(9090)
console.log('監聽9090')
複製代碼

創建socket鏈接

  • 服務器不須要主動創建鏈接,創建鏈接是客戶端的事情,服務器只須要監聽鏈接
  • 客戶端主動鏈接會發送connection事件,服務端只須要監聽connection事件有沒有發送,就知道客戶端有沒有主動鏈接服務器
  • Socket.IO本質是經過發送和接受事件觸發服務器和客戶端之間的通信,任何能被編輯成JSON或二進制的對象均可以傳遞
  • socket.on: 監聽事件,這個方法會有兩個參數,第一個參數是事件名稱,第二個參數是監聽事件的回調函數,監聽到連接就會執行這個回調函數
  • 監聽connection,回調函數會傳入一個鏈接好的socket,這個socket就是客戶端的socket
  • socket鏈接原理,就是客戶端和服務端經過socket鏈接,服務器有socket,客戶端也有
// 監聽客戶端有沒有鏈接成功,若是鏈接成功,服務端會發送connection事件,通知客戶端鏈接成功
// serverSocket: 服務端, clientSocket: 客戶端
serverSocket.on('connection', function (clientSocket) {
    // 創建socket鏈接成功
    console.log('創建鏈接成功')

    console.log(clientSocket)
})
複製代碼

Socket.IO客戶端

建立socket對象

建立SocketIOClient對象, 兩種建立方式api

// 第一種, SocketIOClientConfiguration: 可選參數
public init(socketURL: URL, config: SocketIOClientConfiguration = [])

// 第二種, 底層仍是使用的第一種方式建立
public convenience init(socketURL: URL, config: [String: Any]?) {
        self.init(socketURL: socketURL, config: config?.toSocketConfiguration() ?? [])
}
複製代碼
  • SocketIOClientConfiguration: 是一個數組, 等同於[SocketIOClientOption]
  • SocketIOClientOption的全部取值以下
public enum SocketIOClientOption : ClientOption {
    /// 使用壓縮的方式進行傳輸
    case compress
    /// 經過字典內容鏈接
    case connectParams([String: Any])
    /// NSHTTPCookies的數組, 在握手過程當中傳遞, Default is nil.
    case cookies([HTTPCookie])
    /// 添加自定義請求頭初始化來請求, 默認爲nil
    case extraHeaders([String: String])
    /// 將爲每一個鏈接建立一個新的connect, 若是你在從新鏈接有bug時使用.
    case forceNew(Bool)
    /// 傳輸是否使用HTTP長輪詢, 默認false
    case forcePolling(Bool)
    /// 是否使用 WebSockets. Default is `false`
    case forceWebsockets(Bool)
    /// 調度handle的運行隊列, 默認在主隊列
    case handleQueue(DispatchQueue)
    /// 是否打印調試信息. Default is false
    case log(Bool)
    /// 可自定義SocketLogger調試日誌
    case logger(SocketLogger)
    /// 自定義服務器使用的路徑.
    case path(String)
    /// 連接失敗時, 是否從新連接, Default is `true`
    case reconnects(Bool)
    /// 從新鏈接多少次. Default is `-1` (無限次)
    case reconnectAttempts(Int)
    /// 等待重連時間. Default is `10`
    case reconnectWait(Int)
    /// 是否使用安全傳輸, Default is false
    case secure(Bool)
    /// 設置容許那些證書有效
    case security(SSLSecurity)
    /// 自簽名只能用於開發模式
    case selfSigned(Bool)
    /// NSURLSessionDelegate 底層引擎設置. 若是你須要處理自簽名證書. Default is nil.
    case sessionDelegate(URLSessionDelegate)
}
複製代碼

建立SocketIOClient

// 注意協議:ws開頭
guard let url = URL(string: "ws://localhost:9090") else { return }
let manager = SocketManager(socketURL: url, config: [.log(true), .compress])
// SocketIOClient
let socket = manager.defaultSocket
複製代碼

監聽鏈接

  • 建立好socket對象,而後鏈接用connect方法
  • 由於socket須要進行3次握手,不可能立刻建議鏈接,須要監聽是否鏈接成功的回調,使用on方法
  • ON方法兩個參數
    • 參數一: 監聽的事件名稱,參數二:監聽事件回調函數,會自動調用
    • 回調函數也有兩個參數(參數一:服務器傳遞的數據 參數二:確認請求數據ACK)
    • TCP/IP協議中,若是接收方成功的接收到數據,那麼會回覆一個ACK數據- ACK只是一個標記,標記是否成功傳輸數據
// 回調閉包
public typealias NormalCallback = ([Any], SocketAckEmitter) -> ()

// on方法
@discardableResult
open func on(_ event: String, callback: @escaping NormalCallback) -> UUID

// SocketClientEvent: 接受枚舉類型的on方法
@discardableResult
open func on(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID {
    // 這裏調用的是上面的on方法
    return on(event.rawValue, callback: callback)
}
複製代碼

完整代碼

guard let url = URL(string: "ws://localhost:9090") else { return }

let manager = SocketManager(socketURL: url, config: [.log(true), .compress])
let socket = manager.defaultSocket

// 監聽連接成功
socket.on(clientEvent: .connect) { (data, ack) in
    print("連接成功")
    print(data)
    print(ack)
}
        
socket.connect()
複製代碼

SocketIO事件

SocketIO經過事件連接服務器和傳遞數據

客戶端監聽事件

// 監聽連接成功
socket.on(clientEvent: .connect) { (data, ack) in
    print("連接成功")
    print(data)
    print(ack)
}
複製代碼

客戶端發送事件

只有鏈接成功以後,才能發送事件

// 創建一個鏈接到服務器. 鏈接成功會觸發 "connect"事件
open func connect()

// 鏈接到服務器. 若是鏈接超時,會調用handle
open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?)

// 重開一個斷開鏈接的socket
open func disconnect()

// 向服務器發送事件, 參數一: 事件的名稱,參數二: 傳輸的數據組
open func emit(_ event: String, with items: [Any])
複製代碼

服務器監聽事件

  • 監聽客戶端事件,須要嵌套在鏈接好的connect回調函數中
  • 必須使用回調函數的socket參數,如function(s)中的s,監聽事件,所以這是客戶端的socket,確定監聽客戶端發來的事件
  • 服務器監聽鏈接的回調函數的參數能夠添加多個,具體看客戶端傳遞數據數組有幾個,每一個參數都是與客戶段一一對應,第一個參數對應客戶端數組第0個數據
// 監聽socket鏈接
socket.on('connection',function(s){

    console.log('監聽到客戶端鏈接');

    // data:客戶端數組第0個元素
    // data1:客戶端數組第1個元素
    s.on('chat',function(data,data1){

        console.log('監聽到chat事件');

        console.log(data,data1);
        
    });
});
複製代碼

服務器發送事件

這裏的socket必定要用服務器端的socket

// 給當前客戶端發送數據,其餘客戶端收不到.
socket.emit('chat', '服務器' + data)

// 發給全部客戶端,不包含當前客戶端
socket.emit.broadcast.emit('chat', '發給全部客戶端,不包含當前客戶端' + data)

// 發給全部客戶端,包含當前客戶端
socket.emit.sockets.emit('chat', '發給全部客戶端,包含當前客戶端' + data)
複製代碼

SocketIO分組

  • 每個客戶端和服務器只會保持一個socket連接, 那麼怎麼吧每一條信息推送到對應的聊天室, 針對多個聊天室的問題有如何解決
  • 給每一個聊天室都分組, 服務器就能夠給指定的組進行數據的推送, 就不會影響到其餘的聊天室

如何分組

  • socket.io提供rooms和namespace的API
  • rooms的API就能夠實現多房間聊天了,總結出來無外乎就是:join/leave roomsay to room
  • 這裏的socket是客戶端的socket,也就是鏈接成功,傳遞過來的socket
// join和leave
io.on('connection', function(socket){
  socket.join('some room');
  // socket.leave('some room');
});
 
// say to room
io.to('some room').emit('some event'):
io.in('some room').emit('some event'):
複製代碼

分組的原理

  • 只要客戶端socket調用join,服務器就會把客戶端socket和分組的名稱綁定起來
  • 到時候就能夠根據分組的名稱找到對應客戶端的socket,就能給指定的客戶端推送信息
  • 一個客戶端socket只能添加到一組,離開的時候,要記得移除

歡迎您掃一掃下面的微信公衆號,訂閱個人博客!

微信公衆號
相關文章
相關標籤/搜索