Socket搭建即時通訊伺服器

RunTitan發表於2018-11-16

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、Websocket

短輪詢:

  • 每隔一小段時間就傳送一個請求到伺服器,伺服器返回最新資料,然後客戶端根據獲得的資料來更新介面,這樣就間接實現了即時通訊
  • 優點是簡單,缺點是對伺服器壓力較大,浪費頻寬流量(通常情況下資料都是沒有發生改變的)。
  • 主要是客戶端人員寫程式碼,伺服器人員比較簡單,適於小型應用

長輪詢:

  • 客戶端傳送一個請求到伺服器,伺服器檢視客戶端請求的資料(伺服器中資料)是否發生了變化(是否有最新資料),如果發生變化則立即響應返回,否則保持這個連線並定期檢查最新資料,直到發生了資料更新或連線超時
  • 同時客戶端連線一旦斷開,則再次發出請求,這樣在相同時間內大大減少了客戶端請求伺服器的次數.
  • 弊端:伺服器長時間連線會消耗資源,返回資料順序無保證,難於管理維護
  • 底層實現:在伺服器的程式中加入一個死迴圈,在迴圈中監測資料的變動。當發現新資料時,立即將其輸出給瀏覽器並斷開連線,瀏覽器在收到資料後,再次發起請求以進入下一個週期

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還是websocket

// 請求頭
     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的瀏覽器)

客戶端 API

以下 API 用於建立WebSocket物件。

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

WebSocket屬性

以下是WebSocket物件的屬性。假定我們使用了以上程式碼建立了Socket物件

  • Socket.readyState: 只讀屬性readyState表示連線狀態, 可以是以下值
    • 0 : 表示連線尚未建立
    • 1 : 表示連線已建立,可以進行通訊
    • 2 : 表示連線正在進行關閉
    • 3 : 表示連線已經關閉或者連線不能開啟。
  • Socket.bufferedAmount: 只讀屬性bufferedAmount
    • 表示已被send() 放入正在佇列中等待傳輸,但是還沒有發出的UTF-8文字位元組數

WebSocket事件

以下是WebSocket物件的相關事件。假定我們使用了以上程式碼建立了Socket 物件:

事件 事件處理程式 描述
open Socket.onopen 連線建立時觸發
message Socket.onmessage 客戶端接收服務端資料時觸發
error Socket.onerror 通訊發生錯誤時觸發
close Socket.onclose 連線關閉時觸發

WebSocket方法

以下是WebSocket物件的相關方法。假定我們使用了以上程式碼建立了Socket物件:

方法 描述
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常用的有以下三種

下面就著重研究一下Socket.IO吧, 因為別的我也不會, 哈哈哈哈......

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物件, 兩種建立方式

// 第一種, 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只能新增到一組,離開的時候,要記得移除

相關文章