本文为开源工程:“github.com/GuoZhaoran/fastIM”的配套文章,原作者:“绘你一世倾城”,现为:猎豹移动php开发工程师,感谢原作者的技术分享。
52im_qr_即时通讯技术圈_400px.png (17.47 KB, 下载次数: 2456)
下载附件 保存到相册
4 年前 上传
1.jpg (14.5 KB, 下载次数: 2584)
2.jpg (7.17 KB, 下载次数: 2545)
3.jpg (40.2 KB, 下载次数: 2503)
4.png (21.66 KB, 下载次数: 2486)
5.jpg (42.74 KB, 下载次数: 2528)
public function dohandshake($sock, $data, $key) { if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) { $response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: " . $response . "\r\n\r\n"; socket_write($sock, $upgrade, strlen($upgrade)); $this->isHand[$key] = true; } }
6.jpg (16.01 KB, 下载次数: 2554)
7.jpg (51.46 KB, 下载次数: 2536)
8.jpg (46.74 KB, 下载次数: 2450)
package main import ( "net/http" "time" "github.com/gorilla/websocket" ) var ( //完成握手操作 upgrade = websocket.Upgrader{ //允许跨域(一般来讲,websocket都是独立部署的) CheckOrigin:func(r *http.Request) bool { return true }, } ) func wsHandler(w http.ResponseWriter, r *http.Request) { var ( conn *websocket.Conn err error data []byte ) //服务端对客户端的http请求(升级为websocket协议)进行应答,应答之后,协议升级为websocket,http建立连接时的tcp三次握手将保持。 if conn, err = upgrade.Upgrade(w, r, nil); err != nil { return } //启动一个协程,每隔1s向客户端发送一次心跳消息 go func() { var ( err error ) for { if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil { return } time.Sleep(1 * time.Second) } }() //得到websocket的长链接之后,就可以对客户端传递的数据进行操作了 for { //通过websocket长链接读到的数据可以是text文本数据,也可以是二进制Binary if _, data, err = conn.ReadMessage(); err != nil { goto ERR } if err = conn.WriteMessage(websocket.TextMessage, data); err != nil { goto ERR } } ERR: //出错之后,关闭socket连接 conn.Close() } func main() { http.HandleFunc("/ws", wsHandler) http.ListenAndServe("0.0.0.0:7777", nil) }
9.jpg (34.78 KB, 下载次数: 2486)
10.jpg (33.22 KB, 下载次数: 2481)
app │ ├── args │ │ ├── contact.go │ │ └── pagearg.go │ ├── controller //控制器层,api入口 │ │ ├── chat.go │ │ ├── contract.go │ │ ├── upload.go │ │ └── user.go │ ├── main.go //程序入口 │ ├── model //数据定义与存储 │ │ ├── community.go │ │ ├── contract.go │ │ ├── init.go │ │ └── user.go │ ├── service //逻辑实现 │ │ ├── contract.go │ │ └── user.go │ ├── util //帮助函数 │ │ ├── md5.go │ │ ├── parse.go │ │ ├── resp.go │ │ └── string.go │ └── view //模版资源 │ │ ├── ... asset //js、css文件 resource //上传资源,上传图片会放到这里
func registerView() { tpl, err := template.ParseGlob("./app/view/**/*") if err != nil { log.Fatal(err.Error()) } for _, v := range tpl.Templates() { tplName := v.Name() http.HandleFunc(tplName, func(writer http.ResponseWriter, request *http.Request) { tpl.ExecuteTemplate(writer, tplName, nil) }) } } ... func main() { ...... http.Handle("/asset/", http.FileServer(http.Dir("."))) http.Handle("/resource/", http.FileServer(http.Dir("."))) registerView() log.Fatal(http.ListenAndServe(":8081", nil)) }
11.jpg (15.12 KB, 下载次数: 2580)
12.jpg (22.52 KB, 下载次数: 2505)
package model import "time" const ( SexWomen = "W" SexMan = "M" SexUnknown = "U" ) type User struct { Id int64 `xorm:"pk autoincr bigint(64)" form:"id" json:"id"` Mobile string `xorm:"varchar(20)" form:"mobile" json:"mobile"` Passwd string `xorm:"varchar(40)" form:"passwd" json:"-"` // 用户密码 md5(passwd + salt) Avatar string `xorm:"varchar(150)" form:"avatar" json:"avatar"` Sex string `xorm:"varchar(2)" form:"sex" json:"sex"` Nickname string `xorm:"varchar(20)" form:"nickname" json:"nickname"` Salt string `xorm:"varchar(10)" form:"salt" json:"-"` Online int `xorm:"int(10)" form:"online" json:"online"` //是否在线 Token string `xorm:"varchar(40)" form:"token" json:"token"` //用户鉴权 Memo string `xorm:"varchar(140)" form:"memo" json:"memo"` Createat time.Time `xorm:"datetime" form:"createat" json:"createat"` //创建时间, 统计用户增量时使用 }
package model import ( "errors" "fmt" _ "github.com/go-sql-driver/mysql" "github.com/go-xorm/xorm" "log" ) var DbEngine *xorm.Engine func init() { driverName := "mysql" dsnName := "root:root@(127.0.0.1:3306)/chat?charset=utf8" err := errors.New("") DbEngine, err = xorm.NewEngine(driverName, dsnName) if err != nil && err.Error() != ""{ log.Fatal(err) } DbEngine.ShowSQL(true) //设置数据库连接数 DbEngine.SetMaxOpenConns(10) //自动创建数据库 DbEngine.Sync(new(User), new(Community), new(Contact)) fmt.Println("init database ok!") }
############################ //app/controller/user.go ############################ ...... //用户注册 func UserRegister(writer http.ResponseWriter, request *http.Request) { var user model.User util.Bind(request, &user) user, err := UserService.UserRegister(user.Mobile, user.Passwd, user.Nickname, user.Avatar, user.Sex) if err != nil { util.RespFail(writer, err.Error()) } else { util.RespOk(writer, user, "") } } ...... ############################ //app/service/user.go ############################ ...... type UserService struct{} //用户注册 func (s *UserService) UserRegister(mobile, plainPwd, nickname, avatar, sex string) (user model.User, err error) { registerUser := model.User{} _, err = model.DbEngine.Where("mobile=? ", mobile).Get(®isterUser) if err != nil { return registerUser, err } //如果用户已经注册,返回错误信息 if registerUser.Id > 0 { return registerUser, errors.New("该手机号已注册") } registerUser.Mobile = mobile registerUser.Avatar = avatar registerUser.Nickname = nickname registerUser.Sex = sex registerUser.Salt = fmt.Sprintf("%06d", rand.Int31n(10000)) registerUser.Passwd = util.MakePasswd(plainPwd, registerUser.Salt) registerUser.Createat = time.Now() //插入用户信息 _, err = model.DbEngine.InsertOne(®isterUser) return registerUser, err } ...... ############################ //main.go ############################ ...... func main() { http.HandleFunc("/user/register", controller.UserRegister) }
############################ //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 = token model.DbEngine.ID(loginUser.Id).Cols("token").Update(&loginUser) //返回新用户信息 return loginUser, nil } ... ############################ //main.go ############################ ...... func main() { http.HandleFunc("/user/login", controller.UserLogin) }
############################ //app/controller/chat.go ############################ ...... //本核心在于形成userid和Node的映射关系 type Node struct { Conn *websocket.Conn //并行转串行, DataQueue chan []byte GroupSets 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链接conn node := &Node{ Conn: conn, DataQueue: make(chan []byte, 50), GroupSets: set.New(set.ThreadSafe), } //获取用户全部群Id comIds := concatService.SearchComunityIds(userId) for _, v := range comIds { node.GroupSets.Add(v) } rwlocker.Lock() clientMap[userId] = node rwlocker.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) } ......
type Node struct { Conn *websocket.Conn //并行转串行, DataQueue chan []byte GroupSets set.Interface }
//userid和Node映射关系表 var clientMap map[int64]*Node = make(map[int64]*Node, 0)
//校验token是否合法 islegal := checkToken(userId, token) conn, err := (&websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return islegal }, }).Upgrade(writer, request, nil)
//获得websocket链接conn node := &Node{ Conn: conn, DataQueue: make(chan []byte, 50), GroupSets: set.New(set.ThreadSafe), } //获取用户全部群Id comIds := concatService.SearchComunityIds(userId) for _, v := range comIds { node.GroupSets.Add(v) } rwlocker.Lock() clientMap[userId] = node rwlocker.Unlock()
...... //开启协程处理发送逻辑 go sendproc(node) //开启协程完成接收逻辑 go recvproc(node) sendMsg(userId, []byte("welcome!")) ......
############################ //app/controller/chat.go ############################ type Message struct { Id int64 `json:"id,omitempty" form:"id"` //消息ID Userid int64 `json:"userid,omitempty" form:"userid"` //谁发的 Cmd int `json:"cmd,omitempty" form:"cmd"` //群聊还是私聊 Dstid int64 `json:"dstid,omitempty" form:"dstid"` //对端用户ID/群ID Media int `json:"media,omitempty" form:"media"` //消息按照什么样式展示 Content string `json:"content,omitempty" form:"content"` //消息的内容 Pic string `json:"pic,omitempty" form:"pic"` //预览图片 Url string `json:"url,omitempty" form:"url"` //服务的URL Memo string `json:"memo,omitempty" form:"memo"` //简单描述 Amount int `json:"amount,omitempty" form:"amount"` //其他和数字相关的 }
//定义命令行格式 const ( CmdSingleMsg = 10 CmdRoomMsg = 11 CmdHeart = 0 )
############################ //app/controller/chat.go ############################ ...... //发送逻辑 func sendproc(node *Node) { for { select { case data := <-node.DataQueue: err := node.Conn.WriteMessage(websocket.TextMessage, data) if err != nil { log.Println(err.Error()) return } } } } //接收逻辑 func recvproc(node *Node) { for { _, data, err := node.Conn.ReadMessage() if err != nil { log.Println(err.Error()) return } dispatch(data) //todo对data进一步处理 fmt.Printf("recv<=%s", data) } } ...... //后端调度逻辑处理 func dispatch(data []byte) { msg := Message{} err := json.Unmarshal(data, &msg) if err != nil { log.Println(err.Error()) return } switch msg.Cmd { case CmdSingleMsg: sendMsg(msg.Dstid, data) case CmdRoomMsg: for _, v := range clientMap { if v.GroupSets.Has(msg.Dstid) { v.DataQueue <- data } } case CmdHeart: //检测客户端的心跳 } } //发送消息,发送到消息的管道 func sendMsg(userId int64, msg []byte) { rwlocker.RLock() node, ok := clientMap[userId] rwlocker.RUnlock() if ok { node.DataQueue <- msg } } ......
func sendproc(node *Node) { for { select { case data := <-node.DataQueue: err := node.Conn.WriteMessage(websocket.TextMessage, data) if err != nil { log.Println(err.Error()) return } } } }
func recvproc(node *Node) { for { _, data, err := node.Conn.ReadMessage() if err != nil { log.Println(err.Error()) return } dispatch(data) //todo对data进一步处理 fmt.Printf("recv<=%s", data) } }
func dispatch(data []byte) { msg := Message{} err := json.Unmarshal(data, &msg) if err != nil { log.Println(err.Error()) return } switch msg.Cmd { case CmdSingleMsg: sendMsg(msg.Dstid, data) case CmdRoomMsg: for _, v := range clientMap { if v.GroupSets.Has(msg.Dstid) { v.DataQueue <- data } } case CmdHeart: //检测客户端的心跳 } } ...... func sendMsg(userId int64, msg []byte) { rwlocker.RLock() node, ok := clientMap[userId] rwlocker.RUnlock() if ok { node.DataQueue <- msg } }
13.jpg (22.74 KB, 下载次数: 2492)
14.jpg (22.7 KB, 下载次数: 2566)
{ "dstid":1, "cmd":10, "userid":2, "media":4, "url":"/asset/plugins/doutu//emoj/2.gif" }
############################ //app/controller/upload.go ############################ func init() { os.MkdirAll("./resource", os.ModePerm) } func FileUpload(writer http.ResponseWriter, request *http.Request) { UploadLocal(writer, request) } //将文件存储在本地/im_resource目录下 func UploadLocal(writer http.ResponseWriter, request *http.Request) { //获得上传源文件 srcFile, head, err := request.FormFile("file") if err != nil { util.RespFail(writer, err.Error()) } //创建一个新的文件 suffix := ".png" srcFilename := head.Filename splitMsg := strings.Split(srcFilename, ".") if len(splitMsg) > 1 { suffix = "." + splitMsg[len(splitMsg)-1] } filetype := request.FormValue("filetype") if len(filetype) > 0 { suffix = filetype } filename := fmt.Sprintf("%d%s%s", time.Now().Unix(), util.GenRandomStr(32), suffix) //创建文件 filepath := "./resource/" + filename dstfile, err := os.Create(filepath) if err != nil { util.RespFail(writer, err.Error()) return } //将源文件拷贝到新文件 _, err = io.Copy(dstfile, srcFile) if err != nil { util.RespFail(writer, err.Error()) return } util.RespOk(writer, filepath, "") } ...... ############################ //main.go ############################ func main() { http.HandleFunc("/attach/upload", controller.FileUpload) }
来源:即时通讯网 - 即时通讯开发者社区!
轻量级开源移动端即时通讯框架。
快速入门 / 性能 / 指南 / 提问
轻量级Web端即时通讯框架。
详细介绍 / 精编源码 / 手册教程
移动端实时音视频框架。
详细介绍 / 性能测试 / 安装体验
基于MobileIMSDK的移动IM系统。
详细介绍 / 产品截图 / 安装体验
一套产品级Web端IM系统。
详细介绍 / 产品截图 / 演示视频
引用此评论
引用:超超超 发表于 2020-10-12 11:31 楼主有sql脚本嘛
引用:15805817394 发表于 2022-01-05 15:01 写得真好
精华主题数超过100个。
连续任职达2年以上的合格正式版主
为论区做出突出贡献的开发者、版主等。
Copyright © 2014-2024 即时通讯网 - 即时通讯开发者社区 / 版本 V4.4
苏州网际时代信息科技有限公司 (苏ICP备16005070号-1)
Processed in 0.312500 second(s), 45 queries , Gzip On.