🐉大家好,我是gopher_looklook,现任某独角兽企业Go语言工程师,喜欢钻研Go源码,发掘各项技术在大型Go微服务项目中的最佳实践,期待与各位小伙伴多多交流,共同进步!
前言
最近在读Go语言的net/rpc源码,感觉源码涉及到了太多细节和错误处理,层层嵌套的函数调用关系错综复杂,难以窥见RPC框架的本质。
其实对于一个RPC (R emote P rocedure Call 远程过程调用) 框架,我认为最核心的功能有2点:
- 客户端与服务端通信。
- 服务端注册服务,并处理客户端请求。
举个例子,这段代码定义了一个简单的SortServer 服务,并提供了一个简单的排序功能Sort 。当我们希望其他程序可以跨服务调用我们写的Sort方法时,需要实现的最核心的功能就是实现不同应用程序间的通信,并且将服务端的SortServer服务注册到一个地方,以随时响应客户端调用服务方法获取执行结果的请求。
这个系列将分成两篇文章。
第一篇实现服务端与客户端通信。服务端与客户端协商,约定使用固定的JSON数据格式进行通信。客户端将消息发送给服务端,服务端简单将得到的消息简单组装,返回给客户端。
第二篇在服务端实现服务注册功能 ,并修改解析客户端请求 的函数逻辑,将客户端发送过来的消息转化为具体的服务调用请求 ,在注册的服务列表中找到对应的服务,利用反射实现服务功能调用,将执行结果返回给客户端。
话不多说,下面让我们开始动手搭建RPC框架吧!
服务端客户端通信
为了方便演示效果,新创建一个Go项目gopher-rpc。
消息格式约定
- 服务端与客户端消息格式
go
type Request struct {
Method string `json:"method"`
Args interface{} `json:"args"`
}
Method 表示要调用的服务名方法,比如"SortServer.Sort "表示想要调用的是SortServer服务的Sort方法 ;"ArithServer.Add "则表示想要调用的是ArithServer服务的Add方法 。由于不知道具体请求参数的个数和类型,因此Args暂时用空切片interface{}类型表示即可。
- 消息结束分隔符
约定好以\n
作为消息结束的标志。
项目结构
其中server.go 和client.go用于编写RPC框架的代码,在main文件夹下建一个basic包,用于演示基本的服务端与客户端的通信功能。
框架源码1.0
- server.go
go
package gopher_rpc
import (
"bufio"
"encoding/json"
"fmt"
"io"
"strings"
)
type Server struct{}
func NewServer() *Server {
return &Server{}
}
var DefaultServer = NewServer()
type Request struct {
Method string `json:"method"`
Args interface{} `json:"args"`
}
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
message, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
fmt.Printf("读取数据时出错: %v\n", err)
return
}
var request Request
err = json.Unmarshal([]byte(message), &request)
if err != nil {
fmt.Printf("反序列化 JSON 数据时出错: %v\n", err)
return
}
serviceMethodName := request.Method
dot := strings.LastIndexByte(serviceMethodName, '.')
if dot < 0 {
fmt.Printf("无效的方法名: %s\n", serviceMethodName)
}
serviceName := serviceMethodName[:dot]
methodName := serviceMethodName[dot+1:]
// 约定好以\n作为消息结尾
response := fmt.Sprintf("serviceName:%s, methodName:%s, request.Arg:%#v \n ", serviceName, methodName, request.Args)
_, err = conn.Write([]byte(response))
if err != nil {
fmt.Printf("发送响应时出错: %v\n", err)
return
}
}
}
func ServerConn(conn io.ReadWriteCloser) {
DefaultServer.ServeConn(conn)
}
在server.go中,首先定义了一个Server结构体 ,用来承载服务端提供的功能。接着定义了一个默认Server---------DefaultServer 。Server指针实现了一个处理连接请求conn的函数ServerConn 。在ServerConn函数中,利用bufio包提供的相关功能,从conn中读取到客户端发送过来的请求消息message ,接着使用JSON反序列化成约定好的消息体格式。解析出客户端消息中请求的Method 和Args 。之后把消息中的服务名、方法名、参数重新组装成一条消息响应response 。利用conn.Write函数向网络连接中写入请求,等待客户端接收。同时提供了一个全局的ServerConn函数 ,调用DefaultServer.ServeConn方法,供服务端使用。
- client.go
go
package gopher_rpc
import (
"bufio"
"fmt"
"net"
)
type Client struct {
conn net.Conn
reader *bufio.Reader
done chan struct{}
errChan chan error
Response string
}
// Dail 用于建立与服务器的连接
func Dial(network, address string) (*Client, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, fmt.Errorf("无法连接到服务器 %s: %v", address, err)
}
client := &Client{
conn: conn,
reader: bufio.NewReader(conn),
done: make(chan struct{}), // 无缓冲channel
errChan: make(chan error, 1),
}
// 开一个协程接受服务端返回
go client.receive()
return client, nil
}
func (c *Client) receive() {
defer close(c.done)
defer close(c.errChan)
response, err := c.reader.ReadString('\n')
if err != nil {
c.errChan <- fmt.Errorf("接收服务器响应时出错:%v", err.Error())
return
}
c.Response = response
c.done <- struct{}{}
}
// Call 方法用于向服务端发送消息并等待响应(创建Client时开了个协程去接受服务端响应)
func (c *Client) Call(serviceMethod string) error {
doneChan := c.Go(serviceMethod)
select {
case <-doneChan:
return nil
case err := <-c.errChan:
return err
}
}
func (c *Client) Go(serviceMethod string) chan struct{} {
_, err := c.conn.Write([]byte(serviceMethod + "\n"))
if err != nil {
c.errChan <- fmt.Errorf("发送消息时出错: %v", err)
close(c.done)
close(c.errChan)
return nil
}
return c.done
}
// Close 方法用于关闭客户端连接
func (c *Client) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}
在client.go中,首先定义了一个Client结构体 ,封装了一些跟网络请求与处理相关的字段。接着提供了一个对外暴露的Dial方法。用于连接到服务端。在Dial 方法返回Client之前,还开了一个Go协程 ,用于监听和接收服务端发送过来的消息;若成功读取到消息,会往无缓冲channel 字段done 中发送一个空结构体,这样做的目的是为了当receive协程 接收到服务端消息时,可以利用无缓冲通道done 通知给主程序。Client结构体指针还提供了一个Call方法 ,Call方法调用了Go方法 ,用于往conn中写入消息发送给服务端,并返回无缓冲通道done。Client.Call方法调用完Go方法后,使用select 监听两个通道client.done 和client.errChan 。若发生错误,则返回相应的error。如果没有错误,则监听client.done 通道。若之前的receive协程成功读取到了服务端响应。<-doneChan 将不再阻塞,Call函数正常退出,服务端返回的响应数据存储在Client结构体的Response字段当中。
实战服务端客户端通信
- 服务端监听请求
server_main.go
go
package main
import (
"fmt"
"gopher_rpc"
"net"
)
func main() {
listener, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Printf("监听端口时出错:%v\n", err)
return
}
defer listener.Close()
fmt.Println("Server is listening on port 8888....")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("接收连接时出错:%v\n", err)
continue
}
// 开一个go协程,异步处理连接请求
go gopher_rpc.ServerConn(conn)
}
}
- 客户端发送数据并打印响应结果
client_main.go
go
package main
import (
"encoding/json"
"fmt"
"gopher_rpc"
)
type ServiceMethod struct {
Method string `json:"method"`
Args Args `json:"args"`
}
type Args struct {
Num1 int64 `json:"num_1"`
Num2 int64 `json:"num_2"`
}
func main() {
// 连接到服务端
client, err := gopher_rpc.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer client.Close()
param := &ServiceMethod{
Method: "ArithServer.Add", // 可以表示想调用ArithServer服务的Add方法
Args: Args{
Num1: 10,
Num2: 20,
}, // 可以表示两个参数,一个是10,一个是20
}
bs, _ := json.Marshal(param)
if err = client.Call(string(bs)); err != nil {
fmt.Println(err)
return
}
fmt.Printf("客户端接收的数据: %s", client.Response)
}
程序运行
- 服务端
- 客户端
从运行结果可以看到,服务端正常解析出了客户端请求的服务名、方法名和参数,并重新组装消息,简单地返回给了客户端。说明我们实现了服务端与客户端的通信。
未完待续
在下一篇文章中,我们将修改server.go ,实现服务注册 并处理客户端请求的功能。
下一篇传送门: 从零到一: 用Go语言搭建简易RPC框架并实践 (二)