52. Socket Server 自定义协议的简单实现 | 厚土Go学习笔记

在 Server 和 Client 通讯中,由于网络等原因很有可能会发生数据丢包的现象。如果数据确实,服务端接收的信息不完整,就会造成混乱。

我们就需要在 Server 和 Client 之间建立一个通讯协议,通过协议中的规则,判断当前接收到的信息是否完整。根据信息的完整情况,采取不同的处理方法。

通讯协议 protocol 的核心就是设计一个头部。如果传来的信息不包含这个头部,就说明当前信息和之前的信息是同一条。那么就把当前信息和之前的那条信息合并成一条。

而协议主要包含的功能是封装(Enpack)和解析(Depack)。

Enpack 是客户端对信息进行数据封装。封装之后可以传递给服务器。

Depack 是服务端对信息进行数据解封。

其中有个 Const 部分,用于定义头部、头部长度、客户端传入信息长度。

在代码中,我们这样定义

const (
    ConstHeader = "Headers"
    ConstHeaderLength = 7
    ConstMLength = 4
)

头部的内容为 "Headers",长度为 7 。

所以 ConstHeaderLength = 7

而信息传递中,我们会把 int 类型转换成 byte 类型。一个 int 的长度等于 4 个 byte 的长度。因此,我们设置 ConstMLength = 4。代表客户端传来的信息大小。

自定义协议 protocol 的代码示例如下:

/**
* protocol
* @Author:  Jian Junbo
* @Email:   junbojian@qq.com
* @Create:  2017/9/14 11:49
*
* Description:  通讯协议处理
*/
package protocol

import (
    "bytes"
    "encoding/binary"
)

const (
    ConstHeader = "Headers"
    ConstHeaderLength = 7
    ConstMLength = 4
)

//封包
func Enpack(message []byte) []byte {
    return append(append([]byte(ConstHeader), IntToBytes(len(message))...), message...)
}

//解包
func Depack(buffer []byte) []byte {
    length := len(buffer)

    var i int
    data := make([]byte, 32)
    for i = 0; i < length; i++ {

        if length < i + ConstHeaderLength + ConstMLength{
            break
        }
        if string(buffer[i:i+ConstHeaderLength]) == ConstHeader {
            messageLength := ByteToInt(buffer[i+ConstHeaderLength : i+ConstHeaderLength+ConstMLength])
            if length < i+ConstHeaderLength+ConstMLength+messageLength {
                break
            }
            data = buffer[i+ConstHeaderLength+ConstMLength : i+ConstHeaderLength+ConstMLength+messageLength]
        }
    }

    if i == length {
        return make([]byte, 0)
    }

    return data
}

//字节转换成整形
func ByteToInt(n []byte) int {
    bytesbuffer := bytes.NewBuffer(n)
    var x int32
    binary.Read(bytesbuffer, binary.BigEndian, &x)

    return int(x)
}

//整数转换成字节
func IntToBytes(n int) []byte {
    x := int32(n)
    bytesBuffer := bytes.NewBuffer([]byte{})
    binary.Write(bytesBuffer, binary.BigEndian, x)
    return bytesBuffer.Bytes()
}

Server 端主要是通过协议来解析客户端发送来的信息。

建立一个函数,用来完成连接对接收信息的处理。其中建立了通道readerChannel,并把接收来的信息放在通道里。在放入通道之前,使用 protocol 的 Depack 对信息进行解析。

//连接处理
func handleConnection(conn net.Conn) {
    //缓冲区,存储被截断的数据
    tmpBuffer := make([]byte, 0)
    //接收解包
    readerChannel := make(chan []byte, 10000)
    go reader(readerChannel)

    buffer := make([]byte, 1024)
    for{
        n, err := conn.Read(buffer)
        if err != nil{
            Log(conn.RemoteAddr().String(), "connection error: ", err)
            return
        }
        tmpBuffer = protocol.Depack(append(tmpBuffer, buffer[:n]...))
        readerChannel <- tmpBuffer      //接收的信息写入通道

    }
    defer conn.Close()
}

如果信息读取发生错误(包括读取到信息结束符 EOF),都会打印错误信息,并挑出循环。

Log(conn.RemoteAddr().String(), "connection error: ", err)
return

由于通道内的数据是 []byte 型的。需要转换成 string。这个工作有专门的获取通道数据的 reader(readerChannel chan []byte) 来完成。

//获取通道数据
func reader(readerchannel chan []byte) {
    for{
        select {
        case data := <-readerchannel:
            Log(string(data))       //打印通道内的信息
        }
    }
}

查看 Server 端代码示例:

/**
* MySocketProtocalServer
* @Author:  Jian Junbo
* @Email:   junbojian@qq.com
* @Create:  2017/9/14 13:54
* Copyright (c) 2017 Jian Junbo All rights reserved.
*
* Description:  服务端,接收客户端传来的信息
*/
package main

import (
    "net"
    "fmt"
    "os"
    "log"
    "protocol"
)

