Golang网络编程

​ 说到网络,避不开的是OSI层次模型。下三层主要实现互联网通信,通过路由、IP、MAC地址实现跨跨网段和局域网内部通信,而编程过程一般会用到网络层、传输层和应用层,以及传输层和应用层之间的Socket抽象层。

Socket编程

IP地址和端口对应一个socket,如果两个进程分别有两个socket,进程相互通信就可以将socket组成一个socket pair

常见的socket类型有两种:流式SocketSOCK_STREAM)和数据报式SocketSOCKET_DGRAM)。前者是一种面向连接,针对TCP,后者是一种无连接的socket,对应UDP

TCP

例如基于TCPsocket通信

server端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer listen.Close()

conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}

buf := make([]byte, 1024)
_, err = conn.Read(buf)
if err != nil {
log.Fatal()
}
fmt.Println(string(buf))
}

client

1
2
3
4
5
6
7
8
9
10
11
func main() {
dial, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
str := "hello"
_, err = dial.Write([]byte(str))
if err != nil {
log.Fatal(err)
}
}

双向通信代码例子

client

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
package main

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

func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer listen.Close()

conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}
defer conn.Close()

go func() {
for {
buf := make([]byte, 1024)
_, err = conn.Read(buf)
if err != nil {
log.Println(err)
continue
}
fmt.Println("receive from server: ", string(buf))
}
}()

for {
input := ""
fmt.Println("please intput:")
fmt.Scanln(&input)
_, err = conn.Write([]byte(input))
if err != nil {
log.Println(err)
continue
}
}
}

client

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
package main

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

func main() {
dial, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer dial.Close()
go func() {
for {
buf := make([]byte, 1024)
_, err = dial.Read(buf)
if err != nil {
log.Println(err)
if err == io.EOF {
return
}
continue
}
fmt.Println("receive from server: ", string(buf))
}
}()
for {
str := ""
fmt.Println("please input:")
fmt.Scanln(&str)
_, err = dial.Write([]byte(str))
if err != nil {
log.Fatal(err)
}
}
}

远程控制的例子

server

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
package main

import (
"fmt"
"log"
"net"
"os/exec"
)

func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer listen.Close()

conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Println(err)
}
str := string(buf[:n])
fmt.Printf("receive cmd: %s\n", str)
cmd := exec.Command(str)
bytes, err := cmd.CombinedOutput()
fmt.Printf("cmd result: %s\n", bytes)
if err != nil {
log.Println(err)
bytes = []byte(err.Error())
}

_, err = conn.Write(bytes)
if err != nil {
log.Println(err)
}
fmt.Printf("send res: %s\n", bytes)
}
}

client

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
package main

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

func main() {
dial, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}

for {
fmt.Println("Please input cmd:")
var cmd string
_, err = fmt.Scanln(&cmd)
if err != nil {
log.Println(err)
}

_, err = dial.Write([]byte(cmd))
if err != nil {
log.Fatal(err)
}
fmt.Printf("send cmd: %s\n", cmd)
res := make([]byte, 1024)
_, err = dial.Read(res)
if err != nil {
log.Println(err)
}
fmt.Printf("get reply: %s\n", res)
}
}

一对多聊天例子

server

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
package main

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

var conMap sync.Map

func receive(conn net.Conn) {
defer conn.Close()
defer conMap.Delete(conn.RemoteAddr().String())
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Println(conn.RemoteAddr(), " out ")
return
}
str := string(buf[:n])
fmt.Printf("receive msg: %s\n", str)
}
}

func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer listen.Close()

go func() {
for {
conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}
conMap.Store(conn.RemoteAddr().String(), conn)
log.Println(conn.RemoteAddr(), "connect")
go receive(conn)
}
}()

for {
msg := ""
fmt.Println("please input msg:")
fmt.Scanln(&msg)
fmt.Println("send to:")
var user string
fmt.Scanln(&user)

conn, ok := conMap.Load(user)
if !ok {
log.Println("user not found")
continue
}
conn.(net.Conn).Write([]byte(msg))
fmt.Printf("send %s to %s\n", msg, user)
}

}

client

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
package main

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

func main() {
dial, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer dial.Close()

go func() {
for {
get := make([]byte, 1024)
_, err = dial.Read(get)
if err != nil {
log.Fatal(err)
}
fmt.Printf("receive msg: %s\n", get)
}
}()

for {
fmt.Println("Please input:")
var cmd string
_, err = fmt.Scanln(&cmd)
if err != nil {
log.Println(err)
}

_, err = dial.Write([]byte(cmd))
if err != nil {
log.Fatal(err)
}
fmt.Printf("send cmd: %s\n", cmd)
}
}

群聊例子

server

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
package main

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

var conMap sync.Map

func receive(conn net.Conn) {
defer conn.Close()
defer conMap.Delete(conn.RemoteAddr().String())
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Println(conn.RemoteAddr(), " out ")
return
}
str := string(buf[:n])
fmt.Printf("receive msg: %s\n", str)
conMap.Range(func(key, value any) bool {
if key != conn.RemoteAddr().String() {
value.(net.Conn).Write(buf)
}
return true
})
}
}

func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer listen.Close()

for {
conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}
conMap.Store(conn.RemoteAddr().String(), conn)
log.Println(conn.RemoteAddr(), "connect")
go receive(conn)
}

}

client

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
package main

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

func main() {
dial, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
defer dial.Close()

go func() {
for {
get := make([]byte, 1024)
_, err = dial.Read(get)
if err != nil {
log.Fatal(err)
}
fmt.Printf("receive msg: %s\n", get)
}
}()

for {
fmt.Println("Please input:")
var cmd string
_, err = fmt.Scanln(&cmd)
if err != nil {
log.Println(err)
}

_, err = dial.Write([]byte(cmd))
if err != nil {
log.Fatal(err)
}
fmt.Printf("send cmd: %s\n", cmd)
}
}

UDP

udp是无连接,也就是接受和发送,都需要指定对端

server

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
package main

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

func hander(udpConn *net.UDPConn, udp *net.UDPAddr) {
for {
input := ""
fmt.Scanln(&input)
_, err := udpConn.WriteToUDP([]byte(input), udp)
if err != nil {
log.Println(err)
}
fmt.Printf("send %s to remote %s\n", input, udp.String())
buf := make([]byte, 1024)
_, udp, err = udpConn.ReadFromUDP(buf)
if err != nil {
log.Println(err)
}
fmt.Printf("receive: %s\n", buf)
}
}

