这里我们来实现这个RPC的client端
为了实现RPC的效果,我们调用的Hello方法,即server端的方法,应该是由代理来调用,让proxy里面封装网络请求,消息的发送和接受处理。而上一篇文章提到的服务端的代理已经在.rpc.go文件中实现,我们将客户端的实现也写在这里
ClientProxy
// 客户端代理接口
type HelloClientProxy interface {
Hello(ctx context.Context, in *HelloRequest, opts ...client.Option) (*HelloReply, error)
}
// 客户端代理实现
type HelloClientProxyImpl struct {
client client.Client
opts []client.Option
}
// 创建客户端代理
func NewHelloClientProxy(opts ...client.Option) HelloClientProxy {
return &HelloClientProxyImpl{
client: client.DefaultClient,
opts: opts,
}
}
- 这里的HelloClientProxyImpl其中的client类主要是负责invoke方法,抽象网络IO和编解码,opts主要是记录客户端启动时传入的配置项,如server的ip地址等
- 创建出客户端代理,我们就可以通过代理来调用Hello方法
// 实现Hello方法
func (c *HelloClientProxyImpl) Hello(ctx context.Context, req *HelloRequest, opts ...client.Option) (*HelloReply, error) {
// 创建一个msg结构,存储service相关的数据,如serviceName等,并放到context中
// 用msg结构可以避免在context中太多withValue传递过多的参数
msg := internel.NewMsg()
msg.WithServiceName("helloworld")
msg.WithMethodName("Hello")
ctx = context.WithValue(ctx, internel.ContextMsgKey, msg)
rsp := &HelloReply{}
// 这里需要将opts添加前面newProxy时传入的opts
newOpts := append(c.opts, opts...)
err := c.client.Invoke(ctx, req, rsp, newOpts...)
if err != nil {
return nil, err
}
return rsp, nil
}
- 这里需要明确service的名字和对应方法,为了后续封装在协议数据里,到达server端才能正确路由。当代理类实现了这个Hello后,我们就可以通过proxy.Hello得到相应结果,Invoke方法隐藏了具体的网络处理,我们跟进Invoke方法
Client(clientTransPort)
上文提到,client类主要处理invoke方法,我们可以预见它的职责就是,
- 序列化请求体
- 编码
- 发送请求,接受响应
- 解码
- 反序列化响应体
- 返回客户端
为了代码的解耦,我们和server的处理一样,将以上操作放到clientTransPort上,client持有transPort,让transPort处理以上的逻辑
// 实现Send方法
func (c *clientTransport) Send(ctx context.Context, reqBody interface{}, rspBody interface{}, opt *ClientTransportOption) error {
// 获取连接
// TODO 这里的连接后续可以优化从连接池获取
conn, err := net.Dial("tcp", opt.Address)
if err != nil {
return err
}
defer conn.Close()
// reqbody序列化
reqData, err := codec.Marshal(reqBody)
if err != nil {
return err
}
// reqbody编码,返回请求帧
framedata, err := opt.Codec.Encode(ctx, reqData)
if err != nil {
return err
}
// 写数据到连接中
err = c.tcpWriteFrame(ctx, conn, framedata)
if err != nil {
return err
}
// 读取tcp帧
rspDataBuf, err := c.tcpReadFrame(ctx, conn)
if err != nil {
return err
}
// 获取msg
ctx, msg := internel.GetMessage(ctx)
// rspDataBuf解码,提取响应体数据
rspData, err := opt.Codec.Decode(msg, rspDataBuf)
if err != nil {
return err
}
// 将rspData反序列化为rspBody
err = codec.Unmarshal(rspData, rspBody)
if err != nil {
return err
}
return nil
}
-
序列化是根据protobuf协议,编码的格式我们之间写Server的时候提到,我们需要将数据编码成以下格式:
-
当编码完成后,我们就需要写数据到连接中,并监听该连接的数据,当有数据后,我们再依次解码得到响应体,再将响应体反序列化,返回客户端。
-
写数据到连接和读连接中的数据也很简单,这里我们直接开启一个连接,调用Write写,而codec.ReadFrame在server端的时候已经介绍过
func (c *clientTransport) tcpWriteFrame(ctx context.Context, conn net.Conn, frame []byte) error {
// 写入tcp
_, err := conn.Write(frame)
if err != nil {
return fmt.Errorf("write frame error: %v", err)
}
return nil
}
func (c *clientTransport) tcpReadFrame(ctx context.Context, conn net.Conn) ([]byte, error) {
return codec.ReadFrame(conn)
}
效果测试
至此client端处理完毕,我们来看看效果:
//client端的测试main.go:
func main() {
c := pb.NewHelloClientProxy(client.WithTarget("127.0.0.1:8000"))
if c == nil {
fmt.Println("Failed to create client")
return
}
rsp, err := c.Hello(context.Background(), &pb.HelloRequest{Msg: "world"})
if err != nil {
fmt.Println("RPC call error:", err)
return
}
fmt.Println("Response:", rsp.Msg)
}
// server端的测试的main.go
func main() {
// Create a new server instance
s := server.NewServer()
// Register the HelloService with the server
pb.RegisterHelloServer(s, &HelloServerImpl{})
// Start the server on port 50051
if err := s.Serve(":8000"); err != nil {
panic(err)
}
fmt.Print("启动成功")
}
// 创建一个HelloServer的实现类
type HelloServerImpl struct{}
// 实现HelloServer接口的Hello方法
func (h *HelloServerImpl) Hello(req *pb.HelloRequest) (*pb.HelloReply, error) {
// 这里可以实现具体的业务逻辑
reply := &pb.HelloReply{
Msg: "Hello " + req.Msg,
}
return reply, nil
}
server端启动:
server端接收到client的连接请求:
client收到响应:
现在version1开发完了,目前的版本主要是实现基础功能,并且为了考虑后续的扩展性做了比较多的解耦,在后面的版本,我们可以逐渐提升这个rpc的性能和融入更多的功能