Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。
五层协议只是OSI七层和TCP/IP四层的综合,实际应用还是TCP/IP的四层结构。应用层、传输层、网络层、链路层。
Socket
是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket
其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在 Socket
后面,对用户来说只需要调用Socket规定的相关函数,让 Socket
去组织符合指定的协议数据然后进行通信。
c/s架构
![image.png](/p/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/media/image.png)
服务器部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//设置监听,此处及下面都会返回err,为了缩短代码量丢弃了err,工作中不要丢弃
listener, _ := net.Listen("tcp", "127.0.0.1:8888")
//阻塞等待用户数据
conn, _ := listener.Accept()
//接收用户请求
buf := make([]byte, 1024)
//用户数据最后返回到了buf切片中,n表示从用户数据那读取的字节数,最大值为切片的长度1024
n, _ := conn.Read(buf)
fmt.Println(string(buf[:n]))
//最后处理完数据记得关闭连接
defer func() {
listener.Close()
conn.Close()
}()
|
服务器端先定义一个监听,表示将这个服务器以什么协议放置于什么位置,然后listener.Accept()让服务器阻塞等待用户向服务器端发送数据,用户发送数据后会存入conn中,通过conn.Read()来获取用户输入的数据并放到buf切片中,通过string(buf[:n])强转用户的字节数据为字符串,n表示数据量大于切片则返回切片最大值数据量,小于切片则返回全部数据,n返回的是Read所读取的总字节量,其值不会超过buf定义的1024字节
客户端部分:
1
2
3
4
5
|
//主动连接服务器
conn, _ := net.Dial("tcp", "127.0.0.1:8888")
//发送数据
conn.Write([]byte("are you ok?"))
defer conn.Close()
|
客户端部分只需要连接服务器并且发送数据,连接服务器需要指定服务器的ip端口和协议,发送的数据是字节切片类型
如何多个客户端同时连接同一个服务器(重要)
服务器端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
//conn的类型为net包里的Conn接口
func HandleConn(conn net.Conn) {
//每个用户使用完毕后关闭协程
defer conn.Close()
//获取客户端的网络信息,并以ip 端口的形式输出
addr := conn.RemoteAddr().String()
fmt.Println(addr, "---连接成功")
buf := make([]byte, 2048)
//用for循环套住Read(),用户发送一次数据,接收一次并赋值给buf,
//运行下面的打印和回传,再次循环并阻塞在Read()处等待用户再次发送请求
//但是这样会让子进程一直存在,除非设置了err不为空时退出实现强制退出
//因此加一个用户输入exit退出的逻辑
for{
n, _ := conn.Read(buf)
//打印用户发送过来的内容
fmt.Printf("%s输入了: %s\n", addr, string(buf[:n]))
//把用户信息转为大写发回给用户(先将小写的字节切片转为string,然后变成大写再强转为字节切片)
//n-是因为在windows中输入的语句有一个\r\n换行符,需要-2去除它
//各个平台都不一样,因此可以在前面通过len(string(buf[:n]))来判断到底多了几个字符
if string(buf[:n-2]) == "exit" {
fmt.Println(addr, " exit")
return
}
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
}
func main() {
//设置监听,此处及下面都会返回err,为了缩短代码量丢弃了err,工作中不要丢弃
listener, _ := net.Listen("tcp", "127.0.0.1:8888")
//利用for循环实现多个客户端连接同一个服务器
for {
//循环阻塞等待用户请求,一个用户请求然后往下走,然后循环继续等下一个用户
conn, _ := listener.Accept()
//开子协程处理多个用户请求,如果没有用户进入就会阻塞到第一步直到第一个用户请求,
//然后往下开一个新协程给此用户,继续for循环等待下一个用户请求
go HandleConn(conn)
}
defer listener.Close()
}
|
客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
func main() {
//主动连接服务端
conn, _ := net.Dial("tcp", "127.0.0.1:8888")
defer conn.Close()
//从键盘获取输入并发往服务器端
go func() {
str := make([]byte, 2048)
//用for循环可以实现当os.Stdin.Read()中没有数据时(即没有进行输入),
//阻塞在这一步,直到用户输入内容才继续向下进行
for {
//os.Stdin.Read可以提示键盘输入并且将输入的内容转换为字节切片,并赋值到str中,返回切片的长度
n, _ := os.Stdin.Read(str)
//发送给服务器端
conn.Write(str[:n])
}
}()
//从服务器端获取数据
buf := make([]byte, 2048)
for {
//for循环实现当服务器未往客户端发送数据时,conn.Read(buf)为空,阻塞在这一步,
//有数据循环一遍,然后等待下次服务器的数据
//当服务器端输入exit后,服务器端所对应的子协程结束,conn.Read()返回err,
//通过return结束主协程,子协程同时结束,退出程序
n, err := conn.Read(buf)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(buf[:n]))
}
}
|
远程发送文件:
服务器端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
func RecvFile(path string, conn net.Conn) {
//创建文件
f, _ := os.Create(path)
//循环接收文件的内容
buf := make([]byte, 1024*4)
for {
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("文件接收完毕")
} else {
fmt.Println(err)
}
return
}
//往文件写入内容
f.Write(buf[:n])
}
}
func main() {
//建立监听
listener, _ := net.Listen("tcp", "127.0.0.1:8888")
//阻塞等待用户请求
conn, _ := listener.Accept()
//接收用户文件名
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
path := string(buf[:n])
//返回消息
conn.Write([]byte("开始发送"))
//接收文件内容
RecvFile(path, conn)
defer listener.Close()
defer conn.Close()
}
|
客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
func SendFile(path string, conn net.Conn) {
//打开文件,读取文件
f, _ := os.Open(path)
buf := make([]byte, 1024*4)
for {
n, err := f.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("文件传输完成")
} else {
fmt.Println(err)
}
return
}
//发送到服务器
conn.Write(buf[:n])
}
defer f.Close()
defer conn.Close()
}
func main() {
fmt.Println("请输入文件名:")
var path string
fmt.Scan(&path)
//os.Stat()返回FileInfo类型变量,可以获取文件信息,info.Name()获取文件名,没有此文件则会报错
info, err := os.Stat(path)
if err != nil {
fmt.Println("没有这个文件", err)
return
}
//连接服务器,工作中err别丢空
conn, _ := net.Dial("tcp", "127.0.0.1:8888")
//给服务器先发送文件名,err和n都可以丢空
conn.Write([]byte(info.Name()))
//服务器接收到文件名,向客户端发送消息,客户端进行判断开始进行发送
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
if string(buf[:n]) == "开始发送" {
SendFile(path, conn)
}
}
|
并发聊天服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
type Clinet struct {
C chan string //管道string类型,暂存用户发送的数据
Name string //用户名
Addr string //网络地址
}
var onlineMap = make(map[string]Clinet)
var message = make(chan string)
func WriteMsgToClient(cli Clinet, conn net.Conn) {
//这个是为了实现cli.C中有数据时向各自客户端发送数据,没有数据时阻塞在这一步
//且任意用户都会在登录时都会通过Manager()方法向每个用户的cli.C发送登录信息,
//只要cli.C一有信息,这个子协程就会检测到cli.C不再阻塞,就能向客户端写入新的他人登录信息
//只要发送处的管道没有关闭,cli.Close(),这个for就不会检测到false,会一直堵塞在这里
for msg := range cli.C {
conn.Write([]byte(msg + "\n"))
}
}
//将用户存进在线用户变量onlineMap中
func HandleConn(conn net.Conn) {
defer conn.Close()
//获取网络地址
cliAddr := conn.RemoteAddr().String()
//创建一个结构体,添加到map中
cli := Clinet{make(chan string), cliAddr, cliAddr}
onlineMap[cliAddr] = cli
//新开一个协程,专门给当前用户发送消息
go WriteMsgToClient(cli, conn)
//广播某个人在线
message <- "[" + cli.Name + "]---login"
//退出进行广播并且关闭子协程
isQuit := make(chan bool)
hasData := make(chan bool)
//新开一个协程,接收用户发送过来的请求
go func() {
buf := make([]byte, 2048)
//for循环可以避免输入一次就不再进行接收信息的问题
for {
n, _ := conn.Read(buf)
if n == 0 { //对方断开或者出问题
isQuit <- true
return
}
msg := string(buf[:n-2]) //过滤window末尾的/r/n符号
//查询所有用户
if len(msg) == 3 && msg == "who" { //避免whoami和who匹配
//遍历map,给当前用户发送所有成员
conn.Write([]byte("user list:\n"))
for _, tmp := range onlineMap {
msg = tmp.Name + "-----is online\n"
conn.Write([]byte(msg))
}
//给用户重命名,输入rename|mike
} else if len(msg) >= 8 && msg[:6] == "rename" {
name := strings.Split(msg, "|")[1] //将msg以|分割
cli.Name = name
conn.Write([]byte("u name is rename"))
} else {
//message复用来给所有用户广播它发送的消息,包括自己也看见
message <- cli.Name + ":" + msg
}
hasData <- true
}
}()
//用for循环让此子协程不会结束,避免发送消息后子协程结束,这个用户就通信结束了
//目的是让用户可以接收到后面登录和发送的信息,所以这个子协程就必须一直存在,除非用户退出
for {
//通过select检测管道isQuit的流动
select {
case <-isQuit:
//删除用户并且广播谁下线了
delete(onlineMap, cliAddr)
message <- cli.Name + "--is login out"
return
case <-hasData: //有数据不作处理
case <-time.After(60 * time.Second): //60s后超时执行此case,超时强制退出
delete(onlineMap, cliAddr)
message <- cli.Name + "--is time out leave out"
return
}
}
}
//新开一个协程,转发消息,只要消息来了就遍历map,给map每个成员都发送此消息
func Manager() {
for {
//mes为string类型的变量,自动推导类型
msg := <-message
//遍历map,给map每个成员都发送此消息
for _, cli := range onlineMap {
cli.C <- msg
}
}
}
func main() {
//监听
listener, _ := net.Listen("tcp", "127.0.0.1:8888")
defer listener.Close()
go Manager()
//循环
for {
//循环阻塞,形成多个客户端共用一个服务器
conn, _ := listener.Accept()
//处理用户连接
go HandleConn(conn)
}
}
|
先是主函数启动子协程HandleConn(conn),它作用于往map中写入在线成员,并将消息发给管道message,再通过Manager()将message管道发给msg字符串,遍历map,往每个map中的管道写入信息,紧接着通过WriteMsgToClient()中的range向客户端写入消息,且for遍历管道时,没有cli.Close()的存在,它会一直堵塞在此处等待新的信息写进cli.C
TCP黏包
1
2
3
4
5
6
7
8
9
10
11
12
|
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
}
}
|
将msg输出20次到服务端,可以看到服务端输出结果如下:
1
2
3
4
5
|
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
|
客户端分20次发送的数据,在服务端并没有成功的输出20次,而是多条数据“粘”到了一起。
为什么会出现粘包
主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。
“粘包”可发生在发送端也可发生在接收端:
- **由Nagle算法造成的发送端的粘包:**Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- **接收端接收不及时造成的接收端粘包:**TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
解决办法
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
// Encode 将消息编码
func Encode(message string) ([]byte, error) {
// 读取消息的长度,转换成int32类型(占4个字节)
var length = int32(len(message))
var pkg = new(bytes.Buffer)
// 写入消息头
//LittleEndian:小端,可以百度搜大端小端进行了解,它只是写入内存的顺序不一样
//只要编码和解码都用小端或大端就没问题
//这段代码的意思就是往pkg字节缓存中写入一个数据,是int32的message长度,int32是32位,8位为一个字节,所以这里就占了4个字节
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度,前4个字节就是一条消息的长度
lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
//将前4个字节放到length中
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered返回缓冲中现有的可读取的字节数。现在发送过来的数据长度是消息的长度信息length加上存放长度信息的包头4个字节
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 读取真正的消息数据
pack := make([]byte, int(4+length))
//这里是测试是否能从pack中读出数据,并将读的数据放入pack,读的出就从第5个字节读到尾(下标是从0开始的,因此下标4代表第五个字节)
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
|
接下来在服务端和客户端分别使用上面定义的 proto
包的 Decode
和 Encode
函数处理数据。
服务端代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// socket_stick/server2/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
msg, err := proto.Decode(reader)
if err == io.EOF {
return
}
if err != nil {
fmt.Println("decode msg failed, err:", err)
return
}
fmt.Println("收到client发来的数据:", msg)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
|
客户端代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// socket_stick/client2/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
data, err := proto.Encode(msg)
if err != nil {
fmt.Println("encode msg failed, err:", err)
return
}
conn.Write(data)
}
}
|
b/s架构:
![image.png](/p/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/media/image-2.png)
![image.png](/p/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/media/image-3.png)
![image.png](/p/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/media/image-3.png)
![image.png](/p/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/media/image-5.png)
![image.png](/p/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/media/image-4.png)
但是如果工作中每次都需要向服务器发送一长串的请求包过于繁琐,所以可以使用net/http包来简化
![image.png](/p/socket%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/media/image-1.png)
http服务器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//w为给客户端回复的数据
//req,读取客户端的数据
func HandConn(w http.ResponseWriter, req *http.Request) {
//给客户端浏览器发送数据
w.Write([]byte("hello go"))
//获取客户端的请求头部参数等
//在https://studygolang.com/pkgdoc中的net/http中搜type Request可以获取req的所有参数
fmt.Println(req.URL.Path)
}
func main() {
//注册处理函数,用户连接进来自动调用指定的处理函数(即如果域名后面接了/hello则调用后面那个函数)
//源代码中第二个参数为handler func(ResponseWriter, *Request)
//即在定义函数时已经定义了这是一个func,且默认已经传了两个参数进去,所以不需要加()来调用,
//自己写HandConn函数时也不再需要想办法获取ResponseWriter和*http.Request的值了
http.HandleFunc("/hello", HandConn)
//该方法用于在指定的网络地址进行监听,然后调用服务端处理程序来处理传入的连接请求
//该方法有两个参数:第一个为监听地址;第二个参数表示服务器端处理程序,通常为空
//第二个参数为空意味着服务端调用http.DefaultServerMux进行处理
http.ListenAndServe("127.0.0.1:8000", nil)
}
|
http客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//获取从百度回传回来的请求包,http是必须要加的
resp, _ := http.Get("http://www.baidu.com")
//body是从服务器端读取资源(类似于conn.Read()),最后是需要进行关闭的
defer resp.Body.Close()
fmt.Println("Status =", resp.Status)
fmt.Println("StatusCode =", resp.StatusCode)
fmt.Println("Header =", resp.Header)
//获取baidu.com中的数据Read(),然后赋值给buf,最后追加到tmp中
buf := make([]byte, 1024*4)
var tmp string
for {
n, err := resp.Body.Read(buf)
if n == 0 {
fmt.Println("read err =", err)
break
}
tmp += string(buf[:n])
}
fmt.Println(tmp)
|
参考文档 https://www.liwenzhou.com/posts/Go/go_http/
用go写爬虫爬百度贴吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
func SpiderPage(i int, page chan int) {
//寻找网址规律,每页pn加50,用for循环获取每个网址
url := "http://tieba.baidu.com/f?kw=%E6%8A%97%E5%8E%8B%E8%83%8C%E9%94%85&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
fmt.Printf("正在爬取第%d页网页%s\n", i, url)
result, _ := HttpGet(url)
//将内容写到文件中
f, _ := os.Create(strconv.Itoa(i) + ".html")
f.Write([]byte(result))
f.Close()
page <- i
}
func HttpGet(url string) (result string, err error) {
rep, _ := http.Get(url)
defer rep.Body.Close()
//爬取
buf := make([]byte, 1024*4)
for {
n, err := rep.Body.Read(buf)
if n == 0 {
fmt.Println(err)
break
}
result += string(buf[:n])
}
return
}
func DoWork(start, end int) {
fmt.Printf("正在爬取 %d到%d的页面。。。", 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页已经读取完毕", <-page)
}
return
}
func main() {
var start, end int
fmt.Println("请输入起始页")
fmt.Scan(&start)
fmt.Println("请输入结束页")
fmt.Scan(&end)
DoWork(start, end)
}
|
此方法可以爬取每页内容(包括html内容)存到新建的.html中,如果要爬取需要的内容,可以查看源代码然后通过正则表达式爬出来再存入文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
如爬取每个帖子的某段内容,先在每个主页查看源代码爬取出每个帖子的url,
<a rel="noreferrer" href="/p/6978750013" title="老马是真的叼,剪辑的更吊!!!" target="_blank" class="j_th_tit "
用正则表达式 (`<a rel="noreferrer" href="(?s:(.*?))" title=`) 来爬取出网页链接,
regexp的FindAllStringSubmatch会返回一个二维切片,切片中的里切片第一个值是通过表达式过滤出的内容,
第二个值是正则表达式代表的内容,即/p/6978750013,给它拼接上贴吧网址即可访问
通过range迭代外切片然后在里面调用里切片[1]即可
然后在range中用http.Get爬取内容,依然是查看源代码找到对应的内容进行过滤
过来出来的内容可能会有些\t <br />之类的,可以用strings.Replace(text,"\t","",-1)去掉
最后写入文件,如果多标题多内容,可以分开存入到两个切片中,然后后面一口气写进文件中
|
如果不用子协程来爬取,它就是单协程的程序,它会爬完一个再爬下一个,开了子协程后它可以同时爬取多个,节省了大量时间,但是要注意子协程开启后主协程不能关闭,这样会导致子协程也同时消失,可以使用切片来阻塞主协程,直到爬取完毕
Go语言实现UDP通信
UDP协议
UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
UDP服务端
使用Go语言的 net
包实现的UDP服务端代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// UDP/server/main.go
// UDP server端
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
//addr就是发送端的地址类型
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
|
UDP客户端
使用Go语言的 net
包实现的UDP客户端代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// UDP 客户端
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
//close要写在err判断后面,避免真的出错直接结束,导致直接执行defer造成问题
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
|