http实现编程
服务端
RPC的概念
RPC(Remote Procedure Call Protocol),是远程过程调用的缩写,通俗的说就是调用远处的一个函数。与之相对应的是本地函数调用,我们先来看一下本地函数调用。当我们写下如下代码的时候:result := Add(1,2)
我们知道,我们传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值。这时参数,返回值,代码段都在一个进程空间内,这是本地函数调用。
那有没有办法,我们能够调用一个跨进程(所以叫”远程”,典型的事例,这个进程部署在另一台服务器上)的函数呢?
这也是RPC主要实现的功能。
RPC开发的要素分析
为什么微服务需要RPC
我们使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,如下图:
这样的话,如果没有统一的服务框架,RPC框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。所以,统一RPC框架把上述“业务之外”的技术劳动统一处理,是服务化首要解决的问题。
RPC:远程进通信 —— 应用层协议(http协议同层)。底层使用 TCP 实现。
理解RPC:==像调用本地函数一样,去调用远程函数。==
- 通过rpc协议,传递:函数名、函数参数。达到在本地,调用远端函数,得返回值到本地的目标。
为什么微服务使用 RPC:
- 每个服务都被封装成进程。彼此”独立“。
- 进程和进程之间,可以使用不同的语言实现。
RPC 入门使用1
远程 —— 网络!!
回顾:Go语言 一般性 网络socket通信
server端:
net.Listen() —— listener 创建监听器
listener.Accpet() —— conn 启动监听,建立连接
conn.read()
conn.write()
defer conn.Close() / listener.Close()
client端:
net.Dial() —— conn
conn.Write()
conn.Read()
defer conn.Close()
RPC 使用的步骤
编程实现:
—— 服务端:
- 注册 rpc 服务对象。给对象绑定方法( 1. 定义类, 2. 绑定类方法 )
rpc.RegisterName("服务名",回调对象)
- 创建监听器
listener, err := net.Listen()
- 建立连接
conn, err := listener.Accept()
- 将连接 绑定 rpc 服务。
rpc.ServeConn(conn)
—— 客户端:
- 用 rpc 连接服务器。
conn, err := rpc.Dial()
- 调用远程函数。
conn.Call("服务名.方法名", 传入参数, 传出参数)
RPC 相关函数
- 注册 rpc 服务
```go
func (server *Server) RegisterName(name string, rcvr interface{}) error
参1:服务名,字符串类型。将来我们的RPC服务叫什么名字,方便我们找到对应的服务。
参2:对应 rpc 对象。 因为是空接口。可以传任意类型
举例:该对象绑定方法要满足如下条件:
1)方法必须是导出的即包外可见。 就是首字母大写。
2)方法必须有两个参数,都是导出类型、內建类型。
3)方法的第二个参数必须是 “指针” (传出参数)
4)方法只有一个 error 接口类型的 返回值。
type World stuct {
}
func (this World) HelloWorld (name string, resp string) error {
}
rpc.RegisterName(“服务名”, new(World))
2. 绑定 rpc 服务
```go
func (server *Server) ServeConn(conn io.ReadWriteCloser)
conn: 成功建立好连接的 socket —— conn
- 调用远程函数
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error
serviceMethod: “服务名.方法名”
args:传入参数。 方法需要的数据。
reply:传出参数。定义 var 变量,&变量名 完成传参。
编码实现
编程实现这里,先看下面的RPC版的helloword,再看csserver端
```go package main
import ( “net/rpc” “fmt” “net” )
// 定义类对象 type World struct { }
// 绑定类方法 func (this World) HelloWorld (name string, resp string) error { *resp = name + “ 你好!” return nil }
func main() { // 1. 注册RPC服务, 绑定对象方法 err := rpc.RegisterName(“hello”, new(World)) if err != nil { fmt.Println(“注册 rpc 服务失败!”, err) return }
// 2. 设置监听
listener, err := net.Listen("tcp", "127.0.0.1:8800")
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
defer listener.Close()
fmt.Println("开始监听 ...")
// 3. 建立链接
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept() err:", err)
return
}
defer conn.Close()
fmt.Println("链接成功...")
// 4. 绑定服务
rpc.ServeConn(conn)
}
<a name="NurEl"></a>
### client端
```go
package main
import (
"net/rpc"
"fmt"
)
func main() {
// 1. 用 rpc 链接服务器 --Dial()
conn, err := rpc.Dial("tcp", "127.0.0.1:8800")
if err != nil {
fmt.Println("Dial err:", err)
return
}
defer conn.Close()
// 2. 调用远程函数
var reply string // 接受返回值 --- 传出参数
err = conn.Call("hello.HelloWorld", "李白", &reply)
if err != nil {
fmt.Println("Call:", err)
return
}
fmt.Println(reply)
}
RPC版的”hello world”
Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。接着我们尝试基于rpc实现一个类似的例子。我们先构造一个HelloService类型,其中的Hello方法用于实现打印功能:
type HelloService struct{}
func(p *HelloService)Hello(request string,reply *string)error{
*reply = "hello:" + request
return nil
}
Hello方法方法必须满足Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。
golang 中的类型比如:channel(通道)、complex(复数类型)、func(函数)均不能进行序列化
然后就可以将HelloService类型的对象注册为一个RPC服务:
func main(){
//rpc注册服务
//注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址
rpc.RegisterName("HelloService",new(HelloService))
//设置服务监听
listener,err := net.Listen("tcp",":1234")
if err != nil {
panic(err)
}
//接受传输的数据
conn,err := listener.Accept()
if err != nil {
panic(err)
}
//rpc调用,并返回执行后的数据
//1.read,获取服务名称和方法名,获取请求数据
//2.调用对应服务里面的方法,获取传出数据
//3.write,把数据返回给client
rpc.ServeConn(conn)
}
其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
下面是客户端请求HelloService服务的代码:
func main(){
//用rpc连接
client,err := rpc.Dial("tcp","localhost:1234")
if err != nil {
panic(err)
}
var reply string
//调用服务中的函数
err = client.Call("HelloService.Hello","world",&reply)
if err != nil {
panic(err)
}
fmt.Println("收到的数据为,",reply)
}
首选是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定义RPC方法的两个参数。
json 版 rpc
经过序列化发出去的数据,可以实现跨语言,比如用python当客户端,用go写服务端,交流都是用的json格式的数据。
- 使用 nc -l 127.0.0.1 880 充当服务器。运行这个命令,就不需要我们自己写server了。
- 02-client.go 充当 客户端。 发起通信。 —— 乱码。
- 前面没有乱码是因为服务器和客户端都是我们自己写的,net包中的函数内部底层封装了一系列的序列化的操作。
- 因为:这里乱码是因为我们服务端是命令发起的不是我们自己写的。RPC 使用了go语言特有的数据序列化 gob。 其他编程语言不能解析。
- 使用 通用的 序列化、反序列化。 —— json、protobuf
修改客户端
修改客户端,使用jsonrpc:conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8800")
使用 nc -l 127.0.0.1 880 充当服务器。
看到结果:
{“method”:”hello.HelloWorld”,”params”:[“李白”],”id”:0}修改服务器端
修改服务器端,使用 jsonrpc:jsonrpc.ServeConn(conn)
使用 nc 127.0.0.1 880 充当客户端。
看到结果:
echo -e ‘{“method”:”hello.HelloWorld”,”params”:[“李白”],”id”:0}’ | nc 127.0.0.1 8800
如果,绑定方法返回值的 error 不为空? 无论传出参数是否有值,服务端都不会返回数据。RPC 封装
上面的代码服务名都是写死的,不够灵活(容易写错),这里我们对RPC的服务端和客户端再次进行一次封装,来屏蔽掉服务名。服务端封装
```go package server_proxy
import ( “OldPackageTest/new_helloworld/hanlder” “net/rpc” )
type HelloServicer interface { Hello(request string, reply *string) error }
//如果做到解耦 - 我们关系的是函数 鸭子类型 func RegisterHelloService(srv HelloServicer) error { return rpc.RegisterName(hanlder.HelloServiceName, srv) }
```go
package hanlder
//名称冲突的问题
const HelloServiceName = "handler/HelloService"
//我们关心的是NewHelloService这个名字呢 还是这个结构体中的方法
type NewHelloService struct{}
func (s *NewHelloService) Hello(request string, reply *string) error {
//返回值是通过修改reply的值
*reply = "hello, " + request
return nil
}
封装之后的服务端实现如下:
package main
import (
"OldPackageTest/new_helloworld/hanlder"
"net"
"net/rpc"
"OldPackageTest/new_helloworld/server_proxy"
)
func main() {
//1. 实例化一个server
listener, _ := net.Listen("tcp", ":1234")
//2. 注册处理逻辑 handler
_ = server_proxy.RegisterHelloService(&hanlder.NewHelloService{})
//3. 启动服务
for {
conn, _ := listener.Accept() //当一个新的连接进来的时候,
go rpc.ServeConn(conn)
}
//一连串的代码大部分都是net的包好像和rpc没有关系
//不行。rpc调用中有几个问题需要解决 1. call id 2. 序列化和反序列化 编码和解码
//python下的开发而言 这个就显得不好用
//可以跨语言调用呢 1. go语言的rpc的序列化协议是什么(Gob) 2. 能否替换成常见的序列化
}
客户端封装
// 定义类
type MyClient struct {
c *rpc.Client
}
// 初始客户端,初始化的时候,直接拨号加返回一个rpc协议的客户端
func InitClient(addr string) MyClient {
conn, _ := jsonrpc.Dial("tcp", adddr)
return MyClient{c:conn}
}
// 绑定类方法, rpc协议的客户端调用里面的c成员,然后调用Call
func (this *MyClient)HelloWorld(a string, b *string) error {
return this.c.Call("hello.HelloWorld", a, b)
}
package client_proxy
import (
"OldPackageTest/new_helloworld/hanlder"
"net/rpc"
)
type HelloServiceStub struct {
*rpc.Client
}
//在go语言中没有类、对象 就意味着没有初始化方法
func NewHelloServiceClient(protcol, address string) HelloServiceStub {
conn, err := rpc.Dial(protcol, address)
if err != nil {
panic("connect error!")
}
return HelloServiceStub{conn}
}
func (c *HelloServiceStub) Hello(request string, reply *string) error {
err := c.Call(hanlder.HelloServiceName+".Hello", request, reply)
if err != nil {
return err
}
return nil
}
package main
import (
"OldPackageTest/new_helloworld/client_proxy"
"fmt"
)
func main() {
//1. 建立连接
client := client_proxy.NewHelloServiceClient("tcp", "localhost:1234")
//1. 只想写业务逻辑 不想关注每个函数的名称
// 客户端部分
var reply string //string有默认值
err := client.Hello("bobby", &reply)
if err != nil {
panic("调用失败")
}
fmt.Println(reply)
//1. 这些概念在grpc中都有对应
//2. 发自灵魂的拷问: server_proxy 和 client_proxy能否自动生成啊 为多种语言生成
//3. 都能满足 这个就是protobuf + grpc
}
注意,传入参数如果是两个的话,可以这样设计
service端
// rpc demo/service.go
package main
import (
"log"
"net"
"net/http"
"net/rpc"
)
type Args struct {
X, Y int
}
// ServiceA 自定义一个结构体类型
type ServiceA struct{}
// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
func main() {
service := new(ServiceA)
rpc.Register(service) // 注册RPC服务
rpc.HandleHTTP() // 基于HTTP协议
l, e := net.Listen("tcp", ":9091")
if e != nil {
log.Fatal("listen error:", e)
}
http.Serve(l, nil)
}
client 端
// rpc demo/client3.go
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
func main() {
// 建立TCP连接
conn, err := net.Dial("tcp", "127.0.0.1:9091")
if err != nil {
log.Fatal("dialing:", err)
}
// 使用JSON协议
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
// 同步调用
args := &Args{10, 20}
var reply int
err = client.Call("ServiceA.Add", args, &reply)
if err != nil {
log.Fatal("ServiceA.Add error:", err)
}
fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)
// 异步调用
var reply2 int
divCall := client.Go("ServiceA.Add", args, &reply2, nil)
replyCall := <-divCall.Done // 接收调用结果
fmt.Println(replyCall.Error)
fmt.Println(reply2)
}