golang利用socket封装数据

什么是socket?

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程,能够唯一标示网络中的进程后,它们就可以利用socket进行通信。

建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

socket与http的区别

在网络七层架构中,http属于应用层协议,如图:图

而socket是位于应用层和传输层之间的一个抽象层,是本来不存在与七层架构中的:图


socket通信流程是怎样进行的?

socket的通信流程:


流程解读:

1. server端创建socket

2. server端绑定socket和端口号

3. server端监听该端口号

4. server端启动accept()接收来自client端的连接请求,此时有连接进入时会往后续执行,没有链接则会阻塞在此

5. client端创建socket

6. client端根据server端的ip和port连接server端(tcp的三次握手)

7. 如果第6步连接成功,在server端将会收到一个连接,假如这个连接名叫conn

8. client端向server端发送数据

9. server端从conn中读取到client端发送过来的数据

10. 任何一端都可以主动断开连接

socket中传输数据出现的问题

粘包:当server端用一个buff接收数据,发现buff中除了一个完整的包之外还有其他的数据。

半包:server端未接收到一个完整的包,只接收到了一部分。

socket的封包与拆包

在实际开发中往往会封装自己的数据,一般分为Head和Body,Head中除了封装额外的信息之外,还会封装Body的长度在里面。这样就形成了如下数据:

Head{        //头信息  (总共占用6个字节)

One byte      //第一个标志  (1个字节)

Two byte      //第二个标志  (1个字节)

Length int32    //存放Body的数据长度 (4个字节) 

}

Body{       //消息体 (总共占用?+4个字节)

Name []byte   //名字   (占用?个字节)

Age int32   //年龄   (占用4个字节)

}

Data{

Head

Body

}

server端要对接收到的数据[]byte进行拆包以防止粘包和半包情况的发生。

注:Data这个数据结构在server端和client端都应保持一致

封包过程:

将一个封装好的Data数据转化成[]byte,然后发送。

拆包过程:

1. 循环从conn中读取数据,每次循环都判断接收到的数据是否>=6,若是,则表明接收完了Head,若否,继续执行下一次循环,知道满足条件

2. 接收完Head后,将前6个字节的数据解析到Data的Head中,对于从conn接收到的数据长度减去6(得到的是接收到的Body数据的长度),判断这个结果是否>=Head中Length,若否,循环继续接收数据;若是,则解析出Length长度的数据放入Data的Body,自此之后的数据又重新用一个Data来解析并存放

3. 关闭连接最优方案应该是server端判断数据接收完时关闭连接

这样拆包就解决了粘包和半包的问题。

golang代码实现

首先分为3个包,1:Data包;2:client包;3:server包

1:Data包的代码

//socketDataprojectData.go

package Data

import(

    "bytes"

)

//Head总共占用6个字节

type Head struct{

    One byte//第一个数据(1个字节)

    Two byte//第二个数据(1个字节)

    Length int32//存放Body数据的长度(4个字节)

}

//Body总共占用?+4个字节

type Body struct{

    Name string//名字(?个字节)

    Age    int32//年龄(4个字节)

}

//传输数据

type Data struct{

    Head

    Body

}

//初始化一个Data包

func NewData(one,twobyte,namestring,ageint32) *Data{

    data := &Data{}

    data.One = one

    data.Two = two

    data.Length = int32(len([]byte(name))+1)

    data.Name = name

    data.Age = age

    return data

}

//将Data转化成[]byte(封包)

func (d*Data)ToByte() []byte{

    var bb bytes.Buffer

    bb.WriteByte(d.One)

    bb.WriteByte(d.Two)

    bb.WriteByte(byte(d.Length))//注意这里是将length转化成byte,所以之后解析头的时候长度是3

    bb.Write([]byte(d.Name))

    bb.WriteByte(byte(d.Age))//这里是将age转化成byte,所以之后解析body中的name时-1

    return  bb.Bytes()

}

//将[]byte解析到d中(拆包)

