即時通訊
- 相關程式碼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協議,設定
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
伺服器推送事件):為了解決瀏覽器只能夠單向傳輸資料到服務端,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
伺服器端實現都需要一個非同步伺服器
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
常用的有以下三種
下面就著重研究一下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'):
複製程式碼
分組的原理
- 只要客戶端
socket
呼叫join
,伺服器就會把客戶端socket
和分組的名稱繫結起來 - 到時候就可以根據分組的名稱找到對應客戶端的
socket
,就能給指定的客戶端推送資訊 - 一個客戶端
socket
只能新增到一組,離開的時候,要記得移除