应用设计

当一个请求到达服务器时,多路复用器会检查请求,并重定向至正确的处理器,处理器在接受到多路复用器转发的请求之后,从中取出相应的信息,并根据这些信息对请求进行处理。在请求完毕之后,处理器会将所得的数据传递给模板引擎,而模板引擎会根据数据返回HTML-768557219.jpg
绝大部分应用都需要以某种方式与数据打交道。在这里,使用的PostgreSQL,并通过SQL与之交互
ChitChat的数据模型非常简单,只包含4种数据结构:

  • User—表示论坛的用户信息
  • Session—表示论坛用户
  • Thread—表示论坛里面的帖子,每一个帖子都记录了多个论坛之间的对话
  • Post—表示用户在帖子里面添加的回复

以上4种数据结构都会被映射到关系数据库
2128701064.jpeg

请求的接受与处理

请求 的接受和处理是所有Web应用的核心,流程已经在上面呈现过不再呈现

多路复用器

编译后的二进制Go应用总是以main函数作为执行的起点,我们看一个简化版的主源码文件main.go

packag main

import{
    "net.http" //net.http标准库提供了默认的多路复用器
}

func main(){
    mux :http.NewServeMux()//通过这个NewServerMux函数来创建多路复用器
    files :http.FileServer(http.Dir("public"))
    mux.Handle("/static/"),http.StripPrefix("/static/",files)

    mux.HandleFunc("/",index)//使用了HandleFunc将发送至根URL的请求重定向到处理器

    server :=&http.Server{
        Addr: "0.0.0.0:8080",
        Handler: mux,
    }
}

对于上述调用来说,当访问根目录时,请求就会被重定向到名为index的 处理器函数

服务静态文件

除了负责将重定向到相应的处理器之外,多路复用器还将程序使用了FileServer函数创建了一个能为指定目录中的静态文件的处理器,并将这个处理器传递给了多路复用器的Handle函数,还使用了StripPrefix函数去移除请求URL中的指定前缀:

files :=http.FileSever(http.Dir("/public"))
mux.Handle("/static/",http.StripPrefix("/static/",files))//以上两行代码会移除URL中的/static/字符串,然后在public目录中查找被请求的文件。

所以倘若一个请求http://localhost/static/css/bootstrap.min.css的请求时,就会在Public中找css/bootstrap.min.css当服务器找到之后就会把它返回给客户端

创建处理器函数

正如前面所说,ChitChat应用会通过HandleFunc函数把请求重定向到处理器函数。处理器函数实际上就是一个接受ResponseWriter和Request指针作为参数的Go函数,接下来是index处理器函数
main.go中的index处理器函数

func index(w http.ResponseWriter, r *http.Request){
    file := []string{"templates/layout.html",
                     "templates/navbar.html",
                     "templates/index.html",}
    templates := template.Must(template.ParseFiles(files...))
    threaads, err := data.Threads(); if err ==nil{
        templates.ExecuteTemplate(w,"layout",thread)
    }
}

index函数负责人生成HTML并将其写入ResponseWriter中(接下去略),详见后面,在php或者Python中常常会要求用户编写代码去包含(include)被应用,另一些则是link,在Go语言中,用户只需要把位于相同目录下的所有文件都设置成一个包,那么这些文件就会与包中其他文件分享定义,或者可以通过导入包来使用。

使用cookie进行访问控制

当一个用户已经登录以后,服务器必须标识这个是一个已经登录的用户,为了做到这点,服务器会写入一个cookie,我们来看看这个route_auth.go中的authenticate函数

