Dawn's Blogs

分享技术 记录成长

0%

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
dain/
|--context.go
|--dain.go
|--router.go
|--go.mod
main.go
go.mod

实现目标

最终实现得效果如下,此时更加接近于 GIN:

  • Handler 得参数变为 dain.Context,同时提供了对表单和 URL 的查询 PostForm/Query
  • dain.Context 同 GIN 一样,封装了 HTML/String/JSON/Data 函数,快速构建 HTTP 响应。

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"DawnGin/dain"
"net/http"
)

func main() {
e := dain.New()

e.Get("/", func(c *dain.Context) {
c.HTML(http.StatusOK, "<h1>Hello Dawn</h1>")
})

e.Get("/hello", func(c *dain.Context) {
c.String(http.StatusOK, "Hello World, URL path = %v", c.Path)
})

e.Post("/login", func(c *dain.Context) {
c.JSON(http.StatusOK, dain.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})

e.Run(":9000")
}

构建上下文

dain/context.go

上下文 Context 即一个请求的上下文,它随着每一个请求的出现而产生,响应的结束而销毁。

Context 结构

Context 可以对一些代码进行封装,使用起来更加简便。首先看 Context 的结构:

  • dain.H 同 gin.H 的作用一样,是 map[string]interface{} 的简便写法。
  • 在 Context 中,保存了一次HTTP的请求 Req 和响应 Writer,同时保存了一些请求和响应信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type H map[string]interface{}

type Context struct {
// HTTP 请求 响应
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string // 请求路径
Method string // 请求方法
// 响应信息
StatusCode int // 响应码
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Req: r,
Path: r.URL.Path,
Method: r.Method,
}
}

获取请求数据

封装关于获取请求数据的函数 PostForm/Query

1
2
3
4
5
6
7
8
9
// PostForm 根据 key 获取第一个表单数据
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}

// Query 根据 key 获取请求的 query 数据
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}

改变 HTTP 响应头

封装改变 HTTP 响应的函数 Status/SetHeader

1
2
3
4
5
6
7
8
9
10
// Status 设置响应状态码
func (c *Context) Status(code int) {
c.StatusCode = code
c.Writer.WriteHeader(code)
}

// SetHeader 设置响应头
func (c *Context) SetHeader(key, value string) {
c.Writer.Header().Set(key, value)
}

快速响应

封装快速响应的函数 String/JSON/Data/HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (c *Context) String(code int, format string, values ...interface{}) error {
c.SetHeader("Content-Type", "text/plain")
c.Status(code)
_, err := c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
return err
}

func (c *Context) JSON(code int, obj interface{}) error {
c.SetHeader("Content-Type", "application/json")
c.Status(code)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
return err
}

return nil
}

func (c *Context) Data(code int, data []byte) error {
c.Status(code)
_, err := c.Writer.Write(data)
return err
}

func (c *Context) HTML(code int, html string) error {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
_, err := c.Writer.Write([]byte(html))
return err
}

路由器

dain/router.go

router 结构

于此同时,将路由器从 dain/dain.go 中分离出来。路由器的结构为:

1
2
3
4
5
6
7
8
9
type router struct {
handlers map[string]HandlerFunc
}

func NewRouter() *router {
return &router{
handlers: make(map[string]HandlerFunc),
}
}

路由功能

需要稍微更改路由注册和路由功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// addRouter 实现路由注册功能
func (r *router) addRouter(method, pattern string, handler HandlerFunc) {
log.Printf("Router %v - %v\n", method, pattern)
key := method + "-" + pattern
r.handlers[key] = handler
}

// handle 实现路由功能
func (r *router) handle(c *Context) {
key := c.Method + "-" + c.Path
if handler, ok := r.handlers[key]; ok {
handler(c)
} else {
fmt.Fprintf(c.Writer, "404 NOT FOUND FOR PATH: %v", c.Path)
}
}

框架入口

dain/dain.go

与此同时,也要更改框架入口。

首先需要将 HandlerFunc 的参数改变为 Context:

1
2
// HandlerFunc handler 函数类型
type HandlerFunc func(c *Context)

改变 Engine 的内部结构,使之内嵌 router 结构体

1
2
3
4
5
6
7
8
9
10
11
type Engine struct {
// 路由器
router *router
}

// New 返回一个 Engine 指针
func New() *Engine {
return &Engine{
router: NewRouter(),
}
}

更改路由注册的内部逻辑为,调用 router 的路由注册:

1
2
3
4
// addRouter 实现路由注册功能
func (e *Engine) addRouter(method, pattern string, handler HandlerFunc) {
e.router.addRouter(method, pattern, handler)
}

最后,需要更改 http.Handler 接口函数 ServeHTTP 的实现:

1
2
3
4
5
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := NewContext(w, r)
e.router.handle(c)
}

本系列参考于 极客兔兔-7天用Go从零实现Web框架Gee教程,将从零开始实现一个简易的仿 GIN 的框架,称为 Dawn’s Gin 简称 dain

在本节,最终的代码目录结构如下:

1
2
3
4
5
dain/
|--dain.go
|--go.mod
main.go
go.mod

实现目标

我们最终的实现效果如下,可以看到自实现的框架与 GIN 十分相似:

  • 通过 e.GETe.POST 注册路由
  • 最后调用 e.RUN 运行 Web 服务器。

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"DawnGin/dain"
"fmt"
"net/http"
)

func main() {
e := dain.New()

e.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World, URL path = %v", r.URL.Path)
})

e.Post("/", func(w http.ResponseWriter, r *http.Request) {
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
})

e.Run(":9000")
}

引擎

dain/dain.go

在 GIN 中,添加路由等的操作都是通过引擎 Engine 完成的,所以自定义 Engine。

其中 Engine.router 是一个路由器,类型为 map。key 记录路由注册时的 pattern,value 则为相应的处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// HandlerFunc handler 函数类型
type HandlerFunc func(w http.ResponseWriter, r *http.Request)

type Engine struct {
// 路由器
router map[string]HandlerFunc
}

// New 返回一个 Engine 指针
func New() *Engine {
return &Engine{
router: make(map[string]HandlerFunc),
}
}

静态路由注册

我们需要实现通过 POST 和 GET 注册静态路由,首先编写同一的路由注册入口,将路由注册到 Engine.router 中:

1
2
3
4
5
// AddRouter 实现路由注册功能
func (e *Engine) AddRouter(method, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
e.router[key] = handler
}

添加对外暴露的与 HTTP Method 相关的路由注册方法,共实现了 GET 和 POST 方法:

1
2
3
4
5
6
7
8
9
// Get 路由注册 GET 请求方式
func (e *Engine) Get(pattern string, handler HandlerFunc) {
e.AddRouter("GET", pattern, handler)
}

// Post 路由注册 POST 请求方式
func (e *Engine) Post(pattern string, handler HandlerFunc) {
e.AddRouter("POST", pattern, handler)
}

实现 http.Handler 接口

需要使得自定义的路由器工作,首先需要实现 http.Handler 接口,接口定义如下:

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

所以只需要实现 ServeHTTP 方法即可:

1
2
3
4
5
6
7
8
9
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
key := r.Method + "-" + r.URL.Path
if handler, ok := e.router[key]; ok {
handler(w, r)
} else {
fmt.Fprintf(w, "404 NOT FOUND FOR PATH: %v", r.URL.Path)
}
}

最后,包装一层执行函数即可:

1
2
3
4
// Run 运行服务器
func (e *Engine) Run(addr string) error {
return http.ListenAndServe(addr, e)
}

Go 语言中可以使用第三方日志系统:logrus 和 seelog,它们实现了很强大的日志功能。

logrus

介绍

logrus 是用 Go 语言实现的一个日志系统,安装 logrus:

1
go get -u github.com/sirupsen/logrus

六个日志等级(从低到高):

  • Debug
  • Info
  • Warn
  • Error
  • Fatal,随后触发 os.Exit(1)
  • Panic,随后触发 panic()

使用

设置 logrus 参数

可以设置 logrus 的参数:

1
2
3
4
5
6
7
8
9
10
func init() {
// 日志格式化为 JSON,而不是默认的 ASCII
logrus.SetFormatter(&logrus.JSONFormatter{})

// 输出 stdout 而不是默认的 stderr,也可以是一个文件
logrus.SetOutput(os.Stdout)

// 只记录 Warn 以及以上的错误等级
logrus.SetLevel(logrus.WarnLevel)
}

Feilds

可以添加 Fields 来自定义输出:

1
2
3
4
5
6
7
logrus.WithFields(logrus.Fields{
"name": "zh",
"age": 23,
}).Warn("This is a warn level log.")

// output:
// {"age":23,"level":"warning","msg":"This is a warn level log.","name":"zh","time":"2022-05-20T13:11:48+08:00"}

有时候我们需要固定的 Fields,只需要生成一个 log.Entry 就可以复用 Fields

1
2
3
4
5
6
7
// 生成 log.Entry 复用 Feilds
contextLogger := logrus.WithFields(logrus.Fields{
"common": "this is a common field",
})

contextLogger.Info("I'll be logged with common and other field")
contextLogger.Info("Me too")

Logger

如果想在一个应用里面向多个地方记录日志,可以创建 Logger实例,Logger 结构如下:

