热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Golang构建网络传输数据包

Golang构建网络传输数据包,Go语言社区,Golang程序员人脉社

网络通信中,端与端之间只能传输二进制数据流。TCP/IP协议的解析已经完全交给了硬件设备完成,即便是软路由等用服务器上装软件来替代硬件设备也已经相当成熟。我们需要面对的都是应用层的通信问题。而大部分情况下也无需考虑通信细节,因为总有各种框架比如长连接的websocket框架,处理HTTP协议的网站框架。或者直接提供网络访问包比如访问数据库的包,各种消息队列服务包。总之网络通信只剩下序列化对象,发送出去,另一端接收,反序列化成对象。甚至有些框架序列化的步骤都给省略了。
前人造出了各种轮子,所以我们只需要装几个沙发蒙张皮就可以卖车了。但深入研究才是根本。有时我们需要自行构建一套简单的协议来实现客户端与服务器或者不同程序之间通信。当然按照一些有影响力的协议来实现会更具有通用性,更规范安全。不过实现难度可就大大提高了。

定长数据包

golang建立网络连接还是相当容易的。以下代码为了尽可能清爽,省略错误处理等。客户端和服务器端都采用定长数据包,且采用 请求1 > 响应1 >> 请求2 > 响应2 这样的模式通信。


func main() {

    go server()

    client()

    time.Sleep(time.Second * 4)
}


//客户端实现
func client() {
    con, _ := net.Dial("tcp", "127.0.0.1:6666")

    buf := make([]byte, 50000)
    for i := 0; i < 5; i++ {
        data := make([]byte, 10000)
        copy(data, []byte("PING"+string.))
        con.Write(data)

        n, _ := io.ReadFull(con, buf)
        msg := string(buf[:n])

        fmt.Printf("%v %vn", msg[0:4], len(msg))
    }
    con.Close()
}

//服务器端实现
func server() {

    s, _ := net.Listen("tcp", "127.0.0.1:6666")
    for {
        c, _ := s.Accept()
        go func() {
            fmt.Println("someone connected")

            //固定PING数据包长度为10000
            buf := make([]byte, 10000)
            for {
                //读满数据
                n, err := io.ReadFull(c, buf)

                if err != nil {
                    fmt.Println(err)
                    break
                }

                msg := string(buf[:n])
                fmt.Printf("%v %vn", msg[0:4], len(msg))

                //固定PONG数据包长度为50000
                data := make([]byte, 50000)
                copy(data, []byte("PONG"))

                c.Write(data)

            }
            fmt.Println("disconnect")
        }()
    }

}

output:

someone connected
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
EOF
disconnect

定长数据包是最容易处理的。如果实际应用中数据包长度平均,可以加一部分填补空数据进去形成定长数据。虽然浪费了一定的网络带宽,但有得有失也是一种解决方案。

不定长数据包

不定长的数据包才是实际应用中经常遇到的。因此必须要解决数据流通信的包完整性问题和粘包问题。包的完整性当然是接收时得接收够数据。粘包问题是接收当前数据包时不能多截取到下一个包的数据。解决方案中网站这种打开连接传数据,传完数据关连接是最方便的。不过应用中除了网站很少采用这种模式通信。那么另一种解决方案就是在每个包末尾加个结束符号,当读取到结束符号时,即代表包完整结束,否则一直读取数据。

//客户端实现
func client() {
    con, _ := net.Dial("tcp", "127.0.0.1:6666")

    r := bufio.NewReaderSize(con, 5000) //缓冲区大小大于数据包
    data := make([]byte, 100)
    for i := 0; i < 5; i++ {
        data = append(data, data...)
        copy(data, []byte("PING"))
        con.Write(data)
        con.Write([]byte{' '}) //以空格作为结束符

        //接收以空格符作为结束的数据包
        s, _ := r.ReadSlice(' ')
        msg := string(s[:len(s)-1])

        fmt.Printf("%v %vn", msg[0:4], len(msg))
    }
    con.Close()
}

//服务器端实现
func server() {

    s, _ := net.Listen("tcp", "127.0.0.1:6666")
    for {
        c, _ := s.Accept()
        go func() {
            fmt.Println("someone connected")

            //缓冲区大小大于数据包
            r := bufio.NewReaderSize(c, 5000)
            data := make([]byte, 90)
            for {
                //接收以空格符作为结束的数据包
                s, err := r.ReadSlice(' ')

                if err != nil {
                    fmt.Println(err)
                    break
                }

                msg := string(s[:len(s)-1])
                fmt.Printf("%v %vn", msg[0:4], len(msg))

                data = append(data, data...)
                copy(data, []byte("PONG"))

                c.Write(data)
                c.Write([]byte{' '})//以空格作为结束符

            }
            fmt.Println("disconnect")
        }()
    }

}

output:

someone connected
PING 200
PONG 180
PING 400
PONG 360
PING 800
PONG 720
PING 1600
PONG 1440
PING 3200
PONG 2880
EOF
disconnect

