1.并发编程与协程Goroutine
1.1并行与并发
并行:在同一时刻,有多条指令在多个处理器上同时执行,借助多核cpu实现
并发:宏观:用户体验上,程序在并行执行
微观:多个计划任务,顺序执行。在飞快的切换,轮换使用cpu时间轮片
时间轮片:
1.2常见并发编程技术
进程并发:
程序:编译成功得到的二进制文件。 (占用磁盘空间) (死的) 1
进程:运行起来的程序,占用系统资源。 (内存) (活的) N
进程状态:初始态、就绪态、运行态、挂起(阻塞态)、终止(停止态)
线程并发:
线程:LWP 轻量级的进程 最小的执行单位--cpu分配时间轮片的对象
进程:最小的系统资源分配单位,给线程提供执行环境
线程同步:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一 致性,不能调用该功能。
同步:协同步调,规划先后顺序
线程同步机制:互斥锁(互斥量):建议锁,拿到锁以后,才能访问数据,没有拿到锁的线程,阻塞等待
读写锁:一把锁(读属性、写属性),写独占,读共享,写锁优先级高
协程并发:
协程:coroutine,轻量级线程,提高程序执行的效率
总结:进程(稳定性强)、线程(节省资源)、协程(效率高)都可以完成并发。
老板--手机
生产线--设备、材料、厂房--进程(资源分配单位)
工人--线程 --单进程、单线程的程序
50工人--50线程 --单进程、多线程的程序
10条生产线--500工人 --多进程、多线程的程序
利用闲暇时间义务板砖 --协程
1.3Goroutine
1.3.1Goroutine概念,创建及特性
概念:go程,创建于进程中,直接使用go关键,放置于函数调用前面,产生一个go程,实现并发
package main
import (
"fmt"
"time"
)
func sing() {
for i := 0; i < 5; i++ {
fmt.Println("唱--爱是一本书")
time.Sleep(100 * time.Millisecond)
}
}
func dance() {
for i := 0; i < 5; i++ {
fmt.Println("跳--跳一跳")
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go sing()
go dance()
for {
;
}
}
实例:
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine:i=%d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}
func main() {
//创建一个goroutine,启动另外一个任务
go newTask()
i := 0
//main goroutine 循环打印
for {
i++
fmt.Printf("main goroutine:i=%d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}
特性:主go程结束,子go程随之退出。
package main
import (
"fmt"
"time"
)
func main() {
//创建一个子go程
go func() {
for i := 0; i < 10; i++ {
fmt.Println("-----I'm goroutine-----")
time.Sleep(1 * time.Second)
}
}()
//主go程
for i := 0; i < 10; i++ {
fmt.Println("-----I'm main-----")
time.Sleep(1 * time.Second)
if i == 2 {
break
}
}
}
1.3.2runtime包
runtime.Gosched()
:出让当前go程所占用的cpu时间片,再次获得cpu时,从出让位置继续执行
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for {
fmt.Println("this is goroutine test ")
}
}()
for {
runtime.Gosched() //出让当前cpu时间片
fmt.Println("this is main test")
}
}
runtime.Goexit()
:结束调用该函数的当前go程。Goexit0):之前注册的 defer都生效。
return
:返回当前函数调用到调用者那里去。retumn之前的 defer 注册生效。
package main
import (
"fmt"
"runtime"
)
func test() {
defer fmt.Println("cccccccc")
//return
runtime.Goexit() //退出当前go程
fmt.Println("ddddddd")
}
func main() {
go func() {
defer fmt.Println("aaaaaaaa")
test()
fmt.Println("bbbbbbb")
}()
for {
;
}
}
runtime.GOMAXPROCS()
:设置当前进程使用的最大cpu核数,返回上一次调用成功的设置值,首次返回默认值
package main
import (
"fmt"
"runtime"
)
func main() {
n := runtime.GOMAXPROCS(1) //单核
fmt.Println("n=", n)
n = runtime.GOMAXPROCS(2) //双核
fmt.Println("n=", n)
for i := 0; i < 10; i++ {
go fmt.Println(0) //子go程
fmt.Println(1) //主go程
}
}
2.协程间通信与Channel
2.1channel
概念:是一种数据类型,对应一个“管道”主要用来解决go程的同步问题以及协程之间数据共享(数据传递)的问题。通过通信来共享内存,而不是共享内存来通信
定义:
make(chan 在channel中传递的数据类型,容量)
容量=0:无缓冲channel || 容量>0:有缓存channel
【补充知识】:每当有一个进程启动时,系统会自动打开三个文件:标准输入(stdin)、标准输出(stdout)、标准错误(stderr),当进程运行结束,操作系统自动关闭三个文件
channel有两个端:
一端:写端(传入端) chan <-
另一端:读端(传出端) <- chan
要求:读端和写端必须同时满足条件,才在channel上进行数据流动,否则,则阻塞
package main
import (
"fmt"
"time"
)
// 全局定义channel,用来完成数据同步
var channel = make(chan int)
// 定义一台打印机
func Printer(s string) {
for _, ch := range s {
fmt.Printf("%c", ch) //屏幕:stdout
time.Sleep(300 * time.Millisecond)
}
}
// 定义两个人使用打印机
func person1() { //先执行
Printer("hello")
channel <- 1
}
func person2() { //后执行
<-channel
Printer("world")
}
func main() {
go person1()
go person2()
for {
;
}
}
channel同步,数据传递:
package main
import "fmt"
func main() {
ch := make(chan string) //无缓冲channel
//len(ch):channel中剩余未读取数据个数,cap(ch):channel容量
fmt.Println("len=", len(ch), "cap=", cap(ch))
go func() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
//通知主go打印完成
ch <- "done"
}()
fmt.Println(<-ch)
}
无缓冲channel(同步通信):通道容量为0,len=0,应用于两个go程中,一个读另一个写,具备同步的能力(打电话)
有缓冲channel(异步通信):通道容量为0,应用于两个go程中,一个读另一个写,缓冲区可以进行数据存储,存储至容量上限,阻塞。具备异步能力,不需同时操作缓冲区(发短信)
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 5) //存满3个元素之前,不会阻塞
fmt.Println("len=", len(ch), "cap=", cap(ch))
go func() {
for i := 0; i < 8; i++ {
ch <- i
fmt.Println("子go程", i, "len=", len(ch), "cap=", cap(ch))
}
}()
time.Sleep(3 * time.Second)
for i := 0; i < 8; i++ {
num := <-ch
fmt.Println("主go程读到", num)
}
}
关闭channel:
确定不再对端发送、传输数据,使用close(ch)关闭channel
对端可以判断channel是否关闭:
if num,ok := <-ch; ok == true{
如果对端已经关闭, ok --> false . num无数据
如果对端没有关闭,ok--> true . num保存读到的数据
可以使用range替代ok
总结:1.数据不发送完不应该关闭
2.已经关闭的channel,不能再向其写入数据,可以从中读取数据,无缓冲(读到0)
有缓冲(缓冲区内有数据,先读数据,读完数据后,可以继续读,读到0)
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) //写端,写完数据主动关闭channel
fmt.Println("读端关闭")
}()
time.Sleep(2 * time.Second)
//for {
// if num, ok := <-ch; ok == true {
// fmt.Println("读到数据:", num)
// } else {
// n := <-ch
// fmt.Println("关闭后:", n)
// break
// }
//}
for num := range ch {
fmt.Println("读到数据:", num)
}
}
单向channel:
默认的channel是双向的。 var ch chan int ch = make(chan int)
单向写channel:var sendCh chan <- int sendCh = make(chan <- int) 不能读操作
单向读channel:var recvCh <- chan int recvCh = make(<- chan int)
转换:
双向channel可以隐式转换为任意一种单项channel
sendCh = ch
单向channel不能转换为双向channel
ch = sendCh/recvCh error!!
传参:传【引用】
package main
import "fmt"
func send(out chan<- int) {
out <- 1
close(out)
}
func recv(in <-chan int) {
n := <-in
fmt.Println(n)
}
func main() {
ch := make(chan int) //双向channel
go func() {
send(ch) //双向channel转为写入channel
}()
recv(ch)
}
2.2单向channel及应用
生产者:发送数据端
消费者:接收数据端
缓冲区:1.解耦(降低生产者和消费者之间耦合度)
2.并发(生产者消费者数量不对等时,能保持正常通信)
3.缓存(生产者和消费者数据处理速度不一致时,暂存数据)
package main
import (
"fmt"
"time"
)
func Producer(out chan<- int) {
for i := 0; i < 10; i++ {
fmt.Println("生产者生产:", i)
out <- i * i
}
close(out)
}
func Consumer(in <-chan int) {
for num := range in {
fmt.Println("消费者拿到:", num)
time.Sleep(1 * time.Second)
}
}
func main() {
ch := make(chan int)
go Producer(ch) //子go程 生产者
Consumer(ch) //主go程 消费者
}
订单模拟:
package main
import "fmt"
type OrderInfo struct {
id int
}
func Producer(out chan<- OrderInfo) {
for i := 0; i < 10; i++ {
order := OrderInfo{id: i}
out <- order
}
close(out)
}
func Consumer(in <-chan OrderInfo) {
for order := range in {
fmt.Println("订单id为:", order.id)
}
}
func main() {
ch := make(chan OrderInfo)
go Producer(ch)
Consumer(ch)
}
2.3定时器
2.3.1time.Timer
time.Timer:创建定时器,指定定时时长,定时到达后。系统会自动向定时器的成员C写系统当前时间。(对 chan 的写操作)
type Time struct{
C <- chan Time
r runtime Timer
}
读取 Timer.C 得到 定时后的系统时间。并且完成一次 chan 的 读操作
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("当前时间:", time.Now())
//创建定时器
myTimer := time.NewTimer(time.Second * 2)
nowTime := <-myTimer.C //chan类型
fmt.Println("现在时间:", nowTime)
}
三种定时方法:
package main
import (
"fmt"
"time"
)
// 三种定时方法
func main() {
//1.sleep
time.Sleep(time.Second)
//2.Timer.C
myTimer := time.NewTimer(time.Second * 2) //创建一个定时器,指定定时时长
nowTime := <-myTimer.C //定时满,系统自动写入系统时间
fmt.Println("现在时间:", nowTime)
//3.time.After
nowTime2 := <- time.After(time.Second)
fmt.Println("现在时间:", nowTime2)
}
定时器的停止和重置:
package main
import (
"fmt"
"time"
)
func main() {
myTimer := time.NewTimer(time.Second * 3) //创建定时器
myTimer.Reset(time.Second * 1) //重置定时时长为1
go func() {
<-myTimer.C
fmt.Println("子go程定时完毕")
}()
myTimer.Stop() //设置定时器停止 <-myTimer.C会阻塞
for {
;
}
}
2.3.2time.Ticker
time.Ticker:定时时长到达后,系统会自动向 Ticker的 C中写入系统当前时间。并且,每隔一个定时时长后,循环写入系统当前时间。在子go程中循环读取 C,获取系统写入的时间,
type Ticker struct{
C <- chan Time
r runtime Timer
}
周期定时:
package main
import (
"fmt"
"time"
)
func main() {
quit:= make(chan bool) //创建一个判断是否终止的channel
fmt.Println("now:", time.Now())
myTicker := time.NewTicker(time.Second * 1)
i:=0
go func() {
for {
nowTime := <-myTicker.C
i++
fmt.Println("nowTime:", nowTime)
if i==3{
quit <- true //解除主go程阻塞
}
break//return//runtime.Goexit()
}
}()
<-quit //子go程循环获取<-myTicker.C期间,一直阻塞
}
3.并发编程与同步机制
3.1select
select监听channel通信:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
ch := make(chan int) //用来进行数据通信的channel
quit := make(chan bool) //用来判断是否退出的channel
go func() { //写数据
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(1 * time.Second)
}
close(ch)
quit <- true //通知主go程退出
runtime.Goexit()
}()
for { //主go程,读数据
select {
case num := <-ch:
fmt.Println("读到:", num)
case <-quit:
return //终止进程
}
fmt.Println("=========")
}
}
select实现斐波那契数列:
package main
import (
"fmt"
"runtime"
)
func fibonacci(ch <-chan int, quit <-chan bool) {
for {
select {
case n := <-ch:
fmt.Print(n, " ")
case <-quit:
runtime.Goexit()
}
}
}
func main() {
ch := make(chan int)
quit := make(chan bool)
go fibonacci(ch, quit) //子go程 打印fibonacci数列
x, y := 1, 1
for i := 0; i < 20; i++ {
ch <- x
x, y = y, x+y
}
quit <- true
}
超时处理:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
quit := make(chan bool)
go func() { //子go程获取数据
for {
select {
case v := <-ch:
fmt.Println("Received:", v)
case <-time.After(3 * time.Second):
quit <- true
goto lable
}
}
lable:
fmt.Println("Exit")
}()
for i := 0; i < 2; i++ {
ch <- i
time.Sleep(2 * time.Second)
}
<-quit //主go程,阻塞等待 子go程通知,退出
fmt.Println("Done")
}
3.2锁和条件变量
3.2.1锁
死锁:不是一种锁,是一种错误使用锁导致的现象
1.单go程自己死锁:channel应该在最少两个以上go程中进行通信
2.go程间channel访问顺序导致死锁:使用channel一端读(写),要保证另一端写(读)操作同时有机会执行
3.多go程,多channel交叉死锁:Ago程:掌握M,尝试拿N Bgo程:掌握N,尝试拿M
4.在go语言中,尽量不要将互斥锁、读写锁与channel混用--隐性死锁
package main
func main() {
//死锁1
//ch := make(chan int)
//ch <- 789
//num := <-ch
//fmt.Println(num)
//死锁2
//ch := make(chan int)
//num := <-ch
//fmt.Println(num)
//go func() {
// ch <- 789
//}()
//死锁3
// ch1 := make(chan int)
// ch2 := make(chan int)
// go func() {
// for {
// select {
// case num := <-ch1:
// ch2 <- num
// }
// }
// }()
// for {
// select {
// case num := <-ch2:
// ch1 <- num
// }
// }
}
互斥锁:(互斥量)
A、B go程共同访问共享数据。由于cpu调度随机,需要对共享数据访问顺序加以限定(同步)
创建mutex(互斥锁),访问共享数据之前,加锁,访问结束,解锁。在Ago程加锁时,Bgo程加锁会失败-阻塞
直至Ago程解放mutex,B从阻塞处恢复执行
package main
import (
"fmt"
"sync"
"time"
)
//使用“锁”完成同步--互斥锁
var mutex sync.Mutex //创建一个互斥量,新建的互斥锁转换为,未加锁,锁只有一把
// 定义一台打印机
func Printer(s string) {
mutex.Lock() //访问共享数据之前,加锁
for _, ch := range s {
fmt.Printf("%c", ch)
time.Sleep(300 * time.Millisecond)
}
mutex.Unlock() //共享数据访问结束,解锁
}
// 定义两个人使用打印机
func person1() { //先执行
Printer("hello")
}
func person2() { //后执行
Printer("world")
}
func main() {
go person1()
go person2()
for {
;
}
}
读写锁:读时共享,写时独占,写锁优先级更高
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var reMutex sync.RWMutex //锁只有一把
var value int//共享数据
func readGo(index int) {
for {
reMutex.RLock() //读模式加锁
fmt.Printf("------%d个 读go程,读取:%d\n", index, value)
reMutex.RUnlock() //读模式解锁
}
}
func writeGo(index int) {
for {
//生成随机数
num := rand.Intn(1000)
reMutex.Lock() //写模式加锁
value = num
fmt.Printf("%d个 写go程,写入:%d\n", index, num)
time.Sleep(300 * time.Millisecond) //放大实验现象,主动让出cpu
reMutex.Unlock() //写模式解锁
}
}
func main() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 5; i++ {
go readGo(i + 1)
}
for i := 0; i < 5; i++ {
go writeGo(i + 1)
}
//<- quit
for {
;
}
}
3.2.2条件变量
概念:本身不是锁,但经常与锁结合使用
使用流程:
1.创建条件变量:
var cond sync.Cond
2.指定条件变量用的锁:
cond.L = new(sync.Mutex)
给公共区加锁(互斥量)cond.Lock()
4.判断是否到达阻塞条件(缓冲区满/空)--for循环判断
for len(ch)==cap(ch){cond.Wait()}
--1)阻塞2)解锁3)加锁
5.访问公共区--读、写数据、打印
6.解锁条件变量用的锁
cond.L.Unlock()
7.唤醒阻塞在条件变量上的对端
cond.Signal()
cond.Broadcast()
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var cond sync.Cond //定义全局条件变量
func Producer(out chan<- int, idx int) {
for {
//先加锁
cond.L.Lock()
//判断缓冲区是否满
for len(out) == 5 {
cond.Wait() //1.阻塞 2.释放锁 3.加锁
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
num := r.Intn(800)
out <- num
fmt.Printf("生产者%dth,生产:%d\n", idx, num)
//访问公共区结束,打印结束,解锁
cond.L.Unlock()
//唤醒阻塞在条件变量上的对端
cond.Signal()
time.Sleep(time.Millisecond * 200)
}
}
func Consumer(in <-chan int, idx int) {
for {
//先加锁
cond.L.Lock()
//判断缓冲区是否空
for len(in) == 0 {
cond.Wait() //1.阻塞 2.释放锁 3.加锁
}
num := <-in
fmt.Printf("消费者%dth,消费:%d\n", idx, num)
//访问公共区结束,打印结束,解锁
cond.L.Unlock()
//唤醒阻塞在条件变量上的对端
cond.Signal()
time.Sleep(time.Millisecond * 200)
}
}
func main() {
product := make(chan int, 5)
//指定条件变量使用的锁
cond.L = new(sync.Mutex) //互斥锁初值为0
for i := 0; i < 5; i++ {
go Producer(product, i+1)
}
for i := 0; i < 5; i++ {
go Consumer(product, i+1)
}
for {
;
}
}
注意:seed在1.20已弃用
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
fmt.Println(r.Int63n(3))
num := r.Int63n(500) + 1000
fmt.Println(num)
int63 := r.Int63()
fmt.Println(int63)
}
4.网络编程详解
注:部分网络编程知识引用于java guide:JavaGuide(Java学习&面试指南) | JavaGuide
4.1网络编程理论
协议:一组规则,要求使用协议的双方,必须严格遵守协议内容
4.1.1网络分层架构
OSI七层模型:
TCP/IP四层模型:
4.1.2层与协议
应用层:应用层位于传输层之上,主要提供两个终端设备上的应用程序之间信息交换的服务,它定义了信息交换的格式,消息会交给下一层传输层来传输。 我们把应用层交互的数据单元称为报文。
- HTTP(Hypertext Transfer Protocol,超文本传输协议):基于 TCP 协议,是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。
- SMTP(Simple Mail Transfer Protocol,简单邮件发送协议):基于 TCP 协议,是一种用于发送电子邮件的协议。注意 ⚠️:SMTP 协议只负责邮件的发送,而不是接收。要从邮件服务器接收邮件,需要使用 POP3 或 IMAP 协议。
- POP3/IMAP(邮件接收协议):基于 TCP 协议,两者都是负责邮件接收的协议。IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。
- FTP(File Transfer Protocol,文件传输协议) : 基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。建议在传输敏感数据时使用更安全的协议,如 SFTP。
- Telnet(远程登陆协议):基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。
- SSH(Secure Shell Protocol,安全的网络传输协议):基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务
- RTP(Real-time Transport Protocol,实时传输协议):通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。
- DNS(Domain Name System,域名管理系统): 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。
传输层:传输层的主要任务就是负责向两台终端设备进程之间的通信提供通用的数据传输服务。 应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。
- TCP(Transmission Control Protocol,传输控制协议 ):提供 面向连接 的,可靠 的数据传输服务。
- UDP(User Datagram Protocol,用户数据协议):提供 无连接 的,尽最大努力 的数据传输服务(不保证数据传输的可靠性),简单高效。
网络层:网络层负责为分组交换网上的不同主机提供通信服务。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报,简称数据报。
⚠️ 注意:不要把运输层的“用户数据报 UDP”和网络层的“IP 数据报”弄混。
网络层的还有一个任务就是选择合适的路由,使源主机运输层所传下来的分组,能通过网络层中的路由器找到目的主机。
这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称。
互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Internet Protocol)和许多路由选择协议,因此互联网的网络层也叫做 网际层 或 IP 层。
- IP(Internet Protocol,网际协议):TCP/IP 协议中最重要的协议之一,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。
- ARP(Address Resolution Protocol,地址解析协议):ARP 协议解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。
- ICMP(Internet Control Message Protocol,互联网控制报文协议):一种用于传输网络状态和错误消息的协议,常用于网络诊断和故障排除。例如,Ping 工具就使用了 ICMP 协议来测试网络连通性。
- NAT(Network Address Translation,网络地址转换协议):NAT 协议的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,LAN)内,各主机使用的是同一个 LAN 下的 IP 地址,但在该 LAN 以外,在广域网(WAN)中,需要一个统一的 IP 地址来标识该 LAN 在整个 Internet 上的位置。
- OSPF(Open Shortest Path First,开放式最短路径优先) ):一种内部网关协议(Interior Gateway Protocol,IGP),也是广泛使用的一种动态路由协议,基于链路状态算法,考虑了链路的带宽、延迟等因素来选择最佳路径。
- RIP(Routing Information Protocol,路由信息协议):一种内部网关协议(Interior Gateway Protocol,IGP),也是一种动态路由协议,基于距离向量算法,使用固定的跳数作为度量标准,选择跳数最少的路径作为最佳路径。
- BGP(Border Gateway Protocol,边界网关协议):一种用来在路由选择域之间交换网络层可达性信息(Network Layer Reachability Information,NLRI)的路由选择协议,具有高度的灵活性和可扩展性。
ARP协议:
- ARP 协议在协议栈中的位置? ARP 协议在协议栈中的位置非常重要,在理解了它的工作原理之后,也很难说它到底是网络层协议,还是链路层协议,因为它恰恰串联起了网络层和链路层。国外的大部分教程通常将 ARP 协议放在网络层。
- ARP 协议解决了什么问题,地位如何? ARP 协议,全称 地址解析协议(Address Resolution Protocol),它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。
- ARP 工作原理? 只希望大家记住几个关键词:ARP 表、广播问询、单播响应。
数据通信过程:
4.2Socket编程
特性:在网络通信过程中,socket一定是成对出现的
4.2.1网络应用程序设计模式
C/S模式:传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
优点:协议选用灵活、缓存数据、
缺点:存在安全风险、开发工作量大
B/S模式:浏览器(browser)/服务器(server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
优点:安全、开发工作量节省约1/3、跨平台
缺点:协议选择不灵活(要支持所选协议全部内容)、小型(缓存数据小
TCP的C/S架构:
4.2.2简单的C/S模型通信
nc工具环境变量配置:
netcat 1.11 for Win32/Win64
解压 netcat-win32-1.12.zip 文件 到指定目录
拷贝 解压后目录,保存到环境变量:
方法:我的电脑-->属性-->高级系统设置-->环境变量-->系统变量中找“path”-->双击它-->新建-->粘贴
启动 cmd 执行 nc 命令 充当客户端测试
nc 127.0.01 8000 (注意 IP 和 端口之间是“空格”)
输入 hello socket。 服务器应该能读到并向服务器的屏幕打印 “hello socket”
TCP-CS服务器:
1.创建监听socket
listener := net.Listen("tcp", "IP+port")
IP+port---服务器自己的IP 和 port
2.启动监听
conn:=listener.Accept()
conn 用于通信的 socket
coon.Read()
4.处理使用数据
coon.Write()
6.关闭listener、coon
package main
import (
"fmt"
"net"
)
func main() {
//指定服务器通信协议,ip地址,端口号
//创建一个用于监听的socket
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println(err)
return
}
defer listener.Close()
fmt.Println("服务器等待客户端建立连接...")
//阻塞监听客户端连接请求,成功建立连接,返回用于通信的socket
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
fmt.Println("服务器与客户端连接成功")
//读取客户端发送的数据
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Println(err)
return
}
conn.Write(buf[:n]) //读多少写多少
//处理数据
fmt.Println("客户端发送的数据是:", string(buf[:n]))
}
TCP-CS客户端:
conn, err := net.Dial("tcp",服务器的IP+port)
2.写数据给服务器
conn.Write()
3.读取服务器回发的数据
conn.Read()
conn.Close()
package main
import (
"fmt"
"net"
)
func main() {
//指定服务器ip+port 创建通信套接字
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
//主动写数据给服务器
conn.Write([]byte("hello world"))
//读取服务器返回的数据
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("服务器返回的数据是:", string(buf[:n]))
}
4.2.3TCP-CS并发通信
TCP-CS并发服务器:
1.创建监听套接字
listener, err := net.Listen("tcp", "IP+PORT")
defer listener.Close()
3.for循环阻塞监听客户端连接时间
conn, err := listener.Accept()
4.创建go程对应每一个客户端进行数据通信
go HandlerConnect(conn)
5.实现
go HandlerConnect(conn)
1)
defer conn.Close()
2)获取成功连接的客户端Addr
addr := conn.RemoteAddr()
3)for循环读取客户端发送数据
n, err := conn.Read(buf)
4)处理数据 小写转大写
strings.ToUpper(string(buf[:n]))
5)回写转换后的数据
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
6)判断服务器关闭:Read读客户端,返回0或者客户端输入exit则关闭连接
package main
import (
"fmt"
"net"
"strings"
)
func HandlerConnect(conn net.Conn) {
defer conn.Close()
//获取连接的客户端Addr
addr := conn.RemoteAddr()
fmt.Println("客户端地址是:", addr)
buf := make([]byte, 4096)
//循环读取客户端发送数据
for {
n, err := conn.Read(buf)
if "exit\n" == string(buf[:n]) || "exit\r\n" == string(buf[:n]) {
fmt.Println("接收到退出请求,客户端退出了")
return
}
if n == 0 {
fmt.Println("客户端退出了")
return
}
if err != nil {
fmt.Println("conn.Read err=", err)
return
}
fmt.Println("客户端发送的数据是:", string(buf[:n]))
//小写转大写,回发给客户端
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
}
func main() {
//创建监听socket
listener, err := net.Listen("tcp", "127.0.0.1:8001")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listener.Close()
//监听客户端连接请求
for {
fmt.Println("服务器等待客户端连接...")
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err=", err)
return
}
//具体完成服务器和客户端的数据通信
go HandlerConnect(conn)
}
}
TCP-CS并发客户端:
匿名 go 程 , 获取 键盘输入, 写给服务器
for 循环读取服务器回发数据
发送数据时,默认在结尾自带‘ \r\n ’
package main
import (
"fmt"
"net"
"os"
)
func main() {
//主动发起连接请求
conn, err := net.Dial("tcp", "127.0.0.1:8001")
if err != nil {
fmt.Println("err=", err)
return
}
defer conn.Close()
//获取用户键盘输入(stdin),将输入数据发送给服务器
go func() {
for {
str := make([]byte, 4096)
n, err := os.Stdin.Read(str)
if err != nil {
fmt.Println("os.Stdin.Read err=", err)
continue
}
//写给服务器
conn.Write(str[:n])
}
}()
//回显服务器回发的大写数据
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
fmt.Println("检测到服务器关闭,客户端退出")
return
}
if err != nil {
fmt.Println("conn.Read err=", err)
return
}
fmt.Println("服务器回发数据:", string(buf[:n]))
}
}
4.3TCP
TCP(Transmission Control Protocol,传输控制协议 ):提供 面向连接 的,可靠 的数据传输服务。
4.3.1TCP通信过程
建立连接-三次握手:
- 一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务端的确认;
- 二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态;
- 三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入ESTABLISHED 状态,完成 TCP 三次握手。
为什么要三次握手:目的就是双方确认自己与对方的发送与接收是正常的。
- 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
- 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
- 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常
断开连接-TCP四次挥手:
- 第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。
- 第二次挥手:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
- 第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
- 第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
为什么要四次挥手:TCP 是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。
举个例子:A 和 B 打电话,通话即将结束后。
- 第一次挥手:A 说“我没啥要说的了”
- 第二次挥手:B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话
- 第三次挥手:于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”
- 第四次挥手:A 回答“知道了”,这样通话才算结束。
4.3.2TCP状态转换
- 主动发起连接请求端: CLOSED —— 完成三次握手 —— ESTABLISEHED(数据通信状态)—— Dial()函数返回
- 被动发起连接请求端: CLOSED —— 调用Accept()函数 —— LISTEN —— 完成三次握手 —— ESTABLISEHED (数据通信状态)—— Accept()函数返回
- 数据传递期间 —— ESTABLISEHED (数据通信状态)
- 主动关闭连接请求端:ESTABLISEHED —— FIN_WAIT_2 (半关闭)—— TIME_WAIT —— 2MSL —— 确认最后一个ACK被对端成功接收。—— CLOSE
- 半关闭、TIME_WAIT、2MSL ——只会出现在 “主动关闭连接请求端”
- 被动关闭连接请求端:ESTABLISEHED —— CLOSE
查看状态命令:
windows:netstat -an | findstr 8001(端口号)
Linux: netstat -apn | grep 8001
4.3UDP
UDP(User Datagram Protocol,用户数据协议):提供 无连接 的,尽最大努力 的数据传输服务(不保证数据传输的可靠性),简单高效。
UDP服务器:
创建 server端地址结构(IP + port)
net.ResolveUDPAddr()
创建用于通信的socket, 绑定地址结构
udpConn = net.ListenUDP(“udp”, server端地址结构)
defer udpConn.Close()
读取客户端发送数据
ReadFromUDP(buf)
返回:
n, cltAddr(客户端的IP+port) , err
写数据给 客户端
WriteToUDP("待写数据",cltAddr)
package main
import (
"fmt"
"net"
"time"
)
func main() {
//组织一个udp地址结构,指定服务器ip+port
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8002")
if err != nil {
fmt.Println("net.ResolveUDPAddr err=", err)
return
}
fmt.Println("udp服务器地址结构创建完成")
//创建用户通信的socket
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
if err != nil {
fmt.Println("net.ListenUDP err=", err)
return
}
}
defer udpConn.Close()
fmt.Println("udp服务器socket创建完成")
//读取客户端发送的数据
buf := make([]byte, 4096)
//返回读取到的字节数,客户端的地址,error
n, remoteAddr, err := udpConn.ReadFromUDP(buf)
if err != nil {
fmt.Println("udpConn.ReadFromUDP err=", err)
return
}
//模拟处理数据
fmt.Println("客户端发送的数据是:", string(buf[:n]))
//回写数据给客户端
daytime := time.Now().String()
_, err = udpConn.WriteToUDP([]byte(daytime), remoteAddr)
if err != nil {
fmt.Println("udpConn.WriteToUDP err=", err)
return
}
}
UDP客户端:
参考 TCP 客户端。
net.Dial("udp", server 的IP+port)
package main
import (
"fmt"
"net"
)
func main() {
// 指定 服务器 IP + port 创建 通信套接字。
conn, err := net.Dial("udp", "127.0.0.1:8002")
if err != nil {
fmt.Println("net.Dial err:", err)
return
}
defer conn.Close()
for i := 0; i < 1000000; i++ {
// 主动写数据给服务器
conn.Write([]byte("Are you Ready?"))
buf := make([]byte, 4096)
// 接收服务器回发的数据
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
fmt.Println("服务器回发:", string(buf[:n]))
}
}
UDP并发服务器:
UDP默认支持客户端并发访问
使用 go 程 将 服务器处理 ReadFromUDP 和 WriteToUDP操作分开。提高并发效率。
package main
import (
"net"
"fmt"
"time"
)
func main() {
// 组织一个 udp 地址结构, 指定服务器的IP+port
srvAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8006")
if err != nil {
fmt.Println("ResolveUDPAddr err:", err)
return
}
fmt.Println("udp 服务器地址结构,创建完程!!!")
// 创建用户通信的 socket
udpConn, err := net.ListenUDP("udp", srvAddr)
if err != nil {
fmt.Println("ListenUDP err:", err)
return
}
defer udpConn.Close()
fmt.Println("udp 服务器通信socket创建完成!!!")
// 读取客户端发送的数据
buf := make([]byte, 4096)
for {
// 返回3个值,分别是 读取到的字节数, 客户端的地址, error
n, cltAddr, err := udpConn.ReadFromUDP(buf) // --- 主go程读取客户端发送数据
if err != nil {
fmt.Println("ReadFromUDP err:", err)
return
}
// 模拟处理数据
fmt.Printf("服务器读到 %v 的数据:%s\n", cltAddr, string(buf[:n]))
go func() { // 每有一个客户端连接上来,启动一个go程 写数据。
// 提取系统当前时间
daytime := time.Now().String() + "\n"
// 回写数据给客户端
_, err = udpConn.WriteToUDP([]byte(daytime), cltAddr)
if err != nil {
fmt.Println("WriteToUDP err:", err)
return
}
}()
}
}
4.4TCP与UDP
TCP 与 UDP 的区别(重要)
- 是否面向连接:UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。
- 是否是可靠传输:远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
- 是否有状态:这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了(这很渣男!)。
- 传输效率:由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。
- 传输形式:TCP 是面向字节流的,UDP 是面向报文的。
- 首部开销:TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。
- 是否提供广播或多播服务:TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多;
什么时候选择 TCP,什么时候选 UDP?
- UDP 一般用于即时通信,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。
- TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。
4.5网络编程案例
网络文件传输:
命令行参数: 在main函数启动时,向整个程序传参。
语法: go run xxx.go argv1 argv2 argv3
xxx.go: 第 0 个参数。argv1 :第 1 个参数。argv2 :第 2个参数。 argv3 :第 3 个参数。
使用:
list := os.Args
参数3 = list[3]
获取文件属性:
fileInfo := os.Stat
(文件访问绝对路径)
fileInfo 接口,两个接口,Name() 获取文件名, Size() 获取文件大小。
文件传输-发送端(客户端):
提示用户使用命令行参数输入文件名。接收文件名 filepath(含访问路径)
使用 os.Stat()获取文件属性,得到纯文件名 fileName(去除访问路径)
主动发起连接服务器请求,结束时关闭连接。
发送文件名到接收端 conn.Write()
读取接收端回发的确认数据 conn.Read()
判断是否为“ok”。如果是,封装函数 SendFile() 发送文件内容。传参 filePath 和 conn
只读 Open 文件, 结束时Close文件
循环读本地文件,读到 EOF,读取完毕。
将读到的内容原封不动 conn.Write 给接收端(服务器)
package main
import (
"fmt"
"io"
"net"
"os"
)
func sendFile(conn net.Conn, filePath string) {
// 只读打开文件
f, err := os.Open(filePath)
if err != nil {
fmt.Println("os.Open err:", err)
return
}
defer f.Close()
// 从本地文件中,读数据,写给网络接收端。 读多少,写多少。原封不动。
buf := make([]byte, 4096)
for {
n, err := f.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("发送文件完成。")
} else {
fmt.Println("os.Open err:", err)
}
return
}
// 写到网络socket中
_, err = conn.Write(buf[:n])
if err != nil {
fmt.Println("conn.Write err:", err)
return
}
}
}
func main() {
list := os.Args // 获取命令行参数
if len(list) != 2 {
fmt.Println("格式为:go run xxx.go 文件绝对路径")
return
}
// 提取 文件的绝对路径
filePath := list[1]
//提取文件名
fileInfo, err := os.Stat(filePath)
if err != nil {
fmt.Println("os.Stat err:", err)
return
}
fileName := fileInfo.Name()
// 主动发起连接请求
conn, err := net.Dial("tcp", "127.0.0.1:8003")
if err != nil {
fmt.Println("net.Dial err:", err)
return
}
defer conn.Close()
// 发送文件名给 接收端
_, err = conn.Write([]byte(fileName))
if err != nil {
fmt.Println("conn.Write err:", err)
return
}
// 读取服务器回发的 OK
buf := make([]byte, 16)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
if "ok" == string(buf[:n]) {
// 写文件内容给服务器——借助conn
sendFile(conn, filePath)
}
}
文件传输-接收端(服务器):
创建监听 listener,程序结束时关闭。
阻塞等待客户端连接 conn,程序结束时关闭conn。
读取客户端发送文件名。保存 fileName。
回发“ok”。
封装函数 RecvFile 接收客户端发送的文件内容。传参 fileName 和 conn
按文件名 Create 文件,结束时 Close
循环 Read 发送端网络文件内容,当读到 0 说明文件读取完毕。
将读到的内容原封不动Write到创建的文件中
package main
import (
"fmt"
"net"
"os"
)
func recvFile(conn net.Conn, fileName string) {
// 按照文件名创建新文件
f, err := os.Create(fileName)
if err != nil {
fmt.Println("os.Create err:", err)
return
}
defer f.Close()
// 从 网络中读数据,写入本地文件
buf := make([]byte, 4096)
for {
n, _ := conn.Read(buf)
if n == 0 {
fmt.Println("接收文件完成。")
return
}
// 写入本地文件,读多少,写多少。
f.Write(buf[:n])
}
}
func main() {
// 创建用于监听的socket
listener, err := net.Listen("tcp", "127.0.0.1:8003")
if err != nil {
fmt.Println(" net.Listen err:", err)
return
}
defer listener.Close()
// 阻塞监听
conn, err := listener.Accept()
if err != nil {
fmt.Println(" listener.Accept() err:", err)
return
}
defer conn.Close()
// 获取文件名,保存
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Println(" conn.Read err:", err)
return
}
fileName := string(buf[:n])
// 回写 ok 给发送端
conn.Write([]byte("ok"))
// 获取文件内容
recvFile(conn, fileName)
}
5.并发服务器开发
网络聊天室
聊天室模块划分:
主go程:创建监听socket。 for 循环 Accept() 客户端连接 —— conn。 启动 go 程 HandlerConnect
HandlerConnect:创建用户结构体对象。 存入 onlineMap。发送用户登录广播、聊天消息。处理查询在线用户、改名、下线、超时提出。
Manager:监听 全局 channel message, 将读到的消息 广播给 onlineMap 中的所有用户。
WriteMsgToClient:读取 每个用户自带 channel C 上消息(由Manager发送该消息)。回写给用户。
全局数据模块:
用户结构体: Client { C、Name、Addr string }
在现用户列表: onlineMap[string]Client key: 客户端IP+port value: Client
消息通道: message
广播用户上线:
主go程中,创建监听套接字。 记得defer
for 循环监听客户端连接请求。Accept()
有一个客户端连接,创建新 go 程 处理客户端数据 HandlerConnet(conn) defer
定义全局结构体类型 C 、Name、Addr
创建全局map、channel
实现HandlerConnet, 获取客户端IP+port —— RemoteAddr()。 初始化新用户结构体信息。 name == Addr
创建 Manager 实现管理go程。 —— Accept() 之前。
实现 Manager 。 初始化 在线用户 map。 循环 读取全局 channel,如果无数据,阻塞。 如果有数据, 遍历在线用户 map ,将数据写到 用户的 C 里
将新用户添加到 在线用户 map 中 。 Key == IP+port value= 新用户结构体
创建 WriteMsgToClient go程,专门给当前用户写数据。 —— 来源于 用户自带的 C 中
实现 WriteMsgToClient(clnt,conn) 。遍历自带的 C ,读数据,conn.Write 到 客户端。
HandlerConnet中,结束位置,组织用户上线信息, 将 用户上线信息 写 到全局 channel —— Manager 的读就被激活(原来一直阻塞)
HandlerConnet中,结尾 加 for { ;}
广播用户消息:
封装 函数 MakeMsg() 来处理广播、用户消息
HandlerConnet中, 创建匿名go程, 读取用户socket上发送来的 聊天内容。写到 全局 channel
for 循环 conn.Read n == 0 err != nil
写给全局 message —— 后续的事,原来广播用户上线模块 完成。(Manager、WriteMsgToClient)
查询在线用户:
将读取到的用户消息 msg 结尾的 “\n”去掉。
判断是否是“who”命令
如果是,遍历在线用户列表,组织显示信息。写到 socket 中。
如果不是。 写给全局 message
修改用户名:
将读取到的用户消息 msg 判断是否包含 “rename|”
提取“|”后面的字符串。存入到Client的Name成员中
更新在线用户列表。onlineMap。 key —— IP + prot
提示用户更新完成。conn.Write
用户退出:
在 用户成功登陆之后, 创建监听 用户退出的 channel —— isQuit
当 conn.Read == 0 , isQuit <- true
在 HandlerConnet 结尾 for 中, 添加 select 监听 <-isQuit
条件满足。 将用户从在线列表移除。 组织用户下线消息,写入 message (广播)
超时强踢:
在 select 中 监听定时器。(time.After())计时到达。将用户从在线列表移除。 组织用户下线消息,写入 message (广播)
创建监听 用户活跃的 channel —— hasData
只用户执行:聊天、改名、who 任意一个操作,hasData<- true
在 select 中 添加监听 <-hasData。 条件满足,不做任何事情。 目的是重置计时器。
package main
import (
"fmt"
"net"
"strings"
"time"
)
// 创建用户结构体类型!
type Client struct {
C chan string
Name string
Addr string
}
// 创建全局map,存储在线用户
var onlineMap map[string]Client
// 创建全局 channel 传递用户消息。
var message = make(chan string)
func WriteMsgToClient(clnt Client, conn net.Conn) {
// 监听 用户自带Channel 上是否有消息。
for msg := range clnt.C {
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(clnt Client, msg string) (buf string) {
buf = "[" + clnt.Addr + "]" + clnt.Name + ": " + msg
return
}
func HandlerConnect(conn net.Conn) {
defer conn.Close()
// 创建channel 判断,用户是否活跃。
hasData := make(chan bool)
// 获取用户 网络地址 IP+port
netAddr := conn.RemoteAddr().String()
// 创建新连接用户的 结构体. 默认用户是 IP+port
clnt := Client{make(chan string), netAddr, netAddr}
// 将新连接用户,添加到在线用户map中. key: IP+port value:client
onlineMap[netAddr] = clnt
// 创建专门用来给当前 用户发送消息的 go 程
go WriteMsgToClient(clnt, conn)
// 发送 用户上线消息到 全局channel 中
//message <- "[" + netAddr + "]" + clnt.Name + "login"
message <- MakeMsg(clnt, "login")
// 创建一个 channel , 用来判断用户退出状态
isQuit := make(chan bool)
// 创建一个匿名 go 程, 专门处理用户发送的消息。
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
isQuit <- true
fmt.Printf("检测到客户端:%s退出\n", clnt.Name)
return
}
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
// 将读到的用户消息,保存到msg中,string 类型
msg := string(buf[:n-1])
// 提取在线用户列表
if msg == "who" && len(msg) == 3 {
conn.Write([]byte("online user list:\n"))
// 遍历当前 map ,获取在线用户
for _, user := range onlineMap {
userInfo := user.Addr + ":" + user.Name + "\n"
conn.Write([]byte(userInfo))
}
// 判断用户发送了 改名 命令
} else if len(msg) >= 8 && msg[:6] == "rename" { // rename|
newName := strings.Split(msg, "|")[1] // msg[8:]
clnt.Name = newName // 修改结构体成员name
onlineMap[netAddr] = clnt // 更新 onlineMap
conn.Write([]byte("rename successful\n"))
} else {
// 将读到的用户消息,写入到message中。
message <- MakeMsg(clnt, msg)
}
hasData <- true
}
}()
// 保证 不退出
for {
// 监听 channel 上的数据流动
select {
case <-isQuit:
delete(onlineMap, clnt.Addr) // 将用户从 online移除
message <- MakeMsg(clnt, "logout") // 写入用户退出消息到全局channel
return
case <-hasData:
// 什么都不做。 目的是重置 下面 case 的计时器。
case <-time.After(time.Second * 60):
delete(onlineMap, clnt.Addr) // 将用户从 online移除
message <- MakeMsg(clnt, "time out leaved") // 写入用户退出消息到全局channel
return
}
}
}
func Manager() {
// 初始化 onlineMap
onlineMap = make(map[string]Client)
// 监听全局channel 中是否有数据, 有数据存储至 msg, 无数据阻塞。
for {
msg := <-message
// 循环发送消息给 所有在线用户。要想执行,必须 msg := <-message 执行完, 解除阻塞。
for _, clnt := range onlineMap {
clnt.C <- msg
}
}
}
func main() {
// 创建监听套接字
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("Listen err", err)
return
}
defer listener.Close()
// 创建管理者go程,管理map 和全局channel
go Manager()
// 循环监听客户端连接请求
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept err", err)
return
}
// 启动go程处理客户端数据请求
go HandlerConnect(conn)
}
}
6.HTTP服务器开发
Web工作方式:
客户端 ——> 访问 www.baidu.com ——> DNS 服务器。 返回 该域名对应的 IP地址。
客户端 ——> IP + port ——> 访问 网页数据。(TCP 连接。 HTTP协议。)
HTTP协议:
HTTP 协议,全称超文本传输协议(Hypertext Transfer Protocol)。顾名思义,HTTP 协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息,具体来说,主要是来规范浏览器和服务器端的行为的。
HTTP 和 HTTPS 有什么区别?
- 端口号:HTTP 默认是 80,HTTPS 默认是 443。
- URL 前缀:HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://。
- 安全性和资源消耗:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
- SEO(搜索引擎优化):搜索引擎通常会更青睐使用 HTTPS 协议的网站,因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响
URL:统一资源定位。 在网络环境中唯一定位一个资源数据。 浏览器地址栏内容。
6.1HTTP报文解析
HTTP请求包:
请求报文格式:
请求行:请求方法(空格)请求文件URL(空格)协议版本(\r\n)
GET、POST
请求头:语法格式 : key :value
空行:\r\n —— 代表 http请求头结束。
请求包体:请求方法对应的数据内容。 GET方法没有内容!!
获取http请求服务器:
package main
import (
"fmt"
"net"
"os"
)
func errFunc(err error, info string) {
if err != nil {
fmt.Println(info, err)
//return // 返回当前函数调用
//runtime.Goexit() // 结束当前go程
os.Exit(1) // 将当前进程结束。
}
}
func main() {
listener, err := net.Listen("tcp", "127.0.0.1:8000")
errFunc(err, "net.Listen err:")
defer listener.Close()
conn, err := listener.Accept()
errFunc(err, "Accpet err:")
defer conn.Close()
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if n == 0 {
return
}
errFunc(err, "conn.Read")
fmt.Printf("|%s|\n", string(buf[:n]))
}
HTTP应答包:
- 使用 net/http包 创建 web 服务器
1) 注册回调函数。
http.HandleFunc("/itcast", handler)
参1:用户访问文件位置
参2:回调函数名 —— 函数必须是 (w http.ResponseWriter, r *http.Request) 作为参数。
2)绑定服务器监听地址。
http.ListenAndServe("127.0.0.1:8000", nil)
- 回调函数:
本质:函数指针。通过地址,在某一特定位置,调用函数。
在程序中,定义一个函数,但不显示调用。当某一条件满足时,该函数由操作系统自动调用。
应答包格式:
状态行:协议版本号(空格)状态码(空格)状态码描述(\r\n)
响应头:语法格式 : key :value
空行:\r\n
响应包体: 请求内容存在: 返回请求页面内容
请求内容不存在: 返回错误页面描述。
测试服务器:
package main
import (
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// w:写回给客户端(浏览器)的数据
// r: 从 客户端 浏览器 读到的数据
w.Write([]byte("hello world"))
}
func main() {
// 注册回调函数。 该回调函数会在服务器被访问时,自动被调用。
http.HandleFunc("/handsome", handler)
// 绑定服务器监听地址
http.ListenAndServe("127.0.0.1:8000", nil)
}
测试客户端:
package main
import (
"fmt"
"net"
"os"
)
func errFunc(err error, info string) {
if err != nil {
fmt.Println(info, err)
os.Exit(1)
}
}
// 模拟浏览器
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8000")
errFunc(err, "net.Dial err:")
defer conn.Close()
httpRequest := "GET /handsome HTTP/1.1\r\nHost:127.0.0.1:8000\r\n\r\n"
conn.Write([]byte(httpRequest))
buf := make([]byte, 4096)
n, err := conn.Read(buf)
errFunc(err, "conn.Read err:")
fmt.Println(string(buf[:n]))
}
图片总结:
6.2Go语言HTTP编程
http WEB服务器:
- 注册回调函数:http.HandleFunc("/", myHandler)
func myHandler(w http.ResponseWriter, r *http.Request)
w:给客户端回发数据
r:从客户端读到的数据
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
type Request struct {
Method string // 浏览器请求方法 GET、POST…
URL *url.URL // 浏览器请求的访问路径
……
Header Header // 请求头部
Body io.ReadCloser // 请求包体
RemoteAddr string // 浏览器地址
……
ctx context.Context
}
- 绑定服务器地址结构:http.ListenAndServe("127.0.0.1:8000", nil)
参2:通常传 ni 。 表示 让服务端 调用默认的 http.DefaultServeMux 进行处理
Web服务器练习:
在计算机中选定一个目录,存放jpg、png、txt、mp3、gif、m4a等类型文件。编写一个服务器程序,可以给浏览器提供该目录下文件的访问服务。
如:目录中存有图片文件:lf.jpg。用户在浏览器中输入:127.0.0.1:8000/If.jpg可以查看该图片。“
package main
import (
"fmt"
"net/http"
"os"
)
func OpenSendFile(fNmae string, w http.ResponseWriter) {
pathFileName := "C:/Users/Lenovo/Pictures" + fNmae
f, err := os.Open(pathFileName)
if err != nil {
fmt.Println("Open err:", err)
w.Write([]byte(" No such file or directory !"))
return
}
defer f.Close()
buf := make([]byte, 4096)
for {
n, _ := f.Read(buf) // 从本地将文件内容读取。
if n == 0 {
return
}
w.Write(buf[:n]) // 写到 客户端(浏览器)上
}
}
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("客户端请求:", r.URL)
OpenSendFile(r.URL.String(), w)
}
func main() {
// 注册回调函数
http.HandleFunc("/", myHandler)
// 绑定监听地址
http.ListenAndServe("127.0.0.1:8000", nil)
}
http WEB客户端:
- 获取web服务器数据:
func Get(url string) (resp *Response, err error)
返回:http应答包,保存成 struct
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
……
}
defer resp.Body.Close()
for 循环提取 Body 数据:
n, err := resp.Body.Read(buf)
if n == 0 {
fmt.Println("--Read finish!")
break
}
if err != nil && err != io.EOF {
fmt.Println("resp.Body.Read err:", err)
return
}
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
// 使用Get方法获取服务器响应包数据
//resp, err := http.Get("http://www.baidu.com")
resp, err := http.Get("http://127.0.0.1:8005/golang")
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)
}
7.爬虫开发
7.1简单的爬虫开发
概念:访问web服务器,获取指定数据信息的一段程序。
工作流程:
明确目标 Url
发送请求,获取应答数据包。 http.Get(url)
过滤 数据。提取有用信息。
使用、分析得到数据信息。
贴吧爬虫实现:
提示用户指定 起始、终止页。 创建working函数
使用 start、end 循环 爬取每一页数据
获取 每一页的 URL —— 下一页 = 前一页 + 50
封装、实现 HttpGet() 函数,爬取一个网页的数据内容,通过 result 返回。
http.Get/ resp.Body.Close/ buf := make(4096)/ for { resp.Body.Read(buf)/ result += string(buf[:n]) return
创建 .html 文件。 使用循环因子 i 命名。
将 result 写入 文件 f.WriteString(result)。 f.close() 不推荐使用 defer 。
安徽理工大学吧-百度贴吧
下一页+50
安徽理工大学吧-百度贴吧
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func HttpGet(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1 // 将封装函数内部的错误,传出给调用者。
return
}
defer resp.Body.Close()
time.Sleep(time.Second)
// 循环读取 网页数据, 传出给调用者
buf := make([]byte, 4096)
for {
n, err2 := resp.Body.Read(buf)
if n == 0 {
fmt.Println("读取网页完成")
break
}
if err2 != nil && err2 != io.EOF {
err = err2
return
}
// 累加每一次循环读到的 buf 数据,存入result 一次性返回。
result += string(buf[:n])
}
return
}
// 爬取页面操作。
func working(start, end int) {
fmt.Printf("正在爬取第%d页到%d页....\n", start, end)
// 循环爬取每一页数据
for i := start; i <= end; i++ {
url := "https://tieba.baidu.com/f?kw=%E5%AE%89%E5%BE%BD%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err:", err)
continue
}
//fmt.Println("result=", result)
// 将读到的整网页数据,保存成一个文件
f, err := os.Create("第 " + strconv.Itoa(i) + " 页" + ".html")
if err != nil {
fmt.Println("Create err:", err)
continue
}
f.WriteString(result)
f.Close() // 保存好一个文件,关闭一个文件。
}
}
func main() {
// 指定爬取起始、终止页
var start, end int
fmt.Print("请输入爬取的起始页:")
fmt.Scan(&start)
fmt.Print("请输入爬取的终止页:")
fmt.Scan(&end)
working(start, end)
}
并发版百度贴吧爬虫:
封装 爬取一个网页内容的 代码 到 SpiderPage(index)函数中
在 working 函数 for 循环启动 go 程 调用 SpiderPage() —— > n个待爬取页面,对应n个go程
为防止主 go 程提前结束,引入 channel 实现同步。 SpiderPage(index,channel)
在SpiderPage() 结尾处(一个页面爬取完成), 向channel中写内容 。 channel <- index
在 working 函数 添加新 for 循环, 从 channel 不断的读取各个子 go 程写入的数据。 n个子go程 —— 写n次channel —— 读n次channel
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func HttpGet2(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1 // 将封装函数内部的错误,传出给调用者。
return
}
defer resp.Body.Close()
time.Sleep(time.Second)
// 循环读取 网页数据, 传出给调用者
buf := make([]byte, 4096)
for {
n, err2 := resp.Body.Read(buf)
if n == 0 {
fmt.Println("读取网页完成")
break
}
if err2 != nil && err2 != io.EOF {
err = err2
return
}
// 累加每一次循环读到的 buf 数据,存入result 一次性返回。
result += string(buf[:n])
}
return
}
// 爬取单个页面的函数
func SpiderPage(i int, page chan int) {
url := "https://tieba.baidu.com/f?kw=%E5%AE%89%E5%BE%BD%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
result, err := HttpGet2(url)
if err != nil {
fmt.Println("HttpGet err:", err)
return
}
//fmt.Println("result=", result)
// 将读到的整网页数据,保存成一个文件
f, err := os.Create("第 " + strconv.Itoa(i) + " 页" + ".html")
if err != nil {
fmt.Println("Create err:", err)
return
}
f.WriteString(result)
f.Close() // 保存好一个文件,关闭一个文件。
page <- i // 与主go程完成同步。
}
// 爬取页面操作。
func working2(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.Print("请输入爬取的起始页:")
fmt.Scan(&start)
fmt.Print("请输入爬取的终止页:")
fmt.Scan(&end)
working2(start, end)
}
7.2正则表达式
能使用 string、strings、strcnov 包函数解决的问题,首选使用库函数。 其次再选择正则表达式。
基本语法:可以借助在线网站:正则表达式在线测试 | 菜鸟工具
字符类:
“.”: 匹配任意一个字符
"[ ]": 匹配 [ ] 内任意一个字符。
“-”:指定范围: a-z、A-Z、0-9
"^": 取反。 使用在 [ ] 内部。[^xy]8
[[:digit:]] (可以替代)——> 数字 == [0-9]
数量限定符:
“?”: 匹配 前面 单元出现 0-1次
“+”:匹配 前面 单元 出现 1-N次
“*”:匹配 前面 单元 出现 0-N次
“{N}”: 匹配 前面 单元 精确匹配 N 次
"{N,}": 匹配 前面 单元 至少匹配 N 次
"{N,M}": 匹配 前面 单元 匹配 N -- M 次。
其他特殊字符:
“()”: 可以将一部分正则表达式,组成一个 单元,可以对该单元使用 数量限定符
正则表达式匹配规则:
7.3Go语言使用正则表达式
- 解析编译正则表达式:
MustCompile(str string) *Regexp
参数:正则表达式: 建议使用“反引号”——
返回值: 编译后的正则表达式 (结构体类型)
- 提取需要的数据:
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
参数1:待匹配的字符串。
参数2:匹配次数。 -1 表匹配全部
返回值: 存储匹配结果的 [ ][ ]string
数组的每一个成员都有 string1 和 string2 两个元素。
string1:表示, 带有匹配参考项的字符串。 【0】
string2:表示,不包含匹配参考项的字符串内容。【1】
字符测试:
package main
import (
"fmt"
"regexp"
)
func main() {
str := "abc a7c mfc cat 8ca azc cba"
//解析、编译正则表达式
ret := regexp.MustCompile(`a.c`) //``:表示使用原生字符串
//提取需要信息
alls := ret.FindAllStringSubmatch(str, -1) //-1表示找所有的
fmt.Println(alls)
}
小数测试:
package main
import (
"fmt"
"regexp"
)
func main() {
str := "3.14 123.123 .68 haha 1.0 abc 7. ab.3 66.6 123."
// 解析、编译正则表达式
//ret := regexp.MustCompile(`[0-9]+\.[0-9]+`)
//ret := regexp.MustCompile(`\d+\.\d+`)
ret := regexp.MustCompile(`\d\.\d`)
// 提取需要的信息
alls := ret.FindAllStringSubmatch(str, -1)
fmt.Println("alls:", alls)
}
网页标签数据测试:
举例: 提取
之中的数据1)
2) 对于 div 标签中 含有多行内容清空:
正则表达式:(?s:(.*?))
package main
import (
"fmt"
"regexp"
)
func main() {
str := `<html lang="zh-CN">
<head>
<title>Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<meta charset="utf-8">
<link rel="shortcut icon" href="/static/img/go.ico">
<link rel="apple-touch-icon" type="image/png" href="/static/img/logo2.png">
<meta name="author" content="polaris <[email protected]>">
<meta name="keywords" content="中文, 文档, 标准库, Go语言,Golang,Go社区,Go中文社区,Golang中文社区,Go语言社区,Go语言学习,学习Go语言,Go语言学习园地,Golang 中国,Golang中国,Golang China, Go语言论坛, Go语言中文网">
<meta name="description" content="Go语言文档中文版,Go语言中文网,中国 Golang 社区,Go语言学习园地,致力于构建完善的 Golang 中文社区,Go语言爱好者的学习家园。分享 Go 语言知识,交流使用经验">
</head>
<title></title>
<div>hello regexp</div>
<div>hello 2</div>
<div>hello 890</div>
<div>hello 664</div>
<div>
说出那五个字
勇士总冠军!
</div>
<body>身体</body>
<frameset cols="15,85">
<frame src="/static/pkgdoc/i.html">
<frame name="main" src="/static/pkgdoc/main.html" tppabs="main.html" >
<noframes>
</noframes>
</frameset>
</html>`
// 解析、编译正则表达式
//ret := regexp.MustCompile(`<div>(.*)</div>`)
ret := regexp.MustCompile(`<div>(?s:(.*?))</div>`) //单行模式
// 提取需要的信息
alls := ret.FindAllStringSubmatch(str, -1)
//fmt.Println("alls:", alls)
for _, one := range alls {
fmt.Println("one[0]=", one[0])
fmt.Println("one[1]=", one[1])
}
}
7.4爬取豆瓣电影
双向爬取:
横向:以页为单位。
纵向:以一个页面内的条目为单位。
横向:
下一页+25
豆瓣电影 Top 250 1
豆瓣电影 Top 250 2
豆瓣电影 Top 250 3
豆瓣电影 Top 250 4
纵向:
电影名称: <img width="100" alt="电影名称" ——> <img width="100" alt="(.*?)"
分数:分数 ——>
<span class="rating_num" property="v:average">(.*?)</span>
评分人数: 评分人数 人评价 ——> <span>(.*?)人评价</span>
爬取豆瓣电影信息:
获取用户输入 起始、终止页、启动 toWork 函数 循环 调用 SpiderPageDB(url) 爬取每一个页面
SpiderPageDB 中, 获取 豆瓣电影 横向爬取url 信息。封装 HttpGet 函数,爬取一个页面所有数据 存入 result 返回
找寻、探索豆瓣网页 纵向爬取规律。找出“电影名”、“分数”、“评分人数”网页数据特征。
分别 对这三部分数据使用 go 正则函数: 1) 解析、编译正则表达式 2) 提取信息 ——> string[1]: 代表没有 匹配参考项内容。
将提取到的数据,按自定义格式写入文件。使用 网页编号命名文件。
实现并发。 1) go SpiderPageDB(url) 。
2) 创建 channel 防止主 go 程退出
3) SpiderPageDB 函数末尾,写入 channel
4) 主 go 程 for 读取 channel 。
注意:现在这段代码已经爬取不了了,估计是加了反爬虫机制,具体了解这段代码的用法含义就行
package main
import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
)
// 爬取指定url 的页面,返回 result
func HttpGetDB(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}
defer resp.Body.Close()
buf := make([]byte, 4096)
// 循环爬取整页数据
for {
n, err2 := resp.Body.Read(buf)
if n == 0 {
break
}
if err2 != nil && err2 != io.EOF {
err = err2
return
}
result += string(buf[:n])
}
return
}
func Save2file(idx int, filmName, filmScore, peopleNum [][]string) {
path := "第 " + strconv.Itoa(idx) + " 页.txt"
f, err := os.Create(path)
if err != nil {
fmt.Println("os.Create err:", err)
return
}
defer f.Close()
n := len(filmName) // 得到 条目数。 应该是 25
// 先打印 抬头 电影名称 评分 评分人数
f.WriteString("电影名称" + "\t\t\t" + "评分" + "\t\t" + "评分人数" + "\n")
for i := 0; i < n; i++ {
f.WriteString(filmName[i][1] + "\t\t\t" + filmScore[i][1] + "\t\t" + peopleNum[i][1] + "\n")
}
}
// 爬取一个豆瓣页面数据信息
func SpiderPageDB(idx int, page chan int) {
// 获取 url
url := "https://movie.douban.com/top250?start=" + strconv.Itoa((idx-1)*25) + "&filter="
// 封装 HttpGet2 爬取 url 对应页面
result, err := HttpGetDB(url)
if err != nil {
fmt.Println("HttpGet2 err:", err)
return
}
//fmt.Println("result=", result)
// 解析、编译正则表达式 —— 电影名称:
ret1 := regexp.MustCompile(`<img width="100" alt="(?s:(.*?))"`)
// 提取需要信息
filmName := ret1.FindAllStringSubmatch(result, -1)
fmt.Println("filmName=", filmName)
// 解析、编译正则表达式 —— 分数:
pattern := `<span class="rating_num" property="v:average">(?s:(.*?))</span>`
ret2 := regexp.MustCompile(pattern)
// 提取需要信息
filmScore := ret2.FindAllStringSubmatch(result, -1)
fmt.Println("filmScore=", filmScore)
// 解析、编译正则表达式 —— 评分人数:
ret3 := regexp.MustCompile(`<span>(?s:(\d*?))人评价</span>`)
//ret3 := regexp.MustCompile(`<span>(.*?)人评价</span>`)
// 提取需要信息
peopleNum := ret3.FindAllStringSubmatch(result, -1)
fmt.Println("peopleNum=", peopleNum)
// 将提取的有用信息,封装到文件中。
Save2file(idx, filmName, filmScore, peopleNum)
// 与主go程配合 完成同步
page <- idx
}
func toWork(start, end int) {
fmt.Printf("正在爬取 %d 到 %d 页...\n", start, end)
page := make(chan int) //防止主go 程提前结束
for i := start; i <= end; i++ {
go SpiderPageDB(i, page)
}
for i := start; i <= end; i++ {
fmt.Printf("第 %d 页爬取完毕\n", <-page)
}
}
func main() {
// 指定爬取起始、终止页
var start, end int
fmt.Print("请输入爬取的起始页(>=1):")
fmt.Scan(&start)
fmt.Print("请输入爬取的终止页(>=start):")
fmt.Scan(&end)
toWork(start, end)
}
//使用goquery实现爬虫(后续学习) 还有一个gocolly框架(后续学习)
安装依赖:
go get github.com/PuerkitoBio/goquery
package main
import (
"fmt"
"net/http"
"os"
"strconv"
"github.com/PuerkitoBio/goquery"
)
// 解析代码示例 可以删除
func fetch(url string) (*goquery.Document, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 使用goquery包解析网页内容
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, err
}
return doc, nil
}
// 保存文件函数
func Savefile(filmName string, filmScore string, peopleNum string, file *os.File) {
// 写入电影信息
_, err := file.WriteString(filmName + "\t\t\t" + filmScore + "\t\t" + peopleNum + "\n")
if err != nil {
fmt.Println("写入文件失败:", err)
return
}
fmt.Println("写入文件成功:", filmName)
}
// 爬虫本体
func spider() {
// 设定客户端
client := &http.Client{}
// 创建文件
path := "豆瓣电影top250.txt"
file, err := os.Create(path)
if err != nil {
fmt.Println("os.Create err:", err)
return
}
defer file.Close()
// 先打印 抬头 电影名称 评分 评分人数
file.WriteString("电影名称" + "\t\t\t" + "评分" + "\t\t" + "评分人数" + "\n")
// 发送请求 爬取n页 换页+25
for i := 0; i < 250; i += 25 {
req, err := http.NewRequest("GET", "https://movie.douban.com/top250?start="+strconv.Itoa(i)+"&filter=", nil)
if err != nil {
fmt.Println("req err", err)
}
// 仿造header防止浏览器检测到是爬虫访问
req.Header.Set("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")
// req.Header.Set("Accept-Encoding", "gzip, deflate, br") 无用header 若添加会导致乱码
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
req.Header.Set("Cache-Control", "max-age=0")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0")
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-Site", "none")
req.Header.Set("Sec-Fetch-User", "?1")
req.Header.Set("Upgrade-Insecure-Requests", "1")
resp, err := client.Do(req)
if err != nil {
fmt.Println("请求失败", err)
} else {
fmt.Println("读取网页成功:", "https://movie.douban.com/top250?start="+strconv.Itoa(i)+"&filter=")
}
defer resp.Body.Close()
DocDetial, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
fmt.Println("解析失败", err)
}
for j := 1; j <= 25; j++ {
title := DocDetial.Find("#content > div > div.article > ol > li:nth-child( " + strconv.Itoa(j) + ") > div > div.info > div.hd > a > span:nth-child(1)").Text()
rating := DocDetial.Find("#content > div > div.article > ol > li:nth-child( " + strconv.Itoa(j) + ") > div > div.info > div.bd > div > span.rating_num").Text()
votes := DocDetial.Find("#content > div > div.article > ol > li:nth-child( " + strconv.Itoa(j) + ") > div > div.info > div.bd > div > span:nth-child(4)").Text()
// 去除评论人数中的“人评价”字样
votes = votes[:len(votes)-3]
// 写入文件
Savefile(title, rating, votes, file)
}
}
}
func main() {
spider()
}
7.5图片爬取
斗鱼颜值图片爬取:
package main
import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
)
func SaveImg(idx int, url string, page chan int) {
path := strconv.Itoa(idx+1) + ".jpg"
f, err := os.Create(path)
if err != nil {
fmt.Println(" http.Get err:", err)
return
}
defer f.Close()
resp, err := http.Get(url)
if err != nil {
fmt.Println(" http.Get err:", err)
return
}
defer resp.Body.Close()
buf := make([]byte, 4096)
for {
n, err2 := resp.Body.Read(buf)
if n == 0 {
break
}
if err2 != nil && err2 != io.EOF {
err = err2
return
}
f.Write(buf[:n])
}
page <- idx
}
func main() {
url := "https://www.douyu.com/g_yz"
// 爬取 整个页面,将整个页面全部信息,保存在result
result, err := HttpGet1(url)
if err != nil {
fmt.Println("HttpGet err:", err)
return
}
// 解析编译正则 <img loading="lazy" src="url" (?s:(.*?))
ret := regexp.MustCompile(`<img loading="lazy" src="(?s:(.*?))"`)
// 提取每一张图片的 url
alls := ret.FindAllStringSubmatch(result, -1)
page := make(chan int)
n := len(alls)
for idx, imgURL := range alls {
//fmt.Println("imgURL:", imgURL[1])
go SaveImg(idx, imgURL[1], page)
}
for i := 0; i < n; i++ {
fmt.Printf("下载第 %d 张图片完成\n", <-page)
}
}
// 获取一个网页所有的内容, result 返回
func HttpGet1(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}
defer resp.Body.Close()
buf := make([]byte, 4096)
for {
n, err2 := resp.Body.Read(buf)
if n == 0 {
break
}
if err2 != nil && err2 != io.EOF {
err = err2
return
}
result += string(buf[:n])
}
return
}
版权归原作者 王猪精 所有, 如有侵权,请联系我们删除。