websocket 是 html5 之后的一个新事物, 可以方便的实现客户端到服务端的长会话, 特别适合用于客户端需要接收服务端推送的场景例如在线客服聊天, 提醒推送等等改变了以往客户端只能通过轮询或者 long poll 来获取服务端状态的限制
和 HTTP 协议有什么关系
首先我们来看一下 Websocket 协议和 HTTP 有什么关系呢?
本质上说, Websocket 和 HTTP 就不是一个协议, 层级不一样但是为了兼容现有浏览器的握手规范, 必须借助 HTTP 协议建立连接
这是一个 Websocket 的握手请求
- GET wss://server.example.com/ HTTP/1.1
- Host: server.example.com
- Pragma: no-cache
- Cache-Control: no-cache
- Connection: Upgrade
- Upgrade: websocket
- Origin: https://server.example.com
- Accept-Encoding: gzip, deflate, br
- Sec-WebSocket-Version: 13
- Sec-WebSocket-Key: fFFIlFcwULSAmQacRAbS2A==
- Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
这里面有几个和一般 HTTP Request 不一样的地方,
- Connection: Upgrade
- Upgrade: websocket
- Sec-WebSocket-Version: 13
- Sec-WebSocket-Key: fFFIlFcwULSAmQacRAbS2A==
- Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
这是告诉服务端这不是一个普通的请求, 而是 Websocket 协议 Sec-WebSocket-Key 是一个 Base64 encode 的值, 是浏览器随机生成的, 用于让服务端知道这是一个全新的 socket 客户端
服务端如果开启了 Socket 监听, 那么就会返回这样的 Response
- HTTP/1.1 101 Switching Protocols
- Date: Fri, 09 Mar 2018 16:24:45 GMT
- Connection: upgrade
- upgrade: websocket
- sec-websocket-accept: i/tCy92JmOXIoZwGi8ROh6CgUwk=
表示接收了请求, 并且即将切换到 Websocket 协议, 所以 code 是 101Sec-WebSocket-Accept 这个则是经过服务器确认, 并且加密过后的 Sec-WebSocket-Key 到这里 HTTP 协议的任务就已经完成, 之后的通信都是基于 Websocket 协议了
怎么通过 nginx 转发 Websocket 的握手请求
本质上说握手请求就是一个特殊的 HTTP Request, 只是需要加一些上文提到的特殊内容, 从 Nignx 官方介绍 可以看到
- location /wsapp/ {
- proxy_pass http://wsbackend;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- }
只是在 Request header 加了两个属性, 并且强制升级到 HTTP 1.1, 原因是 HTTP 1.0 不支持 keep alive 如果使用 HTTP 1.0 发握手请求, 服务端返回 101 以后就会直接结束这次 HTTP 会话了这一点也为之后的坑埋下了伏笔
坑从何来
自从上线了 Websocket 服务之后, 就会经常发现 socket 无法建立, 获得 504 的超时响应
- HTTP/1.1 504 Gateway Time-out
- Date: Fri, 09 Mar 2018 03:34:54 GMT
- Content-Type: text/html
- Content-Length: 272
- Connection: keep-alive
而且这一响应只有在经过 SLB(负载均衡) 时才有, 如果直接请求到我们自己的 nginx 是没有问题的但是基于对阿里的信任, 还是觉得问题应该还是我们自己这儿从 code review 到 nginx 配置, 折腾了五六个小时
最后只有自己搭建的 nginx access log 上寻找蛛丝马迹, 一开始抓到一些响应都是 499 的返回, 并且 request_time 时间都在 60s 上下
[09/Mar/2018:15:04:51 +0800] 100.97.89.10 - - - 10.0.21.11 to: 10.0.20.11:8011: GET /ws/?id=168451&url= http://server.example.com/ HTTP/1.0 upstream_response_time - msec 1520579091.139 request_time 60.000 status 499 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
就考虑是不是 socket 服务端建立连接后响应不及时, 让 SLB 发现 60s 没有报文交互直接就切断请求了
但是因为我们在前端是做了心跳的, 即使服务端不响应, 只要 socket 建立通过心跳肯定也会在 60s 内进行交互不应该出现上面的场景
之后我们把 access log 中 socket 建立成功的请求和不成功的请求分开放到一起对比, 发现不成功的都是 HTTP 1.0 的协议
- [09/Mar/2018:15:03:51 +0800] 100.97.88.238 - - - 10.0.20.11 to: 127.0.0.1:8011: GET /ws/?id=168451&url= http://server.example.com HTTP/1.1 upstream_response_time 11.069 msec 1520579031.198 request_time 11.|
- 069 status 101 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 |
- [09/Mar/2018:15:04:32 +0800] 100.97.88.254 - - - 10.0.20.11 to: 127.0.0.1:8011: GET /ws/?id=168451&url= http://server.example.com HTTP/1.0 upstream_response_time - msec 1520579072.716 request_time 36.755 s|
- tatus 499 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
就好像这两个请求, 同一个页面发出的, 但是一个成功一个失败失败的正好就是 HTTP/1.0, 为什么会有两个版本的协议呢,
为了证据更加确凿, 我们对请求进行了抓包分析, 并将 Sec-WebSocket-Key 打印到 Nginx 的 access log 中方便 trace 同一个请求
- GET http://server.example.com/ws/ HTTP/1.1
- Host: app.linkflowtech.com
- Connection: Upgrade
- Pragma: no-cache
- Cache-Control: no-cache
- Upgrade: websocket
- Origin: http://server.example.com
- Sec-WebSocket-Key: 8+qDYeKJGFTWKB2ov4p5TA==
- Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
- [09/Mar/2018:17:07:07 +0800] 100.97.88.252 - - - 10.0.21.11 to: 10.0.20.11:8011: GET /ws/ HTTP/1.0 upstream_response_time - msec 1520586427.537 request_time 59.999 status 499 client - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 8+qDYeKJGFTWKB2ov4p5TA==
- 2018-03-09 17:12:04
可以看到都是 8+qDYeKJGFTWKB2ov4p5TA== 的请求, 但是在经过 SLB 进入 nginx 时候协议降级到了 1.0. 这叫一个酸爽, 赶紧给阿里云开了工单, 经过大概 3~4 个小时的交流最终获得一个链接, 里面有这样的描述
如何在阿里云负载均衡上启用 WS/WSS 支持?
无需配置, 当选用 HTTP 监听时, 默认支持无加密版本 WebSocket 协议 (WS 协议); 当选择 HTTPS 监听时, 默认支持加密版本的 WebSocket 协议 (WSS 协议)
注意: 需要将实例升级为 性能保障型实例 详细参见如何使用负载均衡性能保障型实例
这个大坑就在注意那一段, 我们的 SLB 是性能共享型而不是性能保障型看来也不是阿里云的问题, 是我们的 SLB 档次不够高啊知道原因后, 立刻付费升级了保障型实测一下所有问题都解决了
虽然问题解决了, 但是其实很难理解厂商的逻辑, 为什么性能共享型中某些 SLB 节点就会降级 HTTP 协议版本呢, 要知道 1.0 版本已经是一个相当落后的版本了
在此记录一下心路历程, 为了让其他使用阿里云的同学不要重蹈覆辙
来源: http://www.tuicool.com/articles/va6bimN