Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
geektutu opened this issue Oct 8, 2020 · 73 comments

Comments

@geektutu
Copy link
Owner

geektutu commented Oct 8, 2020

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 等多种传输协议。第一天实现了一个简单的服务端和消息的编码与解码。

@gaodansoft
Copy link

非常感谢这个教程

@geektutu
Copy link
Owner Author

@gaodansoft 笔芯~

@JYFiaueng
Copy link

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

@geektutu
Copy link
Owner Author

geektutu commented Nov 4, 2020

@JYFiaueng 感谢认可~ 😯

@w4096
Copy link

w4096 commented Dec 25, 2020

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

var _ Codec = (*GobCodec)(nil)

@geektutu
Copy link
Owner Author

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

var _ Codec = (*GobCodec)(nil)

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

@wangwenjunfromlanzhou
Copy link

写的太好了

@geektutu
Copy link
Owner Author

geektutu commented Jan 11, 2021

写的太好了

@wangwenjunfromlanzhou 感谢认可~ 😊😊😊

@leigexiaohuozi
Copy link

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

@IAOTW
Copy link

IAOTW commented Mar 13, 2021

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字段是线程安全的,不会被其他线程或协程访问到吗?

@Euraxluo
Copy link

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

应该是 defer conn.Close()

@wilgx0
Copy link

wilgx0 commented Mar 16, 2021

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

@XiaoyeFang
Copy link

刚才照着大佬的代码敲了一遍,发现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
Copy link

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

var _ Codec = (*GobCodec)(nil)

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

@wilgx0
Copy link

wilgx0 commented Mar 23, 2021

@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次请求也退出了

@liyuxuan89
Copy link

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

@liyuxuan89
Copy link

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

@liyuxuan89
Copy link

geektutu/7days-golang#34 不知道是不是这个问题

@yangchen97
Copy link

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

@wilgx0
Copy link

wilgx0 commented Apr 8, 2021

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

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

@tangwy01
Copy link

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

@XiaoyeFang
Copy link

@wilgx0

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

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

学到了

@sam-lc
Copy link

sam-lc commented Apr 22, 2021

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就报错了

@4ttenji
Copy link

4ttenji commented May 20, 2021

@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),这两句

@pieceof
Copy link

pieceof commented May 27, 2021

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

@gu18168
Copy link

gu18168 commented Jun 4, 2021

@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 中读内容即可.

@z-learner
Copy link

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

@valiner
Copy link

valiner commented Jun 23, 2021

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

@cyj19
Copy link

cyj19 commented Sep 9, 2021

@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)
	}

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

for循环后加一句time.Sleep(2 * time.Second),在运行就可以看到5个请求都处理了,所以应该就是主程序提前退出导致的

@cyj19
Copy link

cyj19 commented Sep 9, 2021

@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就报错了

为什么会导致Option和Header一起传过去啊?使用bufio不是为了提高写入的效率吗?按理说使用gob.NewEncoder(conn)也是可以的呀

@MoneyHappy
Copy link

day1-codec/main/main.go
文件中这行代码
_ = cc.ReadHeader(h)
的作用是啥,没太理解😢

@whitebluepants
Copy link

@MoneyHappy
day1-codec/main/main.go
文件中这行代码
_ = cc.ReadHeader(h)
的作用是啥,没太理解😢

这里就是读取服务端返回的response的header

@whitebluepants
Copy link

@wilgx0

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

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

但是涉及到json的只有前面的option,后面的header和body是gob编解码的,会有上述的问题吗?

@duanbiaowu
Copy link

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

var _ Codec = (*GobCodec)(nil)
  1. 将空值转换为 *GobCodec 类型
  2. 再转换为 Codec 接口
  3. 如果转换失败,说明 GobCodec 并没有实现 Codec 接口的所有方法

@sportwxp
Copy link

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

var _ Codec = (*GobCodec)(nil)

这个是go的一个小技巧,确定GobCodec实现了Codec的接口,java里面不需要这一行

@panjianning
Copy link

学习一下,分享下我画的图解

@AveryQi115
Copy link

我想请问一下main.go中这里client端通过Write函数写request之后就通过ReadHeader,ReadBody读取Response了。但是这里没有任何的同步点,是否可能出现client端ReadBody的时候server端还没有写入Response的情况呢?

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)
	}

@wjh791072385
Copy link

@Nicola115
我想请问一下main.go中这里client端通过Write函数写request之后就通过ReadHeader,ReadBody读取Response了。但是这里没有任何的同步点,是否可能出现client端ReadBody的时候server端还没有写入Response的情况呢?

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)
	}

我尝试注销cc.write部分,会发现cc.ReadHeader会阻塞住,应该是conn内如果还没有写入数据的话,就不会读出来,直到有数据写入

@AveryQi115
Copy link

AveryQi115 commented Mar 20, 2022

@wjh791072385

@Nicola115
我想请问一下main.go中这里client端通过Write函数写request之后就通过ReadHeader,ReadBody读取Response了。但是这里没有任何的同步点,是否可能出现client端ReadBody的时候server端还没有写入Response的情况呢?

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)
	}

我尝试注销cc.write部分,会发现cc.ReadHeader会阻塞住,应该是conn内如果还没有写入数据的话,就不会读出来,直到有数据写入

谢谢。 我之前理解错了,conn应该是本身在client端和server端就有独立的数据缓冲区的,所以如果server端还没写入,client端这边就读不到就会被block

@Sentiger
Copy link

Sentiger commented May 9, 2022

这里应该存在发送options的时候,服务端读取到options后面header的内容吧。导致再次读取后面的内容是不完整的,导致gob编码失败

@callmePicacho
Copy link

请教各位大佬一个问题,例如在 main.go 中,大量存在

_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
_ = cc.ReadHeader(h)

像这样的返回值既然要抛弃,为何不直接写成这样呢?

cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
cc.ReadHeader(h)

@ZAKLLL
Copy link

ZAKLLL commented Aug 23, 2022

当我尝试将Options 通过gob 编码发送后(服务端也使用 gob 解码)代码会在readRequestHeader 这个函数阻塞,这是为什么呢

main.go

	time.Sleep(time.Second)
	// 告知服务端本次链接的options
	//_ = json.NewEncoder(conn).Encode(server.DefaultOption)
	_ = gob.NewEncoder(conn).Encode(server.DefaultOption)

	//使用GobCodec 作为编辑编解码器
	cc := codec.NewGobCodec(conn)
	// send request & receive response
	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)
	}

server.go

// ServeConn runs the server on a single connection.
// ServeConn blocks, serving the connection until the client hangs up.
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	defer func() { _ = conn.Close() }()
	var opt Option
	//if err := json.NewDecoder(conn).Decode(&opt); err != nil {
	if err := gob.NewDecoder(conn).Decode(&opt); err != nil {
		log.Println("rpc server: options error: ", err)
		return
	}
	if opt.MagicNumber != MagicNumber {
		log.Printf("rpc server: invalid magic number %x", opt.MagicNumber)
		return
	}
	f := codec.NewCodecFuncMap[opt.CodecType]
	if f == nil {
		log.Printf("rpc server: invalid codec type %s", opt.CodecType)
		return
	}
	server.serveCodec(f(conn))
}

@yikuaibro
Copy link

	_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
	//_ = cc.ReadHeader(h)
    time.Sleep(5 * time.Second)
	var reply string
	_ = cc.ReadBody(&reply)

请教下,为什么注释掉ReadHeader,ReadBody会读不到东西,已经加了time.sleep,我理解的不是很清楚。

        &{Foo.Sum 0 } geerpc req 0
        reply:
        &{Foo.Sum 1 } geerpc req 1
        reply: geerpc resp 0
        &{Foo.Sum 2 } geerpc req 2
        reply:
        &{Foo.Sum 3 } geerpc req 3
        reply: geerpc resp 1
        &{Foo.Sum 4 } geerpc req 4
        reply:

@simon12138-code
Copy link

我出现了一个rpc server: invalid codec type 的报错,代码都是一摸一样的,请问这是哪里出现了问题,我调试出来主要是,没有接收到对应默认option的codectype

@simon12138-code
Copy link

已经解决, Option属性私有化了,非常的愚蠢

@Drum-sys
Copy link

@yikuaibro
_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
//_ = cc.ReadHeader(h)
time.Sleep(5 * time.Second)
var reply string
_ = cc.ReadBody(&reply)

请教下,为什么注释掉ReadHeader,ReadBody会读不到东西,已经加了time.sleep,我理解的不是很清楚。

        &{Foo.Sum 0 } geerpc req 0
        reply:
        &{Foo.Sum 1 } geerpc req 1
        reply: geerpc resp 0
        &{Foo.Sum 2 } geerpc req 2
        reply:
        &{Foo.Sum 3 } geerpc req 3
        reply: geerpc resp 1
        &{Foo.Sum 4 } geerpc req 4
        reply:

数据格式|header|body|heder|body
Header 类型codec.header raply 类型string
cc.ReadBody(&reply) reply字符串应该不能解析到codec.header 结构体当中

把reply类型改一下 var reply codec.Header
_ = cc.ReadBody(&reply)
2022/10/17 17:08:36 start rpc server on [::]:62015
2022/10/17 17:08:37 reply: {Foo.Sum 0 }
2022/10/17 17:08:37 reply: { 0 }
2022/10/17 17:08:37 reply: {Foo.Sum 1 }
2022/10/17 17:08:37 reply: { 0 }
2022/10/17 17:08:37 reply: {Foo.Sum 2 }

