blog icon indicating copy to clipboard operation
blog copied to clipboard

动手写RPC框架 - GeeRPC第一天 服务端与消息编码 | 极客兔兔

Open geektutu opened this issue 5 years ago • 73 comments
trafficstars

https://geektutu.com/post/geerpc-day1.html

7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第一天实现了一个简单的服务端和消息的编码与解码。

geektutu avatar Oct 08 '20 02:10 geektutu

非常感谢这个教程

gaodansoft avatar Oct 23 '20 01:10 gaodansoft

@gaodansoft 笔芯~

geektutu avatar Oct 24 '20 06:10 geektutu

说点什么,那当然是赞了😏

JYFiaueng avatar Nov 04 '20 05:11 JYFiaueng

@JYFiaueng 感谢认可~ 😯

geektutu avatar Nov 04 '20 12:11 geektutu

请问下面这一行是什么意思呢?

var _ Codec = (*GobCodec)(nil)

w4096 avatar Dec 25 '20 08:12 w4096

请问下面这一行是什么意思呢?

var _ Codec = (*GobCodec)(nil)

@wy-ei 参考 7days-golang 有价值的问题讨论汇总贴

geektutu avatar Dec 25 '20 15:12 geektutu

写的太好了

wangwenjunfromlanzhou avatar Jan 06 '21 08:01 wangwenjunfromlanzhou

写的太好了

@wangwenjunfromlanzhou 感谢认可~ 😊😊😊

geektutu avatar Jan 11 '21 17:01 geektutu

直接这样可以吗defer func() { conn.Close() }() 而不是 defer func() { _ = conn.Close() }()

leigexiaohuozi avatar Mar 09 '21 14:03 leigexiaohuozi

client.sending.Lock() defer client.sending.Unlock() client.mu.Lock() defer client.mu.Unlock() 问题1:不太理解为啥terminateCalls方法,需要sending和mu两把锁,而注册call和删除call,只需要mu锁? 问题2:当client的某一个字段比如sending Lock()时,是不是在unlock()之前,这个client对象的sending字段是线程安全的,不会被其他线程或协程访问到吗?

IAOTW avatar Mar 13 '21 08:03 IAOTW

@leigexiaohuozi 直接这样可以吗defer func() { conn.Close() }() 而不是 defer func() { _ = conn.Close() }()

应该是 defer conn.Close()

Euraxluo avatar Mar 13 '21 11:03 Euraxluo

再一次被大佬精湛的技术 按在地板上摩擦

wilgx0 avatar Mar 16 '21 08:03 wilgx0

刚才照着大佬的代码敲了一遍,发现main函数中的循环发送请求如果不执行readBody或者执行出错,我们的sendResponse就会只打印其中的两三个请求,有时甚至还会报EOF错误:

代码如下:

     for i := 0; i < 5; i++ {
		h := &codec.Header{
			ServiceMethod: "Foo.Sum",
			Seq:           uint64(i),
		}
		_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
		_ = cc.ReadHeader(h)
		//var reply string
		//_ = cc.ReadBody(&reply)
		//log.Println("reply:", reply)
	}

是什么原因阻塞了我们的请求呢,又或者执行什么超时导致主线程退出了

XiaoyeFang avatar Mar 19 '21 10:03 XiaoyeFang

@wy-ei 请问下面这一行是什么意思呢?

var _ Codec = (*GobCodec)(nil)

就是检查结构体是否实现了这个接口

XiaoyeFang avatar Mar 19 '21 10:03 XiaoyeFang

@XiaoyeFang 刚才照着大佬的代码敲了一遍,发现main函数中的循环发送请求如果不执行readBody或者执行出错,我们的sendResponse就会只打印其中的两三个请求,有时甚至还会报EOF错误:

代码如下:

     for i := 0; i < 5; i++ {
		h := &codec.Header{
			ServiceMethod: "Foo.Sum",
			Seq:           uint64(i),
		}
		_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
		_ = cc.ReadHeader(h)
		//var reply string
		//_ = cc.ReadBody(&reply)
		//log.Println("reply:", reply)
	}

是什么原因阻塞了我们的请求呢,又或者执行什么超时导致主线程退出了

因为你的主协程 发送完5次请求后就退出了 此时运行服务器的协程还没有打印完这5次请求也退出了

wilgx0 avatar Mar 23 '21 06:03 wilgx0

我直接使用的day-1的代码,在windows上可以运行,到我虚拟机的ubuntu上,会阻塞在readRequestHeader,这可能是什么原因呢?

liyuxuan89 avatar Apr 05 '21 03:04 liyuxuan89

image 运行到箭头就阻塞了,不知道为啥? image 第一个箭头应该是option,第二个是header和body,client的request应该是发出去了? 在windows上没有问题。

liyuxuan89 avatar Apr 05 '21 05:04 liyuxuan89

https://github.com/geektutu/7days-golang/issues/34 不知道是不是这个问题

liyuxuan89 avatar Apr 05 '21 06:04 liyuxuan89

我想问问如果不在header里指定body长度,会有粘包拆包的问题吗?

yangchen97 avatar Apr 08 '21 06:04 yangchen97

@yangchen97 我想问问如果不在header里指定body长度,会有粘包拆包的问题吗?

json 字符串是有数据的边界的即 "{" 和 "}"所以这里并不会出现粘包的问题

wilgx0 avatar Apr 08 '21 07:04 wilgx0

