go

Go语言打造高并发web即时聊天(IM-Instant Mess

2019-07-05  本文已影响0人  蔻蔻哒

Go语言打造高并发web即时聊天(IM)应用-支持10万人同时在线

1.技术栈

1.1.前端技术

1.2. 后端技术

1.3.架构技术

2. 需求分析

2.1.基本需求

// 消息体核心代码快-go语言结构体
type Message struct {
    // 消息ID
    Id int64 `json:"id,omitempty" form:"id"`
    // 消息发送方
    UserId int64 `json:"userid,omitempty" form:"userid"`
    // 群聊还是私聊标记
    Cmd int `json:"cmd,omitempty" form:"cmd"`
    // 对端ID,或者群ID
    Dstid int64 `json:"dstid,omitempty" form:"dstid"`
    // 消息样式
    Media int `json:"media,omitempty" form:"media"`
    // 消息的内容
    Content string `json:"content,omitempty" form:"content"`
    // 预览图片
    Pic string `json:"pic,omitempty" form:"pic"`
    // 服务的URL
    Url string `json:"url,omitempty" form:"url"`
    // 简单的描述
    Memo string `json:"memo,omitempty" form:"memo"`
    // 数字、数值相关
    Amount int `json:"amount,omitempty" form:"amount"`
}
假设群聊中:
用户A发送图片数据512k
100人在线群人员同时收到
512kb * 100 = 1024kb * 50 = 50M
假设有1024个群
1024* 50 M = 50G

3.IM系统架构

IM系统的一般结构
实现及重点
网络结构

4. WebSocket

4.1. 选型

github.com/joewalnes/websocketd
github.com/gorilla/websocket

以上两个包非官方,但是都依赖于下面的官方的扩展包下的net包中WebSocket。

github.com/golang/net

4.2.安装net包

由于大陆境内有强的缘故,所以对于golang.org/x/net需要手动创建目录后,然后使用git clone方式进行下载

cd $GOPATH/src/
mkdir -p golang.org/x/
cd golang.org/x/
git clone https://github.com/golang/net.git
ls
下载安装golang的扩展包下的net包

4.3. 安装gorilla/websocket

本次选用gorilla/websocket为WebSocket服务

go get -u -v github.com/gorilla/websocket
安装gorilla/websocket

4.4.鉴权

判断id和token是否一致,一致则鉴权成功

4.4.1. 鉴权成功

鉴权成功

4.4.2.鉴权失败

鉴权失败

4.4.3.鉴权接入/用户信息

接入鉴权/用户信息表

4.4.4.鉴权接入/conn

鉴权接入/conn

4.4.5.conn的维护

最简单的Conn的维护,让userid和conn形成一个映射关系,一个map【ClientMap】的键是int64类型的userid,值是conn的指针,实际开发过程中,一个用户的信息远不止这些,所以定义了一个ClientNode的结构体,用来存放conn的指针,以及用户的各种其他信息,所以对map【clientMap】做了升级,key为int64类型的用户id,值为clientNode的结构体指针

conn的维护

4.5.后端消息的接收

后端消息的接收

4.6.后端消息的发送

后端消息的发送

4.7.前端使用WebSocket

前端使用WebSocket

4.8.WebSocket的心跳机制

WebSocket的心跳机制

4.9.前端消息的发送

前端消息的发送
前端发送消息格式 前端消息的队列发送

4.10. 前端消息的接收

前端消息的接收

4.11.流程

流程

5.单机支持高并发

5.1.设计高质量的代码优化map

  1. 由于对map的频繁读写,会存在一个安全性问题,所以要设计稿质量的代码来优化map
  1. map不要太大

5.2.突破系统的瓶颈优化最大连接数

5.3.优化CPU资源的使用

5.4.优化IO资源的使用

5.5.应用服务和资源服务分离

6.golang web

6.1. web http编程核心API

// 绑定请求和处理函数
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
   DefaultServeMux.HandleFunc(pattern, handler)
}
// pattern string:请求的路径
// handler func(ResponseWriter, *Request):回调函数、处理函数

