Go语言打造高并发web即时聊天(IM-Instant Messaging)应用-支持10万人同时在线
// 消息体核心代码快-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
github.com/joewalnes/websocketd
github.com/gorilla/websocket
以上两个包非官方,但是都依赖于下面的官方的扩展包下的net包中WebSocket。
github.com/golang/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
本次选用gorilla/websocket为WebSocket服务
go get -u -v github.com/gorilla/websocket
判断id和token是否一致,一致则鉴权成功
最简单的Conn的维护,让userid和conn形成一个映射关系,一个map【ClientMap】的键是int64类型的userid,值是conn的指针,实际开发过程中,一个用户的信息远不止这些,所以定义了一个ClientNode的结构体,用来存放conn的指针,以及用户的各种其他信息,所以对map【clientMap】做了升级,key为int64类型的用户id,值为clientNode的结构体指针
// 绑定请求和处理函数
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/
业务说明 | |
---|---|
业务名称 | 登录 |
请求格式 | /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接入的时候用到
}
}
// 获得参数
request.ParseForm()
// 解析参数
mobile := request.PostForm.Get("mobile")
password := request.PostForm.Get("password")
// 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))
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)
}
}
概要 | 技术实现 |
---|---|
实现静态资源服务 | func FileServer(root FileSystem) Handler {return &fileHandler{root}} |
模板渲染技术 | template |
前端技术 | Vue+Mui+Ajax+Promis |
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)
}
}
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)
}
}
{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/login.shtml"}}
<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}}
{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/register.shtml"}}
<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}}
{{/*定义模板的名称,shtml表示是后端渲染的文件*/}}
{{define "/user/test.shtml"}}
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
<title>testtitle>
<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">testh1>
header>
<h1>testh1>
body>
html>
{{end}}
xorm的github地址:https://github.com/go-xorm/xorm
xorm的中文文档地址:https://github.com/go-xorm/xorm/blob/master/README_CN.md
xorm教程地址:https://books.studygolang.com/xorm/
go get -u -v github.com/go-xorm/xorm
go的mysql驱动的github地址:https://github.com/go-sql-driver/mysql
go get -u -v github.com/go-sql-driver/mysql
import (
"encoding/json"
_ "github.com/go-sql-driver/mysql" //只执行该包的init函数
"github.com/go-xorm/xorm"
"html/template"
"log"
"net/http"
)
// xorm的引擎
var DBEngine *xorm.Engine
// init函数实现数据库连接的初始化
func init() {
// 数据库驱动名
driverName := "mysql"
//data source name(DSN):数据源名称:
//DSN=[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
//DSN最完整的形式=username:password@protocol(address)/dbname?param=value
//除databasename外,所有值都是可选的。所以最小的DSN是=/dbname
//如果您不想预先选择数据库,请dbname留空=/这与空DSN字符串具有相同的效果:
//或者,Config.FormatDSN可用于通过填充结构来创建DSN字符串。
dataSourceName := "root:root@(127.0.0.1:3306)/chat?charset=utf8&parseTime=true&loc=Asia%2FShanghai"
//连接数据库
DBEngine, err := xorm.NewEngine(driverName, dataSourceName)
if err != nil {
log.Fatal("xorm.NewEngine(driverName,dataSourceName) Error:", err.Error())
}
// 是否显示SQL语句
DBEngine.ShowSQL(true)
// 设置数据库的最大连接数
DBEngine.SetMaxOpenConns(2)
log.Println("database init success!")
}