什么是socket?
網(wǎng)絡(luò)上的兩個(gè)程序通過(guò)一個(gè)雙向的通信連接實(shí)現(xiàn)數(shù)據(jù)的交換,這個(gè)連接的一端稱為一個(gè)socket。
我們知道兩個(gè)進(jìn)程如果需要進(jìn)行通訊最基本的一個(gè)前提能能夠唯一的標(biāo)示一個(gè)進(jìn)程,在本地進(jìn)程通訊中我們可以使用PID來(lái)唯一標(biāo)示一個(gè)進(jìn)程,但PID只在本地唯一,網(wǎng)絡(luò)中的兩個(gè)進(jìn)程PID沖突幾率很大,這時(shí)候我們需要另辟它徑了,我們知道IP層的ip地址可以唯一標(biāo)示主機(jī),而TCP層協(xié)議和端口號(hào)可以唯一標(biāo)示主機(jī)的一個(gè)進(jìn)程,這樣我們可以利用ip地址+協(xié)議+端口號(hào)唯一標(biāo)示網(wǎng)絡(luò)中的一個(gè)進(jìn)程,能夠唯一標(biāo)示網(wǎng)絡(luò)中的進(jìn)程后,它們就可以利用socket進(jìn)行通信。
建立網(wǎng)絡(luò)通信連接至少要一對(duì)端口號(hào)(socket)。socket本質(zhì)是編程接口(API),對(duì)TCP/IP的封裝,TCP/IP也要提供可供程序員做網(wǎng)絡(luò)開(kāi)發(fā)所用的接口,這就是Socket編程接口;HTTP是轎車,提供了封裝或者顯示數(shù)據(jù)的具體形式;Socket是發(fā)動(dòng)機(jī),提供了網(wǎng)絡(luò)通信的能力。
socket與http的區(qū)別
在網(wǎng)絡(luò)七層架構(gòu)中,http屬于應(yīng)用層協(xié)議,如圖:圖

而socket是位于應(yīng)用層和傳輸層之間的一個(gè)抽象層,是本來(lái)不存在與七層架構(gòu)中的:圖

socket通信流程是怎樣進(jìn)行的?
socket的通信流程:

