天天看點

記錄一次遷移 wss WebSocket 的事故

  今天是2018年04月21日。

  過去的這一個多月裡,我的工(開)作(發)任務轉戰回了遊戲。短短的一個月裡,催着輸出兩款h5遊戲,再加上對接、聯調,想想真是夠辛(ku)苦(bi)的。本人負責後端,也就是服務端這塊的遊戲主流程輸出。去年下半年,在前任大佬的帶領下,做過一兩款棋牌類的手遊,雖然目前的營運狀況不太樂觀。不過好在,過去學的那點皮毛也還沒丢光,是以這次寫h5後端總體還算順暢。至于怎麼用Java來寫遊戲,下來如果有時間會整理下這塊的思路和知識。

關于WebSocket,維基百科是這樣介紹的:

   以前,很多網站為了實作實時推送技術,所用的技術都是輪詢。輪詢是在特定的時間間隔(如每1秒),由浏覽器對伺服器發出HTTP請求,然後由伺服器傳回最新的資料給用戶端。這種傳統的模式帶來的缺點很明顯,即浏覽器需要不斷的向伺服器送出請求,然而HTTP請求包含較多的請求頭資訊,而其中真正有效的資料隻是很小的一部分,顯然這樣會浪費很多的帶寬等資源。在這種情況下,HTML5定義了WebSocket協定,能更好的節省伺服器資源和帶寬,并且能夠更實時地進行通訊。

   WebSocket是一種在單個TCP連接配接上進行全雙工通訊的協定,使得用戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向用戶端推送資料。在WebSocket API中,浏覽器和伺服器隻需要完成一次握手,兩者之間就可以建立持久性的連接配接,并進行雙向資料傳輸。

  WebSocket 協定在2008年誕生,2011年成為國際标準,現在幾乎所有浏覽器都已經支援了。它的最大特點就是,伺服器可以主動向用戶端推送資訊,用戶端也可以主動向伺服器發送資訊,是真正的雙向平等對話,屬于伺服器推送技術的一種。

記錄一次遷移 wss WebSocket 的事故

  簡單來說,WebSocket減少了用戶端與伺服器端建立連接配接的次數,減輕了伺服器資源的開銷,隻需要完成一次HTTP握手。整個通訊過程是建立在一次連接配接/狀态中,也就避免了HTTP的非狀态性,服務端會一直與用戶端保持連接配接,直到雙方發起關閉請求,同時由原本的用戶端主動詢問,轉換為伺服器有資訊的時候推送。是以,它能做實時通信(聊天室、直播間等),其他特點還包括:

  • 建立在 TCP 協定之上,伺服器端的實作比較容易
  • 與 HTTP 協定有着良好的相容性。預設端口也是80和443,并且握手階段采用 HTTP 協定,是以握手時不容易屏蔽,能通過各種 HTTP 代理伺服器
  • 資料格式比較輕量,性能開銷小,通信高效
  • 可以發送文本,也可以發送二進制資料
  • 沒有同源限制,用戶端可以與任意伺服器通信
  • 協定辨別符是ws(如果加密,則為wss),伺服器網址就是 URL
記錄一次遷移 wss WebSocket 的事故

  差點就跑題了。這不,由于業務需求,上頭要求新出的h5遊戲要配上Https。無奈,公司小,沒有專業的運維人員,是以隻能由我們這些開發“猿”頂上了,以為會很順暢,但一連串的問題沒想到也才剛剛開始。是以本文,就是用來記錄這些踩過的“坑”,希望可以讓後人少走點彎路。

1. 申領證書

   公有雲伺服器上,一般大家都習慣使用Nginx來做反向代理。首先,配置Https,需要我們到專業的CA機構去申領證書,這個證書大多數情況下都是要錢的,但其實也有免費的(有效期1年),例如利用國内的阿裡雲或者騰訊雲就可以很友善的申請這證書。

   - 阿裡雲 - Https證書申請

   - 騰訊雲 - Https證書申請

記錄一次遷移 wss WebSocket 的事故
記錄一次遷移 wss WebSocket 的事故

  PS: 通過阿裡雲申領免費版SSL證書有點套路,藏得有點深。點選以上連結進入後,如果在“證書類型”一欄中沒找到“免費型DV SSL”,那麼請依次點選第三欄的“選擇品牌”中的“Symantec”,然後回到第一欄的“證書類型”,點選出現的第三個選項“增強型OV SSL”,之後就會在“證書類型”中出現我們需要的第二項:“免費型DV SSL”。

騰訊雲Https證書申請

記錄一次遷移 wss WebSocket 的事故

  确認申領、購買之後,下來還需要綁定我們的域名(注意:免費型的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之後開始出現的問題。沒辦法,于是連忙打開電腦,開始排查解決問題,直覺告訴我要先打開浏覽器的控制台,果不其然,立刻發現了問題。

記錄一次遷移 wss WebSocket 的事故

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連接配接後,測試發現還是無法正常遊戲。無奈,再次打開浏覽器面闆,果然,又看到一個新的問題。

記錄一次遷移 wss WebSocket 的事故
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,沒一會下個問題又來了,測試回報的結果還是不可以,第三次打開浏覽器控制台,果然又是一個新的錯誤資訊。

記錄一次遷移 wss WebSocket 的事故
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. 寫在最後

  事故一波三折,現在回想起當時,也是一把辛酸史,一把辛酸淚(累)啊。是以僅以此文,記錄下我的填“坑”過程。

繼續閱讀