37|代码操练:怎么实现一个TCP服务器?(中)
该思维导图由 AI 生成,仅供参考
建立对协议的抽象
- 深入了解
- 翻译
- 解释
- 总结
本文深入介绍了如何在Go语言中实现基于TCP的自定义应用层协议的通信服务端。作者首先强调了在程序世界中建立对协议的抽象的重要性,然后详细讲解了自定义应用层协议的各个字段含义,并介绍了建立Frame和Packet抽象的过程。文章讨论了协议的解包与打包原理,包括字节流到Frame的解包和Frame到字节流的打包过程。通过这些步骤,读者可以了解如何在程序中实现对自定义应用层协议的抽象设计和解包打包操作。此外,还介绍了Frame和Packet的实现,以及如何编写单元测试来保证编解码的正确性。整体而言,本文内容深入浅出,适合想要深入了解网络编程和协议设计的读者阅读。文章通过示例代码展示了服务端和客户端的实现,并进行了验证测试,展示了自定义应用层协议的通信过程。读者可以从中学习到如何构建基于TCP的自定义应用层协议的服务端和客户端,并了解其工作原理。文章内容丰富,适合对网络编程和协议设计感兴趣的技术人员阅读。
《Tony Bai · Go 语言第一课》,新⼈⾸单¥59
全部留言(24)
- 最新
- 精选
- Aeins几点疑问 1. 协议处理程序保证使用相同的字节序的情况下,有必要一定用大端序吗,改成小端序,也能成功。 2. TCP 保证顺序交付的,不指定字节序,顺序处理数据流可以吗。这时会有字节序问题吗,如果协议栈都使用同一种字节序呢。(我认为字节序和程序使用的字节序有关,如果每个程序都使用同一种字节序,那应该就不存在字节序问题了,比如本程序,收发都用相同的字节序处理,不知道这个结论对不对) 3. 协议头和协议体,分两次写入的,会不会有并发安全问题,为什么?这里应该没做到上节课说的,一次写入一个“业务包”吧。 4. 多次运行 client,错误偶发。有时 io.ReadFull 读不满数据,有时读取的数据长度不对,会是哪些原因导致的呢?
作者回复: 问题很棒! 这里逐一回答一下: 1、2:网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。 3. 按照每个连接一个 Goroutine 的模型,不是并发写,不存在你说的问题。 4. go doc io.ReadFull一下,一般情况下,ReadFull都会读出你想要的长度的数据。你遇到错误时,ReadFull返回什么error呢。 [upd]: 发现问题了。是client的SetReadDeadline设置为1s,太短了。已改,请pull最新demo代码。
2022-06-077 - 左耳朵东client 代码中的 done chan 好像没必要吧,去掉它也能正常退出
作者回复: 这里的确没必要。但是如果handle ack的goroutine在退出前需要执行一些清理工作,那么done就有必要了。否则可能会出现handle ack的goroutine没有执行完清理工作,send goroutine就退出的进而导致main goroutine退出前某handle ack的goroutine都没有执行完清理工作。
2022-02-1224 - 枫// select { case <-quit: done <- struct{}{} return default: } 老师,client中读取服务端返回响应的这个goroutine中,这段select的作用不是很理解,如果没有从quit中收到值就会一直轮询,但是从quit中收到值又会return,那下面的代码不是一直都没有机会执行了吗
作者回复: 如果没有从quit中收到值,是会轮询啊。不过每次轮询的间隔是5s,程序会先在socket上做阻塞读,直到超时。超时后就回到for开始处,这也给了goroutine一个优雅退出的机会。
2022-07-28归属地:陕西23 - 晚枫为什么totalLen指定了字节序,payload不需要指定吗?
作者回复: 字节序是针对size>=2个字节的整型数而言的。payload对于该协议来说只是一个“字节序列”。协议的任务就是解析出payload,然后交给上层处理。
2022-05-0922 - 罗杰还是老师实现的代码优雅,我们项目的这块代码是刚开始学 Go 时实现的,只能说可以用。但对比老师的实现,我觉得我们的代码可以好好优化一下了。
作者回复: 👍
2022-01-262 - 张尘白老师好, 本节课受益颇多, 有点疑问, 还望有时间能够帮忙解答下: frameCodec.Decode返回值是自定义数据结构FramePayload packet.Decode的入参是[]byte client/server 代码中直接将FramePayload当做[]byte使用 frameCodec.Decode为什么要返回自定义数据结构FramePayload而不是[]byte呢? 是因为FramePayload的结构可能改变吗? FramePayload可能不是[]byte吗? FramePayload可能包含Packet之外的其它数据吗? 可是如果FramePayload的结构改变, 那client/server 的代码中直接将FramePayload当做[]byte的用法不是就有问题了吗?
作者回复: 可以直接使用[]byte类型,这里定义FramePayload更多为了强调其是frame的payload,仅此而已。
2022-12-27归属地:北京1 - Sunrise有个小疑问: func (p *myFrameCodec) Encode(w io.Writer, framePayload FramePayload) error { var f = framePayload ... } var f = framePayload 这个地方有必要重新定义一个 f 吗,直接使用 framePayload 会有什么问题?
作者回复: “直接使用 framePayload ” 也没有问题。
2022-11-24归属地:辽宁1 - 农民园丁请问老师,framePayload, err := frameCodec.Decode(c) 以上代码中"c"是net.Conn 类型, 而frameCodec.Decode(io.Reader)的输入参数是io.Reader, 这两个为什么可以不一样?
作者回复: net.Conn可以理解为io.Reader这个接口类型的方法集合的超集,也就是说所有实现了net.Conn的类型,也都实现了io.Reader接口类型。
2022-11-02归属地:北京1 - Geek_25f93f老师,单元测试的代码是不是有点问题,就判断条件是if err == nil
作者回复: 你指的是TestEncodeWithWriteFail这个unit test? 这个测试就是为了测试Encode失败的情况。只有err == nil的情况下,才不符合我们的预期。
2022-06-191 - qiutian// tcp-server-demo1/packet/packet.go func Encode(p Packet) ([]byte, error) { var commandID uint8 var pktBody []byte var err error switch t := p.(type) { case *Submit: commandID = CommandSubmit pktBody, err = p.Encode() if err != nil { return nil, err } case *SubmitAck: commandID = CommandSubmitAck pktBody, err = p.Encode() if err != nil { return nil, err } default: return nil, fmt.Errorf("unknown type [%s]", t) } return bytes.Join([][]byte{[]byte{commandID}, pktBody}, nil), nil } 老师,这段代码的最后的 return bytes.Join(), nil这个在什么情况下回运行到呢?不是很理解
作者回复: return语句最后的nil是代表err=nil,就是一切ok,没有报错。Encode函数的原型,最后一个返回值是一个error类型。
2022-06-1021