一、HTTP概述:
Ø web工作方式:
我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢?
对于普通的上网过程,系统其实是这样做的:
浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。
一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。
web服务器的工作原理可以简单地归纳为:
1) 客户机通过TCP/IP协议建立到服务器的TCP连接;
2) 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档;
3) 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端;
4) 客户机与服务器断开,由客户端解释HTML文档,在客户端屏幕上渲染图形结果。
Ø HTTP协议:
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示:
注:
SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。
Ø 地址(URL):
URL全称为Unique Resource Location,即统一资源定位符,用来表示网络资源,可以理解为网络文件路径。URL的格式如下:
http://host[":"port][abs_path]
http://192.168.31.1/html/index
URL的长度有限制,不同的服务器的限制值不太相同,但是不能无限长。
二、HTTP报文解析:
Ø 请求包和响应包介绍:
Ø 请求报文分析:
1) 想要知道请求报文格式并对其进行分析,只需要在服务器端进行,因为通过浏览器地址栏中输入url地址,对服务器进行http请求的时候,浏览器会给服务器端发送一些请求报文,只要我们在服务器端通过程序将这个请求报文获取到,那么就可以知道这个请求报文的格式。
示例程序:
package main
import (
"fmt"
"io"
"net"
)
func main() {
fmt.Println("服务端打开一个监听...")
//监听
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("Listen err=", err)
return
}
defer listener.Close()
fmt.Println("服务端创建一个连接...")
//阻塞,等待用户的连接
conn, err1 := listener.Accept()
if err1 != nil {
fmt.Println("Accept err1=", err1)
return
}
defer conn.Close()
fmt.Println("服务端准备开始读取浏览器请求数据...")
buf := make([]byte, 1024)
for {
n, err2 := conn.Read(buf)
if err2 != nil {
if err2 == io.EOF {
fmt.Println("数据接收完毕!")
} else {
fmt.Println("Read err2=", err2)
}
return
}
fmt.Printf("#浏览器请求报文为:\n%v#", string(buf[:n]))
}
}
此时在开启服务器端的程序,并在浏览器中输入http://localhost:8080,则可以看到在服务器端输出结果为:
浏览器中执行:
此时服务器端输出:
2) 请求报文格式说明:
HTTP 请求报文由请求行、请求头部、空行、请求包体4个部分组成,如下图所示:
注:上述的程序演示中没有请求包体。
①. 请求行:
请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST。
(1) GET:
1) 当客户端要从服务器中读取某个资源时,使用GET 方法。GET 方法要求服务器将URL 定位的资源放在响应报文的数据部分,回送给客户端,即向服务器请求某个资源。
2) 使用GET方法时,请求参数和对应的值附加在 URL 后面,利用一个问号(“?”)代表URL 的结尾与请求参数的开始,传递参数长度受限制,因此GET方法不适合用于上传数据。
3) 通过GET方法来获取网页时,参数会显示在浏览器地址栏上,因此保密性很差。
(2) POST:
1) 当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。
2) GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,而且长度没有限制,因为POST携带的数据,在HTTP的请求正文中,以名称/值的形式出现,可以传输大量数据。
②. 请求头部:
请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
请求头 含义
User-Agent 请求的浏览器类型
Accept 客户端可识别的响应内容类型列表,星号“ * ”用于按范围将类型分组,用“ */* ”指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型
Accept-Language 客户端可接受的自然语言
Accept-Encoding 客户端可接受的编码压缩格式
Accept-Charset 可接受的应答的字符集
Host 请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机
connection 连接方式(close或keepalive)
Cookie 存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie
③. 空行:
最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。
④. 请求包体:
请求包体不在GET方法中使用,而是POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型Content-Type和包体长度Content-Length。
Ø 响应报文分析:
1) 响应报文:
想要知道响应报文格式并对其进行分析,应需要在客户端进行,上一个例子我们是通过在浏览器地址栏中输入url地址,对服务器进行http的请求,浏览器会给服务器端发送一些请求报文,只要我们在服务器端通过程序将这个请求报文获取到,那么就可以知道这个请求报文的格式。同样的道理,我们通过程序代码来模拟一个客户端程序,来实现对服务器的请求,而不是通过浏览器直接的对服务器进行请求,此时我们只需要知道我们之前通过浏览器发送给服务器的请求信息,对其进行包装发送即可。之前请求的信息为:
我们需要将请求信息复制出来,然后组成一个字符串信息,需要注意的是上图中的请求信息每一行末尾都一个回车符和换行符,即\r\n,请求头最后一行后面是空行,空行是两个回车符和两个换行符\r\n\r\n,即在组合字符串的时候需要将组合在字符串中。
//定义一个请求的字符串requstHeader
requstHeader := "GET /go HTTP/1.1\r\nHost: localhost:8080\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.8\r\n\r\n"
注意,上面的字符串中的/go表示请求的路径,这个与服务器端中指定的路径要一致,否则就会报404错误。
• 测试服务器端代码:
package main
import (
"fmt"
"net/http"
)
//服务器端编写的业务逻辑处理程序
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(w, "hello world")
}
func main() {
//可以理解HandleFunc是一个钩子函数,路径"/go"表示请求的路径
//参数myHandler是一个函数类型,表示这个请求要处理的事情
http.HandleFunc("/go", myHandler)
//指定的地址进行监听,开启一个HTTP
http.ListenAndServe("127.0.0.1:8080", nil)
}
• 客户端代码:
package main
import (
"fmt"
"io"
"net"
)
func main() {
conn, err := net.Dial("tcp", ":8080")
if err != nil {
fmt.Println("Dial err=", err)
return
}
//释放conn连接
defer conn.Close()
//定义一个请求的字符串
requstHeader := "GET /go HTTP/1.1\r\nHost: localhost:8080\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.8\r\n\r\n"
//发送请求包,服务器才会响应包
conn.Write([]byte(requstHeader))
//定义一个切片,用来接收服务器的响应报文
buf := make([]byte, 1024*2)
n, err1 := conn.Read(buf)
if err1 != nil {
if err1 == io.EOF {
fmt.Println("响应读取完毕!")
} else {
fmt.Println("Read err=", err1)
}
return
}
//将获取的响应信息转化为字符串并打印
fmt.Printf("接收到的响应信息是:#\n%v#", string(buf[:n]))
}
分别打开服务器端和客户端执行命令行,
服务器端:
客户端:
假如此时,将请求字符串中的/go,更改为其它的路径,比如改为/golang,但是此时服务器端的路径没有改,仍然为/go,则再执行客户端的时候,就会报404错误信息:
2) 响应报文格式说明:
HTTP 响应报文由状态行、响应头部、空行、响应包体4个部分组成,如下图所示:
①. 状态行:
状态行由 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 服务器临时不能处理客户端请求(稍后可能可以)
②. 响应头部:
响应头可能包括:
响应头 含义
Location Location响应报头域用于重定向接受者到一个新的位置
Server Server 响应报头域包含了服务器用来处理请求的软件信息及其版本
Vary 指示不可缓存的请求头列表
Connection 连接方式
③. 空行:
最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
④. 响应包体:
服务器返回给客户端的文本信息。
三、HTTP编程:
Ø http服务器编程:
服务器端代码:
package main
import (
"fmt"
"net/http"
)
//w,表示给客户端回复数据
//r,表示读取客户端发送的数据
func HandConn(w http.ResponseWriter, r *http.Request) {
fmt.Println("执行客户请求的连接...")
w.Write([]byte("hello go")) //给客户端回复数据
fmt.Println("客户请求处理完毕!")
}
func main() {
fmt.Println("Server starting...")
//注册处理函数,用户连接,自动调用指定的处理函数
http.HandleFunc("/go", HandConn)
//监听绑定
http.ListenAndServe(":8080", nil)
}
以上的服务器端的代码想实现的功能是,在服务器端对本机的8080端口进行监听,并注册了一个处理函数HandConn,指定的路劲是/go;当服务器端启动的时候,打开浏览器并在地址栏中输入http://localhost:8080/go,此时在浏览器中就会回显hello go
打开服务器端的程序:
打开浏览器并在地址栏中输入http://localhost:8080/go:
此时浏览器处理完成:
拓展了解:
上述中的两个函数
1) func ListenAndServe(addr string, handler Handler) error
ListenAndServe会监听TCP地址addr,并且会使用handler参数调用Serve函数处理接收到的连接。handler参数一般会设为nil,此时会使用DefaultServeMux。
即,函数ListenAndServe()使用指定的监听地址和处理器启动一个HTTP服务端。
2) func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFunc注册一个处理器函数handler和对应的模式pattern(注册到DefaultServeMux)。ServeMux的文档解释了模式的匹配机制。
函数func HandConn(w http.ResponseWriter, r *http.Request)中的两个参数,分别表示:
w,表示给客户端回复数据
r,表示读取客户端发送的数据
通过w和r,我们可以分别向客户端响应数据,和获取客户端的请求信息。
程序代码:
func HandConn(w http.ResponseWriter, r *http.Request) {
fmt.Println("执行客户请求的连接...")
w.Write([]byte("hello go")) //给客户端回复数据
fmt.Println("r.Method=", r.Method)
fmt.Println("r.RemoteAddr=", r.RemoteAddr)
fmt.Println("r.URL=", r.URL)
fmt.Println("r.RequestURI=", r.RequestURI)
fmt.Println("r.Header=", r.Header)
fmt.Println("r.Body=", r.Body)
fmt.Println("客户请求处理完毕!")
}
打印结果:
Ø http客户端编程:
客户端代码:
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//指定需要获取的信息的网址,注意网址需要添加"http://",否则会报错
//返回的值resp是一个*Response结构体类型的指针类型变量
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println("http.Get err=", err)
return
}
defer resp.Body.Close()
fmt.Println("Stuatus=", resp.Status) //返回状态码及描述信息
fmt.Println("StatusCode=", resp.StatusCode) //返回状态码
fmt.Println("Header=", resp.Header) //返回响应头信息
// fmt.Println("Body=", resp.Body)
//因为响应体是resp.Body是一个io.ReadCloser接口类型的变量,该接口是一个聚合了基本的io读取和关闭操作的接口
//所以欲获取响应体中的信息需要使用io中的Read()方法来实现
buf := make([]byte, 4*1024)
var tmp string
for {
count += 1
n, err := resp.Body.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("数据读取完毕!")
} else {
fmt.Println("resp.Body.Read err=", err)
}
break //结束for循环 (注意:不能使用return)
}
tmp += string(buf[:n]) //将获取的字符串进行拼接
}
fmt.Println("tmp=", tmp) //输出获取的百度网页信息
}
上述的几个函数用法介绍:
1) func (*Client) Get 函数:
func (c *Client) Get(url string) (resp *Response, err error)
Get向指定的URL发出一个GET请求。
响应信息变量resp是一个*Response结构体类型的指针类型变量:
2) type ReadCloser 接口:
type ReadCloser interface {
Reader
Closer
}
ReadCloser接口聚合了基本的读取和关闭操作。
需要注意的是,如果采用了for循环,循环的结束需要使用break,而不能使用return。因为,break结束了循环之后还会执行循环语句之后的代码,而return会将整个函数结束,循环之后的代码当然也是执行不到的。