热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

程序员如何设计一个rpc框架

本文主要介绍关于rpc,网络协议,golang的知识点,对2、RPC从零到进阶和程序员如何设计一个rpc框架有兴趣的朋友可以看下由【无休止符】投稿的技术文章,希望该技术和经验能帮到你解决你所遇的相关技

本文主要介绍关于rpc,网络协议,golang的知识点,对2、RPC从零到进阶和程序员如何设计一个rpc框架有兴趣的朋友可以看下由【无休止符】投稿的技术文章,希望该技术和经验能帮到你解决你所遇的相关技术问题。

程序员如何设计一个rpc框架

目录 一、什么是RPC二、http实现server和client的add1 - http的server端2 - http的client端 三、rpc开发四大要素四、go的rpc实现1 - 快速体验rpc2 - rpc实现json序列化协议3 - rpc实现http传输协议 五、rpc调用改造(grpc模拟)1 - serviceName统一和名称冲突的问题2 - 屏蔽HelloServiceName和Hello函数名称

一、什么是RPC

RPC概念:RPC(Remote Procedure Call)远程过程调用,简单的理解就是一个节点请求另一个节点提供的服务

本地过程调用:对应RPC的是本地过程调用,函数调用是最常见的本地过程调用

程序员如何设计一个rpc框架

可能产生的问题:将本地过程调用变成远程过程调用会面临各种问题

①.Call的id映射②. 序列化和反序列化③. 网络传输

程序员如何设计一个rpc框架

Call的id映射:我们怎么告诉远程机器我们要调用add,而不是sub或者Foo呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用add,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

序列化和反序列化:客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

网络传输:远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2(可以保持常连接,而http一旦返回后连接就断开了,http有性能问题)。Java的Netty也属于这层的东西。


二、http实现server和client的add 1 - http的server端
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
)

func main() {
   
	// http://127.0.0.1:8000/add?a=1&b=2
	// 返回的格式化: json {"data":3}
	// 1. callID的问题: r.URL.Path
	// 2. 数据传输协议 url 的参数传递协议
	// 3. 网络传输协议http
	http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
   
		_ = r.ParseForm() //解析参数
		fmt.Println("path: ", r.URL.Path)
		a, _ := strconv.Atoi(r.Form["a"][0])
		b, _ := strconv.Atoi(r.Form["b"][0])
		w.Header().Set("Content-Type", "application/json")
		jData, _ := json.Marshal(map[string]int{
   
			"data": a + b,
		})

		_, _ = w.Write(jData)
	})
	_ = http.ListenAndServe(":8000", nil)
}

2 - http的client端
package main

import (
	"encoding/json"
	"fmt"
	"github.com/kirinlabs/HttpRequest"
)

type ResponseData struct {
   
	Data int `json:"data"`
}

func Add(a, b int) int {
   
	req := HttpRequest.NewRequest()
	res, _ := req.Get(fmt.Sprintf("http://127.0.0.1:8000/%s?a=%d&b=%d", "add", a, b))
	body, _ := res.Body()
	//fmt.Println(string(body))
	rspData := ResponseData{
   }
	_ = json.Unmarshal(body, &rspData)
	return rspData.Data
}

func main() {
   
	fmt.Println(Add(3, 2))
}


三、rpc开发四大要素

rpc技术在架构设计上有四分部组成:客户端、客户端存根、服务端、服务端存根

客户端(Client):服务调用发起方,也称为服务消费者 客户端存根(Client Stub):该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端 服务端(Server):远端的计算机机器上运行的程序,其中有客户端要调用的方法 服务端存根(Server Stub):接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序

RPC调用过程

①.客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;②.客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。③.客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。④.服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。⑤.服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。⑥.服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。⑦.服务端Stub程序将程序调用结果按照约定的协议进行序列化,并通过网络发送回客户端Stub程序。⑧.客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作,并将调用返回的数据传递给客户端请求发起者。⑨.客户端请求发起者得到调用结果,整个RPC调用过程结束。

程序员如何设计一个rpc框架

动态代理技术:上面看到的Client Stub和Server Stub程序,在具体的编码和开发实践过程中,都是使用动态代理技术自动生成的一段程序


四、go的rpc实现 1 - 快速体验rpc server端
package main

import (
	"net"
	"net/rpc"
)

type HelloService struct{
   }

func (s *HelloService) Hello(request string, reply *string) error {
   
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}

func main() {
   
	//1. 实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2. 注册处理逻辑 handler
	_ = rpc.RegisterName("HelloService", &HelloService{
   })
	//3. 启动服务
	conn, _ := listener.Accept() //当一个新的连接进来的时候,
	rpc.ServeConn(conn)

	//一连串的代码大部分都是net的包好像和rpc没有关系
	//不行。rpc调用中有几个问题需要解决 1. call id 2. 序列化和反序列化 编码和解码
	//可以跨语言调用呢 1. go语言的rpc的序列化协议是什么(Gob) 2. 能否替换成常见的序列化
}

client
package main

import (
	"fmt"
	"net/rpc"
)

func main() {
   
	//1. 建立连接
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
   
		return
	}
	var reply string //string有默认值
	err = client.Call("HelloService.Hello", "bobby", &reply)
	if err != nil {
   
		return
	}
	fmt.Println(reply)
}

2 - rpc实现json序列化协议 server:只要发送json数据给server,server都可以解析
package main

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

type HelloService struct{
   }

func (s *HelloService) Hello(request string, reply *string) error {
   
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}

func main() {
   
	//1. 实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2. 注册处理逻辑 handler
	_ = rpc.RegisterName("HelloService", &HelloService{
   })
	//3. 启动服务
	for {
   
		conn, _ := listener.Accept() //当一个新的连接进来的时候,
		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

client
package main

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

func main() {
   
	//1. 建立连接
	conn, err := net.Dial("tcp", "localhost:1234")
	if err != nil {
   
		return
	}
	var reply string //string有默认值
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	err = client.Call("HelloService.Hello", "bobby", &reply)
	if err != nil {
   
		return
	}
	fmt.Println(reply)
}

3 - rpc实现http传输协议 server
package main

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

type HelloService struct{
   }

func (s *HelloService) Hello(request string, reply *string) error {
   
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}

func main() {
   
	//1. 实例化一个server
	_ = rpc.RegisterName("HelloService", &HelloService{
   })
	http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
   
		var conn io.ReadWriteCloser = struct {
   
			io.Writer
			io.ReadCloser
		}{
   
			ReadCloser: r.Body,
			Writer:     w,
		}
		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
	})
	http.ListenAndServe(":1234", nil)
}


五、rpc调用改造(grpc模拟) 1 - serviceName统一和名称冲突的问题 问题分析 server端和client端如何统一serviceName多个server的包中serviceName同名的问题 ** 解耦方案**:新建handler/handler.go文件内容如下
package handler

// 名称冲突的问题
const HelloServiceName = "handler/HelloService"
server_ = rpc.RegisterName(handler.HelloServiceName, &HelloService{}) clienterr = client.Call(handler.HelloServiceName+".Hello", "imooc", &reply) 2 - 屏蔽HelloServiceName和Hello函数名称 项目结构

程序员如何设计一个rpc框架

handle.go
package handler

// 名称冲突的问题
const HelloServiceName = "handler/HelloService"

// 我们关心的是NewHelloService这个名字呢 还是这个结构体中的方法
type NewHelloService struct{
   }

func (s *NewHelloService) Hello(request string, reply *string) error {
   
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}

server代理:server_proxy.go
package server_proxy

import (
	"net/rpc"
	
	"test_project/handler"
)

type HelloServicer interface {
   
	Hello(request string, reply *string) error
}

//如果做到解耦 - 我们关心的是函数 鸭子类型
func RegisterHelloService(srv HelloServicer) error {
   
	return rpc.RegisterName(handler.HelloServiceName, srv)
}

server.go
package main

import (
	"net"
	"net/rpc"

	"test_project/handler"
	"test_project/server_proxy"
)

func main() {
   
	//1. 实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2. 注册处理逻辑 handler
	_ = server_proxy.RegisterHelloService(&handler.NewHelloService{
   })
	//3. 启动服务
	for {
   
		conn, _ := listener.Accept() //当一个新的连接进来的时候,
		go rpc.ServeConn(conn)
	}
}

client代理:client_proxy.go
package client_proxy

import (
	"net/rpc"
	"test_project/handler"
)

type HelloServiceStub struct {
   
	*rpc.Client
}

//在go语言中没有类、对象 就意味着没有初始化方法
func NewHelloServiceClient(protocol, address string) HelloServiceStub {
   
	conn, err := rpc.Dial(protocol, address)
	if err != nil {
   
		return HelloServiceStub{
   }
	}
	return HelloServiceStub{
   conn}
}

func (c *HelloServiceStub) Hello(request string, reply *string) error {
   
	err := c.Call(handler.HelloServiceName+".Hello", request, reply)
	if err != nil {
   
		return err
	}
	return nil
}

client.go
package main

import (
	"fmt"
	"test_project/client_proxy"
)

func main() {
   
	//1. 建立连接
	client := client_proxy.NewHelloServiceClient("tcp", "localhost:1234")
	//1. 只想写业务逻辑 不想关注每个函数的名称
	// 客户端部分
	var reply string //string有默认值
	err := client.Hello("bobby", &reply)
	if err != nil {
   
		return
	}
	fmt.Println(reply)

	//1. 这些概念在grpc中都有对应
	//2. 发自灵魂的拷问: server_proxy 和 client_proxy能否自动生成啊 为多种语言生成
	//3. 都能满足 这个就是protobuf + grpc
}

本文《2、RPC从零到进阶》版权归无休止符所有,引用2、RPC从零到进阶需遵循CC 4.0 BY-SA版权协议。


推荐阅读
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 本文介绍了NetCore WebAPI开发的探索过程,包括新建项目、运行接口获取数据、跨平台部署等。同时还提供了客户端访问代码示例,包括Post函数、服务器post地址、api参数等。详细讲解了部署模式选择、框架依赖和独立部署的区别,以及在Windows和Linux平台上的部署方法。 ... [详细]
  • 精讲代理设计模式
    代理设计模式为其他对象提供一种代理以控制对这个对象的访问。代理模式实现原理代理模式主要包含三个角色,即抽象主题角色(Subject)、委托类角色(被代理角色ÿ ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 面向对象之3:封装的总结及实现方法
    本文总结了面向对象中封装的概念和好处,以及在Java中如何实现封装。封装是将过程和数据用一个外壳隐藏起来,只能通过提供的接口进行访问。适当的封装可以提高程序的理解性和维护性,增强程序的安全性。在Java中,封装可以通过将属性私有化并使用权限修饰符来实现,同时可以通过方法来访问属性并加入限制条件。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 解决Sharepoint 2013运行状况分析出现的“一个或多个服务器未响应”问题的方法
    本文介绍了解决Sharepoint 2013运行状况分析中出现的“一个或多个服务器未响应”问题的方法。对于有高要求的客户来说,系统检测问题的存在是不可接受的。文章详细描述了解决该问题的步骤,包括删除服务器、处理分布式缓存留下的记录以及使用代码等方法。同时还提供了相关关键词和错误提示信息,以帮助读者更好地理解和解决该问题。 ... [详细]
  • 本文整理了Java中java.lang.NoSuchMethodError.getMessage()方法的一些代码示例,展示了NoSuchMethodErr ... [详细]
author-avatar
幸福的肖巍
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有