Skip to content

运用socket通信技术,使用golang作为后端,uni-app作为前端,使用数据库为redis,开发的一款聊天室,实现了登录、注册、聊天等功能。

Notifications You must be signed in to change notification settings

brejce/SocketChat

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Socket

使用方法

服务器端 Server 文件夹

安装 redis 参考官方网站
修改 functool.go 内的数据库地址
修改 main.go 内监听地址
go run /server

前端 SocketuView 文件夹

使用 hbuilderx 导入 SocketuView
修改 app.vue内服务器地址
运行到手机(h5端暂不支持)

示例运行结果可以查看截图文件夹


第四章 数据库与项目的设计及搭建

第一节 数据库的设计与搭建

项目文件结构

server/
  • functool.go /提供公共方法
  • go.mod /go mod 包管理器文件
  • go.sum /go mod 包管理器文件
  • client.go /为每一个客户端提供监听功能
  • hub.go /每一个客户端都集中存储到hub里面
  • message.go /关于 message 的方法
  • user.go /关于 user 的方法
  • main.go /服务器的入口

打开 VsCode 新建文件夹 sverver/ ,文件位置可以任选,本文选择在 /home/brejce/Program/server 。

在终端进入 Server/ 执行以下代码。

go run mod init server

获得如下信息,表示我们已经成功使用 go mod 作为包管理器,这样我们不必手动去导包,go mod 会自动搞定。

go: creating new go.mod: module Server
go: to add module requirements and sums:
        go mod tidy

打开 go.mod 可以看到。

//go.mod
module server
go 1.16
require github.com/go-redis/redis/v8 v8.8.0

redis/v8 这是我们使用 Golang 操作 Redis 数据库关键的 包。

数据结构

Redis数据库主要存储User及Message结构体,代码如下:

//functool.go


//Message -> Db 1
type Message struct {
    Name   string `json:"name"`
    IdTime string `json:"idtime"`
    Msge   string `json:"msge"`
}
//User ->Db 0
type User struct {
    Name   string `json:"name"`
    Passwd string `json:"passwd"`
    Status string `json:"status"`
}

//已登录用户
var UserMap = make(map[int]User)

User 管理

序列化

因在数据库直接存储结构体会导致,存储后输出的结果无法反序列化,所以这里再存储之前会对数据进行序列化。 当然有序列化必然有反序列化,以便后期从Redis获取User。

//user.go