1
2
3
4
5
6
7
8
9
10
11
type Logger struct {
Out io.Writer
Hooks LevelHooks
Formatter Formatter
//最小级别
Level Level
//被用来同步写入,比如两个地方同时log.默认是被锁住的
mu MutexWrap
// Reusable empty entry
entryPool sync.Pool
}

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"os"
"github.com/sirupsen/logrus"
)

// 你可以创建很多instance
var log = logrus.New()

func main() {
file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
if err == nil {
log.Out = file
} else {
log.Info("Failed to log to file, using default stderr")
}
log.WithFields(logrus.Fields{
"filename": "123.txt",
}).Info("打开文件失败")
}

seelog

介绍

seelog 主要有以下特性:

  • XML 的动态配置,支持动态改变配置而不需要重新启动应用。
  • 支持多输出流,能够同时把日志输出到多种流中、例如文件流、网络流等。
  • 日志级别:trace、debug、info、warn、error、critical、off

安装 seelog:

1
go get -u github.com/cihub/seelog

使用

基本使用

1
2
3
4
5
6
7
8
9
package main
import (
log "github.com/cihub/seelog"
)

func main() {
defer log.Flush()
log.Info("hello world")
}

配置文件说明

seelog 的配置文件通过 XML 进行配置,其说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 
1. type: 日志输出类型,有4中类型,分别是:sync,asyncloop(default),asynctimer,adaptive
type=“sync”:在同一个go中处理日志消息,仅当调用日志函数时才被执行。
type=“asyncloop”:在单独的go中独自处理日志消息,循环从日志队列中读取日志并消费(输出到控制台或者文件)。
type="asynctimer":在单独的go中独自处理日志消息,在指定时间间隔去读取日志队列消息,所以该类型还需要配置一个间隔时间(纳秒)。
type="adaptive":在单独的go中独自处理日志消息,但是不是固定的每隔指定时间去读取日志消息,间隔时间与队列剩余的日志量有关,如果剩余日志量多,则间隔时间短,反之亦然
2. minlevel: 全局最低输出日志级别
3. maxlevel: 全局最高输出日志级别
4. exceptions: 日志的特殊处理情况,可根据指定文件或者函数进行日志输出
5. formatid: 输出格式标签,可以在formats中找到对应的标签
6. console: 将日志输出到控制台
7. splitter: 用于细分outputs日志格式,支持: file(文件), rollingfile(滚动文件), buffered(缓存到内存再输出到文件), smtp(发送日志邮件), con(网络转发)
8. rollingfile: 滚动文件,可基于日期(type="date")或者文件大小(type="size")进行日志切割,maxsize: 单个日志文件最大size,如果设置为100M,则maxsize=100*1024*1024,maxrolls: 最大文件数量,超出的日志文件数量会被滚动删除
9. buffered: 将日志先存在内存中,定期写入文件,适合日志并发量较大或 IO 比较紧张的场合,size:缓存大小, flushperiod:缓存时间
10. filter: 单独处理某级别的日志
11. formats: 日志输出格式
-->

自定义配置文件

seelog 支持自定义日志处理,可以通过配置文件进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package logs

import (
"fmt"

seelog "github.com/cihub/seelog"
)

var Logger seelog.LoggerInterface

func loadAppConfig() {
appConfig := `
<seelog minlevel="warn">
<outputs formatid="common">
<rollingfile type="size" filename="/data/logs/roll.log" maxsize="100000" maxrolls="5"/>
<filter levels="critical">
<file path="/data/logs/critical.log" formatid="critical"/>
<smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
<recipient address="xiemengjun@gmail.com"/>
</smtp>
</filter>
</outputs>
<formats>
<format id="common" format="%Date/%Time [%LEV] %Msg%n" />
<format id="critical" format="%File %FullPath %Func %Msg%n" />
<format id="criticalemail" format="Critical error on our server!\n %Time %Date %RelFile %Func %Msg \nSent by Seelog"/>
</formats>
</seelog>
`
logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig))
if err != nil {
fmt.Println(err)
return
}
UseLogger(logger)
}

func init() {
DisableLog()
loadAppConfig()
}

// DisableLog disables all library log output
func DisableLog() {
Logger = seelog.Disabled
}

// UseLogger uses a specified seelog.LoggerInterface to output library log.
// Use this func if you are using Seelog logging system in your app.
func UseLogger(newLogger seelog.LoggerInterface) {
Logger = newLogger
}

上面实现了三个函数:

  • DisableLog 函数:初始化全局变量 Logger 为 seelog 的禁用状态,主要为了防止 Logger 被多次初始化
  • loadAppConfig 函数:根据配置文件初始化 seelog 的配置信息,配置文件说明如下:
    • seelog:minlevel 参数可选,如果被配置,高于或等于此级别的日志会被记录,同理 maxlevel。
    • outputs:输出信息的目的地,这里分成了两份数据,一份记录到 log rotate 文件里面。另一份设置了 filter,如果这个错误级别是 critical,那么将发送报警邮件。
    • formats:定义了日志的格式。
  • UseLogger 函数:设置当前的日志器为相应的日志处理。

错误处理

在 Go 语言中,定义了 error 类型,来显示的表达错误,错误作为函数的返回值进行返回。

Error 类型

error 类型是一个接口类型:

1
2
3
type error interface {
Error() string
}

可以通过 errors.New 把一个字符串转化为 errorString,以得到一个 error 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}

自定义 Error

error 因为是一个接口,所以可以通过实现 error 接口,自定义错误类型。如 json.SyntaxError 类型,除了错误描述外,还定义了错误发生的位置:

1
2
3
4
5
6
type SyntaxError struct {
msg string // 错误描述
Offset int64 // 错误发生的位置
}

func (e *SyntaxError) Error() string { return e.msg }

再如 net.Error 类型,定义了更复杂的错误处理,判断是否超时、或者临时性错误:

1
2
3
4
5
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}

可以通过类型断言,获取自定义的错误类型:

1
2
3
serr, ok := err.(*json.SyntaxError)

nerr, ok := err.(net.Error)

异常

Go 语言中,将异常与错误区分开来:

  • 错误指可以预期的错误,这时可以将错误当作函数返回值返回。
  • 异常指无法预期、无法继续执行的严重程序错误,如数组地址越界。

异常恢复机制

Go 语言中可以使用 recover 机制来恢复异常:

1
2
3
4
5
6
defer func(){
if x := recover(); x != nil {
// 如果发生了异常
// ....
}
}()

测试

编写测试用例

测试文件必须遵循如下原则:

  • 文件名必须是 _test.go 结尾的。
  • 必须 import testing
  • 所有的测试函数的格式为 func TestXxx (t *testing.T)
  • 通过调用 testing.TError, Errorf, FailNow, Fatal, FatalIf 方法,说明测试不通过,调用 Log 方法用来记录测试的信息。

如编写 go 文件 gotest.go

1
2
3
4
5
6
7
8
9
10
package gotest

import "errors"

func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("被除数不为0")
}
return a / b, nil
}

编写相应的测试文件 gotest_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package gotest

import "testing"

func TestDivide_1(t *testing.T) {
if i, e := Divide(6, 2); i != 3 || e != nil {
t.Error("除法函数测试没通过")
}
t.Log("第一个测试通过")
}

func TestDivide_2(t *testing.T) {
if _, e := Divide(6, 0); e == nil {
t.Error("除数为0测试未通过")
}
t.Log("第二个测试通过")
}

执行测试文件可以得到以下结果:

1
2
3
4
5
6
7
=== RUN   TestDivide_1
gotest_test.go:9: 第一个测试通过
--- PASS: TestDivide_1 (0.00s)
=== RUN TestDivide_2
gotest_test.go:16: 第二个测试通过
--- PASS: TestDivide_2 (0.00s)
PASS

压力测试

压力测试用来测试函数的性能,需要注意:

  • 压力测试函数的格式为 func BenchmarkXXX(b *testing.B)
  • go test 不会默认执行压力测试的函数,如果要执行压力测试需要带上参数 -test.bench,语法: -test.bench="test_name_regex"

对上述编写的除法函数进行压力测试:

1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkDivide(b *testing.B) {
b.StopTimer() // 调用该函数停止压力测试的时间计数

// 做一些初始化的工作,例如读取文件数据,数据库连接之类的,
// 这样这些时间不影响我们测试函数本身的性能

b.StartTimer() // 重新开始时间

for i := 0; i < b.N; i++ {
Divide(10, 3)
}
}

可以通过调用 b.StopTimer()b.StartTimer() 暂停和开始时间计数。执行结果如下:

1
2
3
4
5
6
7
goos: windows
goarch: amd64
pkg: go-web-demo/ch11/gotest
cpu: Intel(R) Pentium(R) CPU G4560 @ 3.50GHz
BenchmarkDivide
BenchmarkDivide-4 1000000000 0.4220 ns/op
PASS

CSRF

CSRF 原理

CSRF(Cross-Site Request Forgery)跨站请求伪造,可以伪装为受害者的身份,向服务器发送各种请求。原理如下:

  • 受害者登录受信任网站 A,并在本地生成 Cookie
  • 在不退出 A 的情况下,访问危险网站 B。此时访问 B 时会发送请求给受信任网站 A 并且会附上 Cookie 信息。

img

CSRF 防御

在服务器端防御 CSRF,主要有两个方面:

  • 正确使用 GET、POST 请求。
  • 在非 GET 请求中增验证。

