http实现编程

服务端

strconv是字符串转int
image.png
image.png

RPC的概念

RPC(Remote Procedure Call Protocol),是远程过程调用的缩写,通俗的说就是调用远处的一个函数。与之相对应的是本地函数调用,我们先来看一下本地函数调用。当我们写下如下代码的时候:
result := Add(1,2)
我们知道,我们传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值。这时参数,返回值,代码段都在一个进程空间内,这是本地函数调用。
那有没有办法,我们能够调用一个跨进程(所以叫”远程”,典型的事例,这个进程部署在另一台服务器上)的函数呢?
image.png
这也是RPC主要实现的功能。

RPC开发的要素分析

image.png
image.png
image.png
image.png
image.png

为什么微服务需要RPC

我们使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,如下图:
image.png
这样的话,如果没有统一的服务框架,RPC框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。所以,统一RPC框架把上述“业务之外”的技术劳动统一处理,是服务化首要解决的问题。
RPC:远程进通信 —— 应用层协议(http协议同层)。底层使用 TCP 实现。
理解RPC:==像调用本地函数一样,去调用远程函数。==

  1. - 通过rpc协议,传递:函数名、函数参数。达到在本地,调用远端函数,得返回值到本地的目标。

为什么微服务使用 RPC:

  1. 每个服务都被封装成进程。彼此”独立“。
  2. 进程和进程之间,可以使用不同的语言实现。

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 使用的步骤

image.png
编程实现:
—— 服务端:

  1. 注册 rpc 服务对象。给对象绑定方法( 1. 定义类, 2. 绑定类方法 )

rpc.RegisterName("服务名",回调对象)

  1. 创建监听器

listener, err := net.Listen()

  1. 建立连接

conn, err := listener.Accept()

  1. 将连接 绑定 rpc 服务。

rpc.ServeConn(conn)
—— 客户端:

  1. 用 rpc 连接服务器。

conn, err := rpc.Dial()

  1. 调用远程函数。

conn.Call("服务名.方法名", 传入参数, 传出参数)

RPC 相关函数

  1. 注册 rpc 服务 ```go func (server *Server) RegisterName(name string, rcvr interface{}) error 参1:服务名,字符串类型。将来我们的RPC服务叫什么名字,方便我们找到对应的服务。 参2:对应 rpc 对象。 因为是空接口。可以传任意类型
    1. 该对象绑定方法要满足如下条件:
    2. 1)方法必须是导出的即包外可见。 就是首字母大写。
    3. 2)方法必须有两个参数,都是导出类型、內建类型。
    4. 3)方法的第二个参数必须是 “指针” (传出参数)
    5. 4)方法只有一个 error 接口类型的 返回值。
    举例:

type World stuct { }
func (this World) HelloWorld (name string, resp string) error { } rpc.RegisterName(“服务名”, new(World))

  1. 2. 绑定 rpc 服务
  2. ```go
  3. func (server *Server) ServeConn(conn io.ReadWriteCloser)
  4. conn: 成功建立好连接的 socket —— conn
  1. 调用远程函数
    1. func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error
    2. serviceMethod: “服务名.方法名”
    3. args:传入参数。 方法需要的数据。
    4. reply:传出参数。定义 var 变量,&变量名 完成传参。

    编码实现

    编程实现这里,先看下面的RPC版的helloword,再看cs

    server端

    ```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 }

  1. // 2. 设置监听
  2. listener, err := net.Listen("tcp", "127.0.0.1:8800")
  3. if err != nil {
  4. fmt.Println("net.Listen err:", err)
  5. return
  6. }
  7. defer listener.Close()
  8. fmt.Println("开始监听 ...")
  9. // 3. 建立链接
  10. conn, err := listener.Accept()
  11. if err != nil {
  12. fmt.Println("Accept() err:", err)
  13. return
  14. }
  15. defer conn.Close()
  16. fmt.Println("链接成功...")
  17. // 4. 绑定服务
  18. rpc.ServeConn(conn)

}

  1. <a name="NurEl"></a>
  2. ### client端
  3. ```go
  4. package main
  5. import (
  6. "net/rpc"
  7. "fmt"
  8. )
  9. func main() {
  10. // 1. 用 rpc 链接服务器 --Dial()
  11. conn, err := rpc.Dial("tcp", "127.0.0.1:8800")
  12. if err != nil {
  13. fmt.Println("Dial err:", err)
  14. return
  15. }
  16. defer conn.Close()
  17. // 2. 调用远程函数
  18. var reply string // 接受返回值 --- 传出参数
  19. err = conn.Call("hello.HelloWorld", "李白", &reply)
  20. if err != nil {
  21. fmt.Println("Call:", err)
  22. return
  23. }
  24. fmt.Println(reply)
  25. }

