12.3 HTTP编程

12.3.1 概述

12.3.1.1 Web工作方式

我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢?

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

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

Web服务器的工作原理可以简单地归纳为:
 客户机通过TCP/IP协议建立到服务器的TCP连接
 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果

12.3.1.2 HTTP协议

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

HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示:
【go】网络编程-HTTP编程_tcp/ip_02

12.3.1.3 地址(URL)

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

URL的格式如下:

http://host[":"port][abs_path]
http://192.168.31.1/html/index

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

12.3.2 HTTP报文浅析

12.3.2.1 请求报文格式

  1. 测试代码
    服务器测试代码:
package main

import (
"fmt"
"log"
"net"
)

func main() {
//创建、监听socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
}

defer listenner.Close()

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

defer conn.Close() //此函数结束时,关闭连接套接字

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

buf := make([]byte, 4096) //缓冲区,用于接收客户端发送的数据

//阻塞等待用户发送的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}

//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据来自[%s]==>:\n%s\n", ipAddr, string(result))
}

浏览器输入url地址:

【go】网络编程-HTTP编程_tcp/ip_03
服务器端运行打印结果如下:

【go】网络编程-HTTP编程_tcp/ip_04

  1. 请求报文格式说明
    HTTP 请求报文由请求行、请求头部、空行、请求包体4个部分组成,如下图所示:
    【go】网络编程-HTTP编程_服务器_05
  2. 请求行
    请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST。

GET:
 当客户端要从服务器中读取某个资源时,使用GET 方法。GET 方法要求服务器将URL 定位的资源放在响应报文的数据部分,回送给客户端,即向服务器请求某个资源。
 使用GET方法时,请求参数和对应的值附加在 URL 后面,利用一个问号(“?”)代表URL 的结尾与请求参数的开始,传递参数长度受限制,因此GET方法不适合用于上传数据。
 通过GET方法来获取网页时,参数会显示在浏览器地址栏上,因此保密性很差。

POST:
 当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。
 GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,而且长度没有限制,因为POST携带的数据,在HTTP的请求正文中,以名称/值的形式出现,可以传输大量数据。

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

请求头

含义

User-Agent

请求的浏览器类型

Accept

客户端可识别的响应内容类型列表,星号“ * ”用于按范围将类型分组,用“ / ”指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型

Accept-Language

客户端可接受的自然语言

Accept-Encoding

客户端可接受的编码压缩格式

Accept-Charset

可接受的应答的字符集

Host

请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机

connection

连接方式(close或keepalive)

Cookie

存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie

  1. 空行
    最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。
  2. 请求包体
    请求包体不在GET方法中使用,而是POST方法中使用。
    POST方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型Content-Type和包体长度Content-Length。

12.3.2.2 响应报文格式

  1. 测试代码
    服务器示例代码:
package main

import (
"fmt"
"net/http"
)

//服务端编写的业务逻辑处理程序
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}

func main() {
http.HandleFunc("/go", myHandler)

//在指定的地址进行监听,开启一个HTTP
http.ListenAndServe("127.0.0.1:8000", nil)
}

启动服务器程序:

【go】网络编程-HTTP编程_http_06

客户端测试示例代码:

package main

import (
"fmt"
"log"
"net"
)

func main() {
//客户端主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
return
}

defer conn.Close() //关闭

requestHeader := "GET /go HTTP/1.1\r\nAccept: image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*\r\nAccept-Language: zh-Hans-CN,zh-Hans;q=0.8,en-US;q=0.5,en;q=0.3\r\nUser-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)\r\nAccept-Encoding: gzip, deflate\r\nHost: 127.0.0.1:8000\r\nConnection: Keep-Alive\r\n\r\n"

//先发送请求包
conn.Write([]byte(requestHeader))

buf := make([]byte, 4096) //缓冲区

//阻塞等待服务器回复的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据[%d]:\n%s\n", n, string(result))
}

启动程序,测试http的成功响应报文:
【go】网络编程-HTTP编程_服务器_07

启动程序,测试http的失败响应报文:
【go】网络编程-HTTP编程_golang_08

  1. 响应报文格式说明
    HTTP 响应报文由状态行、响应头部、空行、响应包体4个部分组成,如下图所示:
    【go】网络编程-HTTP编程_客户端_09
  2. 状态行
    状态行由 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 服务器临时不能处理客户端请求(稍后可能可以)

  1. 响应头部
    响应头可能包括:
    响应头 含义
    Location Location响应报头域用于重定向接受者到一个新的位置
    Server Server 响应报头域包含了服务器用来处理请求的软件信息及其版本
    Vary 指示不可缓存的请求头列表
    Connection 连接方式
  2. 空行
    最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
  3. 响应包体
    服务器返回给客户端的文本信息。

【go】网络编程-HTTP编程_客户端_10

12.3.3 HTTP编程

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

12.3.3.1 HTTP服务端

示例代码:

package main

