2.1 ChitChat简介
ChitChat论坛:在这个论坛里面,用户可以注册账号,并在登录之后发表新帖子又或者回复已有的帖子;未注册用户可以查看帖子,但是无法发表帖子或是回复帖子。
2.2 应用设计
Web应用的一般工作流程是客户端向服务器发送请求,然后服务器对客户端进行响应,如下图所示,ChitChat应用的设计也遵循这一流程。
ChitChat的应用逻辑会被编码到服务器里面。服务器会向客户端提供HTML页面,并通过页面的超链接向客户端表明请求的格式以及被请求的数据,而客户端则会在发送请求时向服务器提供相应的数据。 请求的格式通常是由应用自行决定的,比如,ChitChat的请求使用的是以下格式:
http://<服务器名><处理器名>?<参数> 。
服务器名( server name)是ChitChat服务器的名字,而处理器名 (handler name)则是被调用的处理器的名字。处理器的名字是按层级进行划分的:位于名字最开头是被调用模块的名字,而之后跟着的则是被调用子模块的名字,以此类推,位于处理器名字最末尾的则是子模块中负责处理请求的处理器。比如,对/thread/read 这个处理器名字来说,thread 是被调用的模块,而read 则是这个模块中负责读取帖子内容的处理器。
该应用的参数 (parameter)会以URL查询的形式传递给处理器,而处理器则会根据这些参数对请求进行处理。比如说,假设客户端要向处理器传递帖子的唯一ID,那么它可以将URL的参数部分设置成id=123 ,其中123 就是帖子的唯一ID。
如果chitchat 就是ChitChat服务器的名字,那么根据上面介绍的URL格式规则,客户端发送给ChitChat服务器的URL可能会是这样的:http://chitchat/thread/read?id=123。
当请求到达服务器时,多路复用器 (multiplexer)会对请求进行检查,并将请求重定向至正确的处理器进行处理。处理器在接收到多路复用器转发的请求之后,会从请求中取出相应的信息,并根据这些信息对请求进行处理。在请求处理完毕之后,处理器会将所得的数据传递给模板引擎,而模板引擎则会根据这些数据生成将要返回给客户端的HTML,整个过程如下图所示。
2.3 数据模型
ChitChat的数据将被存储到关系式数据库PostgreSQL里面,并通过SQL与之交互。
ChitChat的数据模型非常简单,只包含4种数据结构:
User——表示论坛的用户信息;
Session——表示论坛用户当前的登录会话;
Thread——表示论坛里面的帖子,每一个帖子都记录了多个论坛用户之间的对话;
Post——表示用户在帖子里面添加的回复。
2.4 请求的接收与处理
请求的接收和处理是所有Web应用的核心。
Web应用的工作流程如下。
(1)客户端将请求发送到服务器的一个URL上。
(2)服务器的多路复用器将接收到的请求重定向到正确的处理器,然后由该处理器对请求进行处理。
(3)处理器处理请求并执行必要的动作。
(4)处理器调用模板引擎,生成相应的HTML并将其返回给客户端。
让我们先从最基本的根URL(/ )来考虑Web应用是如何处理请求的:当我们在浏览
器上输入地址http://localhost 的时候,浏览器访问的就是应用的根URL。
2.4.1 多路复用器
package main
import (
"net/http"
//net/http 标准库提供了一个默认的多路复用器
//这个多路复用器可以通过调NewServeMux 函数来创建:
)
func main() {
mux := http.NewServeMux()
//创建一个多路复用器
files := http.FileServer(http.Dir("/public"))
//除负责将请求重定向到相应的处理器之外,多路复用器还需要为静态文件提供服务。
//程序使用FileServer 函数创建了一个能够为指定目录中的静态文件服务的处理器。
mux.Handle("/static/", http.StripPrefix("/static/", files))
//将这个处理器传递给了多路复用器的Handle 函数。
//程序还使用StripPrefix 函数去移除请求URL中的指定前缀:
//当服务器接收到一个以/static/ 开头的URL请求时,以上两行代码会移除URL中的/static/字符串,
//然后在public 目录中查找被请求的文件。比如说,当服务器接收
//到一个针对文件http://localhost/static/css/bootstrap.min.css 的请求时
//它将会在public 目录中查找以下文件:
//<application root>/css/bootstrap.min.css
//当服务器成功地找到这个文件之后,会把它返回给客户端。
mux.HandleFunc("/", index)
//HandleFunc 函数接受一个URL和一个处理器的名字作为参数,
//并将针给定URL的请求转发至指定的处理器进行处理,
//因此对上述调用来说,当有针对根URL的请求到达时,
//该请求就会被重定向到名为index 的处理器函数。
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: mux,
}
//这里创建了一个 http.Server 结构体,指定了服务器的监听地址和端口
//以及处理器为上述创建的多路复用器。
server.ListenAndServe()
//使用 ListenAndServe 方法启动 HTTP 服务器,开始监听来自客户端的请求,
//并将这些请求分发给相应的处理器进行处理。
}
2.4.3 创建处理器函数
func index(w http.ResponseWriter, r *http.Request) {
//index 函数负责生成HTML并将其写入ResponseWriter 中。因为这个处理器函数
//会用到html/template 标准库中的Template 结构,所以包含这个函数的文件需要在
//文件的开头导入html/template 库。
files := []string{"templates/layout.html",
"templates/navbar.html",
"templates/index.html"}
//定义了模板文件的路径,这些模板文件将被用于生成 HTML 页面。
//这些文件路径是一个字符串切片,包含了模板文件的相对路径或绝对路径。
templates := template.Must(template.ParseFiles(files...))
//使用 template.ParseFiles 函数解析了模板文件。
//这是为了后续生成 HTML 页面时能够使用模板文件中定义的模板结构。
threads, err := data.Threads()
if err == nil {
templates.ExecuteTemplate(w, "layout", threads)
}
//调用了一个函数 data.Threads() 来获取数据(可能是论坛帖子列表等)。
//如果数据获取成功(err== nil),则调用 templates.ExecuteTemplate 方法执行模板,
//生成 HTML 页面并将其写入到http.ResponseWriter 中。
}
2.4.4 使用cookie进行访问控制
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)
}
}//data.UserByEmail函数通过给定的电子邮件地址获取与之对应的User结构,而data.Encrypt 函数则用于加密给定的字符串。
在验证用户身份的时候,程序必须先确保用户是真实存在的,并且提交给处理器的密码在加密之后跟存储在数据库里面的已加密用户密码完全一致。在核实了用户的身份之后,程序会使用User 结构的CreateSession 方法创建一个Session 结构。
type Session struct {
Id int//
Uuid string
//Uuid 字段存储的是一个随机生成的唯一ID,这个ID是实现会话机制的核心,
//服务器会通过cookie把这个ID存储到浏览器里面,并把Session 结构中记录的各项信息存储到数据库中。
Email string//存储用户的电子邮件地址
UserId int//记录用户表中存储用户信息的行的ID
CreatedAt time.Time
}
创建了Session 结构之后,程序又创建了Cookie 结构
cookie := http.Cookie{
Name: "_cookie",
Value: session.Uuid,
//cookie的名字是随意设置的,而cookie的值则是将要被存储到浏览器里面的唯一ID。
//因为程序没有给cookie设置过期时间,所以这个cookie就成了一个会话cookie,
//它将在浏览器关闭时自动被移除。
HttpOnly: true,
//cookie只能通过HTTP或者HTTPS访问,但是却无法通过JavaScript等非HTTP API进行访问。
}
在设置好cookie之后,程序使用以下这行代码,将它添加到了响应的首部里面:
http.SetCookie(writer, &cookie)
在将cookie存储到浏览器里面之后,程序接下来要做的就是在处理器函数里面检查当前访问的用户是否已经登录
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}
if ok, _ := sess.Check(); !ok {
//从cookie中取出会话并调用后者的Check 方法/
err = errors.New("Invalid session")
}
//如果cookie存在,那么session
//函数将继续进行第二项检查——访问数据库并核实会话的唯一ID是否存在。
}
return
}
func index(w http.ResponseWriter, r *http.Request) {
threads, err := data.Threads(); if err == nil {
, err := session(w, r)
public_tmpl_files := []string{"templates/layout.html",
"templates/public.navbar.html",
"templates/index.html"}
private_tmpl_files := []string{"templates/layout.html",
"templates/private.navbar.html",
"templates/index.html"}
var templates *template.Template
if err != nil {
templates = template.Must(template.Parse-
Files(private_tmpl_files...))
} else {
templates = template.Must(template.ParseFiles(public_tmpl_files...))
}
templates.ExecuteTemplate(w, "layout", threads)
}
}
调用session 函数可以取得一个存储了用户信息的Session 结构,不过因为index 函数目前并不需要这些信息,所以它使用空白标识符 (blank identifier)(_)忽略了这一结构。index 函数真正感兴趣的是err 变量,程序会根据这个变量的值来判断用户是否已经登录,然后以此来选择是使用public 导航条还是使用private 导航条。
2.5 使用模板生成HTML响应
private_tmpl_files := []string{"templates/layout.html",
"templates/private.navbar.html",
"templates/index.html"}
切片指定的这3个HTML文件都包含了特定的嵌入命令,这些命令被称为动作 (action),动作在HTML文件里面会被{{ 符号和}} 符号包围。
templates := template.Must(template.ParseFiles(private_tmpl_files...))
接着,程序会调用ParseFiles 函数对这些模板文件进行语法分析,并创建出相应的模板。为了捕捉语法分析过程中可能会产生的错误,程序使用了Must 函数去包围ParseFiles 函数的执行结果,这样当ParseFiles 返回错误的时候,Must 函数就会向用户返回相应的错误报告。
{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ChitChat</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
{{ template "navbar" . }}
<div class="container">
{{ template "content" . }}
</div> <!-- /container -->
<script src="/static/js/jquery-2.1.1.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>
{{ end }}
layout.html 模板文件的源代码,源代码中使用了define 动作,这个动作通过文件开头的{{ define "layout" }} 和文件末尾的{{ end }} ,把被包围的文本块定义成了layout 模板的一部分。另外两个也都是同理。
除了define 动作之外,layout.html 模板文件里面还包含了两个用于引用其他模板文件的template 动作。跟在被引用模板名字之后的点(. )代表了传递给被引用模板的数据,比如{{ template "navbar" . }} 语句除了会在语句出现的位置引入navbar模板之外,还会将传递给layout 模板的数据传递给navbar 模板。
程序通过调用ExecuteTemplate 函数,执行(execute)已经经过语法分析的layout 模板。执行模板意味着把模板文件中的内容和来自其他渠道的数据进行合并,然后生成最终的HTML内容。
2.6 安装PostgreSQL
2.7 流程
(1)客户端向服务器发送请求;
(2)多路复用器接收到请求,并将其重定向到正确的处理器;
(3)处理器对请求进行处理;
(4)在需要访问数据库的情况下,处理器会使用一个或多个数据结构,这些数据结构都是根据数据库中的数据建模而来的;
(5)当处理器调用与数据结构有关的函数或者方法时,这些数据结构背后的模型会与数据库进行连接,并执行相应的操作;
(6)当请求处理完毕时,处理器会调用模板引擎,有时候还会向模板引擎传递一些通过模型获取到的数据;
(7)模板引擎会对模板文件进行语法分析并创建相应的模板,而这些模板又会与处理器传递的数据一起合并生成最终的HTML;
(8)生成的HTML会作为响应的一部分回传至客户端。