前言:大家好,以下所有内容都是我学习韩茹老师的教程时所整理的笔记。部分内容有过删改, 推荐大家去看原作者的文档进行学习, 本文章仅作为个人的学习笔记,后续还会在此基础上不断修改。学习Go Web时应该已经熟悉Go语言基本语法以及计算机网络的相关内容。
学习链接:https://www.chaindesk.cn/witbook/17/253
参考书籍:《Go Web编程》谢孟军
RPC(Remote Procedure Call Protocol)——远程过程调用协议 (补充:RPC是一个协议!),是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互编程。远程过程调用是构建分布式应用的理论基础,它简单而又广受欢迎。 远程过程调用总是由客户端对服务器发出一个执行若干过程请求,并用客户端提供的参数。执行结果将返回给客户端。
一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
那么我们至少从这样的描述中挖掘出几个要点:
远程过程调用流程图:
运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
在golang中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持。Go RPC可以利用tcp或http来传递数据,可以对要传递的数据使用多种类型的编解码方式。golang官方的net/rpc
库使用encoding/gob
进行编解码,支持tcp
或http
数据传输方式,由于其他语言不支持gob
编解码方式,所以使用net/rpc
库实现的RPC方法没办法进行跨语言调用。
golang官方还提供了net/rpc/jsonrpc
库实现RPC方法,JSON RPC 采用JSON进行数据编解码,因而支持跨语言调用。但目前的 jsonrpc 库是基于tcp协议实现的,暂时不支持使用http进行数据传输。
除了golang官方提供的rpc库,还有许多第三方库为在golang中实现RPC提供支持,如:protorpc库,大部分第三方rpc库的实现都是使用protobuf
进行数据编解码,根据protobuf
声明文件自动生成rpc方法定义与服务注册代码,在golang中可以很方便的进行rpc服务调用。
Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。
先介绍Go官方提供的 net/rpc
包,它提供了通过网络访问一个对象的方法的能力。服务器需要注册对象, 通过对象的类型名暴露这个服务。注册后这个对象的输出方法就可以远程调用,这个库封装了底层传输的细节,包括序列化。服务器可以注册多个不同类型的对象,但是注册相同类型的多个对象的时候会出错。
Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:
所以一个输出方法的格式如下:
func (t *T) MethodName(argType T1, replyType *T2) error
这里的T
、T1
、T2
能够被encoding/gob
序列化。
这个方法的第一个参数代表调用者(client)提供的参数,第二个参数代表要返回给调用者的计算结果,方法的返回值如果不为空, 那么它作为一个字符串返回给调用者。
如果返回error,则reply参数不会返回给调用者。
服务器通过调用ServeConn
在一个连接上处理请求,更典型地, 它可以创建一个network listener然后accept请求。对于HTTP listener来说,可以调用 HandleHTTP
和 http.Serve
。
客户端可以调用Dial
和DialHTTP
建立连接。 客户端有两个方法调用服务: Call
和 Go
,可以同步地或者异步地调用服务。当然,调用的时候,需要把服务名、方法名和参数传递给服务器。异步方法调用Go
通过 Done
channel通知调用结果返回。
除非显示的设置codec
,否则这个库默认使用包encoding/gob
作为序列化框架。
首选介绍一个简单的例子。 这个例子中提供了面积和周长的两个方法。
我们先看服务端:
第一步你需要定义传入参数和返回参数的数据结构:
type Params struct {
Width, Height int
}
第二步定义一个服务对象,这个服务对象可以很简单, 比如类型是int
或者是interface{}
, 重要的是它输出的方法。 这里我们定义一个算术类型Rect,它可以是任意类型,也可以是int类型,但是这个值我们在后面方法的实现中也没用到,所以它基本上就起一个辅助的作用。
type Rect struct{}
第三步实现这个类型的两个方法, 面积和周长:
func (r *Rect) Area(p Params, ret *int) error {
*ret = p.Width * p.Height
return nil
}
func (r *Rect) Perimeter(p Params, ret *int) error {
*ret = (p.Width + p.Height) * 2
return nil
}
目前为止,我们的准备工作已经完成,继续下面的步骤。
第四步实现RPC服务器:
rect := new(Rect)
//注册一个rect服务
rpc.Register(rect)
//把服务处理绑定到http协议上
rpc.HandleHTTP()
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
这里我们生成了一个rect对象,并使用rpc.Register
注册这个服务,然后通过HTTP暴露出来。 客户端可以看到服务rect以及它的两个方法Rect.Area
和Rect.Perimeter
。
客户端:
最后创建一个客户端,建立客户端和服务器端的连接:
rpc, err := rpc.DialHTTP("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
然后客户端就可以进行远程调用了。
ret := 0
//调用远程方法
//注意第三个参数是指针类型
err2 := rpc.Call("Rect.Area", Params{50, 100}, &ret)
if err2 != nil {
log.Fatal(err2)
}
fmt.Println(ret)
err3 := rpc.Call("Rect.Perimeter", Params{50, 100}, &ret)
if err3 != nil {
log.Fatal(err3)
}
fmt.Println(ret)
或者异步的方式:
divCall := client.Go("Arith.Divide", Params{50, 100}, &ret, nil)
<-divCall.Done
fmt.Println(ret)
通过上面的调用可以看到参数是我们定义的struct类型,返回值是int类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为client.Call的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。
以上案例完整代码:
服务器端:
package main
import (
"net/rpc"
"net/http"
"log"
)
type Params struct {
Width, Height int
}
type Rect struct{}
func (r *Rect) Area(p Params, ret *int) error {
*ret = p.Width * p.Height
return nil
}
func (r *Rect) Perimeter(p Params, ret *int) error {
*ret = (p.Width + p.Height) * 2
return nil
}
func main() {
rect := new(Rect)
//注册一个rect服务
rpc.Register(rect)
//把服务处理绑定到http协议上
rpc.HandleHTTP()
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
客户端代码:
package main
import (
"fmt"
"log"
"net/rpc"
)
type Params struct {
Width, Height int
}
func main() {
rpc, err := rpc.DialHTTP("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal(err)
}
ret := 0
//调用远程方法
//注意第三个参数是指针类型
err2 := rpc.Call("Rect.Area", Params{50, 100}, &ret)
if err2 != nil {
log.Fatal(err2)
}
fmt.Println(ret)
err3 := rpc.Call("Rect.Perimeter", Params{50, 100}, &ret)
if err3 != nil {
log.Fatal(err3)
}
fmt.Println(ret)
}
客户端运行结果如下:
5000
300
上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示:
package main
import (
"net/rpc"
"log"
"net"
)
type Params struct {
Width, Height int
}
type Rect struct{}
func (r *Rect) Area(p Params, ret *int) error {
*ret = p.Width * p.Height
return nil
}
func (r *Rect) Perimeter(p Params, ret *int) error {
*ret = (p.Width + p.Height) * 2
return nil
}
func main() {
rect := new(Rect)
//注册一个rect服务
rpc.Register(rect)
tcpAddr, err :=net.ResolveTCPAddr("tcp",":2001")
if err != nil {
log.Fatal(err)
}
listener,err := net.ListenTCP("tcp",tcpAddr)
if err != nil {
log.Fatal(err)
}
for{
conn,err :=listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
上面这个代码和http的服务器相比,不同在于: 在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。
如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现。 下面展现了TCP实现的RPC客户端:
package main
import (
"fmt"
"log"
"net/rpc"
)
type Params struct {
Width, Height int
}
func main() {
rpc, err := rpc.Dial("tcp", "127.0.0.1:2001")
if err != nil {
log.Fatal(err)
}
ret := 0
//调用远程方法
//注意第三个参数是指针类型
err2 := rpc.Call("Rect.Area", Params{50, 100}, &ret)
if err2 != nil {
log.Fatal(err2)
}
fmt.Println(ret)
err3 := rpc.Call("Rect.Perimeter", Params{50, 100}, &ret)
if err3 != nil {
log.Fatal(err3)
}
fmt.Println(ret)
}
客户端运行结果:
5000
300
这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。
上面的例子我们演示了使用net/rpc
实现RPC的过程,但是没办法在其他语言中调用上面例子实现的RPC方法。所以接下来的例子我们演示一下使用net/rpc/jsonrpc
库实现RPC方法,此方式实现的RPC方法支持跨语言调用。
服务端示例代码:
package main
import (
"net/rpc"
"os"
"net/rpc/jsonrpc"
"net"
"log"
"fmt"
"errors"
)
// 算数运算结构体
type Arith struct {
}
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
res.Pro = req.A * req.B
return nil
}
// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
if req.B == 0 {
return errors.New("divide by zero")
}
res.Quo = req.A / req.B
res.Rem = req.A % req.B
return nil
}
func main() {
rpc.Register(new(Arith)) // 注册rpc服务
lis, err := net.Listen("tcp", ":8096")
if err != nil {
log.Fatalln("fatal error: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "start connection")
for {
conn, err := lis.Accept() // 接收客户端连接请求
if err != nil {
continue
}
go func(conn net.Conn) { // 并发处理客户端请求
fmt.Fprintf(os.Stdout, "%s", "new client in coming\n")
jsonrpc.ServeConn(conn)
}(conn)
}
}
客户端示例代码:
package main
import (
"net/rpc/jsonrpc"
"log"
"fmt"
)
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
func main() {
conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8096")
if err != nil {
log.Fatalln("dailing error: ", err)
}
req := ArithRequest{9, 2}
var res ArithResponse
err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
if err != nil {
log.Fatalln("arith error: ", err)
}
fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)
err = conn.Call("Arith.Divide", req, &res)
if err != nil {
log.Fatalln("arith error: ", err)
}
fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}
客户端运行结果:
9 * 2 = 18
9 / 2, quo is 4, rem is 1
论复杂度,RPC框架肯定是高于简单的HTTP接口的。但毋庸置疑,HTTP接口由于受限于HTTP协议,需要带HTTP请求头,导致传输起来效率或者说安全性不如RPC。
下面展示了一个请求头,无用数据过多,响应头类似
GET /search/suggest/initial_page/ HTTP/1.1
Host www.toutiao.com
Content-Type application/x-www-form-urlencoded
Accept-Encoding gzip, deflate
Cookie CNZZDATA1259612802=569328305-1527816505-https%253A%252F%252Fwww.baidu.com%252F%7C1527821905; WEATHER_CITY=%E5%8C%97%E4%BA%AC; __tasessionId=i33ntavtt1527822487203; tt_webid=6561930712081466884; UM_distinctid=163b91509b88ff-0e1ee05ec87bae-3f636c4f-13c680-163b91509b96aa; _ga=GA1.2.195827442.1493809988; sso_login_status=0; tt_webid=6561930712081466884; uuid="w:b5453e80f63342d1afe07a5d3c3360f9"
Connection keep-alive
Proxy-Connection keep-alive
Accept text/javascript, text/html, application/xml, text/xml, */*
User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15
Referer https://www.toutiao.com/
Accept-Language zh-cn
X-Requested-With XMLHttpRequest
http接口是在接口不多、系统与系统交互较少的情况下;优点就是简单、直接、开发方便。利用现成的http协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http 一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统 一化的操作。
HTTP与RPC存在重大不同的是:请求是使用具有标准语义的通用的接口定向到资源的,这些语义能够被中间组件和提供服务的来源机器进行解释。结果是使得一个应用支持分层的转换和间接层,并且独立于消息的来源。
与之相比较,RPC的机制是根据语言的API来定义的,而不是根据基于网络的应用来定义的。
事实上,对于http,也可以作为RPC框架的通信层协议和实现。 只不过,对于大部分企业成熟的RPC框架,使用thrift等工具可以实现二进制传输,相比HTTP的文本传输无疑大大提高了传输效率; HTTP通常使用的json,一个需要用户序列化/反序列化,性能和复杂度较高。相比之下,Thrift等工具,使用了成熟的代码生成技术,将通信接口的文件生成了对应语言的代码接口,实现了远程调用接近于本地方法的调用。另外无论是网络传输编码、解码,还是传输内容大小还是网络开销都想比HTTP有较大的优势。
一般rpc框架包括:服务查找,负载均衡,服务降级、熔断,下游路由配置,数据格式约定,链接维护等几个方面。
手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接。TCP协议可以对上层网络提供接口,使上层网络数据的传输建立在“无差别”的网络之上。
建立起一个TCP连接需要经过“三次握手”:(补充:翻一翻计算机网络的相关内容)
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)
HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。
**http 为短连接:**客户端发送请求都需要服务器端回送响应。请求结束后,主动释放链接,因此为短连接。要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的 做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客 户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。
TCP是底层通讯协议,定义的是数据传输和连接方式的规范 HTTP是应用层协议,定义的是传输数据的内容的规范。 http与tcp不是对等的两种协议,http是可以基于tcp传输的协议。
**Socket为长连接:**Socket 是对 TCP/IP 协议的封装,Socket 只是个接口不是协议,通过 Socket 我们去使用 TCP/IP 协议,除了 TCP,也可以使用 UDP 协议来传递数据。
Socket也称作套接字,套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示。建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户 端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
HTTP连接使用的是"请求-响应"方式,不仅在请求时才建立连接,而且客户端向服务器端请求后,服务器才返回数据。
创建 Socket 连接的时候,可以指定传输层协议,可以是 TCP 或者 UDP。通常情况下Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通讯双方开始互发数据内容,直到双方断开连接。
很多情况下,都是需要服务器端向客户端主动推送数据,保持客户端与服务端的实时同步。
若双方是 Socket 连接,可以由服务器直接向客户端发送数据。
若双方是 HTTP 连接,则服务器需要等客户端发送请求后,才能将数据回传给客户端。
因此,客户端定时向服务器端发送请求,不仅可以保持在线,同时也询问服务器是否有新数据,如果有就将数据传给客户端。