流程解讀:
1. server端創(chuàng)建socket
2. server端綁定socket和端口號(hào)
3. server端監(jiān)聽(tīng)該端口號(hào)
4. server端啟動(dòng)accept()接收來(lái)自client端的連接請(qǐng)求,此時(shí)有連接進(jìn)入時(shí)會(huì)往后續(xù)執(zhí)行,沒(méi)有鏈接則會(huì)阻塞在此
5. client端創(chuàng)建socket
6. client端根據(jù)server端的ip和port連接server端(tcp的三次握手)
7. 如果第6步連接成功,在server端將會(huì)收到一個(gè)連接,假如這個(gè)連接名叫conn
8. client端向server端發(fā)送數(shù)據(jù)
9. server端從conn中讀取到client端發(fā)送過(guò)來(lái)的數(shù)據(jù)
10. 任何一端都可以主動(dòng)斷開(kāi)連接
socket中傳輸數(shù)據(jù)出現(xiàn)的問(wèn)題
粘包:當(dāng)server端用一個(gè)buff接收數(shù)據(jù),發(fā)現(xiàn)buff中除了一個(gè)完整的包之外還有其他的數(shù)據(jù)。
半包:server端未接收到一個(gè)完整的包,只接收到了一部分。
socket的封包與拆包
在實(shí)際開(kāi)發(fā)中往往會(huì)封裝自己的數(shù)據(jù),一般分為Head和Body,Head中除了封裝額外的信息之外,還會(huì)封裝Body的長(zhǎng)度在里面。這樣就形成了如下數(shù)據(jù):
Head{ ? ? ? ?//頭信息 ?(總共占用6個(gè)字節(jié))
One byte ? ? ?//第一個(gè)標(biāo)志 ?(1個(gè)字節(jié))
Two byte ? ? ?//第二個(gè)標(biāo)志 ?(1個(gè)字節(jié))
Length int32 ? ?//存放Body的數(shù)據(jù)長(zhǎng)度 (4個(gè)字節(jié))?
}
Body{ ? ? ? //消息體 (總共占用?+4個(gè)字節(jié))
Name []byte ? //名字 ? (占用?個(gè)字節(jié))
Age int32 ? //年齡 ? (占用4個(gè)字節(jié))
}
Data{
Head
Body
}
server端要對(duì)接收到的數(shù)據(jù)[]byte進(jìn)行拆包以防止粘包和半包情況的發(fā)生。
注:Data這個(gè)數(shù)據(jù)結(jié)構(gòu)在server端和client端都應(yīng)保持一致
封包過(guò)程:
將一個(gè)封裝好的Data數(shù)據(jù)轉(zhuǎn)化成[]byte,然后發(fā)送。
拆包過(guò)程:
1. 循環(huán)從conn中讀取數(shù)據(jù),每次循環(huán)都判斷接收到的數(shù)據(jù)是否>=6,若是,則表明接收完了Head,若否,繼續(xù)執(zhí)行下一次循環(huán),知道滿足條件
2. 接收完Head后,將前6個(gè)字節(jié)的數(shù)據(jù)解析到Data的Head中,對(duì)于從conn接收到的數(shù)據(jù)長(zhǎng)度減去6(得到的是接收到的Body數(shù)據(jù)的長(zhǎng)度),判斷這個(gè)結(jié)果是否>=Head中Length,若否,循環(huán)繼續(xù)接收數(shù)據(jù);若是,則解析出Length長(zhǎng)度的數(shù)據(jù)放入Data的Body,自此之后的數(shù)據(jù)又重新用一個(gè)Data來(lái)解析并存放
3. 關(guān)閉連接最優(yōu)方案應(yīng)該是server端判斷數(shù)據(jù)接收完時(shí)關(guān)閉連接
這樣拆包就解決了粘包和半包的問(wèn)題。
golang代碼實(shí)現(xiàn)
首先分為3個(gè)包,1:Data包;2:client包;3:server包
1:Data包的代碼
//socketDataprojectData.go
package Data
import(
? ? "bytes"
)
//Head總共占用6個(gè)字節(jié)
type Head struct{
? ? One byte//第一個(gè)數(shù)據(jù)(1個(gè)字節(jié))
? ? Two byte//第二個(gè)數(shù)據(jù)(1個(gè)字節(jié))
? ? Length int32//存放Body數(shù)據(jù)的長(zhǎng)度(4個(gè)字節(jié))
}
//Body總共占用?+4個(gè)字節(jié)
type Body struct{
? ? Name string//名字(?個(gè)字節(jié))
? ? Age ? ?int32//年齡(4個(gè)字節(jié))
}
//傳輸數(shù)據(jù)
type Data struct{
? ? Head
? ? Body
}
//初始化一個(gè)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轉(zhuǎn)化成[]byte(封包)
func (d*Data)ToByte() []byte{
? ? var bb bytes.Buffer
? ? bb.WriteByte(d.One)
? ? bb.WriteByte(d.Two)
? ? bb.WriteByte(byte(d.Length))//注意這里是將length轉(zhuǎn)化成byte,所以之后解析頭的時(shí)候長(zhǎng)度是3
? ? bb.Write([]byte(d.Name))
? ? bb.WriteByte(byte(d.Age))//這里是將age轉(zhuǎn)化成byte,所以之后解析body中的name時(shí)-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包含了一個(gè)完整的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',"初級(jí)賽亞人",117)
? ? data2 := NewData('2','3',"中級(jí)賽亞人",116)
? ? data3 := NewData('3','4',"超級(jí)賽亞人",115)
? ? //向server發(fā)送數(shù)據(jù)
? ? conn.Write(data.ToByte())
? ? conn.Write(data2.ToByte())
? ? conn.Write(data3.ToByte())
? ? //向server發(fā)送結(jié)束符
? ? conn.Write([]byte("#"))
? ? fmt.Println([]byte("#"))
? ? fmt.Println("發(fā)送消息為data:",data)
? ? fmt.Println("發(fā)送消息為data.toByte=",len(data.ToByte()),data.ToByte())
? ? fmt.Println("發(fā)送消息為data2:",data2)
? ? fmt.Println("發(fā)送消息為data2.toByte=",len(data2.ToByte()),data2.ToByte())
? ? fmt.Println("發(fā)送消息為data3:",data2)
? ? fmt.Println("發(fā)送消息為data3.toByte=",len(data2.ToByte()),data2.ToByte())
? ? var buf []byte=make([]byte,64,64)
? ? conn.CloseWrite()
? ? conn.Read(buf)
? ? fmt.Println("客戶端從服務(wù)端接收到的數(shù)據(jù):",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)
? ? ? ? //開(kāi)啟一個(gè)goroutine處理這個(gè)連接
? ? ? ? go handleConn(conn) ?// 這是典型的BIO方式處理連接,之后會(huì)有一篇專門講解如何用go和chan來(lái)優(yōu)化接收方式
? ? }
}
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中未包含一個(gè)完整的Head信息
? ? ? ? ? ? fmt.Println(newBuff)
? ? ? ? ? ? continue
? ? ? ? }else{
? ? ? ? ? ? var data *Data = &Data{}
? ? ? ? ? ? //取出包頭
? ? ? ? ? ? data.FromBytes(newBuff)
? ? ? ? ? ? //判斷去掉包頭剩下的長(zhǎng)度是否可以取出body體
? ? ? ? ? ? for len(newBuff)-3 >= int(data.Length){
? ? ? ? ? ? ? ? data.FromBytes(newBuff[:(3+data.Length)])
? ? ? ? ? ? ? ? newBuff=newBuff[(3+data.Length):] ? //3+data.Length之前的數(shù)據(jù)已經(jīng)存放到data中
? ? ? ? ? ? ? ? fmt.Println("data=",data) ? //此時(shí)接收到data,可以選擇將data存入一個(gè)list中
? ? ? ? ? ? ? ? iflen(newBuff)>=3{
? ? ? ? ? ? ? ? ? ? ? ? ? //這里重新解析出包頭,會(huì)覆蓋之前的數(shù)據(jù)
? ? ? ? ? ? ? ? ? ? ? ? ? data.FromBytes(newBuff)
? ? ? ? ? ? ? ? ? ? ? ? ? continue
? ? ? ? ? ? ? ? ? ? ?}else{
? ? ? ? ? ? ? ? ? ? ? ? ? break
? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ?}
? ? ? ?}
//收到結(jié)束符,表示client端已發(fā)送完數(shù)據(jù)且不會(huì)再發(fā)送數(shù)據(jù)
? ? ? ? if bytes.Contains(newBuff,[]byte("#")){
? ? ? ? ? ? fmt.Println("get#")
? ? ? ? ? ? break
? ? ? ? }
? ? }
//向client寫入數(shù)據(jù)(隨便寫點(diǎn)什么)
? ? daytime:=time.Now().String()
? ? fmt.Println(daytime)
? ? conn.Write([]byte(daytime))
}
funccheckErr(errerror){
iferr!=nil{
panic(err)
}
}