Go网络编程之HTTP编程

2018-09-08  本文已影响175人  泡泡龙吐泡泡

1. 概述

对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。

Web工作方式.png

DNS域名服务器(Domain Name Server)是进行域名(domain name)和与之相对应的IP地址转换的服务器。DNS中保存了一张域名解析表,解析消息的域名。

一个Web服务器也被称为HTTP服务器,它通过HTTP (HyperText Transfer Protocol 超文本传输协议)协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。

Web服务器的工作原理可以简单地归纳为:

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。

HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示:

HTTP协议.png

URL全称为Unique Resource Location,用来表示网络资源,可以理解为网络文件路径。

基本URL的结构包含模式(协议)、服务器名称(IP地址)、路径和文件名。常见的协议/模式如http、https、ftp等。服务器的名称或IP地址后面有时还跟一个冒号和一个端口号。再后面是到达这个文件的路径和文件本身的名称。如:

http://localhost:8888
http://192.168.31.1/html/index
https://pan.baidu.com/

URL的长度有限制,不同的服务器的限制值不太相同,但是不能无限长。

请求报文
HTTP报文解析.png

获取请求报文
为了更直观的看到浏览器发送的请求包,我们借助前面学习的TCP通信模型,编写一个简单的web服务器,只接收浏览器发送的内容,打印查看。

服务器测试代码:

package main

import (
   "net"
   "fmt"
)

func main() {
   //创建、监听socket
   listenner, err := net.Listen("tcp", "127.0.0.1:8000")
   if err != nil {
      fmt.Println("Listen err:", err)
      return
   }
   defer listenner.Close()

   //阻塞等待客户端连接
   conn, err := listenner.Accept()
   if err != nil {
      fmt.Println("Accept err:", err)
      return
   }
   defer conn.Close()

   fmt.Println(conn.RemoteAddr().String(), "连接成功")       //连接客户端的网络地址

   buf := make([]byte, 4096)  //切片缓冲区,接收客户端发送数据
   n, err := conn.Read(buf)   //n 接收数据的长度
   if err != nil {
      fmt.Println("Read err:", err)
      return
   }
   result := buf[:n]        //切片截取

   fmt.Printf("#\n%s#", string(result))
}

在浏览器中输入url地址: 127.0.0.1:8000

服务器端运行打印结果如下:


服务器端运行打印结果.png

请求报文格式说明
HTTP 请求报文由请求行、请求头部、空行、请求包体4个部分组成,如下图所示:

请求报文格式.png

1) 请求行
请求行由方法字段、URL字段 和HTTP 协议版本字段 3个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST。

GET:

POST:

2) 请求头部
请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:

请求头 含义
User-Agent 请求的浏览器类型
Accept 客户端可识别的响应内容类型列表,星号“ * ”用于按范围将类型分组,用“ / ”指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型
Accept-Language 客户端可接受的自然语言
Accept-Encoding 客户端可接受的编码压缩格式
Accept-Charset 可接受的应答的字符集
Host 请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机
connection 连接方式(close或keepalive)
Cookie 存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie

3) 空行
最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。

4) 请求包体
请求包体不在GET方法中使用,而在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型Content-Type和包体长度Content-Length。

响应报文

响应报文格式
要想获取响应报文,必须先发送请求报文给web服务器。服务器收到并解析浏览器(客户端)发送的请求报文后,借助http协议,回复相对应的响应报文。

下面我们借助net/http包,创建一个最简单的服务器,给浏览器回发送响应包。首先注册处理函数http.HandleFunc(),设置回调函数handler。而后绑定服务器的监听地址http.ListenAndserve()。

这个服务器启动后,当有浏览器发送请求,回调函数被调用,会向浏览器回复“hello world”作为网页内容。当然,是按照http协议的格式进行回复。

创建简单的响应服务器

服务器示例代码:

package main

import "net/http"

// 浏览器访问时,该函数被回调
func handler(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("hello http"))
}

func main()  {
   // 注册处理函数
   http.HandleFunc("/hello", handler)

   // 绑定服务器监听地址
   http.ListenAndServe("127.0.0.1:8000", nil)
}

测试:启动服务器。打开浏览器,在URL中写入127.0.0.1:8000/hello,向服务器发送请求。会在浏览器中看到服务器回发的“hello http”。

回调函数handler会在浏览器访问服务器时被调用。服务器使用w.Write向浏览器写了“hello http”。

但服务器是怎样借助http协议将“hello http”字符串写回来的呢,服务器响应报文的具体格式是什么样的呢?

接下来我们编写一个客户端,模拟浏览器给服务器发送“请求报文”的行为,然后将服务器回发的响应报文打印出来就可以看到了。

客户端测试示例代码:

package main

import (
   "net"
   "fmt"
)

func main()  {
   // 客户端主动连接服务器
   conn, err := net.Dial("tcp", "127.0.0.1:8000")
   if err != nil {
      fmt.Println("Dial err:", err)
      return
   }
   defer conn.Close()

   // 模拟浏览器,组织一个最简单的请求报文。包含请求行,请求头,空行即可。
   requestHttpHeader := "GET /hello HTTP/1.1\r\nHost:127.0.0.1:8000\r\n\r\n"

   // 给服务器发送请求报文
   conn.Write([]byte(requestHttpHeader))

   buf := make([]byte, 4096)
   // 读取服务器回复 响应报文
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("Read err:", err)
      return
   }
   // 打印观察
   fmt.Printf("#\n%s#", string(buf[:n]))
}

启动程序,测试http的成功响应报文:

成功响应报文.png

启动程序,测试http的失败响应报文:

失败响应报文.png

