什么是WebSocket呢泡孩?
WebSocket是HTML5新增的一種通信協(xié)議递瑰,目標(biāo)主流的瀏覽器都支持這個(gè)協(xié)議,比如Google的Chrome枷邪、Apple的Safari榛搔、Mozala的Firefox、Microsoft的IE等东揣。對WebSocket協(xié)議支持最早的當(dāng)屬Chrome瀏覽器践惑,從Chrome12開始就已經(jīng)開始支持,隨著協(xié)議草案不斷完善救斑,各個(gè)瀏覽器對協(xié)議的實(shí)現(xiàn)也在不停的更新童本。
為什么會引入WebSocket協(xié)議呢?
瀏覽器已經(jīng)支持HTTP協(xié)議了脸候,為什么還要開發(fā)一種新的WebSocket協(xié)議呢穷娱?因?yàn)镠TTP協(xié)議是一種單向的網(wǎng)絡(luò)協(xié)議,在建立連接后只允許瀏覽器或用戶代理(UserAgent
)向Web服務(wù)器發(fā)出請求資源后运沦,Web服務(wù)器才能返回相應(yīng)的資源數(shù)據(jù)泵额。而Web服務(wù)器是不能夠主動推送數(shù)據(jù)給瀏覽器的,HTTP設(shè)計(jì)之初的考慮到安全問題携添,如果Web服務(wù)器能夠主動的推送數(shù)據(jù)給瀏覽器嫁盲,那么瀏覽器就太容易受到攻擊,一些廣告商也會主動的將廣告信息在不經(jīng)意間強(qiáng)行推送給用戶烈掠,這不能不說是一個(gè)災(zāi)難羞秤。但是單向的HTTP協(xié)議給現(xiàn)代的網(wǎng)站和Web應(yīng)用程序卻帶來了許多問題。加入要開發(fā)一個(gè)基于Web的應(yīng)用程序去獲取當(dāng)前Web服務(wù)器的實(shí)時(shí)數(shù)據(jù)的話左敌,比如股票的實(shí)時(shí)行情瘾蛋,火車票的剩余票數(shù)等,這個(gè)時(shí)候就需要瀏覽器與Web服務(wù)器之間反復(fù)的進(jìn)行HTTP通信矫限,瀏覽器需要不斷地發(fā)送請求去獲取實(shí)時(shí)數(shù)據(jù)哺哼。
實(shí)時(shí)獲取Web服務(wù)器資源的方式有哪幾種呢?
那么在還沒有WebSocket協(xié)議之前叼风,有哪幾種方式可以實(shí)時(shí)的獲取Web服務(wù)器上的資源數(shù)據(jù)呢取董?
- 短輪詢
Polling
Polling的方式是通過瀏覽器定時(shí)向Web服務(wù)器發(fā)送HTTP請求,Web服務(wù)器接收到請求后會將最新的數(shù)據(jù)返還給瀏覽器无宿。瀏覽器得到數(shù)據(jù)后將其渲染顯示茵汰,然后再定期的重復(fù)這一過程。
Polling的方式雖然能夠滿足實(shí)時(shí)的需求孽鸡,但存在一定的問題经窖,比如在某段時(shí)間內(nèi)Web服務(wù)器沒有數(shù)據(jù)更新呢坡垫,此時(shí)瀏覽器仍然需要定時(shí)發(fā)送請求過來詢問,Web服務(wù)器會將以前的老數(shù)據(jù)再次傳送過去画侣,瀏覽器將這些沒有變化的數(shù)據(jù)又渲染顯示出來冰悠。這樣既浪費(fèi)了網(wǎng)絡(luò)帶寬,又浪費(fèi)了CPU的利用利率配乱。如果將瀏覽器發(fā)送請求的時(shí)間周期調(diào)大一些溉卓,雖然可以緩解這一問題,但如果在Web服務(wù)器上數(shù)據(jù)更新很快時(shí)搬泥,又將無法保證Web應(yīng)用程序獲取數(shù)據(jù)的實(shí)時(shí)性桑寨。
針對這種情況,Polling做出改進(jìn)而衍生出來Long Polling忿檩。
- 長輪詢
Long Polling
Long Polling的操作是這樣的:瀏覽器發(fā)送請求到Web服務(wù)器時(shí)尉尾,Web服務(wù)器可以做兩件事情。第一件事是如果服務(wù)器數(shù)據(jù)更新就會立即將數(shù)據(jù)發(fā)回給瀏覽器燥透,了瀏覽器接收到數(shù)據(jù)后再理解發(fā)送請求給Web服務(wù)器沙咏。第二件事是如果服務(wù)器沒有數(shù)據(jù)更新,此時(shí)與Polling不同的是Web服務(wù)器不會立即發(fā)送回應(yīng)信息給瀏覽器班套,而會見這個(gè)請求保持住肢藐,等到有數(shù)據(jù)更新時(shí),再來響應(yīng)這個(gè)請求吱韭。當(dāng)然吆豹,如果服務(wù)器的數(shù)據(jù)長期沒有更新的話,一段時(shí)間后理盆,這些請求就會超時(shí)痘煤,瀏覽器將會收到超時(shí)消息,當(dāng)瀏覽器收到超時(shí)消息又會立即發(fā)送一個(gè)新的請求給Web服務(wù)器猿规,然后依次循環(huán)這個(gè)過程衷快。
Long Polling的方式雖然在某種程度上減小了網(wǎng)絡(luò)帶寬和CPU的利用率等問題,但仍存在缺陷坎拐,比如Web服務(wù)器的數(shù)據(jù)更新速度較快,當(dāng)Web服務(wù)器在傳送一個(gè)數(shù)據(jù)包給瀏覽器后养匈,必須等待瀏覽器的下一個(gè)請求的到來才能傳遞第二給更新的數(shù)據(jù)包給瀏覽器哼勇。這樣的話,瀏覽器顯示的實(shí)時(shí)數(shù)據(jù)最快的時(shí)間也就是2 x RTT(往返時(shí)間)呕乎。另外积担,由于HTTP數(shù)據(jù)包的頭部數(shù)據(jù)量往往會很大,一般有400多字節(jié)猬仁,但是真正被服務(wù)器使用的卻很少帝璧,有時(shí)只有10字節(jié)左右先誉,這樣的數(shù)據(jù)包在網(wǎng)絡(luò)上周期性的傳輸,難免對網(wǎng)絡(luò)帶寬又是一種浪費(fèi)的烁。
實(shí)際上Long Polling長輪詢的底層實(shí)現(xiàn)是在服務(wù)器的程序中加入一個(gè)死循環(huán)褐耳,在循環(huán)中檢測數(shù)據(jù)的變化,當(dāng)發(fā)現(xiàn)有幸數(shù)據(jù)時(shí)會立即將其輸出給瀏覽器并斷開連接渴庆,瀏覽器收到數(shù)據(jù)后會再次發(fā)起請求進(jìn)入下一個(gè)周期铃芦。
長輪詢的弊端是服務(wù)器長時(shí)間連接會消耗服務(wù)器資源,另外返回的數(shù)據(jù)的順序無法保證襟雷,難以管理和維護(hù)刃滓。
對于長輪詢的處理,服務(wù)器并不會一直保持耸弄,通常的做法是會設(shè)置一個(gè)最大時(shí)限咧虎,可以通過心跳包的方式,設(shè)置多少秒之后沒有接收到心跳包就關(guān)閉當(dāng)前連接计呈。
通過以上的分析可知砰诵,要想在瀏覽器上支持雙向通信而且協(xié)議的頭部又不是那么的龐大,不得不采用新的協(xié)議震叮,WebSocket也就是為了解決這個(gè)問題而設(shè)計(jì)誕生的胧砰。
WebSocket協(xié)議是什么樣的呢?
WebSocket協(xié)議是一種雙向的通信協(xié)議苇瓣,它建立在TCP之上尉间,同HTTP一樣是通過TCP來傳遞數(shù)據(jù)的,不過它與HTTP最大的不同點(diǎn)在于:
- WebSocket是一種雙向通信協(xié)議击罪,在建立連接后哲嘲,WebSocket服務(wù)器和瀏覽器之間都能主動地向?qū)ο蟀l(fā)送或接收數(shù)據(jù),這就像Socket一樣媳禁,只是與之不同的是眠副,WebSocket是一種建立在Web基礎(chǔ)上的簡單模擬Socket的協(xié)議。
- WebSocket需要通過握手建立連接竣稽,類似于TCP也需要客戶端和服務(wù)端進(jìn)行握手成功后才能互相通信囱怕。
這里簡要的說明一下WebSocket握手的過程,當(dāng)Web應(yīng)用程序調(diào)用new WebSocket(url)
接口時(shí)毫别,瀏覽器就會開始與對應(yīng)URL地址的WebSocket服務(wù)器建立握手的連接娃弓。具體的過程是這樣的:
首先,瀏覽器與WebSocket服務(wù)器之間通過TCP的三次握手建立連接岛宦,如果連接建立失敗則后續(xù)流程將不再執(zhí)行台丛,此時(shí)Web應(yīng)用程序?qū)盏藉e(cuò)誤消息通知。
當(dāng)TCP連接建立成功后砾肺,瀏覽器會通過HTTP發(fā)送WebSocket所支持的版本號挽霉、協(xié)議的字版本號防嗡、原始地址、主機(jī)地址等一系列字段給WebSocket服務(wù)器侠坎。
例如:握手請求
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat,superchat
Sec-WebSocket-Version: 13
這里需要重點(diǎn)關(guān)注的是Sec-WebSocket-Key
這個(gè)字段蚁趁,它又稱為“夢幻字符串”也是一個(gè)密鑰,其值采用base64
編碼的隨機(jī)16字節(jié)長的字符序列硅蹦,通過這個(gè)密鑰服務(wù)器才能解碼辨認(rèn)是否為WebSocket握手請求荣德,如果比對辨認(rèn)成功則認(rèn)為此協(xié)議是WebSocket協(xié)議,否則則認(rèn)為是普通的HTTP協(xié)議童芹。
- 當(dāng)WebSocket服務(wù)器接收到瀏覽器發(fā)送過來的握手請求后涮瞻,如果數(shù)據(jù)包的數(shù)據(jù)以及格式正確、客戶端和服務(wù)器的協(xié)議版本號匹配的話假褪,就會接受本次握手連接署咽,并給出相應(yīng)的數(shù)據(jù)回復(fù),同時(shí)回復(fù)的數(shù)據(jù)包也會采用HTTP協(xié)議進(jìn)行傳輸生音。
例如:握手響應(yīng)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
在響應(yīng)頭中同樣存在的一個(gè)“夢幻字段”宁否,不過它的名字叫做Sec-WebSocket-Accept
,同樣也是一個(gè)密鑰缀遍,不同的是這個(gè)字符串是要讓客戶端辨認(rèn)慕匠,當(dāng)客戶端拿到自動解碼后,會辨認(rèn)是否是一個(gè)WebSocket握手響應(yīng)域醇。
- 當(dāng)瀏覽器接收到WebSocket服務(wù)器回復(fù)的數(shù)據(jù)包后台谊,如果數(shù)據(jù)包內(nèi)容、格式正確的話譬挚,就表示本次連接建立成功锅铅,瀏覽器會觸發(fā)
onopen
消息,此時(shí)Web開發(fā)人員就可以在此通過WebSocket接口中的send
方法向WebSocket服務(wù)器發(fā)送數(shù)據(jù)了减宣。否則握手建立失敗盐须,Web應(yīng)用程序?qū)⑹盏?code>onerror的消息,并能夠知道握手連接失敗的原因漆腌。
簡單來說WebSocket的操作流程是:客戶端首先向服務(wù)器發(fā)起一次特殊的HTTP請求贼邓,服務(wù)器接收后開始辨認(rèn)請求頭如果是客戶端的請求則開始進(jìn)行普通的TCP三次握手建立建立,否則將會按照普通的HTTP請求進(jìn)行處理闷尿。
WebSocket提供了兩種數(shù)據(jù)傳輸塑径,一種是文本格式的數(shù)據(jù),另一種則是二進(jìn)制格式的數(shù)據(jù)悠砚。
WebSocket與HTTP和TCP有什么關(guān)系呢晓勇?
了解完WebSocket協(xié)議的工作原理后堂飞,需要弄清楚一點(diǎn)的是WebSocket與TCP和HTTP之間的關(guān)系是什么樣子的呢灌旧?
WebSocket與HTTP協(xié)議一樣都是基于TCP的绑咱,所以它們都是可靠的協(xié)議,Web開發(fā)者調(diào)用WebSocket的send
方法枢泰,在瀏覽器的實(shí)現(xiàn)最終都是通過TCP的接口進(jìn)行傳輸?shù)摹?/p>
WebSocket和HTTP協(xié)議一樣都屬于應(yīng)用層的協(xié)議描融,WebSocket在建立握手連接時(shí),數(shù)據(jù)是通過HTTP協(xié)議傳輸?shù)暮饴欤虼藭捎靡徊糠諬TTP的數(shù)據(jù)包的字段窿克。但是在建立連接之后,真正的數(shù)據(jù)傳輸階段就不需要HTTP參與了毛甲。
要想搭建WebSocket服務(wù)器年叮,你需要明白的是WebSocket作為一種新的通信協(xié)議,目前還處于草案階段并沒有成為標(biāo)準(zhǔn)玻募,市面上也沒有成熟的WebSocket服務(wù)器或類庫實(shí)現(xiàn)WebSocket協(xié)議只损,所以需要自己手動編寫代碼去解析和組裝WebSocket的數(shù)據(jù)包。不過七咧,也確實(shí)有部分的開源庫可供我們使用跃惫,比如基于Python的PyWebSocket,基于Node.js的WebSocket-Node等艾栋,這些類庫文件已經(jīng)實(shí)現(xiàn)了WebSocket數(shù)據(jù)包的封裝和解析爆存,可以直接調(diào)用這些接口來開發(fā),這樣很大程度上減少了工作量蝗砾。
什么是WebSocket服務(wù)器呢先较?
-
swoole_websocket_server
是在swoole_http_server
的基礎(chǔ)上增加了對WebSocket協(xié)議的解析 - 完整的WebSocket協(xié)議請求會被解析并封裝在
frame
對象內(nèi) -
swoole_websocket_server
新增了push
方法用于發(fā)送WebSocket數(shù)據(jù)
WebSocket服務(wù)器簡單來說就是一個(gè)遵循特殊協(xié)議監(jiān)聽服務(wù)器任意端口的TCP應(yīng)用,一個(gè)WebSocket服務(wù)器可以使用任意的服務(wù)器編程語言來實(shí)現(xiàn)遥诉,只要語言能夠?qū)崿F(xiàn)基本的Berkeley Sockets伯克利套接字拇泣。
WebSocket服務(wù)器通常是獨(dú)立的服務(wù)器,因?yàn)樨?fù)載均衡和其他原因矮锈,通常會使用反向代理如標(biāo)準(zhǔn)的HTTP服務(wù)器來發(fā)現(xiàn)WebSocket握手協(xié)議霉翔,預(yù)處理之后將客戶端請求信息發(fā)送到真正的WebSocket服務(wù)器,這也就意味著WebSocket服務(wù)器不必充斥著Cookie和簽名的處理方法苞笨,完全可以放在代理中解決债朵。
在開發(fā)WebSocket服務(wù)器之前需要了解TCP的Socket編程知識。下面基于Swoole從客戶端握手請求到服務(wù)端握手響應(yīng)返回來闡述下TCP下Socket的工作原理瀑凝。
在開始之前序芦,需要解決的第一個(gè)問題是WebSocket的握手規(guī)則。首先粤咪,服務(wù)器必須使用標(biāo)準(zhǔn)的TCP的Socket來監(jiān)聽即將到來的Socket連接谚中,由于WebSocket握手采用的是HTTP,因此在選擇監(jiān)聽端口上一般會是HTTP的80端口或HTTPS的443端口,雖然服務(wù)器可以選擇任意端口宪塔,但除此二者之外的端口可能會遇到防火墻或代理的問題磁奖。
WebSocket握手規(guī)則是什么呢?
接著某筐,我們將WebSocket的握手規(guī)則劃分為兩個(gè)階段來分析:
- 客戶端握手請求
由于WebSocket握手過程是由客戶端發(fā)起的比搭,所以必須要明白服務(wù)器是如何解析客戶端的請求,通衬咸埽客戶端會發(fā)送一個(gè)標(biāo)準(zhǔn)的HTTP請求身诺。
類似于這樣
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
對于客戶端發(fā)起WebSocket握手請求中會存在標(biāo)準(zhǔn)HTTP頭信息字段,另外還會包括WebSocket自定義的字段抄囚。因此在很多公共設(shè)置中霉赡,會有一個(gè)代理服務(wù)器來進(jìn)行處理HTTP請求。如果有的header頭信息不被識別或是非法值幔托,服務(wù)器會發(fā)送類似400 Bad Request
并立即關(guān)閉Socket同廉,通常會在HTTP返回的Body體中給出握手失敗的原因。不過這些信息可能不會被瀏覽器所展示柑司。如果服務(wù)器無法是被WebSocket的版本迫肖,通常會返回一個(gè)Sec-WebSocket-Version
的消息頭,并會在其中指明自己能夠接受的版本號攒驰。
瀏覽器一般會發(fā)送一個(gè)Origin Header的信息頭蟆湖,可使用這個(gè)Header頭來做安全限制,也就是檢查是否具有相同的Origin玻粪。如果不是期望的Origin將會返回了一個(gè)403 Forbidden
的錯(cuò)誤信息隅津。另外需要注意的是,在一些非瀏覽器的客戶端中是可以偽造Origin的劲室,而很多應(yīng)用將會拒絕沒有Origin消息頭的請求伦仍。
請求資源定位符在規(guī)范中并沒有給出明確的定義,所有有很多人在巧妙地使用它很洋,比如讓一個(gè)服務(wù)器處理多個(gè)WebSocket應(yīng)用充蓝。
對于規(guī)范的HTTP code只可以在握手之前使用,當(dāng)握手成功后應(yīng)該使用不同的code集合喉磁。
服務(wù)器握手返回
當(dāng)服務(wù)器接收到來自客戶端的請求時(shí)谓苟,會發(fā)送一個(gè)相當(dāng)奇怪的響應(yīng)。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Swoole提供的WebSocket服務(wù)器是什么樣的呢协怒?
Swoole1.7.9版本開始內(nèi)置了WebSocket服務(wù)器涝焙,方便快速的編寫異步非阻塞多進(jìn)程的WebSocket服務(wù)器。
案例:客戶端通過瀏覽器的WebSocket協(xié)議向服務(wù)端發(fā)送請求孕暇,服務(wù)器接收到請求后先第三方接口請求獲取數(shù)據(jù)并存入Redis仑撞。
$ mkdir test && cd test
$ vim server.php
創(chuàng)建服務(wù)器
<?php
//創(chuàng)建Redis連接
$host = "127.0.0.1";
$port = 6379;
$redis = new Redis();
$redis->connect($host, $port);
//創(chuàng)建websocket服務(wù)器
$host = "0.0.0.0";
$port = 9501;
$server = new swoole_websocket_server($host, $port);
//服務(wù)器監(jiān)聽websocket連接打開事件
$server->on("open", function($ws, $rq) use($redis){
//將連接標(biāo)識保存到Redis的無序集合set中
$redis->sAdd("fd", $rq->fd);
});
//服務(wù)器監(jiān)聽websocket連接的消息事件
$server->on("message", function($ws, $frame) use($redis){
//獲取二進(jìn)制數(shù)據(jù)
$url = "http://imgsrc.baidu.com/imgad/pic/item/267f9e2f07082838b5168c32b299a9014c08f1f9.jpg";
$bin = file_get_contents($url);
//獲取Redis中保存的連接
$fds = $redis->sMembers("fd");
if(count($fds) > 0){
foreach($fds as $fd){
//發(fā)送字符串
$ws->push($fd, $frame->fd.":".$frame->data);
//發(fā)送二進(jìn)制
$ws->push($fd, $bin, WEBSOCKET_OPCODE_BINARY);
}
}
});
//服務(wù)器監(jiān)聽websoket連接關(guān)閉事件
$server->on("close", function($ws, $fd) use($redis){
$redis->sRem("fd", $fd);
});
//啟動服務(wù)器
$server->start();
客戶端
$ vim client.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
if(window.WebSocket)
{
var url = "ws://0.0.0.0:9501";
var ws = new WebSocket(url);
ws.onopen = function(event){
console.log("client websocket open success");
ws.send("hello server");
};
ws.onmessage = function(event)
{
console.log(event);
};
}
</script>
</body>
</html>
案例:
服務(wù)器
$ vim server.php
<?php
/**WebSocket服務(wù)端*/
//創(chuàng)建異步WebSocket服務(wù)端對象
$host = "0.0.0.0";
$port = 9501;
$server = new swoole_websocket_server($host, $port);
echo "[server] ".json_encode($server).PHP_EOL;
//監(jiān)聽客戶端握手
$server->on("open", function(swoole_websocket_server $server, $request){
echo PHP_EOL;
$fd = $request->fd;//獲取客戶端請求的文件描述符
echo "[open] client {$fd} handshake success".PHP_EOL;
echo "[server] ".json_encode($server).PHP_EOL;
echo "[request] ".json_encode($request).PHP_EOL;
});
//監(jiān)聽客戶端發(fā)送的消息
$server->on("message", function(swoole_websocket_server $server, $frame){
echo PHP_EOL;
$fd = $frame->fd;//獲取客戶端請求的文件描述符
$data = $frame->data;//獲取客戶端發(fā)送的消息
echo "[message] client {$fd} : {$data}".PHP_EOL;
echo "[server] ".json_encode($server).PHP_EOL;
echo "[frame] ".json_encode($frame).PHP_EOL;
$message = "success";
$server->push($fd, $message);
});
//監(jiān)聽客戶端斷開連接
$server->on("close", function(swoole_websocket_server $server, $fd){
echo PHP_EOL;
echo "[close] client {$fd}".PHP_EOL;
echo "[server] ".json_encode($server).PHP_EOL;
echo "[fd] ".json_encode($fd).PHP_EOL;
});
//啟動服務(wù)器
$server->start();
客戶端
$ vim client.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="./client.js"></script>
</body>
</html>
$ vim client.js
var host = "127.0.0.1";
var port = 9501;
var address = "ws://"+host+":"+port;
var ws = new WebSocket(address);
ws.onopen = function()
{
console.log("[onopen]");
var message = "onopen";
ws.send(message);
}
ws.onmessage = function(evt)
{
console.log("[onmessage]", evt);
var data = evt.data;
console.log(data);
}
ws.onclose = function(evt)
{
console.log("[onclose]", evt);
}
ws.onerror = function(evt)
{
console.log("[onerror]", evt);
}
var message = "hello";
ws.send(message);
ws.close();
Swoole的事件包括哪幾種呢赤兴?
WebSocket除了能夠接收Swoole\Server
和Swoole\Http\Server
基類的回調(diào)函數(shù)外,額外增加了三個(gè)回調(diào)函數(shù)設(shè)置隧哮。
-
onHandleShake
可選搀缠,WebSocket建立連接后進(jìn)行握手。 -
onOpen
可選近迁,當(dāng)WebSocket客戶端與服務(wù)器建立連接并完成握手會觸發(fā)回調(diào)。 -
onMessge
必選簸州,當(dāng)服務(wù)器收到客戶端數(shù)據(jù)幀時(shí)觸發(fā)回調(diào)鉴竭。
onReuqest
WebSocket
服務(wù)器繼承自HTTP
服務(wù)器,如果是WebSocket服務(wù)器設(shè)置了onRequest
回調(diào)岸浑,那么也可以 同時(shí)將WebSocket服務(wù)器作為HTTP服務(wù)器使用搏存。如果沒有設(shè)置onRequest
回調(diào),WebSocket收到HTTP請求后會返回400錯(cuò)誤頁面矢洲。如果想要通過接收HTTP觸發(fā)所有WebSocket的推送璧眠,想要注意作用域的問題,如果是面向過程的需要使用到global
读虏。對于W ebSocket服務(wù)器進(jìn)行引用责静,面向?qū)ο罂梢詫?code>WebSocket/Server設(shè)置成一個(gè)成員屬性。
例如:面向過程的方式
<?php
$host = "0.0.0.0";
$port = 9501;
$server = new swoole_websocket_server($host, $port);
$server->on("open", function(swoole_websocket_server $server, $request){
$fd = $request->fd;//客戶端連接標(biāo)識
echo "[open] server handshake success with client {$fd}".PHP_EOL;
});
$server->on("message", function(swoole_websocket_server $server, $frame){
$fd = $frame->fd;
$opcode = $frame->opcode;
$finish = $frame->finish;
echo "[message] receive from client {$fd}, opcode {$opcode}, finish {$finish}".PHP_EOL;
$message = "success ";
$server->push($fd, $message);
});
$server->on("close", function(swoole_websocket_server $server, $fd)
{
echo "[close] client {$fd}".PHP_EOL;
});
$server->on("request", function(swoole_http_request $request, swoole_http_response $response) use($server){
//遍歷所有WebSocket連接用戶的fd盖桥,給所有用戶推送
foreach($server->connections as $fd)
{
//判斷是否是正確的WebSocket連接灾螃,若非則有可能會push失敗。
if($server->isEstablished($fd)){
$message = $request->get["message"];
$server->push($fd, $message);
}
}
});
$server->start();
onHandShake
WebSocket建立連接后進(jìn)行握手揩徊,WebSocket服務(wù)器已經(jīng)內(nèi)置了handshake
腰鬼,如果用戶希望自己進(jìn)行握手處理,可設(shè)置onHandShake
事件回調(diào)函數(shù)塑荒。
函數(shù)原型
function onHandShake(
swoole_http_request $request,
swoole_http_response $response
)
參數(shù)列表
-
onHandShake
事件回調(diào)是可選的 - 設(shè)置
onHandShake
回調(diào)函數(shù)后不會再觸發(fā)onOpen
事件熄赡,需要應(yīng)用程序自行處理。 -
onHandShake
中必須調(diào)用$response->status
設(shè)置狀態(tài)嗎為101并調(diào)用end
響應(yīng)齿税,否則會握手失敗彼硫。 - Swoole內(nèi)置的握手協(xié)議為
Sec-WebSocket-Version:13
,低版本瀏覽器需要自行實(shí)現(xiàn)握手凌箕。 - Swoole1.8.1或更高版本可以使用
$server->defer
調(diào)用onOpen
邏輯
需要注意的是乌助,僅僅需要自行處理handshake
握手的時(shí)候再設(shè)置onHandShake
回調(diào)函數(shù),如果不需要自定義握手過程陌知,就不要設(shè)置該回調(diào)他托,使用Swoole默認(rèn)的握手即可。
onOpen
當(dāng)WebSocket客戶端與服務(wù)器建立連接并完成握手之后會回調(diào)onOpen
函數(shù)仆葡。
onOpen
事件函數(shù)是可選的赏参,可以調(diào)用push
方法向客戶端發(fā)送數(shù)據(jù)或調(diào)用close
關(guān)閉連接志笼。
函數(shù)原型
function onOpen(
swoole_websocket_server $server,
swoole_http_request $request
)
參數(shù)列表
-
swoole_http_request $request
是一個(gè)HTTP請求對象,包含了客戶端發(fā)送過來的握手請求信息把篓。
onMessage
當(dāng)服務(wù)器接收到來自客戶端的數(shù)據(jù)幀frame
時(shí)會觸發(fā)并回調(diào)onMessage
函數(shù)纫溃。
onMessage
回調(diào)必須被設(shè)置,如果未設(shè)置服務(wù)器將無法啟動韧掩,客戶端發(fā)送的ping
幀是不會觸發(fā)onMessage
回調(diào)函數(shù)的紊浩,底層會自動回復(fù)pong
包。
函數(shù)原型
function onMessage(
swoole_websocket_server $server,
swoole_websocket_frame $frame
)
參數(shù)列表
-
swoole_websocket_frame $frame
是swoole_websocket_frame
對象疗锐,包含了客戶端發(fā)送過來的數(shù)據(jù)幀信息坊谁。
數(shù)據(jù)幀
swoole_websocket_frame $frame
包含四個(gè)屬性
-
$frame->fd
表示客戶端的socket_id
,使用$server->push
推送數(shù)據(jù)時(shí)需要使用滑臊。 -
$frame->data
表示數(shù)據(jù)內(nèi)容口芍,可以是文本內(nèi)容也可以是二進(jìn)制數(shù)據(jù),可以通過opcode
值來判斷雇卷。如果$data
是文本類型鬓椭,編碼格式必須是UTF-8
,這是WebSocket協(xié)議標(biāo)準(zhǔn)文檔所規(guī)定的关划。 -
$frame->opcode
表示W(wǎng)ebSocket的OpCode類型 -
$frame->finish
表示數(shù)據(jù)幀是否完整小染,一個(gè)WebSocket請求可能會分成多個(gè)數(shù)據(jù)幀進(jìn)行發(fā)送,Swoole底層已經(jīng)實(shí)現(xiàn)了自動合并數(shù)據(jù)幀贮折,不用擔(dān)心接收到的數(shù)據(jù)幀不完整氧映。
OpCode
WebSocket的OpCode數(shù)據(jù)類型分為兩種
-
WEBSOCKET_OPCODE_TEXT = 0x1
表示文本數(shù)據(jù) -
WEBSOCKET_OPCODE_BINARY = 0x2
表示二進(jìn)制數(shù)據(jù)
Swoole WebSocket 提供的函數(shù)有哪些呢?
數(shù)據(jù)推送push
push
方法用于Swoole的WebSocket服務(wù)器向WebSocket客戶端連接推送數(shù)據(jù)脱货,推送的數(shù)據(jù)長度最大不得超過2MB岛都。
push
方法僅適用于Swoole1.7.11+版本
函數(shù)原型
function WebSocket\Server->push(
int $fd,
$data,
int $opcode = 1,
bool $finish = true
)
參數(shù)模式1
-
int $fd
表示客戶端連接ID,如果指定的$fd
對應(yīng)的TCP連接并非WebSocket客戶端振峻,將會發(fā)送失敗臼疫。 -
$data
需要發(fā)送的數(shù)據(jù)內(nèi)容 -
int $opcode
表示指定發(fā)送數(shù)據(jù)內(nèi)容的格式,默認(rèn)為文本WEBSOCKET_OPCODE_TEXT
扣孟,可發(fā)送二進(jìn)制內(nèi)容WEBSOCKET_OPCODE_BINARY
烫堤。
發(fā)送成功時(shí)會返回true
,發(fā)送失敗則返回false
凤价。
參數(shù)模式2
參數(shù)模式2僅適用于Swoole4.2.0+版本鸽斟,其中僅包含一個(gè)參數(shù)$data
,可以傳入一個(gè)swoole_websocket_frame
數(shù)據(jù)幀對象利诺,支持發(fā)送各種幀類型富蓄。
案例:服務(wù)器監(jiān)聽MySQL數(shù)據(jù)庫是否有更改,若有更改則主動告知并將更新數(shù)據(jù)推送給WebSocket客戶端慢逾。
案例:接收來自客戶端的請求