// 启动web服务器:监听并提供服务
func ListenAndServe(addr string, handler Handler) error {
       server := &Server{Addr: addr, Handler: handler}
       return server.ListenAndServe()
   }
// addr string:监听的IP+port
// handler Handler:回调函数,路由,如果没有自定义路由,可以传入nil来调用默认的路由
package main

import (
   "io"
   "log"
   "net/http"
)

func main() {

   // 绑定请求和处理函数
   http.HandleFunc("/user/login", func(writer http.ResponseWriter, request *http.Request) {
       // 执行数据库操作
       // 逻辑处理
       // restful风格API,返回JSON/XML
       io.WriteString(writer, "hello world!")
   })
   // 启动web服务器:监听并提供服务
   if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
       log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
   }
}

启动服务器,并在终端进行测试

curl http://127.0.0.1:8080/user/login/
终端测试

7.实现后端登录接口

7.1.后端登录接口API

业务说明
业务名称 登录
请求格式 /user/login
请求参数 mobile:用户手机号;password:用户密码
返回json {"code":0,"msg":"提示信息","data":{"id":111,"token":333333 }}
{
    "code":0,// 0:成功,-1:失败
    "msg":"提示信息",// 用户名或密码错误等等
    "data":{
        "id":111,// 用户id
        "token":333333 // 鉴权因子,在WebSocket接入的时候用到
    }
}

7.1.1.登录接口API知识点梳理

7.1.1.1. 获取前端传递的参数

// 获得参数
request.ParseForm()
// 解析参数
mobile := request.PostForm.Get("mobile")
password := request.PostForm.Get("password")

7.1.1.2. 返回json格式数据给前端

// 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
writer.Header().Set("Content-Type", "application/json")
// 2.设置header的响应状态码 - 成功-200
writer.WriteHeader(http.StatusOK)
// 3. 返回JSON数据
writer.Write([]byte(str))

7.1.1.3.代码

package main

import (
    "log"
    "net/http"
)

func main() {

    // 绑定请求和处理函数
    http.HandleFunc("/user/login", func(writer http.ResponseWriter, request *http.Request) {
        // 执行数据库操作
        // 逻辑处理
        // restful风格API,返回JSON/XML
        // 获得参数
        request.ParseForm()
        // 解析参数
        mobile := request.PostForm.Get("mobile")
        password := request.PostForm.Get("password")

        // 定义简单校验的标记
        loginOk := false
        if mobile == "17500000000" && password == "123456" {
            loginOk = true
        }
        // 默认的成功的JSON字符串
        str := `{"code":0,"data":{"id":1,"token":"test"}}`
        if !loginOk {
            // 失败的JSON字符串
            str = `{"code":-1,"msg":"用户名或密码错误"}`
        }
        // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
        writer.Header().Set("Content-Type", "application/json")
        // 2.设置header的响应状态码 - 成功-200
        writer.WriteHeader(http.StatusOK)
        // 3. 返回JSON数据
        writer.Write([]byte(str))

    })

    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }

}

curl http://127.0.0.1:8080/user/login -X POST -d "mobile=17500000000&password=123456"
成功
curl http://127.0.0.1:8080/user/login -X POST -d "mobile=17500000000&password=12345"
失败
package main

import (
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 默认的成功的JSON字符串
    str := `{"code":0,"data":{"id":1,"token":"test"}}`
    if !loginOk {
        // 失败的JSON字符串
        str = `{"code":-1,"msg":"用户名或密码错误"}`
    }
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    writer.Write([]byte(str))

}
func main() {

    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)

    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 成功的JSON返回
    if loginOk {
        //"data":{"id":1,"token":"test"
        data := make(map[string]interface{})
        data["id"] = 1
        data["token"] = "test"
        ResponseJson(writer, 0, data, "")
    } else {
        // 失败的JSON返回
        ResponseJson(writer, -1, nil, "用户名或密码错误")
    }
}

// 定义一个结构体
type H struct {
    Code int         `json:"code"`
    Data interface{} `json:"data,omitempty"` //omitempty:如果序列化的字段值是nil,则不进行JSON序列化,不会再JSON数据中看到
    Msg  string      `json:"msg"`
}

