使用go实现一个简单的rpc

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

rpc的作用就是实现远程的服务调用

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

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

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

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

serve.go

go 复制代码
package main

import (
	"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

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

go 复制代码
// http_server.go
package main

import (
	"log"
	"net/http"
)

// pingHandler 是处理 ping 请求的 HTTP 处理函数
func pingHandler(w http.ResponseWriter, r *http.Request) {
	// 如果请求是 ping,就返回 pong
	if r.Method == http.MethodGet && r.URL.Path == "/ping" {
		w.Write([]byte("pong"))
		return
	}
	// 如果请求路径不正确,返回 404
	http.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

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 OK
	if 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

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 int64
	binary.Read(bytebuff, binary.BigEndian, &data)
	return int(data)
}

type ReqData struct {
	ServerName string
	Tag        string //标记哪个线程调用的服务,返回的时候带上可以将数据传输到对应的县城
	Data       []byte
}
type RspData struct {
	Tag  string //标记哪个线程调用的服务,返回的时候带上可以将数据传输到对应的县城
	Data []byte
}

type AddReq struct {
	NumA int
	NumB int
}

type AddRsp struct {
	Sum int
}

server.go

go 复制代码
package main

import (
	"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

go 复制代码
package main

import (
	"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
		}

		//解析数据体并写入对应的chan
		go 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 {
	//生成uuid
	tag := 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] = Ch
	c.Conn.Write(append(public.IntToBytes(len(r)), r...))
	return <-Ch
}

压力测试

go 复制代码
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储存磁盘的,会更慢.

相关推荐
凡人的AI工具箱22 分钟前
40分钟学 Go 语言高并发:RPC服务开发实战
开发语言·后端·性能优化·rpc·golang
ThisIsClark1 小时前
【后端面试总结】golang channel深入理解
面试·职场和发展·golang
9毫米的幻想1 小时前
【C++】—— set 与 multiset
开发语言·c++·rpc
eddieHoo12 小时前
HTTP、RPC
网络协议·http·rpc
明明跟你说过12 小时前
Go 语言函数编程指南:定义、调用技巧与返回值机制
开发语言·后端·golang·go·go1.19
灼华十一13 小时前
消息队列-kafka
linux·docker·golang·kafka
NPE~1 天前
Go实现List、Set、Stack、Deque等数据结构
数据结构·golang·list·set·deque·stack
望天hous1 天前
Go-MediatR:Go语言中的中介者模式
开发语言·golang·中介者模式
抹香鲸之海1 天前
go+gin+mysql+gorm快速实现增删改查接口
mysql·golang·gin