//将User转化为[]byte
func StructTojson(user User) []byte { 
    //将User结构体序列化操作封装成方法
    dataerr := json.Marshal(user) 
    //使用json.Marshal序列化User,data数据类型为[]byte
    CheckError(err)                 
    //error处理
    return data                     
    //返回序列化之后的User
}
//将user型的[]byte转化为User
func JsonTostruct(b []byteUser {
    var user User
    err := json.Unmarshal(b&user)
    CheckError(err)
    return user
}
链接Redis数据库

本文已将链接数据库操作打包为一个方法,直接调用 NewRedis() 即可返回数据库客户端。

//functool.go

func NewRedis(db int*redis.Client { //将数据库连接操作打包为方法使用newRdis(0)方法带入数据库名调用即可
    rdb := redis.NewClient(&redis.Options{
        Addr:     "pi4:6379", //数据库默认安装在开发机,监听localhost,默认端口为6379
        Password: "passwd",              // 使用设置好的redis密码
        DB:       db,                    // 设置要使用的数据库名
    })
    return rdb //返回数据库客户端
}

这里数据库地址为pi4,是因为树莓派4B的主机名(参考上文Manjaro-arm的安装)叫pi4,所以使用pi4这个地址即可。

//functool.go

func TestlinkRedis() ({ //获取单独的一条
    rdb := NewRedis(1)                      //获取redis客户端,并连接到数据库1
    valerr := rdb.Get(ctx, "key").Result() //使用"key"来进行查询,key是一个字符串
    if nil == err {
         fmt.Println("你得到了数据:",val)
    }else{
        fmt.Println("获取数据失败")
    }
    rdb.Close()                             //关闭redis客户端
}

根据TestlinkRedis()方法我们可以得知,获取数据库客户端后可以获得一个 * redis.Client 对象,使用它的Get方法可以获取 "key"对应的"value" 。

将用户User添加到数据库 0 里面

建立 SetUser(user User) bool 方法,我们将一个 User 传入,先判断是否存在该用户 ,如果不存在就保存该用户,存在就不保存,使用 booll 可以帮我们判断用户是否保存成功。

//user.go

//保存该用户到数据库
func SetUser(user Userbool { //保存User
    //使用getUser进行查询
    _, b := GetUser(user)
    if b { //如果有此人就直接返回此人
        return false
    } else {
        rdb := NewRedis(0)                                          //连接数据库0
        err := rdb.Set(ctxuser.NameStructTojson(user), 0).Err() //保存用户
        CheckError(err)
        rdb.Close()
        return true
    }
}

其中可以看到先使用了一个 GetUser() 方法来判断该用户是否存在过,目的是避免同一用户多次存储,造成新用户挤掉老用户的问题 其中还使用到上文所说的 StructTojson(User) 方法。

从数据库 0 获取User

建立 GetUser(user User) (User, bool) 方法,可以看出,使用此方法需要一个 User 对象,因为我们使用 User.Name 作为唯一识别码,所以这里只需要 User.Name 带值即可。

//user.go

//查询该用户,并返回相应数据
func GetUser(user User) (Userbool) { //获取用户全部的信息,这里使用user.Name作为唯一识别号
    rdb := NewRedis(0)                           //连接数据库
    valerr := rdb.Get(ctxuser.Name).Result() //使用user.Name来进行查找
    rdb.Close()
    if nil == err {
        return JsonTostruct([]byte(val)), true //如果有此用户,则返回User,true
    }
    return User{"nil", "nil", "nil"}, false //如果没有此用户,则返回nil,false
}

在上述代码中,从数据库 0 中获取回来的值 val 是一个 string 类型的数据,需要先转化为[]byte类型,再使用 JsonTostruct([]byte) 方法进行反序列化。 GetUser() 方法会返回 User,bool ,可以从bool的值来判断用户获取是否成功。

代码测试

接下来使用一段代码来对上述内容进行测试。

//functool.go

func TestsaveUser() { 
    user := User{
        Name:   "bill",
        Passwd: "123412sdd",
        Statustrue,
    }
    b := SetUser(user)
    if b {
        fmt.Println("oh yeah we saved this user!")
    } else {
        fmt.Println("opps! we alredy have this user!dont save agn!")
    }
    return
}
func TestgetUser() {
    user := User{
        Name:   "bill",
        Passwd: "",
    }
    umsg := GetUser(user)
    if msg {
        fmt.Println("yeah ~~ we have this user :"u)
    } else {
        fmt.Println("opps we dont have this user!")
    }
    return
}

//该方法可以删除Redis里任意数据库 Db 的任意 Key
func DeletSomething(key stringDb int) {
    rdb := NewRedis(Db)
    rdb.Del(ctxkey).Err()
    rdb.Close()
}
运行 TestsaveUser() 方法
func main() {
    TestsaveUser()
}

得到保存成功的信息。

go run server.go functool.go
oh yeah we saved this user!

如再次运行该方法则会得到 我们已经有这个数据了,不要再来保存的提示。

go run server.go functool.go
opps! we alredy have this user!dont save agn!
运行 TestgetUser() 方法
func main() {
    TestgetUser()
}

可以获取到该用户的全部信息。

go run server.go functool.go
yeah ~~ we have this user : {bill 123412sdd true}
运行DeletSomething()
//functool.go
func main() {
    DeletSomething("bill",0)
}

没有返回信息,根据  rdb.Del(ctx, key).Err() 方法,在没有出错的情况下是不会有返回值的。

go run server.go functool.go

可以再次运行 TestgetUser() 方法来查询名为 "bill" 用户。

go run server.go functool.go
opps we dont have this user!

可以看出该用户已经不存在。

户管理功能,成功


Message 管理

序列化 Message
//messaage.go
dataerr := json.Marshal(m)

使用 json.Marshal() 方法实现对 Message 的序列化。

保存Message到数据库 1
//message.go

//保存该message到数据库1
func SetMessage(m Messagebool {
    dataerr := json.Marshal(m//使用json.Marshal序列化,data数据类型为[]byte
    if nil == err {
        rdb := NewRedis(1)
        rdb.Set(ctxm.IdTimedata24*time.Hour).Err() //保存Message,保存24小时24*time.Hour
        rdb.Close()
        return true //返回true表示存储成功
    } else {
        return false //返回false表示存储失败
    }
}

使用 SetMessage() 方法实现 Message 的存储。 其中 rdb.Set(ctx, msg.Id, data, 24 * time.Hour).Err() 这里的 24 * time.Hour 用来控制 Message 存在时间,服务端 Message 存储时限设置为24小时,过期自动删除,在后面的测试中应使用更短的时间例如 10 * time.Second 超过10秒后自动删除。

从数据库 1 中获取 Message
//functool.go

//使用IdTime进行message查询
func GetMessage(idtime string) (Messagebool) { //获取单独的一条
    rdb := NewRedis(1)                        //获取redis客户端
    valerr := rdb.Get(ctxidtime).Result() //使用IdTime获取Message
    rdb.Close()                               //关闭redis客户端
    if nil == err {
        var msg Message
        err := json.Unmarshal([]byte(val), &msg//反序列化
        CheckError(err)
        return msgtrue
        //在无err情况下返回Message,并设置状态为true
        //true表示获取成功
    } else {
        return Message{"", "", ""}, false
        //在err情况下返回空的Message,并设置状态为false
        //false表示获取失败
    }
}

这个是获取单个 Message 的测试,在实际应用中, 服务端收到来自客户端的 Message 后将会把这条 Message 群发给所有在线的用户,随即保存到 Redis 1 里。 这里有一个用户场景,例如用户 bill 不在线,所以 bill 的状态是不在线,当 bill 重新上线后,需要历史消息,就会向客户端发起请求,这里就会用到 GetAllMessageRange() ,将所有的历史消息打包给用户 bill 。

//functool.go

//获取该数据库里所有的key
func GetAllKeys(db int) []string {
    rdb := NewRedis(db)
    defer rdb.Close()
    keyserr := rdb.Keys(ctx, "*").Result()
    CheckError(err)
    return keys
}

//message.go


//获取全部的消息,将其打包为map
func GetAllMessageRange() []Message {
    var MessageSlice []Message
    b := GetAllKeys(1)
    for _, idtime := range b {
        m, _ := GetMessage(idtime)
        MessageSlice = append(MessageSlicem)
    }
    return BubbleSortPro(MessageSlice)
}

//对Message进行冒泡排序,使其按照IdTime的先后顺序
func BubbleSortPro(arr []Message) []Message {
    length := len(arr)
    for i := 0i < lengthi++ {
        over := false
        for j := 0j < length-i-1j++ {
            if arr[j].IdTime > arr[j+1].IdTime {
                over = true
                arr[j], arr[j+1= arr[j+1], arr[j]
            }
        }
        if !over {
            break
        }
    }
    return arr
}

以上代码中我们使用到了冒泡排序,这主要是因为 Redis 是无序数据库,所以需要特别的进行顺序排列,按时间戳 IdTime 进行排序 。有利于后期的 Message 遍历。

删除

在上文中我们提到了, Message 在Redis数据库里超过24小时,将会自动删除,所以这里并不特别需要删除 Message 。 如需删除指定 Message 可以使用上文【用户管理】中提到的 DeletSomething(key string, Db int) 进行删除。

测试
//main.go


func main() {
    testSetMssage()
    for imsg := range GetAllMessageRange() {
        uerr := strconv.ParseInt(msg.IdTime1064)
        CheckError(err)
        d := time.Unix(u/1e90)
        fmt.Println("ID:"i, "时间:", d, "名字", msg.Name, "Message:", msg.Msge)
    }
}
func testSetMssage() {
    for j := 0j < 10j++ {
        s := strconv.Itoa(j)
        time.Sleep(1 * time.Second)
        user := User{
            "bill+ s,
            "",
            "",
        }
        
        t :=time.Now().UnixNano()
        msg := Message{
            user.Name,
            string(t),
            "message " + s,
        }
        SetMessage(msg)
    }
    fmt.Println("数据保存完毕!")
}

用以上代码进行测试,我们可以在控制台看到打印出来的10个 Message 。

go run Server.go functool.go
数据保存完毕!
ID: 2 时间: 2021-04-23 13:48:51 +0800 CST 名字 bill2 Message: message 2
ID: 4 时间: 2021-04-23 13:48:53 +0800 CST 名字 bill4 Message: message 4
ID: 5 时间: 2021-04-23 13:48:54 +0800 CST 名字 bill5 Message: message 5
ID: 8 时间: 2021-04-23 13:48:57 +0800 CST 名字 bill8 Message: message 8
ID: 0 时间: 2021-04-23 13:48:49 +0800 CST 名字 bill0 Message: message 0
ID: 1 时间: 2021-04-23 13:48:50 +0800 CST 名字 bill1 Message: message 1
ID: 7 时间: 2021-04-23 13:48:56 +0800 CST 名字 bill7 Message: message 7
ID: 9 时间: 2021-04-23 13:48:58 +0800 CST 名字 bill9 Message: message 9
ID: 3 时间: 2021-04-23 13:48:52 +0800 CST 名字 bill3 Message: message 3
ID: 6 时间: 2021-04-23 13:48:55 +0800 CST 名字 bill6 Message: message 6

其中我们可以看见, map 里的数据是没有顺序的,因为 Redis 数据库是一个无序数据库,所以打印出来不是顺序排列的,但是我们可以简单的通过遍历将数据顺序排列出来。 在线用户收到消息必然是顺序排列的,因 Message 收到时先群发给所有在线用户,再使用 SetMessage() ,这个事件是有时间顺序的。 新上线用户,会收到历史 Message ,也就是使用上文说的冒泡排序,客户端收到后进行遍历,这样的顺序就是正常的了。

第二节 服务器的设计与搭建

http服务器的搭建

这里使用 Golang 自带的 "net/http" 包来搭建http服务器,http服务器将监听所有来自客户端的请求并返回相应数据。

 //main.go
 
 //设置服务器监听地址与端口
var addr = flag.String("addr", ":8989", "http service address")
///服务器入口
func main() {
    flag.Parse()
    hub := newHub() //实例化hub
    go hub.run() //开启 hub
    http.HandleFunc("/message"func(rw http.ResponseWriterr *http.Request) {
        message(hubrwr)
    })//监听 message聊天功能
    http.HandleFunc("/register", func(rw http.ResponseWriterr *http.Request) {
        Register(rwr)
    })//监听注册功能
    http.HandleFunc("/login"func(rw http.ResponseWriterr *http.Request) {
        Login(rwr)
    })//监听登录功能
    http.HandleFunc("/dislogin"func(rw http.ResponseWriterr *http.Request) {
        DisLogin(rwr)
    })监听注销登录功能
    http.HandleFunc("/chanagepasswd"func(rw http.ResponseWriterr *http.Request) {
        ChanagePasswd(rwr)
    })//监听修改密码功能
    http.HandleFunc("/getallmessage"func(rw http.ResponseWriterr *http.Request) {
        GetAllMsg(rwr)
    })//监听获取24小时消息列表的功能
    err := http.ListenAndServe(*addrnil)//服务器开始监听
    if err != nil {
        log.Fatal("ListenAndServe: "err)
    }
}
注册功能的实现

注册功能,前端对数据进行简单校验后发送给服务器,服务器收到请求后进行相应处理。

//client.go


func Register(rw http.ResponseWriterr *http.Request) {
    s := ""
    data := User{}
    json.NewDecoder(r.Body).Decode(&data)
    b := SetUser(data)
    if b { //成功保存此用户到数据库,用户注册成功
        s = "yes"
    } else { //已存在,用户注册失败
        s = "no"
    }
    rw.Write([]byte(s))
}

将收到的数据解码为 User 调用 SetUser() 方法,进行用户的注册,返回相应信息给客户端。

登录功能的实现
//client.go

func Login(rw http.ResponseWriterr *http.Request) {
    s := "defalut"
    data := User{}
    json.NewDecoder(r.Body).Decode(&data)
    userb := GetUser(data)
    if b { //用户名正确
        if data.Passwd == user.Passwd { //密码正确,加入登录列表,登录成功
            s = "yes"
            if !IsOurUser(data.Name) {
                u1 := user
                u1.Name = data.Name //只保存用户名
                var l = len(UserMap)
                UserMap[l+1= u1
            }
        } else { //密码错误,登录失败
            s = "no"
        }
    } else { //用户名错误
        s = "no"
    }
    rw.Write([]byte(s))
}

//functool.go

//判断是否存在该户,存在true,不存在false
func IsOurUser(name stringbool {
    bo := false
    for _, u1 := range UserMap {
        if u1.Name == name {
            bo = true
        }
    }
    return bo
}

将收到的 User 对其在数据库内进行查询。如果在数据库查询到该用户,用户名正确,进行密码的验证。如密码正确,将对客户端返回 "yes",表示用户成功登录,反之返回 "no" 表示用户登录失败, 用户名或者密码错误。在成功登录后还会觉得是否加入登录列表,浙江决定用户是否能进行正常的聊天操作。确保用户没有登录的情况下是不能进行聊天操作的。

注销登录功能的实现
//client .go

func DisLogin(rw http.ResponseWriterr *http.Request) {
    data := User{}
    json.NewDecoder(r.Body).Decode(&data)
    DeletLoginUser(data)
    rw.Write([]byte("yes"))
}

//functool.go

//将用户踢出用户Map,表示该用户没有登录
func DeletLoginUser(u User) {
    for iuser := range UserMap {
        if user.Name == u.Name {
            delete(UserMapi)
        }
    }
}

服务器收到注销请求后,将用户踢出登录列表,在此功能里,如果用户在登录列表里将会成功注销,服务器返回 "yes" 。如果没有成功踢出登录列表,表示用户没有在登录列表却要注销,这种极端情况,我们也对客户端返回 "yes" ,因该用户并不存在于登录列表 UserMap里,所以不会对其进行删除操作。

密码修改功能的实现
//client.go

func ChanagePasswd(rw http.ResponseWriterr *http.Request) {
    s := "defalut"
    data := User{}
    json.NewDecoder(r.Body).Decode(&data)
    if SetPasswd(data) {
        //修改成功
        s = "yes"
    } else {
        //修改失败,检查账号和密码,如还是失败联系管理员
        s = "no"
    }
    rw.Write([]byte(s))
}

//user.go

//修改密码
//为了不新增对象/结构体,这里约定客户端发来的用户信息如下:
// var user=User{
//  Name: "用户名",
//  Passwd: "旧密码",
//  Status: "新密码",
// }
func SetPasswd(user Userbool {
    uub := GetUser(user)
    if b {
        if user.Passwd == uu.Passwd {
            //密码正确
            u := User{
                Name:   user.Name,
                Passwduser.Status,
                Status: "",
            }
            DeletSomething(u.Name0)
            if SetUser(u) {
                //修改成功
                fmt.Println("此u给i成功")
                return true
            } else {
                //修改失败
                return false
            }
        } else {
            //密码错误
            return false
        }
    } else {
        //没有这个人,用户名错误
        return false
    }
    // return true
}

服务端将用 User 解码,我们与客户端约定好用户修改密码的数据用以上注释内容的 user 定义,这样将不会产生新的数据结构。 用户信息到达后先对其进行数据可查询,用户名与旧密码正确后,才进行修改操作,将客户端发来的用户数据 User.Status 填到 User.Passwd ,先删除该用户再保存该用户,以免调用 SetUser() 方法报错。 如修改成功,服务器会发送 "yes" ,如果失败将发送 "no" ,表示用户提交的信息有误。

获取消息列表
//client.go

func GetAllMsg(rw http.ResponseWriterr *http.Request) {
    //  IsOurUser()
    s := GetAllMessageRange()
    datae := json.Marshal(s)
    CheckError(e)
    rw.Write(data)
}

该方法会将所有24小时内(因上文中的 Message 数据可的设计,保存时限限制为24小时)的消息打包为 Slice (在 Golang 里 Slice 即是数组)然后发送给客户端,客户端将其进行解析,生成历史消息列表。

WebSokcet服务器的搭建

这里参考 Github/gorilla/websocket/examples/chat 示例。 本文讲的是 基于 Socket 的聊天程序设计与实现,所以不会对 Socket 进行重复造轮子,本文会基于上述示例,进行理解与修改,使其符合我们的功能需求。

消息队列
//client.go

// 将连接升级为 webSocket 并将其加入消息队列
func message(hub *Hubw http.ResponseWriterr *http.Request) {
    connerr := upGrader.Upgrade(wrnil)
    if err != nil {
        log.Println(err)
        return
    }
    client := &Client{hubhubconnconnsendmake(chan []byte256)}
    client.hub.register <- client
    go client.writePump()
    go client.readPump()
}


var upGrader = websocket.Upgrader{
    ReadBufferSize:  1024,//读取数据大小
    WriteBufferSize1024,//写入数据大小
    CheckOriginfunc(r *http.Requestbool {
        return true
    },
}

本文使用 Socket 技术作为聊天功能实现技术,那么先将请求升级为 WebSocket ,使用 websocket.Upgrader 进行升级。 升级后,实例化客户端 client ,并对其添加数据。使用管道 channel 将客户端加入 hub.register,进入到队列里。 分别开启该客户端的读、写 线程。至此,该用户处于等待收发消息的状态。

client.readPump 客户端消息读取
//client.go

func (c *ClientreadPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    c.conn.SetReadLimit(maxMessageSize)
    c.conn.SetReadDeadline(time.Now().Add(pongWait))
    c.conn.SetPongHandler(func(stringerror { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
    for {
        mtmessageerr := c.conn.ReadMessage() //读取客户端发送的数据
        if err != nil {
            if websocket.IsUnexpectedCloseError(errwebsocket.CloseGoingAwaywebsocket.CloseAbnormalClosure) {
                log.Printf("error: %v"err)
            }
            break
        }
        var msg Message
        e := json.Unmarshal(message&msg)
        CheckError(e)
        if !IsOurUser(msg.Name) {
            SendToClient(c.connmt, "You are not Login Users")
            break
        }
        SetMessage(msg)
        message = bytes.TrimSpace(bytes.Replace(messagenewlinespace-1))
        c.hub.broadcast <- message
    }
}

使用 defer func() 在该线程运行最后执行客户端 conn.close 关闭,并从 hub中踢出 消息队列 。 在开始该 client 客户端的线程之前,先设置消息长度限制、等待时间等。 开始线程后监听 ReadMessage ,当客户端发送消息后,这里将消息读取出来,message 的类型是 []byte 型。 错误处理:如果发生了错误,那么打印错误信息,并打断 for 循环,后 defer 开始进行收尾处理,关闭 Socket 连接。 如果一切正常,下一步就是将消息解析出来,使用 IsOurUser()来判断是否是登录客户,如果返回 false 代表,该用户为登录,或者信息有误,导致判断不是登录 User,简而言之 返回 false 代表该 message 不是我们约定好的,那么将打断 for 循环,关闭该连接。 如过是我们登录的 User 那么将该条 message 保存到数据库。 继续,该条消息加入到 hub.go 第42行 。 hub.go 第42行 会对其客户端进行循环遍历,直到收到 message ,收到后将其加入到 client.send里,而这时轮到 client.writePump 出场了。

client.writePump 客户端消息写入
//client.go

func (c *ClientwritePump() {
    ticker := time.NewTicker(pingPeriod)//设置定时器
    defer func() {
        ticker.Stop()
        c.conn.Close()
    }()
    for {
        select {
        case messageok := <-c.send:
            c.conn.SetWriteDeadline(time.Now().Add(writeWait))
            if !ok {
                // The hub closed the channel.
                c.conn.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }
            werr := c.conn.NextWriter(websocket.TextMessage)
            if err != nil {
                return
            }
            w.Write(message)
            // Add queued chat messages to the current websocket message.
            n := len(c.send)
            for i := 0i < ni++ {
                w.Write(newline)
                w.Write(<-c.send)
            }
            if err := w.Close(); err != nil {
                return
            }
        case <-ticker.C:
            c.conn.SetWriteDeadline(time.Now().Add(writeWait))
            if err := c.conn.WriteMessage(websocket.PingMessagenil); err != nil {
                return
            }
        }
    }
}

使用 defer func() 在该线程运行最后执行客户端 conn.close 关闭,定时器关闭,并从 hub中踢出 消息队列 。 在 for 循环内 select {} 它将会从第一个可运行的 case 开始运行,直到没有可以运行的 case ,它就开始阻塞, default 总会运行。 如果收到了消息,那么第一个 case 将运行,第一个 case 运行的时候,会将客户端发送的 message 发送该客户端。 因该线程是每一个客户端都会有的,所以接收到 message 后,每一个在线的客户端都会收到该条 message。 如果没有收到 message 那么将会运行 定时器,截至时间已过,将会重新等待下一个 message 的到来。 至此,消息收发功能完成。

第三节 前端的设计与搭建

项目文件结构

SocketuView/
  • .hbuilderx
  • pages
  • Changeasswordp
    • Changeasswordp.vue /密码修改页面
  • login
    • login.vue /登录页面
  • message
    • message.vue /消息,聊天页面
  • register
    • register.vue /注册页面
  • setting
    • setting.vue /设置页面
  • log
    • log.vue /log页面
  • static
  • uni_modules
  • unpackage
  • uview
  • eslintignore
  • App.vue /app 的入口
  • LICENSE
  • main.js
  • manifest.json
  • pages.json
  • uni.scss
  • vue.config.js

可以看见的就是主要结构及文件,其余不在我们这个项目的讨论范围内

建立连接

在前端中将会使用 http请求 以及 websocket 与服务端进行连接。

http请求
uni.request({
    url:getApp().globalData.serverAddr+'dislogin',
    method:'POST',
    data:JSON.stringify(user),
    success: (res) => {
        getApp().globalData.SetLog('dis login success',res)
    },
    fail: (res) => {
        getApp().globalData.SetLog('dis login dail',res)
    }
})

http请求使用 uni-app 的 uni.request() API ,使用非常简单,填入请求地址,方法类型,要发送的数据。 请求成功将会在 success 里面接收数据,进行下一步处理,例如登录功能的请求成功方法如下:

switch(res.data){
    case 'yes':									
        try{
            uni.setStorageSync('user',this.user)
        }catch(e){
            getApp().globalData.SetLog('login get user fail',e)
        }
        uni.reLaunch({
            url:'../message/message'
        });
        uni.showToast({
            title: '登录成功!',
            duration: 2000
        });
        getApp().globalData.SetLog('login success','yes')
    break;
    case 'no':
        uni.showToast({
            title: '信息有误!',
            duration: 2000
        });
        getApp().globalData.SetLog('login fail','Wrong user name or password ')
    break;
    default:
        getApp().globalData.SetLog('login fail',res)
        uni.showToast({
            title: '网络开小差了!',
            duration: 2000
        });
}

请求成功后对收到的数据 res进行解析,而 res 接收到的数据是 json 类型的,不过 uni.request 如果发送的数据类型是 json 那么它会自动尝试解析 JSON.parse 。 在服务端,login 请求的返回值只有 "yes" or "no" 两种情况,所以写两个 case 加一个 default 即可,如果服务器返回 "yes" 那么将使用 uni.setStorageSync 同步缓存 ,把 这个 this.user 保存到本地缓存中,这样下一次打开 APP 将会自动填充账号。

websocket连接

这里使用 uni-app 的 uni.connectSocket() API ,因 uni-app 的 socket 是单一 且唯一的连接,所以我们在 App.vue 创建全局 socket 连接,这样在 app 内任意页面都可以管理这个 socket 连接,使用 getApp().globalData.SocketTask 获取 SocketTask 对象。

//App.vue

globalData:{
    SocketTask:null,
    initSocketTask(){
        getApp().globalData.SocketTask = uni.connectSocket({
        url:getApp().globalData.serverAddr+'message',
        success:(res)=>{
            getApp().globalData.SetLog('message init task success',res)
        }
        });
    },
}

例如在 message.vue 使用:

initTask(){
    getApp().globalData.initSocketTask()//初始化socket
    this.Task = getApp().globalData.SocketTask//获取socketTask
    this.Task.onError((res)=>{
        getApp().globalData.SetLog('message init task onErro',res)
    });
    this.Task.onClose((res)=>{
        getApp().globalData.SetLog('message init task onClose',res)
    });
    this.Task.onOpen((res)=>{
        getApp().globalData.SetLog('message init task onOpen',res)
    });
    this.Task.onMessage((res)=>{//监听服务武器返回消息
        getApp().globalData.SetLog('message init task onMessage','成功')
        if(null == this.list){//防止消息列表为空,服务器里没有任何一条 message 导致出错
            var s = [JSON.parse(res.data)]//新建一个数组来保存这条 message
            this.list = s
        }else{//如果消息列表不为空
            this.list.push(JSON.parse(res.data))//使用push()将这条 messgae 放入数组最后,如果数组为空将不能使用push()
        }
    });
},

以上只提到了接收 message 那么以下就是发送 message 的实例:

sendGo(){
    if(''!=this.msg){//判断发送的 message 是否为空空
        let d = new Date().getTime().toString()//获取时间
        var msgData ={ //拼接 message 对象
            name:this.user.name,
            idtime:d,
            msge:this.msg
        }
        this.Task.send({
            data:JSON.stringify(msgData),
            success:(res)=>{
                this.msg = ''
            },
            fail:(res)=>{
                getApp().globalData.SetLog('message send msg fail',res)
            }
        });
    }else{
        uni.showToast({
            title:'消息不能为空',
            duration:2000
            })
    }	
},

前端页面的实现

为什么使用uView

在21世纪的今天,前端页面不需要开发者一个个 css 去写,优秀、好看、易用的ui框架层出不穷,而本文中使用 uniapp 作为前端app的开发框架,而 uView 正是 uniapp 里的ui框架,所以结论显而易见。

代码部分

具体代码这里不过多阐述,整个项目,前端代码,包括后台服务器代码将上传 GitHub ,如有需要请移步 github/brejce/

About

运用socket通信技术,使用golang作为后端,uni-app作为前端,使用数据库为redis,开发的一款聊天室,实现了登录、注册、聊天等功能。

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published