[Golang] 从零開始写Socket Server(2): 自己定义通讯协议

在上一章我们做出来一个最基础的demo后,已经能够初步实现Server和Client之间的信息交流了~
这一章我会介绍一下怎么在Server和Client之间实现一个简单的通讯协议。从而增强整个信息交流过程的稳定性。

在Server和client的交互过程中,有时候非常难避免出现网络波动,而在通讯质量较差的时候,Client有可能无法将信息流一次性完整发送,终于传到Server上的信息非常可能变为非常多段。

例如以下图所看到的。本来应该是分条传输的json。结果由于一些原因连接在了一起,这时候就会出现故障啦。Server端要怎么推断收到的消息是否完整呢?~

唔,答案就是这篇文章的主题啦:在Server和Client交互的时候。增加一个通讯协议(protocol),让二者的交互通过这个协议进行封装。从而使Server可以推断收到的信息是否为完整的一段。(也就是解决分包的问题)

由于主要目的是为了让Server能推断client发来的信息是否完整,因此整个协议的核心思路并非非常复杂:

协议的核心就是设计一个头部(headers),在Client每次发送信息的时候将header封装进去。再让Server在每次收到信息的时候依照预定格式将消息进行解析,这样依据Client传来的数据中是否包括headers,就能够非常轻松的推断收到的信息是否完整了~

假设信息完整,那么就将该信息发送给下一个逻辑进行处理。假设信息不完整(缺少headers),那么Server就会把这条信息与前一条信息合并继续处理。

以下是协议部分的代码,主要分为数据的封装(Enpack)和解析(Depack)两个部分,当中Enpack用于Client端将传给server的数据封装。而Depack是Server用来解析数据,当中Const部分用于定义Headers。HeaderLength则是Headers的长度,用于后面Server端的解析。这里要说一下ConstMLength,这里代表Client传入信息的长度,由于在golang中。int转为byte后会占4长度的空间,因此设定为4。

每次Client向Server发送信息的时候。除了将Headers封装进去意以外,还会将传入信息的长度也封装进去,这样能够方便Server进行解析和校验。

//通讯协议处理
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 = i + 1 {
		if length < i+ConstHeaderLength+ConstMLength {
			break
		}
		if string(buffer[i:i+ConstHeaderLength]) == ConstHeader {
			messageLength := BytesToInt(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 IntToBytes(n int) []byte {
	x := int32(n)

	bytesBuffer := bytes.NewBuffer([]byte{})
	binary.Write(bytesBuffer, binary.BigEndian, x)
	return bytesBuffer.Bytes()
}

//字节转换成整形
func BytesToInt(b []byte) int {
	bytesBuffer := bytes.NewBuffer(b)

	var x int32
	binary.Read(bytesBuffer, binary.BigEndian, &x)

	return int(x)
}

协议写好之后,接下来就是在Server和Client的代码中应用协议啦,以下是Server端的代码,主要负责解析Client通过协议发来的信息流:

package main  

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

func main() {
    netListen, err := net.Listen("tcp", "localhost:6060")
    CheckError(err)  

    defer netListen.Close()  

    Log("Waiting for clients")
    for {
        conn, err := netListen.Accept()
        if err != nil {
            continue
        }  

        //timeouSec :=10
        //conn.
        Log(conn.RemoteAddr().String(), " tcp connect success")
        go handleConnection(conn)  

    }
}  

func handleConnection(conn net.Conn) {  

    // 缓冲区,存储被截断的数据
    tmpBuffer := make([]byte, 0)  

    //接收解包
    readerChannel := make(chan []byte, 16)
    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]...))
    }
    defer conn.Close()
}  

func reader(readerChannel chan []byte) {
    for {
        select {
        case data := <-readerChannel:
            Log(string(data))
        }
    }
}  

func Log(v ...interface{}) {
    fmt.Println(v...)
}  

func CheckError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}  

然后是Client端的代码,这个简单多了。仅仅要给信息封装一下就能够了~:

package main
import (
"protocol"
"fmt"
"net"
"os"
"time"
"strconv"  

)  