func authenticate(w http.ResponseWriter, r *http.Request){
    r.ParseForm()
    user, _ :=data.UserByEmail(r.PostFormValue("email"))
    if user.Password == data,Encrypt(r.PostFormValue("password")){
        session :=user.CreateSession()
        cookie :=http.Cookie{
            name:"_cookie",
            value:session.Uuid,
            HttpOnly:true,
        }
        http.SetCookie(w,&cookie)
        http.Redirect(w,r,"/",302)
    }else{
        http.Redirect(w,r,"/login",302)
}

在核实了用户的身份之后,程序会使用User结构的CreateSession方法创建一个Session结构,结构的定义如下:

type Sessio struct
{
    ID        INT 
    Uuid      string
    Email     string
    UserId    int 
    CreatedAT time.time
}

这个Uui是唯一的所以服务器会通过cookie将ID存储到浏览器里面,并把Session机构中的各项信息储存到数据库当中。
在创建了Session结构之后,程序又创建了cookie结构

cookie :=http,Cookie{
    Name:"_cookie",
    Value:session.Uuid,
    HttpOnly:true,
    }

在这里因为程序没有给cookie设置过期时间,所以这个cookie就成了一个会话cookie(我认为不安全,因为倘若敏感信息泄露就可能泄露所有的cookie导致所有用户任意登录),并且这里有HttpOnly为true,所以他无法通过Javascript等非HTTP API进行访问。

接下来我们需要创建一个名为session的工具函数,并在各个处理器函数复用,这个工具定义在Util.go文件中,但是因为这个文件隶属于main.go所以无需引入包调用
util.go文件中的session工具函数

func session(w http.ResponseWriter,r *http.Request)(sess data.Session, err error){
    cookie, err := r.Cookie("_cookie")//从请求中取回cookie
    if err ==nil{
        sess = data.Session{Uuid: cookie.value}//判断数据库是否存在唯一iD
        if ok, _ :=sess.Check(); !ok{     //通过check函数判断是否正确
            err = errors,New("Invaild ssession")    
        }
    }return
}

使用session的函数
_, err := session(w,r)

使用模板生成HTML效应

index函数将每个需要用到的模板文件都放到了Go切片中去

private_tmpl_files := []string{
    "..."
    "..."
    "..."
}

跟别的模板引擎相类似的是切片指定的3个HTML文件都包含了特定的嵌入命令,这些称为动作(action)动作在HTML文件里面会被{{xxx}}包围
接着,程序会调用ParseFiles函数对这些模板文件进行语法分析,并创建出相对应的模板,为了捕获错误,程序使用了Must函数去包围ParseFiles函数的执行结果,这样当返回错误时,它就能提供错误报告:
templates := template.Must(template.ParseFiles(private_tmpl_files...))
模板文件和模板一一对应的做法可以给开发带来方便
layout.html模板文件

{{ define "layout"}}

<!DOCTYPE html>
<html lang="en">
  <head>
  </head>
<body>
  {{template "navbar".}}
  <div class="container">
    {{ template "content".}}
  </div>
</body>
</html>
{{ end }}

除了define动作之外,模板文件中还包含了应用其他的template动作。跟在被引用模板名字之后的点(.)代表了传递给被引用模板的数据,也就是说既引用了模板又引用了数据
(navbar模板略,感觉没啥用)

{{ define "content" }}

<p class="lead">
  <a href="/thread/new">Start a thread</a>or join one below!
</p>
{{ range. }}
<div class="panel panel-default">
  <div class="panel-heading">
    <span class ="lead"><i class="fa fa-comment-o"></i>{{.Topic}}</span>
  </div>
  <div class="panel-body">
     Started by {{ .User.name }}-{{ .CreatedAtDate }} - {{ .NumReplies }}post.
    <div class="pull-right">
      <a href="/thread/read?id={{ .Uuid }}">Read more</a>
    </div>
  </div>
</div>
{{ end }}

这里如果前端看不懂也没有很大关系,我们只需要在这里关注这些{{}}内的动作,其中有一大部分的动作很明显是前面所出现过的,如{{.User.name}}类似的,大概明白动作是如何表达的即可,接下来我们要说一说模板运行的原理,不知道你们还记不记得前面所出现过的这行代码:
templates.ExecuteTemplate(write,"layout",threads)
在这行代码中,程序调用ExecuteTemplate函数,执行已经经过语法分析的layout模板。执行意味着把模板文件中的内容和来自其他渠道的数据进行合并,让后生成html,如图
2023-03-14T07:30:46.png
而程序只对layout模板处理的原因则是因为,layout中引用了navbar和content,在处理layout的时候会导致其他两个模板也同时被执行。
代码合并
因为生成HTML的代码会被重复执行多次,所以我们决定对这些代码进行整理,将他们归于generateHTML函数底下

func generateHTML(w http.ResponseWriter, data interface{},fn ...string)//接收一个ResponseWriter一些数据和一系列模板文件。data参数的类型是空接口类型,也就是说这个接口可以接受任何类型的参数
var file []string
for_, file :=range fn{
      files = append(files, fmt.Sprintf("templates/%s.html",file)) 
  }
templates := template.Must(template.ParseFiles(files...))
templates.ExecuteTemplate(write, "layout",data)
}

因为go独特具有的空接口的机制,得以绕过静态编程语言的限制,并因此得到接受多种不同类型输入的能力。在这里还有一个参数fn,它以...开头表示generateHTML是一个可变参数函数,这意味着它可以接受一个值或者多个参数值,也意味着我们可以一次性将多个模板文件传递进来。另外还有一个值得注意的点就是可变参数必须写在这个函数的最后一个。
因此我们可以对index函数进行简化

func index(writer http.ResponseWriter, request *http.Request){
    threads, err :data.Threads(); if err == nil{
        _, err := session(writer,request)
        if err != nil {
            generateHTML(writer,threads,"layout","public.navbar","index")
        } else {
            generateHTML(writer,threads,"layout","private.navbar","index")
            }
        }
    }

连接数据库(略)