前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 使用标准库 net/http 包构建服务器

Go 使用标准库 net/http 包构建服务器

作者头像
frank.
发布2020-09-27 11:28:46
1.3K0
发布2020-09-27 11:28:46
举报

01

概念

在 Go 语言中,使用标准库 net/http 可以很方便的构建服务器,只要调用 ListenAndServe 函数,并传入参数IP地址与端口组成的字符串和处理器(handler)即可。

代码语言:javascript
复制
func ListenAndServe(addr string, handler Handler) error

如果 IP 地址与端口组成的字符串参数为空字符串,那么服务器默认使用 80 端口进行网络连接,如果处理器(handler)参数为 nil,那么服务器将使用默认多路复用器 DefaultServeMux。

02

构建服务器

细心的读者可能会说,服务器配置信息除了 IP 地址和端口之外,还有很多其它配置信息,应该怎么配置给服务器呢?

Go 语言为我们提供了一个结构体 Server,其中包含了很多对服务器的其它配置,结构体 Server 的完整代码如下:

代码语言:javascript
复制
type Server struct {
    Addr              string
    Handler           Handler
    TLSConfig         *tls.Config
    ReadTimeout       time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout      time.Duration
    IdleTimeout       time.Duration
    MaxHeaderBytes    int
    TLSNextProto      map[string]func(*Server, *tls.Conn, Handler)
    ConnState         func(net.Conn, ConnState)
    ErrorLog          *log.Logger
    BaseContext       func(net.Listener) context.Context
    ConnContext       func(ctx context.Context, c net.Conn) context.Context
    inShutdown        atomicBool
    disableKeepAlives int32
    nextProtoOnce     sync.Once
    nextProtoErr      error
    mu                sync.Mutex
    listeners         map[*net.Listener]struct{}
    activeConn        map[*conn]struct{}
    doneChan          chan struct{}
    onShutdown        []func()
}

使用结构体 Server 构建服务器:

代码语言:javascript
复制
server := http.Server{
    Addr: ":8080",
    Handler: nil,
}
server.ListenAndServe()

03

接收 HTTP 请求

在 Go 语言中,一个处理器就是一个拥有 ServeHTTP 方法的接口,这个 ServeHTTP 方法需要接收两个参数,第一个参数是一个 ResponseWriter 接口,第二个参数是一个指向 Request 结构的指针。

DefaultServeMux 默认多路复用器是多路复用器 ServeMux 结构的一个实例,ServeMux 也拥有 ServeHTTP 方法。

所以 DefaultServeMux 既是 ServeMux 结构的实例,也是处理器 Handler 结构的实例,因此 DefaultServeMux 不仅是一个多路复用器,还是一个处理器。但是 DefaultServeMux 是一个特殊的处理器,它唯一要做的就是根据请求的 URL 将请求重定向到不同的处理器。

自定义一个处理器,替代 DefaultServeMux。

代码语言:javascript
复制
type MyHandler struct {

}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello World!")
}

使用自定义处理器,配置服务器。

代码语言:javascript
复制
handler := MyHandler{}
server := http.Server{
    Addr: "127.0.0.1:8080",
    Handler: &handler,
}
server.ListenAndServe()

细心的读者可能已经发现,使用自定义的处理器与服务器进行绑定,启动服务器,不管浏览器访问什么地址,服务器返回的都是同样的响应 Hello World!

这是因为使用自定义的处理器替代了默认多路复用器 DefaultServeMux,服务器不会再通过 URL 匹配来将请求路由至不同的处理器。

怎么解决这个问题呢?

使用多个处理器。使用 http 包的 Handle 函数绑定到 DefaultServeMux。

为了使用多个处理器去处理不同的 URL,我们不再在 Serve 结构

的 Handler 字段中指定处理器,而是让服务器使用默认多路复用器 DefaultServeMux,

然后通过 http.Handle 函数将处理器绑定到 DefaultServeMux。http 包的 Handle 函数实际上是 ServeMux 结构的方法,为了操作便利而创建的函数,调用它们等同于调用 DefaultServeMux 的某个方法。

例如,调用 http.Handle,实际上就是在调用 DefaultServeMux 的 Handle 方法。

示例代码:

编写多个处理器,处理请求

代码语言:javascript
复制
type FirstHandler struct {

}

func (f *FirstHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "FirstHandler")
}

type SecondHandler struct {

}

func (s *SecondHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "SecondHandler")
}

构建服务器:

代码语言:javascript
复制
first := FirstHandler{}
second := SecondHandler{}

server := http.Server{
    Addr: "127.0.0.1:8080",
}

// http.Handle 函数
http.Handle("/first", &first)
http.Handle("/second", &second)

server.ListenAndServe()

以上我们通过使用 http.Handle 函数,将一个创建的处理器绑定到一个 URL 上,实现使用多个处理器处理不同的 URL。