func main() {
udpAddr, err := net.ResolveUDPAddr("udp", ":8080") // udpaddr句柄
if err != nil {
log.Fatal(err)
}
udpConn, err := net.ListenUDP("udp", udpAddr) // 监听udp连接
if err != nil {
log.Fatal(err)
}
defer udpConn.Close()

udp := &net.UDPAddr{}
go func() {
for {
buf := make([]byte, 1024)
_, udp, err = udpConn.ReadFromUDP(buf) // 无连接,获取对端udp地址信息
if err != nil {
log.Println(err)
}
fmt.Printf("receive: %s\n", buf)
}
}()

for {
input := ""
fmt.Scanln(&input)
_, err := udpConn.WriteToUDP([]byte(input), udp) // 往对端udp发送消息
if err != nil {
log.Println(err)
}
fmt.Printf("send %s to remote %s\n", input, udp.String())
}
}

client

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
package main

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

func main() {
udpAddr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
log.Fatal(err)
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
log.Fatal(err)
}
str := "hello"
_, err = conn.Write([]byte(str + udpAddr.String()))
if err != nil {
log.Println(err)
}
buf := make([]byte, 1024)
_, _, err = conn.ReadFromUDP(buf)
if err != nil {
log.Println(err)
}
fmt.Printf("recevie: %s\n", buf)
}

可以看到,上面的链接会出现问题,由于udp是无连接,没有三次握手和四次挥手,接受和发送数据都无法确定对端状态,而且只能通过指定对端地址实现信息发送。

需要实现一对多,则将对端udp的addr记录下来。

传文件

文件传输一般基于TCP实现,因为TCP是面向的可靠连接,有重传机制、ACK、滑动窗口等保障数据传输的可靠性。

通过TCP实现文件传输的核心逻辑可以分为两步,发送文件信息以及发送文件数据。

server

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
package main

import (
"encoding/json"
"fmt"
"log"
"net"
"os"
)

type FileInfo struct {
Name string
Size int64
}

func main() {
log.SetFlags(log.Lshortfile)
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
log.Println(err)
continue
}
// file info
bytes := make([]byte, 1024)
n, err := conn.Read(bytes)
if err != nil {
log.Println(err)
continue
}
var fileinfo FileInfo
if err = json.Unmarshal(bytes[:n], &fileinfo); err != nil {
log.Println(err)
continue
}
fmt.Println("get fileinfo: ", fileinfo)
_, err = conn.Write([]byte("ok"))
if err != nil {
log.Println(err)
continue
}
// file data
os.Remove(fileinfo.Name)
file, err := os.OpenFile(fileinfo.Name, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Println(err)
continue
}
defer file.Close()
for fileinfo.Size > 0 {
data := make([]byte, 1024)
n, err := conn.Read(data)
if err != nil {
log.Println(err)
continue
}
file.Write(data[:n]) // 写入收到的,而不是全部写入。
fileinfo.Size -= int64(n)
}
}
}

client

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
package main

import (
"encoding/json"
"io/ioutil"
"log"
"net"
"os"
)

type FileInfo struct {
Name string
Size int64
}

func main() {
file := "./trace2.out"
f, err := os.Open(file)
if err != nil {
log.Fatal(err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
log.Fatal(err)
}
fileinfo := FileInfo{
Name: info.Name(),
Size: info.Size(),
}
conn, err := net.Dial("tcp", ":8080")
if err != nil {
log.Fatal(err)
}

b, _ := json.Marshal(fileinfo)
_, err = conn.Write(b)
if err != nil {
log.Fatal(err)
}
ok := make([]byte, 1024)
conn.Read(ok)
all, _ := ioutil.ReadAll(f)
conn.Write(all)
}

连接超时,当网络波动或者不可达时,或者端口不通,会出现报错

1
func DialTimeout(network string, address string, timeout time.Duration) (Conn, error)
1
2022/08/13 16:17:03 dial tcp 185.199.111.153:8000: i/o timeout

设置读超时,当到达时间,则不继续读取

1
func (Conn) SetReadDeadline(t time.Time) error

设置写超时,当到达时间,则不继续写入

1
func (Conn) SetWriteDeadline(t time.Time) error

设置超时,相当于同时设置读超时和写超时

1
func (Conn) SetDeadline(t time.Time) error

服务端的socket连接,客户端也可以通过telnet工具连接到服务端

1
telnet 127.0.0.1 8080

TCP粘包

使用TCP很多时候会碰到粘包现象,例如

server

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
package main

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

func main() {
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}
for {
buf := make([]byte, 10)
n, err := conn.Read(buf)
if err != nil { // 客户端断开连接,err = EOF
log.Fatal(err)
}
fmt.Println(n,string(buf[:]))
}
}

client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"log"
"net"
)

func main() {
conn, err := net.Dial("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
str := "hello world"
for i := 0; i < 5; i++ {
conn.Write([]byte(str)) // 每次都发送hello world
}
}

输出

1
2
3
4
5
6
10 hello worl
10 dhello wor
10 ldhello wo
10 rldhello w
10 orldhello
5 world

可以看到,客户端输入是每次输入一个hello world,但是服务端不是按照hello world的数据接受,而是按照能写入的缓存大小,也就是buf := make([]byte, 10)

这是由于TCP的数据传输模式是流模式,在保持长连接的时候,数据可以多次发送或者说多次接受,在客户端和服务端都会发生粘包。

  • 发送端:由Nagle算法造成,发送端会处于节省网络资源的目的,会在将数据提交到TCP时,等待一段时间,如果这段时间还有数据,则会将数据一起发送。

  • 接收端:TCP会将数据缓存到缓冲区中,然后通知应用层读取数据。当接收端接收数据不及时,缓冲区有更多的数据,应用层读取时就会一次性读取更多数据。

粘包解决办法

出现粘包的问题是不知道这次发送的数据量是多少,所以解决办法可以分为两种

  • 将本次传输的数据量在每次传输的时候传入
  • 两端固定每次传输的数据量和接收的数据量,这种方式需要在对端将数据组装起来,而且需要保证发送端不出现粘包。

这里使用方法1,对TCP数据报解封装和封装,将本次传输TCP的数据个数写入到TCP包头中。

编码和解码

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
package tools

import (
"bufio"
"bytes"
"encoding/binary"
)

func Encode(message string) ([]byte, error) {
// 获取消息体长度
var length = int32(len(message))
// 创建写缓存
var pkg = new(bytes.Buffer)
// 将长度写入消息头
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
}

func Decode(reader *bufio.Reader) (string, error) {
// 先读取前4个字节,获取长度
messageLength, _ := reader.Peek(4)
// 创建对应内容大小的缓冲区
buffer := bytes.NewBuffer(messageLength)
var length int32
err := binary.Read(buffer, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// buffered返回缓冲中现有的可读取字节数(ps:个人认为正常情况下这个判断永远不会成立,除非输入端写入的数据不正确)
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 读取真正的消息数据
pack := make([]byte, int(4+length))
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack), nil
}

按照编码和解码方法,分别对发送端编码,接收端解码

server

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
package main

import (
"bufio"
"fmt"
"gostudy/network/tcpstickpackage/tools"
"io"
"log"
"net"
)

func main() {
log.SetFlags(log.Lshortfile)
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}
buffer := bufio.NewReader(conn)
for {
decode, err := tools.Decode(buffer)
if err != nil {
if err == io.EOF {
return
}
log.Fatal(err)
}
fmt.Println(decode)
}
}