func ResponseJson(writer http.ResponseWriter, code int, data interface{}, msg string) {
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    // 定义一个结构体--结构体H
    h := H{
        Code: code,
        Data: data,
        Msg:  msg,
    }
    // 将结构体转换成JSON字符串
    result, err := json.Marshal(h)
    if err != nil {
        log.Fatal("json.Marshal(h) Error:", err)
    }
    // 返回数据
    writer.Write(result)
}
func main() {
    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)
    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}

7.2.实现前端登录页面并接入

概要 技术实现
实现静态资源服务 func FileServer(root FileSystem) Handler {return &fileHandler{root}}
模板渲染技术 template
前端技术 Vue+Mui+Ajax+Promis

7.2.1.后端代码版本1

package main

import (
    "encoding/json"
    "html/template"
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 成功的JSON返回
    if loginOk {
        //"data":{"id":1,"token":"test"
        data := make(map[string]interface{})
        data["id"] = 1
        data["token"] = "test"
        ResponseJson(writer, 0, data, "")
    } else {
        // 失败的JSON返回
        ResponseJson(writer, -1, nil, "用户名或密码错误")
    }
}

// 定义一个结构体
type H struct {
    Code int         `json:"code"`
    Data interface{} `json:"data,omitempty"` //omitempty:如果序列化的字段值是nil,则不进行JSON序列化,不会再JSON数据中看到
    Msg  string      `json:"msg"`
}
func ResponseJson(writer http.ResponseWriter, code int, data interface{}, msg string) {
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    // 定义一个结构体--结构体H
    h := H{
        Code: code,
        Data: data,
        Msg:  msg,
    }
    // 将结构体转换成JSON字符串
    result, err := json.Marshal(h)
    if err != nil {
        log.Fatal("json.Marshal(h) Error:", err)
    }
    // 返回数据
    writer.Write(result)
}
func main() {
    // 提供静态资源目录支持--当前目录
    //http.Handle("/",http.FileServer(http.Dir("./")))// 有安全风险,能让main对外暴露
    // 2.指定目录的静态资源文件支持
    http.Handle("/asset/", http.FileServer(http.Dir("./")))
    // 登录/user/login.shtml的请求--后端渲染
    http.HandleFunc("/user/login.shtml", func(w http.ResponseWriter, r *http.Request) {
        // 解析--使用模板template进行解析
        tpl, err := template.ParseFiles("./view/user/login.html")
        if err != nil {
            log.Fatal(`template.ParseFiles("./view/user/login.html") Error:`, err.Error())
        }
        // func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
        // 参数1:io.Writer
        // 参数2:自定义的模板函数名称,此处在view/user/login.html中定义的{{define "/user/login.shtml"}}名称
        // 参数3:需要给前端做的数据绑定的数据
        tpl.ExecuteTemplate(w, "/user/login.shtml", nil)
    })
    // 注册/user/register.shtml的请求--后端渲染
    http.HandleFunc("/user/register.shtml", func(w http.ResponseWriter, r *http.Request) {
        // 解析--使用模板template进行解析
        tpl, err := template.ParseFiles("./view/user/register.html")
        if err != nil {
            log.Fatal(`template.ParseFiles("./view/user/register.html") Error:`, err.Error())
        }
        // func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
        // 参数1:io.Writer
        // 参数2:自定义的模板函数名称,此处在view/user/register.html中定义的{{define "/user/register.shtml"}}名称
        // 参数3:需要给前端做的数据绑定的数据
        tpl.ExecuteTemplate(w, "/user/register.shtml", nil)

    })
    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)
    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}

7.2.2.后端代码版本1优化

package main

import (
    "encoding/json"
    "html/template"
    "log"
    "net/http"
)

