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

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

01

概念


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



func ListenAndServe(addr string, handler Handler) error


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


02

构建服务器


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


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


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 构建服务器:


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。


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

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


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

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

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


怎么解决这个问题呢?

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


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

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

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

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


示例代码:

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


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")
}


构建服务器:


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 的工作。


示例代码:

编写多个函数,处理请求


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")
}


构建服务器:


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 个基本方法,这些方法可以根据给定的键执行添加、删除、获取和设置等操作。


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


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)
}


构建服务器:


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 方法,该方法接收一个字节切片参数,返回一个被读取内容的字节数和一个可选的错误。


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


func body(w http.ResponseWriter, r *http.Request) {
  len := r.ContentLength
  body := make([]byte, len)
  r.Body.Read(body)
  fmt.Fprintln(w, string(body))
}


构建服务器:


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


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


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 表单值
}


构建服务器:


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


示例代码:

写主体:


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


写首部:

func setHeader(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Location", "https://www.baidu.com")
  w.WriteHeader(302)
}


构建服务器:

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 结构体完整字段:


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:


// 写 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)
}


构建服务器:


server := http.Server{
    Addr: "127.0.0.1:8080",
}
http.HandleFunc("/setCookie", setCookie)
http.HandleFunc("/getCookie", getCookie)
http.HandleFunc("/delCookie", delCookie)
server.ListenAndServe()





目录
相关文章
|
11天前
|
人工智能 物联网 开发工具
.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。
`【7月更文挑战第4天】.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。从企业应用、云服务到游戏开发,其角色日益凸显。随着微软的持续创新与社区合作,未来.NET将在物联网、AI等领域拓宽应用,开发者应把握趋势,共创未来。`
13 0
|
1天前
|
存储 对象存储 Python
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
6 1
|
22天前
|
域名解析 存储 缓存
HTTP请求流程概览:浏览器构建请求行含方法、URL和版本;检查缓存;解析IP与端口
【6月更文挑战第23天】 HTTP请求流程概览:浏览器构建请求行含方法、URL和版本;检查缓存;解析IP与端口;TCP连接(HTTP/1.1可能需排队);三次握手;发送请求头与体;服务器处理并返回响应;TCP连接可能关闭或保持;浏览器接收并显示响应,更新缓存。HTTP版本间有差异。
33 5
|
9天前
|
Linux API C#
.NET开源、跨平台、使用简单的面部识别库
.NET开源、跨平台、使用简单的面部识别库
|
17天前
|
JSON 开发框架 API
【推荐100个unity插件之20】一个强大的JSON处理库——Newtonsoft.Json(也称为Json.NET)
【推荐100个unity插件之20】一个强大的JSON处理库——Newtonsoft.Json(也称为Json.NET)
27 0
|
1月前
|
NoSQL 大数据 Redis
分享5款.NET开源免费的Redis客户端组件库
分享5款.NET开源免费的Redis客户端组件库
|
26天前
|
监控 小程序 前端开发
基础入门-抓包技术&HTTPS协议&WEB&封包监听&网卡模式&APP&小程序
基础入门-抓包技术&HTTPS协议&WEB&封包监听&网卡模式&APP&小程序
|
1月前
|
SQL 开发框架 安全
【.NET Core】深入理解任务并行库 (TPL)
【.NET Core】深入理解任务并行库 (TPL)
24 0
|
5天前
|
存储 关系型数据库 数据库
给阿里云的建议和意见 一个云服务器架构是否可行
摘要(Markdown格式): 在修复阿里云服务器IPv4设置错误时遇到困难,导致服务器远程登录失败及外网访问受阻,耗时三天解决。建议阿里云更新文档,确保设置指导与实际情况一致,例如只需在路由表添加条目关联IPv4。此外,建议优化帮助页面,如采用折叠式设计减少干扰。服务器主要任务是数据分析、存储和分发,文中提出简化服务器框架,消除硬件软件复杂配置,利于初学者和独立开发者快速上手,降低时间成本。该设计旨在减少无用组件,节省资源,同时降低云服务商的人力和支持成本。期望云服务商考虑此类架构创新。目前未知是否有类似产品,期待业界反馈。
214 0
给阿里云的建议和意见 一个云服务器架构是否可行
|
7天前
|
存储 编解码 网络协议
阿里云服务器计算型和通用型四代云服务器实例区别及选择参考
目前阿里云在售的云服务器中,计算型和通用型实例规格都包含了第5代、第6代、第7代和最新第八代倚天云服务器产品,例如计算型实例中有c5、c6、c7、c8y实例,而通用型实例有g5、g6、g7、g8y等实例,有的新手用户并不清楚这四代产品之间的差别,本文为大家展示这四代云服务器实例在规格、CPU(核)、内存(G)、计算、存储、内存以及不同配置的指标数据等方面为大家做个对比,让大家了解一下他们之间的不同,以供参考和选择。
阿里云服务器计算型和通用型四代云服务器实例区别及选择参考