RPC版的”hello world”

Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。接着我们尝试基于rpc实现一个类似的例子。我们先构造一个HelloService类型,其中的Hello方法用于实现打印功能:

  1. type HelloService struct{}
  2. func(p *HelloService)Hello(request string,reply *string)error{
  3. *reply = "hello:" + request
  4. return nil
  5. }

Hello方法方法必须满足Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。
golang 中的类型比如:channel(通道)、complex(复数类型)、func(函数)均不能进行序列化
然后就可以将HelloService类型的对象注册为一个RPC服务:

  1. func main(){
  2. //rpc注册服务
  3. //注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址
  4. rpc.RegisterName("HelloService",new(HelloService))
  5. //设置服务监听
  6. listener,err := net.Listen("tcp",":1234")
  7. if err != nil {
  8. panic(err)
  9. }
  10. //接受传输的数据
  11. conn,err := listener.Accept()
  12. if err != nil {
  13. panic(err)
  14. }
  15. //rpc调用,并返回执行后的数据
  16. //1.read,获取服务名称和方法名,获取请求数据
  17. //2.调用对应服务里面的方法,获取传出数据
  18. //3.write,把数据返回给client
  19. rpc.ServeConn(conn)
  20. }

其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
下面是客户端请求HelloService服务的代码:

  1. func main(){
  2. //用rpc连接
  3. client,err := rpc.Dial("tcp","localhost:1234")
  4. if err != nil {
  5. panic(err)
  6. }
  7. var reply string
  8. //调用服务中的函数
  9. err = client.Call("HelloService.Hello","world",&reply)
  10. if err != nil {
  11. panic(err)
  12. }
  13. fmt.Println("收到的数据为,",reply)
  14. }