正确使用 GET POST 请求

使用 REST 方式可以限制请求的类型:

  • GET 常用在查看,列举,展示等不需要改变资源属性的时候。

  • POST 常用在改变一个资源的属性或者状态。

增加验证

在非 GET 请求中增加验证,可以有三个思路:

  • 为每个用户生成一个唯一的 token,所有表单都包含同一个伪随机值。这种方法最简单,因为攻击者(理论上)不能获取到第三方的 Cookie,所以表单中的数据也就构造失败,但是 XSS 可以窃取到第三方 Cookie,所以这个方案在没有 XSS 时是安全的。

  • 为每一个请求使用验证码,用户体验很差。

  • 每个用户生成的 token 随时更新,实现如下:

    • 生成随机 token:
    1
    2
    3
    4
    5
    6
    7
    h := md5.New()
    io.WriteString(h, strconv.FormatInt(crutime, 10))
    io.WriteString(h, "salt")
    token := fmt.Sprintf("%x", h.Sum(nil))

    t, _ := template.ParseFiles("xxx.gtpl")
    t.Execute(w, token)
    • 表单中的 token:
    1
    <input type="hidden" name="token" value="{{.}}">
    • 验证 token:
    1
    2
    3
    4
    5
    6
    7
    r.ParseForm()
    token := r.Form.Get("token")
    if token != "" {
    // 验证 token 的合法性
    } else {
    // 不存在 token 报错
    }

XSS

XSS 原理

XSS(Cross-Site Scripting)跨站脚本攻击,原理是一段恶意的 JavaScript 代码在用户客户端上被执行,导致信息泄露( Cookie 泄露)。XSS 主要用于攻击用户端的。

主要分为两类:

  • 存储型 XSS:恶意 XSS 代码被服务器存储到了服务器中,应用程序从数据库中查询出来并在客户端中显示,造成 XSS 攻击。
  • 反射型 XSS:将恶意 XSS 代码加入到 URL 的请求参数中,请求参数在页面上直接输出。

XSS 防御

XSS防御可以有如下方式:

  • 过滤特殊字符:text/template 包下面的 HTMLEscapeString、JSEscapeString 等函数可以对敏感字符进行转义。
  • 输入内容长度控制:对于不受信任的输入,都应该限定一个合理的长度,这样可以增加攻击难度。
  • HTTP-Only:禁止从客户端脚本中读取 Cookie 信息,使得攻击者无法窃取 Cookie。
  • 等一系列防御手段。。。

SQL 注入

SQL 注入原理

SQL 注入的原理就是因为用户输入的数据被当作 SQL 语句执行。

SQL 注入防御

SQL 注入可以有如下防御方式:

  • 限制 Web 应用数据库的操作权限,给予用户最低的操作权限。

  • 检查输入的数据,对进入数据库的字符进行转义、过滤。html/template 包的 HTMLEscapeString 函数可以对字符串进行转义处理。

  • 所有的查询语句建议使用数据库提供的参数化查询接口,避免直接拼接 SQL 语句。

存储密码

普通方案:哈希

目前利用最多的方案就是对明文密码进行哈希之后,进行存储。常用的单向哈希算法包括 SHA-256, SHA-1, MD5 等。

缺点:考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合(彩虹表),然后与数据库中的摘要进行比对即可获得对应的密码。

进阶方案:哈希+盐

可以采用加盐的方式来存储密码,常用的方式:

  • 对用户的明文密码进行一次哈希运算
  • 将得到的摘要加上随机串(盐),这个随机串中可以包括某些固定的串,也可以包括用户名(用来保证每个用户加密使用的密钥都不一样)。
  • 再进行一次哈希运算后,放入数据库中存储起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 假设用户名 abc,密码 123456
h := md5.New()
io.WriteString(h, "需要加密的密码")

// pwmd5 等于 e10adc3949ba59abbe56e057f20f883e
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))

// 指定两个 salt: salt1 = @#$% salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"

// salt1 + 用户名 + salt2 + MD5 拼接
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)

last :=fmt.Sprintf("%x", h.Sum(nil))

专家方案:Scrypt

故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的 rainbow table

Scrypt 算法使得并行计算多个摘要异常困难,因此利用rainbow table(彩虹表)进行暴力攻击的难度增加。

在 Go 的 golang.org/x/crypto/scrypt 包中支持 scrypt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"encoding/base64"
"fmt"
"log"

"golang.org/x/crypto/scrypt"
)

func main() {
// DO NOT use this salt value; generate your own random salt. 8 bytes is
// a good length.
salt := []byte{0xc8, 0x28, 0xf2, 0x58, 0xa7, 0x6a, 0xad, 0x7b}

dk, err := scrypt.Key([]byte("some password"), salt, 1<<15, 8, 1, 32)
if err != nil {
log.Fatal(err)
}
fmt.Println(base64.StdEncoding.EncodeToString(dk))
}

加密和解密数据

Go 语言中 crypto 及其子包提供了多种加密算法。

Socket 编程

常用的 Socket 有两种类型:

  • 流式 Socket(SOCK_STREAM):一种面向连接的 Socket,针对于面向连接的 TCP 服务应用。

  • 数据包式 Socket (SOCK_DGRAM)无连接的 Socket,对应于无连接的 UDP 服务应用。

对于在网络上的应用程序来说,(协议类型,IP 地址,端口号)这个三元组可以唯一确定一个进程。

在 Go 的 net 包中,定义了 IP 地址类型,net 包的函数都可以接收 IPv4 和 IPv6 的 IP 地址作为输入。其中 ParseIP(s string) IP 函数会把一个 IPv4 或者 IPv6 的地址转化成 IP 类型。

1
2
3
type IP []byte

func ParseIP(s string) IP

TCP Socket

在 net 包中,有一个类型为 TCPConn,它用来作为客户端和服务器端交互的通道,主要有两个函数,分别可以读写数据:

1
2
func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Read(b []byte) (int, error)

另外,还有一个 TCPAddr 类型,用于表示 TCP 的地址信息,通过 ResolveTCPAddr 函数可以获取一个 TCPAddr:

1
2
3
4
5
6
7
type TCPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net 参数是 “tcp4”、”tcp6”或”tcp” 中的任意一个,分别表示 TCP (IPv4-only), TCP (IPv6-only) 或者 TCP (IPv4, IPv6 的任意一个)。
  • addr 表示域名或者 IP 地址

TCP client

客户端通过 net 包中的 DialTCP 函数来建立一个 TCP 连接,并返回一个 TCPConn 类型的对象:

1
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • net 必须是**”tcp”、”tcp4”、”tcp6”**。
  • laddr 为本地地址,如果 laddr 不是 nil,将使用它作为本地地址,否则自动选择一个本地地址。通常为 nil
  • raddr 为远程的服务器地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 服务器远程地址
serviceAddr = "127.0.0.1:9617"
raddr, err := net.ResolveTCPAddr("tcp", serviceAddr)
if err != nil {
// ...
}
// 与服务器建立连接
conn, err := net.DialTCP("tcp", nil, raddr)
if err != nil {
// ...
}
// 向服务器端发送数据
_, err = conn.Write([]byte("hello world"))
if err != nil {
// ...
}
// 读取数据
result, err := ioutil.ReadAll(conn)
if err != nil {
// ...
}

TCP server

服务器端需要:

  • 绑定服务到指定的非激活端口,并监听此端口。
1
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
  • 当有客户端请求到达的时候可以接收到来自客户端连接的请求。
1
func (l *TCPListener) Accept() (Conn, error)

当 Accept 之后,为每一个 conn 开启一个 goroutine 来处理与客户端的通信,这样可以支持多并发:

1
2
3
4
5
6
7
8
for {
conn, err := listener.Accept()
if err != nil {
continue
}
// 开启协程,支持多并发
go handleClient(conn)
}d

控制 TCP 连接

TCP 有很多连接控制函数,常用如下:

  • 设置建立连接的超时时间,当超过设置时间时,连接自动关闭。
1
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
  • 设置 写入 / 读取 一个连接的超时时间
1
2
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
  • 设置 keepAlive 属性,操作系统在 TCP 上没有数据和 ACK 时,会间隔性的发送 keepalive 包,以此判断这个 TCP 连接是否已经断开。
1
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

UDP Socket

UDP Socket 在服务器端没有 Accept 函数,其他几乎一样。

Web Socket

在 WebSocket 出现之前,为了实现即时通信,采用的技术都是 “”轮询”,即在特定的时间间隔内,由浏览器对服务器发出 HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询” 使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。

Web Socket 采用了特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。URI 以 ws 或者 wss(SSL) 开头。

原理

Web Socket 协议本质上是一个基于 TCP 的协议。Web Socket 的大致流程如下:

  • 握手过程:为了建立一个 Web Socket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息 Upgrade: WebSocket 表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了。

  • 数据传输:双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

Web Socket在第一次握手之后,连接便建立成功,其后的通讯数据都是以 \x00 开头,以 \xFF 结尾。在上层应用中,这是透明的,Web Socket 组件会自动的去掉头部和尾部。

实现

Go 语言官方标准库中没有对于 Web Socket 的支持,但是可以通过以下命令获取:

1
go get golang.org/x/net/websocket

客户端

客户端示例如下,并且客户端绑定了4个事件:

  • onopen 建立连接后触发。
  • onmessage 收到消息后触发。
  • onerror 发生错误时触发。
  • onclose 关闭连接时触发。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:9617";

window.onload = function() {

console.log("onload");

sock = new WebSocket(wsuri);

sock.onopen = function() {
console.log("connected to " + wsuri);
}

sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}

sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};

function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
"golang.org/x/net/websocket"
"html/template"
"log"
"net/http"
)

func Index(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("demo.html")
t.Execute(w, nil)
}

func Echo(ws *websocket.Conn) {
var err error

for {
var reply string

if err = websocket.Message.Receive(ws, &reply); err != nil {
fmt.Println("Can't receive")
break
}

fmt.Println("Received back from client: " + reply)

msg := "Received: " + reply
fmt.Println("Sending to client: " + msg)

if err = websocket.Message.Send(ws, msg); err != nil {
fmt.Println("Can't send")
break
}
}
}

func main() {
http.HandleFunc("/", Index)
http.Handle("/ws", websocket.Handler(Echo))
if err := http.ListenAndServe(":9617", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

REST

REST ( REpresentatianal State Transfer ) 表现层状态转化。

基本概念

要理解什么是 REST,需要理解以下几个概念:

  • 资源(Resource):REST 是 表现层状态转化,实际上 表现层指的是 资源的表现层。平常上网访问的一张图片、一个文档、一个视频等就是资源。这些资源我们通过 URI 来定位,也就是一个 URI 表示一个资源。
  • 表现层(Representation):把资源实体展现出来的方式,就是表现层。比如一段文本信息,可以输出为 HTML,JSON,XML 等。URI 确定一个资源,但是如何确定它的具体表现形式呢?应该在 HTTP 请求的头信息中用 AcceptContent-Type 字段指定,这两个字段才是对 “表现层” 的描述。
  • 状态转换(State Transfer):在访问服务器的过程中,服务器与客户端进行交互,这就涉及到了数据和状态的变化。HTTP 中四个方法 GET、POST、PUT、DELETE,分别对应于四种基本操作:GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源。

RESTful 架构

RESTful 架构就是:

  • 每一个 URI 代表一种资源
  • 客户端和服务器之间,传递这种资源的某种表现层
  • 客户端通过四个 HTTP 动词,对服务器端资源进行操作,实现 表现层状态转化

Web 应用要满足 REST 最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。

另一个重要的 REST 原则是:系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。

REST 架构图:

img

RPC

见文章 GO语言杂谈 2 RPC

Web 开发中需要对输入、输出进行处理。

XML 处理

示例 XML 文件如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<servers version="1">
<server>
<serverName>Shanghai_VPN</serverName>
<serverIP>127.0.0.1</serverIP>
</server>
<server>
<serverName>Beijing_VPN</serverName>
<serverIP>127.0.0.2</serverIP>
</server>
</servers>

读取 XML

读取 XML 选择 xml.Unmarshal 函数:

1
func Unmarshal(data []byte, v interface{}) error

将 XML 写入结构体为例,上述 XML 文件 对应的结构体为:

1
2
3
4
5
6
7
8
9
10
11
12
type Servers struct {
XMLName xml.Name `xml:"servers"`
Version string `xml:"version,attr"`
Svs []Server `xml:"server"`
Description string `xml:",innerxml"`
}

type Server struct {
XMLName xml.Name `xml:"server"`
ServerName string `xml:"serverName"`
ServerIP string `xml:"serverIP"`
}

读取操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main() {
// 打开 XML 文件
file, err := os.Open("servers.xml")
if err != nil {
fmt.Printf("error: %v", err)
return
}
defer file.Close()
// 读取文件内容
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Printf("error: %v", err)
return
}
// 将 XML 转为结构体
v := Servers{}
err = xml.Unmarshal(data, &v)
if err != nil {
fmt.Printf("error: %v", err)
return
}
fmt.Println(v)
}
// 输出:
// {{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
// <server>
// <serverName>Shanghai_VPN</serverName>
// <serverIP>127.0.0.1</serverIP>
// </server>
// <server>
// <serverName>Beijing_VPN</serverName>
// <serverIP>127.0.0.2</serverIP>
// </server>
}

输出 XML

xml 包提供了两个函数 Marshal 和 MarshalIndent 来输出 XML,二者的区别在于 MarshalIndent 会增加前缀和缩进:

1
2
func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

构建结构体:

1
2
3
4
5
6
7
8
9
10
11
12
type Servers struct {
XMLName xml.Name `xml:"servers"`
Version string `xml:"version,attr"`
Svs []Server `xml:"server"`
Description string `xml:",innerxml"`
}

type Server struct {
XMLName xml.Name `xml:"server"`
ServerName string `xml:"serverName"`
ServerIP string `xml:"serverIP"`
}

解析结构体并输出 XML :

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 新建结构体
v := Servers{Version: "1"}
v.Svs = append(v.Svs, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
v.Svs = append(v.Svs, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
// 转为 XML 数据
data, err := xml.MarshalIndent(&v, "", " ")
if err != nil {
fmt.Printf("error: %v\n", err)
}
os.Stdout.Write([]byte(xml.Header))
os.Stdout.Write(data)
}

注意:

之所以会有 os.Stdout.Write([]byte(xml.Header)) 这句代码的出现,是因为 xml.MarshalIndent 或者 xml.Marshal 输出的信息都是不带 XML 头的,为了生成正确的 xml 文件,我们使用了 xml 包预定义的 Header 变量。

JSON 处理

示例 JSON 格式数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"servers": [
{
"serverName": "Shanghai_VPN",
"serverIP": "127.0.0.1"
},
{
"serverName": "Beijing_VPN",
"serverIP": "127.0.0.2"
}
]
}

解析 JSON

json 包中有如下函数可以解析 JSON 数据:

1
func Unmarshal(data []byte, v interface{}) error

解析到结构体

上述 JSON 数据对应的结构体如下:

1
2
3
4
5
6
7
8
type Server struct {
ServerName string `json:"serverName"`
ServerIP string `json:"serverIP"`
}

type ServerSlice struct {
Servers []Server `json:"servers"`
}

解析 JSON 数据到结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// 打开文件
file, err := os.Open("servers.json")
if err != nil {
fmt.Errorf("open file err: %v", err)
return
}
// 读取内容
data, _ := ioutil.ReadAll(file)
// 解析 JSON
v := ServerSlice{}
err = json.Unmarshal(data, &v)
if err != nil {
fmt.Errorf("open file err: %v", err)
return
}
fmt.Println(v)
}

解析到 interface

如果知道 JSON 数据的格式,可以解析到结构体中。如果不知道 JSON 数据格式,可以利用 map [string] interface {}[] interface {} 结构来存储任意的 JSON 对象。

1
2
var f interface{}
err := json.Unmarshal(b, &f)

此时,空接口 f 实际上是一个 map[string] interface{},其结构如下:

1
2
3
4
5
6
7
8
9
10
11
map[string]interface {}{
"servers":[]interface {}{
map[string]interface {}{
"serverIP":"127.0.0.1",
"serverName":"Shanghai_VPN"},
map[string]interface {}{
"serverIP":"127.0.0.2",
"serverName":"Beijing_VPN"
}
}
}

可以通过类型断言将**空接口转为 map[string] interface{}**,接着就可以利用 for range 对 map 进行遍历了:

1
m := f.(map[string]interface{})

目前,simplejson 包可以更加容易的处理未知结构的 JSON 数据,其 github 地址如下:

github.com/bitly/go-simplejson


生成 JSON

json 包提供了生成 JSON 数据的函数:

1
func Marshal(v interface{}) ([]byte, error)

这里不再举例说明具体操作。

模板处理

使用模板

解析模板

html/template 包中有两个函数可以解析模板:

  • ParseFiles 函数创建一个模板并解析filenames指定的文件里的模板定义。返回的模板的名字是第一个文件的文件名(不含扩展名),内容为解析后的第一个文件的内容。
1
func ParseFiles(filenames ...string) (*Template, error)
  • ParseGlob 函数创建一个模板并解析匹配 pattern 的文件里的模板定义。返回的模板的名字是第一个匹配的文件的文件名(不含扩展名),内容为解析后的第一个文件的内容。
1
func ParseGlob(pattern string) (*Template, error)

模板执行

html/template 包中有两个函数可以执行模板:

  • Execute 方法将解析好的模板应用到 data 上,并将输出写入 wr。
1
func (t *Template) Execute(wr io.Writer, data interface{}) error
  • ExecuteTemplate 方法类似 Execute,但是使用名为 nam e的 t 关联的模板产生输出。
1
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