func userLogin(writer http.ResponseWriter, request *http.Request) {
    // 执行数据库操作
    // 逻辑处理
    // restful风格API,返回JSON/XML
    // 获得参数
    request.ParseForm()
    // 解析参数
    mobile := request.PostForm.Get("mobile")
    password := request.PostForm.Get("password")

    // 定义简单校验的标记
    loginOk := false
    if mobile == "17500000000" && password == "123456" {
        loginOk = true
    }
    // 成功的JSON返回
    if loginOk {
        //"data":{"id":1,"token":"test"
        data := make(map[string]interface{})
        data["id"] = 1
        data["token"] = "test"
        ResponseJson(writer, 0, data, "")
    } else {
        // 失败的JSON返回
        ResponseJson(writer, -1, nil, "用户名或密码错误")
    }
}

// 定义一个结构体
type H struct {
    Code int         `json:"code"`
    Data interface{} `json:"data,omitempty"` //omitempty:如果序列化的字段值是nil,则不进行JSON序列化,不会再JSON数据中看到
    Msg  string      `json:"msg"`
}
func ResponseJson(writer http.ResponseWriter, code int, data interface{}, msg string) {
    // 1.设置header为JSON--默认的是text/html,故而要特别的设置为application/json
    writer.Header().Set("Content-Type", "application/json")
    // 2.设置header的响应状态码 - 成功-200
    writer.WriteHeader(http.StatusOK)
    // 3. 返回JSON数据
    // 定义一个结构体--结构体H
    h := H{
        Code: code,
        Data: data,
        Msg:  msg,
    }
    // 将结构体转换成JSON字符串
    result, err := json.Marshal(h)
    if err != nil {
        log.Fatal("json.Marshal(h) Error:", err)
    }
    // 返回数据
    writer.Write(result)
}

//万能模板解析渲染
func RenderingView() {
    // 全局解析--使用模板template进行解析
    tpl, err := template.ParseGlob("./view/**/*") //**表示的是一个目录,*表示的是文件
    // 如果出现错误不再继续
    if err != nil {
        log.Fatal(`template.ParseGlob Error:`, err.Error())
    }
    // 循环遍历所有的模板,并执行注册
    for _, v := range tpl.Templates() {
        // 获取模板名称
        tplName := v.Name()
        http.HandleFunc(tplName, func(writer http.ResponseWriter, request *http.Request) {
            // func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
            // 参数1:io.Writer
            // 参数2:自定义的模板函数名称
            // 参数3:需要给前端做的数据绑定的数据
            tpl.ExecuteTemplate(writer, tplName, nil)
        })
    }

}
func main() {
    // 指定目录的静态资源文件支持
    http.Handle("/asset/", http.FileServer(http.Dir("./")))
    // 调用万能模板解析渲染
    RenderingView()
    // 绑定请求和处理函数
    http.HandleFunc("/user/login", userLogin)
    // 启动web服务器:监听并提供服务
    if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
        log.Fatal("http.ListenAndServe('127.0.0.1:8080', nil) Error:", err)
    }
}

7.2.3.前端代码-login