import (
"fmt"
"net/http"
)

//服务端编写的业务逻辑处理程序
//hander函数: 具有func(w http.ResponseWriter, r *http.Requests)签名的函数
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "连接成功") //r.RemoteAddr远程网络地址
fmt.Println("method = ", r.Method) //请求方法
fmt.Println("url = ", r.URL.Path)
fmt.Println("header = ", r.Header)
fmt.Println("body = ", r.Body)

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

func main() {
http.HandleFunc("/go", myHandler)

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

浏览器输入url地址:
【go】网络编程-HTTP编程_golang_11

服务器运行结果:
【go】网络编程-HTTP编程_golang_12

12.3.3.2 HTTP客户端

package main

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

func main() {

//get方式请求一个资源
//resp, err := http.Get("http://www.baidu.com")
//resp, err := http.Get("http://www.neihan8.com/article/index.html")
resp, err := http.Get("http://127.0.0.1:8000/go")
if err != nil {
log.Println(err)
return
}

defer resp.Body.Close() //关闭

fmt.Println("header = ", resp.Header)
fmt.Printf("resp status %s\nstatusCode %d\n", resp.Status, resp.StatusCode)
fmt.Printf("body type = %T\n", resp.Body)

buf := make([]byte, 2048) //切片缓冲区
var tmp string

for {
n, err := resp.Body.Read(buf) //读取body包内容
if err != nil && err != io.EOF {
fmt.Println(err)
return
}

if n == 0 {
fmt.Println("读取内容结束")
break
}
tmp += string(buf[:n]) //累加读取的内容
}

fmt.Println("buf = ", string(tmp))
}

示例代码,百度贴吧爬虫

package main

import (
"fmt"
"net/http"
"os"
"strconv"
)

//爬取网页内容
func HttpGet(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}

defer resp.Body.Close()

//读取网页body内容
buf := make([]byte, 1024*4)
for {
n, _ := resp.Body.Read(buf)
if n == 0 { //读取结束,或者,出问题
//fmt.Println("resp.Body.Read err = ", err)
break
}

result += string(buf[:n])
}

return
}

func DoWork(start, end int) {
fmt.Printf("正在爬取 %d 到 %d 的页面\n", start, end)

//明确目标 (要知道你准备在哪个范围或者网站去搜索)
//http://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=0 //下一页+50
for i := start; i <= end; i++ {
url := "http://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)

//2) 爬 (将所有的网站的内容全部爬下来)
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
continue
}

//把内容写入到文件
fileName := strconv.Itoa(i) + ".html"
f, err1 := os.Create(fileName)
if err1 != nil {
fmt.Println("os.Create err1 = ", err1)
continue
}

f.WriteString(result) //写内容

f.Close() //关闭文件
}

}

func main() {
var start, end int
fmt.Printf("请输入起始页( >= 1) :")
fmt.Scan(&start)
fmt.Printf("请输入终止页( >= 起始页) :")
fmt.Scan(&end)

DoWork(start, end)
}

并发版爬虫代码

package main

import (
"fmt"
"net/http"
"os"
"strconv"
)

//爬取网页内容
func HttpGet(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}

defer resp.Body.Close()

//读取网页body内容
buf := make([]byte, 1024*4)
for {
n, _ := resp.Body.Read(buf)
if n == 0 { //读取结束,或者,出问题
//fmt.Println("resp.Body.Read err = ", err)
break
}

result += string(buf[:n])
}

return
}

//爬取一个网页
func SpiderPape(i int, page chan<- int) {
url := "http://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)

//2) 爬 (将所有的网站的内容全部爬下来)
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
return
}

//把内容写入到文件
fileName := strconv.Itoa(i) + ".html"
f, err1 := os.Create(fileName)
if err1 != nil {
fmt.Println("os.Create err1 = ", err1)
return
}

f.WriteString(result) //写内容

f.Close() //关闭文件

page <- i
}

func DoWork(start, end int) {
fmt.Printf("正在爬取 %d 到 %d 的页面\n", start, end)
page := make(chan int)

//明确目标 (要知道你准备在哪个范围或者网站去搜索)
//http://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=0 //下一页+50
for i := start; i <= end; i++ {
go SpiderPape(i, page)
}

for i := start; i <= end; i++ {
fmt.Printf("第%d个页面爬取完成\n", <-page)
}

}

func main() {
var start, end int
fmt.Printf("请输入起始页( >= 1) :")
fmt.Scan(&start)
fmt.Printf("请输入终止页( >= 起始页) :")
fmt.Scan(&end)

DoWork(start, end)
}

示例代码,段子网站爬虫

package main

import (
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
)

func HttpGet(url string) (result string, err error) {
resp, err1 := http.Get(url) //发送get请求
if err1 != nil {
err = err1
return
}

defer resp.Body.Close()

//读取网页内容
buf := make([]byte, 4*1024)
for {
n, _ := resp.Body.Read(buf)
if n == 0 {
break
}

result += string(buf[:n]) //累加读取的内容
}

return
}