首选是通过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 充当 客户端。 发起通信。 —— 乱码。
  • image.png
    • 前面没有乱码是因为服务器和客户端都是我们自己写的,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) }

  1. ```go
  2. package hanlder
  3. //名称冲突的问题
  4. const HelloServiceName = "handler/HelloService"
  5. //我们关心的是NewHelloService这个名字呢 还是这个结构体中的方法
  6. type NewHelloService struct{}
  7. func (s *NewHelloService) Hello(request string, reply *string) error {
  8. //返回值是通过修改reply的值
  9. *reply = "hello, " + request
  10. return nil
  11. }

封装之后的服务端实现如下:

  1. package main
  2. import (
  3. "OldPackageTest/new_helloworld/hanlder"
  4. "net"
  5. "net/rpc"
  6. "OldPackageTest/new_helloworld/server_proxy"
  7. )
  8. func main() {
  9. //1. 实例化一个server
  10. listener, _ := net.Listen("tcp", ":1234")
  11. //2. 注册处理逻辑 handler
  12. _ = server_proxy.RegisterHelloService(&hanlder.NewHelloService{})
  13. //3. 启动服务
  14. for {
  15. conn, _ := listener.Accept() //当一个新的连接进来的时候,
  16. go rpc.ServeConn(conn)
  17. }
  18. //一连串的代码大部分都是net的包好像和rpc没有关系
  19. //不行。rpc调用中有几个问题需要解决 1. call id 2. 序列化和反序列化 编码和解码
  20. //python下的开发而言 这个就显得不好用
  21. //可以跨语言调用呢 1. go语言的rpc的序列化协议是什么(Gob) 2. 能否替换成常见的序列化
  22. }

客户端封装

  1. // 定义类
  2. type MyClient struct {
  3. c *rpc.Client
  4. }
  5. // 初始客户端,初始化的时候,直接拨号加返回一个rpc协议的客户端
  6. func InitClient(addr string) MyClient {
  7. conn, _ := jsonrpc.Dial("tcp", adddr)
  8. return MyClient{c:conn}
  9. }
  10. // 绑定类方法, rpc协议的客户端调用里面的c成员,然后调用Call
  11. func this *MyClientHelloWorld(a string, b *string) error {
  12. return this.c.Call("hello.HelloWorld", a, b)
  13. }
  1. package client_proxy
  2. import (
  3. "OldPackageTest/new_helloworld/hanlder"
  4. "net/rpc"
  5. )
  6. type HelloServiceStub struct {
  7. *rpc.Client
  8. }
  9. //在go语言中没有类、对象 就意味着没有初始化方法
  10. func NewHelloServiceClient(protcol, address string) HelloServiceStub {
  11. conn, err := rpc.Dial(protcol, address)
  12. if err != nil {
  13. panic("connect error!")
  14. }
  15. return HelloServiceStub{conn}
  16. }
  17. func (c *HelloServiceStub) Hello(request string, reply *string) error {
  18. err := c.Call(hanlder.HelloServiceName+".Hello", request, reply)
  19. if err != nil {
  20. return err
  21. }
  22. return nil
  23. }
  1. package main
  2. import (
  3. "OldPackageTest/new_helloworld/client_proxy"
  4. "fmt"
  5. )
  6. func main() {
  7. //1. 建立连接
  8. client := client_proxy.NewHelloServiceClient("tcp", "localhost:1234")
  9. //1. 只想写业务逻辑 不想关注每个函数的名称
  10. // 客户端部分
  11. var reply string //string有默认值
  12. err := client.Hello("bobby", &reply)
  13. if err != nil {
  14. panic("调用失败")
  15. }
  16. fmt.Println(reply)
  17. //1. 这些概念在grpc中都有对应
  18. //2. 发自灵魂的拷问: server_proxy 和 client_proxy能否自动生成啊 为多种语言生成
  19. //3. 都能满足 这个就是protobuf + grpc
  20. }

注意,传入参数如果是两个的话,可以这样设计
service端

  1. // rpc demo/service.go
  2. package main
  3. import (
  4. "log"
  5. "net"
  6. "net/http"
  7. "net/rpc"
  8. )
  9. type Args struct {
  10. X, Y int
  11. }
  12. // ServiceA 自定义一个结构体类型
  13. type ServiceA struct{}
  14. // Add 为ServiceA类型增加一个可导出的Add方法
  15. func (s *ServiceA) Add(args *Args, reply *int) error {
  16. *reply = args.X + args.Y
  17. return nil
  18. }
  19. func main() {
  20. service := new(ServiceA)
  21. rpc.Register(service) // 注册RPC服务
  22. rpc.HandleHTTP() // 基于HTTP协议
  23. l, e := net.Listen("tcp", ":9091")
  24. if e != nil {
  25. log.Fatal("listen error:", e)
  26. }
  27. http.Serve(l, nil)
  28. }

client 端

  1. // rpc demo/client3.go
  2. package main
  3. import (
  4. "fmt"
  5. "log"
  6. "net"
  7. "net/rpc"
  8. "net/rpc/jsonrpc"
  9. )
  10. func main() {
  11. // 建立TCP连接
  12. conn, err := net.Dial("tcp", "127.0.0.1:9091")
  13. if err != nil {
  14. log.Fatal("dialing:", err)
  15. }
  16. // 使用JSON协议
  17. client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
  18. // 同步调用
  19. args := &Args{10, 20}
  20. var reply int
  21. err = client.Call("ServiceA.Add", args, &reply)
  22. if err != nil {
  23. log.Fatal("ServiceA.Add error:", err)
  24. }
  25. fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)
  26. // 异步调用
  27. var reply2 int
  28. divCall := client.Go("ServiceA.Add", args, &reply2, nil)
  29. replyCall := <-divCall.Done // 接收调用结果
  30. fmt.Println(replyCall.Error)
  31. fmt.Println(reply2)
  32. }