通过示例学习 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

}

}

Image

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

}

}

Image

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

}

}

Image

MySQL DataBase

当我们需要加载动态数据的时候,就需要用到数据库,比如说用户的登录与注册。

安装数据库驱动

Go 编程语言附带了一个名为 <code>database/sql 的包来查询各种 SQL 数据库,此外还需要不同数据库的驱动来去获得对数据库的连接。

go get -u github.com/go-sql-driver/mysql

连接数据库

检查我们是否可以连接到数据库,导入 database/sqlgo-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 来在 sessioncookie 中存储数据。

会话定义

Cookie 是存储在用户浏览器中的小数据片段,并根据每个请求发送到我们的服务器。通常用于保存用户的登录状态、偏好设置、跟踪用户行为等等,大小通常为 4KB。

http.SetCookie(w, &http.Cookie{

Name: "username",

Value: "john_doe",

Path: "/",

MaxAge: 3600, // 1 hour

})

Session 是存储在服务器上的数据,用于跟踪用户的回话状态。每个 Session 都有一个唯一的 Session ID,客户端通过 CookieURL 参数将这个 ID 传递给服务器。

session, _ := store.Get(r, "session-name")

session.Values["username"] = "john_doe"

session.Save(r, w)

会话常用函数

session 常用的函数有:

sessions.NewCookieStore():创建新的 Cookie 存储对象,通常传入一个随机字符串作为秘钥来加密和验证 Session 数据。store.Get():获取或创建一个 Sessionsession.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 时,用 SecureHttpOnly 标志,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)

}

Image

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)

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。