cilent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"gostudy/network/tcpstickpackage/tools"
"log"
"net"
)

func main() {
conn, err := net.Dial("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
str := "hello world"
for i := 0; i < 5; i++ {
data, err := tools.Encode(str)
if err != nil {
log.Fatal(err)
}
conn.Write(data)
}
}

RPC

Remote Procedure Call,远端过程调用,是一种通信标准,对标的是本地方法调用,相当于将远端服务器上的方法、代码、指令,当做是本地一样执行,并将执行结果返回到本地。基于TCP通信

RPC由四个部分组成:客户端、客户端存根、服务端、服务端存根。

RPC的通信过程是建立在Scoket通信之上,服务到Scoket层中间通过RPC stub实现RPC通信。

RPC stub就是存根,主要作用是解析调用的方法名,调用本地的方法,序列化和反序列化结果信合和参数信息,将信息打包处理。RPC stud在具体的编码和发开过程中,都是通过动态代理技术生成的一段程序。

简单RCP

server

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
package main

import (
"log"
"net"
"net/rpc"
)

// RCP载体
type CalcRPC struct {
}

type Calc struct {
A int
B int
}

func (c *CalcRPC) Add(req Calc, rep *int) error {
*rep = req.A + req.B
return nil
}

func main() {
rpc.RegisterName("add", new(CalcRPC)) // RPC注册名称
listener, err := net.Listen("tcp", ":8080") // 基于TCP
if err != nil {
log.Fatal(err)
}
rpc.Accept(listener) // 监听连接,处理请求
}

client

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
package main

import (
"fmt"
"log"
"net/rpc"
)

type Calc struct {
A int
B int
}

func main() {
conn, err := rpc.Dial("tcp", ":8080") // 基于socket
if err != nil {
log.Fatal(err)
}
var input = Calc{
A: 100,
B: 200,
}
var rep int
err = conn.Call("add.Add", input, &rep) // 调用远端方法,add是名称,Add是方法名称
if err != nil {
log.Fatal(err)
}
fmt.Println(rep)
}

基于HTTP的RPC

RCP服务除了直接通过socket,还可以基于HTTP请求

server

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
package main

import (
"log"
"net"
"net/http"
"net/rpc"
)

type CalcRPC struct {
}

type Calc struct {
A int
B int
}

func (c *CalcRPC) Add(req Calc, rep *int) error {
*rep = req.A + req.B
return nil
}

func main() {
rpc.RegisterName("add", new(CalcRPC))
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}

rpc.HandleHTTP() // 将RPC绑定到http请求上
http.Serve(listener, nil) // 开启http请求监听
}

client

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
package main

import (
"fmt"
"log"
"net/rpc"
)

type Calc struct {
A int
B int
}

func main() {
conn, err := rpc.DialHTTP("tcp", ":8080") // 调用http
if err != nil {
log.Fatal(err)
}
var input = Calc{
A: 100,
B: 200,
}
var rep int
err = conn.Call("add.Add", input, &rep)
if err != nil {
log.Fatal(err)
}
fmt.Println(rep)
}

基于interface

代码优化,可以将RPC的服务基于interface实现,避免出现服务名称调用不匹配造成的问题

server

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
package main

import (
"log"
"net"
"net/rpc"
)

const CalcServiceName = "path/to/thisPackage.CalcService"

type CalcServiceInterface interface { // 将方法全部都放到接口中
Add(req Calc, rep *int) error
}

func RegisterCalcService(svc CalcServiceInterface) error { // 注册函数
return rpc.RegisterName(CalcServiceName, svc)
}

type CalcRPC struct {
}

type Calc struct {
A int
B int
}

func (c *CalcRPC) Add(req Calc, rep *int) error {
*rep = req.A + req.B
return nil
}

func main() {
RegisterCalcService(new(CalcRPC))
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
rpc.Accept(listener)
}

client

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
package main

import (
"fmt"
"log"
"net/rpc"
)

var _ CalcServiceInterface = (*CalcServiceClient)(nil) // 匿名变量保证接口被实现了

const CalcServiceName = "path/to/thisPackage.CalcService" // 与服务端定义方式保持一致

type CalcServiceInterface interface { // 与服务端定义方式保持一致
Add(req Calc, rep *int) error
}

type CalcServiceClient struct {
client *rpc.Client
}

func (c *CalcServiceClient) Add(req Calc, rep *int) error { // 函数调用封装
return c.client.Call(CalcServiceName+".Add", req, rep)
}

func DialCalcService(network, address string) (*CalcServiceClient, error) {
conn, err := rpc.Dial("tcp", ":8080")
if err != nil {
return nil, err
}
return &CalcServiceClient{client: conn}, nil
}

type Calc struct {
A int
B int
}

func main() {
calcServiceClient, err := DialCalcService("tcp", "8080")
if err != nil {
log.Fatal(err)
}
var input = Calc{
A: 100,
B: 200,
}
var rep int
err = calcServiceClient.Add(input, &rep) // 接口封装之后,函数调用更加方便
if err != nil {
log.Fatal(err)
}
fmt.Println(rep)
}

