什么是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)
}
}