Tony Bai · Go 语言第一课
Tony Bai
资深架构师,tonybai.com 博主
21492 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 59 讲
开篇词 (1讲)
结束语 (1讲)
Tony Bai · Go 语言第一课
15
15
1.0x
00:00/00:00
登录|注册

37|代码操练:怎么实现一个TCP服务器?(中)

Submit & SubmitAck: 实现Packet接口
Packet接口: Decode & Encode
myFrameCodec: 实现StreamFrameCodec
StreamFrameCodec接口: Encode & Decode
打包(encode): Frame -> 字节流
解包(decode): 字节流 -> Frame
payload/result: 业务数据/响应状态
ID: 消息流水号
commandID: 包类型标识
totalLength: 包的总长度
创建tcp-server-demo1项目
Packet: 表示业务层真正需要的消息
Frame: 表示TCP字节流中的每一个协议消息
main: 监听端口并接受连接
handleConn: 处理TCP连接
Packet的实现
Frame的实现
深入协议字段
源码项目建立
为服务端增加优雅退出机制和panic捕捉
为packet包编写单元测试
回顾协议抽象、打包解包、服务端组装
构建和运行服务端与客户端程序
实现客户端模拟器
服务端的组装
协议的解包与打包
建立协议的抽象
学习网络I/O操作
研究Go语言的socket编程模型
理解TCP服务器的基本概念和工作原理
思考题
小结
验证测试
设计与实现
技术预研与储备
理解问题
TCP服务器实现

该思维导图由 AI 生成,仅供参考

你好,我是 Tony Bai。
上一讲中,我们讲解了解决 Go 语言学习“最后一公里”的实用思路,那就是“理解问题” -> “技术预研与储备” -> “设计与实现”的三角循环,并且我们也完成了“理解问题”和“技术预研与储备”这两个环节,按照“三角循环”中的思路,这一讲我们应该针对实际问题进行一轮设计与实现了。
今天,我们的目标是实现一个基于 TCP 的自定义应用层协议的通信服务端,要完成这一目标,我们需要建立协议的抽象、实现协议的打包与解包、服务端的组装、验证与优化等工作。一步一步来,我们先在程序世界建立一个对上一讲中自定义应用层协议的抽象。

建立对协议的抽象

程序是对现实世界的抽象。对于现实世界的自定义应用协议规范,我们需要在程序世界建立起对这份协议的抽象。在进行抽象之前,我们先建立这次实现要用的源码项目 tcp-server-demo1,建立的步骤如下:
$mkdir tcp-server-demo1
$cd tcp-server-demo1
$go mod init github.com/bigwhite/tcp-server-demo1
go: creating new go.mod: module github.com/bigwhite/tcp-server-demo1
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
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-07
    7
  • 左耳朵东
    client 代码中的 done chan 好像没必要吧,去掉它也能正常退出

    作者回复: 这里的确没必要。但是如果handle ack的goroutine在退出前需要执行一些清理工作,那么done就有必要了。否则可能会出现handle ack的goroutine没有执行完清理工作,send goroutine就退出的进而导致main goroutine退出前某handle ack的goroutine都没有执行完清理工作。

    2022-02-12
    2
    4
  • // select { case <-quit: done <- struct{}{} return default: } 老师,client中读取服务端返回响应的这个goroutine中,这段select的作用不是很理解,如果没有从quit中收到值就会一直轮询,但是从quit中收到值又会return,那下面的代码不是一直都没有机会执行了吗

    作者回复: 如果没有从quit中收到值,是会轮询啊。不过每次轮询的间隔是5s,程序会先在socket上做阻塞读,直到超时。超时后就回到for开始处,这也给了goroutine一个优雅退出的机会。

    2022-07-28归属地:陕西
    2
    3
  • 晚枫
    为什么totalLen指定了字节序,payload不需要指定吗?

    作者回复: 字节序是针对size>=2个字节的整型数而言的。payload对于该协议来说只是一个“字节序列”。协议的任务就是解析出payload,然后交给上层处理。

    2022-05-09
    2
    2
  • 罗杰
    还是老师实现的代码优雅,我们项目的这块代码是刚开始学 Go 时实现的,只能说可以用。但对比老师的实现,我觉得我们的代码可以好好优化一下了。

    作者回复: 👍

    2022-01-26
    2
  • 张尘
    白老师好, 本节课受益颇多, 有点疑问, 还望有时间能够帮忙解答下: 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-19
    1
  • 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-10
    2
    1
收起评论
显示
设置
留言
24
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部