指针
指针就是地址,指针变量就是存储地址的变量
*p : 解引用,间接引用
栈帧:用来给函数运行提供内存空间,取内存于stack上。
当函数调用时,产生栈帧,函数调用结束,释放栈帧。
栈存放:局部变量,形参,内存字段描述值(栈基指针与栈顶指针)。
指针使用注意:
空指针:未被初始化的指针。
野指针:被一片无效的地址初始化的指针。
new:在heap上申请一片内存地址空间
%q:打印go语言格式的字符串
变量存储:
等号左边的变量代表变量所指向的内存空间。(写)
等号右边的变量代表变量内存存储的数据值。(读)
指针的函数传参(传引用):
传引用。:将地址值作为函数参数,返回值后传递。
传值:将实参的值拷贝一份给形参。
传引用:在A栈帧内部,修改B栈帧中的变量值。
函数传参:值传递。
切片
为什么切片:
1. 数组的容量固定,不能自动扩展。
2. 值传递。数组作为函数参数时,将整个数组拷贝一份给形参。
在go语言中,几乎可以使用切片代替数组使用。
-
切片的本质:
- 不是一个数组的指针,是一种数据结构体,用了操作数组内部元素。
-
数组和切片区别:
- 数组指定长度
- 切片不需要。
-
切片的使用:
- 切片名称[low:high:max]
- low:起始下标位置
- high:结束下标位置 len = high - low
- 容量:cap = max- low。
- 截取数组,初始化切片时,如果没有指定容量,则容量跟随原数组(切片)容量。
-
切片创建:
- slice := []int{1,2,3,4}
- slice := make([]int,长度,容量)
- slice := make([]int,长度) 创建切片时,没有指定容量,容量==长度
切片做函数参数--传引用。
-
append:在切片末尾添加
- append(切片对象,待追加元素)
- 向切片增加元素时,切片的容量会自动增长,
-
copy:
- copy(目标位置切片,源切片)
- 拷贝过程中,直接位置拷贝。
map
字典、映射:key-value ,key唯一且无序,不能为引用类型或包含引用类型的结构。
map不能使用cap。
cap支持:array、slice、channel
-
创建方式:
var m1 map[int]string //此种声明为nil,没有空间不能直接存储key-value m2 := map[int]string{} //可以存放,长度为0, m3 := make(map[int]string) //没有指定长度,默认len=0 m4 := make(map[int]string,5) //指定长度len=5
-
初始化:
var m map[int]string = map[int]string{1:"ivn",2:"osenc"} //key不能重复 m2 := map[int]string{1:"ivn",2:"osenc"} //key不能重复
-
赋值:赋值过程中,如果map的key与之前的相同,则替换,不同的添加。
m3 := make(map[int]string,1) m3[100] = "string" m3[3] = "hello" m3[3] = "world" //将之前key为3的替换。
-
遍历
for k,v := range m{ fmt.Printf("key:%d----value:%d\n",k,v) } for k:= range m{ fmt.Printf("key:%d\n",k) }
-
判断
var m map[int]string = map[int]string{1:"ivn",2:"osenc"} if v,has := m[1];has{ fmt.Println("value:",v,"has:",has) }else{ fmt.Println("value:",v,"has:",has) } //返回value,bool是否存在?
-
做函数参数、返回值,传引用。
删除失败则什么也不做。func mapDelete(m map[int]string,key int){ delete(m,key) } func main(){ var m map[int]string = map[int]string{1:"ivn",2:"osenc"} mapDelete(m,2) fmt.Println(m) }
package main
import(
"fmt"
"strings"
)
func wordCountFunc(str string)map[string]int{
s := strings.Fields(str) //将字符串拆分为字符串切片
m := make(map[string]int) //创建一个用于存储word次数map
for ,v := range s{
if _,has := m[v];has{
m[v]++
}else{
m[v] = 1
}
}
return m
}
func main(){
str := "I lov my work and I love my family too"
mret := wordCountFunc(str)
for k,v := range mret{
fmt.Println(k + ":" + v)
}
}
结构体
是一种数据类型,类型定义
内部的成员变量不能赋值,只能用这种定义方式,不能使用var name string 定义内部变量。
type Person struct{
name string
sex byte
age int
}
-
普通变量定义和初始化
-
顺序初始化,依次将结构体内部的成员初始化。
var man Person = Person{"andy",'m',20}
-
指定初始化成员:
var man Person = Person{name:"andy"}
-
-
普通变量的赋值和使用:
-
使用“."索引成员变量
man.name = "andy"
-
-
结构体变量的比较和赋值:
- 比较:只能使用==和!= 不能使用> < >= <=
- 相同结构体(成员变量的顺序、内容相同)之间可以直接赋值
-
结构体传参:
- 讲结构体变量的值拷贝一份,传递。 --几乎不用,内存消耗大,效率低
-
结构体地址:
结构体变量的地址==结构体首个元素的地址。
unsafe.Sizeof(变量名)-->求此种类型变量的占用内存大小。 string在结构体中类型一般会16字节,int一般在64位系统下会自动占8个字节, 这两个组成的结构体,虽然16比8大,但是依然是以8字节对齐的,这与内存的对齐结构、CPU、数据总线等有关,以8字节对齐,这种结构体所占内存大小为8
指针与结构体
-
指针遍历的定义和初始化:
-
顺序初始化:依次将结构体内部所有成员初始化
var man *Person = &Person{"andy",'m',20}
-
new(Person)
var man *Person man = new(Person)
-
-
指针索引成员变量:
-
使用“."索引成员变量
man.name = "andy"
-
-
结构体地址:
- 结构体变量的值==结构体首个元素的地址。
-
结构体指针传参:
unsafe.Sizeof(指针)-->不管何种类型的指针,64位系统下,大小一直,均为8字节。 讲结构体变量地址值传递,传引用--效率高,节省空间。
- 结构体指针做函数返回值:
- 不能返回局部变量的地址值。 -- 局部变量保存在栈帧上,函数调用结束后,栈帧是否,局部变量的地址,不能受系统的保护,随时可能分配给其他程序。-->个人认为这一条有歧义,因为有可能会发生逃逸。
- 可以返回局部变量的值。
字符串
strings.Split(str,sep) //str按sep拆分
strings.Fields(str) //str按空格拆分
strings.HasSuffix(str,sep) //判断str是否sep结束标记
strings.HasPrefix(str,sep) //判断str是否sep开始标记
文件
-
创建文件Create: 文件不存在后缀,文件存在,将文件内容清空。
- 参数:name,打开文件的路径:绝对路径、相对路径
-
打开文件 Open: 以只读方式打开文件。文件不存在,打开失败
- 参数name:打开文件的路径:绝对路径、相对路径
-
打开文件OpenFile: 以只读、只写、读写方式打开文件。
- 参数 name:打开文件的路径:绝对路径、相对路径
- 参数打开文件权限:O_RDONLY,O_WRONLY,O_RDWR
- 参数3:一般传6
-
写文件:
- 按字符串写:WriteString() 返回n个写入的字符,
- 回车换行:win:\r\n Linux:\n
- 按位置写:Seek():修改文件的读写指针位置
- 参1:偏移量,正数:向文件尾偏,负:向文件头偏。
- 参2:偏移起始位置
- io.SeekStart:文件起始位置
- io.SeekCurrent:文件当前位置
- io.SeekEnd:文件结束位置
- 返回值:从起始位置,到当前文件读写指针位置的偏移量。
- 按字节写:WriteAt():在文件指定偏移位置,写入[]byte,通常搭配Seek()
- 参1:[]byte:要写入的数据
- 参2:偏移量。
- 返回值:实际写入的字节数。
- 按字符串写:WriteString() 返回n个写入的字符,
-
读文件:
- 按行读:buf,err := bufio.NewReader(f).ReadBytes('\n') fmt.Println(string(buf))]
- 创建一个带有(用户)缓冲区的Reader(读写器)
- reader := bufio.NewReader(打开文件的指针)
- 从reader的缓冲区中,读取指定长度的数据,数据长度取决于参数dlime
- buf,err := reader.ReadBytes('\n') 按行读
- 判断到达文件结尾:if err != nil && err == io.EOF 到文件结尾。
- 文件结束标记,是单独读一次获取到的。
- 创建一个带有(用户)缓冲区的Reader(读写器)
- 按行读:buf,err := bufio.NewReader(f).ReadBytes('\n') fmt.Println(string(buf))]
-
按字节读、写文件
- read():按字节读文件 - write():按字节写文件
创建一个读文件,一个写文件,读多少,写多少即可。
- buf := make([]byte,4096) //创建缓冲区
- for {
- n,err := f_r.Read(buf)
- if err != nil && err == io.EOF{
fmt.Println("over")
return
- }
- f_w.Write(buf[:n]) //读多少写多少
- }
目录操作:
-
打开目录:OpenFile
- 参1:路径
- 参2:只读方式os.O_RDONLY
- 参3:权限os.ModeDir
- 返回值:返回一个可以读写目录的文件指针。
-
读目录:ReadDir
参数:n:几个目录项。-1读取全部目录项。
f,err := os.OpenFile(path,os.O_RDONLY,os.ModeDir) if err != nil{ return } defer f.Close() info,err := f.ReadDir(-1) for _,fileinfo := range info{ if fileinfo.IsDir(){ fmt.Println(fileInfo.Name(),"是目录") }else{ fmt.Println(fileInfo.Name(),"是文件") } } if strings.HasSuffix(fileInfo.Name(),".jpg"){ //判断文件是否以jpg结尾的文件 }
返回值:fileinfo切片。
- 参考代码:
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func OpenDir(path *string) *os.File{
f,err := os.OpenFile(*path,os.O_RDONLY,os.ModeDir)
if err != nil{
fmt.Println("打开目录失败")
return nil
}
return f
}
func FindFile(f *os.File,rep string)(str []string){
info,err := f.Readdir(-1)
if err != nil{
fmt.Println("读取目录失败")
return nil
}
for _,fileinfo := range info{
if strings.HasSuffix(fileinfo.Name(),rep){
str = append(str,fileinfo.Name())
}
}
return str
}
func Read(str []string)(lstr []string){
for _,s := range str{
f,err := os.Open(s)
if err != nil{
fmt.Println("打开文件"+s+"失败")
return
}
defer f.Close()
buf := bufio.NewReader(f)
for{
by,err := buf.ReadBytes('\n') //byte切片输出什么?
if err != nil && err == io.EOF{
fmt.Println("读取文件"+s+"结束")
break
}else if err != nil{
fmt.Println("读取文件"+s+"失败")
return
}
lstr = append(lstr,strings.Fields(string(by))...)
}
}
return
}
func main(){
//打开指定目录
var path string
fmt.Scanln(&path)
fd := OpenDir(&path)
defer fd.Close()
//D:/code/ubuntu_code/Go/src/github.com/cold-rivers-snow/study/GoAdvance/01ptr/
//找到指定的文件
var rep string
fmt.Scanln(&rep)
str := FindFile(fd,rep)
//打开文件并读取内容
lstr := Read(str)
var count int = 0
for _,v := range lstr {
if v == "return"{
count++
}
}
fmt.Println(count)
}
1s = 1000ms
1ms = 1000us
1us = 1000ns
并行:借助多核CPU实现。
-
并发:
- 宏观:用户体验上,程序在并行执行。
-
微观:多个计划任务,顺序执行。在飞快的切换,轮换使用CPU时间轮片。
-
进程并发:
- 程序:编译成功得到的二进制文件。 占用磁盘空间。 死的 1
- 进程:运行起来程序,占用系统资源(内存) 活的 N
进程的状态:初始态、就绪态、运行态、阻塞态、终止态。
-
线程并发:
- 线程:LWP轻量级的进程,最小的执行单位。-- CPU分配时间轮片的对象
- 进程:最小的系统资源分配单位。
-
同步:
- 协同步调,规划先后顺序。
-
线程同步:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回,同时其他线程为保证数据一致性,不能调用该功能。
- 线程同步机制:
- 互斥锁(互斥量):建议锁,拿到锁后,才能访问数据,没有拿到锁的线程,阻塞等待,等到拿锁的线程释放锁。
- 读写锁:一把锁(读属性、写属性)。写独占,读共享。写锁优先级高。
- 线程同步机制:
-
协程并发:
- 协程:coroutine。轻量级的线程。
- Python、Lua、Rust ......新兴语言有。
- 提高程序执行效率。
-
总结:
- 进程、线程、协程都可以完成并发。
- 进程:稳定性强
- 线程:节省资源
- 协程:效率高
-
go程:
-
-
go程:
- 创建:
- 创建于进程中,直接使用go关键字,放置于函数调用签名,产生一个go程,并发。
- 特性:
- 主goroutine结束,子goroutine随之退出。
- 创建:
-
runtime包:
runtime.Gosched():出让当前go程所占用的CPU时间片。当再次获得CPU时,从出让位置继续恢复执行。
func main(){ go func(){ for i:= 0;i < 10;i++{ fmt.Println("this is groutine test") //time.Sleep(100*time.Microsecond) } }() for{ runtime.Gosched() //出让当前go程所占用CPU时间片 fmt.Println("this is main test") //time.Sleep(100*time.Microsecond) } }
runtime.Goexit():结束调用改函数的当前go程,Goexit()之前注册的defer都生效。
return:返回当前函数调用到调用者那里去。return之前的defer注册。
func test(){ defer fmt.Println("cc") return //runtime.Goexit() fmt.Println("dd") } func main(){ go func(){ defer fmt.Println("aaa") test() defer fmt.Println("bbb") }() for{ ; } }
-
runtime.GOMAXPROCS()设置当前进程使用最大CPU核数。,返回上一次的核数
func main(){ n := runtime.GOMAXPROCS(1) //返回的是上一次的核数。 fmt.Println(n) for{ go fmt.Print(0) //子go程 fmt.Print(1) //主go程 } }
runtime.GOROOT() 返回go的根目录
channel
是一种数据类型,对应一个“管道”、(通道FIFO)。
内部实现了同步,确保并发安全。
通过通信来共享内存,而不是共享内存来通信。
-
channel的定义
make(chan 在channel中传递的数据类型,容量) 容量=0,无缓冲channel,容量> 0,有缓冲channel
make (chan int) make(chan int ,5)
//全局定义channel,用了数据同步 var channel = make(chan int) func printer(s string){ for _,ch := range s{ fmt.Printf("%c",ch) time.Sleep(300 * time.Millisecond) } } func person1(){ //person1先执行 printer("hello") channel <- 8 } func person2(){ //person2后执行 <-channel printer("workd") } func main(){ go person1() go person2() for{ ; } }
-
补充知识点:
- 每当一个进程启动,系统会自动打开三个文件:标准输入、标准输出、标准错误 -- 对应stdin(0)、stdout(1)、stderr(2)
- 当进程运行结束,操作系统自动关闭三个文件。
-
channel有两个端:
- 一端:写端(传入端) chan <-
- 另一端:读端(传出端) <- chan
- 要求:读端和写端必须同时满足条件,才能在chan上进行数据流动,否则,则阻塞。
-
channel同步传递数据
func main(){ ch := make(chan string) //无缓冲channel fmt.Println(len(ch),cap(ch)) //len求取剩余未读取数据个数,cap通道容量。 go func(){ for i:=0;i < 2;i++{ fmt.Println(i) } //通知go打印 ch <- "子go打印完毕" }() str := <- ch fmt.Println(str) }
-
无缓冲channel:-- 同步通信
- 通道容量为0,len = 0
- channel应用于两个go程中。一个读,一个写。
- 具备同步的能力,读写同步.
-
有缓冲channel:-- 异步通信
通道容量为非0,len(ch):channel中剩余未读取的数据个数,cap(ch):通道的容量
channel应用于两个go程中。一个读,一个写。
缓冲区可以进行数据存储,存储到容量上限,阻塞,具有异步的能力,不需要同时操作channel缓冲区。
func main(){ ch := make(chan int,3) //存满3个元素之前不会阻塞 fmt.Println(len(ch),cap(ch)) go func(){ for i:=0;i < 8;i++{ fmt.Println("子go程",i,len(ch),cap(ch)) ch <- i } }() time.Sleep(time.Second*4) for i:=0;i < 8;i++{ num := ch fmt.Println("主go程",num,len(ch),cap(ch)) } }
io输出会有延迟。
-
关闭channel:
确定不再向对端发送、接收数据。使用close(ch)关闭ch channel
-
对端可以判断channel是否关闭:
if num,ok ;= <- ch;ok == true{ 如果对端已经关闭,ok ->false,num无数据 如果对端没有关闭,ok ->true,num保存读到的数据。 }
func main(){ ch := make(chan int) go func(){ for i:=0;i < 8;i++{ ch <- i } close(ch) //写端,写完数据主动关闭channel }() //ok方式 for{ if num,ok := <- ch;ok == true{ fmt.Println("读到数据",true) }else{ break } } //range方式读取 for num := range ch{ fmt.Println(ch) } }
-
总结:
- 数据不发送完,不应该关闭。
- 已经关闭的channel,不能再向其写数据,报错(panic:send on closed channel),可以读数据,读到的数据为0 -- 写端关闭。无缓冲channel :读到0,有缓冲channel:如果缓冲区内有数据则读数据,读到数据完毕之后,可以继续读,读到0/false/""等默认值。
-
单向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
- 双向channel可以饮食转换为任意一种单向channel
传参:传引用。
func main(){ ch := make(chan int) //双向channel var sendCh chan <- int = ch sendCh <- 789 num:= sendCh var recvCh <- chan int = ch num := <- recvCh //反向赋值 var ch2 chan int = sendCh //error }
func send(out chan<- int){ out <- 89 close(out) } func recv(in <- chan int){ n := <- in fmt.Println(n) } func main(){ ch := make(chan int) go func(){ send(ch) //双向channel转为写channel }() recv(ch) }
生产者消费者模型
- 生产者:发送数据端
- 消费者:接收数据端
- 缓冲区:
- 解耦:降低生产者和消费者之间的耦合度
- 并发:生产者消费者不对等时,能保持正常通信
- 缓存:生产者和消费者数据处理速度不一致时,暂存数据
//生产者消费者模型
func producer(out chan<- int){
for i:=0;i < 10;i++{
out <- i*i
}
close(out) //不写close会怎样?只有写关闭,才能一次通信完毕。
}
func consumer(in <-chan int){
for num := range in{
fmt.Println("消费者拿到",num)
}
}
func main(){
ch := make(chan int)
//ch := make(chan int,5)
go producer(ch) //子go程 生产者
consumer(ch) //主go程 消费者
}
定时器
time.Timer:创建定时器,指定定时时长,定时到达后,系统会自动向定时器的成员C写系统当前时间(对chan的写操作)
type Timer struct{ C <- chan Time r runtimeTimer }
读Timer.C得到定时后的系统时间,并且完成一次chan的读操作。
func main(){ fmt.Println("当前时间",time.Now()) //创建定时器 myTime := time.NewTimer(time.Second*2) nowTime := <- myTime.C //channel类型 fmt.Println("现下时间",nowTime) }
-
定时方法:time包
- 第一种:sleep
- 第二种:Timer
- time.After
fmt.Println("当前时间",time.Now()) nowTim := <-time.After(time.Second*2) fmt.Println(nowTim)
-
定时器的停止和重置
func main(){ myTimer := time.NewTimer(time.Second*2) //创建定时器 myTimer.Reset(1 * time.Second) //重置定时器时长 go func(){ <- myTimer.C fmt.Println("子go程,定时完毕") }() myTimer.Stop() //设置定时器停止,归0 再执行<-myTime.C会阻塞 for{ ; } }
-
周期定时:
time.Ticker
type Ticker struct{ C <- chan Time r runtime.Timer }
func main(){ quit := make(chan bool) fmt.Println(time.Now()) myTicker := time.NewTicker(time.Second) //定义一个定时器 i := 0 go func(){ nowTime := <- myTicker.C i++ fmt.Println(nowTime) if i == 8{ quit <- true //解除 主go程阻塞 } }() <-quit //子go程 循环获取<-myTicker.C期间,一直阻塞 }
select
可以监听channel上的数据流动,读、写。
case语句必须是IO操作,不能任意写判别表达式
按照顺序从头到尾评估每一个发送和接收的数据,如果任意一个语句可以继续执行,则从可执行的语句中任意选择一条来使用。
func main(){ ch := make(chan int) //用来进行数据通信的channel quit := make(chan bool) //用来判断是否退出的channel //ch2 := make(chan string) go func(){ //写数据 for i:= 0;i < 5;i++{ ch <- i time.Sleep(time.Second) } close(ch) quit<-true //通知主go程退出 runtime.Goexit() }() for{ //主go程 读数据 select{ case num := <- ch: fmt.Println(num) case <- quit: return //终止进程 } fmt.Println("------------------") } }
select判断不能分两步,判断的时候直接拿出来。
-
注意事项:
- 监听case中,没有满足的监听条件,阻塞
- 监听的case中,有多个满足监听条件,任选一个执行。
- 可以使用default来出来所以case都不满足监听条件的状态,通常不用(会产生忙轮询)
- select自身不带有循环机制,需借助for来循环监听
- break只能跳出select中额一个case选项,类似于switch中的用法。
-
select实现fibonacci数列:
func fibonacci(ch <-chan int,quit <-chan bool){ for { select{ case num := <-ch: fmt.Println(num) case <-quit: //return runtime.Goexit() //等效于return } } } func main(){ ch := make(chan int) quit := make(chan bool) go fibonacci(ch,quit) //子go程,打印channel x,y := 1,1 for i := 0;i < 20;i++{ ch <- x x,y = y,x + y } quit <- true }
-
select超时
func main(){ ch := make(chan int) quit := make(chan bool) go func(){ //子go程获取数据 for{ select{ case num:= <-ch: fmt.Println(num) case <-time.After(3*time.Second) quit<-true return //runtime.Goexit() } } }() for i:=0;i <2;i++{ ch <- i time.Sleep(2*time.Second) } <- quit fmt.Println("finish") }
goto 的标签必须在本函数内。
死锁
-
单go程自己死锁
-
channel应该至少2个及以上的go程中通信
//死锁版 func main(){ ch := make(chan int) ch <- 789 num := <-ch fmt.Println("num:",num) }
//正确版 func main(){ ch := make(chan int) num := <-ch fmt.Println("num:",num) go func(){ ch <- 789 }() }
-
-
go程间channel访问顺序导致死锁
使用channel一端读(写),要保证另一端(写)读操作,同时有机会执行。否则死锁。
//错误版 func main(){ ch := make(chan int) num := <-ch fmt.Println(num) go func(){ ch <- 789 }() }
//正确版 func main(){ ch := make(chan int) go func(){ ch <- 789 }() num := <-ch fmt.Println(num) }
-
多go程,多channel交叉死锁。
func main(){ ch1 := make(chan int) ch2 := make(chan int) go func(){ //子 for{ select{ case num := <-ch1: ch2 <- num } } }() for{ select{ case num := <-ch2: ch1 <- num } } }
在go语言中,尽量不要将互斥锁、读写锁与channel混用---隐性死锁
不是一种锁,是一种错误使用锁的现象。
-
互斥锁
//使用channel实现同步 ch := make(chan int) func printer(str string){ for _,ch := range str{ fmt.Printf("%c",ch) time.Sleep(time.Millisecond*300) } } func person1(){ printer("hello") ch <- 98 } func person2(){ <- ch printer("world") } func main(){ go person1() go person2() for{ ; } }
//使用互斥锁实现同步 var mutex sync.Mutex //创建一个互斥量,新建的互斥锁状态为0,未加锁状态,锁只有一把 func printer(str string){ for _,ch := range str{ fmt.Printf("%c",ch) time.Sleep(time.Millisecond*300) } } func person1(){ mutex.Lock() //访问数据之前加锁 printer("hello") mutex.Unlock() //共享数据访问结束解锁。 } func person2(){ printer("world") } func main(){ go person1() go person2() for{ ; } }
A、B go程共同访问共享数据,由于CPU调度随机,需要对共享数据访问顺序加以限定(同步)。创建mutex(互斥锁),访问共享数据之前,加锁,访问结束,解锁。在Ago程加锁期间,Bgo程加锁失败--阻塞。直到Ago程解锁mutex,B从阻塞处,恢复执行。
读写锁
sync.RWMutex
读时共享,写时独占,写锁优先级比读锁高。
产生死锁:
var rwMutex sync.RWMutex func readGo(in <-chan int,idx int){ for{ rwMutex.RLock() //读模式加锁 num := <- in fmt.Printf("%dth 读go程,读出:%d\n",idx,num) rwMutex.RUnlock() } } func writeGo(out chan<- int,idx int){ for{ //生成随机数 num := rand.Intn(1000) rwMutex.Lock() //写模式加锁 out <- num fmt.Printf("%dth 写go程,写入:%d\n",idx,num) time.Sleep(time.Millisecond*300) //放大实验现象 rwMutex.Unlock() } } func main(){ //播种随机数种子 rand.Send(time.Now().UnixNano()) //quit := make(chan bool) //用于关闭主go程channel ch := make(chan int) //用于数据传递channel for i:=0;i < 8;i++{ go readGo(ch,i+1) } for i:=0;i < 8;i++{ go writeGo(ch,i+1) } //<-quit for{ ; } }
-
在go语言中,尽量不要将互斥锁、读写锁与channel混用---隐性死锁
-
读写锁数据同步
var rwMutex sync.RWMutex var value int //定义全局变量模拟共享数据 func readGo(idx int){ for{ rwMutex.RLock() //读模式加锁 num := value fmt.Printf("%dth 读go程,读出:%d\n",idx,num) rwMutex.RUnlock() } } func writeGo(idx int){ for{ //生成随机数 num := rand.Intn(1000) rwMutex.Lock() //写模式加锁 value = num fmt.Printf("%dth 写go程,写入:%d\n",idx,num) time.Sleep(time.Millisecond*300) //放大实验现象 rwMutex.Unlock() } } func main(){ //播种随机数种子 rand.Send(time.Now().UnixNano()) for i:=0;i < 8;i++{ go readGo(i+1) } for i:=0;i < 8;i++{ go writeGo(i+1) } for{ ; } }
//channel替换读写锁 func readGo(in <-chan int,idx int){ for{ num := <-in fmt.Printf("%dth 读go程,读出:%d\n",idx,num) } } func writeGo(out chan<- int,idx int){ for{ //生成随机数 num := rand.Intn(1000) out <- num fmt.Printf("%dth 写go程,写入:%d\n",idx,num) time.Sleep(time.Millisecond*300) //放大实验现象 } } func main(){ //播种随机数种子 rand.Send(time.Now().UnixNano()) ch := make(chan int) for i:=0;i < 8;i++{ go readGo(ch,i+1) } for i:=0;i < 8;i++{ go writeGo(ch,i+1) } for{ ; } }
-
条件变量
-
本身不是锁,但经常与锁结合使用。
-
-
Wait的三点:
- 阻塞等待条件变量满足
- 释放已掌握的互斥锁相当于cond.L.Unlock(),注意:两步为一个原子操作。
- 当被唤醒,Wait函数返回时,解除阻塞并重新获取互斥锁,相当于cond.L.Lock()
Signal:唤醒一个go程
Broadcast:唤醒所有的go程。
-
使用流程:
创建条件变量 var cond sync.Cond
指定条件变量用的锁:cond.L = new(sync.Mutex)
cond.L.Lock() 给公共区加锁(互斥量)
-
判断是否到达阻塞条件(缓冲区满/空) for循环判断
for len(ch) == cap(ch){ cond.Wait() 1)阻塞2)解锁3)加锁 }
访问公共区--读、写数据,打印
解锁条件变量用的锁 cond.L.Unlock()
唤醒阻塞在条件变量的对端。Signal,Broadcast
//条件变量实现生产者消费者 //生产者消费者模型 var cond sync.Cond func producer(out chan<- int,idx int){ for{ //先加锁 cond.L.Lock() //判断缓冲区是否满 for len(out) == 5{ cond.Wait() } num := rand.Intn(800) out <- num fmt.Printf("生产者%dth,生产,%d\n",idx,num) //访问公共区结束,释放锁,并打印结果 cond.L.Unlock() //唤醒阻塞在条件变量上的消费者 cond.Signal() time.Sleep(time.Millisecond*300) } } func consumer(in <-chan int,idx,int){ for{ //先加锁 cond.L.Lock() //判断是否为空 for len(in) == 0{ cond.Wait() } //访问公共区 num := in fmt.Printf("消费者%dth,消费,%d\n",idx,num) //访问结束,结束 cond.L.Unlock() //唤醒阻塞的生产者 cond.Signal() time.Sleep(time.Millisecond*300) } } func main(){ ch := make(chan int,5) quit := make(chan bool) rand.Seed(time.Now().UnixNano()) for i:=0;i < 8;i++{ go producer(ch,i+1) //子go程 生产者 } for i:=0;i < 8;i++{ go consumer(ch,i+1) //消费者 } <- quit }
网络编程
协议:一组规则,要求使用协议的双方,要严格的遵守的协议内容
-
网络分层结构:
- OSI七层模型:
- TCP/IP四层模型:
- 链路层:设备
- 网络层:IP
- 传输层:port
- 应用层:应用程序
-
各层功能:
- 链路层:ARP
- 源Mac----目标Mac
-
ARP协议作用:借助IP获取Mac地址。
- 链路层:ARP
-
网络层:IP
- 源IP--目标IP
- IP协议作用:在网络中唯一标识一台主机。
- IP地址本质:2进制数。---点分十进制IP地址(string)
-
传输层TCP/UDP
- port --- 在一台主机上唯一标识一个进程。
-
应用层:ftp、http、自定义
- 对数据进行封装、解封装。
-
数据通信过程:
- 封装:应用层---传输层--网络层---链路层,没有经过封装不能再网络中传递。
-
解封装:链路层--网络层--传输层--应用层
总结通信过程:
MAC地址:不需要用户指定, (ARP协议)IP--->MAC
IP地址:需要用户指定 --- 确定主机
-
port端口号(需要用户指定) --确定程序
- 1、不能用的系统占用的默认端口。 5000+端口 (8080除外)
- 2、65535为上限
socket编程
-
网络通信过程中,socket一定是成对出现的。
- 一共三个socket,listen、accept、dial等返回值
- 通信的两个socket,accept,dial的返回值,listen的socket用于设定IP和port。
-
C/S
- 优点:数据传输效率高,协议选择灵活
- 缺点:工作量大、安全性构成威胁
- B/S:
- 优点:开发工作较小,不受平台限制,安全威胁小。
- 缺点:缓存数据差,协议选择不灵活。
TCP-C/S服务器
1、创建监听socket listener:= net.Listen("tcp","ip+port") ip+port --- 服务器自己的IP和port
2、启动监听 conn := listener.Accept() conn用于通信的socket
3、conn.Read()
4、处理使用数据
5、conn.Write()
6、关闭listener,conn
server.go func main(){ //指定服务器通信协议,IP和port,创建一个监听的socket listener,err := net.Listen("tcp","127.0.0.1:8000") if err != nil{ fmt.Println("net Listen err",err) return } defer listener.Close() fmt.Println("服务器等等客户端连接") //阻塞监听客户端连接请求 //用于返回通信socket conn,err := listener.Accept() if err != nil{ fmt.Println("listener.Accept()err",err) return } defer conn.Close() fmt.Println("服务器与客户端建立连接成功") //读取客户端发送的数据 buf := make([]byte,4096) n,err := conn.Read(buf) if err != nil{ fmt.Println("conn.Read err ",err) return } conn.Write(buf[:n]) //读多少,写多少 //处理数据-- 打印 fmt.Println("服务器读到数据:",string(buf[:n])) }
client.go func main(){ //指定服务器IP+port创建 通信套接字 conn,err := net.Dial("tcp","127.0.0.1:8000") if err != nil{ fmt.Println("net.Dial err ",err) return } defer conn.Close() //主动写数据 conn.Write([]byte("Are you OK")) buf := make([]byte,4096) //接收服务器的数据 n,err := conn.Read(buf) if err != nil{ fmt.Println("conn.Read err",err) return } fmt.Println("服务器回发",string(buf[:n])) }
TCP-C/S客户端:
conn,err := net.Dial("TCP",服务器的IP和port)
写数据给服务器conn.Write()
读服务器回发的数据conn.Read()
conn.Close()
-
TCP并发C/S服务器
1、创建监听套接字 listener := net.Listen("tcp",服务器的ip+port) //tcp 不能大写
2、defer listener.Close()
3、for循环阻塞监听客户端连接事件 conn := listener.Accept()
4、创建go程对应每一个客户端进行数据通信 go HandlerConnet()
-
5、实现HandlerConnet(conn net.Conn)
- 1、defer conn.Close()
- 2、获取成功连接的客户端Address conn.RemoteAddr()
- 3、for 循环读取客户端发送数据 conn.Read(buf)
- 4、处理数据 小--大 strings.ToUpper()
- 5、回写转化后的数据 conn.Write([]byte(buf[:n]))
func HandlerConnect(conn net.Conn){ defer conn.Close() //获取连接的客户端Address addr := conn.RemoteAddr() fmt.Println(addr,"客户端成功连接!") //魂环读取客户端发送的数据 buf := make([]byte,4096) for{ n,err := conn.Read(buf) //测试一下接收到的数据。 //fmt.Println(buf[:n]) 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(string.ToUpper(string(buf[:n])))) } } func main(){ //创建监听套接字 listener,err := net.Listen("tcp","127.0.0.1:8000") 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) } }
-
服务器判断关闭:
- Read客户端/服务器返回0,则客户端关闭。(channel关闭时,关闭写channel后,读到的是默认值)
- 使用nc命令发送数据,末尾会自带\n。
-
TCP-C/S并发客户端
func main(){ //主动发起连接请求 conn,err := net.Dial("tcp","127.0.0.1:8000") if err != nil{ fmt.Println("net.Dial err ",err) return } defer conn.Close() //获取用户键盘书输入stdin,将输入数据发送给服务器 go func(){ str := make([]byte,4096) for{ n,err := os.Stdin.Read(str) if err != nil{ fmt.Println("os.Stdin 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])) } }
TCP通信过程:
- 三次握手:
- 1、主动发起请求端:发送SYN
- 2、被动建立连接请求端:应答ACK同时发送SYN
- 3、主动发起请求端,发送应答ACK
- 标志TCP三次握手建立完成---server:Accept()返回, --- client:Dial()返回
- 四次挥手
- 1、主动关闭连接请求端,发送FIN
- 2、被动关闭连接请求端,发送ACK 。标志半关闭完成---close()
- 3、被动发送连接请求端,发送FIN
- 4、主动关闭连接请求端,应答ACK 标志:四次挥手完成--close().
TCP状态转换图:
主动发起连接请求端:CLOSED--完成三次握手--ESTABLISEHED(数据通信状态)
被动发起连接请求端:CLOSED--调用Accept函数--LISTEN--完成三次握手--ESTABLISEHED(数据通信状态)--Accept函数返回,数据传递期间---ESTABLISEHED(数据通信状态)
-
主动关闭连接请求端:
- ESTABLISEHED--FIN_WAIT_2(半关闭)--TIME_WAIT--2MSL_-- 确认最后一个ACK被对端成功接收--CLOSE
- 半关闭、TIME_WAIT、2MSL--只会出现在“主动关闭连接请求端”
被动关闭连接请求端:ESTABLISEHED--CLOSE
利用netstat查看网络状态。netstat -an
windows:netstat -an | findstr 8001
Linux:netstat -an|grep 8001
-
TCP通信:
- 面向连接的,可靠的数据包传输
-
UDP通信:
-
无连接的,不可靠的报文传递。
UDP服务器:
-
func main(){ //组织一个udp地址结构,指定服务器的IP+port srcAddr,err := net.ResolveUDPAddr("udp",127.0.0.1:8000) if err != nil{ fmt.Println("ResolceUDPAddr err",err) return } fmt.Println("udp服务器地址结构,创建完成") //创建用于通信的socket udpConn,err := net.ListenUDP("udp",srcAddr) if err != nil{ fmt.Println("net.ListenUDP err",err) return } defer udpConn.Close() fmt.Println("服务器socket创建完成") //读取客户端发送的数据 buf := make([]byte,4096) //返回3个值,分别是读到的字节数,客户端地址,error n,cltAddr,err := udpConn.ReadFromUDP(buf) if err != nil{ fmt.Println("udpConn.ReadFromUDP err",err) return } //模拟出来数据 fmt.Printf("服务器读到%v的数据:%s\n",cltAddr,string(buf[:n])) //回写数据给客户端 daytime := time.Now().String() _,err := udpConn.WriteToUDP([]byte(daytime),cltAddr) if err != nil{ fmt.Println("udpConn.Write err",err) return } }
client.go func main(){ conn,err := net.Dial("udp","127.0.0.1:8000") if err != nil{ fmt.Println("net.Dial err ",err) return } defer conn.Close() //主动写数据 conn.Write([]byte("Are you OK")) 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服务器:
- 1、创建server端地址结构(IP+port) net.ResolveUDPAddr()
- 2、创建用于通信的socket,绑定地址结构 udpConn = net.ListenUDP()
- 3、defer udpConn.Close()
- 4、读取客户端发送数 ReadFromUDP()返回 n,cltAddr(客户端的IP+Port),err
- 5、写数据给客户端 WriteToUDP("待写数据",客户端地址)
-
UDP客户端:
- 参考TCP客户端
- net.Dial("udp",server的IP+port)
-
UDP并发服务器
func main(){ //组织一个udp地址结构,指定服务器的IP+port srcAddr,err := net.ResolveUDPAddr("udp",127.0.0.1:8000) if err != nil{ fmt.Println("ResolceUDPAddr err",err) return } fmt.Println("udp服务器地址结构,创建完成") //创建用于通信的socket udpConn,err := net.ListenUDP("udp",srcAddr) if err != nil{ fmt.Println("net.ListenUDP err",err) return } defer udpConn.Close() fmt.Println("服务器socket创建完成") for{ //读取客户端发送的数据 buf := make([]byte,4096) //返回3个值,分别是读到的字节数,客户端地址,error n,cltAddr,err := udpConn.ReadFromUDP(buf) if err != nil{ fmt.Println("udpConn.ReadFromUDP err",err) return } //模拟出来数据 fmt.Printf("服务器读到%v的数据:%s\n",cltAddr,string(buf[:n])) go func(){ //回写数据给客户端 daytime := time.Now().String()+'\ns' _,err := udpConn.WriteToUDP([]byte(daytime),cltAddr) if err != nil{ fmt.Println("udpConn.Write err",err) return } }() } }
-
UDP并发客户端
func main(){ conn,err := net.Dial("udp","127.0.0.1:8000") if err != nil{ fmt.Println("net.Dial err ",err) return } defer conn.Close() for i:= 0;i < 1000;i++{ //主动写数据 conn.Write([]byte("Are you OK")) buf := make([]byte,4096) //接收服务器的数据 n,err := conn.Read(buf) if err != nil{ fmt.Println("conn.Read err",err) return } fmt.Println("服务器回发",string(buf[:n])) time.Sleep(time.Second) } }
-
UDP服务器并发:
- 1、UDP默认支持客户端并发访问
-
2、使用go程将服务器出了ReadFromUDP 和WriteToUDP操作分开,提高并发效率。
-
使用场景:
- TCP:对数据传输安全性、稳定性要求较高的场合。网络文件传输。下载,上传。
- UDP:对数据的实时传输要求较高的场合,视频直播,在线电话会议,游戏。
-
优点:
- TCP:稳定、安全、有序
- UDP:效率高、开销小、开发复杂度低
-
缺点:
- TCP:效率低,开销大、开发复杂度高。
- UDP:稳定性差、安全低、无序。
文件传输
文件属性
os.Args //获取命令行参数,在main函数启动时, 向整个程序传参
go run xx.go argv1 argv2 argv3 argv4
-
获取文件属性
- fileInfo: os.Stat(文件访问绝对路径)
- fileInfo 接口,两个接口
- Name()获取文件名
- Size()获取文件大小
func main(){ list := os.Args if len(list) != 2{ fmt.Println("格式为go run xx.go 文件名") return } //提取文件名 path := list[1] //获取文件属性 fileInfo,err := os.Stat(path) if err != nil{ fmt.Println("os.Stat err",err) return } fmt.Println("文件名:",fileInfo.Name()) fmt.Println("文件大小",fileInfo.Size()) }
//发送端
package main
import (
"fmt"
"io"
"net"
"os"
)
func main(){
list := os.Args //获取命令行参数
if len(list) != 2{
fmt.Println("格式为:go run xx.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:8000")
if err != nil{
fmt.Println("net.Dial err",err)
return
}
defer conn.Close()
//发送文件名给接收到
conn.Write([]byte(fileName))
//读取服务器回发
buf := make([]byte,4096)
n,err := conn.Read(buf)
if err != nil{
fmt.Println("net.Read err",err)
return
}
if "ok" == string(buf[:n]){
//写文件内容给服务器
sendFile(conn,filePath)
}
}
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("发送 err",err)
}
return
}
//写到网络socket中
_,err = conn.Write(buf[:n])
if err != nil{
fmt.Println("conn.Write err",err)
return
}
}
}
//接收端
package main
import (
"fmt"
"net"
"os"
)
func main(){
//创建用于监听的socket
listener,err := net.Listen("tcp","127.0.0.1:8000")
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)
}
func recvFile(conn net.Conn, name string) {
//按照文件名创建文件
f,err := os.Create(name)
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])
}
}
- 文件传输:--发送端(客户端)
- 1、提示用户使用命令行参数输入文件名,接收文件名filePath(含访问路径)
- 2、使用os.Stat()获取文件属性,得到文件fileName(去除访问路径)
- 3、主动发起连接服务器请求,结束时关闭连接。
- 4、发送文件名到接收端conn.Write()
- 5、读取接收到回发的确认数据conn.Read()
- 6、判断是否为“ok",如果是,封装函数SendFile()发送文件内容,传参filePath和conn
- 7、只读Open文件,结束时Close文件
- 8、循环读本地文件,读到EOF,读取完毕
- 9、将读到的内容原封不动conn.Write给接收到(服务器)
- 文件传输--接收到(服务器)
- 1、创建监听listener,程序结束时关闭
- 2、阻塞等待客户端连接conn,程序结束时关闭conn
- 3、读取客户端发送文件名,保存fileName
- 4、回发“Ok”
- 5、封装函数RecvFile接收客户端发送的文件内容,传参fileName和conn
- 6、按文件名Create文件,结束时close
- 7、循环Read发送端网络文件内容,读到0说明文件读取结束
- 8、将读到的内容原封不动write到创建的文件中。
并发聊天室
模块:
- 主go程(服务器):监听用户请求
- 处理用户连接go程:HandleConnect:负责新上线用户的存储,用户消息读取、发送、用户改名、下线处理及超时处理。
为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。
用户消息广播go程:Manager:负责在线用户遍历,用户消息广播发送,需要与HandleConnect go程及用户子go程协作完成
-
go程间数据及通信:map:存储所有登录聊天室的用户信息,key:用户的ip+port。value:client结构体。
Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
通道message:协调并发go程间消息的传递。 -
聊天室模块划分:
-
主go程(服务器):
- 创建监听socket,for循环Accept()客户端连接--conn。启动go程 HandlerConnet
-
处理用户连接go程:HandleConnect:
- 创建用户结构体对象,存入onlineMap。发送用户登录广播、聊天消息。处理查询在线用户、改名、下线、超时退出。(负责新上线用户的存储,用户消息读取、发送、用户改名、下线处理及超时处理。)为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。
-
用户消息广播go程:Manager:
- 监听全局channel message,将读到的消息广播给onlineMap中的所有用户。(负责在线用户遍历,用户消息广播发送,需要与HandleConnect go程及用户子go程协作完成)
go程间数据及通信:map:存储所有登录聊天室的用户信息,key:用户的ip+port。value:client结构体。
Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
通道message:协调并发go程间消息的传递。-
WriteMsgToClient:
- 读取每个用户自带channel C上的消息(由Manager发送该消息)。回写给用户。
-
全局数据模块:
- 用户结构体Client{C、Name、Addr string}
- 在线用户列表:onlineMap[string]Client key:客户端IP+Port value:Client
- 消息通道:message。
-
-
广播用户上线:
- 1、主go程中,创建监听套接字,记得defer
- 2、for 循环监听客户端连接请求。Accept()
- 3、有一个客户端连接,创建新go程 处理客户端数据HandlerConnet(conn) defer
- 4、定义全局结构体类型, C Name Addr
- 5、创建全局map、channel
- 6、实现HandlerConnet函数。获取客户端的IP+Port---RemoteAddr()。初始化新用户结构体信息。name == Addr
- 7、创建Manager管理go程--Accept之间
- 8、实现Manager。初始化在线用户map。循环读取全局的channel,如果无数据,阻塞,如果有数据,遍历在线用户map,将数据写到用户的C中。
- 9、将新用户添加到在线用户map中。 Key== IP+Port value= 新用户结构体
- 10、创建WriteMsgToClient go程,专门给当前用户写数据。 ---- 来源于用户自带的C中。
- 11、实现WriteMsgToClient(clnt,conn).遍历自带的C读数据,conn.Write到客户端。
- 12、HandlerConnet中,结束位置,组织用户上线信息。将用户上线信息写到全局channel中--manager的读就被激活,原来一直阻塞。
- 13、HandlerConnet中,结尾加for{;}
chatRoom.go
package main
import (
"fmt"
"net"
)
//创建用户结构体类型
type Client struct {
C chan string
Name string
Addr string
}
//创建全局map,存储在线用户
var onlineMap map[string]Client
//创建全局channel,传递用户消息
var message = make(chan string)
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 HandleConnect(conn)
}
}
func Manager() {
//初始化onlineMap
onlineMap = make(map[string]Client)
//循环从message中读取
for{
//监听全局channel中是否有数据,有数据,存储至msg,无数据阻塞
msg := <-message
//循环发送消息给所有在线用户
for _,clnt := range onlineMap{
clnt.C <- msg
}
}
}
func HandleConnect(conn net.Conn) {
defer conn.Close()
//获取用户网络地址Ip+port
netAddr := conn.RemoteAddr().String()
//创建新连接用户的结构体,默认用户为IP+Port
clnt := Client{
make(chan string),
netAddr,
netAddr,
}
//将新连接用户,添加到在线用户map中
onlineMap[netAddr] = clnt
//创建专门用来给当前用户发送消息的go程
go WriteMsgToClient(clnt,conn)
//发送用户上线消息到全局channel中
message <- "[" + netAddr + "]" + clnt.Name + "login"
//保证不退出
for{
;
}
}
func WriteMsgToClient(clnt Client,conn net.Conn){
//监听用户自带channel上是否有消息。
for msg := range clnt.C{
conn.Write([]byte(msg + "\n"))
}
}
- 用户广播消息:
-
- 封装函数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+Port
- 提示用户更新完成。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 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 HandleConnect(conn)
}
}
func Manager() {
//初始化onlineMap
onlineMap = make(map[string]Client)
//循环从message中读取
for{
//监听全局channel中是否有数据,有数据,存储至msg,无数据阻塞
msg := <-message
//循环发送消息给所有在线用户
for _,clnt := range onlineMap{
clnt.C <- msg
}
}
}
func MakeMsg(clnt Client,msg string)(buf string){
buf = "[" + clnt.Addr + "]" + clnt.Name + ": " + msg
return
}
func HandleConnect(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中
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中。
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"{
newName := strings.Split(msg,"|")[1]
//msg[8:]
clnt.Name = newName //修改结构体成员name
onlineMap[netAddr] = clnt //更新在线用户列表
conn.Write([]byte("rename successful\n"))
}else{
//将读到的用户消息广播给所有在线用户,写入到message中
message <- MakeMsg(clnt,msg)
}
hasData<-true
}
}()
//保证不退出
for{
//监听channel上的数据流动
select {
case <-isQuit:
delete(onlineMap,clnt.Addr)//将用户从onlineMap中移除
message <- MakeMsg(clnt,"logout") //写入用户退出消息到全局channel
return
case <- hasData:
//什么都不做,目的是重置下面case的计时器。
case <-time.After(time.Second*10):
delete(onlineMap,clnt.Addr)//将用户从onlineMap中移除
message <- MakeMsg(clnt,"logout") //写入用户退出消息到全局channel
return
}
}
}
func WriteMsgToClient(clnt Client,conn net.Conn){
//监听用户自带channel上是否有消息。
for msg := range clnt.C{
conn.Write([]byte(msg + "\n"))
}
}
HTTP编程
-
web工作方式:
-
客户端——>访问www.baidu.com ——>DNS服务器。返回该域名对应的IP地址
客户端——>IP+Port——>访问网页数据(TCP连接。HTTP)
-
-
HTTP和URL:
HTTP超文本传输协议,规定了浏览器访问web服务器进行数据通信的规则。HTTP-TLS、SSL -https(加密)
URL:统一资源定位符.在网络环境中唯一定位一个资源数据。(浏览器地址栏内容)。
获取HTTP请求服务器
func errFunc(err error,info string){
if err != nil{
fmt.Println(info,err)
//return //返回当前函数调用
//runtime.Goexit() //结束当前go程
os.Exit(1) //将当前进程结束,0值为正常,所有一般传非0 值。
}
}
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,"listener.Accept() err:")
defer conn.Close()
buf := make([]byte,4096)
n,err := conn.Read(buf)
if n == 0{
return
}
errFunc(err,"conn.Read() err:")
fmt.Printf("[%s]\n",string(buf[:n]))
}
-
http请求包:
- 请求行:请求方法(空格)请求文件URL(空格)协议版本(\r\n)
- Get POST
- 请求头:语法格式:key:value
- 空行:\r\n --- 代表http请求头结束
- 请求包体:请求方法对应的数据内容
- 请求行:请求方法(空格)请求文件URL(空格)协议版本(\r\n)
-
HTTP响应包:
-
- 使用net/http包创建web服务器
- 注册回调函数
- http.HandleFunc("/itcast",handler)
- 参1:用户访问位置
- 回调函数名----函数必须是(http.ResponseWriter,*http.Request)作为函数。
- http.HandleFunc("/itcast",handler)
- 绑定服务器监听地址
- http.ListenAndServe("127.0.0.1:8000",nil)
- 注册回调函数
- 回调函数:
- 本质:函数指针。通过地址,在某一特定位置调用函数
- 在程序中,定义一个函数,但 不显示调用,当某一条件满足时,该函数由操作系统自动调用。
- 使用net/http包创建web服务器
-
-
web服务器:
func handler(w http.ResponseWrite,r *http.Request){ //w:写回给客户端(浏览器)的数据 //r:从客户端浏览器读到的数据 w.Write([]byte("hello world")) } func main(){ //注册回调函数,该回调函数会在服务器被访问时,自动被调用 http.HandleFunc("/itcast",handler) //绑定服务器监听地址 http.ListenAndServe("127.0.0.1:8000",nil) }
-
客户端拿应答包:
func main(){ conn,err := net.Dial("tcp","127.0.0.1:8000") if err != nil{ fmt.Println("net.Dial err:",err) return } defer conn.Close() httpRequest := "GET /itcast HTTP/1.1\r\nHost:127.0.0.1:8000\r\n\r\n" conn.Write([]byte(httpRequest)) buf := make([]byte,4096) n,_ := conn.Read(buf) if n==0{ return } fmt.Printf("[%s]\n",string(buf[:n])) }
http服务器:
func myHandle(w http.ResponseWriter,r *http.Request){ //w:写给客户端的内容 w.Write([]byte("this is a Web server")) //r:从客户端读到的内容 fmt.Println("Header:",r.Header) fmt.Println("URL:",r.URL) fmt.Println("Method:",r.Method) fmt.Println("Host:",r.Host) fmt.Println("RemoteAddr:",r.RemoteAddr) fmt.Println("Body:",r.Body) } func main(){ //注册回调函数,在客户端访问时,系统自动调用 http.HandleFunc("/itcast",myHandle) //如果不指定死"/"或"/itcast/"需要判断访问的URL服务器中是否存在。 //绑定服务器地址 http.ListenAndServe("127.0.0.1:8000",nil) }
-
-
注册回调函数 http.HandleFunc("/",myHandler)
func myHandle(w http.ResponseWriter,r *http.Request)
//w:写给客户端的内容
//r:从客户端读到的内容
-
type ResponseWriter interface{
Header() Header
Write([]byte)(int,error)
WriteHeawder(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:通常传nil,表示让服务器调用默认的http.DefaultServeMux 进行处理
-
package main
import (
"fmt"
"net/http"
"os"
)
func OpenSendFile(fName string,w http.ResponseWriter){
pathFileName := "D:/code/ubuntu_code/Go/src/github.com/cold-rivers-snow/study/GoAdvance/01ptr"+fName
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([]byte(string(buf[:n])))
}
}
func myHandle(w http.ResponseWriter,r *http.Request){
fmt.Println("URL:",r.URL)
OpenSendFile(r.URL.String(),w)
}
func main(){
//注册回调函数
http.HandleFunc("/",myHandle)
//绑定监听地址
http.ListenAndServe("127.0.0.1:8000",nil)
}
爬虫
概念:访问web服务器,获取指定数据信息的一段程序。
-
流程:
-
构建、发送请求连接
获取服务器返回的响应数据
过滤、保存、使用得到的数据。
关闭请求连接。
-
-
聚焦爬虫的工作流程:
-
明确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写入文件,WriteString(result).f.Close() 不推荐使用defer。
-
https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=50
https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=100
下一页+50。关系。
-
并发版百度贴吧爬虫:
-
- 封装爬取一个网页内容的代码到SpiderPage函数中
-
在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"
)
func main(){
//指定爬取起始,终止页
var start,end int
fmt.Println("请输入爬取的起始页(>=1):")
fmt.Scan(&start)
fmt.Println("请输入爬取的终止页(>=start):")
fmt.Scan(&end)
working(start,end)
}
//爬取单个页面的函数
func SpiderPage(i int,page chan int){
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)
result,err := HttpGet(url)
if err != nil{
fmt.Printf("HttpGet err:",err)
return
}
//fmt.Printf("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 working(start int, 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 HttpGet(url string)(result string,err error) {
resp,err1 := http.Get(url)
if err != nil{
err = err1 //将封装函数内部的错误,传出给调用者
return
}
defer resp.Body.Close()
//循环读取网页数据,传出给调用者
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
}
正则表达式
在线网站测试正则表达式:https://tool.oschina.net/regex
能使用string、strings、strconv包函数解决的问题,首选库函数,其次选择正则表达式
字符类
-:- | -:- |
---|---|
. | 匹配任意一个字符 |
[] | 匹配[]内任意一个字符 |
- | 在[]内表示指定范围 |
^ | 取反,位于[]开头,匹配除[]之外的任意一个 |
[[:xxx:]] | 匹配预定义的字符 [[:digit:]]:匹配数字 |
次数
-:- | -:- |
---|---|
? | 匹配前面单元0或1个 |
+ | 匹配前面单元1或多位 |
* | 匹配前面单元0或多次 |
{N} | 匹配前面单元精确匹配N次 |
{N,} | 匹配前面单元至少匹配N次 |
{,M} | 匹配前面单元至多匹配M次 |
{N,M} | 匹配前面单元N-M次 |
单元限定符
-:- | -:- |
---|---|
() | 将一部分正则表达式组成一个单元,可以对该单元使用数量限定符 |
Go标准库使用RE2标准
package main
import (
"fmt"
"regexp"
)
func main(){
//测试字符串
str := "abc a7c mfc cat 8ca asc cba"
//解析,编译正则表达式
ret := regexp.MustCompile(`a.c`) //` 表示使用原生字符串
//提取需要信息
alls := ret.FindAllStringSubmatch(str,-1) //-1代表全部匹配
fmt.Println("alls:",alls)
//测试小数
strd := "3.14 123.123 .46.haha 1.0 abc 5. ab.4 r3.3 23."
retd := regexp.MustCompile(`[0-9]\.[0-9]`)
//retd := regexp.MustCompile(`\d\.\d`)
allsd := retd.FindAllStringSubmatch(strd,-1)
fmt.Println("allsd:",allsd)
}
(?s)单行模式,可以匹配换行符
-
步骤:
-
-
解析编译正则表达式
MustCompile(str string) *Regexp
参数:正则表达式:建议使用反引号--``
返回值:编译后的正则表达式(结构体类型)
-
提取需要的数据:
func (re *Regexp)FindAllStringSubmatch(s string,n int) [][]string
参1:待匹配的字符串
参2:匹配次数。-1表示匹配全部
-
返回值:存储匹配结构的 [][]string
数组的每一个成员都有string1 和string2的两个元素。- string1:表示,带有匹配参考项的字符串。 - string2:表示,不包含带有参考项的字符串。
-
-
-
提取网页标签数据:
-
举例:提取
之中的数据-
-
(.*)可以提取div标签中的内容
对于div标签中含有多行内容:正则表达式(?s:(.*?))
-
-
-
- 双向爬取:
- 横向:以页为单位
- 纵向:以一个页中的条目为单位
横向:
https://movie.douban.com/top250?start=0&filter= 1
https://movie.douban.com/top250?start=25&filter= 2
https://movie.douban.com/top250?start=50&filter= 3
https://movie.douban.com/top250?start=75&filter= 4
-
纵向:
电影名称:
分数:
---->评分人数: 评分人数 人评价 --->
(?s:(.*?)) 人评价
爬取豆瓣电影信息:
-
- 获取用户输入起始、终止页,启动toWork函数,循环调用SpiderPage爬取每一个页面
- SpiderPage中,获取豆瓣电影横向爬取URL信息,封装HTTPGet函数,爬取一个页面所有数据存入result返回。
- 找寻、探索豆瓣网页纵向爬取规律,找出电影名、分数、评分人数的网页数据特征。
- 分别对这三部分数据使用go正则函数:1. 解析编译正则表达式2.提取信息->string[1]:代表没有匹配项的内容
- 将提取的数据,按照自定义格式写入文件。使用网页编号命名文件。
- 实现并发:
- go SpiderPage(url)
- 创建channel防止主go程退出。
- SpiderPage函数末尾,写入channel
- 主go程 for 读取channel。
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/service/acm"
"io"
"net/http"
"os"
"regexp"
"strconv"
)
func main(){
//指定爬取起始页、终止页
var start,end int
fmt.Println("请输入爬取的起始页(>=1):")
fmt.Scan(&start)
fmt.Println("请输入爬取的终止页(>=start):")
fmt.Scan(&end)
toWork(start,end)
}
func toWork(start int, end int) {
fmt.Printf("正在爬取%d到%d页...\n",start,end)
page := make(chan int)
for i:= start;i <= end;i++{
go SpiderPage2(i,page)
}
for i:= start;i <= end;i++{
fmt.Println("第%d页爬取完毕\n",<-page)
}
}
func SpiderPage2(i int,page chan int) {
//获取URL地址
url := "https://movie.douban.com/top250?start="+strconv.Itoa((i-1)*25)+"&filter="
//封装httpget2爬取URL对应页面
result,err := HttpGet2(url)
if err != nil{
fmt.Println("HTTPGet2 err:",err)
return
}
fmt.Println("result = ",result)
//解析编译正则表达式---电影名称
ret := regexp.MustCompile(`(.*?)`)
//提取信息
fileScore := ret2.FindAllStringSubmatch(result,-1)
//解析编译正则表达式---评分人数
ret3 := regexp.MustCompile(` (.*?) 人评价`)
//提取信息
peopleNum := ret3.FindAllStringSubmatch(result,-1)
Save2file(i,fileName,fileScore,peopleNum)
page<-i
}
//爬取指定URL页面,烦result
func HttpGet2(url string)(result string,err error) {
resp,err1 := http.Get(url)
if err1 != nil{
err = err1
return
}
defer resp.Body.Close()
//ret,err2 := ioutil.ReadAll(resp.Body)
//if err != nil{
// err = err2
// return
//}
//result = string(ret)
//循环爬取整页数据
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
}
//result += string(buf[:n])
}
return
}
func Save2file(i int,fileName,fileScore,peopleNum [][]string){
path := "D:/code/"+ "第"+strconv.Itoa(i)+"页.txt"
f,err := os.Create(path)
if err != nil{
fmt.Println("os.Create err:",err)
return
}
defer f.Close()
n := len(fileScore) //得到条目数
//先打印抬头 电影名称 评分 评分人数
f.WriteString("电影名称"+"\t\t\t"+"评分"+"\t\t"+"评分人数"+"\n")
for i:=0;i < n;i++{
f.WriteString(fileName[i][1]+"\t\t\t"+fileScore[i][1]+"\t\t"+peopleNum[i][1]+"\n")
}
}
- 捧腹网
https://www.pengfu.com/xiaohua_1.html
https://www.pengfu.com/xiaohua_2.html
https://www.pengfu.com/xiaohua_3.html
-
获取一个页面中,一个段子的URL特征:
取一个段子中的title特征:
title
-有2处,取第一处取一个段子的content特征:
段子内容package main import ( "fmt" "github.com/aws/aws-sdk-go/service/codebuild" "io" "net/http" "os" "regexp" "strconv" "strings" ) func main(){ //指定爬取起始页、终止页 var start,end int fmt.Println("请输入爬取的起始页(>=1):") fmt.Scan(&start) fmt.Println("请输入爬取的终止页(>=start):") fmt.Scan(&end) toWork1(start,end) } func toWork1(start int, end int) { fmt.Printf("正在爬取%d到%d页...\n",start,end) page := make(chan int) for i:= start;i <= end;i++{ go SpiderPage3(i,page) } for i:= start;i <= end;i++{ fmt.Printf("第%d页爬取完毕\n",<-page) } } func SpiderPage3(i int,page chan int) { url := "https://www.pengfu.com/xiaohua_"+strconv.Itoa(i)+".html" result,err := HttpGet3(url) if err != nil{ fmt.Println("HttpGet err:",err) return } //解析编译正则表达式 ret := regexp.MustCompile(`
(?s:(.*?))
`) alls := ret1.FindAllStringSubmatch(result,1)//有2处,取第一个 for _,tmptitle := range alls{ title = tmptitle[1] title = strings.Replace(title,"\n","",-1) title = strings.Replace(title,"\t","",-1) break } //编译解析正则表达式---content ret2 := regexp.MustCompile(`