{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/login.shtml"}}
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
        <title>登录</title>
        <link rel="stylesheet" href="/asset/plugins/mui/css/mui.css"/>
        <link rel="stylesheet" href="/asset/css/login.css"/>
        <script src="/asset/plugins/mui/js/mui.js"></script>
        <script src="/asset/js/vue.min.js"></script>
        <script src="/asset/js/util.js"></script>
    </head>
    <body>

    <header class="mui-bar mui-bar-nav">
        <h1 class="mui-title">登录</h1>
    </header>
    <div class="mui-content" id="pageapp">
        <form id='login-form' class="mui-input-group">
            <div class="mui-input-row">
                <label>账号</label>
                <input v-model="user.mobile" placeholder="请输入手机号" type="text" class="mui-input-clear mui-input">
            </div>
            <div class="mui-input-row">
                <label>密码</label>
                <input v-model="user.passwd" placeholder="请输入密码" type="password" class="mui-input-clear mui-input">
            </div>
        </form>
        <div class="mui-content-padded">
            <button @click="login" type="button" class="mui-btn mui-btn-block mui-btn-primary">登录</button>
            <div class="link-area"><a id='reg' href="register.shtml">注册账号</a> <span class="spliter">|</span> <a
                        id='forgetPassword'>忘记密码</a>
            </div>
        </div>
        <div class="mui-content-padded oauth-area">
        </div>
    </div>
    </body>
    </html>
    <script>
        var app = new Vue({
            el: "#pageapp",
            data: function () {
                return {
                    user: {
                        mobile: "",
                        passwd: ""
                    }
                }
            },
            methods: {
                login: function () {
                    //检测手机号是否正确
                    console.log("login")
                    //检测密码是否为空

                    //网络请求
                    //封装了promis
                    util.post("/user/login", this.user).then(res => {
                        console.log(res)
                        if (res.code != 0) {
                            mui.toast(res.msg)
                        } else {
                            //location.replace("//127.0.0.1/demo/index.shtml")
                            mui.toast("登录成功,即将跳转")
                        }
                    })
                },
            }
        })
    </script>
{{end}}

7.2.4.前端代码-register

{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/register.shtml"}}
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
        <title>注册</title>
        <link rel="stylesheet" href="/asset/plugins/mui/css/mui.css"/>
        <link rel="stylesheet" href="/asset/css/login.css"/>
        <script src="/asset/plugins/mui/js/mui.js"></script>
        <script src="/asset/js/vue.min.js"></script>
        <script src="/asset/js/util.js"></script>
    </head>
    <body>

    <header class="mui-bar mui-bar-nav">
        <h1 class="mui-title">注册</h1>
    </header>
    <div class="mui-content" id="pageapp">
        <form id='login-form' class="mui-input-group">
            <div class="mui-input-row">
                <label>账号</label>
                <input v-model="user.mobile" placeholder="请输入手机号" type="text" class="mui-input-clear mui-input">
            </div>
            <div class="mui-input-row">
                <label>密码</label>
                <input v-model="user.passwd" placeholder="请输入密码" type="password" class="mui-input-clear mui-input">
            </div>
        </form>
        <div class="mui-content-padded">
            <button @click="login" type="button" class="mui-btn mui-btn-block mui-btn-primary">注册</button>
            <div class="link-area"><a id='reg' href="register.shtml">注册账号</a> <span class="spliter">|</span> <a
                        id='forgetPassword'>忘记密码</a>
            </div>
        </div>
        <div class="mui-content-padded oauth-area">
        </div>
    </div>
    </body>
    </html>
    <script>
        var app = new Vue({
            el: "#pageapp",
            data: function () {
                return {
                    user: {
                        mobile: "",
                        passwd: ""
                    }
                }
            },
            methods: {
                login: function () {
                    //检测手机号是否正确
                    console.log("login")
                    //检测密码是否为空

                    //网络请求
                    //封装了promis
                    util.post("/user/login", this.user).then(res => {
                        console.log(res)
                        if (res.code != 0) {
                            mui.toast(res.msg)
                        } else {
                            //location.replace("//127.0.0.1/demo/index.shtml")
                            mui.toast("登录成功,即将跳转")
                        }
                    })
                },
            }
        })
    </script>
{{end}}

7.2.5.前端代码-test

{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/test.shtml"}}
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
        <title>test</title>
        <link rel="stylesheet" href="/asset/plugins/mui/css/mui.css"/>
        <link rel="stylesheet" href="/asset/css/login.css"/>
        <script src="/asset/plugins/mui/js/mui.js"></script>
        <script src="/asset/js/vue.min.js"></script>
        <script src="/asset/js/util.js"></script>
    </head>
    <body>

    <header class="mui-bar mui-bar-nav">
        <h1 class="mui-title">test</h1>
    </header>
    <h1>test</h1>
    </body>
    </html>

{{end}}

8.在golang中使用xorm操作数据库

8.1.xorm安装

xorm的github地址https://github.com/go-xorm/xorm
xorm的中文文档地址https://github.com/go-xorm/xorm/blob/master/README_CN.md

go get -u -v github.com/go-xorm/xorm
安装xorm

8.2.MySQL驱动的安装

go的mysql驱动的github地址https://github.com/go-sql-driver/mysql

go get -u -v github.com/go-sql-driver/mysql
go的mysql驱动的安装

8.3.xorm初始化

8.4.xorm实现增删改查

上一篇 下一篇

猜你喜欢

热点阅读