//开始爬取每一个笑话,每一个段子 title, content, err := SpiderOneJoy(url)
func SpiderOneJoy(url string) (title, content string, err error) {
//开始爬取页面内容
result, err1 := HttpGet(url)
if err1 != nil {
//fmt.Println("HttpGet err = ", err)
err = err1
return
}

//取关键信息
//取标题 <h1> 标题 </h1> 只取1个
re1 := regexp.MustCompile(`<h1>(?s:(.*?))</h1>`)
if re1 == nil {
//fmt.Println("regexp.MustCompile err")
err = fmt.Errorf("%s", "regexp.MustCompile err")
return
}
//取内容
tmpTitle := re1.FindAllStringSubmatch(result, 1) //最后一个参数为1,只过滤第一个
for _, data := range tmpTitle {
title = data[1]
// title = strings.Replace(title, "\r", "", -1)
// title = strings.Replace(title, "\n", "", -1)
// title = strings.Replace(title, " ", "", -1)
title = strings.Replace(title, "\t", "", -1)
break
}

//取内容 <div class="content-txt pt10"> 段子内容 <a id="prev" href="
re2 := regexp.MustCompile(`<div class="content-txt pt10">(?s:(.*?))<a id="prev" href="`)
if re2 == nil {
//fmt.Println("regexp.MustCompile err")
err = fmt.Errorf("%s", "regexp.MustCompile err2")
return
}

//取内容
tmpContent := re2.FindAllStringSubmatch(result, -1)
for _, data := range tmpContent {
content = data[1]
content = strings.Replace(content, "\t", "", -1)
content = strings.Replace(content, "\n", "", -1)
content = strings.Replace(content, "\r", "", -1)
content = strings.Replace(content, "<br />", "", -1)
break
}

return
}

//把内容写入到文件
func StoreJoyToFile(i int, fileTitle, fileContent []string) {
//新建文件
f, err := os.Create(strconv.Itoa(i) + ".txt")
if err != nil {
fmt.Println("os.Create err = ", err)
return
}

defer f.Close()

//写内容
n := len(fileTitle)
for i := 0; i < n; i++ {
//写标题
f.WriteString(fileTitle[i] + "\n")
//写内容
f.WriteString(fileContent[i] + "\n")

f.WriteString("\n=================================================================\n")
}

}

func SpiderPape(i int, page chan int) {
//明确爬取的url
//https://www.pengfu.com/xiaohua_1.html
url := "https://www.pengfu.com/xiaohua_" + strconv.Itoa(i) + ".html"
fmt.Printf("正在爬取第%d个网页:%s\n", i, url)

//开始爬取页面内容
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
return
}

//fmt.Println("r = ", result)
//取,<h1 class="dp-b"><a href=" 一个段子url连接 "
//解释表达式
re := regexp.MustCompile(`<h1 class="dp-b"><a href="(?s:(.*?))"`)
if re == nil {
fmt.Println("regexp.MustCompile err")
return
}

//取关键信息
joyUrls := re.FindAllStringSubmatch(result, -1)
//fmt.Println("joyUrls = ", joyUrls)

fileTitle := make([]string, 0)
fileContent := make([]string, 0)

//取网址
//第一个返回下标,第二个返回内容
for _, data := range joyUrls {
//fmt.Println("url = ", data[1])

//开始爬取每一个笑话,每一个段子
title, content, err := SpiderOneJoy(data[1])
if err != nil {
fmt.Println("SpiderOneJoy err = ", err)
continue
}
//fmt.Printf("title = #%v#", title)
//fmt.Printf("content = #%v#", content)

fileTitle = append(fileTitle, title) //追加内容
fileContent = append(fileContent, content) //追加内容
}

//fmt.Println("fileTitle= ", fileTitle)
//fmt.Println("fileContent= ", fileContent)

//把内容写入到文件
StoreJoyToFile(i, fileTitle, fileContent)

page <- i //写内容,写num

}

func DoWork(start, end int) {
fmt.Printf("准备爬取第%d页到%d页的网址\n", start, end)

page := make(chan int)

for i := start; i <= end; i++ {
//定义一个函数,爬主页面
go SpiderPape(i, page)
}

for i := start; i <= end; i++ {
fmt.Printf("第%d个页面爬取完成\n", <-page)
}

}

func main() {
var start, end int
fmt.Printf("请输入起始页( >= 1) :")
fmt.Scan(&start)
fmt.Printf("请输入终止页( >= 起始页) :")
fmt.Scan(&end)

DoWork(start, end) //工作函数
}

爬虫总结

实际上爬虫一共就四个主要步骤:

  1. 明确目标 (要知道你准备在哪个范围或者网站去搜索)
  2. 爬 (将所有的网站的内容全部爬下来)
  3. 取 (去掉对我们没用处的数据)
  4. 处理数据(按照我们想要的方式存储和使用)