环境
后端
- 语言golang
- 数据库elastisearch
- web框架是基于gin封装的
- websocket库用的是gorilla/websocket
- 日志: zap
前端
- 框架: react
- 服务端渲染: nextjs
- 登录认证: cookie nookie
- websocket是浏览器原生支持
- 后续聊天功能模仿:https://getstream.io/
登录
体验地址:
PC体验较好,h5只是做了一个评论组件。支持Github/Google登录,只用作登录。有人建议我放开登录,但是我相信有耐心看文字的人,也会有实际点击体验的人
如何建立连接
websocket复用http的握手通道,客户端通过HTTP请求与WebSocket服务端协商升级协议,协议升级完成后,后续的数据交换则按按照websocket的协议。
1. 客户端:申请协议升级
Connection: UpgradeUpgrade: websocketSec-WebSocket-Version: 13Sec-WebSocket-VersionSec-WebSocket-Key: m3xNMIzhueJyd3N66EAK6w==Sec-WebSocket-Accept
2. 服务端:响应协议升级
101
3. Sec-WebSocket-Accept的计算
Sec-WebSocket-Accept
Sec-WebSocket-AcceptSec-WebSocket-Key
计算公式为:
Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11
伪代码如下:
验证下前面的返回结果:
前端难点
服务端渲染框架如何使用cookie
nextjs是服务端渲染框架,nextjs会请求是在服务器上初始化一次dom,要获取cookie可以有2种方式
- 从请求request中获取cookie,返回给组件
- 客户端请求完成后用hook从浏览器本地加载cookie
cookie 库用的是 nookies
另一个就是本地开发cookie跨域的问题, 本地域名localhost:3000,服务端域名localhost:9000:
本来是想用http-proxy-middleware 来解决跨域的问题,但是websocket代理关于cookie的问题难以搞定,版本是1.0.3
最后还是nginx了,怎么统一一个域名呢?location,后端服务统一以api开头
修改本地dns http://douyacun.io => 127.0.0.1
如何保证websocket初始化一次
还是因为框架用的nextjs服务端渲染框架,不能在全局初始化websocket,只能在useEffect中使用
这样会有一个问题,只要有state变更,useEffect就会被调用,导致webSocket循环初始化
todo: 后端服务如何识别这种重复性的初始化,如何拒绝?
react hook 文档给出了答案
[]
conn.onmessage(() => {setMessages()}) 不会触发state组件的重新渲染?
这个问题没有弄懂,不过react hook提供另一种状态管理,useReducer(), 就像redux一样。
useReducer
滑动条的位置
- 收到新消息时,滑动条回到最底部
hook 提供了useRef可以操作组件, 在收到消息时,控制滑动条到最底部,
- 上拉加载更多历史消息
滑动条高度 = 加载历史消息完成后文档高度 - 加载前文档高度
- 正在查看历史消息,如果有新消息来会导致滑动条到最底部
todo::
nginx如何反向代理websocket?
前端开发环境配置的时候已经展示过一次
proxy_http_version 1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection 'upgrade';
群聊和私聊是如何实现的
不管是私聊还是群聊,都会创建一个channel来包含成员,每次消息发送消息指定channel.id即可,服务器在收到消息后,根据channel.id向channel.members推送消息。
channel分了3中类型:
- global 全局,每个人都会订阅
- public 群聊 聊天人数超过2个
- private 私聊 聊天人群2个
一个连接的成本是多少?
这边使用gorilla/websocket 1万个连接测试
占用147.93MB RAM, 平均连接每个占用15kb 测试代码见:github gwebsocket
goroutine是10003,每个goroutine占用4kb的内存
根据 Eran Yanay 在 Gophercon Israel 分享的讲座 https://www.youtube.com/watch?reload=9&v=LI1YTFMi8W4 优化, 代码在github
这边使用epoll 内存节省了 147.93 - 79.94 = 67.99MB,
运行的goroutine只有5个
wsutil
1w个链接3M内存, 5个goroutine
heartbeat???
使用gobws重构了一下聊天室,每隔一段时间,如果不发消息连接会自动断开,问题定位在nginx上,代理超时的机制,2次读之间的超时。
nginx有2个参数是涉及到timeout
- proxy_read_timeout 默认值 60s 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。 这个时间不是获得整个response的时间,而是两次reading操作的时间。
- proxy_send_timeout 默认值 60s 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。 如果超时后,upstream没有收到新的数据,nginx会关闭连接
如何解决?
通过定期发送ping帧以保持连接并确认连接是否还在使用
定时给scoket发送ping消息?
如果每个connection持有一个goroutine的话,初始化一个ticker定时器每隔N秒发送一次ping就ok了,问题是我们使用epoll来节省goroutine
TODO:: epoll如何实现定时轮询
喜欢可以关注微信公众号: