深入解析GeeRPC项目:构建高性能并发与异步客户端
本文是《7天用Go从零实现RPC框架GeeRPC》系列的第二篇,将重点介绍如何实现一个支持并发与异步调用的高性能RPC客户端。通过本文,你将了解RPC客户端核心设计理念和实现细节。
RPC调用的基本要求
在Go语言的net/rpc
包中,一个方法要能够被远程调用,必须满足五个条件:
- 方法所属类型必须是导出的(首字母大写)
- 方法本身必须是导出的
- 方法必须有两个参数,且都是导出类型或内置类型
- 方法的第二个参数必须是指针类型
- 方法必须返回error类型
用代码表示就是:
func (t *T) MethodName(argType T1, replyType *T2) error
Call结构体设计
为了承载RPC调用信息并支持异步操作,我们设计了Call
结构体:
type Call struct {
Seq uint64 // 调用序列号
ServiceMethod string // 服务名.方法名格式
Args interface{} // 方法参数
Reply interface{} // 方法返回值
Error error // 错误信息
Done chan *Call // 调用完成通知通道
}
func (call *Call) done() {
call.Done <- call
}
Call
结构体中的Done
字段是一个通道,用于在调用完成时通知调用方,这是实现异步调用的关键。
客户端核心实现
Client结构体
type Client struct {
cc codec.Codec // 消息编解码器
opt *Option // 配置选项
sending sync.Mutex // 发送锁
header codec.Header // 请求头
mu sync.Mutex // 客户端锁
seq uint64 // 请求序列号
pending map[uint64]*Call // 未完成的调用
closing bool // 用户主动关闭标志
shutdown bool // 服务端关闭标志
}
Client结构体包含了RPC客户端所需的所有组件:
- 编解码器:负责请求和响应的序列化与反序列化
- 同步机制:
sending
锁保证请求有序发送,mu
锁保护客户端状态 - 请求管理:
seq
生成唯一请求ID,pending
存储进行中的调用 - 状态控制:
closing
和shutdown
标志客户端状态
核心方法实现
调用管理
func (client *Client) registerCall(call *Call) (uint64, error) {
client.mu.Lock()
defer client.mu.Unlock()
if client.closing || client.shutdown {
return 0, ErrShutdown
}
call.Seq = client.seq
client.pending[call.Seq] = call
client.seq++
return call.Seq, nil
}
func (client *Client) removeCall(seq uint64) *Call {
client.mu.Lock()
defer client.mu.Unlock()
call := client.pending[seq]
delete(client.pending, seq)
return call
}
func (client *Client) terminateCalls(err error) {
client.sending.Lock()
defer client.sending.Unlock()
client.mu.Lock()
defer client.mu.Unlock()
client.shutdown = true
for _, call := range client.pending {
call.Error = err
call.done()
}
}
这三个方法管理调用的生命周期:
registerCall
:注册新调用并分配序列号removeCall
:移除已完成调用terminateCalls
:异常情况下终止所有调用
响应接收
func (client *Client) receive() {
var err error
for err == nil {
var h codec.Header
if err = client.cc.ReadHeader(&h); err != nil {
break
}
call := client.removeCall(h.Seq)
switch {
case call == nil:
// 调用已被移除
err = client.cc.ReadBody(nil)
case h.Error != "":
// 服务端返回错误
call.Error = fmt.Errorf(h.Error)
err = client.cc.ReadBody(nil)
call.done()
default:
// 正常响应
err = client.cc.ReadBody(call.Reply)
if err != nil {
call.Error = errors.New("reading body " + err.Error())
}
call.done()
}
}
// 发生错误时终止所有调用
client.terminateCalls(err)
}
receive
方法持续监听服务端响应,处理三种情况:
- 调用不存在(可能已超时或被取消)
- 服务端返回错误
- 正常响应
请求发送
func (client *Client) send(call *Call) {
client.sending.Lock()
defer client.sending.Unlock()
seq, err := client.registerCall(call)
if err != nil {
call.Error = err
call.done()
return
}
client.header.ServiceMethod = call.ServiceMethod
client.header.Seq = seq
client.header.Error = ""
if err := client.cc.Write(&client.header, call.Args); err != nil {
call := client.removeCall(seq)
if call != nil {
call.Error = err
call.done()
}
}
}
send
方法负责发送请求,包括:
- 注册调用
- 准备请求头
- 序列化并发送请求
用户接口
func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call {
if done == nil {
done = make(chan *Call, 10)
} else if cap(done) == 0 {
log.Panic("rpc client: done channel is unbuffered")
}
call := &Call{
ServiceMethod: serviceMethod,
Args: args,
Reply: reply,
Done: done,
}
client.send(call)
return call
}
func (client *Client) Call(serviceMethod string, args, reply interface{}) error {
call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
return call.Error
}
提供两种调用方式:
Go
:异步调用,立即返回Call对象Call
:同步调用,阻塞等待结果
客户端创建流程
func NewClient(conn net.Conn, opt *Option) (*Client, error) {
f := codec.NewCodecFuncMap[opt.CodecType]
if f == nil {
return nil, fmt.Errorf("invalid codec type %s", opt.CodecType)
}
if err := json.NewEncoder(conn).Encode(opt); err != nil {
_ = conn.Close()
return nil, err
}
return newClientCodec(f(conn), opt), nil
}
func Dial(network, address string, opts ...*Option) (client *Client, err error) {
opt, err := parseOptions(opts...)
if err != nil {
return nil, err
}
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
defer func() {
if client == nil {
_ = conn.Close()
}
}()
return NewClient(conn, opt)
}
客户端创建分为两步:
Dial
:建立网络连接NewClient
:协商编解码方式并初始化客户端
示例演示
func main() {
addr := make(chan string)
go startServer(addr)
client, _ := geerpc.Dial("tcp", <-addr)
defer func() { _ = client.Close() }()
time.Sleep(time.Second)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
args := fmt.Sprintf("geerpc req %d", i)
var reply string
if err := client.Call("Foo.Sum", args, &reply); err != nil {
log.Fatal("call Foo.Sum error:", err)
}
log.Println("reply:", reply)
}(i)
}
wg.Wait()
}
这个示例展示了如何并发调用RPC服务,5个goroutine同时发起请求,服务端和客户端都能正确处理并发。
总结
本文详细介绍了GeeRPC客户端的设计与实现,重点包括:
- 异步调用机制通过Call结构体和Done通道实现
- 并发控制通过互斥锁和请求序列号管理
- 完整的调用生命周期管理
- 优雅的错误处理和资源清理
通过这种设计,GeeRPC客户端能够高效地处理大量并发请求,同时提供简单易用的同步和异步调用接口。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考