模板中插入数据

  • 字段操作{{.}} 代表当前对象,可以通过 {{.FieldName}} 访问当前对象的字段

  • 输出嵌套字段

    • {{with …}}…{{end}} 可以指定当前对象的值,如子结构体。
    • {{range …}}{{end}} 可以循环操作数据。
  • 条件判断{{if …}}…{{else if …}}…{{else}}…{{end}} 语句可以进行条件判断。

  • pipelines:在 {{}} 中的都是 pipeline,如 {{. | html}} 可以将当前对象进行 HTML 转义,变为 HTML 实体。

  • 模板变量:可以通过 $variable := pipeline 方式声明模板局部变量。

  • 模板嵌套

    • 声明:{{define "子模板名称"}}内容{{end}}
    • 调用:{{template "子模板名称"}}
  • 模板函数

    • 每一个模板函数都有一个唯一的名字,可以与一个 Go 函数相关联。FuncMap 类型定义了函数名字符串到函数的映射,每个函数都必须有 1 到 2 个返回值,如果有 2 个则后一个必须是 error 接口类型;如果有 2 个返回值的方法返回的 error 非 nil ,模板执行会中断并返回给调用者该错误。:
    1
    type FuncMap map[string]interface{}
    • 使用 t.Funcs 函数在模板中注册函数:
    1
    t = t.Funcs(template.FuncMap{"TmplFuncName": FuncName})
    • 在模板包内部已经有内置的实现函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var builtins = FuncMap{
    "and": and,
    "call": call,
    "html": HTMLEscaper,
    "index": index,
    "js": JSEscaper,
    "len": length,
    "not": not,
    "or": or,
    "print": fmt.Sprint,
    "printf": fmt.Sprintf,
    "println": fmt.Sprintln,
    "urlquery": URLQueryEscaper,
    }

Must 操作

Must 函数用来检查模板是否正确:

1
func Must(t *Template, err error) *Template

一般用于变量初始化:

1
var t = template.Must(template.New("name").Parse("html"))

Session 和 Cookie

Cookie 是由浏览器维持的,保存在客户端的一段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。通过 Cookie,服务器就可以验证 Cookie 信息、记录用户状态。

img

Cookie 是有时间限制的,根据生命周期不同分为会话 Cookie 和持久 Cookie:

  • 会话 Cookie:不设置生命周期的默认值,表示这个 Cookie 的生命周期到浏览器关闭为止,会话 Cookie 一般保存在内存中。
  • 持久 Cookie:设置了过期时间时,浏览器就会将 Cookie 保存在硬盘上,有效期直到超过了过期时间。

Go 语言中,通过 net/http 包中的 SetCookie 来设置:

1
func SetCookie(w ResponseWriter, cookie *Cookie)

Cookie 结构体结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
// MaxAge=0表示未设置Max-Age属性
// MaxAge<0表示立刻删除该cookie,等价于"Max-Age: 0"
// MaxAge>0表示存在Max-Age属性,单位是秒
MaxAge int
Secure bool
HttpOnly bool
Raw string
Unparsed []string // 未解析的“属性-值”对的原始文本
}

设置 Cookie 例子:

1
2
3
4
5
6
7
8
9
10
11
// 设置过期时间
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
// 实例化 Cookie对象
cookie := &http.Cookie{
Name: "username",
Value: "dawn",
Expires: expiration,
}
// 写入 HTTP 响应
http.SetCookie(w, cookie)

Go 语言中,使用 r.Cookie 根据 name 读取 Cookie:

1
func (r *Request) Cookie(name string) (*Cookie, error)

还可以通过 r.Cookies 一次性得到所有的 Cookie 信息:

1
func (r *Request) Cookies() []*Cookie

Session

Session 是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。服务器使用 Session id 来标识 Session 信息,它由服务器端产生,相当于一个密钥。可以借助 Cookie 或者 GET 请求方式传输 Session id。

img


总结

  • Session 和 Cookie 机制都是为了克服 HTTP 协议的无状态缺陷,用于记录用户状态。
  • Session 依托 Cookie 实现,将 Session id 保存在 Cookie 中。
  • Cookie 因为将所有信息都保存在客户端中,发起请求时会携带这些信息,所以有一定的安全隐患。

Go 实现 Session

Session 创建过程

服务器端在创建 Session 时会分为三个步骤:

  • 生成一个全局唯一的标识符 Session id
  • 开辟数据存储空间来存储 Session 信息:
    • 内存:速度快,系统一旦掉电,所有的会话数据就会丢失。
    • 文件或者数据库:增加 I/O 开销,但是可以实现某种程度上的 Session 持久化,以及 Session 的共享。
  • 将 Session id 发送给客户端
    • Cookie:服务端通过设置 Set-cookie 头就可以将 Session id 发送给客户端,而客户端此后的每一次请求都会带上这个 Session id。一般情况下,会将包含 Session id 的 Cookie 的过期时间设置为 0。
    • URL 重写:返回给用户的页面里的所有的 URL 后面追加 Session id,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上 Session id。如果客户端禁用了 Cookie,这种方案就是首选。

Go 实现 Session 管理

目前 Go 标准包没有为 Session 提供任何支持,下面手动实现 Session 的管理和创建。

Session 管理器

定义一个全局的 Session 管理器,用于 Session 的管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Manager struct {
cookieName string
lock sync.Mutex
provider Provider
maxLifeTime int64
}

func NewManager(provideName, cookieName string, maxLifeTime int64) (*Manager, error) {
provider, ok := provides[provideName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
}

return &Manager{
cookieName: cookieName,
provider: provider,
maxLifeTime: maxLifeTime,
}, nil
}

Session 是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个 Provider 接口,用以表征 Session 管理器底层存储结构

  • SessionInit 实现 Session 的初始化。
  • SessionRead 返回 sid 所代表的 Session 变量,如果不存在,那么将以 sid 为参数调用 SessionInit 函数创建并返回一个新的 Session 变量。
  • SessionDestroy 销毁 sid 对应的 Session 变量。
  • SessionGC 根据 maxLifeTime 来删除过期的数据。
1
2
3
4
5
6
7
8
9
10
type Provider interface {
// SessionInit 实现 Session 的初始化
SessionInit(sid string) (Session, error)
// SessionRead 返回 sid 所代表的 Session 变量,如果不存在,那么将以 sid 为参数调用 SessionInit 函数创建并返回一个新的 Session 变量
SessionRead(sid string) (Session, error)
// SessionDestroy 销毁 sid 对应的 Session 变量
SessionDestroy(sid string) error
// SessionGC 根据 maxLifeTime 来删除过期的数据
SessionGC(maxLifeTime int64)
}

对 Session 的处理基本上就是设置值、读取值、删除值、获取当前 Session id 四种操作,所以 Session 接口定义如下:

1
2
3
4
5
6
type Session interface {
Set(key, value interface{}) error // set session value
Get(key interface{}) interface{} // get session value
Delete(key interface{}) error // delete session value
SessionID() string // back current sessionID
}

全局唯一的 Session id

Session id 用来标识每一个用户以及对应的 Session,所以必须是全局唯一的:

1
2
3
4
5
6
7
8
9
// 创建全局唯一的 session id
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return ""
}

return base64.URLEncoding.EncodeToString(b)
}

Session 的创建

SessionStart 用来检测是否已经有某个 Session 与当前来访用户发生了关联,如果没有则创建之

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SessionStart 用来检测是否已经有某个 Session 与当前来访用户发生了关联,如果没有则创建之
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
manager.lock.Lock()
defer manager.lock.Unlock()
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
// 没有 Session,创建
sid := manager.sessionId()
session, _ = manager.provider.SessionInit(sid)
cookie := &http.Cookie{
Name: manager.cookieName,
Value: url.QueryEscape(sid),
Path: "/",
HttpOnly: true,
MaxAge: int(manager.maxLifeTime),
}
http.SetCookie(w, cookie)
} else {
// 发生了关联
sid, _ := url.QueryUnescape(cookie.Value)
session, _ = manager.provider.SessionRead(sid)
}
return
}

Session 销毁

在销毁 Session 时,除了从存储结构中删除 Session 信息之外,还需要设置 Cookie过期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
}
// 上锁
manager.lock.Lock()
defer manager.lock.Unlock()
// 从存储结构中删除
manager.provider.SessionDestroy(cookie.Value)
// 设置 Cookie
expiration := time.Now()
cookie = &http.Cookie{
Name: manager.cookieName,
Path: "/",
HttpOnly: true,
Expires: expiration,
MaxAge: -1, // 立即删除session
}
http.SetCookie(w, cookie)
}

过期 Session 清理

GC 利用了 time 包的定时器功能,当超时之后调用 GC 清理过期数据。

1
2
3
4
5
6
7
8
9
// GC 删除过期数据
func (manager *Manager) GC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxLifeTime)
time.AfterFunc(time.Duration(manager.maxLifeTime), func() {
manager.GC()
})
}

Go 实现 Session 存储

在上一节中,定义了 Session 管理器 session.Manager,以及两个接口 session.Providersession.Session,这两个接口用于存储 Session 数据。本节将实现一个基于内存的 Session 存储方式。

Provider 接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package memory

import (
"container/list"
"go-web-demo/ch6/session"
"sync"
"time"
)

type Provider struct {
lock sync.Mutex // 用来锁
sessions map[string]*list.Element // 用来存储在内存
list *list.List // 用来做 gc,按照 SessionStore.timeAccessed 最近访问时间排序
}

// Provider 实现 session.Session 接口

// SessionInit 实现 Session 的初始化
func (p *Provider) SessionInit(sid string) (session.Session, error) {
p.lock.Lock()
defer p.lock.Unlock()
v := make(map[interface{}]interface{}, 0)
newSess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
element := p.list.PushFront(newSess)
p.sessions[sid] = element
return newSess, nil
}

// SessionRead 返回 sid 所代表的 Session 变量,如果不存在,那么将以 sid 为参数调用 SessionInit 函数创建并返回一个新的 Session 变量
func (p *Provider) SessionRead(sid string) (session.Session, error) {
if element, ok := p.sessions[sid]; ok {
// 已存在 Session
return element.Value.(*SessionStore), nil
}
// 未存在,新建Session
sess, err := p.SessionInit(sid)
return sess, err
}

