通过示例学习 Go 中的 Web 编程
此子不可久留丶 2024-09-17 11:33:05 阅读 69
Hello World
Go 语言标准库中的 <code>net/http 包就包含了有关 HTTP 协议的一些功能。我们将通过该包内函数,去实现一个简单的网络服务器,能够在本地浏览器来访问网页。
注册请求处理程序
首先我们需要创建一个方法来处理 HTTP 请求,这个方法有两个入参,类型如下。
http.ResponseWriter
:我们通过这个接口来去设置 HTTP 响应的内容,这个接口要求实现三个方法。
Header
方法用于设置或获取响应头信息。Write
方法用于将数据写入响应体。WriteHeader
用于设置响应状态码,如果不调用则默认设置为 200 OK。
type ResponseWriter interface{ -- -->
Header() Header
Write([]byte)(int, error)
WriteHeader(statusCode int)
}
http.Request
:Request 表示服务器接收到的或客户端发送的 HTTP 请求,包含了请求中的字段,如 Method、URL、Header(http.Header)、Body(io.ReadCLoser)、ContentLength、Host、Form、PostForm、MultipartForm、RemoteAddr、RequestURI。
FormValue
方法可以获取表单中指定键的值。PostForm
方法可以获取 POST 表单中指定键的值。FormFile
方法可以获取上传的文件。ParseForm
方法解析 URL 查询参数和 POST 表单数据。ParseMultiparForm
方法解析多部分表单数据。
type Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{ }
Response *Response
Pattern string
}
我们用 http.HandleFunc
方法把匹配路径的请求处理注册到服务器上。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
if err != nil {
return
}
})
监听客户端发送的请求
我们注册好对应的处理方法后,需要打开监听器,对服务器暴露的端口进行监听。
http.ListenAndServe
:该方法会监听 TCP 网络地址,然后用 handler 方法调用服务器来去处理请求,如果 handler 方法为 nil 则会自动在服务器查询已经注册的处理函数。
完整代码示例如下:
package main
import (
"fmt"
"net/http")
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
if err != nil {
return
}
})
err := http.ListenAndServe(":8080", nil)
if err != nil {
return
}
}
HTTP Server
这里我们将要搭建一个 HTTP 服务器,它需要处理这么几个任务。
<code>Process dynamic requets:处理来自用户的请求。Serve static assets
:给用户提供静态资源。Accept connections
:监听指定端口接收请求。
处理动态请求
在上面的内容我们其实已经介绍了如何处理来自用户的请求,主要是通过 http.HandleFunc
方法在服务器中注册匹配项的处理。对于用户发送的请求,我们可以用 r.URL.Query().Get("token")
或 r.FormValue("email")
来获取参数进行处理。
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request){ -- -->
fmt.Fprint(w, "Welcome to my website!")
})
服务器静态资源
我们可以通过内置的 http.FileServer
来指向静态资源的路径,该方法会返回一个实现了 http.Handler
接口的处理器,可以将其挂载到指定的路径上。
这里,我们通过 http.Dir
将指定目录 static
作为根目录创建文件服务器
fs := http.FileServer(http.Dir("static/"))
接下来,我们就将该文件系统挂载到 /static/
路径上,并且通过 http.StripPrefix
方法去除了根目录前缀。假设,我们 static
目录中有一个文件 example.txt
内容为 This is an example file.
那么我们访问 http://localhost/static/example.txt
时,服务器会去除 /static/
前缀,询问文件服务器获取 example.txt
,那么查找返回的就是 static/example.txt
文件的内容,如果不去掉就会查找 static/static/example.txt
了。
http.Handle("/static/", http.StripPrefix("/static/", fs))
监听请求
与我们上个案例介绍的一样,通过 http.ListenAndServe
监听端口。
完整代码如下:
package main
import (
"fmt"
"net/http")
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
if err != nil {
return
}
})
fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
err := http.ListenAndServe(":8080", nil)
if err != nil {
return
}
}
Routing
我们已经使用过 Go 语言提供的一些关于 HTTP 协议的方法了。但是我们会发现,对于 URL 多参数的划分处理其实并不太方面。
那么,这里我们会用 <code>gorilla/mux 来去路由。
安装路由器包
gorilla/mux
是一个适应 Go 默认 HTTP 路由器的包,它提供了许多方法来提高编写 Web 应用程序的效率。
go get -u github.com/gorilla/mux
创建新的路由器
路由器是 Web 应用程序的主路由器,稍后将作为参数传递给服务器,它负责接收所有 HTTP 链接并传递给注册的处理程序。
r := mux.NewRouter()
注册请求处理函数
与之前的区别是,因为我们使用 gorilla/mux
包,所以要用 r.HandleFunc
方法来去注册到路由器上。
资源定位参数
举个例子,在我们的应用有这样一条 URL:
/books/go-programming-blueprint/page/10
Book title
:go-programming-blueprint;Page
:10。
这两个字段都是动态字段,在注册处理函数的时候,匹配 URL 模式中要替换为占位符,我们再通过 mux.Vars
方法来获取参数。
r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request){ -- -->
vars := mux.Vars(r)
vars["title"] // the book title slug
vars["page"] // the page
})
设置服务器路由器
在我们之前的例子中,监听器都是没有设置的路由,也就是第二个参数为 nil
,此时会自动检索默认的服务器。现在,我们有了路由器就可以设置了。
http.ListenServe(":8080", r)
完整代码如下:
package main
import (
"fmt"
"github.com/gorilla/mux" "net/http")
func main() {
r := mux.NewRouter()
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
if err != nil {
return
}
})
r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
title := vars["title"]
page := vars["page"]
_, err := fmt.Fprintf(w, "You've requested the book: %s on page %s\n", title, page)
if err != nil {
return
}
})
err := http.ListenAndServe(":8080", r)
if err != nil {
return
}
}
MySQL DataBase
当我们需要加载动态数据的时候,就需要用到数据库,比如说用户的登录与注册。
安装数据库驱动
Go 编程语言附带了一个名为 <code>database/sql 的包来查询各种 SQL 数据库,此外还需要不同数据库的驱动来去获得对数据库的连接。
go get -u github.com/go-sql-driver/mysql
连接数据库
检查我们是否可以连接到数据库,导入 database/sql
和 go-sql-driver/mysql
包并打开一个连接,
import "database/sql"
import _ "go-sql-driver/mysql"
db, err := sql.Open("mysql", "username:password@(127.0.0.1:3306)/dbname?parseTime=true")
err := db.Ping()
创建数据库表
假设我们要创建如下的表,就要编写如下的 SQL 语句。
id | username | password | created_at |
---|---|---|---|
1 | johndoe | secret | 2019-08-10 12:30:00 |
CREATE TABLE users(
id INT AUTO_INCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
create_at DATETIME,
PRIMARY KEY (id)
);
现在我们在 GO 语言中,通过 database/sql
包来去执行 SQL 语句。
query := `
CREATE TABLE users IF NOT EXIST(
id INT AUTO_INCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
created_at DATETIME,
PRIMARY KEY (id)
);`
_, err := db.Exec(query)
插入数据到表
问号告诉 SQL 驱动程序,它们是实际数据的占位符。
INSERT INTO users(username, password, created_at) VALUES(?, ?, ?)
现在,我们在 Go 中往数据库中插入数据。
import "time"
username := "Yikuanzz"
password := "secret"
createdAt := time.Now()
result, err := db.Exec(
`INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)`,
username, password, createdAt
)
获取最新的用户 ID,可以通过下面的方法。
userID, err := result.LastInsertId()
查询数据库表
通过 db.Query
可以进查询出多行数据,db.QueryRow
可以查询特定的行。
SELECT id, username, password, created_at FROM users WHERE id = ?
在 GO 中我们可以声明一些变量来存储要查询出的数据。
var (
id int
username string
password string
createdAt time.time
)
query := `SELECT id, username, password, created_at FROM user WHERE id = ?`
err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt)
查询所有用户数据
我们通过下面的 SQL 语句来查询全部用户的数据。
SELECT id, username, password, created_at FROM users
不同的是,我们可以把实体的属性写到结构体中,然后迭代返回用户信息。
type user struct { -- -->
id int
username string
password string
createdAt time.Time
}
rows, err := db.Query(`SELECT id, username, password, created_at FROM users`)
defer rows.Close()
var users []user
for rows.Next() {
var u user
err := rows.Scan(&u.id, &u.username, &u.password, &u.createdAt)
users = append(users, u)
}
err := rows.Err()
从表中删除数据
与前面执行的 SQL 语句没有什么区别。
_, err := db.Exec(`DELETE FROM users WHRE id = ?`, 1)
完整代码如下:
package main
import (
"database/sql"
"fmt" _ "github.com/go-sql-driver/mysql"
"log" "time")
func main() {
var queryID int64
db, err := sql.Open("mysql", "user:password@(127.0.0.1:3306)/dbname?parseTime=true")
if err != nil {
log.Fatal(err)
}
{
// Create a new Table
query := `
CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT, username TEXT NOT NULL, password TEXT NOT NULL, created_at DATETIME, PRIMARY KEY (id) );`
if _, err := db.Exec(query); err != nil {
log.Fatal(err)
}
}
{
// Insert a new user
username := "Yikuanzz"
password := "secret"
createAt := time.Now()
res, err := db.Exec(
`INSERT INTO users (username, password, created_at) VALUES ( ?, ?, ?)`,
username, password, createAt)
if err != nil {
log.Fatal(err)
}
id, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
} else {
queryID = id
fmt.Printf("id: %d\n", id)
}
}
{
// Query a single user
var (
id int
username string
password string
createAt time.Time
)
query := "SELECT id, username, password, created_at FROM users WHERE id = ?"
if err := db.QueryRow(query, queryID).Scan(&id, &username, &password, &createAt); err != nil {
log.Fatal(err)
} else {
fmt.Println(id, username, password, createAt)
}
}
{
// Query all users
type User struct {
id int
username string
password string
createAt time.Time
}
rows, err := db.Query("SELECT id, username, password, created_at FROM users")
if err != nil {
log.Fatal(err)
}
defer func(rows *sql.Rows) {
err := rows.Close()
if err != nil {
log.Fatal(err)
}
}(rows)
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.id, &user.username, &user.password, &user.createAt); err != nil {
log.Fatal(err)
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", users)
}
{
// Delete data
_, err := db.Exec(`DELETE FROM users WHERE id = ?`, 1)
if err != nil {
log.Fatal(err)
}
}
}
Templates
GO 语言的 html/template
包提供了丰富的 HTML 模版,能够让我们方便地在 Web 应用中展示数据。
第一个模板
我们写一个 TODO List 用 HTML 的 <ul>
,渲染模版时,传入的数据可以是任意的数据结构。我们用 { {.}}
来访问模版上的数据,其中的 .
是数据的根元素。
data := TodoPageData{
PageTitle: "My TODO list",
Todos: []Todo{
{ Title: "Task 1", Done: false},
{ Title: "Task 2", Done: true},
{ Title: "Task 3", Done: true},
},
}
<h1>{ {.PageTitle}}</h1>
<ul>
{ {range .Todos}}
{ {if .Done}}
<li class="done">{ -- -->{.Title}}</li>code>
{ -- -->{else}}
<li>{ {.Title}}</li>
{ {end}}
{ {end}}
</ul>
控制结构
模版语言包含丰富的控制结构来渲染 HTML。
Control Structure | Definition |
---|---|
{ {/* a comment */}} | 定义注释 |
{ {.}} | 渲染根元素 |
{ {.Title}} | 在嵌套元素中渲染标题字段 |
{ {if .Done}} { {else}} { {end}} | 定义条件语句 |
{ {range .Todos}} { {.}} { {end}} | 循环所有的 “Todos” 并用 { {.}} 渲染每个 “Todos” |
{ {block "content" .}} { {end}} | 定义名为 “content” 的块 |
从文件中解析模版
模版可以从字符串或磁盘上的文件中解析,其中 template.Must
方法是用来确保模板解析过程中没有错误,如果有错误会触发 panic
。
tmpl, err := template.ParseFiles("layout.html")
// or
tmp := template.Must(template.ParseFiles("layout.html"))
请求处理函数中执行模板
Execute
函数接收一个 io.Writer
来去写入模版,通过接口将数据传入模板,此时方法会调用 http.ResponseWriter
进行写入,也自动在 HTTP 响应中写入 Content-Type: text/html;charset=utf-8
。
func (w http.ResponseWrtier, r *http.Request){
tmpl.Execute(w, "data goes here")
}
完整代码如下:
// main.go
package main
import (
"html/template"
"net/http")
type Todo struct {
Title string
Done bool
}
type TodoPageDate struct {
PageTitle string
Todos []Todo
}
func main() {
tmpl := template.Must(template.ParseFiles("./static/layout.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := TodoPageDate{
PageTitle: "My TODO list",
Todos: []Todo{
{ Title: "Task 1", Done: false},
{ Title: "Task 2", Done: true},
{ Title: "Task 3", Done: true},
},
}
err := tmpl.Execute(w, data)
if err != nil {
return
}
})
err := http.ListenAndServe(":8080", nil)
if err != nil {
return
}
}
<!-- layout.html -->
<h1>{ {.PageTitle}}</h1>
<ul>
{ {range .Todos}}
{ {if .Done}}
<li class="done">{ -- -->{.Title}}</li> code>
{ -- -->{else}}
<li>{ {.Title}}</li>
{ {end}}
{ {end}}
</ul>
Assets and Form
这里我们会展示静态资源的渲染和表单的信息传递。
静态资源
比如,我们将资源给到 assets
目录下,也可以将其处理与 static
进行匹配。
// static-files.go
package main
import "net/http"
func main() {
fs := http.FileServer(http.Dir("assets/"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.ListenAndServe(":8080", nil)
}
$ tree assets/
assets/
└── css
└── styles.css
$ go run static-files.go
$ curl -s http://localhost:8080/static/css/styles.css
body {
background-color: black;
}
表单提交
如果客户端发送的是 request
请求,那么就渲染表单模板,而当发送的是 post
请求,就把数据接收,并返回成功。
// forms.go
package main
import (
"html/template"
"net/http"
)
type ContactDetails struct {
Email string
Subject string
Message string
}
func main() {
tmpl := template.Must(template.ParseFiles("forms.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
tmpl.Execute(w, nil)
return
}
details := ContactDetails{
Email: r.FormValue("email"),
Subject: r.FormValue("subject"),
Message: r.FormValue("message"),
}
// do something with details
_ = details
tmpl.Execute(w, struct{ Success bool }{ true})
})
http.ListenAndServe(":8080", nil)
}
<!-- forms.html -->
{ {if .Success}}
<h1>Thanks for your message!</h1>
{ {else}}
<h1>Contact</h1>
<form method="POST">code>
<label>Email:</label><br />
<input type="text" name="email"><br />code>
<label>Subject:</label><br />
<input type="text" name="subject"><br />code>
<label>Message:</label><br />
<textarea name="message"></textarea><br />code>
<input type="submit">code>
</form>
{ -- -->{end}}
Middleware
这里我们,创建一个基础的日志中间件,它将 http.HandlerFunc
作为其参数,包装返回一个新的 http.HandlerFunc
给服务器调用。
简单的中间件
通过简单的封装之后,我们就可以记录每次的访问路径情况了,以此为拓展,我们还可以记录很多其他信息。
// basic-middleware.go
package main
import (
"fmt"
"log"
"net/http"
)
func logging(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL.Path)
f(w, r)
}
}
func foo(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "foo")
}
func bar(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "bar")
}
func main() {
http.HandleFunc("/foo", logging(foo))
http.HandleFunc("/bar", logging(bar))
http.ListenAndServe(":8080", nil)
}
中间件进阶
在这里,我们定义了一种新类型的中间件,它可以轻松地将多个中间件链接在一起。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
type Middleware func(http.HandlerFunc) http.HandlerFunc
func Logging() Middleware{
// 创建一个新的中间件
return func(f http.HandlerFunc) http.HandlerFunc{
// 定义处理函数
return func(w http.ResponseWriter, r *http.Request){
start := time.Now()
defer func(){
log.Println(r.URL.Path, time.Since(start))
}
f(w, r)
}
}
}
func Method(m string) Middleware{
// 创建新的中间件
return func(f http.HandlerFunc) http.HandlerFunc{
// 定义处理函数
return func(w http.ResponseWriter, r *http.Request){
if r.Method != m{
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
f(w, r)
}
}
}
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc{
for _, m := range middlewares{
f = m(f)
}
return f
}
func Hello(w http.ResponseWriter, r *http.Request){
fmt.Fprintln(w, "Hello World")
}
func main(){
http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
http.ListenAndServe(":8080", nil)
}
Sessions
这里我们一起看看怎么通过 gorilla/sessions
来在 session
和 cookie
中存储数据。
会话定义
Cookie
是存储在用户浏览器中的小数据片段,并根据每个请求发送到我们的服务器。通常用于保存用户的登录状态、偏好设置、跟踪用户行为等等,大小通常为 4KB。
http.SetCookie(w, &http.Cookie{
Name: "username",
Value: "john_doe",
Path: "/",
MaxAge: 3600, // 1 hour
})
Session
是存储在服务器上的数据,用于跟踪用户的回话状态。每个 Session
都有一个唯一的 Session ID
,客户端通过 Cookie
或 URL
参数将这个 ID 传递给服务器。
session, _ := store.Get(r, "session-name")
session.Values["username"] = "john_doe"
session.Save(r, w)
会话常用函数
session
常用的函数有:
sessions.NewCookieStore()
:创建新的 Cookie 存储对象,通常传入一个随机字符串作为秘钥来加密和验证 Session
数据。store.Get()
:获取或创建一个 Session
。session.Save()
:保存 Session
数据。session.Values
:用于存储和获取 Session
中的数据。session.Options.MaxAge
:设置 Session
的最大存储时间,负值表示删除 Session
。
一般来说,当客户端发送请求的的时候,我们用 store.Get
检查请求中的 Cookie 看看是否有名为 session-name
的 Cookie。如果没有我们就在程序内存中创建一个新的 Session 并且它具有唯一的 Session ID。然后,在我们完成一些数据操作之后,我们会用 store.Save
来把 Session 存到内存上或者持久化,并在给客户端的响应中设置一个 Cookie,包含这个 Session ID。这个 Cookie 会在后续请求中被客户端发送回服务器,以便服务器识别会话。
此外,可能要注意的问题是会话劫持(Session Hijacking)的安全风险,就是说攻击者截获合法用户的 session ID
并利用它冒充用户访问服务器资源。为了防止这种情况,我们可采取一下安全措施:
用 HTTPS 加密通信,防止 session ID
在传输过程中被截获,确保所有的请求和响应都通过 HTTPS 进行传输。在设置 Cookie 时,用 Secure
和 HttpOnly
标志,Secure
可以确保 Cookie 只能通过 HTTPS 传输,而 HttpOnly
可以防止 JavaScript 访问 Cookie,减少 XSS 攻击风险。
package main
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
var (
// key must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256)
key = []byte("super-secret-key")
store = session.NewCookieStore(key)
)
func secret(w http.ResponseWriter, r *http.Request){
session, _ := store.Get(r, "cookie-name")
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
fmt.Fprintln(w, "The cake is a lie!")
}
func login(w http.ResponseWriter, r *http.Request){
session, _ := store.Get(r, "cookie-name")
session.Value["authenticated"] = true
session.Save(r, w)
}
func logout(w http.ResponseWriter, r *http.Request){
session, _ := store.Get(r, "cookie-name")
session.Values["authenticated"] = false
session.Save(r, w)
}
func main(){
http.HandleFunc("/secret", sercet)
http.HandleFunc("/login", login)
http.HandleFunc("/logout", logout)
http.ListenAndServe(":8080", nil)
}
JSON
这里会展示的是如何用 encoding/json
来编解码 JSON 数据。
编码序列化
json.Marshal
将 Go 结构体编码为 JSON 字符串,我们会用字符标识在结构体字段中标注要转换成的 JSON 名。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
user := User{ Name: "Alice", Age: 30}
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(jsonData))
}
解码反序列化
json.Unmarshal
将 JSON 字符串解码为 GO 结构体。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
jsonString := `{"name":"Alice","age":30}`
var user User
err := json.Unmarshal([]byte(jsonString), &user)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(user)
}
编码和解码使用
// json.go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type User struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Age int `json:"age"`
}
func main() {
http.HandleFunc("/decode", func(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
fmt.Fprintf(w, "%s %s is %d years old!", user.Firstname, user.Lastname, user.Age)
})
http.HandleFunc("/encode", func(w http.ResponseWriter, r *http.Request) {
peter := User{
Firstname: "John",
Lastname: "Doe",
Age: 25,
}
json.NewEncoder(w).Encode(peter)
})
http.ListenAndServe(":8080", nil)
}
Websockets
我们将构建一个简单的服务器,它会回显我们发送给它的所有内容。
安装网络套接字包
<code>go get -u github.com/gorilla/websocket
网络套接字
WebSocket
是一种在单个 TCP 连接上进行全双工通信的协议。
websocket.Upgrader
用于将 HTTP 连接升级为 WebStocket 连接,它提供了一些配置选项来控制升级过程。
HandshakeTimeout:握手完成的超时时间。ReadBufferSize:读取缓冲区的大小(以字节为单位)。WriteBufferSize:写入缓冲区的大小(以字节为单位)。WriteBufferPool:用于写入缓冲区的池。Subprotocols:客户端支持的子协议列表。Error:在握手失败时调用的函数。CheckOrigin:用于检查请求来源的函数,防止跨站点请求伪造(CSRF)攻击。EnableCompression:是否启用 WebSocket 压缩扩展。
type Upgrader struct { -- -->
HandshakeTimeout time.Duration
ReadBufferSize int
WriteBufferSize int
WriteBufferPool BufferPool
Subprotocols []string
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
CheckOrigin func(r *http.Request) bool
EnableCompression bool
}
websocket.Upgrader.Upgrage
是 WebSocket 服务器实现的核心部分,它传入 HTTP 的请求和响应,可选 http.Header
设置额外的头部信息,返回一个 *Conn
WebSocket 连接对象。
*wensocket.Conn
类型常用的方法:
ReadMessage() (messageType int, p []byte, err error)
:读取消息,返回小李类型、消息内容和错误信息。WriteMessage(messageType int, data []byte) error
:发送消息,指定消息类型和消息内容。Close() error
:关闭连接。SetReadDeadline(t time.Timer) error
:设置读取消息的超时时间。SetWriteDeadline(t time.Timer) error
:设置写入消息的超时时间。
回显服务器
// websockets.go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil) // error ignored for sake of simplicity
for {
// Read message from browser
msgType, msg, err := conn.ReadMessage()
if err != nil {
return
}
// Print the message to the console
fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))
// Write message back to browser
if err = conn.WriteMessage(msgType, msg); err != nil {
return
}
}
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "websockets.html")
})
http.ListenAndServe(":8080", nil)
}
<!-- websockets.html -->
<input id="input" type="text" />code>
<button onclick="send()">Send</button>code>
<pre id="output"></pre>code>
<script>
var input = document.getElementById("input");
var output = document.getElementById("output");
var socket = new WebSocket("ws://localhost:8080/echo");
socket.onopen = function () { -- -->
output.innerHTML += "Status: Connected\n";
};
socket.onmessage = function (e) {
output.innerHTML += "Server: " + e.data + "\n";
};
function send() {
socket.send(input.value);
input.value = "";
}
</script>
Password Hashing
在这个示例中,我们会用 bcrypt
来对密码进行哈希处理。
go get -u golang.org/x/crypto/bcrypt
密码哈希算法
在 Go 中,bcrypt
是一种常用的密码哈希算法,用于安全地存储和验证密码。
安全性高:使用了随机盐值和多次哈希,增加了破解难度。适应性强:通过增加迭代次数提高安全性,适应不断提升的计算能力。不可逆:单向哈希,无法通过哈希值反推出原始密码。
bcrypt.GenerateFromPassword
方法用于生成哈希密码。
bcrypt.CompareHashAndPassword
方法用于验证用户输入的密码是否正确。
package utils
import (
"golang.org/x/crypto/bcrypt"
)
// GenerateFromPassword 生成哈希密码
func GenerateFromPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedPassword), nil
}
// CompareHashAndPassword 验证密码
func CompareHashAndPassword(hashPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(password))
return err == nil
}
加密和解密
// passwords.go
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func main() {
password := "secret"
hash, _ := HashPassword(password) // ignore error for the sake of simplicity
fmt.Println("Password:", password)
fmt.Println("Hash: ", hash)
match := CheckPasswordHash(password, hash)
fmt.Println("Match: ", match)
}
上一篇: golang学习笔记10——golang 的 Gin 框架,快速构建高效 Web 应用
下一篇: 从零开始:我的JaveWeb学习笔记之网页设计初探:探索 HTML 、CSS的奇妙世界(JavaWeb、HTML、CSS)
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。