Http请求报文格式分析
package main
import (
"fmt"
"net"
)
func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("listener err", err)
return
}
defer listener.Close()
//阻塞等待用户的连接
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept err = ", err)
return
}
defer conn.Close()
//接收客户端的数据
buf := make([]byte, 1024*4)
readSize, err := conn.Read(buf)
if readSize == 0 { //对方断开,出问题了
fmt.Println("Read err = ", err)
return
}
fmt.Printf("#%v#", string(buf[:readSize]))
}
#GET / HTTP/1.1 //请求行
Host: 127.0.0.1:8000
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Microsoft Edge";v="116"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
#
HTTP编程
go语言标准库内建提供了net/http包,涵盖了HTTP客户端和服务端的具体实现。使用net/http包,我们可以很方便地编写HTTP客户端或服务端的程序。
func ListenAndServe(addr string, handler Handler) error
ListenAndServe监听TCP地址addr,并且会使用handler参数调用Serve函数处理接收到的连接。handler参数一般会设为nil,此时会使用DefaultServeMux。
package main
import "net/http"
// HandleConn 第一个参数,给客户端回复数据,req 读取客户端发送的数据
func HandleConn(w http.ResponseWriter, req *http.Request) {
_, err := w.Write([]byte("hello go")) //给客户端回复数据
if err != nil {
return
}
}
func main() {
//HandleFunc注册一个处理器函数handler和对应的模式pattern(注册到DefaultServeMux)。
//ServeMux的文档解释了模式的匹配机制。
//注册处理函数,用户连接,自动调用指定的处理函数
//func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
http.HandleFunc("/", HandleConn)
//监听绑定
//ListenAndServe监听TCP地址addr,
//并且会使用handler参数调用Serve函数处理接收到的连接。handler参数一般会设为nil,此时会使用DefaultServeMux。
http.ListenAndServe(":8000", nil)
}
http服务器获取客户端的一些信息
type Request struct {
// Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
Method string
// URL在服务端表示被请求的URI,在客户端表示要访问的URL。
//
// 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
// 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
// (参见RFC 2616, Section 5.1.2)
//
// 在客户端,URL的Host字段指定了要连接的服务器,
// 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
URL *url.URL
// 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
// Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// Connection: keep-alive
// 则:
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Connection": {"keep-alive"},
// }
// HTTP规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。
// 在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
Header Header
// Body是请求的主体。
//
// 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
// Client的Transport字段会负责调用Body的Close方法。
//
// 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
// Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
Body io.ReadCloser
// ContentLength记录相关内容的长度。
// 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
// 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
ContentLength int64
// TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
// 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
TransferEncoding []string
// Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
Close bool
// 在服务端,Host指定URL会在其上寻找资源的主机。
// 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
// Host的格式可以是"host:port"。
//
// 在客户端,请求的Host字段(可选地)用来重写请求的Host头。
// 如过该字段为"",Request.Write方法会使用URL字段的Host。
Host string
// Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
// 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
Form url.Values
// PostForm是解析好的POST或PUT的表单数据。
// 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
PostForm url.Values
// MultipartForm是解析好的多部件表单,包括上传的文件。
// 本字段只有在调用ParseMultipartForm后才有效。
// 在客户端,会忽略请求中的本字段而使用Body替代。
MultipartForm *multipart.Form
// Trailer指定了会在请求主体之后发送的额外的头域。
//
// 在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
// (客户端会声明哪些trailer会发送)
// 在处理器从Body读取时,不能使用本字段。
// 在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
// (如果客户端发送了这些键值对),此时才可以访问本字段。
//
// 在客户端,Trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值)
// ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
// 在开始发送请求后,Trailer可以在读取请求主体期间被修改,
// 一旦请求主体返回EOF,调用者就不可再修改Trailer。
//
// 很少有HTTP客户端、服务端或代理支持HTTP trailer。
Trailer Header
// RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,一般用于日志。
// 本字段不是ReadRequest函数填写的,也没有定义格式。
// 本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址。
// 客户端会忽略请求中的RemoteAddr字段。
RemoteAddr string
// RequestURI是被客户端发送到服务端的请求的请求行中未修改的请求URI
// (参见RFC 2616, Section 5.1)
// 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
RequestURI string
// TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
// 本字段不是ReadRequest函数填写的。
// 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
// 客户端会忽略请求中的TLS字段。
TLS *tls.ConnectionState
}
package main
import (
"fmt"
"net/http"
)
// HandleConn 第一个参数,给客户端回复数据,req 读取客户端发送的数据
func HandleConn(w http.ResponseWriter, req *http.Request) {
fmt.Println("r.Method = ", req.Method) //r.Method = GET
fmt.Println("r.URL = ", req.URL) // /
fmt.Println("Header = ", req.Header)
fmt.Println("Body = ", req.Body)
_, err := w.Write([]byte("hello go")) //给客户端回复数据
if err != nil {
return
}
}
func main() {
//HandleFunc注册一个处理器函数handler和对应的模式pattern(注册到DefaultServeMux)。
//ServeMux的文档解释了模式的匹配机制。
//注册处理函数,用户连接,自动调用指定的处理函数
//func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
http.HandleFunc("/", HandleConn)
//监听绑定
//ListenAndServe监听TCP地址addr,
//并且会使用handler参数调用Serve函数处理接收到的连接。handler参数一般会设为nil,此时会使用DefaultServeMux。
http.ListenAndServe(":8000", nil)
}
http客户端编程
type Response struct {
Status string // 例如"200 OK"
StatusCode int // 例如200
Proto string // 例如"HTTP/1.0"
ProtoMajor int // 例如1
ProtoMinor int // 例如0
// Header保管头域的键值对。
// 如果回复中有多个头的键相同,Header中保存为该键对应用逗号分隔串联起来的这些头的值
// (参见RFC 2616 Section 4.2)
// 被本结构体中的其他字段复制保管的头(如ContentLength)会从Header中删掉。
//
// Header中的键都是规范化的,参见CanonicalHeaderKey函数
Header Header
// Body代表回复的主体。
// Client类型和Transport类型会保证Body字段总是非nil的,即使回复没有主体或主体长度为0。
// 关闭主体是调用者的责任。
// 如果服务端采用"chunked"传输编码发送的回复,Body字段会自动进行解码。
Body io.ReadCloser
// ContentLength记录相关内容的长度。
// 其值为-1表示长度未知(采用chunked传输编码)
// 除非对应的Request.Method是"HEAD",其值>=0表示可以从Body读取的字节数
ContentLength int64
// TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
TransferEncoding []string
// Close记录头域是否指定应在读取完主体后关闭连接。(即Connection头)
// 该值是给客户端的建议,Response.Write方法的ReadResponse函数都不会关闭连接。
Close bool
// Trailer字段保存和头域相同格式的trailer键值对,和Header字段相同类型
Trailer Header
// Request是用来获取此回复的请求
// Request的Body字段是nil(因为已经被用掉了)
// 这个字段是被Client类型发出请求并获得回复后填充的
Request *Request
// TLS包含接收到该回复的TLS连接的信息。 对未加密的回复,本字段为nil。
// 返回的指针是被(同一TLS连接接收到的)回复共享的,不应被修改。
TLS *tls.ConnectionState
}
package main
import (
"fmt"
"net/http"
)
func main() {
response, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println("Get response err = ", err)
return
}
defer response.Body.Close() //内容在body里面
fmt.Println("response.status = ", response.Status) //response.status = 200 OK
fmt.Println("response.StatusCode = ", response.StatusCode) //200
fmt.Println("response.Header = ", response.Header)
//fmt.Println("response.Body = ", response.Body) //response.Body = &{[] 0xc000226080 <nil> <nil>}
buf := make([]byte, 4*1024)
var tmp string
for true {
BodySize, err := response.Body.Read(buf)
if BodySize == 0 {
fmt.Println("read err = ", err)
break
}
tmp += string(buf[:BodySize])
}
fmt.Println("tmp = ", tmp)
}
单任务百度贴吧小爬虫
package main
import (
"fmt"
"net/http"
"os"
"strconv"
)
// https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=150
// HttpGet 爬取网页内容
func HttpGet(url string) (result string, err error) {
response, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}
defer response.Body.Close()
//读取网页body
buf := make([]byte, 1024*4)
for true {
readSize, err := response.Body.Read(buf)
if readSize == 0 { //读取结束,或者出问题
fmt.Println("response body read err = ", err)
break
}
result += string(buf[:readSize])
}
return
}
func DoWork(start, end int) {
fmt.Printf("正在爬取%d到%d的页面\n", start, end)
//明确目标(要知道你准备在那个范围或者网站去搜索)
for i := start; i <= end; i++ {
//strconv.Itoa((i-1)*50)//整型转string
url := "https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
fmt.Println("url =", url)
//爬(将所有的网站的内容全部爬下来)
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
continue
}
//把内容写入到文件
fileName := strconv.Itoa(1) + ".html"
file, err := os.Create(fileName)
if err != nil {
fmt.Println("create err = ", err)
continue
}
_, err1 := file.WriteString(result)
if err1 != nil {
fmt.Println("write string err = ", err)
continue
} //写内容
err2 := file.Close()
if err2 != nil {
fmt.Println("close err = ", err2)
continue
} //关闭文件
}
}
func main() {
var start, end int
fmt.Println("请输入起始页(>=1):")
fmt.Scan(&start)
fmt.Println("请输入终止页(>=起始页):")
fmt.Scan(&end)
DoWork(start, end)
}
并发版网络爬虫
package main
import (
"fmt"
"net/http"
"os"
"strconv"
)
// https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=150
// HttpGet 爬取网页内容
func httpGet1(url string) (result string, err error) {
response, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}
defer response.Body.Close()
//读取网页body
buf := make([]byte, 1024*4)
for true {
readSize, err := response.Body.Read(buf)
if readSize == 0 { //读取结束,或者出问题
fmt.Println("response body read err = ", err)
break
}
result += string(buf[:readSize])
}
return
}
// 爬取一个网页
func SpiderPage(i int, page chan int) {
//strconv.Itoa((i-1)*50)//整型转string
url := "https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
fmt.Printf("真正爬%d页的网页:%s\n", i, url)
//爬(将所有的网站的内容全部爬下来)
result, err := httpGet1(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
return
}
//把内容写入到文件
fileName := strconv.Itoa(i) + ".html"
file, err := os.Create(fileName)
if err != nil {
fmt.Println("create err = ", err)
return
}
_, err1 := file.WriteString(result)
if err1 != nil {
fmt.Println("write string err = ", err)
return
} //写内容
err2 := file.Close()
if err2 != nil {
fmt.Println("close err = ", err2)
return
} //关闭文件
page <- i //写i
}
func doWork1(start, end int) {
fmt.Printf("正在爬取%d到%d的页面\n", start, end)
page := make(chan int)
//明确目标(要知道你准备在那个范围或者网站去搜索)
for i := start; i <= end; i++ {
go SpiderPage(i, page)
}
for i := start; i <= end; i++ {
fmt.Printf("第%d个页面爬取完成\n", <-page)
}
}
func main() {
var start, end int
fmt.Println("请输入起始页(>=1):")
fmt.Scan(&start)
fmt.Println("请输入终止页(>=起始页):")
fmt.Scan(&end)
doWork1(start, end)
}