// SessionDestroy 销毁 sid 对应的 Session 变量
func (p *Provider) SessionDestroy(sid string) error {
if element, ok := p.sessions[sid]; ok {
// Session 已存在
// 从字典中删除
delete(p.sessions, sid)
// 从链表中删除
p.list.Remove(element)
return nil
}

return nil
}

// SessionGC 根据 maxLifeTime 来删除过期的数据
func (p *Provider) SessionGC(maxLifeTime int64) {
p.lock.Lock()
defer p.lock.Unlock()

for {
element := p.list.Back()
if element.Value.(*SessionStore).timeAccessed.Unix()+maxLifeTime < time.Now().Unix() {
// 超时,清理 Session
// 从字典中删除
delete(p.sessions, element.Value.(*SessionStore).sid)
// 从链表中删除
p.list.Remove(element)
}
}
}

// SessionUpdate 用于更新最近访问时间
func (p *Provider) SessionUpdate(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()

if element, ok := p.sessions[sid]; ok {
element.Value.(*SessionStore).timeAccessed = time.Now()
p.list.MoveToFront(element)
return nil
}
return nil
}

Session 接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package memory

import (
"container/list"
"time"
)

type SessionStore struct {
sid string // session id
timeAccessed time.Time // 最后访问的时间
value map[interface{}]interface{} // session 存储的值
}

var (
p = &Provider{list: list.New()}
)

// Set 设置值
func (st *SessionStore) Set(key, value interface{}) error {
st.value[key] = value
return p.SessionUpdate(st.sid)
}

// Get 获取值
func (st *SessionStore) Get(key interface{}) interface{} {
p.SessionUpdate(st.sid)
if v, ok := st.value[key]; ok {
return v
}
return nil
}

// Delete 删除值
func (st *SessionStore) Delete(key interface{}) error {
delete(st.value, key)
return p.SessionUpdate(st.sid)
}

// SessionID 获取 sid
func (st *SessionStore) SessionID() string {
return st.sid
}

初始化以及注册 Provider

初始化以及在 session.Manager注册该基于内存的 Provider。

1
2
3
4
5
6
7
8
9
10
11
package memory

import (
"container/list"
"go-web-demo/ch6/session"
)

func init() {
p.sessions = make(map[string]*list.Element, 0)
session.Register("memory", p)
}

调用

当 import 的时候已经执行了 memory 函数里面的 init 函数,这样就已经注册到 session 管理器中,我们就可以使用了:

1
2
3
4
5
6
7
8
9
10
11
12
import (
"go-web-demo/ch6/session"
_ "go-web-demo/ch6/session/providers/memory"
)

var globalSessions *session.Manager

func init() {
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
// 开一个线程启动 GC
go globalSessions.GC()
}

防范 Session 劫持

httponly 和 token

第一种方案通过 httponly + token 来预防 Session 劫持:

  • 可以设置 Session id 的值只能由 Cookie 进行设置,同时设置 httponly 为 true,这个属性可以防止通过客户端脚本访问到 Cookie,进而防止攻击者读取到 Cookie 中的 Session id。
  • 在每个请求里面加上 token,然后每一次验证 token,从而保证用户的请求都是唯一性。

间隔生成新的 Sesseion id

第二种方案是给 Session 额外设置一个创建时间的值,一旦过了一定的时间,就销毁这个 Session id,重新生成 Session。

1
2
3
4
5
6
7
8
9
10
11
// 设置过期时间 60s
expires := 60

createTime := sess.Get("createtime")
if createTime == nil {
sess.Set("createtime", time.Now().Unix())
} else if createTime.(int64) + expires < time.Now().Unix() {
// 当前 Session 过期,重新分配 Session
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}

这里为 Session 设置了一个值,用于记录生成 Session id 的时间。每次请求都判断是否过期,如果过期,则分配新的 Session id。

总结

上面两个手段的组合可以在实践中消除 session 劫持的风险:

  • Session id 的频繁改变,使得攻击者很难获取到有效的 Session id。

  • Session 只在 Cookie 中传递,并且设置了 httponly 选项,可以有效阻止通过客户端脚本访问到 Cookie,也可以预防 XSS 攻击获取 Cookie。

  • 还可以设置 MaxAge=0,这样 Cookie 就不会存储在浏览器的记录中,随着浏览器关闭 Cookie 也随机消失。

表单

处理表单输入

编写 login.gtpl 文件:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="submit" value="登录">
</form>
</body>
</html>

处理登录的逻辑,其中 r.ParseForm() 方法用于解析参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"html/template"
"log"
"net/http"
)

func login(w http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method) // 获取请求的方法
if r.Method == http.MethodGet {
// 如果是 GET 请求
t, _ := template.ParseFiles("./login.gtpl")
log.Println(t.Execute(w, nil))
} else {
// 如果是 POST 请求
err := r.ParseForm()
if err != nil {
log.Fatal("ParseForm error:", err)
}
// 请求的是登录数据,那么执行登录的逻辑判断
fmt.Println("username:", r.Form["username"])
fmt.Println("password:", r.Form["password"])
}

}

func main() {
http.HandleFunc("/login", login) // 设置访问的路由
err := http.ListenAndServe(":9000", nil) // 设置监听端口
if err != nil {
log.Fatal("ListenAndServe error:", err)
}
}

r.Form 里面包含了所有请求的参数,比如 URL 中 query-stringPOST 的数据PUT 的数据,所以当你在 URL 中的 query-string 字段和 POST 冲突时,会保存成一个 slice,里面存储了多个值。

r.From 是一个 url.Values 类型的值,可以对其进行一些操作( Get、Set、Add、Del、Has ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string

// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns
// the empty string. To access multiple values, use the map
// directly.
func (v Values) Get(key string) string {
if v == nil {
return ""
}
vs := v[key]
if len(vs) == 0 {
return ""
}
return vs[0]
}

// Set sets the key to value. It replaces any existing
// values.
func (v Values) Set(key, value string) {
v[key] = []string{value}
}

// Add adds the value to key. It appends to any existing
// values associated with key.
func (v Values) Add(key, value string) {
v[key] = append(v[key], value)
}

// Del deletes the values associated with key.
func (v Values) Del(key string) {
delete(v, key)
}

// Has checks whether a given key is set.
func (v Values) Has(key string) bool {
_, ok := v[key]
return ok
}

注意:

在处理表单的逻辑事,不要忘了对表单的输入的验证,不能信任用户输入的任何信息。


预防跨站脚本

对 XSS 的防护主要在于两方面:

  • 输入的验证,检测攻击。
  • 对所有输出数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。

对于对输出数据的处理,Go 的 html/template 里面带有下面几个函数可以转义

  • func HTMLEscape (w io.Writer, b [] byte):把 b 进行转义之后写到 w 中。
  • func HTMLEscapeString (s string) string:转义 s 之后返回结果字符串。
  • func HTMLEscaper (args ...interface {}) string:支持多个参数一起转义,返回结果字符串

如果需要输出 HTML 标签,可以使用 template.HTML 类型,它用于封装一个已知安全的 HTML 文档片段。它不应被第三方使用,也不能用于含有未闭合的标签或注释的 HTML 文本。

1
2
3
4
5
6
import "html/template"

...

t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))

防止多次提交表单

解决方案是在表单中添加一个带有唯一值的隐藏字段

1
<input type="hidden" name="token" value="{{.}}">

在验证表单时,先检查带有该唯一值( 如 MD5(时间戳) )的表单是否已经递交过了:

  • 如果是,拒绝再次递交。
  • 如果不是,则处理表单进行逻辑处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func login(w http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method) // 获取请求的方法
if r.Method == "GET" {
crutime := time.Now().Unix()
// 通过 md5(时间戳) 构造token
h := md5.New()
io.WriteString(h, strconv.FormatInt(crutime, 10))
token := fmt.Sprintf("%x", h.Sum(nil))

t, _ := template.ParseFiles("login.gtpl")
t.Execute(w, token)
} else {
// 请求的是登录数据,那么执行登录的逻辑判断
r.ParseForm()
token := r.Form.Get("token")
if token != "" {
// 验证 token 的合法性
} else {
// 不存在 token 报错
}
fmt.Println("username length:", len(r.Form["username"][0]))
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) // 输出到服务器端
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
template.HTMLEscape(w, []byte(r.Form.Get("username"))) // 输出到客户端
}
}

处理文件上传

form 的 enctype 属性

form 表单的 enctype 属性指明了发送到服务器时时浏览器使用的编码类型,有三种取值:

  • application/x-www-form-urlencoded默认编码类型,在发送前编码所有的字符。
  • multipart/form-data :不对字符进行编码,指定传输的数据为二进制类型,所以可以用于文件上传。
  • text/plain :空格转换为 “+” 加号,但不对特殊字符编码。

文件上传

创建名为 upload.gtpl 的模板文件用于文件上传:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="uploadfile" />
<input type="hidden" name="token" value="{{.}}"/>
<input type="submit" value="upload" />
</form>
</body>
</html>

编写服务器文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"crypto/md5"
"fmt"
"html/template"
"io"
"net/http"
"os"
"strconv"
"time"
)