基于JSON

上面的几种都是在Golang内部实现,如果跨语言,就无法使用。但是跨语言通信一般可以通过json实现,rpc的包自带这种实现方式

server

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
package main

import (
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)

type CalcRPC struct {
}

type Calc struct {
A int `json:"a"`
B int `json:"b"`
}

func (c *CalcRPC) Add(req Calc, rep *int) error {
*rep = req.A + req.B
return nil
}

func main() {
rpc.RegisterName("CalcService", new(CalcRPC))
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}

client

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
package main

import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)

type Calc struct {
A int `json:"a"`
B int `json:"b"`
}

func main() {
conn, err := net.Dial("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
var input = Calc{
A: 100,
B: 200,
}
var rep int
err = client.Call("CalcService.Add", input, &rep)
if err != nil {
log.Fatal(err)
}
fmt.Println(rep)
}

可以通过netcat监听,获取scoket通信信息

步骤:开启nc监听,打开server端服务,打开client端服务

1
2
# nc -l -p 8080
{"method":"CalcService.Add","params":[{"a":100,"b":200}],"id":0}

那么可以仿照这种格式发送请求

1
2
# echo '{"method":"CalcService.Add","params":[{"a":300,"b":400}],"id":0}' | nc localhost 8080
{"id":0,"result":700,"error":null}

在HTTP上提供jsonrpc服务

上面虽然已经提供了基于HTTP的RPC,但是并不能通过HTTP发送请求。

server

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
package main

import (
"io"
"net/http"
"net/rpc"
"net/rpc/jsonrpc"
)

type CalcRPC struct {
}

type Calc struct {
A int `json:"a"`
B int `json:"b"`
}

func (c *CalcRPC) Add(req Calc, rep *int) error {
*rep = req.A + req.B
return nil
}

func main() {
rpc.RegisterName("CalcService", new(CalcRPC))
http.HandleFunc("/jsonrpc", func(writer http.ResponseWriter, request *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
Writer: writer,
ReadCloser: request.Body,
}
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
http.ListenAndServe(":8080", nil)
}

这样就可以通过发送HTTP请求实现调用RCP服务

1
2
#  curl 127.0.0.1:8080/jsonrpc -X POST --data '{"method":"CalcService.Add","params":[{"a":500,"b":600}],"id":0}'
{"id":0,"result":1100,"error":null}

Protobuf

全称是Protocol buffer,是一种数据描述语言,类似于json、xml。被用来做为接口规范的描述语言,跨语言RPC接口的基础工具。

例如上面的服务,可以通过统一的protobuf文件实现方法。

通过Protobuf结合RPC,通过protobuf定义数据格式

1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3";                        // 语法			
option go_package="./;calc_protobuf"; // 生成go代码指定包名称是calc_protobuf
package main;

message AddRequest { // 请求数据的结构
int64 A = 1;
int64 B = 2;
}

message AddReply { // 返回数据的结构
int64 R = 1;
}

通过命令生成对应golang语法的结构体

1
# protoc --go_out=. calc.proto

正式代码则直接可以省略定义的传输数据结构体,引入生成的代码文件中的结构体即可。

但是,这只是生成了传输数据的结构体,也就是告知RPC stub编码和解码的方式,并没有实现指定方法调用。

protobuf文件中增加以下内容:

1
2
3
service CalcService {
rpc Add (AddRequest) returns (AddReply);
}

再次通过protoc命令生成,但是实际上生成的文件没有生成Add方法。这是因为实现RPC的方法有很多种,protoc 编译器并不知道该如何为 CalcService 服务生成代码。

所以需要有一种插件指定生成代码的方式,这种插件既可以自定义,也可以使用gRPC框架。

更多protobuf语法,深入ProtoBuf

gRPC

gRPC是一种RPC框架,可以在任何环境下运行。该框架提供了负载均衡、跟踪、智能监控、身份验证登功能。基于HTTP/2协议设计。

将上述的protobuf文件通过grpc插件生成代码

1
# protoc --go_out=plugins=grpc:. calc.proto

可以看到,服务接口

1
2
3
4
5
6
7
8
9
// CalcServiceServer is the server API for CalcService service.
type CalcServiceServer interface {
Add(context.Context, *AddRequest) (*AddReply, error)
}

// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type CalcServiceClient interface {
Add(ctx context.Context, in *AddRequest, opts ...grpc.CallOption) (*AddReply, error)
}

那么,后续生成接口中定义的方法即可。

服务端,实现Add方法

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
package main

import (
"context"
"google.golang.org/grpc"
"gostudy/rpc/protobuf/calc_protobuf"
"log"
"net"
)

type CalcService struct{} // 载体,后续注册会使用

func (c *CalcService) Add(ctx context.Context, req *calc_protobuf.AddRequest) (*calc_protobuf.AddReply, error) {
rep := new(calc_protobuf.AddReply)
rep.R = req.A + req.B
return rep, nil
}

func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
server := grpc.NewServer()
calc_protobuf.RegisterCalcServiceServer(server, new(CalcService)) // 注册calc服务
err = server.Serve(listener) // 开启监听
if err != nil {
log.Fatal(err)
}

}

客户端

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
package main

import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gostudy/rpc/protobuf/calc_protobuf"
"log"
)

func main() {
dial, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
defer dial.Close()
calc := calc_protobuf.NewCalcServiceClient(dial) // 声明客户端
ctx := context.Background()
req := calc_protobuf.AddRequest{
A: 100,
B: 200,
}
addReply, err := calc.Add(ctx, &req) // 客户端方法调用
if err != nil {
log.Fatal(err)
}
fmt.Println(addReply.GetR())
}

HTTP

RPC协议主要是通过协商数据流格式,解决TCP传输过程中粘包、安全性等问题,而适用性更广泛的是客户端、服务端通信协议是HTTP协议。

HTTP协议也是通过协商数据格式,实现通信。基于TCP

例如,发送一个HTTP请求,在socket中可以读取到内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET / HTTP/1.1                    // 请求方法  URL 协议版本
Host: 127.0.0.1:8080 // 请求的主机名
Connection: keep-alive // 连接方式(close 或者 keep-alive)
sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 // 浏览器类型
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 // 客户端可识别的响应内容类型列表
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br // 客户端可接受的编码压缩格式
Accept-Language: zh-CN,zh;q=0.9 // 客户端可接受的自然语言