func send(conn net.Conn) {
    for i := 0; i < 100; i++ {
        session:=GetSession()
        words := "{\"ID\":"+ strconv.Itoa(i) +"\",\"Session\":"+session +"2015073109532345\",\"Meta\":\"golang\",\"Content\":\"message\"}"
        conn.Write(protocol.Enpacket([]byte(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:6060"
    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)  

}  

这样我们就成功实如今Server和Client之间建立一套自己定义的基础通讯协议啦,让我们执行一下看下效果:

成功识别每一条Client发来的信息啦~~

很多其它具体信息能够參考这篇文章: golang中tcp socket粘包问题和处理

我已经把SocketServer系列的代码整合到了一起,公布到了我个人的github上:点击链接
希望大家有兴趣的能够学习star一下~

时间: 2024-11-08 21:15:57

[Golang] 从零開始写Socket Server(2): 自己定义通讯协议的相关文章

[Golang] 从零開始写Socket Server(3): 对长、短连接的处理策略(模拟心跳)

通过前两章,我们成功是写出了一套凑合能用的Server和Client,并在二者之间实现了通过协议交流.这么一来,一个简易的socket通讯框架已经初具雏形了,那么我们接下来做的.就是想办法让这个框架更加稳定.茁壮~ 作为一个可能会和非常多Client进行通讯交互的Server.首先要保证的就是整个Server执行状态的稳定性,因此在和Client建立连接通讯的时候,确保连接的及时断开非常重要,否则一旦和多个client建立不关闭的长连接,对于server资源的占用是非常可怕的.因此,我们须要针对

[Golang] 从零開始写Socket Server(4):将执行參数放入配置文件(XML/YAML)

为了将我们写好的Server公布到server上.就要将我们的代码进行build打包.这样假设以后想要改动一些代码的话.须要又一次给代码进行编译打包并上传到server上. 显然,这么做过于繁琐. ..因此常见的做法都是将Server执行中可能会频繁变更的变量.数值写入配置文件里.这样直接让程序从配置文件读取參数,避免对代码频繁的操作. 关于配置文件的格式,在这里推荐YAML 和XML~ XML是传统的配置文件写法,只是本人比較推荐yaml,他比XML要更加人性化,也更好写,关于yaml的具体信

从零開始写游戏引擎(一) - project创建以及文件夹设置还有版本号控制

一句话提要 好的開始等于成功了一半. 创建文件夹结构 project文件夹下最好分为以下几个文件夹 Docs - 开发文档,设计文档 Assets - 角色,动作,模型和音效等 Source - 代码,project文件或者makefile也放在这里,假设有引用第三方的lib,在里面建立一个3rdParty的文件夹,放在里面. Temp - 用于防止编译生成的文件 Lib - 放置编译好的lib文件,将source编译成lib能够更好地保护源码. Game - 用于放置release buid,

《PHP 5.5从零開始学(视频教学版)》内容简单介绍、文件夹

<PHP 5.5从零開始学(视频教学版)>当当网购买地址: http://product.dangdang.com/23586810.html <PHP 5.5从零開始学(视频教学版)>源码.教学视频下载地址: http://pan.baidu.com/s/1zt9hW 内 容 简 介 本书循序渐进地介绍了PHP 5.5开发动态站点的主要知识和技能,提供了大量的PHP应用实例供读者实践.每一章节都清晰讲述了代码作用及其编写思路,使读者能在最短时间内迅速掌握PHP的应用开发技能. 全

Spring 从零開始-01

因为学习需求,近期突然通知须要学习Spring.可是因为之前非常少使用Java.所以感觉还是有点慢,花了大约两周,弄懂了主要的结构也编了点小程序.还是万事开头难啊,网上总是非常难找到学习spring的指引资料.学习一个东西最主要的还是学习他的思想.还有就是一个可靠地学习建议,这东西会让你事半功倍,学习一定要依赖于书本,网上再好的资料都不如一本书.这个是本科+研究生的经验总结,spring学习能够參考<spring实战>,这本书写得非常精彩.可是就是须要有点高度,假设你从零開始那将会有一些困难,

从零開始开发Android版2048 (一)初始化界面

自学Android一个月多了,一直在工作之余零零散散地看一些东西.感觉经常使用的东西都有些了解了,可是一開始写代码总会出各种奇葩的问题.感觉还是代码写得太少.这样继续杂乱地学习下去进度也太慢了,并且学一点忘一点,效率太低.所以从今天開始.我打算实际做点小程序.在开发中不断地学习吧. 恰好近期Android上有个游戏2048比較火,所以就那这个练手吧. 由于对Android还没有太深入的了解,所以我写的东西都会比較基础,所以须要看一些高阶开发的朋友能够绕过了,也希望能够有高手们给我一些指导和建议,

从零開始学Swift之Hello World进化版

上节课,也就是昨晚啦,我们学习到从零開始学Swift之Hello World.那一节仅仅有一句代码,大家会认为不够过瘾. 那么这节课,就给大家来多点瘾货吧! 先上图! //var 代表变量的类型, start 代表变量的名称. "Hello" 代表变量的值. var start = "Hello" var dot = " , " var end = "world!" // let 是什么意思呢? let total = sta

[Golang] 从零开始写Socket Server(2): 自定义通讯协议

在上一章我们做出来一个最基础的demo后,已经可以初步实现Server和Client之间的信息交流了~ 这一章我会介绍一下怎么在Server和Client之间实现一个简单的通讯协议,从而增强整个信息交流过程的稳定性. 在Server和client的交互过程中,有时候很难避免出现网络波动,而在通讯质量较差的时候,Client有可能无法将信息流一次性完整发送,最终传到Server上的信息很可能变为很多段. 如下图所示,本来应该是分条传输的json,结果因为一些原因连接在了一起,这时候就会出现问题啦,

站点搭建从零開始(二)server空间

前面介绍了域名相关知识.这里介绍server,也经常被叫做空间.也就是站点数据实际保存的地方. 1.整体介绍 依据国家相关规定,国内server空间须要进行备案.也就是到相关的机构进行登记.详细方法能够网上找.域名也有备案一说.国内域名不备案可能会被禁用. 国外的域名和server.一般就不须要备案了. 国外server的一大缺点是速度一般比国内server慢,有些甚至差点儿没有速度. (1)自己搭建server server事实上也就是配置比較高的电脑.装了适合做server的操作系统,安装了