问下 这行代码 // send options _ = json.NewEncoder(conn).Encode(geerpc.DefaultOption) 是怎么跟server通讯把option发过去的啊,我看这个conn就是conn, _ := net.Dial("tcp", <-addr).

tangwy01 avatar Apr 10 '21 07:04 tangwy01

@wilgx0

@yangchen97 我想问问如果不在header里指定body长度,会有粘包拆包的问题吗?

json 字符串是有数据的边界的即 "{" 和 "}"所以这里并不会出现粘包的问题

学到了

XiaoyeFang avatar Apr 13 '21 11:04 XiaoyeFang

func NewGobCodec(conn io.ReadWriteCloser) Codec {
	buf := bufio.NewWriter(conn)
	return &GobCodec{
		conn: conn,
		buf:  buf,
		dec:  gob.NewDecoder(conn),
		enc:  gob.NewEncoder(buf),
	}
}

秒啊enc使用新的buffer,我一开始没注意使用的gob.NewEncoder(conn),导致可能会Option和header一起传过去,就会导致用body的解析为header就报错了

sam-lc avatar Apr 22 '21 03:04 sam-lc

@sam-lc

func NewGobCodec(conn io.ReadWriteCloser) Codec {
	buf := bufio.NewWriter(conn)
	return &GobCodec{
		conn: conn,
		buf:  buf,
		dec:  gob.NewDecoder(conn),
		enc:  gob.NewEncoder(buf),
	}
}

秒啊enc使用新的buffer,我一开始没注意使用的gob.NewEncoder(conn),导致可能会Option和header一起传过去,就会导致用body的解析为header就报错了

请问这里可以解释一下,没太明白buf := bufio.NewWriter(conn)以及enc: gob.NewEncoder(buf),这两句

4ttenji avatar May 20 '21 08:05 4ttenji

感谢这个教程!实现这些项目我一个校招生终于春招找到了大厂工作!确实认认真真的熟悉了golang的编程思想和一些设计模式!十分感谢博主!

pieceof avatar May 27 '21 03:05 pieceof

@4ttenji

@sam-lc

func NewGobCodec(conn io.ReadWriteCloser) Codec {
	buf := bufio.NewWriter(conn)
	return &GobCodec{
		conn: conn,
		buf:  buf,
		dec:  gob.NewDecoder(conn),
		enc:  gob.NewEncoder(buf),
	}
}

秒啊enc使用新的buffer,我一开始没注意使用的gob.NewEncoder(conn),导致可能会Option和header一起传过去,就会导致用body的解析为header就报错了

请问这里可以解释一下,没太明白buf := bufio.NewWriter(conn)以及enc: gob.NewEncoder(buf),这两句

encoder 因为是要往 conn 中写入内容, 这里文章中说明了要使用 buffer 来优化写入效率, 所以我们先写入到 buffer 中, 然后我们再调用 buffer.Flush() 来将 buffer 中的全部内容写入到 conn 中, 从而优化效率. 对于读则不需要这方面的考虑, 所以直接在 conn 中读内容即可.

gu18168 avatar Jun 04 '21 07:06 gu18168

type GobCodec struct { conn io.ReadWriteCloser buf *bufio.Writer dec *gob.Decoder enc *gob.Encoder } codec.GobCodec on pkg.go.dev

cannot use (*GobCodec)(nil) (value of type *GobCodec) as Codec value in variable declaration: missing method

z-learner avatar Jun 06 '21 02:06 z-learner

json包里面的encode 和 decode只是简单调用的net包的read和write,没有处理tcp的数据边界问题,这样不会有问题吗?

valiner avatar Jun 23 '21 01:06 valiner

json包里面的encode 和 decode只是简单调用的net包的read和write,没有处理tcp的数据边界问题,这样不会有问题吗?

即使json有{}的分隔符,但是json.decode里面代码是调用了底层conn.read(),这里可能会把header里面的数据读出来,导致下次读取header数据出现残缺。

valiner avatar Jun 23 '21 10:06 valiner

@lazzman

@valiner

json包里面的encode 和 decode只是简单调用的net包的read和write,没有处理tcp的数据边界问题,这样不会有问题吗?

即使json有{}的分隔符,但是json.decode里面代码是调用了底层conn.read(),这里可能会把header里面的数据读出来,导致下次读取header数据出现残缺。

个人见解,不对请指正!/sdk/go1.16.4/src/encoding/json/stream.go:49Decode方法的实现 从方法注释和代码中的注释能看出来工作机制是从缓冲区中不断读取下一个Json编码内容 每次反序列前会从conn中读取所有的数据到缓冲区中,再从缓冲区数据中读取一个完整的Json编码内容,所以消息粘包问题被golang的这种流式编解码机制解决了。

// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for Unmarshal for details about
// the conversion of JSON into a Go value.
func (dec *Decoder) Decode(v interface{}) error {
	if dec.err != nil {
		return dec.err
	}

	if err := dec.tokenPrepareForDecode(); err != nil {
		return err
	}

	if !dec.tokenValueAllowed() {
		return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
	}

	// Read whole value into buffer.
	n, err := dec.readValue()
	if err != nil {
		return err
	}
	dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
	dec.scanp += n

	// Don't save err from unmarshal into dec.err:
	// the connection is still usable since we read a complete JSON
	// object from it before the error happened.
	err = dec.d.unmarshal(v)

	// fixup token streaming state
	dec.tokenValueEnd()

	return err
}

每次反序列前会从conn中读取所有的数据到缓冲区中,这个是时候是不是有可能读到header里面的信息。

valiner avatar Jun 25 '21 11:06 valiner