tinode
- 支持websocket, long polling(长轮询), grpc
- 支持3种数据库:MySQL, Mongodb, rethinkdb
- 完整的客户端: web,ios,andorid
- 后端编程语言:Golang
下载代码
Go Modules
- 设置proxy
运行
goland配置
注意
- 要配置好tinode.conf, 根据你设置的-tags 来设置数据库相关连接
- static_data指向的目录,可以从github上的release直接下载
服务端编译
在goland的配置里已经展现了
一些琐碎的点
- 配置文件里的api_key_salt不是字符串,在代码里是[]byte类型
- MySQL,Mongodb,RethinkDb3种数据库被称为store, 抽象为adapter.
- 数据库类型的选在在go build -tags 指定的, 因为只编译对应的代码,就只有对应的数据库被注册到adapter里了
- 用websocket的时候,如果不勾选
保持登录
一刷新页面就退出了 - 用websocket的时候心跳包机制没搞懂
- 客户端可能发送1,服务端响应0
func (s *Session) dispatchRaw(raw []byte) { var msg ClientComMessage if len(raw) == 1 && raw[0] == 0x31 { // 0x31 == '1'. This is a network probe message. Respond with a '0': s.queueOutBytes([]byte{0x30}) return } // ....
- 在每个ws连接的writeLoop里是有如下代码在发送心跳
case <-ticker.C: if err := wsWrite(sess.ws, websocket.PingMessage, nil); err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) { log.Println("ws: writeLoop ping", sess.sid, err) } return }
- 对应的日志 :
2020/03/04 14:44:31 out: mt = `9`, ``
- 但是web页面的console里没有看到对应的pong消息,这是为啥呢?
- *的解释
- 就如上面的
*的解释
一样,底层协议会自动回复pong,这是ws协议,web端不需要任何显示代码 - 关闭前端网页的时候,是能看到相关退出的日志的
2020/03/04 16:36:38 readLoop, websocket: close 1001 (going away) 2020/03/04 16:36:38 readLoop, defer ,zbz6L4jbbcI
- 对应的日志 :
- 客户端可能发送1,服务端响应0
- 每一个连接都是一个session,不论这个连接是http长轮询,ws,grpc
- 每一个session都有一个sid,所有的session都保存在一个map里,sid为key
- 每一个websocket的连接都有2个goroutine: 一个负责读,一个负责写
- ws的写(负责向客户端发送消息)使用了一个带缓存的channel, 负责读的goroutine把消息写入这个channel
- 读的groutine通过读取ws, 通过switch判断然后给不同的处理函数
- 读有一个plugin的处理过程,默认的只有一个
python_chat_bot
且被disable掉的
- 读有一个plugin的处理过程,默认的只有一个
- 时间都是用的UTC时间,server/store/types/types.go
func TimeNow() time.Time { return time.Now().UTC().Round(time.Millisecond) }
- 发起ws连接的时候apikey非常重要,是连上服务器的凭据
只要有个apikey就能连上服务器,而且一直能连上,感觉像是个bugws://127.0.0.1:6060/v0/channels?apikey=AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K
- websocket是没有压缩的
- 所有的消息都有在服务器上存储: messges这个collections里,带有seqid,而且还挺复杂
- token不是每次和服务器的交流都带着的,好像只是在修改某些东西的时候带着,如果token过期了呢?
- 在登录后返回的
ctrl
消息里带有token和token的过期时间
- 在登录后返回的
基本概念
session
会话是客户端应用程序与服务器之间的网络连接
- 建立会话后,用户可以开始通过主题(topic)与其他用户进行交互
user
代表通过会话(session)连接到服务器的人
- 用户ID是唯一的,带有’usr’前缀的字符串,后跟base64-URL编码的伪随机64位数字,例如usr2il9suCbuko
- 同一用户可以同时建立多个会话
client
诸如移动或Web应用程序之类的客户端
- 客户端需要身份验证才能执行大多数操作
topic
会话(session)之间路由内容的命名通信渠道
- topic id
流程
登录
-
hi
- 客户端发出
hi
{ "hi": { "id": "69718", "ver": "0.16.8-beta1", "ua": "TinodeWeb/0.16.8 (Firefox/83.0; Win32); tinodejs/0.16.8-beta1", "lang": "zh-CN" } }
- 服务端响应
{ "ctrl": { "id": "69718", "params": { "build": "mysql:undef", "maxFileUploadSize": 8388608, "maxMessageSize": 262144, "maxSubscriberCount": 128, "maxTagCount": 16, "maxTagLength": 96, "minTagLength": 2, "ver": "0.16" }, "code": 201, "text": "created", "ts": "2020-11-25T08:26:14.072Z" } }
- 客户端发出login
{ "login": { "id": "69719", "scheme": "basic", "secret": "YWxpY2U6YWxpY2UxMjM=" } }
-
schema
为basic,表示用户名密码登录,其他的方式参见配置文件tinode.conf
里的auth_config
- secret类似http里的HTTP基本认证,是用户和密码的组和的base64编码
-
- 服务端返回
{ "ctrl": { "id": "69719", "params": { "authlvl": "auth", "expires": "2020-12-09T08:26:14.17Z", "token": "VvZ8n1ZhmwAmitBfFAABAAEA7bOB+w3aXZ1J/59lZj44Ilm8yqEao912fbrCyMwinGc=", "user": "usrVvZ8n1ZhmwA" }, "code": 200, "text": "ok", "ts": "2020-11-25T08:26:14.089Z" } }
- server/session.go
这里根据客户端发出的
login.schema
来选择验证方法.func (s *Session) login(msg *ClientComMessage) { // handler := store.GetLogicalAuthHandler(msg.Login.Scheme) //... }
-
basic
模式为例:server/auth/basic/auth_basic.go的func (a *authenticator) Authenticate(secret []byte) (*auth.Rec, []byte, error) { // 从数据库拿到相关的记录,传入的参数是: a.name是模式,比如basic, uname是用户名 // 从`auth`表里根据basic:uname获取记录 uid, authLvl, passhash, expires, err := store.Users.GetAuthUniqueRecord(a.name, uname) // 校验uid if uid.IsZero() { // Invalid login. return nil, nil, types.ErrFailed } // 校验过期时间 if !expires.IsZero() && expires.Before(time.Now()) { // The record has expired return nil, nil, types.ErrExpired } // 校验密码,把明文密码加密后和数据库取出的secret字段对比 err = bcrypt.CompareHashAndPassword(passhash, []byte(password)) if err != nil { // Invalid password return nil, nil, types.ErrFailed } }
- auth表的结构
uname userid scheme authlvl secret expires asic:alice 803470600923254000 basic 20 $2a 10 10 10QuAbMRkzzB/M7wGTTnxRj.DYa34BcXrfMIwh3jylpgot5BeN8KEVS
- auth表的结构
- server/session.go
- 客户端发出
-
交互消息,out是客户端发出,in是客户端接收
out: {"hi":{"id":"83985","ver":"0.16.3","ua":"TinodeWeb/0.16.3 (Chrome/80.0; Win32); tinodejs/0.16.3","lang":"zh-CN"}}
in: {"ctrl":{"id":"83985","params":{"build":"mongodb:undef","maxFileUploadSize":8388608,"maxMessageSize":262144,"maxSubscriberCount":128,"maxTagCount":16,"ver":"0.16"},"code":201,"text":"created","ts":"2020-03-23T07:55:58.439Z"}}
out: {"login":{"id":"83986","scheme":"basic","secret":"bWExMjM0Ok1hQDEyMzQ="}}
in:{"ctrl":{"id":"83986","params":{"authlvl":"auth","expires":"2020-04-06T07:55:58.504Z","token":"Q31lre9hlo6O4IpeFAABAAEAUo6b0ItShDLjnW15vXYZzvVjg2zE++bUZ6swdvVbcro=","user":"usrQ31lre9hlo4"},"code":200,"text":"ok","ts":"2020-03-23T07:55:58.441Z"}}
out: {"sub":{"id":"83987","topic":"me","get":{"what":"sub desc tags cred"}}}
in: {"ctrl":{"id":"83987","topic":"me","code":200,"text":"ok","ts":"2020-03-23T07:55:58.512Z"}}
in: {"meta":{"id":"83987","topic":"me","ts":"2020-03-23T07:55:58.513Z","desc":{"updated":"2020-02-17T03:23:38.392Z","touched":"2020-02-17T03:23:38.392Z","defacs":{"auth":"JRWPAS","anon":"N"},"public":{"fn":"马兵"}}}}
in: {"meta":{"id":"83987","topic":"me","ts":"2020-03-23T07:55:58.513Z","sub":[{"updated":"2020-02-17T03:24:32.548Z","acs":{"mode":"JRWPA","given":"JRWPAS","want":"JRWPA"},"read":11,"recv":11,"public":{"fn":"Alice Johnson","photo":{"data":"<8908, bytes: /9j/4AAQSkZJ...sUaqGs//2Q==>","type":"jpg"}},"topic":"usrE7cMWMo0oAU","touched":"2020-03-23T03:38:50.685Z","seq":11},{"updated":"2020-03-05T07:40:23.407Z","acs":{"mode":"JRWPA","given":"JRWPAS","want":"JRWPA"},"public":{"fn":"Bob Smith","photo":{"data":"<7236, bytes: /9j/4AAQSkZJ...30e375//2Q==>","type":"jpg"}},"topic":"usrurYOjmqq3fo","touched":"2020-03-05T07:40:23.407Z"}]}}
in: {"meta":{"id":"83987","topic":"me","ts":"2020-03-23T07:55:58.516Z","tags":["basic:ma1234","email:cumt_ttr@163.com"]}}
in: {"meta":{"id":"83987","topic":"me","ts":"2020-03-23T07:55:58.516Z","cred":[{"meth":"email","val":"cumt_ttr@163.com","done":true},{"meth":"email","val":"rangerforce007@gmail.com"}]}}
in: {"pres":{"topic":"me","src":"usrE7cMWMo0oAU","what":"on"}}
聊天
- 切换聊天对象: 比如在和A聊天,然后点击了B,进入和B聊天的界面
- “topic”:“usrE7cMWMo0oAU”,
E7cMWMo0oAU
是用户的id - "leave"消息表示离开用户的
E7cMWMo0oAU
聊天界面 - "sub"消息表示进入用户
urYOjmqq3fo
的聊天界面
- “topic”:“usrE7cMWMo0oAU”,
tinode.prod.js:1 [03:47:20:828] out: {"leave":{"id":"88960","topic":"usrE7cMWMo0oAU"}}
tinode.prod.js:1 [03:47:20:829] out: {"sub":{"id":"88961","topic":"usrurYOjmqq3fo","get":{"data":{"limit":24},"sub":{"ims":"2020-03-23T03:44:44.118Z"},"desc":{"ims":"2020-03-23T03:44:44.118Z"},"what":"data sub desc"}}}
tinode.prod.js:1 [03:47:20:844] in: {"ctrl":{"id":"88960","topic":"usrE7cMWMo0oAU","code":200,"text":"ok","ts":"2020-03-23T03:47:20.829Z"}}
tinode.prod.js:1 [03:47:20:847] in: {"ctrl":{"id":"88961","topic":"usrurYOjmqq3fo","code":200,"text":"ok","ts":"2020-03-23T03:47:20.838Z"}}
tinode.prod.js:1 [03:47:20:848] in: {"meta":{"id":"88961","topic":"usrurYOjmqq3fo","ts":"2020-03-23T03:47:20.838Z","desc":{"created":"2020-03-05T07:40:23.407Z","updated":"2020-03-05T07:40:23.407Z","touched":"2020-03-05T07:40:23.407Z","acs":{"mode":"JRWPA","given":"JRWPAS","want":"JRWPA"}}}}
tinode.prod.js:1 [03:47:20:848] in: {"meta":{"id":"88961","topic":"usrurYOjmqq3fo","ts":"2020-03-23T03:47:20.838Z","sub":[{"updated":"2020-03-05T07:40:23.407Z","acs":{"mode":"JRWPA","given":"JRWPAS","want":"JRWPA"},"user":"usrQ31lre9hlo4"},{"updated":"2020-03-05T07:40:23.408Z","acs":{"mode":"JRWPA","given":"JRWPA","want":"JRWPA"},"user":"usrurYOjmqq3fo"}]}}
tinode.prod.js:1 [03:47:20:848] in: {"ctrl":{"id":"88961","topic":"usrurYOjmqq3fo","params":{"count":0,"what":"data"},"code":200,"text":"ok","ts":"2020-03-23T03:47:20.840Z"}}