说到网络,避不开的是OSI
层次模型。下三层主要实现互联网通信,通过路由、IP、MAC地址实现跨跨网段和局域网内部通信,而编程过程一般会用到网络层、传输层和应用层,以及传输层和应用层之间的Socket抽象层。
Socket编程 IP地址和端口对应一个socket
,如果两个进程分别有两个socket
,进程相互通信就可以将socket
组成一个socket pair
。
常见的socket
类型有两种:流式Socket
(SOCK_STREAM
)和数据报式Socket
(SOCKET_DGRAM
)。前者是一种面向连接,针对TCP
,后者是一种无连接的socket
,对应UDP
。
TCP 例如基于TCP
的socket
通信
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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "fmt" "log" "net" "sync" ) var conMap sync.Mapfunc 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 mainimport ( "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 mainimport ( "fmt" "log" "net" "sync" ) var conMap sync.Mapfunc 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 mainimport ( "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 mainimport ( "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" ) if err != nil { log.Fatal(err) } udpConn, err := net.ListenUDP("udp" , udpAddr) if err != nil { log.Fatal(err) } defer udpConn.Close() udp := &net.UDPAddr{} go func () { for { buf := make ([]byte , 1024 ) _, udp, err = udpConn.ReadFromUDP(buf) if err != nil { log.Println(err) } fmt.Printf("receive: %s\n" , buf) } }() 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()) } }
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 mainimport ( "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 mainimport ( "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 } 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 } 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 mainimport ( "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
工具连接到服务端
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 mainimport ( "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 { 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 mainimport ( "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)) } }
输出
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的数据传输模式是流模式,在保持长连接的时候,数据可以多次发送或者说多次接受,在客户端和服务端都会发生粘包。
粘包解决办法 出现粘包的问题是不知道这次发送的数据量是多少,所以解决办法可以分为两种
将本次传输的数据量在每次传输的时候传入
两端固定每次传输的数据量和接收的数据量,这种方式需要在对端将数据组装起来,而且需要保证发送端不出现粘包。
这里使用方法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 toolsimport ( "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 ) { messageLength, _ := reader.Peek(4 ) buffer := bytes.NewBuffer(messageLength) var length int32 err := binary.Read(buffer, binary.LittleEndian, &length) if err != nil { return "" , err } 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 mainimport ( "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 mainimport ( "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 mainimport ( "log" "net" "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.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 mainimport ( "fmt" "log" "net/rpc" ) type Calc struct { A int B int } func main () { conn, err := rpc.Dial("tcp" , ":8080" ) 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) }
基于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 mainimport ( "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() http.Serve(listener, nil ) }
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 mainimport ( "fmt" "log" "net/rpc" ) type Calc struct { A int B int } func main () { conn, err := rpc.DialHTTP("tcp" , ":8080" ) 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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "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 type CalcServiceServer interface { Add(context.Context, *AddRequest) (*AddReply, error ) } 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 mainimport ( "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)) 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 mainimport ( "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 mainimport ( "fmt" "io" "log" "net/http" ) func HelloServer (w http.ResponseWriter, r *http.Request) { 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) io.ReadAll(r.Body) 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" : "/" , "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 mainimport ( "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 mainimport ( "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) }
输出
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中可以包含path
、query
参数等,可以通过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 mainimport ( "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
请求的方法有很多种,例如GET
、POST
、DELETE
、DATCH
、PUT
。
上面的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) mux.HandleFunc("/product" , Handler) http.ListenAndServe(":8080" , mux)
中间件 AOP
:横向关注点,一般用于解决Log
,tracing
,metric
,熔断,限流等。例如在http开发中使用责任链模式。
中间件的实现方式也可以基于设计模式中的装饰器模式,核心逻辑是在闭包中,传入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()) fmt.Println(i.Index) fmt.Println(i.Flags) fmt.Println(i.HardwareAddr.String()) }
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 mainimport ( "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
会弹出提示,需要信任证书。
也可以通过curl
命令测试
1 curl -k https://localhost.xiaoyeshiyu.com:8080/hello
如果不加 -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 mainimport ( "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)) }
多数时候,我们需要对服务端的证书进行校验,而不是像上面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 mainimport ( "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 mainimport ( "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) } pool := LoadCa("../cert/ca.crt" ) config := tls.Config{ Certificates: []tls.Certificate{pair}, InsecureSkipVerify: false , ClientCAs: pool, 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 mainimport ( "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 () { 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" , } 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