webSecket
即時通訊
- 相關代碼Demo地址, 內(nèi)附服務端代碼和
iOS
端聊天室測試Demo
- 原文地址: Socket搭建即時通訊服務器
- 即時通訊
(Instant messaging,簡稱IM)
是一個終端服務,允許兩人或多人使用網(wǎng)路即時的傳遞文字訊息、檔案、語音與視頻交流 - 即時通訊按使用用途分為企業(yè)即時通訊和網(wǎng)站即時通訊
- 根據(jù)裝載的對象又可分為手機即時通訊和PC即時通訊,手機即時通訊代表是短信,網(wǎng)站、視頻即時通訊
IM通信原理
- 客戶端A與客戶端B如何產(chǎn)生通信?客戶端A不能直接和客戶端B,因為兩者相距太遠。
- 這時就需要通過IM服務器,讓兩者產(chǎn)生通信.
- 客戶端A通過socket與IM服務器產(chǎn)生連接,客戶端B也通過socket與IM服務器產(chǎn)生連接
- A先把信息發(fā)送給IM應用服務器,并且指定發(fā)送給B,服務器根據(jù)A信息中描述的接收者將它轉(zhuǎn)發(fā)給B,同樣B到A也是這樣。
- 通訊問題: 服務器是不能主動連接客戶端的,只能客戶端主動連接服務器
即時通訊連接原理
- 即時通訊都是長連接,基本上都是HTTP1.1協(xié)議,設置
Connection
為keep-alive
即可實現(xiàn)長連接,而HTTP1.1
默認是長連接,也就是默認Connection
的值就是keep-alive
- HTTP分為長連接和短連接,其實本質(zhì)上是TCP連接,HTTP協(xié)議是應用層的協(xié)議,而TCP才是真正的傳輸層協(xié)議, IP是網(wǎng)絡層協(xié)議,只有負責傳輸?shù)倪@一層才需要建立連接
- 例如: 急送一個快遞,HTTP協(xié)議指的那個快遞單,你寄件的時候填的單子就像是發(fā)了一個HTTP請求。而TCP協(xié)議就是中間運貨的運輸工具,它是負責運輸?shù)模\輸工具所行駛的路就是所謂的TCP連接
- HTTP短連接(非持久連接)是指,客戶端和服務端進行一次HTTP請求/響應之后,就關閉連接。所以,下一次的HTTP請求/響應操作就需要重新建立連接。
- HTTP長連接(持久連接)是指,客戶端和服務端建立一次連接之后,可以在這條連接上進行多次請求/響應操作。持久連接可以設置過期時間,也可以不設置
即時通訊數(shù)據(jù)傳遞方式
目前實現(xiàn)即時通訊的有四種方式(短輪詢、長輪詢、SSE、Websocket
)
短輪詢:
- 每隔一小段時間就發(fā)送一個請求到服務器,服務器返回最新數(shù)據(jù),然后客戶端根據(jù)獲得的數(shù)據(jù)來更新界面,這樣就間接實現(xiàn)了即時通信
- 優(yōu)點是簡單,缺點是對服務器壓力較大,浪費帶寬流量(通常情況下數(shù)據(jù)都是沒有發(fā)生改變的)。
- 主要是客戶端人員寫代碼,服務器人員比較簡單,適于小型應用
長輪詢:
- 客戶端發(fā)送一個請求到服務器,服務器查看客戶端請求的數(shù)據(jù)(服務器中數(shù)據(jù))是否發(fā)生了變化(是否有最新數(shù)據(jù)),如果發(fā)生變化則立即響應返回,否則保持這個連接并定期檢查最新數(shù)據(jù),直到發(fā)生了數(shù)據(jù)更新或連接超時
- 同時客戶端連接一旦斷開,則再次發(fā)出請求,這樣在相同時間內(nèi)大大減少了客戶端請求服務器的次數(shù).
- 弊端:服務器長時間連接會消耗資源,返回數(shù)據(jù)順序無保證,難于管理維護
- 底層實現(xiàn):在服務器的程序中加入一個死循環(huán),在循環(huán)中監(jiān)測數(shù)據(jù)的變動。當發(fā)現(xiàn)新數(shù)據(jù)時,立即將其輸出給瀏覽器并斷開連接,瀏覽器在收到數(shù)據(jù)后,再次發(fā)起請求以進入下一個周期
SSE
- (
Server-sent Events
服務器推送事件):為了解決瀏覽器只能夠單向傳輸數(shù)據(jù)到服務端,HTML5提供了一種新的技術叫做服務器推送事件SSE - SSE技術提供的是從服務器單向推送數(shù)據(jù)給瀏覽器的功能,加上配合瀏覽器主動HTTP請求,兩者結(jié)合起來,實際上就實現(xiàn)了客戶端和服務器的雙向通信.
WebSocket
- 以上提到的這些解決方案中,都是利用瀏覽器單向請求服務器或者服務器單向推送數(shù)據(jù)到瀏覽器
- 而在HTML5中,為了加強web的功能,提供了
websocket
技術,它不僅是一種web通信方式,也是一種應用層協(xié)議 - 它提供了瀏覽器和服務器之間原生的全雙工跨域通信,通過瀏覽器和服務器之間建立
websocket
連接,在同一時刻能夠?qū)崿F(xiàn)客戶端到服務器和服務器到客戶端的數(shù)據(jù)發(fā)送
WebSocket
- WebSocket 是一種網(wǎng)絡通信協(xié)議。RFC6455 定義了它的通信標準
-
WebSocket
是一種雙向通信協(xié)議,在建立連接后,WebSocket
服務器和客戶端都能主動的向?qū)Ψ桨l(fā)送或接收數(shù)據(jù) -
WebSocket
是基于HTTP
協(xié)議的,或者說借用了HTTP
協(xié)議來完成一部分握手(連接),在握手(連接)階段與HTTP
是相同的,只不過HTTP
不能服務器給客戶端推送,而WebSocket
可以
WebSocket如何工作
- Web瀏覽器和服務器都必須實現(xiàn)
WebSockets
協(xié)議來建立和維護連接。 - 由于
WebSockets
連接長期存在,與典型的HTTP
連接不同,對服務器有重要的影響 - 基于多線程或多進程的服務器無法適用于
WebSockets
,因為它旨在打開連接,盡可能快地處理請求,然后關閉連接 - 任何實際的
WebSockets
服務器端實現(xiàn)都需要一個異步服務器
webServer
Websocket
協(xié)議
協(xié)議頭: ws, 服務器根據(jù)協(xié)議頭判斷是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連接了!!!如果這個字符串沒有按照加密規(guī)則加密,那服務端就認不出來,就會認為這整個協(xié)議就是個HTTP請求。更不會開TCP。其他的字段都可以隨便設置,但是這個字段是最重要的字段,標識WB協(xié)議的一個字段
// 響應頭
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請求。然后進行相應的操作。這個字段也是重中之重,不可隨便修改的。加密規(guī)則,依然是有規(guī)則的
WebSocket客戶端
在客戶端,沒有必要為WebSockets
使用JavaScript
庫。實現(xiàn)WebSockets
的Web
瀏覽器將通過WebSockets
對象公開所有必需的客戶端功能(主要指支持HTML5
的瀏覽器)
客戶端 API
以下 API 用于創(chuàng)建WebSocket
對象。
var Socket = new WebSocket(url, [protocol] );
- 以上代碼中的第一個參數(shù)
url
, 指定連接的URL
- 第二個參數(shù)
protocol
是可選的,指定了可接受的子協(xié)議
WebSocket屬性
以下是WebSocket
對象的屬性。假定我們使用了以上代碼創(chuàng)建了Socket
對象
-
Socket.readyState
: 只讀屬性readyState
表示連接狀態(tài), 可以是以下值- 0 : 表示連接尚未建立
- 1 : 表示連接已建立,可以進行通信
- 2 : 表示連接正在進行關閉
- 3 : 表示連接已經(jīng)關閉或者連接不能打開。
-
Socket.bufferedAmount
: 只讀屬性bufferedAmount
- 表示已被
send()
放入正在隊列中等待傳輸,但是還沒有發(fā)出的UTF-8
文本字節(jié)數(shù)
- 表示已被
WebSocket事件
以下是WebSocket
對象的相關事件。假定我們使用了以上代碼創(chuàng)建了Socket
對象:
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 連接建立時觸發(fā) |
message | Socket.onmessage | 客戶端接收服務端數(shù)據(jù)時觸發(fā) |
error | Socket.onerror | 通信發(fā)生錯誤時觸發(fā) |
close | Socket.onclose | 連接關閉時觸發(fā) |
WebSocket方法
以下是WebSocket
對象的相關方法。假定我們使用了以上代碼創(chuàng)建了Socket
對象:
方法 | 描述 |
---|---|
Socket.send() | 使用連接發(fā)送數(shù)據(jù) |
Socket.close() | 關閉連接 |
示例
// 客戶端
var socket = new WebSocket("ws://localhost:9090")
// 建立 web socket 連接成功觸發(fā)事件
socket.onopen = function () {
// 使用send發(fā)送數(shù)據(jù)
socket.send("發(fā)送數(shù)據(jù)")
console.log(socket.bufferedAmount)
alert('數(shù)據(jù)發(fā)送中')
}
// 接受服務端數(shù)據(jù)是觸發(fā)事件
socket.onmessage = function (evt) {
var received_msg = evt.data
alert('數(shù)據(jù)已經(jīng)接受..')
}
// 斷開 websocket 連接成功觸發(fā)事件
socket.onclose = function () {
alert('鏈接已經(jīng)關閉')
console.log(socket.readyState)
}
WebSocket服務端
WebSocket
在服務端的實現(xiàn)非常豐富。Node.js
、Java
、C++
、Python
等多種語言都有自己的解決方案, 其中Node.js
常用的有以下三種
下面就著重研究一下Socket.IO
吧, 因為別的我也不會, 哈哈哈哈......
Socket.IO
- Socket.IO是一個庫,可以在瀏覽器和服務器之間實現(xiàn)實時,雙向和基于事件的通信
-
Socket.IO是一個完全由
JavaScript
實現(xiàn)、基于Node.js
、支持WebSocket
的協(xié)議用于實時通信、跨平臺的開源框架 -
Socket.IO包括了客戶端(
iOS,Android
)和服務器端(Node.js
)的代碼,可以很好的實現(xiàn)iOS即時通訊技術 - Socket.IO支持及時、雙向、基于事件的交流,可在不同平臺、瀏覽器、設備上工作,可靠性和速度穩(wěn)定
-
Socket.IO實際上是
WebSocket
的父集,Socket.io
封裝了WebSocket
和輪詢等方法,會根據(jù)情況選擇方法來進行通訊 - 典型的應用場景如:
- 實時分析:將數(shù)據(jù)推送到客戶端,客戶端表現(xiàn)為實時計數(shù)器、圖表、日志客戶
- 實時通訊:聊天應用
- 二進制流傳輸:
socket.io
支持任何形式的二進制文件傳輸,例如圖片、視頻、音頻等 - 文檔合并:允許多個用戶同時編輯一個文檔,并能夠看到每個用戶做出的修改
Socket.IO服務端
-
Socket.IO實質(zhì)是一個庫, 所以在使用之前必須先導入
Socket.IO
庫 -
Node.js
導入庫和iOS
導入第三方庫性質(zhì)一樣, 只不過iOS
使用的是pods
管理,Node.js
使用npm
導入Socket.IO
庫
// 1. 進入當當前文件夾
cd ...
// 2. 創(chuàng)建package.json文件
npm init
/// 3. 導入庫
npm install socket.io --sava
npm install express --sava
創(chuàng)建socket
-
socket
本質(zhì)還是http
協(xié)議,所以需要綁定http
服務器,才能啟動socket服務. - 而且需要通過
web
服務器監(jiān)聽端口,socket
不能監(jiān)聽端口,有人訪問端口才能建立連接,所以先創(chuàng)建web
服務器
// 引入http模塊
var http = require('http')
// 面向express框架開發(fā),加載express框架,方便處理get,post請求
var express = require('express')
// 創(chuàng)建web服務器
var server = http.Server(express)
// 引入socket.io模塊
var socketio = require('socket.io')
// 創(chuàng)建愛你socket服務器
var serverSocket = socketio(server)
server.listen(9090)
console.log('監(jiān)聽9090')
建立socket連接
- 服務器不需要主動建立連接,建立連接是客戶端的事情,服務器只需要監(jiān)聽連接
- 客戶端主動連接會發(fā)送
connection
事件,服務端只需要監(jiān)聽connection
事件有沒有發(fā)送,就知道客戶端有沒有主動連接服務器 -
Socket.IO
本質(zhì)是通過發(fā)送和接受事件觸發(fā)服務器和客戶端之間的通訊,任何能被編輯成JSON
或二進制的對象都可以傳遞 -
socket.on
: 監(jiān)聽事件,這個方法會有兩個參數(shù),第一個參數(shù)是事件名稱,第二個參數(shù)是監(jiān)聽事件的回調(diào)函數(shù),監(jiān)聽到鏈接就會執(zhí)行這個回調(diào)函數(shù) - 監(jiān)聽
connection
,回調(diào)函數(shù)會傳入一個連接好的socket
,這個socket
就是客戶端的socket
-
socket
連接原理,就是客戶端和服務端通過socket
連接,服務器有socket
,客戶端也有
// 監(jiān)聽客戶端有沒有連接成功,如果連接成功,服務端會發(fā)送connection事件,通知客戶端連接成功
// serverSocket: 服務端, clientSocket: 客戶端
serverSocket.on('connection', function (clientSocket) {
// 建立socket連接成功
console.log('建立連接成功')
console.log(clientSocket)
})
Socket.IO客戶端
-
Socket.IO-Client-Swift是
iOS
使用的庫, 目前只有Swift
版本 - iOS中的使用
創(chuàng)建socket對象
創(chuàng)建SocketIOClient
對象, 兩種創(chuàng)建方式
// 第一種, SocketIOClientConfiguration: 可選參數(shù)
public init(socketURL: URL, config: SocketIOClientConfiguration = [])
// 第二種, 底層還是使用的第一種方式創(chuàng)建
public convenience init(socketURL: URL, config: [String: Any]?) {
self.init(socketURL: socketURL, config: config?.toSocketConfiguration() ?? [])
}
-
SocketIOClientConfiguration
: 是一個數(shù)組, 等同于[SocketIOClientOption]
-
SocketIOClientOption
的所有取值如下
public enum SocketIOClientOption : ClientOption {
/// 使用壓縮的方式進行傳輸
case compress
/// 通過字典內(nèi)容連接
case connectParams([String: Any])
/// NSHTTPCookies的數(shù)組, 在握手過程中傳遞, Default is nil.
case cookies([HTTPCookie])
/// 添加自定義請求頭初始化來請求, 默認為nil
case extraHeaders([String: String])
/// 將為每個連接創(chuàng)建一個新的connect, 如果你在重新連接有bug時使用.
case forceNew(Bool)
/// 傳輸是否使用HTTP長輪詢, 默認false
case forcePolling(Bool)
/// 是否使用 WebSockets. Default is `false`
case forceWebsockets(Bool)
/// 調(diào)度handle的運行隊列, 默認在主隊列
case handleQueue(DispatchQueue)
/// 是否打印調(diào)試信息. Default is false
case log(Bool)
/// 可自定義SocketLogger調(diào)試日志
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)
/// 自簽名只能用于開發(fā)模式
case selfSigned(Bool)
/// NSURLSessionDelegate 底層引擎設置. 如果你需要處理自簽名證書. Default is nil.
case sessionDelegate(URLSessionDelegate)
}
創(chuàng)建SocketIOClient
// 注意協(xié)議: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
監(jiān)聽連接
- 創(chuàng)建好
socket
對象,然后連接用connect
方法 - 因為
socket
需要進行3次握手,不可能馬上建議連接,需要監(jiān)聽是否連接成功的回調(diào),使用on
方法 -
ON
方法兩個參數(shù)- 參數(shù)一: 監(jiān)聽的事件名稱,參數(shù)二:監(jiān)聽事件回調(diào)函數(shù),會自動調(diào)用
- 回調(diào)函數(shù)也有兩個參數(shù)(參數(shù)一:服務器傳遞的數(shù)據(jù) 參數(shù)二:確認請求數(shù)據(jù)
ACK
) - 在
TCP/IP
協(xié)議中,如果接收方成功的接收到數(shù)據(jù),那么會回復一個ACK
數(shù)據(jù)-ACK
只是一個標記,標記是否成功傳輸數(shù)據(jù)
// 回調(diào)閉包
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 {
// 這里調(diào)用的是上面的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
// 監(jiān)聽鏈接成功
socket.on(clientEvent: .connect) { (data, ack) in
print("鏈接成功")
print(data)
print(ack)
}
socket.connect()
SocketIO事件
SocketIO
通過事件鏈接服務器和傳遞數(shù)據(jù)
客戶端監(jiān)聽事件
// 監(jiān)聽鏈接成功
socket.on(clientEvent: .connect) { (data, ack) in
print("鏈接成功")
print(data)
print(ack)
}
客戶端發(fā)送事件
只有連接成功之后,才能發(fā)送事件
// 建立一個連接到服務器. 連接成功會觸發(fā) "connect"事件
open func connect()
// 連接到服務器. 如果連接超時,會調(diào)用handle
open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?)
// 重開一個斷開連接的socket
open func disconnect()
// 向服務器發(fā)送事件, 參數(shù)一: 事件的名稱,參數(shù)二: 傳輸?shù)臄?shù)據(jù)組
open func emit(_ event: String, with items: [Any])
服務器監(jiān)聽事件
- 監(jiān)聽客戶端事件,需要嵌套在連接好的
connect
回調(diào)函數(shù)中 - 必須使用回調(diào)函數(shù)的
socket
參數(shù),如function(s)
中的s,監(jiān)聽事件,因此這是客戶端的socket
,肯定監(jiān)聽客戶端發(fā)來的事件 - 服務器監(jiān)聽連接的回調(diào)函數(shù)的參數(shù)可以添加多個,具體看客戶端傳遞數(shù)據(jù)數(shù)組有幾個,每個參數(shù)都是與客戶段一一對應,第一個參數(shù)對應客戶端數(shù)組第0個數(shù)據(jù)
// 監(jiān)聽socket連接
socket.on('connection',function(s){
console.log('監(jiān)聽到客戶端連接');
// data:客戶端數(shù)組第0個元素
// data1:客戶端數(shù)組第1個元素
s.on('chat',function(data,data1){
console.log('監(jiān)聽到chat事件');
console.log(data,data1);
});
});
服務器發(fā)送事件
這里的socket
一定要用服務器端的socket
// 給當前客戶端發(fā)送數(shù)據(jù),其他客戶端收不到.
socket.emit('chat', '服務器' + data)
// 發(fā)給所有客戶端,不包含當前客戶端
socket.emit.broadcast.emit('chat', '發(fā)給所有客戶端,不包含當前客戶端' + data)
// 發(fā)給所有客戶端,包含當前客戶端
socket.emit.sockets.emit('chat', '發(fā)給所有客戶端,包含當前客戶端' + data)
SocketIO分組
- 每一個客戶端和服務器只會保持一個
socket
鏈接, 那么怎么吧每一條信息推送到對應的聊天室, 針對多個聊天室的問題有如何解決 - 給每個聊天室都分組, 服務器就可以給指定的組進行數(shù)據(jù)的推送, 就不會影響到其他的聊天室
如何分組
-
socket.io
提供rooms和namespace的API - 用
rooms
的API就可以實現(xiàn)多房間聊天了,總結(jié)出來無外乎就是: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
調(diào)用join
,服務器就會把客戶端socket
和分組的名稱綁定起來 - 到時候就可以根據(jù)分組的名稱找到對應客戶端的
socket
,就能給指定的客戶端推送信息 - 一個客戶端
socket
只能添加到一組,離開的時候,要記得移除