Socket搭建即時通訊伺服器
即時通訊
- 相關程式碼 Socket/">WebSocketDemo" target="_blank" rel="nofollow,noindex">Demo地址
- 即時通訊
(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 協議,設定
Connection
為keep-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
WebSocket
websocket websocket
WebSocket
- WebSocket 是一種網路通訊協議。 RFC6455 定義了它的通訊標準
-
WebSocket
是一種雙向通訊協議,在建立連線後,WebSocket
伺服器和客戶端都能主動的向對方傳送或接收資料 -
WebSocket
是基於HTTP
協議的,或者說借用了HTTP
協議來完成一部分握手(連線),在握手(連線)階段與HTTP
是相同的,只不過HTTP
不能伺服器給客戶端推送,而WebSocket
可以
WebSocket如何工作
- Web瀏覽器和伺服器都必須實現
WebSockets
協議來建立和維護連線。 - 由於
WebSockets
連線長期存在,與典型的HTTP
連線不同,對伺服器有重要的影響 - 基於多執行緒或多程序的伺服器無法適用於
WebSockets
,因為它旨在開啟連線,儘可能快地處理請求,然後關閉連線 - 任何實際的
WebSockets
伺服器端實現都需要一個非同步伺服器
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
庫。實現 WebSockets
的 Web
瀏覽器將通過 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.js
、 Java
、 C++
、 Python
等多種語言都有自己的解決方案, 其中 Node.js
常用的有以下三種
- µWebSockets
- socket.io/" target="_blank" rel="nofollow,noindex">Socket.IO
- WebSocket-Node
下面就著重研究一下 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.IO-Client-Swift 是
iOS
使用的庫, 目前只有Swift
版本 - iOS中的使用
建立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 room
和say 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'):