HTTP服务端

通过http包启动HTTP服务,获取传输信息

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
package main

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

func HelloServer(w http.ResponseWriter, r *http.Request) {
fmt.Println("Header: ", r.Header) // 请求头
fmt.Println("URL: ", r.URL) // URL
fmt.Println("Method: ", r.Method) // 方法
fmt.Println("Host: ", r.Host) // 请求主机
fmt.Println("RemoteAddr: ", r.RemoteAddr) // 远端地址
fmt.Println("Body: ", r.Body) // 请求体,读取一次就读完了,推荐用r.GetBody
io.ReadAll(r.Body) // r.Body 是一个io.ReadCloser,是一个interface,是对流式文件的操作,因此读了一次之后,标记会移动到尾部,再次读取就会读取空白
io.WriteString(w, "hello world")
return
}

func main() {
http.HandleFunc("/", HelloServer)
log.Fatal(http.ListenAndServe(":8080", nil))
}

输出

1
2
3
4
5
6
Header:  map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9] Accept-Encoding:[gzip, deflate, br] Accept-Language:[zh-CN,zh;q=0.9] Cache-Control:[max-age=0] Connection:[keep-alive] Sec-Ch-Ua:["Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:["macOS"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[none] Sec-Fetch-User:[?1] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36]]
URL: /
Method: GET
Host: 127.0.0.1:8080
RemoteAddr: 127.0.0.1:54281
Body: {}

可以看到请求的Header种包含的内容,主要用于描述此次请求的一些信息,例如连接方式、浏览器类型、客户端可识别的相应内容类型等。

URL内容,是一个结构体

1
2
3
4
5
6
7
8
9
10
11
12
{
"Scheme": "",
"Opaque": "",
"User": null,
"Host": "",
"Path": "/", // 默认只有Path有值,其他字段没有值
"RawPath": "",
"ForceQuery": false,
"RawQuery": "",
"Fragment": "",
"RawFragment": ""
}

Header内容

1
2
3
4
5
6
7
type Request struct {
...
Header Header // Header类型
...
}

type Header map[string][]string // Header 实际是一个map

请求的header会经过处理,处理成首字母大写。

1
2
3
"Accept-Language": [        // accept-language 首字母大写
"zh-CN,zh;q=0.9"
],

form表单

form表单需要格式化才能生成数据,如果是表单,建议在Header里面加Content-Type: application/x-www-form-urlencoded

1
2
3
fmt.Println(r.Form)        // 空
r.ParseForm()
fmt.Println(r.Form) // 实际数据

HTTP客户端

除了通过浏览器发送请求,也可以通过socket发送请求,保证内容格式即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"net"
)

func main() {
conn, _ := net.Dial("tcp", ":8080")
str := `GET / HTTP/1.1 // 内容格式需要保持一致
Host: 127.0.0.1:8080

`
conn.Write([]byte(str))
res := make([]byte, 1024)
conn.Read(res)
fmt.Println(string(res))
}

返回

1
2
3
4
5
6
HTTP/1.1 200 OK
Date: Sun, 14 Aug 2022 10:59:08 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8

hello world

或者通过http包提供的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

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

func main() {
resp, err := http.Get("http://localhost:8080")
if err != nil {
log.Fatal(err)
}
fmt.Println("Status: ", resp.Status) // 返回状态
fmt.Println("StatusCode: ", resp.StatusCode) // 返回状态码
fmt.Println("Header: ", resp.Header) // 返回头
fmt.Println("Body: ", resp.Body) // 返回Body
}

输出

1
2
3
4
Status:  200 OK
StatusCode: 200
Header: map[Content-Length:[11] Content-Type:[text/plain; charset=utf-8] Date:[Sun, 14 Aug 2022 11:03:50 GMT]]
Body: &{0x1400015e3c0 {0 0} false <nil> 0x1025c6f60 0x1025c7050}

通过URL提取信息

请求的URL中可以包含pathquery参数等,可以通过url包实现解析

1
2
3
4
parse, _ := url.Parse("http://localhost:8080/abc?k1=v2")
fmt.Println(parse.Host)
fmt.Println(parse.Path)
fmt.Println(parse.RawQuery)

基于HTTP请求,还可以实现追踪,追踪请求的整个过程DNS解析、GET请求、转发等,为爬虫做准备。

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
package main

import (
"fmt"
"io"
"net/http"
"net/http/httptrace"
"os"
)

func main() {
url := "https://www.xiaoyeshiyu.com"
client := http.Client{}
req, _ := http.NewRequest("GET", url, nil)
trace := &httptrace.ClientTrace{
GotFirstResponseByte: func() {
fmt.Println("First response byte!")
},
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Printf("Got Conn: %+v\n", connInfo)
},
DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
fmt.Printf("DNS Info: %+v\n", dnsInfo)
},
ConnectStart: func(network, addr string) {
fmt.Println("Dial start")
},
ConnectDone: func(network, addr string, err error) {
fmt.Println("Dial done")
},
WroteHeaders: func() {
fmt.Println("Wrote headers")
},
}

req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
fmt.Println("Requesting data from server!")
_, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
fmt.Println(err)
return
}

response, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
io.Copy(os.Stdout, response.Body)
}

HTTP超时

当HTTP请求出现阻塞,为了避免客户端请求一直卡主,可以设置HTTP请求超时

1
2
3
4
5
6
7
client := http.Client{Timeout: 2 * time.Millisecond}
request, _ := http.NewRequest("GET", "https://www.xiaoyeshiyu.com", nil)
response, err := client.Do(request)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.StatusCode)

请求报错

1
2022/08/14 19:20:21 Get "https://www.xiaoyeshiyu.com": context deadline exceeded (Client.Timeout exceeded while awaiting headers)

通过Scoket实现HTTP服务

客户端可以仿照Socket,服务端也可以

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
package main

import (
"io"
"log"
"net"
)

func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}

body := `Hello World

`

for {
conn, err := listener.Accept()
if err != nil {
continue
}
io.WriteString(conn, "HTTP/1.1 200 OK\r\n")
io.WriteString(conn, "\r\n")
io.WriteString(conn, body)
}
}

指定方法

HTTP请求的方法有很多种,例如GETPOSTDELETEDATCHPUT