这种方式当数据包中的数据含有结束符时就会出错。因此我们需要对数据进行一次编码,比如Base64。Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,是一种基于64个可打印字符来表示二进制数据的方法。编码后的数据比原始数据略长,为原来的1.3倍。在电子邮件中,根据RFC822规定,每76个字符,还需要加上一个回车换行。可以估算编码后数据长度大约为原长的135.1%。下面是通信例子中更改的部分。

    //发送时
    //将连接con用base64包装一下,对data进行编码
    w := base64.NewEncoder(base64.RawStdEncoding, con)
    w.Write(data)
    w.Close()
    //写入结束符
    con.Write([]byte{' '})

    //接收时
    s, _ := r.ReadSlice(' ')
    //去掉结束符
    s = s[:len(s)-1]

    //解码接收到的数据
    de := base64.RawStdEncoding
    d := make([]byte, de.DecodedLen(len(s)))
    de.Decode(d, s)
    msg := string(d)

以上方案只能适用简单场景,耗费带宽或者性能不高。

结构化数据包

结构化数据包应该是最常采用的方案。数据包拥有固定长度的消息头,不定长度的消息体,消息头内含消息体的长度。每次解析先读取固定长度消息头,通过消息头内的消息体长度再次读取完整的消息体。消息头内还可以包含各种命令,状态等数据,能在解析消息体之前先做一步业务处理。定义消息头的数据结构和含义的一整套规则可以统称为xxx协议。比如下面这个是websocket协议的包结构定义。关于websocket的详细资料可以在度娘上轻松找到。这里只是借来做个例子。

这里写图片描述
FIN
标识是否为此消息的最后一个数据包,占 1 bit
RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占1bit
Opcode
数据包类型(frame type),占4bits
0x0:标识一个中间数据包
0x1:标识一个text类型数据包
0x2:标识一个binary类型数据包
0x3-7:保留
0x8:标识一个断开连接类型数据包
0x9:标识一个ping类型数据包
0xA:表示一个pong类型数据包
0xB-F:保留
MASK:占1bits
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
Payload length
Payload data的长度,占7bits,7+16bits,7+64bits:
如果其值在0-125,则是payload的真实长度。
如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。

 
处理websocket协议太麻烦了,在此定义一种新协议A,就一条规则:首4字节为命令,后4字节为消息体长度。代码例子如下


//客户端实现
func client() {
    con, _ := net.Dial("tcp", "127.0.0.1:6666")

    data := make([]byte, 10)
    head := make([]byte, 8)
    for i := 0; i < 4; i++ {
        data = append(data, data...)
        //4字节的命令
        copy(data, []byte("PING"))
        //4字节的消息体长度
        binary.BigEndian.PutUint32(data[4:8], uint32(len(data)-8))
        con.Write(data)

        io.ReadFull(con, head)

        //取出命令
        cmd := string(head[0:4])
        //取出消息体长度
        bodylen := int(binary.BigEndian.Uint32(head[4:8]))
        //按长度, 再次读取消息体
        buf := make([]byte, bodylen)
        io.ReadFull(con, buf)

        msg := buf

        fmt.Printf("%v %vn", cmd, len(msg)+8)
    }
    con.Close()
}

//服务器端实现
func server() {

    s, _ := net.Listen("tcp", "127.0.0.1:6666")
    for {
        c, _ := s.Accept()
        go func() {
            fmt.Println("someone connected")

            data := make([]byte, 9)
            head := make([]byte, 8)
            for {
                _, err := io.ReadFull(c, head)

                if err != nil {
                    fmt.Println(err)
                    break
                }

                cmd := string(head[0:4])
                bodylen := int(binary.BigEndian.Uint32(head[4:8]))

                buf := make([]byte, bodylen)
                _, err2 := io.ReadFull(c, buf)
                if err2 != nil {
                    fmt.Println(err2)
                    break
                }
                msg := buf

                fmt.Printf("%v %vn", cmd, len(msg)+8)

                data = append(data, data...)
                copy(data, []byte("PONG"))
                binary.BigEndian.PutUint32(data[4:8], uint32(len(data)-8))
                c.Write(data)

            }
            fmt.Println("disconnect")
        }()
    } 
}

output:

someone connected
PING 20
PONG 18
PING 40
PONG 36
PING 80
PONG 72
PING 160
PONG 144
EOF
disconnect


推荐阅读
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文详细介绍了解决全栈跨域问题的方法及步骤,包括添加权限、设置Access-Control-Allow-Origin、白名单等。通过这些操作,可以实现在不同服务器上的数据访问,并解决后台报错问题。同时,还提供了解决second页面访问数据的方法。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • WebSocket与Socket.io的理解
    WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 如何提高PHP编程技能及推荐高级教程
    本文介绍了如何提高PHP编程技能的方法,推荐了一些高级教程。学习任何一种编程语言都需要长期的坚持和不懈的努力,本文提醒读者要有足够的耐心和时间投入。通过实践操作学习,可以更好地理解和掌握PHP语言的特异性,特别是单引号和双引号的用法。同时,本文也指出了只走马观花看整体而不深入学习的学习方式无法真正掌握这门语言,建议读者要从整体来考虑局部,培养大局观。最后,本文提醒读者完成一个像模像样的网站需要付出更多的努力和实践。 ... [详细]
author-avatar
mySi2502876237
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有