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