@sunviv
Copy link

sunviv commented Dec 20, 2022

time.Sleep(time.Second) 为什么这地方要阻塞 1s

@huty1998
Copy link

@panjianning

学习一下,分享下我画的图解

请问兄弟用的什么软件画的图?

@Asolmn
Copy link

Asolmn commented May 17, 2023

@yikuaibro
_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
//_ = cc.ReadHeader(h)
time.Sleep(5 * time.Second)
var reply string
_ = cc.ReadBody(&reply)

请教下,为什么注释掉ReadHeader,ReadBody会读不到东西,已经加了time.sleep,我理解的不是很清楚。

        &{Foo.Sum 0 } geerpc req 0
        reply:
        &{Foo.Sum 1 } geerpc req 1
        reply: geerpc resp 0
        &{Foo.Sum 2 } geerpc req 2
        reply:
        &{Foo.Sum 3 } geerpc req 3
        reply: geerpc resp 1
        &{Foo.Sum 4 } geerpc req 4
        reply:

解析是要安装顺序的,序列化的时候是先header然后body。读取的时候进行反序列化,所以也得安装序列化时候的顺序去解析,这就得先读取header再读body

@SCUTking
Copy link

@callmePicacho
请教各位大佬一个问题,例如在 main.go 中,大量存在

_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
_ = cc.ReadHeader(h)

像这样的返回值既然要抛弃,为何不直接写成这样呢?

cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
cc.ReadHeader(h)

用_接收不会有警告 为了好看

@LumosLiang
Copy link

@sunviv
time.Sleep(time.Second) 为什么这地方要阻塞 1s

同问,不过结合之前大佬们的回复,这里是解决粘包的问题吗?

@callmePicacho
Copy link

@sunviv
time.Sleep(time.Second) 为什么这地方要阻塞 1s

同问,不过结合之前大佬们的回复,这里是解决粘包的问题吗?

确保连接成功建立

@taosu0216
Copy link


画的有点乱,但是大概逻辑是能理清了

@TheWaveLab
Copy link

@callmePicacho

@sunviv
time.Sleep(time.Second) 为什么这地方要阻塞 1s

同问,不过结合之前大佬们的回复,这里是解决粘包的问题吗?

确保连接成功建立

粘包是靠在header里指定body长度解决

@TheWaveLab
Copy link

@callmePicacho
请教各位大佬一个问题,例如在 main.go 中,大量存在

_ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
_ = cc.ReadHeader(h)

像这样的返回值既然要抛弃,为何不直接写成这样呢?

cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq))
cc.ReadHeader(h)

这个只是为了让IDE忽略没有错误处理的警告

@TheWaveLab
Copy link

@MoneyHappy
day1-codec/main/main.go
文件中这行代码
_ = cc.ReadHeader(h)
的作用是啥,没太理解😢

为了解决tcp粘包,一般报文设计都会有先是header,在里面指定后面body的长度,所以要先读取header,判断报文类型和后续要读取的长度等。其实这个写的不太严谨,这里应该需要判断错误的,而不是直接忽略。

@yz627
Copy link

yz627 commented May 2, 2024

想请教一下,将header和body通过buffer缓冲一起进行发送,在client或server中进行读取的时候会出现粘包问题吗? 这里header好像没有固定大小?

@vito-go
Copy link

vito-go commented May 8, 2024

想请教一下,将header和body通过buffer缓冲一起进行发送,在client或server中进行读取的时候会出现粘包问题吗? 这里header好像没有固定大小?

Although the header and body are sent together, when the header and body are sent separately, the length of the bytes is marked. enc.countState.encodeUint(uint64(messageLen))

source code: go/src/encoding/gob/encoder.go

// writeMessage sends the data item preceded by an unsigned count of its length.
func (enc *Encoder) writeMessage(w io.Writer, b *encBuffer) {
	// Space has been reserved for the length at the head of the message.
	// This is a little dirty: we grab the slice from the bytes.Buffer and massage
	// it by hand.
	message := b.Bytes()
	messageLen := len(message) - maxLength
	// Length cannot be bigger than the decoder can handle.
	if messageLen >= tooBig {
		enc.setError(errors.New("gob: encoder: message too big"))
		return
	}
	// Encode the length.
	enc.countState.b.Reset()
	enc.countState.encodeUint(uint64(messageLen))
	// Copy the length to be a prefix of the message.
	offset := maxLength - enc.countState.b.Len()
	copy(message[offset:], enc.countState.b.Bytes())
	// Write the data.
	_, err := w.Write(message[offset:])
	// Drain the buffer and restore the space at the front for the count of the next message.
	b.Reset()
	b.Write(spaceForLength)
	if err != nil {
		enc.setError(err)
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests