笔记声明
本人智商较低,好记性不如烂笔头。笔记记录了网上一套Golang免费视频教程的知识点,以供未来自己翻阅查看。不写源视频的名称和网站地址了,免得被说是广告!
20小时快速入门go语言视频 - Day8
一、HTTP 编程概述
1.1 Web 工作方式
输入 URL,首先浏览器会去请求 DNS 服务器,通过 DNS 获取到 URL 所对应的 IP,通过这个 IP 地址找到 Web 服务器后,要求建立 TCP 连接。等浏览器发送完 HTTP Request(请求)包后,服务器收到请求包后才开始处理请求包,接着服务器调用自身服务,返回 HTTP Response(响应)包。浏览器收到来自服务器的响应后开始渲染 Response 包里的 body(主体),等收到全部的内容后断开与该服务器之间的 TCP 连接。
Web 服务器也被称为 HTTP 服务器,它通过 HTTP 协议与客户端通信。(客户端通常指 Web 浏览器,手机端 APP 的内部也是由浏览器实现)
1.2 HTTP 协议
超文本传输协议(HTTP,HyperText Transfer Protocol),它详细规定了浏览器和万维网服务器之间相互通信的规则,通过因特网传送万维网文档的数据传送协议。
1.3 URL
URL 全称:Unique Resource Location,用来表示网络资源,可以理解为网络文件路径。
URL 的长度有限,不同的服务器限制的长度值有所不一样,但不会是无限长。
1.4 HTTP 报文浅析
1.4.1 请求报文格式
1.4.1.1 服务器端测试代码
import (
"fmt"
"net"
)
func main() {
//Listen,开始监听
listener, err := net.Listen("tcp", ":8888")
if err != nil {
panic(err)
}
defer listener.Close() //关闭监听
//Accept,阻塞在此,等待客户端连接进来
conn, err := listener.Accept()
if err != nil {
panic(err)
}
defer conn.Close() //关闭连接
//read,接收发送过来的数据
buf := make([]byte, 1024)
n, err := conn.Read(buf) //n 表示读到了多少个数据
if err != nil {
panic(err)
}
fmt.Printf("%v\n", string(buf[:n])) //读到 n 个内容,就写 n 个内容
}
运行代码,在浏览器中键入服务器的 IP+Port:
得到以下结果:
1.4.1.2 请求报文格式说明
HTTP 请求报文由请求行、请求头部、空行、请求包体,4部分组成。
1.4.1.2.1 请求行
请求行由 3 部分构成:方法字段、URL 字段、HTTP 协议版本字段。
1.4.1.2.2 请求头部
请求头部通知服务器关于客户端请求的信息,为请求报文添加了一些附加信息,由键/值
对组成,每行一对。
常见的请求头有:
1.4.1.2.3 空行
最后一个请求头之后是一个空行,发送回车和换行符,通知服务器以下不再有请求头。
注意:请求报文中,所有的换行都是由 \r\n
组成!
1.4.1.2.4 请求包体
请求包体不在 GET
方法中使用,而是在 POST
方法中才使用。
1.4.2 响应报文格式
1.4.2.1 测试代码
客户端必须要先给服务器端一个请求,服务器端才会反馈给客户端一个响应。
1.4.2.1.1 服务器代码
import (
"fmt"
"net/http"
)
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/go", myHandler)
http.ListenAndServe("127.0.0.1:8000", nil)
}
1.4.2.1.2 客户端代码
import (
"fmt"
"net"
)
func main() {
//主动去连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Dial, error:", err)
return
}
defer conn.Close()
requestBuf := "GET /go HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\n" +
"Upgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36\r\n" +
"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.9\r\n" +
"Accept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\n\r\n"
//注意:请求头部的最后必须再加一对`\r\n`,形成一个空行,告诉服务器接下来不会再有请求头。否则无论如何都收不到服务器的响应
//往服务器发送请求包,服务器才会反馈响应包
conn.Write([]byte(requestBuf))
//接收服务器的响应包
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read, error:", err)
return
}
fmt.Printf("%v", string(buf[:n])) //读到多少打印多少
}
填写代码中 requestBuf
值的技巧(在浏览器抓包):
先运行服务器代码,然后打开浏览器,按F12
调出开发者工具,将选项卡切换成Netword
:
在地址栏输入服务器的 IP+Port+router(此例中应该输入 http://127.0.0.1:8000/go):
点击这个路由之后,可以看到很多很多的信息,如下图所示:
已经切换成横了源码,可以看到熟悉的请求报文格式了:
将源码中的内容一行行复制黏贴到字符串中,黏贴完一行,必须加上 \r\n
,这是 HTTP
协议的规则,不用问为什么。
注意:当黏贴完最后一行之后,必须还要再额外加上一对 \r\n
,生成一个空行,告诉服务器接下来不会再有请求头了,否则服务器就会卡死在处理请求头的那块,永远不会继续往下走。
1.4.2.1.3 验证结果
先启动服务器,再运行客户端代码。
1.4.2.2 响应报文格式说明
HTTP 响应报文由状态行、响应头部、空行、响应主体,4 个组成部分
1.4.2.2.1 状态行
状态行由:HTTP 协议版本字段、状态码和状态码描述文本,3 个部分组成。
1.4.2.2.2 响应头部
常见的响应头部:
1.4.2.2.3 响应包体
服务器返回给客户端的文本信息
二、HTTP 编程
使用 Golang 件的 net/http
包来实现 HTTP 编程,net/http
涵盖了 HTTP 客户端和服务器端的具体实现。
2.1 最基本的 HTTP 编程示例
2.1.1 HTTP 服务器端代码
import "net/http"
//w,向客户端回复数据
//r,读取客户端发送的数据
func HelloGo(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello go"))
}
func main() {
//注册处理函数,客户端连接了指定的 pattern,自动调用已指定的函数
http.HandleFunc("/hello", HelloGo)
//监听绑定
http.ListenAndServe(":8888", nil)
}
2.1.2 HTTP 客户端
客户端不需要额外编写了,直接使用浏览器即可。
2.1.3 运行效果
先运行服务器端的代码,再打开浏览器,在浏览器地址栏键入服务器端的 IP+Port+Pattern(此例中,应该键入 127.0.0.1:8888/hello),回车后即能看到效果。
2.1.4 读取客户端发送的数据
处理函数中有两个参数,第一个 http.ResponseWriter
是往客户端写数据,第二个 *http.Request
是读取客户端发送过来的是数据。
import (
"fmt"
"net/http"
)
//w,向客户端回复数据
//r,读取客户端发送的数据
func HelloGo(w http.ResponseWriter, r *http.Request) {
//打印客户端发送过来的数据
fmt.Println("r.Method =", r.Method)
fmt.Println("r.URL =", r.URL)
fmt.Println("r.Header =", r.Header)
fmt.Println("r.Body =", r.Body)
w.Write([]byte("hello go"))
}
func main() {
//注册处理函数,客户端连接了指定的 pattern,自动调用已指定的函数
http.HandleFunc("/hello", HelloGo)
//监听绑定
http.ListenAndServe(":8888", nil)
}
运行效果:
2.2 HTTP 客户端编程
此例只需要用到内建 net/http
包中的 Get()
函数即可。
import (
"fmt"
"io"
"net/http"
)
func main() {
//必须带上 http 或者 https,否则无法解析
resp, err := http.Get("http://www.baidu.com")
if err != nil {
fmt.Println("http.Get, error:", err)
return
}
//内容是存放在 body 中的,读取内容需要进 body 中读取,读完就要关闭
defer resp.Body.Close()
//打印服务器端回复过来的响应内容
fmt.Println("resp.Status =", resp.Status)
fmt.Println("resp.StatusCode =", resp.StatusCode)
fmt.Println("resp.Header =", resp.Header)
buf := make([]byte, 1024)
content := ""
//不停循环地读取 resp.Body 中的内容
for {
n, err := resp.Body.Read(buf)
if err != nil {
if err == io.EOF { //EOF 代表末尾,全部正常读取完了
fmt.Println("resp.Body 中的内容全读取完了.")
break
} else {
fmt.Println("resp.Body.Read, error:", err)
continue
}
}
content += string(buf[:n])
}
fmt.Println("content:", content)
}
运行结果:
三、爬虫案例
声明:以下内容只用于学习、讨论使用,不得用于任何商业用途!
3.1 爬虫的四个主要步骤
1.明确目标(要知道你准备在哪个范围或者网站爬取)
2.爬(将网站的内容全部爬下来)
3.取(去掉没用处的数据,只取有用的数据)
4.处理数据(按照自己想要的方式去存储和使用)
3.2 Golang 爬虫练习 - 某度贴吧爬虫
3.2.1 代码实现
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func Spider(url string, i int) {
htmlString := ""
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close() //关闭 resp.Body
//循环读取 resp.Body 中的内容
buf := make([]byte, 2048)
for {
n, err := resp.Body.Read(buf)
if err != nil {
if err == io.EOF {
break
} else {
fmt.Println("resp.Body.Read, error:", err)
continue
}
}
htmlString += string(buf[:n]) //读到多少内容,拼接多少
}
//4.处理数据(按照自己想要的方式去存储和使用)
//跳过了第 3 步,这里暂时不处理数据,只把所有内容保存进文件中
f, err := os.Create(strconv.Itoa(i) + ".html")
if err != nil {
fmt.Println("os.Create, error:", err)
return
}
defer f.Close() //关闭文件
f.WriteString(htmlString)
}
func main() {
var start, end int
fmt.Print("enter start page number:")
fmt.Scan(&start)
fmt.Print("enter end page number:")
fmt.Scan(&end)
startTime := time.Now().UnixNano()
//1.明确目标(要知道你准备在哪个范围或者网站爬取)
//url:https://tieba.baidu.com/f?kw=%E6%9C%88%E8%BD%AE%E9%B9%A6%E9%B9%89&ie=utf-8&pn=0 //下一页的参数:pn=pn+50
for i := start; i <= end; i++ {
url := "https://tieba.baidu.com/f?kw=%E6%9C%88%E8%BD%AE%E9%B9%A6%E9%B9%89&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
fmt.Println("正在爬取:", url)
//2.爬(将网站的内容全部爬下来)
Spider(url, i)
}
//打印一下总耗时多少毫秒
fmt.Printf("cost:%vms\n", (time.Now().UnixNano()-startTime)/int64(time.Millisecond))
}
运行程序:
查看其中一个文件中的内容:
3.2.2 并发版爬虫
跟上面 3.2 中的代码几乎一样,只是在 main() 函数中稍作修改。
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func Spider(url string, i int, ch chan<- string) {
htmlString := ""
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close() //关闭 resp.Body
//循环读取 resp.Body 中的内容
buf := make([]byte, 2048)
for {
n, err := resp.Body.Read(buf)
if err != nil {
if err == io.EOF {
break
} else {
fmt.Println("resp.Body.Read, error:", err)
continue
}
}
htmlString += string(buf[:n]) //读到多少内容,拼接多少
}
//4.处理数据(按照自己想要的方式去存储和使用)
//跳过了第 3 步,这里暂时不处理数据,只把所有内容保存进文件中
f, err := os.Create(strconv.Itoa(i) + ".html")
if err != nil {
fmt.Println("os.Create, error:", err)
return
}
defer f.Close() //关闭文件
f.WriteString(htmlString)
ch <- url
}
func main() {
var start, end int
fmt.Print("enter start page number:")
fmt.Scan(&start)
fmt.Print("enter end page number:")
fmt.Scan(&end)
startTime := time.Now().UnixNano()
ch := make(chan string)
//1.明确目标(要知道你准备在哪个范围或者网站爬取)
//url:https://tieba.baidu.com/f?kw=%E6%9C%88%E8%BD%AE%E9%B9%A6%E9%B9%89&ie=utf-8&pn=0 //下一页的参数:pn=pn+50
for i := start; i <= end; i++ {
url := "https://tieba.baidu.com/f?kw=%E6%9C%88%E8%BD%AE%E9%B9%A6%E9%B9%89&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
//2.爬(将网站的内容全部爬下来)
go Spider(url, i, ch) //来一个,就新开一个子协程
}
//避免子协程还没结束,因为主协程结束而导致子协程全部结束
for i := start; i <= end; i++ {
fmt.Println("已爬完:", <-ch)
}
//打印一下总耗时多少毫秒
fmt.Printf("cost:%vms\n", (time.Now().UnixNano()-startTime)/int64(time.Millisecond))
}
运行代码,爬取效果明显比上面 3.2 的示例快了很多很多: