使用go实现一个简单的rpc

      • 什么是rpc, rpc是干什么的?
        • 几种协议的压测数据对比:
          • tcp
          • http
      • 使用tcp实现一个简单的rpc服务

什么是rpc, rpc是干什么的?

rpc的作用就是实现远程的服务调用
工作流程: 客户端携带服务信息(服务名,方法名)+数据 去请求服务端,服务端拿到数据,解析后执行对应的方法,将结果返回给客户端.

看上去与http的请求流程很类似. 的确,有的rpc框架就是使用的http作为通信协议.(grpc使用的http2;rpcx也支持http的通信协议)

要提高rpc架构的关键在于选用高性能的网络协议,很少有人会使用http1作为rpc的网络协议;因为http1的性能并不是很好,单线程的形式以及http1.0还会频繁的创建与断开tcp,这会拉低rpc这种频繁调用的框架的性能

几种协议的压测数据对比:
tcp

serve.go

package mainimport ("io""log""net"
)func handleConnection(conn net.Conn) {defer conn.Close()buffer := make([]byte, 4)// 读取客户端的消息for {_, err := conn.Read(buffer)if err != nil {if err != io.EOF {log.Println("Error reading from connection:", err)}return}go conn.Write([]byte("pong"))}
}func startServer(address string) {listener, err := net.Listen("tcp", address)if err != nil {log.Fatal("Error starting server:", err)}defer listener.Close()log.Printf("Server listening on %s...\n", address)// 等待并处理客户端连接for {conn, err := listener.Accept()if err != nil {log.Println("Error accepting connection:", err)continue}// 每个连接启动一个 goroutine 来处理go handleConnection(conn)}
}func main() {startServer(":8080")
}