上面的GET请求依然可以通过POST实现

1
2
3
4
5
6
7
8
resp, err := http.Post("http://localhost:8080", "json", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Status: ", resp.Status)
fmt.Println("StatusCode: ", resp.StatusCode)
fmt.Println("Header: ", resp.Header)
fmt.Println("Body: ", resp.Body)

重定向

类似于代理,可以将流量转发到指定目的。

1
2
io.WriteString(conn, "HTTP/1.1 302 OK\r\n")
fmt.Fprintf(conn, "Location: http://www.xiaoyeshiyu.com\r\n")

通过HTTP Code 302代表重定向,重定向到Location字段中。

HTTP客户端请求方式

HTTP客户端发送请求有多种风格或者说使用方式

直接使用http.Get发送请求

1
2
resp, err := http.Get("http://localhost:8080")
resp, err := http.Post("http://localhost:8080", "json", nil)

通过client发送请求

客户端的传输通常具有内部状态(缓存的TCP连接),因此应该重用客户端,而不是根据需要创建客户端。多个goroutine并发使用客户端是安全的。

1
2
3
4
5
client := http.Client{            // 或者直接使用DefaultClient var DefaultClient = &Client{} 
Timeout: time.Second,
}
client.Post("http://localhost:8080", "json", nil)
client.Get("http://localhost:8080")

HTTP服务端风格

HTTP启动服务端可以有几种方式,例如通过socket,或者直接使用HTTP

1
2
3
listener, err := net.Listen("tcp", ":8080")
conn, err := listener.Accept()
io.WriteString(conn, "HTTP/1.1 302 OK\r\n")

通过http.Server

1
2
listener, err := net.Listen("tcp", ":8080")
http.Serve(listener, handler)

或者通过http.ListernAndServer

1
2
http.HandleFunc("/hello", helloHandler) 
log.Fatal(http.ListenAndServe(":8080", nil))
1
2
3
4
5
6
7
server := http.Server{
Addr: ":8080",
Handler: Handler,
ReadTimeout: time.Second,
WriteTimeout: time.Second,
}
server.ListenAndServe()

或者基于多个路由功能

1
2
3
4
mux := http.NewServeMux()
mux.HandleFunc("/user", Handler) // 路由/user
mux.HandleFunc("/product", Handler) // 路由/product
http.ListenAndServe(":8080", mux)

中间件

AOP:横向关注点,一般用于解决Logtracingmetric,熔断,限流等。例如在http开发中使用责任链模式。

1

中间件的实现方式也可以基于设计模式中的装饰器模式,核心逻辑是在闭包中,传入function,输出一个function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func HelloServer(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "hello world")
return
}

// 中间件,传入一个函数,输出一个函数
func Midware(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println(time.Now())
fmt.Println(r.RemoteAddr)
f(w, r) // 被调用时,处理中间件逻辑,然后执行原始函数
}
}

func main() {
http.HandleFunc("/", Midware(HelloServer)) // 通过装饰器装饰的函数被调用时,原函数没有感知。
log.Fatal(http.ListenAndServe(":8080", nil))
}

net包

net包中包含对网络相关内容的处理

获取mx记录

1
2
3
4
5
6
7
func LookupHost(host string) (addrs []string, err error)    // 通过域名查看主机
func LookupCNAME(host string) (cname string, err error) // 查看别名
func LookupIP(host string) ([]IP, error) // 通过域名获取IP
func LookupPort(network string, service string) (port int, err error) // 获取对应服务的端口
func LookupAddr(addr string) (names []string, err error) // 获取地址对应的域名
func LookupMX(name string) ([]*MX, error) // 获取邮件服务域名
func LookupNS(name string) ([]*NS, error) // 获取域名服务器名称

使用

1
2
3
4
5
6
7
8
9
fmt.Println(net.LookupHost("www.xiaoyeshiyu.com"))
fmt.Println(net.LookupCNAME("www.xiaoyeshiyu.com"))
fmt.Println(net.LookupIP("www.xiaoyeshiyu.com"))
fmt.Println(net.LookupPort("tcp", "https"))
fmt.Println(net.LookupAddr("185.199.111.153"))
res, _ := net.LookupMX("baidu.com")
fmt.Println(res[0].Host)
ns, _ := net.LookupNS("xiaoyeshiyu.com")
fmt.Println(ns[0].Host)

返回

1
2
3
4
5
6
7
[185.199.108.153 185.199.111.153 185.199.110.153 185.199.109.153 2606:50c0:8003::153 2606:50c0:8002::153 2606:50c0:8001::153 2606:50c0:8000::153] <nil>
xiaoyeshiyu.github.io. <nil>
[185.199.108.153 185.199.111.153 185.199.110.153 185.199.109.153 2606:50c0:8001::153 2606:50c0:8000::153 2606:50c0:8003::153 2606:50c0:8002::153] <nil>
443 <nil>
[cdn-185-199-111-153.github.com.] <nil>
mx.maillb.baidu.com.
football.dnspod.net.

使用方式也可以这样

1
2
resolver := net.Resolver{PreferGo: true}
fmt.Println(resolver.LookupNS(context.Background(), "www.xiaoyeshiyu.com"))

获取网口信息

1
2
3
4
5
6
7
8
interfaces, _ := net.Interfaces()
for _, i := range interfaces {
fmt.Println(i.Name) // 网卡名称
fmt.Println(i.Addrs()) // 网卡上的IP
fmt.Println(i.Index) // 序号
fmt.Println(i.Flags) // 网卡状态,up|broadcast|multicast
fmt.Println(i.HardwareAddr.String()) // mac 地址
}

HTTPS

安全的HTTP,通过证书保证网站的身份,以及传输加密。可以通过SSL协议或者TLS协议实现,这两者的区别主要是所支持的加密算法。加密方式是非对称加密,使用这种传输方式,一般是客户端通过公钥加密,发送给服务端,服务端通过私钥解密,然后将数据通过私钥假面传输给客户端,客户端再用公钥解密,实现客户端和服务端之间的加密传输。

前面说过,HTTPS的数据传输是加密的。实际使用中,HTTPS利用的是对称与非对称加密算法结合的方式。

对称加密,就是通信双方使用一个密钥,该密钥既用于数据加密(发送方),也用于数据解密(接收方)。
非对称加密,使用两个密钥。发送方使用公钥(公开密钥)对数据进行加密,数据接收方使用私钥对数据进行解密。

