Gin闪亮登场
简介
Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确。
Gin特点主要有: 速度快、性能优;支持中间件操作,方便编码处理;非常简单的实现路由等。
安装gin框架库
go get -u github.com/gin-gonic/gin
基架搭建
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个 默认的 路由引擎
r := gin.Default()
// 路由 和 控制器
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"hello": "world",
})
})
// 服务监听
err := r.Run("localhost:8080") // 默认 监听并在 0.0.0.0:8080 上启动服务
if err != nil {
panic(err)
}
}
路由引擎
在gin框架中,
路由引擎
是一个 一个结构体,其中包含了路由组、中间件、页面渲染接口、等相关内容。
engine1 = gin.Default() // 创建默认的 路由引擎
engine2 = gin.New() // 创建一个新的 路由引擎
- 说明:
gin.Default其实也使用gin.New()创建engine实例,但是会默认使用Logger
和Recovery
中间件。 - Logger是负责进行打印并输出日志的中间件,方便开发者进行程序调试;
- Recovery中间件的作用是如果程序执行过程中遇到panic中断了服务,则Recovery会恢复程序执行,并返回服务器500内部错误。
- 通常情况下,我们使用默认的gin.Default创建Engine实例。
1. 项目配置
这是一种 通用的 项目配置文件 的手段。
1. 配置文件: config/app.json
{
"app_name": "cloudrestaurant",
"app_mode": "debug",
"app_host": "localhost",
"app_port": "8090",
"database": {
"dbsize": "mysql",
"username": "root",
"password": "123456",
"host": "localhost",
"port": "3679",
"dbname": "gin",
"charset": "utf8mb4"
},
"redis_config": {
"addr": "127.0.0.1",
"port": "6379",
"password": "",
"db": 0
}
}
2. 解析文件: app/app.go
package app
import (
"bufio"
"encoding/json"
"os"
)
type Config struct {
AppName string `json:"app_name"`
AppMode string `json:"app_mode"`
AppHost string `json:"app_host"`
AppPort string `json:"app_port"`
Database Database `json:"database"`
RedisConfig RedisConfig `json:"redis_config"`
}
type Database struct {
DBsize string `json:"dbsize"`
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Port string `json:"port"`
DBname string `json:"dbname"`
Charset string `json:"charset"`
}
type RedisConfig struct {
Addr string `json:"addr"`
Port string `json:"port"`
Password string `json:"password"`
Db int `json:"db"`
}
var AppConfig *Config = nil
func ParseConfig(path string) {
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer file.Close()
reader := bufio.NewReader(file)
decoder := json.NewDecoder(reader)
err = decoder.Decode(&AppConfig)
if err != nil {
panic(err)
}
}
func init() {
ParseConfig("./config/app.json")
}
强调:
在Gin中,项目开发完最终所有的包 都会打包成一个可执行文件,这个文件和main包同目录,所以 项目中凡是设计
相对路径
的 都要站在main包的角度
去写相对路径。
3. 激活使用
package main
import (
// 在main文件引入,即 激活配置
"项目名称/app"
"fmt"
)
func main() {
// 使用配置中的数据
fmt.Println(app.AppConfig.AppName)
fmt.Println(app.AppConfig.Database.DBname)
}
2. 控制器
外观描述
是一个函数, 只有一个参数, 参数类型为:*gin.Context, 没有返回值,就是这么简单。
*gin.Context 参数是一个结构体, 是对 请求、响应报文 的封装,是Gin框架的主要内容,是上下文
!!!
上下文(gin.Context) 上 的 api
- Next()
将处理权 移交到 下一棒 中间件(或控制器) 手中
- Abort()
终断该请求的处理
- Set(“name”, “tom”)、Get(“name”)
在上下文中 写入数据,可以在 下一棒 中间件(或控制器) 手中 通过 Get()获取到对应数据
- 重定向
Redirect(302, “https://www.baidu.com”)
- 请求报文: Request
- c.Request.Proto => 请求协议
- c.Request.Host => 请求协议
- c.Request.URL => 请求路径
- c.Request.Method => 请求方法
- c.Request.Header[“Content-Type”] => 请求头相关
控制器 的实现
package controller
import (
"github.com/gin-gonic/gin"
)
// 将 控制器绑在 结构体上 是 模块化 思想的体现
// 很好的 避免了 不同路由 的 控制器 重名的问题。
type Testing struct {
}
func (that *Testing) HandGetTest(context *gin.Context) {
...
}
func (that *Testing) HandPostTest(context *gin.Context) {
...
}
3. 路由
单路由
使用 路由引擎
上封装 路由相关的api 可以很方便地实现 路由功能
package router
import (
"testGin/controller"
"github.com/gin-gonic/gin"
)
// 路由表
func ActiveRouterList(e *gin.Engine) {
e.Handle("GET", "/test", new(controller.Testing).HandGetTest)
// "语法糖"
e.GET("/test", new(controller.Testing).HandGetTest)
e.POST("/test", new(controller.Testing).HandPostTest)
}
- 在 main函数 中 激活🔥路由表
package main
import (
"testGin/router"
"github.com/gin-gonic/gin"
)
func main() {
app := gin.Default()
// 激活🔥路由表
router.ActiveRouterList(app)
app.Run()
}
路由组
使用 路由引擎 的 Group 方法
// 激活 路由表
func ActiveRouterList(e *gin.Engine) {
group := e.Group("/aaa")
{ // 加这个 块级括号 ,是为了 彰显 路由组 层次感
group.GET("/b1", new(controller.Testing).HandGetTest)
group.GET("/b2", new(controller.Testing).HandGetTest)
group2 := group.Group("b3")
{
group2.GET("/c1", new(controller.Testing).HandGetTest)
group2.GET("/c2", new(controller.Testing).HandGetTest)
}
}
// 路由组访问地址: /aaa/b1 和 /aaa/b3/c1
}
其他路由
404路由
e.NoRoute(控制器)
广角路由
// 可以匹配 任何请求方法
e.Any("test", 控制器)
静态路由
上面的路由都是动态路由,下面介绍一下 静态路由。
静态路由,为静态文件 指定 的访问路径。
工程目录下 的
static文件夹
下的images文件夹
下存放这aaa.png
图片文件,
写一个静态路由来访问这个文件。
// 路由表
func ActiveRouterList(e *gin.Engine) {
e.Static("/img", "./static/images/")
// 图片的访问路径: /img/aaa.png
}
4. 中间件
gin框架允许开发者在 处理请求 的过程中,加入用户自己的 钩子函数。这个 钩子函数 就是 中间件。
中间件适合处理一些 公共的 业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
Gin 的中间件 是 洋葱模型的 运行流程。
定义一个中间件
// 设计一个 统计 请求耗时 的中间件(默认路由引擎 中已经实现,这里重点定义中间件)
package midlleware
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// 中间件 的 本质上 就是 控制器 生产函数
// 所以 返回值 是一个 控制器
func TimeCost() (gin.HandlerFunc) {
return func(c *gin.Context) {
start := time.Now()
c.Next() // 将控制权 主动交给 下一棒
cost := time.Since(start)
fmt.Println(cost)
}
}
注册 使用 中间件
单路由 注册
// 在路由表中,找到 对应的路由 加入即可
e.GET("/test", midlleware.TimeCost(), new(controller.Testing).HandGetTest)
// 注意,要以调用的方式 放入 中间件
全局 注册
// 在路由表中
e.Use(midlleware.TimeCost())
路由组 注册
// 方式一, 单路由注册 的方式
group := e.Group("aaa", midlleware.TimeCost())
{
...
}
// 方式二, 全局 注册 的方式
group := e.Group("aaa")
group.Use(midlleware.TimeCost())
{
...
}
实用中间件: 请求追踪 中间件
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// 两种启动模式
func main() {
addr := "0.0.0.0:8888"
// app := gin.Default()
app := gin.New()
app.Use(RequestLog)
app.GET("/ping", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"hello": "world",
})
})
app.POST("/ping", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"hello": "world",
})
})
fmt.Println("服务监听:" + addr)
err := app.Run(addr)
if err != nil {
panic(err)
}
}
/*
gin的 请求日志 de 中间件,
记录请求信息: 请求数据、响应数据、请求路径、请求方法、客户端ip、请求发生时间、处理时长、请求头
*/
func RequestLog(c *gin.Context) {
// 记录请求开始时间
t := time.Now()
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
// 必须!
c.Writer = blw
// 获取请求信息
requestBody := getRequestBody(c)
c.Next()
// 记录请求所用时间
latency := time.Since(t)
// 获取响应内容
responseBody := blw.body.String()
logContext := make(map[string]interface{})
// 日志 输出 项目:
logContext["request_uri"] = c.Request.RequestURI // 请求路径
logContext["request_method"] = c.Request.Method // 请求方法
logContext["refer_service_name"] = c.Request.Referer() //
logContext["refer_request_host"] = c.ClientIP() // 客户端ip
logContext["request_body"] = requestBody // 请求数据
logContext["request_time"] = t.String() // 请求发生时间
logContext["response_body"] = responseBody // 响应数据
logContext["time_used"] = fmt.Sprintf("%v", latency) // 处理时长
logContext["header"] = c.Request.Header // 请求头
fmt.Println(logContext)
}
// bodyLogWriter 定义一个存储响应内容的结构体
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
// Write 读取响应数据
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
// getRequestBody 获取请求参数
func getRequestBody(c *gin.Context) interface{} {
switch c.Request.Method {
case http.MethodGet:
return c.Request.URL.Query()
case http.MethodPost:
fallthrough
case http.MethodPut:
fallthrough
case http.MethodPatch:
var bodyBytes []byte // 我们需要的body内容
// 可以用buffer代替ioutil.ReadAll提高性能
bodyBytes, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
return nil
}
// 将数据还回去
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
return string(bodyBytes)
}
return nil
}
5. 前后交互
接收数据
api直取
获取get 请求的 Param参数(url参数)
e.GET("/test/:name", func(c *gin.Context){
name := c.Param("name")
})
获取get 的 请求参数
e.GET("/test", func(c *gin.Context){
name := c.Query("name")
})
说明:
如果 请求参数 name是必须的,但是客户访问的时候又不一定传时,我们可以为 name设置一个
默认值:
name := c. DefaulQuery("name", " 默认值 ")
获取 POST 以 表单的形式
提交数据
engine.Handle("POST", "/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("pwd")
})
- 说明:
post请求参数 也可以设置默认值:
name := c. DefaultPostForm ("name ", "默认值")
对应绑定
api直取 的 方式 获取数据 有两方面的 缺点:
- 无法获取 POST 以
json
形式 提交的数据 - 一个一个获取单个属性键值对,如果要获取的键值对多了,再一个个获取,就有无语了。
获取 GET 的 请求参数 (ShouldBindQuery)
/aaa?name=tom&age=10
通过 ShouldBindQuery()方法实现
package router
import (
"fmt"
"github.com/gin-gonic/gin"
)
//form之后的名 和 提交 数据 的键名 对应不起来,
//则无法做到对应赋值
type man struct {
Name string `form:"name"`
Age int `form:"age"`
}
// 路由表
func ActiveRouterList(e *gin.Engine) {
e.GET("/aaa", func(c *gin.Context) {
var peop man
err := c.ShouldBindQuery(&peop)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%T >>---> %+v\n", peop, peop)
//router.man >>---> {Name:tom Age:10}
c.JSON(200, gin.H{"hi": "hi"})
})
}
获取 POST 以 json形式
提交数据 (ShouldBindJSON)
通过
ShouldBindJSON()
方法实现
只需将上面的 ShouldBindQuery 改为 BindJSON 即可,其他都不用变。
获取 POST 以 表单的形式
提交数据 (ShouldBind)
通过
ShouldBind()
方法实现
只需将上面的 ShouldBindQuery 改为 ShouldBind 即可,其他都不用变。
获取上传文件
单文件
e.POST("/aaa", func(c *gin.Context) {
file, err := c.FormFile("avatar")
if err != nil {
fmt.Println(err)
}
// SaveUploadedFile 不会创建文件夹,
// 所以 确保 filePath 上的文件夹都是已经存在的。
// 这里最好 用一下uuid
var filePath = "./uploadfile/" + file.Filename
err = ctx.SaveUploadedFile(file, filePath)
}
多文件
package filesupload
import (
"fmt"
"zi_li_hua_xia/global"
"zi_li_hua_xia/pkg/resp"
"github.com/gin-gonic/gin"
uuid "github.com/go-basic/uuid"
)
func UploadMutipleFile(c *gin.Context) {
form, _ := c.MultipartForm()
fmt.Printf("%T---%+v\n", form, form)
files := form.File
for key, val := range files {
fmt.Println(key, val)
var filePath = global.AppSetting.UploadSavePath + uuid.New() + val[0].Filename
// 上传文件到指定的目录
err := c.SaveUploadedFile(val[0], filePath)
if err != nil {
resp.Fail(c, 1, err.Error())
return
}
}
resp.Ok(c)
}
流式上传文件
一个 HTML 表单中的 enctype 有三种类型
- application/x-www-urlencoded
- multipart/form-data
- text-plain
默认情况下是 application/x-www-urlencoded,当表单使用 POST 请求时,数据会被以 x-www-urlencoded 方式编码到 Body 中来传送.
multipart/x-www-form-urlencoded:被发送到服务端的http消息的body在本质上是一个巨大的查询字符串:name/value对被&符号分开,name和value被=符号分开,例如:
MyVariableOne=ValueOne&MyVariableTwo=ValueTwo
非字母和数字的字符会被%HH来代替,一个百分比符号和两个16位进制的数字代表着那个字符的ASCII码
这意味着每一个在value中的非字母和数字的字节,都将被3个字节的数据代替。如果是一个大的二进制的文件,那么3倍的传输数据将会变得十分低效率。
这时multipart/form-data就该出现了。
multipart/form-data
顾名思义可以上传多个form-data 并且用分隔符进行分割,多用于文件上传
通过 boundary 定义 内容分割标志 和 内容长度行 来综合控制包体的内容划分。
- main.go
package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
/// 解析多个文件上传中,每个具体的文件的信息
type FileHeader struct {
ContentDisposition string
Name string
FileName string ///< 文件名
ContentType string
ContentLength int64
}
/// 解析描述文件信息的头部
/// @return FileHeader 文件名等信息的结构体
/// @return bool 解析成功还是失败
func ParseFileHeader(h []byte) (FileHeader, bool) {
arr := bytes.Split(h, []byte("\r\n"))
var out_header FileHeader
out_header.ContentLength = -1
const (
CONTENT_DISPOSITION = "Content-Disposition: "
NAME = "name=\""
FILENAME = "filename=\""
CONTENT_TYPE = "Content-Type: "
CONTENT_LENGTH = "Content-Length: "
)
for _, item := range arr {
if bytes.HasPrefix(item, []byte(CONTENT_DISPOSITION)) {
l := len(CONTENT_DISPOSITION)
arr1 := bytes.Split(item[l:], []byte("; "))
out_header.ContentDisposition = string(arr1[0])
if bytes.HasPrefix(arr1[1], []byte(NAME)) {
out_header.Name = string(arr1[1][len(NAME) : len(arr1[1])-1])
}
l = len(arr1[2])
if bytes.HasPrefix(arr1[2], []byte(FILENAME)) && arr1[2][l-1] == 0x22 {
out_header.FileName = string(arr1[2][len(FILENAME) : l-1])
}
} else if bytes.HasPrefix(item, []byte(CONTENT_TYPE)) {
l := len(CONTENT_TYPE)
out_header.ContentType = string(item[l:])
} else if bytes.HasPrefix(item, []byte(CONTENT_LENGTH)) {
l := len(CONTENT_LENGTH)
s := string(item[l:])
content_length, err := strconv.ParseInt(s, 10, 64)
if err != nil {
log.Printf("content length error:%s", string(item))
return out_header, false
} else {
out_header.ContentLength = content_length
}
} else {
log.Printf("unknown:%s\n", string(item))
}
}
if len(out_header.FileName) == 0 {
return out_header, false
}
return out_header, true
}
/// 从流中一直读到文件的末位
/// @return []byte 没有写到文件且又属于下一个文件的数据
/// @return bool 是否已经读到流的末位了
/// @return error 是否发生错误
func ReadToBoundary(boundary []byte, stream io.ReadCloser, target io.WriteCloser) ([]byte, bool, error) {
read_data := make([]byte, 1024*8)
read_data_len := 0
buf := make([]byte, 1024*4)
b_len := len(boundary)
reach_end := false
var i = 0
for !reach_end {
read_len, err := stream.Read(buf)
if err != nil {
if err != io.EOF && read_len <= 0 {
return nil, true, err
}
reach_end = true
}
fmt.Printf("--%d-------------- read_data: %s\n", i, read_data)
i++
//todo: 下面这一句很蠢,值得优化
copy(read_data[read_data_len:], buf[:read_len]) //追加到另一块buffer,仅仅只是为了搜索方便
read_data_len += read_len
if read_data_len < b_len+4 {
continue
}
loc := bytes.Index(read_data[:read_data_len], boundary)
if loc >= 0 {
//找到了结束位置
target.Write(read_data[:loc-4])
return read_data[loc:read_data_len], reach_end, nil
}
target.Write(read_data[:read_data_len-b_len-4])
copy(read_data[0:], read_data[read_data_len-b_len-4:])
read_data_len = b_len + 4
}
target.Write(read_data[:read_data_len])
return nil, reach_end, nil
}
/// 解析表单的头部
/// @param read_data 已经从流中读到的数据
/// @param read_total 已经从流中读到的数据长度
/// @param boundary 表单的分割字符串
/// @param stream 输入流(请求体)
/// @return FileHeader 文件名等信息头
/// []byte 已经从流中读到的部分
/// error 是否发生错误
func ParseFromHead(read_data []byte, read_total int, boundary []byte, stream io.ReadCloser) (FileHeader, []byte, error) {
buf := make([]byte, 1024*4)
found_boundary := false
boundary_loc := -1
var file_header FileHeader
for {
read_len, err := stream.Read(buf) // 从 读取流(请求体)中读取指定长度的数据
if err != nil {
if err != io.EOF {
return file_header, nil, err
}
break
}
// fmt.Println("+++++++++++++++++++++++++++++++++++")
// fmt.Println("+++++++++++++++++++++++++++++++++++")
// fmt.Println("+++++++++++++++++++++++++++++++++++")
// fmt.Printf("read_total: %v\n", read_total)
// fmt.Printf("read_len: %v\n", read_len)
// fmt.Printf("cap(read_data): %v\n", cap(read_data))
if read_total+read_len > cap(read_data) {
return file_header, nil, fmt.Errorf("not found boundary")
}
// fmt.Printf("--- read_data: %s\n", read_data)
copy(read_data[read_total:], buf[:read_len])
// fmt.Printf("+++ read_data: %s\n", read_data)
read_total += read_len
if !found_boundary {
boundary_loc = bytes.Index(read_data[:read_total], boundary)
if -1 == boundary_loc {
continue
}
found_boundary = true
}
start_loc := boundary_loc + len(boundary)
// fmt.Printf("start_loc: %v\n", start_loc)
file_head_loc := bytes.Index(read_data[start_loc:read_total], []byte("\r\n\r\n"))
// fmt.Printf("file_head_loc: %v\n", file_head_loc)
if -1 == file_head_loc {
continue
}
file_head_loc += start_loc
ret := false
file_header, ret = ParseFileHeader(read_data[start_loc:file_head_loc])
// fmt.Printf("file_header: %+v\n", file_header)
if !ret {
return file_header, nil, fmt.Errorf("ParseFileHeader fail:%s", string(read_data[start_loc:file_head_loc]))
}
return file_header, read_data[file_head_loc+4 : read_total], nil
}
return file_header, nil, fmt.Errorf("reach to sream EOF")
}
// 路由处理函数
func UploadStreamFileHandleFunction(c *gin.Context) {
/*
step1: 获取 请求报文 的 Content-Length: 27096850
step2: 获取 请求报文 的 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2LPMYAVhylDfw08W
*/
log.SetFlags(log.LstdFlags | log.Lshortfile)
var content_length int64 = c.Request.ContentLength
// fmt.Printf("c.Request.ContentLength: %d\n", c.Request.ContentLength) //(文件大小 单位byte)
if content_length <= 0 || content_length > 1024*1024*1024*2 {
log.Printf("content_length error\n")
return
}
content_type_, has_key := c.Request.Header["Content-Type"]
if !has_key {
log.Printf("Content-Type error\n")
return
}
// fmt.Printf("content_type_: %v\n", content_type_) //[multipart/form-data; boundary=----WebKitFormBoundaryVI7jEWfO55qBsBkm]
if len(content_type_) != 1 {
log.Printf("Content-Type count error\n")
return
}
content_type := content_type_[0]
// fmt.Printf("content_type: %v\n", content_type) // multipart/form-data; boundary=----WebKitFormBoundaryVI7jEWfO55qBsBkm
const BOUNDARY string = "; boundary=" //边界符
loc := strings.Index(content_type, BOUNDARY) // 返回边界符所在字符串的索引号,找不到返回-1
// fmt.Printf("loc: %v \n", loc) //19
if -1 == loc {
log.Printf("Content-Type error, no boundary\n")
return
}
// 获取 multipart/form-data 的 内容分割标志
boundary := []byte(content_type[(loc + len(BOUNDARY)):])
// fmt.Printf("content_type ==slice==》boundary: %s\n", boundary) //----WebKitFormBoundaryYsjLeYO8UjoZgB5e
//
read_data := make([]byte, 1024*12)
var read_total int = 0
var i = 1
for {
fmt.Println(1)
i++
//---------------//
file_header, file_data, err := ParseFromHead(read_data, read_total, append(boundary, []byte("\r\n")...), c.Request.Body)
if err != nil {
log.Printf("%v", err)
return
}
log.Printf("file :%s\n", file_header.FileName)
//
f, err := os.Create(file_header.FileName)
if err != nil {
log.Printf("create file fail:%v\n", err)
return
}
f.Write(file_data)
file_data = nil
//需要反复搜索boundary
temp_data, reach_end, err := ReadToBoundary(boundary, c.Request.Body, f)
f.Close()
if err != nil {
log.Printf("%v\n", err)
return
}
if reach_end {
break
} else {
copy(read_data[0:], temp_data)
read_total = len(temp_data)
continue
}
}
c.JSON(200, gin.H{
"message": "UploadOk",
})
}
func main() {
r := gin.Default()
r.StaticFile("/index", "./upload.html") // 访问 localhost:55555/index 进行文件上传操作
r.POST("/gin_upload", UploadStreamFileHandleFunction)
r.Run("localhost:55555")
}
- upload.html (同目录下的html文件)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>upload file</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="/gin_upload">
<input type="file" name="ff" multiple="multiple"/><br/>
<input type="submit" value="提交"/>
</form>
</body>
响应数据
返回普通 字符串
涉及到的 api是 gin.Context 上的
Writer
实例
[]byte
engine.GET("/hello", func (c *gin.Context) {
var s []byte = []byte{'a', 'b', 88}
c.Writer.Write(s)
// 前端收到的是: abX
})
string
var s string = "abc"
c.Writer.WriteString(s)
// 前端收到的是: abc
返回 json 字符串
涉及到的 api是 gin.Context 上的
JOSN
方法
map
e.POST("/aaa", func(c *gin.Context) {
m := map[string]interface{}{
"name": "tom",
"age": 11,
}
c.JSON(200, m)
// 返回给前端的数据: `{ "age": 11,"name": "tom"}`
})
struct
e.POST("/aaa", func(c *gin.Context) {
obj := struct {
Name string `json:"name"`
Age int `json:"old"`
}{"tom", 8}
c.JSON(200, obj)
// 返回给前端的数据: `{"name": "tom", "old": 8}`
})
6. 数据模型
请转到我的GORM那一篇。
7. 会话管理
设置cookie
浏览器中收到 设置的 cookie 会自动进行保存。
c.SetCookie("cookie-key", "cookie-val", 3600, "/", "", false, true)
获取cookie
cookie 信息 在
请求头的 Cookie 字段
中保存,下面的方法封装了 对 请求头 Cookie 字段的分段操作
cookie, err := c.Cookie("cookie-key")
8. 日志记录
通过使用logrus日志框架,以中间件的方式 实现日志记录
package midlleware
import (
"fmt"
"os"
"path"
"time"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// 判断目录是否存在
func isExist(filePath string) (bool, error, string) {
_, err := os.Stat(filePath)
if err == nil {
return true, nil, "“文件存在”"
} else {
if os.IsNotExist(err) {
return false, nil, "“文件不存在”"
} else {
return false, err, "“包错原因不是因为文件不存在导致的”"
}
}
}
//日志记录到文件
func LoggerToFile() gin.HandlerFunc {
logFilePath := "./logFile"
logFileName := "logName"
//文件路径
fileName := path.Join(logFilePath, logFileName)
res, _, _ := isExist(fileName)
if !res {
err := os.Mkdir("./logFile", os.ModePerm)
if err != nil {
fmt.Println(err)
}
}
// 写入文件
file, err := os.OpenFile(fileName, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0700)
if err != nil {
fmt.Println("err", err)
}
logger := logrus.New()
// 设置输出
logger.Out = file
// 设置日志级别
logger.SetLevel(logrus.DebugLevel)
return func(c *gin.Context) {
startTime := time.Now()
c.Next()
endTime := time.Now()
// 下面是 要写入日志的 字段
// 执行时间
allTime := endTime.Sub(startTime)
// 请求方式
reqMethod := c.Request.Method
// 请求路由
reqUrl := c.Request.RequestURI
// 状态码
statusCode := c.Writer.Status()
// 请求ip
clientIP := c.ClientIP()
// 换一下日期格式
logger.SetFormatter(&logrus.TextFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
// 换成json格式
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
// 日志格式一: 日志变成json
entry := logger.WithFields(logrus.Fields{
"status_code": statusCode,
"all_time": allTime,
"client_ip": clientIP,
"req_method": reqMethod,
"req_url": reqUrl,
})
entry.Info()
// //日志格式二: 日志是字符串
// logger.Infof("| %3d | %13v | %15s | %s | %s |",
// statusCode,
// allTime,
// clientIP,
// reqMethod,
// reqUrl,
// )
}
}
- 注意:
日志的文件存储路径 通过配置文件来决定,这里只是简单的演示。
9. 错误处理
404 路由 + 重定向 or 制定404控制器
10. 协议升级
https
借助 github.com/unrolled/secure 包,以 全局中间件的形式开启https
// 开启https
func TlsHandler() gin.HandlerFunc {
return func(c *gin.Context) {
secureMiddleware := secure.New(secure.Options{
SSLRedirect: true,
SSLHost: tools.Config.AppHost + ":" + tools.Config.AppPort,
})
err := secureMiddleware.Process(c.Writer, c.Request)
// If there was an error, do not continue.
if err != nil {
tools.Console_error("https 开启异常")
return
}
c.Next()
}
}
WebSocket
WebSocket 是 应用层 即 第七层上的一个协议, 因此 它必须依赖于 HTTP协议
进行一次握手
。 握手成功后, 数据 就直接从TCP
通过传输,其后的动作就与HTTP无关了。
WebSocket 是一种在单个TCP连接上的 全双工通信 的协议。
WebSocket建立连接后,相互沟通所消耗的请求头是很小的,服务器 向 客户端 推送的消息的资源消耗也就小了。
在go中借助 github.com/gorilla/websocket包实现
基本用法
服务端
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
//webSocket请求ping 返回pong
func ping(c *gin.Context) {
//升级get请求为webSocket协议
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer ws.Close()
//读取ws中的数据
mt, message, err := ws.ReadMessage()
if err != nil {
return
}
// 读取 前端发过来的信息
fmt.Println(string(message))
for {
// 训环发送信息 到前端
var msg string = `{"name":"tom", "age":11}`
time.Sleep(2 * time.Second)
//写入ws数据
err = ws.WriteMessage(mt, []byte(msg))
if err != nil {
break
}
}
}
func main() {
bindAddress := "localhost:2303"
r := gin.Default()
//websocket本机服务的请求地址:ws://localhost:2303/ping
r.GET("/ping", ping)
r.Run(bindAddress)
}
客户端
通过vue 实现socket请求
- 在src下封装的websocket请求的js文件:
// 文件名: socketRequest.js
let websock = null
let messageCallback = null
let errorCallback = null
// let wsUrl = 'ws://localhost:2303/ping'
// let tryTime = 0
// 接收ws后端返回的数据
function websocketonmessage (e) {
messageCallback(JSON.parse(e.data))
}
/**
* 发起websocket连接
* @param {Object} agentData 需要向后台传递的参数数据
*/
function websocketSend (agentData) {
// 加延迟是为了尽量让ws连接状态变为OPEN
setTimeout(() => {
// 添加状态判断,当为OPEN时,发送消息
if (websock.readyState === websock.OPEN) { // websock.OPEN = 1
// 发给后端的数据需要字符串化
websock.send(JSON.stringify(agentData))
}
if (websock.readyState === websock.CLOSED) { // websock.CLOSED = 3
console.log('websock.readyState=3')
console.log('ws连接异常,请稍候重试')
errorCallback()
}
}, 500)
}
// 关闭ws连接
function websocketclose (e) {
// e.code === 1000 表示正常关闭。 无论为何目的而创建, 该链接都已成功完成任务。
// e.code !== 1000 表示非正常关闭。
if (e && e.code !== 1000) {
console.log('ws连接异常,请稍候重试')
errorCallback()
// 如果需要设置异常重连则可替换为下面的代码,自行进行测试
// if (tryTime < 10) {
// setTimeout(function() {
// websock = null
// tryTime++
// initWebSocket()
// console.log(`第${tryTime}次重连`)
// }, 3 * 1000)
// } else {
// alert('重连失败!请稍后重试')
// }
} else {
console.log("链接关闭成功")
}
}
// 建立ws连接
function websocketOpen (e) {
console.log(e)
console.log('ws连接成功')
}
// 初始化weosocket
function initWebSocket (requstWsUrl) {
if (typeof (WebSocket) === 'undefined') {
alert('您的浏览器不支持WebSocket,无法获取数据')
return false
}
// 请求建立链接
websock = new WebSocket(requstWsUrl)
// 链接是 websocket 链接的 4事件
// 链接事件: 链接建立成功
websock.onopen = function () {
websocketOpen()
}
// 链接事件: 链接收到消息
websock.onmessage = function (e) {
websocketonmessage(e)
}
// 链接事件: 链接异常
websock.onerror = function () {
alert('ws连接异常,请稍候重试')
errorCallback()
}
// 链接事件: 链接关闭
websock.onclose = function (e) {
websocketclose(e)
}
}
/**
* 发起websocket请求函数
* @param {string} url ws连接地址
* @param {Object} agentData 传给后台的参数
* @param {function} successCallback 接收到ws数据,对数据进行处理的回调函数
* @param {function} errCallback ws连接错误的回调函数
*/
export function sendWebsocket (url, agentData, successCallback, errCallback) {
messageCallback = successCallback
errorCallback = errCallback
initWebSocket(url)
websocketSend(agentData)
}
/**
* 关闭websocket函数
*/
export function closeWebsocket () {
if (websock) {
websock.close() // 关闭websocket
console.log("关闭链接")
}
}
在App.vue 文件中使用:
<template>
<div id="app">
<button @click="OpenConn">点击发起websocket请求</button>
<button @click="closeConn">关闭websocket请求</button>
<h1>{{res.name}}</h1>
<h1>{{res.age}}</h1>
</div>
</template>
<script>
import { sendWebsocket, closeWebsocket } from "./socketRequest.js";
export default {
name: "App",
data: () => ({
res:{
name:"cat",
age:0,
},
}),
methods: {
// ws连接成功,后台返回的ws数据,组件要拿数据渲染页面等操作
wsMessage(data) {
this.res.name = data.name
this.res.age = data.age
},
// ws连接失败,组件要执行的代码
wsError() {
// 比如取消页面的loading
console.warn("conn abnormal")
},
OpenConn() {
// 防止用户多次连续点击发起请求,所以要先关闭上次的ws请求。
closeWebsocket();
// 跟后端协商,需要什么参数数据给后台
const obj = {
monitorUrl: "aaa",
userName: "bbb",
};
// 发起ws请求
sendWebsocket(
"ws://localhost:2303/ping",
obj,
this.wsMessage,
this.wsError
);
},
closeConn() {
closeWebsocket()
}
},
};
</script>
指定socket通信
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { // 处理跨域
return true
},
}
// socket 大本营
var socketsOrganization = make(map[string]*websocket.Conn)
// 收录 socket
func RecordSocketConn(c *gin.Context) {
name := c.Query("name")
//升级get请求为webSocket协议
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("将进来的 socket 连接 起一个别名为 %s 保存在map中", name)
socketsOrganization[name] = ws
}
// 测试 socket
func TestSocket(ctx *gin.Context) {
msg := ctx.Query("msg")
toUser := ctx.Query("to")
socketConn, has := socketsOrganization[toUser]
if !has {
ctx.JSON(0, "找不到 指定 别名 的 socket 连接")
return
}
fmt.Printf("给别名为%s的 socket 连接发送信息: %s", toUser, msg)
err := socketConn.WriteMessage(1, []byte(msg))
if err != nil {
fmt.Println(err)
ctx.JSON(0, err.Error())
return
}
ctx.JSON(0, "ok")
}
func main() {
bindAddress := "localhost:2303"
r := gin.Default()
r.GET("/ws", RecordSocketConn)
r.GET("/test", TestSocket)
r.Run(bindAddress)
}
/* 测试:
socket链接一:
ws://localhost:2303/ws?name=tom
socket链接二:
ws://localhost:2303/ws?name=cat
通过测试地址 给 指定的 socket 链接发信息:
http://localhost:2303/test?to=cat&msg=hi_cat
*/
11. 常见问题
- [GIN-debug] redirecting request 307
Gin 的 url 是
/a/b/
, 如果客户端发送的请求是/a/b
, 比url 在最后位置 少一个 /
就会出现这个问题,客户端要和 Gin 的路径一模一样, 就可以解决