client_test.go

	func pingServer(conn net.Conn) error {// 发送 ping 请求_, err := conn.Write([]byte("ping"))if err != nil {return fmt.Errorf("error sending ping: %v", err)}// 读取响应buffer := make([]byte, 4)_, err = conn.Read(buffer)if err != nil {return fmt.Errorf("error reading response: %v", err)}return nil}// BenchmarkPingServer 是基准测试函数,用于测试 TCP 客户端的性能func BenchmarkPingServer(b *testing.B) {conn, err := net.Dial("tcp", "localhost:8080")if err != nil {b.Fatalf("Failed to connect to server: %v", err)}defer conn.Close()// 设置并发数,使用 b.RunParallel 来模拟并发b.RunParallel(func(pb *testing.PB) {for pb.Next() {// 在每个 goroutine 中执行 ping 请求if err := pingServer(conn); err != nil {b.Errorf("Failed to ping server: %v", err)}}})//BenchmarkPingServer-8   	  191895	      6838 ns/op	       8 B/op	       2 allocs/op}func BenchmarkPingServer2(b *testing.B) {conn, err := net.Dial("tcp", "localhost:8080")if err != nil {b.Fatalf("Failed to connect to server: %v", err)}defer conn.Close()for i := 0; i < b.N; i++ {// 每个 goroutine 发送一次 ping 请求if err := pingServer(conn); err != nil {b.Errorf("Failed to ping server: %v", err)}}//BenchmarkPingServer2-8   	   49065	     20747 ns/op	       8 B/op	       2 allocs/op}
http

serve.go

// http_server.go
package mainimport ("log""net/http"
)// pingHandler 是处理 ping 请求的 HTTP 处理函数
func pingHandler(w http.ResponseWriter, r *http.Request) {// 如果请求是 ping,就返回 pongif r.Method == http.MethodGet && r.URL.Path == "/ping" {w.Write([]byte("pong"))return}// 如果请求路径不正确,返回 404http.NotFound(w, r)
}// 启动 HTTP 服务器
func startServer(address string) {http.HandleFunc("/ping", pingHandler)log.Printf("Server listening on %s...\n", address)err := http.ListenAndServe(address, nil)if err != nil {log.Fatal("Error starting server: ", err)}
}func main() {// 启动 HTTP 服务端startServer(":8081")
}

client_test.go

// pingServer 向服务器发送 ping 请求并接收响应
func pingServerHttp(address string) error {// 向服务器的 /ping 接口发送 GET 请求resp, err := http.Get(address + "/ping")if err != nil {return fmt.Errorf("error sending GET request: %v", err)}defer resp.Body.Close()// 确保返回的状态码是 200 OKif resp.StatusCode != http.StatusOK {return fmt.Errorf("unexpected status code: %d", resp.StatusCode)}return nil
}// BenchmarkPingServer 是基准测试函数,用于测试 HTTP 客户端的性能
func BenchmarkPingServerHttp(b *testing.B) {address := "http://localhost:8081"// 使用 b.RunParallel 来模拟并发请求b.RunParallel(func(pb *testing.PB) {for pb.Next() {// 每个 goroutine 发送一次 ping 请求if err := pingServerHttp(address); err != nil {b.Errorf("Failed to ping server: %v", err)}}})
}func BenchmarkPingServerHttp2(b *testing.B) {address := "http://localhost:8081"for i := 0; i < b.N; i++ {// 每个 goroutine 发送一次 ping 请求if err := pingServerHttp(address); err != nil b.Errorf("Failed to ping server: %v", err)}//BenchmarkPingServerHttp2-8   	    4976	    225154 ns/op	   16762 B/op	     122 allocs/op}
}

在单线程单连接 的情况下tcp有近5万的qps;单连接多线程的情况下有近20万的qps;而http在单线程情况下只有5000左右,并发的情况下有2万左右, 性能差距有10倍左右.

使用tcp实现一个简单的rpc服务

public.go

package public
//公共的方法与类
import ("bytes""encoding/binary"
)func Encode(data []byte) []byte {l := len(data)lBytes := IntToBytes(l)return append(lBytes, data...)
}func IntToBytes(n int) []byte {data := int64(n)bytebuf := bytes.NewBuffer([]byte{})binary.Write(bytebuf, binary.BigEndian, data)return bytebuf.Bytes()
}func BytesToInt(bys []byte) int {bytebuff := bytes.NewBuffer(bys)var data int64binary.Read(bytebuff, binary.BigEndian, &data)return int(data)
}type ReqData struct {ServerName stringTag        string //标记哪个线程调用的服务,返回的时候带上可以将数据传输到对应的县城Data       []byte
}
type RspData struct {Tag  string //标记哪个线程调用的服务,返回的时候带上可以将数据传输到对应的县城Data []byte
}type AddReq struct {NumA intNumB int
}type AddRsp struct {Sum int
}

server.go

package mainimport ("bufio""encoding/json""fmt""net""rpc_demo/public"
)type Server struct{}func (s *Server) Add(a *public.AddReq) *public.AddRsp {return &public.AddRsp{Sum: a.NumA + a.NumB}
}// 服务调用
// 服务名+方法名
// 封装对应的服务调用过程:根据方法名解析数据,并调用对应的方法
// 数据打包返回
// 这里做简化板手写处理:1. 没有实现自动化的服务方法注册;2. 我暂定使用uuid进行标识请求,以便于客户端可以将数据读取到对应的请求线程上,但事实上uuid过长,应该使用更为简单的标识方式
func serve(conn net.Conn) {defer conn.Close()reader := bufio.NewReader(conn)for {//解析长度lBytes := make([]byte, 8)_, err := reader.Read(lBytes[:])if err != nil {fmt.Printf("数据读取失败%v\n", err)return}l := public.BytesToInt(lBytes)reqBytes := make([]byte, l)_, err = reader.Read(reqBytes)if err != nil {fmt.Printf("数据读取失败%v\n", err)return}go func(reqData []byte) {req := new(public.ReqData)err = json.Unmarshal(reqData, req)if err != nil {fmt.Printf("json 解析失败%v\n", err)return}//解析处理(这里只注册了一个服务接口)switch req.ServerName {case "Server.Add":s := &Server{}data := new(public.AddReq)err := json.Unmarshal(req.Data, data)if err != nil {fmt.Printf("json 解析失败%v\n", err)return}rsp := s.Add(data)result, err := json.Marshal(rsp)if err != nil {fmt.Printf("数据编码失败%v\n", err)return}rspBytes, err := json.Marshal(&public.RspData{Tag: req.Tag, Data: result})if err != nil {fmt.Printf("数据编码失败%v\n", err)return}rspData := append(public.IntToBytes(len(rspBytes)), rspBytes...)conn.Write(rspData)default:conn.Write([]byte("该方法没有注册"))}}(reqBytes)}}func main() {listen, err := net.Listen("tcp", "127.0.0.1:9999")if err != nil {fmt.Println("Listen() failed, err: ", err)return}for {conn, err := listen.Accept() // 监听客户端的连接请求if err != nil {fmt.Println("Accept() failed, err: ", err)continue}go serve(conn) // 启动一个goroutine来处理客户端的连接请求}
}

client.go

package mainimport ("bufio""encoding/json""fmt""net""rpc_demo/public""time""github.com/google/uuid"
)type Client struct{ Conn net.Conn }func NewClient() *Client {conn, err := net.Dial("tcp", "127.0.0.1:9999")if err != nil {fmt.Println("err : ", err)return nil}return &Client{Conn: conn}
}// 每次调用都生成单独的uuid,并作为key,请求后select uuid对应的chan,直到有数据,读取数据,关闭通道,清除对应的map记录
var M map[string]chan ([]byte)// 启动客户端连接服务端并解析数据
func (c *Client) Run() {defer c.Conn.Close() // 关闭TCP连接reader := bufio.NewReader(c.Conn)for {lBytes := make([]byte, 8)_, err := reader.Read(lBytes[:])if err != nil {fmt.Printf("数据读取失败")return}l := public.BytesToInt(lBytes)reqBytes := make([]byte, l)_, err = reader.Read(reqBytes)if err != nil {fmt.Printf("数据读取失败")return}//解析数据体并写入对应的chango func(data []byte) {rspData := new(public.RspData)err := json.Unmarshal(data, rspData)if err != nil {fmt.Printf("数据解析失败")return}M[rspData.Tag] <- rspData.Data}(reqBytes)}
}// 我这边就不封装自动call了,直接手动call
func (c *Client) Call(serverAndfunc string, data []byte) []byte {//生成uuidtag := uuid.New().String()reqData := &public.ReqData{ServerName: serverAndfunc, Tag: tag, Data: data}r, err := json.Marshal(reqData)if err != nil {fmt.Println("编码错误")return nil}Ch := make(chan []byte)defer close(Ch)defer delete(M, tag)M[tag] = Chc.Conn.Write(append(public.IntToBytes(len(r)), r...))return <-Ch
}

压力测试

func BenchmarkJson(b *testing.B) {M = make(map[string]chan []byte)//建立tcp连接服务端client := NewClient()// 启动处理go client.Run()//模拟调用call方法req1 := &public.AddReq{NumA: 1,NumB: 2,}reqdata1, err := json.Marshal(req1)if err != nil {fmt.Println("编码错误")return}// 开始计时b.StartTimer()for i := 0; i < b.N; i++ {_ = client.Call("Server.Add", reqdata1)}// BenchmarkJson-8   	   29181	     39678 ns/op	    1065 B/op	      21 allocs/op
}

在单线程单连接的情况下有近3万的吞吐量,性能也还可以.因为这个做得很粗糙

总结: 网络通信才是rpc框架的瓶颈,json在本地编码解码可以有1000万的qps;proto的性能是json的4-5倍;但是rpcx的吞吐量是20万左右,grpc是15万左右,因为tcp的通信是20万,很http2是15万左右;http1.0是两万左右 这就是瓶颈;

这样我们可以联系到redis;redis可以有10万qps,为什么是10万?因为网络的瓶颈就是10多万
mysql为什么只有1万,因为mysql储存磁盘的,会更慢.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/62855.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】continue语句、goto语句

1、continue 语句 作用&#xff1a;在循环语句中&#xff0c;跳过本次循环中余下尚未执行的语句。继续下一次循环。 注意&#xff1a;continue只能用于循环中。 示例&#xff1a; 代码&#xff1a; //continue的用法 #include<iostream> using namespace std; int ma…

最长最短单词

最长最短单词 C语言实现C实现Java实现Python实现 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 输入1行句子&#xff08;不多于200个单词&#xff0c;每个单词长度不超过100&#xff09;&#xff0c;只包含字母、空格和逗号。单词由至少一…

ESP32项目 --- 智能门锁(WiFi 蓝牙 OTA)

1 项目简介 1.1 项目概述 本项目是实现一款智能门锁中的智能控制部分, 可以应用在家庭, 办公室等任何使用门锁的场所. 本项目实现了以下主要功能: &#xff08;1&#xff09;通过按键配置密码 &#xff08;2&#xff09;通过按键输入密码开锁 &#xff08;3&#xff09;录…

【Qt】QTableView选中行发生变化时触发的信号

问题 QTableView选中的行发生变化时&#xff0c;使用的信号是QTableView的selectionModel()里的currentChanged信号&#xff0c;界面点击行来回切换&#xff0c;发现怎么也触发不了&#xff1f; 原因 信号槽连接放在了QTableView数据初始化前面&#xff0c;这时候QTableView…

洛谷题单-入门2-分支结构-python-下

找出出现的最早的最大值 count 0 list_number [] while True:list_number.append(list(map(int, input().split())))count 1if count 7:breaklist2_number_total []for i1,i2 in list_number:list2_number_total.append(i1i2)target max(list2_number_total)index 0 if…

Java Collections 深度探索

在 Java 编程中&#xff0c;java.util.Collections是一个非常重要的工具类&#xff0c;它提供了一系列对集合进行操作的静态方法。本文将深入探讨 Java Collections 的功能、用法、优势以及在实际编程中的应用。 一、引言 Java Collections 框架为开发者提供了一套强大而灵活…

Taro小程序开发随记

处理taro小程序显示wangeditor内的a标签跳转 Taro.options.html.transformElement (el) > {if (el.props.class h5-a) {el.__handlers.tap [() > toWebView(el.props.href)]}return el } 处理wangeditor富文本内容中图片视频到小程序中展示问题 <view class&qu…

Pytorch使用手册-What is torch.nn really?(专题九)

我们建议将本教程作为 notebook 而不是脚本运行。要下载 notebook(.ipynb)文件,请点击页面顶部的链接。 PyTorch 提供了精心设计的模块和类,如 torch.nn、torch.optim、Dataset 和 DataLoader,帮助你创建和训练神经网络。为了充分利用这些工具的强大功能并根据你的问题进…

框架模块说明 #05 权限管理_03

背景 权限设计可以分为两个主要方面&#xff1a;操作权限和数据权限。前两篇文章已经详细介绍了操作权限的设计与实现&#xff0c;以及如何将其与菜单关联起来的具体方法。本篇将聚焦于数据权限&#xff0c;为您深入讲解相关的设计与实现方式。 全局开关 Value("${syst…

Linux网络编程之---多线程实现并发服务器

下面我们来使用tcp集合多线程实现并发服务器 一.服务端 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h>typedef struct sockinfo {char ip[16];unsigne…

二分查找!

问题描述 小明在图书馆借阅书籍&#xff0c;图书馆的书籍在系统中按序号顺次排列&#xff0c;小明在借阅后&#xff0c;需在系统中从“在馆书籍列表”中将该书删除。请帮助小明编写一个函数&#xff0c;在现有列表{1, 3, 5, 6, 7, 10, 12, 14, 26, 32, 35, 39, 42, 45, 54, 56…

按vue组件实例类型实现非侵入式国际化多语言翻译

#vue3##国际化##本地化##international# web界面国际化&#xff0c;I18N&#xff08;Internationalization&#xff0c;国际化&#xff09;&#xff0c;I11L(International&#xff0c;英特纳雄耐尔)&#xff0c;L10N&#xff08;Localization&#xff0c;本地化&#xff09;&…

Linux C/C++编程之静态库

【图书推荐】《Linux C与C一线开发实践&#xff08;第2版&#xff09;》_linux c与c一线开发实践pdf-CSDN博客《Linux C与C一线开发实践&#xff08;第2版&#xff09;&#xff08;Linux技术丛书&#xff09;》(朱文伟&#xff0c;李建英)【摘要 书评 试读】- 京东图书 (jd.com…

网际协议(IP)与其三大配套协议(ARP、ICMP、IGMP)

网际协议&#xff08;Internet Protocol&#xff0c;IP&#xff09;&#xff0c;又称互联网协议。是OSI中的网络层通信协议&#xff0c;用于跨网络边界分组交换。它的路由功能实现了互联互通&#xff0c;并从本质上建立了互联网。网际协议IP是 TCP/IP 体系中两个最主要的协议之…

flutter 多语言 国际化 flutter Intl的使用方法

一使用 flutter Intl Android studio需要添加插件 flutter Intl 路径 File>>Settings>>Plugins>>Marketplace>>flutter Intl>>Install 安装插件重新启动Android studio Android studio 创建一个flutter测试的新项目 在项目文件中配置 ** 添加…

uniapp实现加密Token并在每次请求前动态更新(vue、微信小程序、原生js也通用!)

导语&#xff1a;在Web开发中&#xff0c;Token作为一种身份验证的机制&#xff0c;被广泛应用于前后端交互过程中。本文将为大家介绍如何在每次请求前动态设置加密的Token&#xff0c;并在请求一次后使Token值加1&#xff08;或其他动态改变的逻辑&#xff09;&#xff0c;从而…

IDL学习笔记(二)IDL处理卫星数据

IDL处理卫星数据 HDF文件数据集属性通用属性 常用HDF4操作函数常用的HDF5操作函数读取HDF文件的一般步骤 HDF4文件读取-----数据信息查询HDF4文件读取示例-----目标数据TIFF输出 HDF文件 数据集属性 数据集名称&#xff0c;是“&#xff1a;”前的一部分&#xff0c;不是long_…

论文阅读——量子退火Experimental signature of programmable quantum annealing

摘要&#xff1a;量子退火是一种借助量子绝热演化解决复杂优化问题的通用策略。分析和数值证据均表明&#xff0c;在理想化的封闭系统条件下&#xff0c;量子退火可以胜过基于经典热化的算法&#xff08;例如模拟退火&#xff09;。当前设计的量子退火装置的退相干时间比绝热演…

TCP/IP协议簇自学笔记

摘抄于大学期间记录在QQ空间的一篇自学笔记&#xff0c;当前清理空间&#xff0c;本来想直接删除掉的&#xff0c;但是感觉有些舍不得&#xff0c;因此先搬移过来。 曾经&#xff0c;我只知道socket函数能进行网络间数据的通信&#xff0c;知道tcp/ip协议也是用来进行网络数据…

JAVA OPCUA 服务端开发,客户端连接会话监听和订阅事件监听

前言 关于使用milo开源库,开发opc ua服务器,有网友咨询如何设置服务端如何监听客户端的连接或断开事件,如何监听客户端发起订阅事件的代码实现,于是我完善了这部分的空缺整理整了这篇教程,希望能解决有同样需求,但是遇到困难的网友!因为milo没有官方文档的教程且网上详…