实际操作中,单纯使用对称加密或单纯使用非对称加密都会存在一些问题,比如对称加密的密钥管理复杂;非对称加密的处理性能低、资源占用高等,因 此HTTPS结合了这两种方式。

HTTPS服务端在连接建立过程(ssl shaking握手协议)中,会将自身的公钥发送给客户端。客户端拿到公钥后,与服务端协商数据传输通道的对称加密密钥-对话密钥,随后的这个协商过程则 是基于非对称加密的(因为这时客户端已经拿到了公钥,而服务端有私钥)。一旦双方协商出对话密钥,则后续的数据通讯就会一直使用基于该对话密 钥的对称加密算法了。

上述过程有一个问题,那就是双方握手过程中,如何保障HTTPS服务端发送给客户端的公钥信息没有被篡改呢?实际应用中,HTTPS并非直接 传输公钥信息,而是使用携带公钥信息的数字证书来保证公钥的安全性和完整性。

数字证书,又称互联网上的”身份证”,用于唯一标识一个组织或一个服务器的,这就好比我们日常生活中使用的”居民身份证”,用于唯一标识一个 人。服务端将数字证书传输给客户端,客户端如何校验这个证书的真伪呢?我们知道居民身份证是由国家统一制作和颁发的,个人向户 口所在地公安机关申请,国家颁发的身份证才具有法律 效力,任何地方这个身份证都是有效和可被接纳的。大悦城的会员卡也是一种身份标识,但你若用大悦城的会员卡去买机票,对不起, 不卖。航空公司可不认大悦城的会员卡,只认居民身份证。网站的证书也是同样的道理。一般来说数字证书从受信的权威证书授权机构 (Certification Authority,证书授权机构)买来的(免费的很少)。一般浏览器在出厂时就内置了诸多知名CA(如Verisign、GoDaddy、美国国防部、 CNNIC等)的数字证书校验方法,只要是这些CA机构颁发的证书,浏览器都能校验。对于CA未知的证书,浏览器则会报错(就像下面那个截图一 样)。主流浏览器都有证书管理功能,但鉴于这些功能比较高级,一般用户是不用去关心的。

通过opssl生产密钥对:

生成私钥server.key

1
openssl genrsa -out server.key 2048

创建证书请求 server.csr

1
openssl req -new -key server.key -out server.csr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []: // 国家
State or Province Name (full name) []: // 省份
Locality Name (eg, city) []: // 城市
Organization Name (eg, company) []: // 组织名称
Organizational Unit Name (eg, section) []: // 组织单位名称
Common Name (eg, fully qualified host name) []: localhost.xiaoyeshiyu.com // 服务域名,认证时会用到,用于指定被认证的服务器域名绑定
Email Address []: // 邮件地址

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:

使用上面的key和证书生成请求生成证书文件 server.crt

1
openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 365

启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

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

func main() {
http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
fmt.Println(request.RemoteAddr)
_, err := writer.Write([]byte("hello world"))
if err != nil {
log.Fatal(err)
}
})
log.Fatal(http.ListenAndServeTLS(":8080", "../cert/server.crt", "../cert/server.key", nil))
}

此时访问https://localhost.xiaoyeshiyu.com:8080/hello会弹出提示,需要信任证书。

image-20221018001204553

也可以通过curl命令测试

1
curl -k https://localhost.xiaoyeshiyu.com:8080/hello
1
hello world

如果不加 -k则会报错如下

1
2
3
4
5
6
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

使用跳过验证的客户端

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
package main

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)

func main() {
client := http.Client{}
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
resp, err := client.Get("https://localhost.xiaoyeshiyu.com:8080/hello")
if err != nil {
log.Fatal(err)
}
all, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(all))
}
1
hello world

多数时候,我们需要对服务端的证书进行校验,而不是像上面client2.go那样忽略这个校验。我大脑中的这个产品需要服务端和客户端双向 校验,我们先来看看如何能让client端实现对Server端证书的校验呢?

client端校验证书的原理是什么呢?回想前面我们提到的浏览器内置了知名CA的相关信息,用来校验服务端发送过来的数字证书。那么浏览器 存储的到底是CA的什么信息呢?其实是CA自身的数字证书(包含CA自己的公钥)。而且为了保证CA证书的真实性,浏览器是在出厂时就内置了 这些CA证书的,而不是后期通过通信的方式获取的。CA证书就是用来校验由该CA颁发的数字证书的。

那么如何使用CA证书校验Server证书的呢?这就涉及到数字证书到底是什么了!

我们可以通过浏览器中的”https/ssl证书管理”来查看证书的内容,一般服务器证书都会包含诸如站点的名称和主机名、公钥、签发机构 (CA)名称和来自签发机构的签名等。我们重点关注这个来自签发机构的签名,因为对于证书的校验,就是使用客户端CA证书来验证服务端证书的签名是否这 个CA签的。

通过签名验证我们可以来确认两件事:
1、服务端传来的数字证书是由某个特定CA签发的(如果是self-signed,也无妨),数字证书中的签名类似于日常生活中的签名,首先 验证这个签名签的是Tony Bai,而不是Tom Bai, Tony Blair等。
2、服务端传来的数字证书没有被中途篡改过。这类似于”Tony Bai”有无数种写法,这里验证必须是我自己的那种写法,而不是张三、李四写的”Tony Bai”。

一旦签名验证通过,我们因为信任这个CA,从而信任这个服务端证书。由此也可以看出,CA机构的最大资本就是其信用度。

CA在为客户签发数字证书时是这样在证书上签名的:

数字证书由两部分组成:
1、C:证书相关信息(对象名称+过期时间+证书发布者+证书签名算法….)
2、S:证书的数字签名

其中的数字签名是通过公式S = F(Digest(C))得到的。

Digest为摘要函数,也就是 md5、sha-1或sha256等单向散列算法,用于将无限输入值转换为一个有限长度的“浓缩”输出值。比如我们常用md5值来验证下载的大文件是否完 整。大文件的内容就是一个无限输入。大文件被放在网站上用于下载时,网站会对大文件做一次md5计算,得出一个128bit的值作为大文件的 摘要一同放在网站上。用户在下载文件后,对下载后的文件再进行一次本地的md5计算,用得出的值与网站上的md5值进行比较,如果一致,则大 文件下载完好,否则下载过程大文件内容有损坏或源文件被篡改。