现在,可能有读者会说,创建多个处理器来处理多个请求,这也太不优雅了,有没有其它方式呢?

先告诉大家答案,有其它方式,使用处理器函数。

http.HandleFunc 函数将自定义函数转换成一个处理器 Handler,并将它与 DefaultServeMux 进行绑定,从而简化创建并绑定 Handler 的工作。

示例代码:

编写多个函数,处理请求

代码语言:javascript
复制
func first(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "func first")
}

func second(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "func second")
}

构建服务器:

代码语言:javascript
复制
server := http.Server{
    Addr: "127.0.0.1:8080",
}

// http.HandleFunc
http.HandleFunc("/first", first)
http.HandleFunc("/second", second)

server.ListenAndServe()

使用处理器函数和使用处理器,都可以实现根据请求的 URL 将请求重定向到不同的处理器,而且处理器函数比处理器的代码更为简洁。

但是也不是完全使用处理器函数代替处理器,因为如果代码已经包含了某个接口或某种类型,我们只需为它们添加 ServeHTTP 方法就可以将它们转变为处理器。

ServeMux 无法使用变量实现 URL 模式匹配,使用三方多路复用器 httprouter 包可以实现 URL 模式匹配。此外,还有一个非常优秀的三方多路复用器,gorilla/mux。篇幅限制,这里就不展开了。

04

处理 HTTP 请求

客户端和服务器端传递的消息,我们称之为 HTTP 报文,有两种类型,分别是 HTTP 请求和 HTTP 响应,并且这两种类型的结构相同。

  1. 请求行/响应行
  2. 零个/多个首部
  3. 一个空行
  4. 一个可选的报文主体

在 Go 语言中,标准库 net/http 提供了一系列用于表示 HTTP 报文的结构体。其中,Request 结构体代表 HTTP 请求报文。

Request 结构体的组成部分:

  • URL 字段
  • Header 字段
  • Body 字段
  • Form 字段、PostForm 字段和 MultipartForm 字段

请求/响应的首部都是使用 Header 类型描述,Header 类型使用一个 map 来表示 HTTP 首部中的多个键值对。Header 类型有 4 个基本方法,这些方法可以根据给定的键执行添加、删除、获取和设置等操作。

获取请求首部的示例代码:

代码语言:javascript
复制
func headers(w http.ResponseWriter, r *http.Request) {
  h := r.Header
  fmt.Fprintln(w, h)
  h2 := r.Header["User-Agent"]
  fmt.Fprintln(w, h2)
  h3 := r.Header.Get("User-Agent")
  fmt.Fprintln(w, h3)
}

构建服务器:

代码语言:javascript
复制
server := http.Server{
    Addr: "127.0.0.1:8080",
}
http.HandleFunc("/headers", headers)
server.ListenAndServe()

代码中,通过 r.Header 获取所有首部,通过 r.Header["key"] 获取指定的首部,直接引用 Header 获取的是一个字符串切片,如果我们需要获取字符串格式的首部值,可以使用 r.Header.Get("key") 方法。

请求/响应的主体都是用 Request 结构体的 Body 字段表示,这个字段是一个io.ReadCloser 接口,该接口即包含了 Reader 接口,也包含了 Closer 接口。其中 Reader 接口有 Read 方法,该方法接收一个字节切片参数,返回一个被读取内容的字节数和一个可选的错误。

获取请求主体中的数据的代码:

代码语言:javascript
复制
func body(w http.ResponseWriter, r *http.Request) {
  len := r.ContentLength
  body := make([]byte, len)
  r.Body.Read(body)
  fmt.Fprintln(w, string(body))
}

构建服务器:

代码语言:javascript
复制
server := http.Server{
    Addr: "127.0.0.1:8080",
}
http.HandleFunc("/body", body)
server.ListenAndServe()

代码中,通过 r.ContentLength 方法获取主体数据的字节长度,然后根据字节长度创建一个字节数组,然后调用 Read 方法将主体数据读取到字节数组中。

可能有的读者朋友们开始抱怨了,这也太麻烦了。别担心,Go 语言标准库net/http 提供了相关函数来满足用户对数据提取方面的需求,通过调用 Request 结构体提供的方法,可以将 URL、主体的数据提取到该结构体的 Form、PostForm 和 MultipartForm 等字段中。

示例代码:

使用 Request 结构体提供的方法提取数据(enctype 属性的值为application/x-www-form-urlencoded):

代码语言:javascript
复制
func getVal(w http.ResponseWriter, r *http.Request) {
  // 语法分析
  r.ParseForm()
  fmt.Fprintln(w, r.Form)
  fmt.Fprintln(w, r.PostForm)
  fmt.Fprintln(w, r.FormValue("username")) // 只获取第一个值
  fmt.Fprintln(w, r.PostFormValue("username")) // 只获取 form 表单值
}

使用 Request 结构体提供的方法提取数据(enctype 属性的值为multipart/form-data):

