Socket封装数据详解(Golang版)

什么是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的长度在里面。这样就形成了如下数据:

//头信息  (总共占用6个字节)
Head{        
    One byte      //第一个标志  (1个字节)
    Two byte      //第二个标志  (1个字节)
    Length int32    //存放Body的数据长度 (4个字节) 
}



//消息体 (总共占用?+4个字节)
Body{       
    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(err error){
    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',"Golang语言",117)
    data2 := NewData('2','3',"Python语言",116)
    data3 := NewData('3','4',"Java语言",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(err error){
    iferr!=nil{
        panic(err)
    }
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金戈鐡馬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值