深入解析GeeRPC项目:构建高性能并发与异步客户端

深入解析GeeRPC项目:构建高性能并发与异步客户端

7days-golang 7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列 7days-golang 项目地址: https://gitcode.com/gh_mirrors/7d/7days-golang

本文是《7天用Go从零实现RPC框架GeeRPC》系列的第二篇,将重点介绍如何实现一个支持并发与异步调用的高性能RPC客户端。通过本文,你将了解RPC客户端核心设计理念和实现细节。

RPC调用的基本要求

在Go语言的net/rpc包中,一个方法要能够被远程调用,必须满足五个条件:

  1. 方法所属类型必须是导出的(首字母大写)
  2. 方法本身必须是导出的
  3. 方法必须有两个参数,且都是导出类型或内置类型
  4. 方法的第二个参数必须是指针类型
  5. 方法必须返回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客户端所需的所有组件:

  1. 编解码器:负责请求和响应的序列化与反序列化
  2. 同步机制sending锁保证请求有序发送,mu锁保护客户端状态
  3. 请求管理seq生成唯一请求ID,pending存储进行中的调用
  4. 状态控制closingshutdown标志客户端状态

核心方法实现

调用管理
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()
    }
}

这三个方法管理调用的生命周期:

  1. registerCall:注册新调用并分配序列号
  2. removeCall:移除已完成调用
  3. 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方法持续监听服务端响应,处理三种情况:

  1. 调用不存在(可能已超时或被取消)
  2. 服务端返回错误
  3. 正常响应
请求发送
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方法负责发送请求,包括:

  1. 注册调用
  2. 准备请求头
  3. 序列化并发送请求
用户接口
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
}

提供两种调用方式:

  1. Go:异步调用,立即返回Call对象
  2. 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)
}

客户端创建分为两步:

  1. Dial:建立网络连接
  2. 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客户端的设计与实现,重点包括:

  1. 异步调用机制通过Call结构体和Done通道实现
  2. 并发控制通过互斥锁和请求序列号管理
  3. 完整的调用生命周期管理
  4. 优雅的错误处理和资源清理

通过这种设计,GeeRPC客户端能够高效地处理大量并发请求,同时提供简单易用的同步和异步调用接口。

7days-golang 7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列 7days-golang 项目地址: https://gitcode.com/gh_mirrors/7d/7days-golang

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

任凝俭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值