2小时快速搭建一个高可用的IM系统( 六 )

首先我们使用 util.Bind(request, &user) 将用户参数绑定到 user 对象上 , 使用的是 util 包中的 Bind 函数 , 具体实现细节读者可以自行研究 , 主要模仿了 Gin 框架的参数绑定 , 可以拿来即用 , 非常方便 。
然后我们根据用户手机号搜索数据库中是否已经存在 , 如果不存在就插入到数据库中 , 返回注册成功信息 , 逻辑非常简单 。
登录逻辑更简单:
############################ //app/controller/user.go ############################ ... //用户登录 func UserLogin(writer http.ResponseWriter, request *http.Request) {request.ParseForm()mobile := request.PostForm.Get("mobile")plainpwd := request.PostForm.Get("passwd")//校验参数if len(mobile) == 0 || len(plainpwd) == 0 {util.RespFail(writer, "用户名或密码不正确")}loginUser, err := UserService.Login(mobile, plainpwd)if err != nil {util.RespFail(writer, err.Error())} else {util.RespOk(writer, loginUser, "")} } ... ############################ //app/service/user.go ############################ ... func (s *UserService) Login(mobile, plainpwd string) (user model.User, err error) {//数据库操作loginUser := model.User{}model.DbEngine.Where("mobile = ?", mobile).Get(&loginUser)if loginUser.Id == 0 {return loginUser, errors.New("用户不存在")}//判断密码是否正确if !util.ValidatePasswd(plainpwd, loginUser.Salt, loginUser.Passwd) {return loginUser, errors.New("密码不正确")}//刷新用户登录的token值token := util.GenRandomStr(32)loginUser.Token = tokenmodel.DbEngine.ID(loginUser.Id).Cols("token").Update(&loginUser)//返回新用户信息return loginUser, nil } ... ############################ //main.go ############################ ...... func main() {http.HandleFunc("/user/login", controller.UserLogin) } 实现了登录逻辑 , 接下来我们就到了用户首页 , 这里列出了用户列表 , 点击即可进入聊天页面 。
用户也可以点击下边的 Tab 栏查看自己所在的群组 , 可以由此进入群组聊天页面 。
具体这些工作还需要读者自己开发用户列表、添加好友、创建群组、添加群组等功能 , 这些都是一些普通的 API 开发工作 , 我们的代码程序中也实现了 , 读者可以拿去修改使用 , 这里就不再演示了 。
我们再重点看一下用户鉴权这一块吧 , 用户鉴权是指用户点击聊天进入聊天界面时 , 客户端会发送一个 GET 请求给服务端 。
请求建立一条 WebSocket 长连接 , 服务端收到建立连接的请求之后 , 会对客户端请求进行校验 , 以确实是否建立长连接 , 然后将这条长连接的句柄添加到 Map 当中(因为服务端不仅仅对一个客户端服务 , 可能存在千千万万个长连接)维护起来 。
我们下边来看具体代码实现:
############################ //app/controller/chat.go ############################ ...... //本核心在于形成userid和Node的映射关系 type Node struct {Conn *websocket.Conn//并行转串行,DataQueue chan []byteGroupSets set.Interface } ...... //userid和Node映射关系表 var clientMap map[int64]*Node = make(map[int64]*Node, 0) //读写锁 var rwlocker sync.RWMutex //实现聊天的功能 func Chat(writer http.ResponseWriter, request *http.Request) {query := request.URL.Query()id := query.Get("id")token := query.Get("token")userId, _ := strconv.ParseInt(id, 10, 64)//校验token是否合法islegal := checkToken(userId, token)conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {return islegal},}).Upgrade(writer, request, nil)if err != nil {log.Println(err.Error())return}//获得websocket链接connnode := &Node{Conn:conn,DataQueue: make(chan []byte, 50),GroupSets: set.New(set.ThreadSafe),}//获取用户全部群IdcomIds := concatService.SearchComunityIds(userId)for _, v := range comIds {node.GroupSets.Add(v)}rwlocker.Lock()clientMap[userId] = noderwlocker.Unlock()//开启协程处理发送逻辑go sendproc(node)//开启协程完成接收逻辑go recvproc(node)sendMsg(userId, []byte("welcome!")) }......//校验token是否合法 func checkToken(userId int64, token string) bool {user := UserService.Find(userId)return user.Token == token }......############################ //main.go ############################ ...... func main() {http.HandleFunc("/chat", controller.Chat) } ...... 进入聊天室 , 客户端发起 /chat 的 GET 请求 , 服务端首先创建了一个 Node 结构体 , 用来存储和客户端建立起来的 WebSocket 长连接句柄 。
每一个句柄都有一个管道 DataQueue , 用来收发信息 , GroupSets 是客户端对应的群组信息 , 后边我们会提到 。


推荐阅读