文章目录
Go语言Web编程基础
计算机网络基础
计算机网络是独立自主的计算机互联而成的系统的总称,组建计算机网络最主要的目的是实现多台计算机之间的通信和资源共享。今天计算机网络中的设备和计算机网络的用户已经多得不可计数,而计算机网络也可以称得上是一个“复杂巨系统”,对于这样的系统,我们不可能用一两篇文章把它讲清楚,有兴趣的读者可以自行阅读Andrew S.Tanenbaum老师的经典之作《计算机网络》或Kurose和Ross老师合著的《计算机网络:自顶向下方法》来了解计算机网络的相关知识。
计算机网络发展史
-
1960s - 美国国防部ARPANET项目问世,奠定了分组交换网络的基础。
-
1980s - 国际标准化组织(ISO)发布OSI/RM,奠定了网络技术标准化的基础。
-
1990s - 英国人蒂姆·伯纳斯-李发明了图形化的浏览器,浏览器的简单易用性使得计算机网络迅速被普及。
TCP/IP模型
实现网络通信的基础是网络通信协议,这些协议通常是由互联网工程任务组 (IETF)制定的。所谓“协议”就是通信计算机双方必须共同遵从的一组约定,例如怎样建立连接、怎样互相识别等,网络协议的三要素是:语法、语义和时序。构成我们今天使用的Internet的基础的是TCP/IP协议族,所谓协议族就是一系列的协议及其构成的通信模型,我们通常也把这套东西称为TCP/IP模型。与国际标准化组织发布的OSI/RM这个七层模型不同,TCP/IP是一个四层模型,也就是说,该模型将我们使用的网络从逻辑上分解为四个层次,自底向上依次是:网络接口层、网络层、传输层和应用层,如下图所示。
IP通常被翻译为网际协议,它服务于网络层,主要实现了寻址和路由的功能。接入网络的每一台主机都需要有自己的IP地址,IP地址就是主机在计算机网络上的身份标识。当然由于IPv4地址的匮乏,我们平常在家里、办公室以及其他可以接入网络的公共区域上网时获得的IP地址并不是全球唯一的IP地址,而是一个局域网(LAN)中的内部IP地址,通过网络地址转换(NAT)服务我们也可以实现对网络的访问。计算机网络上有大量的被我们称为“路由器”的网络中继设备,它们会存储转发我们发送到网络上的数据分组,让从源头发出的数据最终能够找到传送到目的地通路,这项功能就是所谓的路由。
TCP全称传输控制协议,它是基于IP提供的寻址和路由服务而建立起来的负责实现端到端可靠传输的协议,之所以将TCP称为可靠的传输协议是因为TCP向调用者承诺了三件事情:
- 数据不传丢不传错(利用握手、校验和重传机制可以实现)。
- 流量控制(通过滑动窗口匹配数据发送者和接收者之间的传输速度)。
- 拥塞控制(通过RTT时间以及对滑动窗口的控制缓解网络拥堵)。
网络应用模式
- C/S模式和B/S模式。这里的C指的是Client(客户端),通常是一个需要安装到某个宿主操作系统上的应用程序;而B指的是Browser(浏览器),它几乎是所有图形化操作系统都默认安装了的一个应用软件;通过C或B都可以实现对S(服务器)的访问。关于二者的比较和讨论在网络上有一大堆的文章,在此我们就不再浪费笔墨了。
- 去中心化的网络应用模式。不管是B/S还是C/S都需要服务器的存在,服务器就是整个应用模式的中心,而去中心化的网络应用通常没有固定的服务器或者固定的客户端,所有应用的使用者既可以作为资源的提供者也可以作为资源的访问者。
DNS解析
DNS解析是互联网上的一项核心服务,它将人类可读的域名(如 www.example.com)转换为机器可读的IP地址(如 192.0.2.1)。这个过程允许用户通过简单易记的域名来访问互联网上的资源,而不需要记住复杂的数字序列。以下是DNS解析的详细步骤
- 域名结构
- 域名是由多个部分组成的层次结构,从右到左依次是顶级域(TLD)、二级域、三级域等。
- 例如,在域名 www.example.com 中,.com 是顶级域,example 是二级域,www 是三级域
- DNS查询
- 当用户在浏览器中输入一个域名时,他们的计算机会发起一个DNS查询,以获取与该域名关联的IP地址
- 这个查询通常通过UDP协议发送到本地DNS服务器(通常由ISP提供)
- 本地DNS服务器
- 本地DNS服务器接收到查询后,会首先检查其缓存中是否有该域名的记录。一般位于hosts文件中,如果有,并且记录还没有过期,它会直接返回结果
- 如果缓存中没有记录,或者记录已过期,本地DNS服务器将作为DNS客户端,向其他DNS服务器发起查询
- 根域名服务器
- 如果本地DNS服务器无法解析域名,它会向根域名服务器发起查询。根域名服务器是DNS层次结构的顶部,它们知道所有顶级域的地址
- 根域名服务器不直接提供IP地址,而是返回指向相应顶级域的DNS服务器的地址
DNS解析是一个分布式数据库查询的过程,它涉及到多个服务器和多个网络通信步骤。这个过程对于确保互联网的稳定运行至关重要
Go语言的标准库"net/http"的使用
Go语言的标准库 net/http
提供了HTTP客户端和服务器的实现。它是Go语言中进行Web编程的基础,非常轻量级且易于使用。以下是一个服务端的简单示例
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8080", nil)
}
/*
启动成功后,可以在浏览器尝试访问
http://127.0.0.1:8080
看看会得到什么结果
*/
在这个例子中,我们定义了一个处理函数 helloHandler,它响应所有的HTTP请求,并发送一个简单的消息 “Hello, World!”。然后,我们使用 http.HandleFunc
将这个处理函数绑定到根路径 /,并启动服务器监听8080端口。
服务端的一些基本用法
- 创建基本的HTTP服务器
- 使用
http.HandleFunc
函数可以快速设置路由,将特定的路径与处理函数关联起来 - 使用
http.ListenAndServe
启动服务器,它会在指定的地址上监听HTTP请求
- 使用
- 处理HTTP请求
- 处理函数通常有一个形式为
func(w http.ResponseWriter, r *http.Request)
的签名,其中w
是响应写入器,用于发送响应给客户端;r
是请求对象,包含了请求的所有信息
- 处理函数通常有一个形式为
- 读取请求参数
- 可以通过
r.URL.Query()
获取URL中的查询参数 - 通过
r.ParseForm()
或r.FormValue("key")
可以解析和处理表单数据
- 可以通过
- 设置响应头和状态码
- 使用
w.Header().Set("Content-Type", "application/json")
设置响应头 - 使用
w.WriteHeader(http.StatusOK)
设置响应状态码
- 使用
- 响应正文数据处理
- 使用
fmt.Fprintf(w, "Hello, World!")
或w.Write([]byte("Hello, World!"))
发送响应正文。
- 使用
发起HTTP请求
在上面的例子中,我们创建了一个服务端,监听8080端口,来接收HTTP的请求;下面的例子会展示如何发起一个HTTP请求。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// 创建一个请求
req, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// 设置请求头
req.Header.Set("User-Agent", "my-app")
// 发起请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
// 读取响应正文
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
// 打印响应正文
fmt.Println("Response:", string(body))
}
在这个例子中,我们创建了一个GET请求,设置了一些请求头,然后使用 http.Client
发送请求并读取响应正文。
以上例子也被称为客户端,主要用于获取爬虫,数据api调用等,在项目合作开发时十分常用
- 发起GET请求:
- 使用
http.Get
发起一个GET请求,它返回一个响应对象。 - 可以使用
resp.StatusCode
检查状态码,resp.Body
读取响应正文。
- 使用
- 发起POST请求:
- 使用
http.Post
或创建一个http.Request
对象并使用http.DefaultClient.Do(req)
发起POST请求。 - 可以通过
req.Body
设置请求正文。
- 使用
- 自定义HTTP客户端:
- 通过创建
http.Client
对象来自定义HTTP客户端,例如设置超时、代理或其他选项。
- 通过创建
- 处理响应:
- 读取响应正文,通常需要使用
ioutil.ReadAll(resp.Body)
或io.Copy
。
- 读取响应正文,通常需要使用
路由和中间件
路由
路由是指将URL路径与处理函数关联起来的过程,比如"http://127.0.0.1/test",/test
就是路由,通过上面的介绍,我们知道可以使用http.HandleFunc
来设置路由,除此之外,http.Handle
也有同样的功能。示例如下
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// 定义一个结构体
type Message struct {
Message string `json:"message"`
}
// 定义一个处理函数,返回一个简单的JSON
func testHandler(w http.ResponseWriter, r *http.Request) {
// 创建一个Message对象
message := Message{Message: "Hello, World!"}
// 将Message对象转换为JSON格式
jsonData, err := json.Marshal(message)
if err != nil {
http.Error(w, "Error marshaling JSON", http.StatusInternalServerError)
return
}
// 设置响应头
w.Header().Set("Content-Type", "application/json")
// 发送JSON响应
w.Write(jsonData)
}
// 定义一个处理函数,返回一个中断字符串
func stopHandler(w http.ResponseWriter, r *http.Request) {
// 设置响应头
w.Header().Set("Content-Type", "text/plain")
// 发送中断字符串
w.Write([]byte("Stop"))
}
func main() {
// 设置路由
http.HandleFunc("/test", testHandler)
http.HandleFunc("/stop", stopHandler)
// 启动服务器
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
return
}
}
在这个例子中,我们定义了两个处理函数 testHandler
和 stopHandler
。testHandler
函数返回一个简单的JSON对象,而 stopHandler
函数返回一个简单的字符串。然后,我们使用 http.HandleFunc
将这两个处理函数分别绑定到路径 /test
和 /stop
。服务器启动后,访问 http://localhost:8080/test 将返回一个JSON对象,而访问 http://localhost:8080/stop 将返回一个字符串。
中间件
中间件是一个函数,它在请求到达最终处理函数之前或响应发送到客户端之前执行一些操作。可以使用 http.HandlerFunc
来创建中间件,示例
package main
import (
"fmt"
"net/http"
)
// 定义中间件
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Logging request:", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
// 定义处理函数
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
// 设置中间件
http.HandleFunc("/hello", loggingMiddleware(http.HandlerFunc(helloHandler)))
// 启动服务器
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
return
}
}
在这个例子中,我们定义了一个中间件 loggingMiddleware,它会在每个请求到来时打印一条日志。然后,我们将这个中间件应用到我们的处理函数 helloHandler 上,这样每次请求 helloHandler 时都会先经过日志中间件。
模板渲染
模板渲染是Web开发中的一项重要技术,它允许开发者将动态内容与静态模板结合起来,以生成最终的HTML页面,其特点如下
- 代码重用:通过模板,你可以创建可重用的布局和组件,这些布局和组件可以被多个页面共享。这不仅减少了代码的重复,还提高了开发效率。
- 动态内容:模板允许你将动态数据(如数据库查询结果、用户输入等)插入到HTML中。这使得页面可以根据用户请求或上下文动态变化。
- 易用性:模板渲染通常提供简单易用的语法,使得非技术背景的人员也可以参与页面的设计和开发
说明:渲染通常指的是将静态HTML模板与动态数据结合,生成最终的HTML页面的过程
在Go语言中,模板渲染通常使用标准库中的 html/template 包。这个包提供了定义和渲染HTML模板的功能。
创建模板文件
首先,你需要创建一个模板文件,通常以 .tmpl 扩展名结尾。例如,创建一个名为 index.tmpl 的文件,并添加以下内容
<!DOCTYPE html>
<html>
<head>
<title>Go Template Example</title>
</head>
<body>
<h1>{{.Title}}</h1>
<p>{{.Message}}</p>
</body>
</html>
在这个模板中,我们使用了Go模板语法,其中 {{.Title}} 和 {{.Message}} 是模板变量,它们将在渲染时被替换
渲染
接下来,需要创建一个Go程序,并使用 html/template 包来渲染模板
package main
import (
"html/template"
"net/http"
)
// 定义模板变量
type TemplateData struct {
Title string
Message string
}
// 定义处理函数
func indexHandler(w http.ResponseWriter, r *http.Request) {
// 创建模板实例
tmpl, err := template.ParseFiles("index.tmpl")
if err != nil {
http.Error(w, "Error parsing template", http.StatusInternalServerError)
return
}
// 创建模板数据
data := TemplateData{
Title: "Go Template Example",
Message: "Welcome to the Go template example!",
}
// 渲染模板
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Error executing template", http.StatusInternalServerError)
return
}
}
func main() {
// 设置路由
http.HandleFunc("/", indexHandler)
// 启动服务器
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
return
}
}
执行结果如下
会话管理
会话管理通常指的是在Web应用程序中跟踪用户状态的过程。这可以通过多种方式实现,包括使用Cookies、Session、Token等。Go语言的标准库 net/http 提供了基础的会话管理功能,示例如下
package main
import (
"fmt"
"net/http"
"time"
)
// 会话结构体
type Session struct {
UserID int
}
// 初始化会话
func newSession(w http.ResponseWriter, r *http.Request) (*Session, error) {
session := &Session{UserID: 1} // 示例用户ID
// 创建一个Cookie
cookie := http.Cookie{
Name: "session",
Value: session.UserID,
Expires: time.Now().Add(time.Hour * 24), // 有效期为一天
}
http.SetCookie(w, &cookie)
return session, nil
}
// 获取会话
func getSession(w http.ResponseWriter, r *http.Request) (*Session, error) {
cookie, err := r.Cookie("session")
if err != nil {
return nil, err
}
session := &Session{UserID: int(cookie.Value)}
return session, nil
}
// 处理会话的函数
func handleSession(w http.ResponseWriter, r *http.Request) {
// 获取会话
session, err := getSession(w, r)
if err != nil {
http.Error(w, "Error getting session", http.StatusInternalServerError)
return
}
// 处理会话逻辑
fmt.Fprintf(w, "User ID: %d", session.UserID)
}
func main() {
// 设置路由
http.HandleFunc("/session", handleSession)
// 启动服务器
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
return
}
}
在这个例子中,我们定义了一个会话结构体 Session,它包含了一个用户ID。我们使用了Cookies来存储会话信息,并在每次请求中检查Cookie来获取会话。
例子是非常基础的,它没有实现会话的持久化存储、会话超时、会话的安全性等高级功能,要实现上述这些功能,需要引用第三方库来实现,这个我们就后面再介绍了
应用案例
练习1
编写一个简单客户端实现以下功能
- 根路径 / 返回一个简单的HTML页面。
- 路径 /user/:id 返回一个JSON对象,其中包含用户ID。
- 使用中间件记录所有请求的URL
参考答案
package main
import (
"fmt"
"io/ioutil"
"net/http"
"strconv"
"time"
)
// 定义一个模板变量
type TemplateData struct {
Title string
}
// 中间件函数,记录所有请求的URL
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request URL:", r.URL.Path)
next.ServeHTTP(w, r)
})
}
// 处理函数,返回一个简单的HTML页面
func rootHandler(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("template.tmpl")
if err != nil {
http.Error(w, "Error parsing template", http.StatusInternalServerError)
return
}
data := TemplateData{Title: "Welcome to the Go Web Server"}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Error executing template", http.StatusInternalServerError)
return
}
}
// 处理函数,返回一个JSON对象
func userHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "User ID is required", http.StatusBadRequest)
return
}
userID, err := strconv.Atoi(id)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
response := map[string]int{"id": userID}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
http.HandleFunc("/", rootHandler)
http.HandleFunc("/user/", userHandler)
http.Handle("/", loggingMiddleware(http.HandlerFunc(userHandler))) // 应用中间件
// 启动服务器
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server:", err)
return
}
}