(转)记录一次迁移 wss WebSocket 的事故
【轉(zhuǎn)載一下】
?
??今天是2018年04月21日。
??過去的這一個(gè)多月里,我的工(開)作(發(fā))任務(wù)轉(zhuǎn)戰(zhàn)回了游戲。短短的一個(gè)月里,催著輸出兩款h5游戲,再加上對(duì)接、聯(lián)調(diào),想想真是夠辛(ku)苦(bi)的。本人負(fù)責(zé)后端,也就是服務(wù)端這塊的游戲主流程輸出。去年下半年,在前任大佬的帶領(lǐng)下,做過一兩款棋牌類的手游,雖然目前的運(yùn)營(yíng)狀況不太樂觀。不過好在,過去學(xué)的那點(diǎn)皮毛也還沒丟光,所以這次寫h5后端總體還算順暢。至于怎么用Java來寫游戲,下來如果有時(shí)間會(huì)整理下這塊的思路和知識(shí)。
關(guān)于WebSocket,維基百科是這樣介紹的:
?? 以前,很多網(wǎng)站為了實(shí)現(xiàn)實(shí)時(shí)推送技術(shù),所用的技術(shù)都是輪詢。輪詢是在特定的時(shí)間間隔(如每1秒),由瀏覽器對(duì)服務(wù)器發(fā)出HTTP請(qǐng)求,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端。這種傳統(tǒng)的模式帶來的缺點(diǎn)很明顯,即瀏覽器需要不斷的向服務(wù)器發(fā)出請(qǐng)求,然而HTTP請(qǐng)求包含較多的請(qǐng)求頭信息,而其中真正有效的數(shù)據(jù)只是很小的一部分,顯然這樣會(huì)浪費(fèi)很多的帶寬等資源。在這種情況下,HTML5定義了WebSocket協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實(shí)時(shí)地進(jìn)行通訊。
?? WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通訊的協(xié)議,使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
??WebSocket 協(xié)議在2008年誕生,2011年成為國(guó)際標(biāo)準(zhǔn),現(xiàn)在幾乎所有瀏覽器都已經(jīng)支持了。它的最大特點(diǎn)就是,服務(wù)器可以主動(dòng)向客戶端推送信息,客戶端也可以主動(dòng)向服務(wù)器發(fā)送信息,是真正的雙向平等對(duì)話,屬于服務(wù)器推送技術(shù)的一種。
??簡(jiǎn)單來說,WebSocket減少了客戶端與服務(wù)器端建立連接的次數(shù),減輕了服務(wù)器資源的開銷,只需要完成一次HTTP握手。整個(gè)通訊過程是建立在一次連接/狀態(tài)中,也就避免了HTTP的非狀態(tài)性,服務(wù)端會(huì)一直與客戶端保持連接,直到雙方發(fā)起關(guān)閉請(qǐng)求,同時(shí)由原本的客戶端主動(dòng)詢問,轉(zhuǎn)換為服務(wù)器有信息的時(shí)候推送。所以,它能做實(shí)時(shí)通信(聊天室、直播間等),其他特點(diǎn)還包括:
- 建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易
- 與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過各種 HTTP 代理服務(wù)器
- 數(shù)據(jù)格式比較輕量,性能開銷小,通信高效
- 可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)
- 沒有同源限制,客戶端可以與任意服務(wù)器通信
- 協(xié)議標(biāo)識(shí)符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL
??差點(diǎn)就跑題了。這不,由于業(yè)務(wù)需求,上頭要求新出的h5游戲要配上Https。無奈,公司小,沒有專業(yè)的運(yùn)維人員,所以只能由我們這些開發(fā)“猿”頂上了,以為會(huì)很順暢,但一連串的問題沒想到也才剛剛開始。因此本文,就是用來記錄這些踩過的“坑”,希望可以讓后人少走點(diǎn)彎路。
1. 申領(lǐng)證書
?? 公有云服務(wù)器上,一般大家都習(xí)慣使用Nginx來做反向代理。首先,配置Https,需要我們到專業(yè)的CA機(jī)構(gòu)去申領(lǐng)證書,這個(gè)證書大多數(shù)情況下都是要錢的,但其實(shí)也有免費(fèi)的(有效期1年),例如利用國(guó)內(nèi)的阿里云或者騰訊云就可以很方便的申請(qǐng)這證書。
?? -?阿里云 - Https證書申請(qǐng)
?? -?騰訊云 - Https證書申請(qǐng)
??PS:?通過阿里云申領(lǐng)免費(fèi)版SSL證書有點(diǎn)套路,藏得有點(diǎn)深。點(diǎn)擊以上鏈接進(jìn)入后,如果在“證書類型”一欄中沒找到“免費(fèi)型DV SSL”,那么請(qǐng)依次點(diǎn)擊第三欄的“選擇品牌”中的“Symantec”,然后回到第一欄的“證書類型”,點(diǎn)擊出現(xiàn)的第三個(gè)選項(xiàng)“增強(qiáng)型OV SSL”,之后就會(huì)在“證書類型”中出現(xiàn)我們需要的第二項(xiàng):“免費(fèi)型DV SSL”。
騰訊云Https證書申請(qǐng)
??確認(rèn)申領(lǐng)、購(gòu)買之后,下來還需要綁定我們的域名(注意:免費(fèi)型的SSL證書一般僅支持綁定一個(gè)一級(jí)域名或者子域名,通配符的證書一般是需要花錢的),以及進(jìn)行域名身份驗(yàn)證等操作。等這兩步都完成之后,只需要等待CA機(jī)構(gòu)掃描認(rèn)證之后,我們就可以拿到真正的證書了。
2. 配置Https
??下載好證書壓縮包并解壓之后,一般里面有IIS、Apache和Nginx三款主流服務(wù)器的ssl證書,這里我們也僅需要Nginx的證書。首先,將證書里Nginx文件夾下的1_{域名}bundle.crt 和2{域名}.key復(fù)制到我們服務(wù)器上的指定位置(假設(shè)在/root/ssl/下面)。基于Nginx的Https配置還是比較簡(jiǎn)單的,參考如下。
?server {
#listen 80; #如果需要同時(shí)支持http和https
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate "/root/ssl/1_{域名}_bundle.crt";
ssl_certificate_key "/root/ssl/2_{域名}.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
server_name {域名};
location / {
proxy_pass http://localhost:{代理端口};
}
}
??附:下面是開啟Nginx的Gzip壓縮的配置,有需要的也可以參考。
?http {
gzip on;
gzip_disable "msie6";
gzip_min_length 1k;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types application/font-woff text/plain application/javascript application/json text/css application/xml text/javascript image/jpg image/jpeg image/png image/gif image/x-icon;
server {
# 這里是server相關(guān)的配置
}
}
3. 事故現(xiàn)場(chǎng)
??完成以上步驟后,按道理來說,h5游戲確實(shí)可以通過https的形式來打開了,簡(jiǎn)單測(cè)試后的確沒啥問題,然后大家也就這樣愉快的下班了。不過正如“墨菲定律”所說的:“凡事只要有可能出錯(cuò),那就一定會(huì)出錯(cuò)”。果不其然,一段時(shí)間后,測(cè)試就在群里反饋,某段時(shí)間后h5游戲就無法加載正常進(jìn)行下去了,一看時(shí)間,正是配完Https之后開始出現(xiàn)的問題。沒辦法,于是連忙打開電腦,開始排查解決問題,直覺告訴我要先打開瀏覽器的控制面板,果不其然,立刻發(fā)現(xiàn)了問題。
Mixed Content: The page at ‘https://{域名}.com/‘ was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint ‘ws://{ip}:{port}/‘. This request has been blocked; this endpoint must be available over WSS.
Uncaught DOMException: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.
??好家伙,這種情況,毫無疑問我們就需要使用 wss:// 安全協(xié)議了,于是立即聯(lián)系h5客戶端,把連接服務(wù)端webscoket的形式由ws:// 改為 wss:// 。本以為這樣就解決了,沒想到一段時(shí)間后下一個(gè)問題又來了。
擴(kuò)展:關(guān)于 ws 和 wss
WebSocket可以使用 ws 或 wss 來作為統(tǒng)一資源標(biāo)志符,類似于 HTTP 或 HTTPS。其中 ,wss 表示在 TLS 之上的 WebSocket,相當(dāng)于 HTTPS。默認(rèn)情況下,WebSocket的 ws 協(xié)議基于Http的 80 端口;當(dāng)運(yùn)行在TLS之上時(shí),wss 協(xié)議默認(rèn)是基于Http的 443 端口。說白了,wss 就是 ws 基于 SSL 的安全傳輸,與 HTTPS 一樣樣的道理。所以,如果你的網(wǎng)站是 HTTPS 協(xié)議的,那你就不能使用 ws:// 了,瀏覽器會(huì) block 掉連接,和 HTTPS 下不允許 HTTP 請(qǐng)求一樣。
??h5客戶端改成wss連接后,測(cè)試發(fā)現(xiàn)還是無法正常游戲。無奈,再次打開瀏覽器面板,果然,又看到一個(gè)新的問題。
WebSocket connection to ‘wss://{ip}:{port}/‘ failed: Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR
??之前在Http的情況下,客戶端一直是用ip+port的形式來連接服務(wù)端,當(dāng)然了也不會(huì)出現(xiàn)什么問題。很明顯,在更改成Https后,若還是以這種方式連接服務(wù)端,瀏覽器就會(huì)報(bào) SSL 協(xié)議錯(cuò)誤,這很明顯就是證書的問題。如果這時(shí)候還用 IP + 端口號(hào) 的方式連接 WebSocket ,是根本就沒有證書存在的(即使我們?cè)贜ginx配置了SSL證書,但這種方式其實(shí)是不會(huì)走Nginx代理的),所以在生成環(huán)境下,更推薦大家用域名的方式來連接。于是,立刻又聯(lián)系前端,再一次做更改,修改為 wss://{域名}/ 進(jìn)行連接。我以為這樣就真的解決了,沒想到還是too young too simple,沒一會(huì)下個(gè)問題又來了,測(cè)試反饋的結(jié)果還是不可以,第三次打開瀏覽器控制面板,果然又是一個(gè)新的錯(cuò)誤信息。
WebSocket connection to ‘wss://{域名}/‘ failed: Error during WebSocket handshake: Unexpected response code: 400
??看到這個(gè)錯(cuò)誤信息后,確定這是服務(wù)端返回的400響應(yīng)。既然可以請(qǐng)求到服務(wù)端,就說明客戶端這邊是沒有問題的,那么問題最可能出在客戶端和服務(wù)端之間。由于中間層使用了Nginx做轉(zhuǎn)發(fā),所以導(dǎo)致服務(wù)端無法知道這是一個(gè)合法的WebSocket請(qǐng)求。于是立刻查找了網(wǎng)上資料,在Nginx配置文件加入了以下配置,成功解決了這個(gè)問題。
?server {
location / {
proxy_pass http://localhost:{port};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
??接著,連忙拿域名進(jìn)行再次連接測(cè)試,終于看到了101 Switching Protocols的響應(yīng)Status Code。就這樣,也算是終于解決完在 HTTPS 下以 wss://{域名}/ 的方式連接 WebSocket的一系列問題。不過,最后這其中還有一個(gè)小問(插)題(曲)。
關(guān)于Nginx中的WebSocket配置
?? 自1.3 版本開始,Nginx就支持 WebSocket,并且可以為 WebSocket 應(yīng)用程序做反向代理和負(fù)載均衡。WebSocket 和 HTTP 是兩種不同的協(xié)議,但是 WebSocket 中的握手和 HTTP 中的握手兼容,它使用 HTTP 中的 Upgrade 協(xié)議頭將連接從 HTTP 升級(jí)到 WebSocket,當(dāng)客戶端發(fā)過來一個(gè) Connection: Upgrade請(qǐng)求頭時(shí),其實(shí)Nginx是不知道的。所以,當(dāng) Nginx 代理服務(wù)器攔截到一個(gè)客戶端發(fā)來的 Upgrade 請(qǐng)求時(shí),需要我們顯式的配置Connection、Upgrade頭信息,并使用 101(交換協(xié)議)返回響應(yīng),在客戶端、代理服務(wù)器和后端應(yīng)用服務(wù)之間建立隧道來支持 WebSocket。
?? 當(dāng)然,還需要注意一點(diǎn),此時(shí)WebSocket 仍然受到 Nginx 缺省為60秒的 proxy_read_timeout 配置影響。這意味著,如果你有一個(gè)程序使用了 WebSocket,但又可能超過60秒不發(fā)送任何數(shù)據(jù)的話,那么需要增大超時(shí)時(shí)間(配置proxy_read_timeout),要么實(shí)現(xiàn)一個(gè)Ping、Pong的心跳消息以保持客戶端和服務(wù)端的聯(lián)系。使用Ping、Pong的解決方法有額外的好處,如:可以發(fā)現(xiàn)連接是否被意外關(guān)閉等。
??關(guān)于最后的這個(gè)小問題,主要是在對(duì)Nginx配置的時(shí)候?qū)ocation=/的請(qǐng)求都進(jìn)行了proxy_pass(轉(zhuǎn)發(fā))。由于h5客戶端的文件打包成靜態(tài)文件后,存放在服務(wù)器的指定目錄下(這里假設(shè)在/root/html/static/路徑下),這也就導(dǎo)致這種配置的情況下Nginx無法正常代理指定目錄下的客戶端文件。于是再一次修改配置文件,添加location配置,最終完美解決所有問題。
?location /static/ {
root /root/html;
}
4. 寫在最后
??事故一波三折,現(xiàn)在回想起當(dāng)時(shí),也是一把辛酸史,一把辛酸淚(累)啊。所以僅以此文,記錄下我的填“坑”過程。
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的(转)记录一次迁移 wss WebSocket 的事故的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 这些是实际面试中遇到的面试题
- 下一篇: 《2020总结-2021展望》