代码语言:javascript
复制
func getMultipart(w http.ResponseWriter, r *http.Request) {
  // 语法分析
  r.ParseMultipartForm(1024)
  fmt.Fprintln(w, r.Form)
  fmt.Fprintln(w, r.PostForm) // 只取表单值,不取 URL 值
  fmt.Fprintln(w, r.MultipartForm)
  fmt.Fprintln(w, r.FormValue("username")) // 只取 URL 值
  fmt.Fprintln(w, r.PostFormValue("username")) // 只取 form 表单值
}

构建服务器:

代码语言:javascript
复制
server := http.Server{
    Addr: "127.0.0.1:8080",
}
http.HandleFunc("/getVal", getVal)
http.HandleFunc("/getMultipart", getMultipart)
server.ListenAndServe()

使用 Request 结构的方法获取表单数据:

1. 调用 ParseForm 方法或者 ParseMultipartForm 方法,对请求进行语法分析。

2. 取值:

r.Form,map 类型,键是字符串,值是字符串切片。

如果键同时存在表单和 URL,值包含表单值和 URL 值,并且表单值排在前面。

r.PostForm,如果键同时存在表单和 URL,只取要表单的值。

只支持 application/x-www-form-urlencoded 编码。

r.MultipartForm,支持 multipart/form-data 编码。

只取表单的值,不取 URL 的值。

上面提到的几个方法,可能有些读者朋友感觉比较繁琐,别担心,Request 结构体还提供了另外一些方法,FormValue 和 PostFormValue,它们可以让用户更容易地获取表单中的键值对。

FormValue 方法直接获取指定键的值,不需要在之前调用语法分析的方法。

如果键同时存在表单和 URL,只取表单的值。

PostFormValue 方法只会取表单的值,不取 URL 的值。

05

给客户端发送响应

处理器通过 ResponseWriter 接口创建 HTTP 响应。ResponseWriter 接口有以下 3 个方法:

  • Writer
  • WriterHeader
  • Header

示例代码:

写主体:

代码语言:javascript
复制
func setVal(w http.ResponseWriter, r *http.Request) {
  str := "Hello World!"
  w.WriteHeader(501) // 设置响应返回的状态码,必须在 Write 方法之前调用。
  w.Write([]byte(str)) // 写入响应主体
}

写首部:

代码语言:javascript
复制
func setHeader(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Location", "https://www.baidu.com")
  w.WriteHeader(302)
}

构建服务器:

代码语言:javascript
复制
server := http.Server{
    Addr: "127.0.0.1:8080",
}
http.HandleFunc("/setVal", setVal)
http.HandleFunc("/setHeader", setHeader)
server.ListenAndServe()

需要注意的是,WriteHeader 方法执行完,不能再对首部写入,所以要提前对首部写入。

06

Cookie

关于 Cookie 本身的内容,可以阅读我们之前的一篇文章Gin 学习之 cookie 读写

本篇文章,我们只演示一些如何使用标准库 net/http 操作 cookie,包括写 cookie、读 cookie 和删 cookie。

在 Go 语言中,使用 Cookie 结构体表示 cookie。Cookie 结构体完整字段:

代码语言:javascript
复制
type Cookie struct {
    Name       string
    Value      string
    Path       string
    Domain     string
    Expires    time.Time
    RawExpires string
    MaxAge     int
    Secure     bool
    HttpOnly   bool
    SameSite   SameSite
    Raw        string
    Unparsed   []string
}

通过代码,我们演示如何使用标准库 net/http 操作 cookie。

示例代码:

操作 cookie:

代码语言:javascript
复制
// 写 Cooie
func setCookie(w http.ResponseWriter, r *http.Request) {
  c1 := http.Cookie{
    Name:  "c1",
    Value: "val1",
  }

  c2 := http.Cookie{
    Name:  "c2",
    Value: "val2",
  }

  c3 := http.Cookie{
    Name:  "c3",
    Value: "val3",
  }
  w.Header().Set("Set-Cookie", c1.String())

  w.Header().Add("Set-Cookie", c2.String())

  http.SetCookie(w, &c3) // 指针类型
}

// 读 Cookie
func getCookie(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, r.Header["Cookie"])
  c1, _ := r.Cookie("c1")
  fmt.Fprintln(w, c1)
  fmt.Fprintln(w, r.Cookies())
}

// 删 Cookie
func delCookie(w http.ResponseWriter, r *http.Request) {
  c2 := http.Cookie{
    Name:    "c2",
    MaxAge:  -1,
    Expires: time.Unix(1, 0),
  }
  http.SetCookie(w, &c2)
}

构建服务器:

代码语言:javascript
复制
server := http.Server{
    Addr: "127.0.0.1:8080",
}
http.HandleFunc("/setCookie", setCookie)
http.HandleFunc("/getCookie", getCookie)
http.HandleFunc("/delCookie", delCookie)
server.ListenAndServe()
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go语言开发栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档