F为签名函数。CA自己的私钥是唯一标识CA签名的,因此CA用于生成数字证书的签名函数一定要以自己的私钥作为一个输入参数。在RSA加密 系统中,发送端的解密函数就是一个以私钥作 为参数的函数,因此常常被用作签名函数使用。签名算法是与证书一并发送给接收 端的,比如apple的一个服务的证书中关于签名算法的描述是“带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )”。因此CA用私钥解密函数作为F,对C的摘要进行运算得到了客户数字证书的签名,好比大学毕业证上的校长签名,所有毕业证都是校长签发的。

接收端接收服务端数字证书后,如何验证数字证书上携带的签名是这个CA的签名呢?接收端会运用下面算法对数字证书的签名进行校验:
F’(S) ?= Digest(C)

接收端进行两个计算,并将计算结果进行比对:
1、首先通过Digest(C),接收端计算出证书内容(除签名之外)的摘要。
2、数字证书携带的签名是CA通过CA密钥加密摘要后的结果,因此接收端通过一个解密函数F’对S进行“解密”。RSA系统中,接收端使用 CA公钥对S进行“解密”,这恰是CA用私钥对S进行“加密”的逆过程。

将上述两个运算的结果进行比较,如果一致,说明签名的确属于该CA,该证书有效,否则要么证书不是该CA的,要么就是中途被人篡改了。

但对于self-signed(自签发)证书来说,接收端并没有你这个self-CA的数字证书,也就是没有CA公钥,也就没有办法对数字证 书的签名进行验证。因此如果要编写一个可以对self-signed证书进行校验的接收端程序的话,首先我们要做的就是建立一个属于自己的 CA,用该CA签发我们的server端证书,并将该CA自身的数字证书随客户端一并发布。

创建CA

1
2
openssl genrsa -out ca.key 2048 // 生成CA私钥
openssl req -x509 -new -nodes -key ca.key -subj "/CN=localhost.xiaoyeshiyu.com" -days 5000 -out ca.crt // 生成CA数字证书

生成server端的私钥,生成数字证书请求,并用我们的ca私钥签发server的数字证书:

1
2
3
openssl genrsa -out server.key 2048    // 生成server私钥
openssl req -new -key server.key -subj "/CN=localhost.xiaoyeshiyu.com" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost.xiaoyeshiyu.com")) -out server.csr // 生成数字证书请求
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extensions SAN -extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost.xiaoyeshiyu.com")) -out server.crt -days 5000 // 用ca私钥签发server的数字证书

ps:Go 1.15 版本开始废弃 CommonName,因此要使用 SAN 证书。不然客户端会出现报错:

1
x509: certificate relies on legacy Common Name field, use SANs instead

服务端代码不改变,客户端则需要通过ca的数字证书校验服务端的证书

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
package main

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)

func loadCA(caFile string) *x509.CertPool {
pool := x509.NewCertPool()
if ca, err := ioutil.ReadFile(caFile); err != nil {
log.Fatal(err)
} else {
pool.AppendCertsFromPEM(ca)
}
return pool
}

func main() {
client := http.Client{}
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: loadCA("../cert/ca.crt"),
},
}
resp, err := client.Get("https://localhost.xiaoyeshiyu.com:8080/hello")
if err != nil {
log.Fatal(err)
}
all, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(all))
}

socket认证

socket通信也可以使用TLS认证,这里使用双向验证举例

生成客户端的私钥

1
openssl genrsa -out client.key 2048

生成证书签名请求

1
openssl req -new -key client.key -subj "/CN=mitaka_localhost" -out client.csr

用自己的CA私钥对客户端端提交的csr进行签名处理,得到客户端端的数字证书client.crt

1
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000

安全通道server端,需要加载用于校验客户端证书的ca.crt

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
package main

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
)

func LoadCa(path string) *x509.CertPool {
pool := x509.NewCertPool()

ca, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal(err)
}

pool.AppendCertsFromPEM(ca)

return pool
}

func main() {
log.SetFlags(log.Llongfile)

// 加载证书
pair, err := tls.LoadX509KeyPair("../cert/server.crt", "../cert/server.key")
if err != nil {
log.Fatal(err)
}

// 加载验证客户端证书的ca证书
pool := LoadCa("../cert/ca.crt")
config := tls.Config{
Certificates: []tls.Certificate{pair},
// 验证证书,并且验证请求的主机名与证书一致
InsecureSkipVerify: false,
// 用来校验客户端证书的ca certificate
ClientCAs: pool,
// 实现Server强制校验client端证书
ClientAuth: tls.RequireAndVerifyClientCert,
}
listen, err := tls.Listen("tcp", ":8080", &config)
if err != nil {
log.Fatal(err)
}
for {
conn, err := listen.Accept()
if err != nil {
log.Println(err)
continue
}
read := make([]byte, 100)
_, err = conn.Read(read)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(read))
_, err = conn.Write([]byte("hello world"))
if err != nil {
log.Println(err)
continue
}
}
}

client

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
package main

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
)

func LoadCa(path string) *x509.CertPool {
pool := x509.NewCertPool()

ca, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal(err)
}

pool.AppendCertsFromPEM(ca)

return pool
}

func main() {
// 加载ca证书,用于给客户端证书签名
pool := LoadCa("../cert/ca.crt")

// 加载客户端证书
keyPair, _ := tls.LoadX509KeyPair("../cert/client.crt", "../cert/client.key")
config := tls.Config{
Certificates: []tls.Certificate{keyPair},
InsecureSkipVerify: false,
RootCAs: pool,
// 用于校验请求主机名是否与服务端证书一致
ServerName: "localhost.xiaoyeshiyu.com",
}

// 这里使用域名连接,ServerName则可不需要
conn, err := tls.Dial("tcp", ":8080", &config)
if err != nil {
log.Fatal(err)
}
_, err = conn.Write([]byte("i am client"))
if err != nil {
log.Fatal(err)
}
read := make([]byte, 100)
_, err = conn.Read(read)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(read))
}

发起测试

服务端

1
2
go run main.go
i am client

客户端

1
2
go run main.go
hello world

推荐阅读:

Go和HTTPS

废弃 CommonName