func upload(w http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method) // 获取请求的方法
if r.Method == http.MethodGet {
// 若是 GET 请求
curTime := time.Now().Unix() // 获取当前时间的时间戳
h := md5.New()
_, err := io.WriteString(h, strconv.FormatInt(curTime, 10)) // 将时间戳进行 md5 加密
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
token := fmt.Sprintf("%x", h.Sum(nil))

// 渲染页面
t, _ := template.ParseFiles("upload.gtpl")
t.Execute(w, token)
} else {
// POST 请求
r.ParseMultipartForm(32 << 20)
file, handler, err := r.FormFile("uploadfile")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
fmt.Fprintf(w, "%v", handler.Header)
f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) // 此处假设当前目录下已存在test目录
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
io.Copy(f, file)
}
}

func main() {
http.HandleFunc("/upload", upload)
http.ListenAndServe(":9000", nil)
}

处理文件上传需要调用 r.ParseMultipartForm( maxMemory ) 函数,其中 maxMemroy 表示文件存储在内存中的大小,如果文件大小超过了该值,则剩余的部分将存储在系统的临时文件中。使用 r.FormFile 函数可以获取文件句柄以及文件属性。

其中,文件 handler 的类型为 *multipart.FileHeader,结构体如下:

1
2
3
4
5
6
7
8
9
// A FileHeader describes a file part of a multipart request.
type FileHeader struct {
Filename string
Header textproto.MIMEHeader
Size int64
// 非导出字段
content []byte
tmpfile string
}

访问数据库

database/sql 接口

Go 定义了了一些标准的接口,开发者可以根据定义的接口来开发相应的数据库驱动。

sql.Register

1
func Register(name string, driver driver.Driver)

Register 注册并命名一个数据库,可以在 Open 函数中使用该命名启用该驱动。

mymysql、sqlite3的驱动通过 init() 函数注册自己的数据库驱动名称以及相应的 driver 实现:

1
2
3
4
5
6
7
8
9
10
11
12
// https://github.com/mattn/go-sqlite3 驱动
func init() {
sql.Register("sqlite3", &SQLiteDriver{})
}

// https://github.com/mikespook/mymysql 驱动
// Driver automatically registered in database/sql
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
func init() {
Register("SET NAMES utf8")
sql.Register("mymysql", &d)
}

而在 database/sql 内部,通过一个 map 来存储用户定义的驱动,所以可以通过注册函数 Register 同时注册多个数据库驱动:

1
var drivers = make(map[string]driver.Driver)

sql.DB

DB 中 freeConn 是一个简易的连接池,它的实现相当的简单:

  • 当执行 db.prepare -> db.prepareDC 的时候会 defer dc.releaseConn
  • 调用db.putConn,也就是把这个连接放入连接池。
  • 每次调用 db.conn 的时候会先判断 freeConn 的长度是否大于 0,大于 0 说明有可以复用的 conn,直接拿出来用就是了,如果不大于 0,则创建一个 conn,然后再返回之。
1
2
3
4
5
6
7
type DB struct {
driver driver.Driver
dsn string
mu sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
closed bool
}

driver.Driver

Driver 是一个数据库驱动接口,它定义了一个方法 Open,这个方法返回一个数据库 Conn 接口:

1
2
3
4
5
6
7
8
9
type Driver interface {
// Open返回一个新的与数据库的连接,参数name的格式是驱动特定的。
//
// Open可能返回一个缓存的连接(之前关闭的连接),但这么做是不必要的;
// sql包会维护闲置连接池以便有效的重用连接。
//
// 返回的连接同一时间只会被一个go程使用。
Open(name string) (Conn, error)
}

注意,返回的 Conn 连接同一时间只能被一个 goroutine 使用。

driver.Conn

Conn 是一个数据库连接的接口定义,他定义了一系列方法,这个 Conn 只能应用在一个 goroutine 里面,不能使用在多个 goroutine 里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Conn interface {
// Prepare返回一个准备好的、绑定到该连接的状态。
Prepare(query string) (Stmt, error)

// Close作废并停止任何现在准备好的状态和事务,将该连接标注为不再使用。
//
// 因为sql包维护着一个连接池,只有当闲置连接过剩时才会调用Close方法,
// 驱动的实现中不需要添加自己的连接缓存池。
Close() error

// Begin开始并返回一个新的事务。
Begin() (Tx, error)
}
  • Prepare 函数返回与当前连接相关的执行 SQL 语句的准备状态,可以进行查询、删除等操作。
  • Close 函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了 database/sql 里面建议的 conn pool,所以你不用再去实现缓存 conn 之类的,这样会容易引起问题。
  • Begin 函数返回一个代表事务处理的 Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。

driver.Stmt

Stmt 是一种准备好的状态,和 Conn 相关联( Stmt会绑定到一个连接 ),而且只能应用于一个 goroutine 中,不能应用于多个 goroutine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Stmt interface {
// Close关闭Stmt。
//
// 和Go1.1一样,如果Stmt被任何查询使用中的话,将不会被关闭。
Close() error

// NumInput返回占位参数的个数。
//
// 如果NumInput返回值 >= 0,sql包会提前检查调用者提供的参数个数,
// 并且会在调用Exec或Query方法前返回数目不对的错误。
//
// NumInput可以返回-1,如果驱动占位参数的数量。
// 此时sql包不会提前检查参数个数。
NumInput() int

// Exec执行查询,而不会返回结果,如insert或update。
Exec(args []Value) (Result, error)

// Query执行查询并返回结果,如select。
Query(args []Value) (Rows, error)
}
  • Close 函数关闭当前的链接状态,但是如果当前正在执行 query,query 还是有效返回 rows 数据。
  • NumInput 函数返回当前预留参数的个数,当返回 >=0 时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回 -1。
  • Exec 函数执行 Prepare 准备好的 SQL,传入参数执行 update/insert 等操作,返回 Result 数据。
  • Query 函数执行 Prepare 准备好的 SQL,传入需要的参数执行 select 操作,返回 Rows 结果集。

driver.Tx

事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以。Tx 代表一次事务

1
2
3
4
type Tx interface {
Commit() error
Rollback() error
}

driver.Execer

这是一个 Conn 可选择实现的接口:

1
2
3
type Execer interface {
Exec(query string, args []Value) (Result, error)
}

如果这个接口没有定义,那么在调用 DB.Exec,就会首先调用 Prepare 返回 Stmt,然后执行 StmtExec,然后关闭 Stmt

driver.Result

这个是执行 Update/Insert 等操作返回的结果接口定义:

1
2
3
4
5
6
7
type Result interface {
// LastInsertId返回insert等命令后数据库自动生成的ID
LastInsertId() (int64, error)

// RowsAffected返回被查询影响的行数
RowsAffected() (int64, error)
}
  • LastInsertId 函数返回由数据库执行插入操作得到的自增 ID 号

  • RowsAffected 函数返回 query 操作影响的数据条目数。

driver.Rows

Rows 是执行查询得到的结果的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Rows interface {
// Columns返回各列的名称,列的数量可以从切片长度确定。
// 如果某个列的名称未知,对应的条目应为空字符串。
Columns() []string

// Close关闭Rows。
Close() error

// 调用Next方法以将下一行数据填充进提供的切片中。
// 提供的切片必须和Columns返回的切片长度相同。
//
// 切片dest可能被填充同一种驱动Value类型,但字符串除外。
// 所有string值都必须转换为[]byte。
//
// 当没有更多行时,Next应返回io.EOF。
Next(dest []Value) error
}
  • Columns 函数返回查询数据库表的字段信息,这个返回的 slice 和 sql 查询的字段一一对应,而不是返回整个表的所有字段。
  • Close 函数用来关闭 Rows 迭代器。
  • Next 函数用来返回下一条数据,把数据赋值给 dest。dest 里面的元素必须是 driver.Value 的值除了 string,返回的数据里面所有的 string 都必须要转换成 [] byte。如果最后没数据了,Next 函数最后返回 io.EOF。

driver.RowsAffected

RowsAffected 其实就是一个 int64 的别名,但是他实现了 Result 接口,用于 insertupdate 操作,这些操作会修改零到多行数据。:

1
2
3
4
5
type RowsAffected int64

func (RowsAffected) LastInsertId() (int64, error)

func (v RowsAffected) RowsAffected() (int64, error)

driver.Value

1
type Value interface{}

Value 是驱动必须能处理的值。它要么是nil,要么是如下类型的实例:

1
2
3
4
5
6
int64
float64
bool
[]byte
string [*] Rows.Next不会返回该类型值
time.Time

driver.ValueConverter

ValueConverter 接口定义了如何把一个普通的值转化成 driver.Value 的接口:

1
2
3
type ValueConverter interface {
ConvertValue(v interface{}) (Value, error)
}

在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个 ValueConverter 有很多好处:

  • 转化 driver.value 到数据库表相应的字段,例如 int64 的数据如何转化成数据库表 uint16 字段。
  • 把数据库查询结果转化成 driver.Value 值。
  • 在 scan 函数里面如何把 driver.Value 值转化成用户定义的值。

driver.Valuer

Valuer 接口定义了返回一个 driver.Value 的方式,很多类型都实现了这个 Value 方法,用来自身与 driver.Value 的转化。

1
2
3
type Valuer interface {
Value() (Value, error)
}