响应报文格式说明
HTTP 响应报文由状态行、响应头部、空行、响应包体
4个部分组成,如下图所示:

响应报文格式.png

1) 状态行
状态行由 HTTP 协议版本字段、状态码和状态码的描述文本3个部分组成,他们之间使用空格隔开。

状态码:状态码由三位数字组成,第一位数字表示响应的类型,常用的状态码有五大类如下所示:

状态码 含义
1xx 表示服务器已接收了客户端请求,客户端可继续发送请求
2xx 表示服务器已成功接收到请求并进行处理
3xx 表示服务器要求客户端重定向
4xx 表示客户端的请求有非法内容
5xx 表示服务器未能正常处理客户端的请求而出现意外错误

常见的状态码举例:

状态码 含义
200 OK 客户端请求成功
400 Bad Request 请求报文有语法错误
401 Unauthorized 未授权
403 Forbidden 服务器拒绝服务
404 Not Found 请求的资源不存在
500 Internal Server Error 服务器内部错误
503 Server Unavailable 服务器临时不能处理客户端请求(稍后可能可以)

2) 响应头部
响应头可能包括:

响应头 含义
Location Location响应报头域用于重定向接受者到一个新的位置
Server Server 响应报头域包含了服务器用来处理请求的软件信息及其版本
Vary 指示不可缓存的请求头列表
Connection 连接方式

3) 空行
最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。

4) 响应包体
服务器返回给客户端的文本信息。

2. Go语言HTTP编程

Go语言标准库内建提供了net/http包,涵盖了HTTP客户端和服务端的具体实现。使用net/http包,我们可以很方便地编写HTTP客户端或服务端的程序。

示例代码:

package main

import (
    "fmt"
    "net/http"
)

//服务端编写的业务逻辑处理程序 —— 回调函数
func myHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("method = ", r.Method)  //请求方法
    fmt.Println("URL = ", r.URL)        // 浏览器发送请求文件路径
    fmt.Println("header = ", r.Header)      // 请求头
    fmt.Println("body = ", r.Body)      // 请求包体
    fmt.Println(r.RemoteAddr, "连接成功")   //客户端网络地址

    w.Write([]byte("hello http"))   //给客户端回复数据
}

func main() {
    http.HandleFunc("/hello", myHandler)    // 注册处理函数

    //该方法用于在指定的 TCP 网络地址 addr 进行监听,然后调用服务端处理程序来处理传入的连接请求。
    //该方法有两个参数:第一个参数 addr 即监听地址;第二个参数表示服务端处理程序,通常为nil
    //当参2为nil时,服务端调用 http.DefaultServeMux 进行处理
    http.ListenAndServe("127.0.0.1:8000", nil)
}

浏览器输入url地址:127.0.0.1:8000/hello

回调函数myHandler的函数原型固定。func myHandler(w http.ResponseWriter, r http.Request) 有两个参数:w http.ResponseWriter r http.Request。w用来“给客户端回发数据”。它是一个interface

type ResponseWriter interface {
   Header() Header          
   Write([]byte) (int, error)   
   WriteHeader(int)         
}

r 用来“接收客户端发送的数据”。浏览器发送给服务器的http请求包的内容可以借助r来查看。它对应一个结构体:

type Request struct {
    Method string       // 浏览器请求方法 GET、POST…
    URL *url.URL        // 浏览器请求的访问路径
    ……
    Header Header       // 请求头部
    Body io.ReadCloser  // 请求包体
    RemoteAddr string   // 浏览器地址
    ……
        ctx context.Context
}

查看一下结构体成员:

fmt.Println("Method = ", r.Method)
fmt.Println("URL = ", r.URL)
fmt.Println("Header = ", r.Header)
fmt.Println("Body = ", r.Body)
fmt.Println(r.RemoteAddr, "连接成功")

客户端访问web服务器数据,主要使用*func Get(url string) (resp Response, err error)函数来完成。读到的响应报文数据被保存在 Response 结构体中。

type Response struct {
   Status     string // e.g. "200 OK"
   StatusCode int    // e.g.  200
   Proto      string // e.g. "HTTP/1.0"
   ……
   Header Header
   Body io.ReadCloser
   ……
}

服务器发送的响应包体被保存在Body中。可以使用它提供的Read方法来获取数据内容。保存至切片缓冲区中,拼接成一个完整的字符串来查看。

结束的时候,需要调用Body中的Close()方法关闭io。

示例代码:

package main

import (
   "net/http"
   "fmt"
   "io"
)

func main()  {
   // 使用Get方法获取服务器响应包数据
   //resp, err := http.Get("http://www.baidu.com")
   resp, err := http.Get("http://127.0.0.1:8000/hello")
   if err != nil {
      fmt.Println("Get err:", err)
      return
   }
   defer resp.Body.Close()

   // 获取服务器端读到的数据
   fmt.Println("Status = ", resp.Status)           // 状态
   fmt.Println("StatusCode = ", resp.StatusCode)   // 状态码
   fmt.Println("Header = ", resp.Header)           // 响应头部
   fmt.Println("Body = ", resp.Body)               // 响应包体

   buf := make([]byte, 4096)         // 定义切片缓冲区,存读到的内容
   var result string
   // 获取服务器发送的数据包内容
   for {
      n, err := resp.Body.Read(buf)  // 读body中的内容。
      if n == 0 {
         fmt.Println("--Read finish!")
         break
      }
      if err != nil && err != io.EOF {
         fmt.Println("resp.Body.Read err:", err)
         return
      }
      result += string(buf[:n])     // 累加读到的数据内容
   }
   // 打印从body中读到的所有内容
   fmt.Println("result = ", result)
}
上一篇下一篇

猜你喜欢

热点阅读