func main() {
    netListen, err := net.Listen("tcp", "localhost:7373")
    CheckErr(err)
    defer netListen.Close()

    Log("Waiting for client ...")       //启动后,等待客户端访问。
    for{
        conn, err := netListen.Accept()     //监听客户端
        if err != nil {
            Log(conn.RemoteAddr().String(), "发了了错误:", err)
            continue
        }
        Log(conn.RemoteAddr().String(), "tcp connection success")
        go handleConnection(conn)
    }
}

//连接处理
func handleConnection(conn net.Conn) {
    //缓冲区,存储被截断的数据
    tmpBuffer := make([]byte, 0)
    //接收解包
    readerChannel := make(chan []byte, 10000)
    go reader(readerChannel)

    buffer := make([]byte, 1024)
    for{
        n, err := conn.Read(buffer)
        if err != nil{
            Log(conn.RemoteAddr().String(), "connection error: ", err)
            return
        }
        tmpBuffer = protocol.Depack(append(tmpBuffer, buffer[:n]...))
        readerChannel <- tmpBuffer      //接收的信息写入通道

    }
    defer conn.Close()
}

//获取通道数据
func reader(readerchannel chan []byte) {
    for{
        select {
        case data := <-readerchannel:
            Log(string(data))       //打印通道内的信息
        }
    }
}

//日志处理
func Log(v ...interface{}) {
    log.Println(v...)
}

//错误处理
func CheckErr(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

客户端使用 Enpack 封装要发送到服务端的信息后,写入连接 conn 中。

/**
* MySocketProtocalClient
* @Author:  Jian Junbo
* @Email:   junbojian@qq.com
* @Create:  2017/9/14 15:23
* Copyright (c) 2017 Jian Junbo All rights reserved.
*
* Description:  
*/
package main

import (
    "net"
    "time"
    "strconv"
    "protocol"
    "fmt"
    "os"
)

//发送100次请求
func send(conn net.Conn)  {
    for i := 0; i < 100; i++ {
        session := GetSession()
        words := "{\"ID\":\""+strconv.Itoa(i)+"\",\"Session\":\""+session+"20170914165908\",\"Meta\":\"golang\",\"Content\":\"message\"}"
        conn.Write(protocol.Enpack([]byte(words)))
        fmt.Println(words)      //打印发送出去的信息
    }
    fmt.Println("send over")
    defer conn.Close()
}
//用当前时间做识别。当前时间的十进制整数
func GetSession() string {
    gs1 := time.Now().Unix()
    gs2 := strconv.FormatInt(gs1, 10)
    return gs2
}

func main() {
    server := "localhost:7373"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", server)
    if err != nil{
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil{
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }

    fmt.Println("connect success")

    send(conn)

}

代码运行效果:

服务端运行效果

客户端运行效果


原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2018-01-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术碎碎念

OS存储器管理(一)

存储器的层次: 分为寄存器、主存(内存)和 辅存(外存)三个层次。 主存:高速缓冲存储器、主存储器、磁盘缓冲存储器,          主存又称为可执行存储...

40390
来自专栏Django中文社区

分类与归档

侧边栏已经正确地显示了最新文章列表、归档、分类等信息。现在来完善归档和分类功能,当用户点击归档下的某个日期或者分类下的某个分类时,跳转到文章列表页面,显示该日期...

37290
来自专栏企鹅号快讯

Python3爬取1024图片

分析 列表页面 首先进入1024的导航网站,随便点击一个地址进入选择图片区或者在网站地址后面添加,这就是1024网站的图片区,这个爬虫就是主要抓取这个区域的所有...

96690
来自专栏前端新视界

使用 gulp-file-include 构建前端静态页面

前言 虽然现在单页面很流行,但是在 PC 端多页面还是常态,所以构建静态页面的工具还有用武之地。最近也看到了一些询问如何 include HTML 文件的问题。...

39560
来自专栏Python、Flask、Django

python中用requests获取API参数

24660
来自专栏我的技术专栏

《Go in action》读后记录:Go的并发与并行

10230
来自专栏Golang语言社区

《GO IN ACTION》读后记录:GO的并发与并行

一、使用goroutine来运行程序 1. Go的并发与并行 Go的并发能力,是指让某个函数独立于其他函数运行的能力。当为一个函数创建goroutine时,该函...

46870
来自专栏用户2442861的专栏

修改npm全局安装模式的路径

刚学nodeJS不久,很纳闷为什么全局安装的模块在 'node安装目录/node_modules‘ 中没找到!后来仔细看了下安装成功后的信息,才发现原来是自动安...

11220
来自专栏Golang语言社区

实效go编程--4

func handle(queue chan *Request) { for r := range queue { process(r)...

369150
来自专栏破晓之歌

vue中axios处理http发送请求的示例(Post和get)

axios中文文档:https://github.com/mzabriskie/axios#using-applicationx-www-form-urlenc...

29730

扫码关注云+社区

领取腾讯云代金券