MySQL数据库

github.com/go-sql-driver/mysql 为支持 MySQL 的驱动,它支持 database/sql 标准。

打开驱动

1
2
3
4
5
6
7
// 打开驱动
dsn := "user:password@tcp(ip:port)/dbname?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
defer db.Close()

插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 插入数据
stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?")
if err != nil {
log.Fatal(err)
}

res, err := stmt.Exec("dawn", "研发部门", "2022-05-15")
if err != nil {
log.Fatal(err)
}

id, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
fmt.Println(id)

更新数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 更新数据
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
if err != nil {
log.Fatal(err)
}

res, err = stmt.Exec("zh", id)
if err != nil {
log.Fatal(err)
}

affect, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
fmt.Println(affect)

查询数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 查询数据
rows, err := db.Query("SELECT * FROM userinfo")
if err != nil {
log.Fatal(err)
}

for rows.Next() {
var uid int
var username string
var department string
var created string
err = rows.Scan(&uid, &username, &department, &created)
if err != nil {
log.Fatal(err)
}
// ...
}

删除数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 删除数据
stmt, err = db.Prepare("delete from userinfo where uid=?")
if err != nil {
log.Fatal(err)
}

res, err = stmt.Exec(id)
if err != nil {
log.Fatal(err)
}

affect, err = res.RowsAffected()
if err != nil {
log.Fatal(err)
}

fmt.Println(affect)

Web 基础

Go 搭建一个 Web 服务器

Go 搭建一个 Web 服务器

使用 http 包可以轻松的搭建一个 Web 服务器:

  • http.HandleFunc 函数:用于绑定路由。
  • http.ListenAndServe 函数:设置监听的端口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"log"
"net/http"
"strings"
)

func sayHelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // 解析参数
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
fmt.Println("path:", r.URL.Path)
fmt.Println("scheme:", r.URL.Scheme)

for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}

fmt.Fprintf(w, "Hello astaxie!") // 这个写入到 w 的是输出到客户端的
}

func main() {
http.HandleFunc("/", sayHelloName) // 设置路由
err := http.ListenAndServe(":9000", nil) // 设置监听端口
if err != nil {
log.Fatal(err)
}
}

Go 中 Web 的工作方式

以下是服务器端的几个基本概念:

  • Request :用户的请求信息。
  • Response :服务器需要反馈给客户端的信息。
  • Conn :用户每次请求的链接。
  • Handler :处理请求和生成返回信息的处理逻辑。

http 包运行机制

Go 实现 Web 服务的工作模式如下:

  1. 创建 Listen Socket,监听指定的端口,等待客户端请求到来。
  2. Listen Socket 接收客户端的请求,得到 Client Socket,通过Client Socket 与客户端进行通信。
  3. 处理客户端的请求,交给 Handler进行处理,Handler 处理好数据之后通过 Client Socket 返回给客户端。

img

这整个的过程里面我们只要了解清楚下面三个问题,也就知道 Go 是如何让 Web 运行起来了:

  • 如何监听端口?

通过 http.ListenAndServe 函数,可以监听端口并分配handler,底层实现如下:初始化一个 server 对象,然后调用了 net.Listen("tcp", addr),也就是底层用 TCP 协议搭建了一个服务,然后监控我们设置的端口。

  • 如何接收客户端请求?

以下来自 http 包的源码,可以看到整个 http 的处理过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
var tempDelay time.Duration // how long to sleep on accept failure
for {
rw, e := l.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c, err := srv.newConn(rw)
if err != nil {
continue
}
go c.serve()
}
}

在监听完端口过后,调用了 srv.Serve(net.Listener) 方法,这个方法就是处理客户端的请求信息。在 Accept 之后,创建一个 Conn,然后开启一个协程来单独处理来自客户端的 http 请求:go c.serve()

  • 如何分配 handler?

Conn 首先会解析 request:c.readRequest(),然后获取相应的 handler:handler := c.server.Handerc.server.Handler 即为 http.ListenAndServe 的第二个参数;若为,则默认获取 handler = DefaultServeMux

DefaultServeMux 实际上是一个路由器,它用来匹配 url 跳转到其相应的 handle 函数,通过 http.HandleFunc 函数设置。DefaultServeMux 会调用 ServeHTTP方法,这个方法内部调用了 http.HandleFunc 函数定义的 handler。

和http包img

http 包

Go 的 http 有两个核心功能:Conn、ServeMux

Conn 的 goroutine

在 http 包中,当与客户端建立 TCP 连接之后,会建立一个协程来单独处理这一次用户的请求:

1
2
3
4
5
c, err := srv.newConn(rw)
if err != nil {
continue
}
go c.serve()

ServeMux

c.server.Handler时,则默认获取 handler = DefaultServeMux。这个 DefaullServeMux 是一个路由器,结构如下:

1
2
3
4
5
type ServeMux struct {
mu sync.RWMutex // 锁,由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达式
hosts bool // 是否在任意的规则中带有 host 信息
}

muxEntry 的结构如下:

1
2
3
4
5
type muxEntry struct {
explicit bool // 是否精确匹配
h Handler // 这个路由表达式对应哪个 handler
pattern string // 匹配字符串
}

Handler 是一个接口:

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由实现器
}

但是用户自定义的 Handler 并没有实现 ServeHTTP 方法,所以在 http 包里面还定义了一个类型 HandlerFunc,这个类型实现了 ServeHTTP 方法,也就是 Handler 接口。调用了 http.HandlerFunc 函数之后,我们自定义的 Handler 会被强制类型转换为 HandleFunc 类型,这样就有了 ServeHTTP方法:

1
2
3
4
5
6
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的呢?请看下面的代码,默认的路由器实现了 ServeHTTP

1
2
3
4
5
6
7
8
9
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

在路由器收到请求之后,如果是 *,则关闭连接。否则调用 mux.Handler(r) 返回应设置路由的处理 Handler, 然后执行 h.ServeHTTP(w, r)

mux.Handler(r) 函数实现如下,它根据用户请求的 URL 和路由器里面存储的 map 去匹配的,当匹配到之后返回存储的 handler,调用这个 handler 的 ServeHTTP 接口就可以执行到相应的函数了。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
return RedirectHandler(p, StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()

// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}

自定义路由器

ListenAndServe 函数的第二个参数可以用来配置外部路由器,它是一个 Handler 接口,即即外部路由器只要实现了 Handler 接口就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

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

type myMux struct {
}

func (p myMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayHello(w, r)
return
}

http.NotFound(w, r)
return
}

func sayHello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, I am Dawn.")
}

func main() {
mux := myMux{}
err := http.ListenAndServe(":9000", mux)
if err != nil {
log.Fatal(err)
}
}

总结

Go语言中 Web 的工作流程如下:

  • 首先调用 http.HandleFunc 函数:

    1. 调用 DefaultServeMux 的HandleFunc
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // HandleFunc registers the handler function for the given pattern
    // in the DefaultServeMux.
    // The documentation for ServeMux explains how patterns are matched.
    func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
    }

    // HandleFunc registers the handler function for the given pattern.
    func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
    panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
    }
    1. 调用了 DefaultServeMux 的 Handle
    2. 往 DefaultServeMux 的 map[string]muxEntry增加对应条目
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // Handle registers the handler for the given pattern.
    // If a handler already exists for pattern, Handle panics.
    func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()

    if pattern == "" {
    panic("http: invalid pattern")
    }
    if handler == nil {
    panic("http: nil handler")
    }
    if _, exist := mux.m[pattern]; exist {
    panic("http: multiple registrations for " + pattern)
    }

    if mux.m == nil {
    mux.m = make(map[string]muxEntry)
    }
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
    if pattern[len(pattern)-1] == '/' {
    mux.es = appendSorted(mux.es, e)
    }

    if pattern[0] != '/' {
    mux.hosts = true
    }
    }
  • 其次调用 http.ListenAndServe 函数:

    1. 实例化 Server
    2. 调用 Server 的 ListenAndServe
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // ListenAndServe listens on the TCP network address addr and then calls
    // Serve with handler to handle requests on incoming connections.
    // Accepted connections are configured to enable TCP keep-alives.
    //
    // The handler is typically nil, in which case the DefaultServeMux is used.
    //
    // ListenAndServe always returns a non-nil error.
    func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
    }
    1. 调用 net.Listen(“tcp”, addr) 监听端口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // ListenAndServe listens on the TCP network address srv.Addr and then
    // calls Serve to handle requests on incoming connections.
    // Accepted connections are configured to enable TCP keep-alives.
    //
    // If srv.Addr is blank, ":http" is used.
    //
    // ListenAndServe always returns a non-nil error. After Shutdown or Close,
    // the returned error is ErrServerClosed.
    func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
    return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
    addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
    return err
    }
    return srv.Serve(ln)
    }
    1. 启动一个 for 循环,在循环体中 Accept 请求。对于每一个请求实例化一个 Conn,请开启一个 goroutine 运行服务 go c.serve()
    2. 读取每一个请求的内容 w, err := c.readRequest()
    3. 判断 handler 是否为空,如果为空则将 handler 设置为 DefaultServeMux
    4. 调用 handler 的 ServeHTTP,即 DefaultServeMux.ServeHTTP
    5. 根据请求选择 handler,并进入这个handler 的 ServeHTTPmux.handler(r).ServeHTTP(w, r)