一个聊天的地方
好了,直接进入正题,怎么实现一个带前后端分离的,可以在网页上聊天的东西呢?
先分解任务,通常来讲,一个聊天需要分为一下几个部分
1.用户的注册和登录
2.用户登录后,查找好友与群组
3.用户进行一对一的聊天或者在群组里进行聊天
4.用户修改个人信息
5.用户聊天的内容的形式,如语音,视频等
接下来的任务,就是需要去实现这几个功能
那就开始吧
一个项目应该有清晰的结构
这里结构呢,我们采用的是这个结构,详细可以查看这个
https://gitcode.net/mirrors/golang-standards/project-layout/-/blob/master/README_zh.md
/cmd本项目的主干。每个应用程序的目录名应该与你想要的可执行文件的名称相匹配(例如,/cmd/myapp)。不要在这个目录中放置太多代码。如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg 目录中。如果代码不是可重用的,或者你不希望其他人重用它,请将该代码放到 /internal 目录中。你会惊讶于别人会怎么做,所以要明确你的意图!通常有一个小的 main 函数,从 /internal 和 /pkg 目录导入和调用代码,除此之外没有别的东西。有关示例,请参阅 /cmd 目录。/internal私有应用程序和库代码。这是你不希望其他人在其应用程序或库中导入代码。请注意,这个布局模式是由 Go 编译器本身执行的。有关更多细节,请参阅Go 1.4 release notes 。注意,你并不局限于顶级 internal 目录。在项目树的任何级别上都可以有多个内部目录。你可以选择向 internal 包中添加一些额外的结构,以分隔共享和非共享的内部代码。这不是必需的(特别是对于较小的项目),但是最好有有可视化的线索来显示预期的包的用途。你的实际应用程序代码可以放在 /internal/app 目录下(例如 /internal/app/myapp),这些应用程序共享的代码可以放在 /internal/pkg 目录下(例如 /internal/pkg/myprivlib)。/pkg外部应用程序可以使用的库代码(例如 /pkg/mypubliclib)。其他项目会导入这些库,希望它们能正常工作,所以在这里放东西之前要三思:-)注意,internal 目录是确保私有包不可导入的更好方法,因为它是由 Go 强制执行的。/pkg 目录仍然是一种很好的方式,可以显式地表示该目录中的代码对于其他人来说是安全使用的好方法。由 Travis Jeffery 撰写的 I'll take pkg over internal 博客文章提供了 pkg 和 internal 目录的一个很好的概述,以及什么时候使用它们是有意义的。当根目录包含大量非 Go 组件和目录时,这也是一种将 Go 代码分组到一个位置的方法,这使得运行各种 Go 工具变得更加容易(正如在这些演讲中提到的那样: 来自 GopherCon EU 2018 的 Best Practices for Industrial Programming , GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps 和 GoLab 2018 - Massimiliano Pippi - Project layout patterns in Go )。如果你想查看哪个流行的 Go 存储库使用此项目布局模式,请查看 /pkg 目录。这是一种常见的布局模式,但并不是所有人都接受它,一些 Go 社区的人也不推荐它。如果你的应用程序项目真的很小,并且额外的嵌套并不能增加多少价值(除非你真的想要:-),那就不要使用它。当它变得足够大时,你的根目录会变得非常繁琐时(尤其是当你有很多非 Go 应用组件时),请考虑一下。/vendor应用程序依赖项(手动管理或使用你喜欢的依赖项管理工具,如新的内置 Go Modules 功能)。go mod vendor 命令将为你创建 /vendor 目录。请注意,如果未使用默认情况下处于启用状态的 Go 1.14,则可能需要在 go build 命令中添加 -mod=vendor 标志。如果你正在构建一个库,那么不要提交你的应用程序依赖项。注意,自从 1.13 以后,Go 还启用了模块代理功能(默认使用 https://proxy.golang.org 作为他们的模块代理服务器)。在here 阅读更多关于它的信息,看看它是否符合你的所有需求和约束。如果需要,那么你根本不需要 vendor 目录。国内模块代理功能默认是被墙的,七牛云有维护专门的的模块代理 。服务应用程序目录/apiOpenAPI/Swagger 规范,JSON 模式文件,协议定义文件。有关示例,请参见 /api 目录。Web 应用程序目录/web特定于 Web 应用程序的组件:静态 Web 资产、服务器端模板和 SPAs。通用应用目录/configs配置文件模板或默认配置。将你的 confd 或 consul-template 模板文件放在这里。/initSystem init(systemd,upstart,sysv)和 process manager/supervisor(runit,supervisor)配置。/scripts执行各种构建、安装、分析等操作的脚本。这些脚本保持了根级别的 Makefile 变得小而简单(例如, https://github.com/hashicorp/terraform/blob/master/Makefile )。有关示例,请参见 /scripts 目录。/build打包和持续集成。将你的云( AMI )、容器( Docker )、操作系统( deb、rpm、pkg )包配置和脚本放在 /build/package 目录下。将你的 CI (travis、circle、drone)配置和脚本放在 /build/ci 目录中。请注意,有些 CI 工具(例如 Travis CI)对配置文件的位置非常挑剔。尝试将配置文件放在 /build/ci 目录中,将它们链接到 CI 工具期望它们的位置(如果可能的话)。/deploymentsIaaS、PaaS、系统和容器编排部署配置和模板(docker-compose、kubernetes/helm、mesos、terraform、bosh)。注意,在一些存储库中(特别是使用 kubernetes 部署的应用程序),这个目录被称为 /deploy。/test额外的外部测试应用程序和测试数据。你可以随时根据需求构造 /test 目录。对于较大的项目,有一个数据子目录是有意义的。例如,你可以使用 /test/data 或 /test/testdata (如果你需要忽略目录中的内容)。请注意,Go 还会忽略以“.”或“_”开头的目录或文件,因此在如何命名测试数据目录方面有更大的灵活性。有关示例,请参见 /test 目录。其他目录/docs设计和用户文档(除了 godoc 生成的文档之外)。有关示例,请参阅 /docs 目录。/tools这个项目的支持工具。注意,这些工具可以从 /pkg 和 /internal 目录导入代码。有关示例,请参见 /tools 目录。/examples你的应用程序和/或公共库的示例。有关示例,请参见 /examples 目录。/third_party外部辅助工具,分叉代码和其他第三方工具(例如 Swagger UI)。/githooksGit hooks。/assets与存储库一起使用的其他资产(图像、徽标等)。/website如果你不使用 Github 页面,则在这里放置项目的网站数据。有关示例,请参见 /website 目录。
明确了使用的结构以后,我们确定自己使用gin框架来进行编写,那我们现在应该拥有的文件大概如下,
当然现在里面都是空白的,但是大概结构如此,我们可以进行接下来gin框架的解析
什么是gin
gin是用go语言编写的一个轻量型的web框架,今天的任务就是在internal文件夹里实现一个route包进行路由
gin使用起来很简便,下面结合代码进行解读
/*route.go*/package routeimport("gochat/api/ChatApi" //使用的包"github.com/gin-gonic/gin" //调用gin"net/http")func NewRoute() *gin.Engine { //一个进行初始化路由的东西gin.SetMode(gin.DebugMode) //将gin设置为debug模式,方便调试,进行上线的时候更改为release模式server:=gin.Default() //这个东西就会生成一个gin实例server.Use(Cors()) //这俩是使用下面俩中间件server.Use(gin.Recovery())group := server.Group("") //进行路由,group为一个组,前缀为“”也就是空的都会进行路由到下面的api进行处理,今天先实现login与register{group.POST("/user/login", ChatApi.Login)group.POST("/user/register", ChatApi.Register) }return server}//cors中间件是进行跨越操作的,对于跨域操作来说,可以这么理解func Cors() gin.HandlerFunc {return func(c *gin.Context) {method := c.Request.Methodorigin := c.Request.Header.Get("Origin") //请求头部if origin != "" {c.Header("Access-Control-Allow-Origin", "*") // 可将将 * 替换为指定的域名c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")c.Header("Access-Control-Allow-Credentials", "true")}//允许类型校验if method == "OPTIONS" {c.JSON(http.StatusOK, "ok!")}defer func() {if err := recover(); err != nil {}}()c.Next()}}
在上面有个cors的问题,要理解cors,首先得知道什么是ajax
ajax是一种异步通信技术。在ajax出现之前,客户端与服务端之间直接通信。引入ajax之后,客户端与服务端加了一个第三者—ajax。有了ajax之后,通过在后台与服务器进行少量数据交换,可以达到在不刷新整个页面的情况下实现局部刷新。
同源策略是浏览器的一种安全策略,所谓同源是指浏览器的url 地址中的 域名,协议,端口完全相同, 只有同源的地址才可以相互通过AJAX的方式请求。
所以说 ajax是同源策略的
那么有哪些场景会有跨域ajax的需求呢?
- 当你调用一个现有的API或公开API:想象一下,你接到了一个新需求,需要在当前开发的新闻详细页http://www.yournews.com/p/123展示该新闻的相关推荐。令人欣慰的是,推荐的接口已经在你们公司的其他产品线里实现了,你只需要给该接口一个query即可:http://www.mynews.com/recommend?query=123。然而问题来了——你发起了一个跨域请求。
- 前后端分离的开发模式下,在本地进行接口联调时:也许在你的项目里,你想尝试前后端分离的开发模式。你在本地开发时,mock了一些假数据来帮助自己本地开发。而有一天,你希望在本地和后端同学进行联调。此时,后端rd的接口地址和你发生了跨域问题。这阻止了你们的联调,你只能继续使用你mock的假数据。
我是这么理解的,在本地开发的时候,部署的时候,你在后端开了一个http服务,端口为8888,但是前端的端口是3000,然后你在前端访问的时候,需要访问的是8888端口的资源,这就产生了跨域问题
解决跨域的方法有两种, CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。JSONP 只支持GET请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。 这里使用cors进行解决
http://javascript.ruanyifeng.com/bom/cors.html这个是cors的说明
所以必须实现cors中间件
在route.go中发现了什么
在route.go中,我们发现我们在chatapi中调用了一些东西,其实就是api,分别用来处理登录和注册
所以接下来进行编写api
在api文件夹中新建ChatApi文件夹,编写user_control.go
//user_control.gopackage ChatApiimport("github.com/gin-gonic/gin""gochat/internal/service""gochat/internal/model""net/http""gochat/pkg/common/request""gochat/pkg/common/response")func Login(c *gin.Context){var user model.User//然后绑定到json上c.ShouldBindJSON(&user)if service.UserService.Login(&user){ //调用service这个包c.JSON(http.StatusOK,response.SuccessMsg(user))return}c.JSON(http.StatusOK, response.FailMsg("Login failed"))}func Register(c *gin.Context){var user model.Userc.ShouldBindJSON(&user)err := service.UserService.Register(&user)if err != nil {c.JSON(http.StatusOK, response.FailMsg(err.Error()))return}c.JSON(http.StatusOK, response.SuccessMsg(user))}
在其中发现,其实主要逻辑还是存在于service这个包中,主要数据模型存在于model这个包内
所以接下来继续在internal中实现service包与model包,
//service , user_serivce.gopackage serviceimport ("gochat/internal/model""gochat/internal/pool""gochat/pkg/errors""github.com/google/uuid""time""gochat/pkg/common/request""gochat/pkg/common/response")type userService struct {}var UserService = new(userService)func (u *userService) Login(user *model.User) bool {pool.GetDB().AutoMigrate(&user)db := pool.GetDB()var queryUser *model.Userdb.First(&queryUser, "username = ?", user.Username)user.Uuid = queryUser.Uuidreturn queryUser.Password == user.Password}func (u *userService) Register(user *model.User) error {db := pool.GetDB()var userCount int64 //用usercount来判断id是否重复db.Model(user).Where("username", user.Username).Count(&userCount)if userCount>0{return errors.New("user exists!")}user.Uuid = uuid.New().String()//随机生成uuiduser.CreateAt = time.Now()user.DeleteAt = 0db.Create(&user)return nil}
//model user.gopackage modelimport ("time""gorm.io/gorm""gorm.io/plugin/soft_delete")type User struct {Id int32 `json:"id" gorm:"primary_key;AUTO_INCREMENT;comment:'id'"`Uuid string `json:"uuid" gorm:"type:varchar(150);not null;unique_index:idx_uuid;comment:'uuid'"`Username string `json:"username" form:"username" binding:"required" gorm:"unique;not null; comment:'用户名'"`Password string `json:"password" form:"password" binding:"required" gorm:"type:varchar(150);not null; comment:'密码'"`Nickname string `json:"nickname" gorm:"comment:'昵称'"`Avatar string `json:"avatar" gorm:"type:varchar(150);comment:'头像'"`Email string `json:"email" gorm:"type:varchar(80);column:email;comment:'邮箱'"`CreateAt time.Time `json:"createAt"`UpdateAt *time.Time `json:"updateAt"`DeleteAt int64 `json:"deleteAt"`}
上面的代码其实很好理解,model包定义了数据结构,因为使用的是gorm,所以其实相当于一个表
对于service包中的代码逻辑进行简单说明
对于register方法,就是进行数据库的读取,同时对于传入参数user中的username进行判断重复,因为你登陆的时候传入的参数只有用户名和密码等信息,所以不可以用id或者uuid进行判断重复,在对于用户名进行判断存在相同的个数,如果大于0个的话,就是说明是存在用户信息,所以用户存在,但是如果不存在呢,要进行随机生成uuid防止uuid重复的问题,同时对数据库进行create操作,这些操作我们利用了gorm,这是一个非常好用的东西,可以利用结构体进行对数据库的增删查改。
对于login方法,就是判断密码是否相同,如果相同就登录上了
gorm?
接下来就是dao的内容了,开始进行数据库的读写任务,
首先了解什么是gorm,那得首先说起orm的定义了
orm, Object-Relationl Mapping,即对象关系映射,这里的Relationl指的是关系型数据库
他的作用就是让你写代码的时候像操作对象一样的操作数据库
同时也是我们在internal中写的pool库中的内容
pool库内容如下,具体内容可以百度
//mysql.gopackage poolimport ("gochat/config""gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger")var _db *gorm.DBfunc init() {username := config.GetConfig().MySQL.User //账号password := config.GetConfig().MySQL.Password //密码host := config.GetConfig().MySQL.Host //数据库地址,可以是Ip或者域名port := config.GetConfig().MySQL.Port //数据库端口Dbname := config.GetConfig().MySQL.Name //数据库名timeout := "10s" //连接超时,10秒//拼接下dsn参数, dsn格式可以参考上面的语法,这里使用Sprintf动态拼接dsn参数,因为一般数据库连接参数,我们都是保存在配置文件里面,需要从配置文件加载参数,然后拼接dsn。dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)var err error//连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。_db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Info),})if err != nil {panic("连接数据库失败, error=" + err.Error())}sqlDB, _ := _db.DB()//设置数据库连接池参数sqlDB.SetMaxOpenConns(100) //设置数据库连接池最大连接数sqlDB.SetMaxIdleConns(20) //连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于20,超过的连接会被连接池关闭。}func GetDB() *gorm.DB {return _db}
viper?
然后在这里,使用了viper来进行配置,配置数据库的账号密码啊啥的,所以又得编写config中的内容了
package configimport ("fmt""github.com/spf13/viper")type TomlConfig struct {AppName stringMySQL MySQLConfigLog LogConfigStaticPath PathConfigMsgChannelType MsgChannelType}type LogConfig struct {Path stringLevel string}// 相关地址信息,例如静态文件地址type PathConfig struct {FilePath string}// 消息队列类型及其消息队列相关信息// gochannel为单机使用go默认的channel进行消息传递// kafka是使用kafka作为消息队列,可以分布式扩展消息聊天程序type MsgChannelType struct {ChannelType stringKafkaHosts stringKafkaTopic string}// MySQL相关配置type MySQLConfig struct {Host stringName stringPassword stringPort intTablePrefix stringUser string}var c TomlConfigfunc init() {// 设置文件名viper.SetConfigName("config")// 设置文件类型viper.SetConfigType("toml")// 设置文件路径,可以多个viper会根据设置顺序依次查找viper.AddConfigPath(".")viper.AutomaticEnv()err := viper.ReadInConfig()if err != nil {panic(fmt.Errorf("fatal error config file: %s", err))}viper.Unmarshal(&c) //读到结构体里}func GetConfig() TomlConfig {return c}
viper这个库很简单,就是对配置信息的读取,把配置信息读取到结构体里,然后编写config.toml文件就可以了
//config.toml[mysql]host = "127.0.0.1"name = "chat"password = "zhao0901"port = 3306tablePrefix = ""user = "root"
然后回到ChatApi中,发现我们还有一个response没有说,这个就是进行消息的回答的,对于下面具体的code
在前端中有具体的要求,这玩意主要就是对前端的回应
package responsetype ResponseMsg struct {Code int `json:"code"`Msg string `json:"msg"`Data interface{} `json:"data"`}func SuccessMsg(data interface{}) *ResponseMsg {msg := &ResponseMsg{Code: 0,Msg: "SUCCESS",Data: data,}return msg}func FailMsg(msg string) *ResponseMsg {msgObj := &ResponseMsg{Code: -1,Msg: msg,}return msgObj}func FailCodeMsg(code int, msg string) *ResponseMsg {msgObj := &ResponseMsg{Code: code,Msg: msg,}return msgObj}
你在运行之前还需要编写go.mod,具体方法就是打开cmd,输入go mod init gochat
然后在运行go mod vendor,他就会自动生成vendor文件夹,里面是你需要下载的包,不成功的可能需要更换一下云
ok了,今天学习结束了,然后运行他,就会发现可以正常的进行登录与注册,可以进入到下一个主体页面了
这就是开始,接下来明天继续
