zoukankan      html  css  js  c++  java
  • build-web-application-with-golang学习笔记

    build-web-application-with-golang 学习教程

    这几周学习以上教程,仅记录一些重点难点部分。

    Go语言

    Go语言基础

    Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个:

    break    default      func    interface    select
    case     defer        go      map          struct
    chan     else         goto    package      switch
    const    fallthrough  if      range        type
    continue for          import  return       var
    
    • var和const参考2.2Go语言基础里面的变量和常量申明
    • package和import已经有过短暂的接触
    • func 用于定义函数和方法
    • return 用于从函数返回
    • defer 用于类似析构函数
    • go 用于并发
    • select 用于选择不同类型的通讯
    • interface 用于定义接口,参考2.6小节
    • struct 用于定义抽象数据类型,参考2.5小节
    • break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
    • chan用于channel通讯
    • type用于声明自定义类型
    • map用于声明map类型数据
    • range用于读取slice、map、channel数据

    Go程序是通过package来组织的,package <pkgName>这一行告诉我们当前文件属于哪个包,而包名main则告诉我们它是一个可独立运行的包,它在编译后会产生可执行文件。除了main包之外,其它的包最后都会生成*.a文件(也就是包文件)并放置在$GOPATH/pkg/$GOOS_$GOARCH中(以Mac为例就是$GOPATH/pkg/darwin_amd64)。

    每一个可独立运行的Go程序,必定包含一个package main,在这个main包中必定包含一个入口函数main,而这个函数既没有参数
    ,也没有返回值。

    包的概念和Python中的package类似,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。

    Go语言重点难点

    interface

    简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。

    
    package main
    
    import "fmt"
    
    type Human struct {
    	name string
    	age int
    	phone string
    }
    
    type Student struct {
    	Human //匿名字段
    	school string
    	loan float32
    }
    
    type Employee struct {
    	Human //匿名字段
    	company string
    	money float32
    }
    
    //Human实现SayHi方法
    func (h Human) SayHi() {
    	fmt.Printf("Hi, I am %s you can call me on %s
    ", h.name, h.phone)
    }
    
    //Human实现Sing方法
    func (h Human) Sing(lyrics string) {
    	fmt.Println("La la la la...", lyrics)
    }
    
    //Employee重载Human的SayHi方法
    func (e Employee) SayHi() {
    	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s
    ", e.name,
    		e.company, e.phone)
    	}
    
    // Interface Men被Human,Student和Employee实现
    // 因为这三个类型都实现了这两个方法
    type Men interface {
    	SayHi()
    	Sing(lyrics string)
    }
    
    func main() {
    	mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    	paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    	sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    	tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}
    
    	//定义Men类型的变量i
    	var i Men
    
    	//i能存储Student
    	i = mike
    	fmt.Println("This is Mike, a Student:")
    	i.SayHi()
    	i.Sing("November rain")
    
    	//i也能存储Employee
    	i = tom
    	fmt.Println("This is tom, an Employee:")
    	i.SayHi()
    	i.Sing("Born to be wild")
    
    	//定义了slice Men
    	fmt.Println("Let's use a slice of Men and see what happens")
    	x := make([]Men, 3)
    	//这三个都是不同类型的元素,但是他们实现了interface同一个接口
    	x[0], x[1], x[2] = paul, sam, mike
    
    	for _, value := range x{
    		value.SayHi()
    	}
    }
    

    interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现。

    空interface

    空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

    
    // 定义a为空接口
    var a interface{}
    var i int = 5
    s := "Hello world"
    // a可以存储任意类型的数值
    a = i
    a = s
    

    goroutine

    goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

    goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数。

    
    package main
    
    import (
    	"fmt"
    	"runtime"
    )
    
    func say(s string) {
    	for i := 0; i < 5; i++ {
    		runtime.Gosched()
    		fmt.Println(s)
    	}
    }
    
    func main() {
    	go say("world") //开一个新的Goroutines执行
    	say("hello") //当前Goroutines执行
    }
    
    // 以上程序执行后将输出:
    // hello
    // world
    // hello
    // world
    // hello
    // world
    // hello
    // world
    // hello
    

    channels

    goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel:

    
    package main
    
    import "fmt"
    
    func sum(a []int, c chan int) {
    	total := 0
    	for _, v := range a {
    		total += v
    	}
    	c <- total  // send total to c
    }
    
    func main() {
    	a := []int{7, 2, 8, -9, 4, 0}
    
    	c := make(chan int)
    	go sum(a[:len(a)/2], c)
    	go sum(a[len(a)/2:], c)
    	x, y := <-c, <-c  // receive from c
    
    	fmt.Println(x, y, x + y)
    }
    

    Web Service With Golang

    Go搭建一个Web服务器

    Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。

    
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"strings"
    	"log"
    )
    
    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)
    	fmt.Println(r.Form["url_long"])
    	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(":9090", nil) //设置监听的端口
    	if err != nil {
    		log.Fatal("ListenAndServe: ", err)
    	}
    }
    
    

    看到上面这个代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了(类似于Python的tornado)。我们build之后,然后执行web.exe,这个时候其实已经在9090端口监听http链接请求了。

    在浏览器输入http://localhost:9090

    可以看到浏览器页面输出了Hello astaxie!

    可以换一个地址试试:http://localhost:9090/?url_long=111&url_long=222

    看看浏览器输出的是什么,服务器输出的是什么?

    用户访问Web之后服务器端打印的信息

    Go的Web服务底层

    Go实现Web服务的工作模式的流程图

    http包执行流程

    1. 创建Listen Socket, 监听指定的端口, 等待客户端请求到来。

    2. Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信。

    3. 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。

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

    • 如何监听端口?
    • 如何接收客户端请求?
    • 如何分配handler?

    前面小节的代码里面我们可以看到,Go是通过一个函数ListenAndServe来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,然后调用了net.Listen("tcp", addr),也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口。

    下面代码来自Go的http包的源码,通过下面的代码我们可以看到整个的http处理过程:

    
    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)函数,这个函数就是处理接收客户端的请求信息。这个函数里面起了一个for{},首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个goroutine,把这个请求的数据当做参数扔给这个conn去服务:go c.serve()。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。

    那么如何具体分配到相应的函数来处理请求呢?conn首先会解析request:c.readRequest(),然后获取相应的handler:handler := c.server.Handler,也就是我们刚才在调用函数ListenAndServe时候的第二个参数,我们前面例子传递的是nil,也就是为空,那么默认获取handler = DefaultServeMux,那么这个变量用来做什么的呢?对,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,那么这个我们有设置过吗?有,我们调用的代码里面第一句不是调用了http.HandleFunc("/", sayhelloName)嘛。这个作用就是注册了请求/的路由规则,当请求uri为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端。

    详细的整个流程如下图所示:
    一个http连接处理流程

    Go代码的执行流程

    通过对http包的分析之后,现在让我们来梳理一下整个的代码执行过程。

    • 首先调用Http.HandleFunc

      按顺序做了几件事:

      1 调用了DefaultServeMux的HandleFunc

      2 调用了DefaultServeMux的Handle

      3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则

    • 其次调用http.ListenAndServe(":9090", nil)

      按顺序做了几件事情:

      1 实例化Server

      2 调用Server的ListenAndServe()

      3 调用net.Listen("tcp", addr)监听端口

      4 启动一个for循环,在循环体中Accept请求

      5 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve()

      6 读取每个请求的内容w, err := c.readRequest()

      7 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux

      8 调用handler的ServeHttp

      9 在这个例子中,下面就进入到DefaultServeMux.ServeHttp

      10 根据request选择handler,并且进入到这个handler的ServeHTTP

        mux.handler(r).ServeHTTP(w, r)
      

      11 选择handler:

      A 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)

      B 如果有路由满足,调用这个路由handler的ServeHTTP

      C 如果没有路由满足,调用NotFoundHandler的ServeHTTP

    表单

    表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。
    表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(

    )定义。

    处理表单输入

    默认情况下,Handler里面是不会自动解析form的,必须显式的调用r.ParseForm()后,你才能对这个表单数据进行操作。

    r.Form里面包含了所有请求的参数,比如URL中query-string、POST的数据、PUT的数据,所以当你在URL中的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。

    request.Form是一个url.Values类型,里面存储的是对应的类似key=value的信息,下面展示了可以对form数据进行的一些操作:

    
    v := url.Values{}
    v.Set("name", "Ava")
    v.Add("friend", "Jess")
    v.Add("friend", "Sarah")
    v.Add("friend", "Zoe")
    // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
    fmt.Println(v.Get("name"))
    fmt.Println(v.Get("friend"))
    fmt.Println(v["friend"])
    
    

    验证表单输入

    我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,这里关注如何在服务器端验证。

    处理文件上传

    在服务器端,我们增加一个handlerFunc:

    
    http.HandleFunc("/upload", upload)
    
    // 处理/upload 逻辑
    func upload(w http.ResponseWriter, r *http.Request) {
    	fmt.Println("method:", r.Method) //获取请求的方法
    	if r.Method == "GET" {
    		crutime := time.Now().Unix()
    		h := md5.New()
    		io.WriteString(h, strconv.FormatInt(crutime, 10))
    		token := fmt.Sprintf("%x", h.Sum(nil))
    
    		t, _ := template.ParseFiles("upload.gtpl")
    		t.Execute(w, token)
    	} else {
    		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)
    	}
    }
    

    通过上面的代码可以看到,处理文件上传我们需要调用r.ParseMultipartForm,里面的参数表示maxMemory,调用ParseMultipartForm之后,上传的文件存储在maxMemory大小的内存里面,如果文件大小超过了maxMemory,那么剩下的部分将存储在系统的临时文件中。我们可以通过r.FormFile获取上面的文件句柄,然后实例中使用了io.Copy来存储文件。

    获取其他非文件字段信息的时候就不需要调用r.ParseForm,因为在需要的时候Go自动会去调用。而且ParseMultipartForm调用一次之后,后面再次调用不会再有效果。

    通过上面的实例我们可以看到我们上传文件主要三步处理:

    1. 表单中增加enctype="multipart/form-data"
    2. 服务端调用r.ParseMultipartForm,把上传的文件存储在内存和临时文件中
    3. 使用r.FormFile获取文件句柄,然后对文件进行存储等处理。

    访问数据库

    使用MySQL数据库

    Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口,常用的有如下几种:

    接下来的例子我主要以第一个驱动为例(我目前项目中也是采用它来驱动),也推荐大家采用它,主要理由:

    • 这个驱动比较新,维护的比较好
    • 完全支持database/sql接口
    • 支持keepalive,保持长连接,虽然星星fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。

    示例代码

    接下来的几个小节里面我们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。

    
    CREATE TABLE `userinfo` (
    	`uid` INT(10) NOT NULL AUTO_INCREMENT,
    	`username` VARCHAR(64) NULL DEFAULT NULL,
    	`departname` VARCHAR(64) NULL DEFAULT NULL,
    	`created` DATE NULL DEFAULT NULL,
    	PRIMARY KEY (`uid`)
    );
    
    CREATE TABLE `userdetail` (
    	`uid` INT(10) NOT NULL DEFAULT '0',
    	`intro` TEXT NULL,
    	`profile` TEXT NULL,
    	PRIMARY KEY (`uid`)
    )
    

    如下示例将示范如何使用database/sql接口对数据库表进行增删改查操作

    
    package main
    
    import (
    	"database/sql"
    	"fmt"
    	//"time"
    
    	_ "github.com/go-sql-driver/mysql"
    )
    
    func main() {
    	db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
    	checkErr(err)
    
    	//插入数据
    	stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
    	checkErr(err)
    
    	res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
    	checkErr(err)
    
    	id, err := res.LastInsertId()
    	checkErr(err)
    
    	fmt.Println(id)
    	//更新数据
    	stmt, err = db.Prepare("update userinfo set username=? where uid=?")
    	checkErr(err)
    
    	res, err = stmt.Exec("astaxieupdate", id)
    	checkErr(err)
    
    	affect, err := res.RowsAffected()
    	checkErr(err)
    
    	fmt.Println(affect)
    
    	//查询数据
    	rows, err := db.Query("SELECT * FROM userinfo")
    	checkErr(err)
    
    	for rows.Next() {
    		var uid int
    		var username string
    		var department string
    		var created string
    		err = rows.Scan(&uid, &username, &department, &created)
    		checkErr(err)
    		fmt.Println(uid)
    		fmt.Println(username)
    		fmt.Println(department)
    		fmt.Println(created)
    	}
    
    	//删除数据
    	stmt, err = db.Prepare("delete from userinfo where uid=?")
    	checkErr(err)
    
    	res, err = stmt.Exec(id)
    	checkErr(err)
    
    	affect, err = res.RowsAffected()
    	checkErr(err)
    
    	fmt.Println(affect)
    
    	db.Close()
    
    }
    
    func checkErr(err error) {
    	if err != nil {
    		panic(err)
    	}
    }
    
    

    通过上面的代码我们可以看出,Go操作Mysql数据库是很方便的。

    关键的几个函数我解释一下:

    sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式:

    user@unix(/path/to/socket)/dbname?charset=utf8
    user:password@tcp(localhost:5555)/dbname?charset=utf8
    user:password@/dbname
    user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
    

    db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。

    db.Query()函数用来直接执行Sql返回Rows结果。

    stmt.Exec()函数用来执行stmt准备好的SQL语句

    我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。

    使用Beego orm库进行ORM开发

    beego orm是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beego orm是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beego orm是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。

    beego orm是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beego orm。目前我测试过的驱动包括下面几个:

    Mysql: github/go-mysql-driver/mysql

    PostgreSQL: github.com/lib/pq

    SQLite: github.com/mattn/go-sqlite3

    Mysql: github.com/ziutek/mymysql/godrv

    首先你需要import相应的数据库驱动包、database/sql标准接口包以及beego orm包,如下所示:

    
    import (
    	"database/sql"
    	"github.com/astaxie/beego/orm"
    	_ "github.com/go-sql-driver/mysql"
    )
    
    func init() {
    	//注册驱动
    	orm.RegisterDriver("mysql", orm.DR_MySQL)
    	//设置默认数据库
    	orm.RegisterDataBase("default", "mysql", "root:root@/my_db?charset=utf8", 30)
    	//注册定义的model
        	orm.RegisterModel(new(User))
    
       	// 创建table
            orm.RunSyncdb("default", false, true)
    }
    

    导入必须的package之后,我们需要打开到数据库的链接,然后创建一个beego orm对象(以MySQL为例),如下所示
    beego orm:

    
    func main() {
        	o := orm.NewOrm()
    }
    

    简单示例:

    
    package main
    
    import (
        "fmt"
        "github.com/astaxie/beego/orm"
        _ "github.com/go-sql-driver/mysql" // 导入数据库驱动
    )
    
    // Model Struct
    type User struct {
        Id   int
        Name string `orm:"size(100)"`
    }
    
    func init() {
        // 设置默认数据库
        orm.RegisterDataBase("default", "mysql", "root:root@/my_db?charset=utf8", 30)
        
        // 注册定义的 model
        orm.RegisterModel(new(User))
    //RegisterModel 也可以同时注册多个 model
    //orm.RegisterModel(new(User), new(Profile), new(Post))
    
        // 创建 table
        orm.RunSyncdb("default", false, true)
    }
    
    func main() {
        o := orm.NewOrm()
    
        user := User{Name: "slene"}
    
        // 插入表
        id, err := o.Insert(&user)
        fmt.Printf("ID: %d, ERR: %v
    ", id, err)
    
        // 更新表
        user.Name = "astaxie"
        num, err := o.Update(&user)
        fmt.Printf("NUM: %d, ERR: %v
    ", num, err)
    
        // 读取 one
        u := User{Id: user.Id}
        err = o.Read(&u)
        fmt.Printf("ERR: %v
    ", err)
    
        // 删除表
        num, err = o.Delete(&u)
        fmt.Printf("NUM: %d, ERR: %v
    ", num, err)
    }
    
    

    session和数据存储

    session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上通过javascript获取document.cookie,并传递给自己的appB)。

    session创建过程

    session的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随需要创建session,这个过程可以概括为三个步骤:

    • 生成全局唯一标识符(sessionid);
    • 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,当然这样会增加I/O开销,但是它可以实现某种程度的session持久化,也更有利于session的共享;
    • 将session的全局唯一标示符发送给客户端。

    以上三个步骤中,最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义,数据无非可以放到请求行、头域或Body里,所以一般来说会有两种常用的方式:cookie和URL重写。

    1. Cookie
      服务端通过设置Set-cookie头就可以将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另外一般包含session信息的cookie会将失效时间设置为0(会话cookie),即浏览器进程有效时间。至于浏览器怎么处理这个0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候);
    2. URL重写
      所谓URL重写,就是在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。虽然这种做法比较麻烦,但是,如果客户端禁用了cookie的话,此种方案将会是首选。
    
    package memory
    
    import (
    	"container/list"
    	"github.com/astaxie/session"
    	"sync"
    	"time"
    )
    
    var pder = &Provider{list: list.New()}
    
    type SessionStore struct {
    	sid          string                      //session id唯一标示
    	timeAccessed time.Time                   //最后访问时间
    	value        map[interface{}]interface{} //session里面存储的值
    }
    
    func (st *SessionStore) Set(key, value interface{}) error {
    	st.value[key] = value
    	pder.SessionUpdate(st.sid)
    	return nil
    }
    
    func (st *SessionStore) Get(key interface{}) interface{} {
    	pder.SessionUpdate(st.sid)
    	if v, ok := st.value[key]; ok {
    		return v
    	} else {
    		return nil
    	}
    }
    
    func (st *SessionStore) Delete(key interface{}) error {
    	delete(st.value, key)
    	pder.SessionUpdate(st.sid)
    	return nil
    }
    
    func (st *SessionStore) SessionID() string {
    	return st.sid
    }
    
    type Provider struct {
    	lock     sync.Mutex               //用来锁
    	sessions map[string]*list.Element //用来存储在内存
    	list     *list.List               //用来做gc
    }
    
    func (pder *Provider) SessionInit(sid string) (session.Session, error) {
    	pder.lock.Lock()
    	defer pder.lock.Unlock()
    	v := make(map[interface{}]interface{}, 0)
    	newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
    	element := pder.list.PushBack(newsess)
    	pder.sessions[sid] = element
    	return newsess, nil
    }
    
    func (pder *Provider) SessionRead(sid string) (session.Session, error) {
    	if element, ok := pder.sessions[sid]; ok {
    		return element.Value.(*SessionStore), nil
    	} else {
    		sess, err := pder.SessionInit(sid)
    		return sess, err
    	}
    	return nil, nil
    }
    
    func (pder *Provider) SessionDestroy(sid string) error {
    	if element, ok := pder.sessions[sid]; ok {
    		delete(pder.sessions, sid)
    		pder.list.Remove(element)
    		return nil
    	}
    	return nil
    }
    
    func (pder *Provider) SessionGC(maxlifetime int64) {
    	pder.lock.Lock()
    	defer pder.lock.Unlock()
    
    	for {
    		element := pder.list.Back()
    		if element == nil {
    			break
    		}
    		if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
    			pder.list.Remove(element)
    			delete(pder.sessions, element.Value.(*SessionStore).sid)
    		} else {
    			break
    		}
    	}
    }
    
    func (pder *Provider) SessionUpdate(sid string) error {
    	pder.lock.Lock()
    	defer pder.lock.Unlock()
    	if element, ok := pder.sessions[sid]; ok {
    		element.Value.(*SessionStore).timeAccessed = time.Now()
    		pder.list.MoveToFront(element)
    		return nil
    	}
    	return nil
    }
    
    func init() {
    	pder.sessions = make(map[string]*list.Element, 0)
    	session.Register("memory", pder)
    }
    

    Web服务

    错误处理,调试和测试

    部署与维护

    如何设计一个Web框架

    扩展Web框架

  • 相关阅读:
    Flask-SQLAlchemy
    with 与 上下文管理器
    使用@property
    C++:如何把一个int转成4个字节?
    尝试理解Flask源码 之 搞懂WSGI协议
    qt setData()和data()
    我使用过的Linux命令之sftp
    linux下如何使用sftp命令
    Linux环境下安装JDK
    CentOS 6.5 配置IP地址的三种方法
  • 原文地址:https://www.cnblogs.com/iamxiaoyubei/p/8850344.html
Copyright © 2011-2022 走看看