func (d*Data)FromBytes (buff []byte){

     bb := bytes.NewBuffer(buff)

    var err error

    d.One,err = bb.ReadByte()

    checkErr(err)

    d.Two,err = bb.ReadByte()

    checkErr(err)

    length,_,err := bb.ReadRune()

    checkErr(err)

    d.Length = int32(length)

    if len(buff)-3 >= int(d.Length){  //为什么-3,查看ToByte()中的注释

           //buff包含了一个完整的body体

        d.Name = string(bb.Next(int(d.Length-1)))  //为什么-1,看ToByte()中的注释

        length,_,err = bb.ReadRune()

        checkErr(err)

        d.Age=int32(length)

    }

}

func  checkErr(errerror){

    if err!=nil {

        panic(err)

    }

}

2.client包的代码

//socketDataClientprojectmain.go

packagemain

import(

    "fmt"

    "net"

    . "socketData"

)

func main(){

    service := ":7777"

    tcpAddr,err := net.ResolveTCPAddr("tcp4",service)

    checkErr(err)

    conn,err := net.DialTCP("tcp",nil,tcpAddr)

    conn.SetKeepAlive(true)

    data := NewData('1','0',"初级赛亚人",117)

    data2 := NewData('2','3',"中级赛亚人",116)

    data3 := NewData('3','4',"超级赛亚人",115)

    //向server发送数据

    conn.Write(data.ToByte())

    conn.Write(data2.ToByte())

    conn.Write(data3.ToByte())

    //向server发送结束符

    conn.Write([]byte("#"))

    fmt.Println([]byte("#"))

    fmt.Println("发送消息为data:",data)

    fmt.Println("发送消息为data.toByte=",len(data.ToByte()),data.ToByte())

    fmt.Println("发送消息为data2:",data2)

    fmt.Println("发送消息为data2.toByte=",len(data2.ToByte()),data2.ToByte())

    fmt.Println("发送消息为data3:",data2)

    fmt.Println("发送消息为data3.toByte=",len(data2.ToByte()),data2.ToByte())

    var buf []byte=make([]byte,64,64)

    conn.CloseWrite()

    conn.Read(buf)

    fmt.Println("客户端从服务端接收到的数据:",buf)

    conn.Close()

}

func checkErr (err error){

    if err != nil {

        panic(err)

    }

}

3. server包的代码

//socketDataServerprojectmain.go

packagemain

import(

    "bytes"

    "fmt"

    "net"

    ."socketData"

    "time"

)

func main(){

    tcpAddr,err := net.ResolveTCPAddr("tcp4",":7777")

    checkErr(err)

    listener,err := net.ListenTCP("tcp",tcpAddr)

    checkErr(err)

    for{

        conn,err := listener.Accept()

        checkErr(err)

        //开启一个goroutine处理这个连接

        go handleConn(conn)  // 这是典型的BIO方式处理连接,之后会有一篇专门讲解如何用go和chan来优化接收方式

    }

}

func handleConn(conn net.Conn){

    defer  conn.Close()

    var buf []byte = make([]byte,64,64)

    var newBuff []byte

    for{

        conn.Read(buf)   //从conn中读取字符

        buf = bytes.TrimRight(buf,"\x00")  //去除buf尾部的所有0

        newBuff = append(newBuff,buf...)

        if len(newBuff)<3{

            //buf中未包含一个完整的Head信息

            fmt.Println(newBuff)

            continue

        }else{

            var data *Data = &Data{}

            //取出包头

            data.FromBytes(newBuff)

            //判断去掉包头剩下的长度是否可以取出body体

            for len(newBuff)-3 >= int(data.Length){

                data.FromBytes(newBuff[:(3+data.Length)])

                newBuff=newBuff[(3+data.Length):]   //3+data.Length之前的数据已经存放到data中

                fmt.Println("data=",data)   //此时接收到data,可以选择将data存入一个list中

                iflen(newBuff)>=3{

                          //这里重新解析出包头,会覆盖之前的数据

                          data.FromBytes(newBuff)

                          continue

                     }else{

                          break

                     }

             }

       }

//收到结束符,表示client端已发送完数据且不会再发送数据

        if bytes.Contains(newBuff,[]byte("#")){

            fmt.Println("get#")

            break

        }

    }

//向client写入数据(随便写点什么)

    daytime:=time.Now().String()

    fmt.Println(daytime)

    conn.Write([]byte(daytime))

}

funccheckErr(errerror){

iferr!=nil{

panic(err)

}

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容