socket編程
Socket是BSD UNIX的進程通信機制,通常也稱作”套接字”,用於描述IP地址和端口,是一個通信鏈的句柄。Socket可以理解為TCP/IP網絡的API,它定義了許多函數或例程,程序員可以用它們來開發TCP/IP網絡上的應用程序。電腦上運行的應用程序通常通過”套接字”向網絡發出請求或者應答網絡請求。
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket后面,對用戶來說只需要調用Socket規定的相關函數,讓Socket去組織符合指定的協議數據然后進行通信。
GO語言實現TCP通信
TCP協議
TCP/IP(Transmission Control Protocol/Internet Protocol) 即傳輸控制協議/網間協議,是一種面向連接(連接導向)的、可靠的、基於字節流的傳輸層(Transport layer)通信協議,因為是面向連接的協議,數據像水流一樣傳輸,會存在黏包問題。
TCP服務端
一個TCP服務端可以同時連接很多個客戶端,Go語言中創建多個goroutine實現並發非常方便和高效,所以可以每建立一次鏈接就創建一個goroutine去處理。
TCP服務端程序的處理流程:
- 監聽端口
- 接收客戶端請求建立鏈接
- 創建goroutine處理鏈接
TCP服務端:
//TCP server端
func process(conn net.Conn) {
defer conn.Close() //關閉連接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n,err := reader.Read(buf[:]) //讀取數據
if err != nil{
fmt.Println("連接客戶端失敗,錯誤信息:",err)
}
recvStr := string(buf[:n])
fmt.Println("收到客戶端信息:",recvStr)
conn.Write([]byte(recvStr)) //發送數據
}
}
func main() {
listen,err := net.Listen("tcp","127.0.0.1:8888")
if err != nil{
fmt.Println("監聽失敗,錯誤:",err)
return
}
for {
conn,err := listen.Accept() //建立連接
if err!= nil{
fmt.Println("建立連接失敗,錯誤:",err)
continue
}
go process(conn) //啟動一個goroutine處理連接
}
}
TCP客戶端
一個TCP客戶端進行TCP通信的流程如下:
- 建立與服務端的鏈接
- 進行數據收發
- 關閉鏈接
TCP客戶端:
//客戶端
func main() {
conn ,err := net.Dial("tcp","127.0.0.1:8888")
if err != nil {
fmt.Println("連接失敗,錯誤:",err)
return
}
defer conn.Close()
inputReader := bufio.NewReader(os.Stdout)
for {
input, _ := inputReader.ReadString('\n') //讀取用戶輸入
inputInfo := strings.Trim(input,"\r\n")
if strings.ToUpper(inputInfo) == "q"{
return //如果輸入q就退出
}
_,err = conn.Write([]byte(inputInfo)) //發送數據
if err != nil{
return
}
buf := [512]byte{}
n,err := conn.Read(buf[:])
if err != nil{
fmt.Println("接受失敗,錯誤:",err)
return
}
fmt.Println(string(buf[:n]))
}
}
先啟動server,后啟動client:
$go run main.go
我是客戶端
我是客戶端
$go run main.go
收到客戶端信息: 我是客戶端
GO語言實現UDP通信
UDp協議
UDP協議(User Datagram Protocol)中文名稱是用戶數據報協議,是OSI(Open System Interconnection,開放式系統互聯)參考模型中一種無連接的傳輸層協議,不需要建立連接就能直接進行數據發送和接收,屬於不可靠的、沒有時序的通信,但是UDP協議的實時性比較好,通常用於視頻直播相關領域。
UDP服務端
//服務端
func main() {
listen,err := net.ListenUDP("udp",&net.UDPAddr{
IP:net.IPv4(0,0,0,0),
Port:8888,
})
if err != nil{
fmt.Println("監聽失敗,錯誤:",err)
return
}
defer listen.Close()
for {
var data [1024]byte
n,addr,err := listen.ReadFromUDP(data[:])
if err != nil{
fmt.Println("接收udp數據失敗,錯誤:",err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_ ,err = listen.WriteToUDP(data[:n],addr) //發送數據
if err != nil{
fmt.Println("發送數據失敗,錯誤:",err)
continue
}
}
}
UDP客戶端
//客戶端
func main() {
socket,err := net.DialUDP("udp",nil,&net.UDPAddr{
IP:net.IPv4(0,0,0,0),
Port:8888,
})
if err != nil{
fmt.Println("連接服務器失敗,錯誤:",err)
return
}
defer socket.Close()
sendData := []byte("hello world!")
_,err = socket.Write(sendData)
if err != nil{
fmt.Println("發送數據失敗,錯誤:",err)
return
}
data := make([]byte,4096)
n,remoteAddr,err := socket.ReadFromUDP(data)
if err != nil{
fmt.Println("接受數據失敗,錯誤:",err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
先啟動server,后啟動client:
$go run main.go
recv:hello world! addr:127.0.0.1:8888 count:12
$go run main.go
data:hello world! addr:127.0.0.1:51222 count:12
HTTP客戶端和服務端
HTTP協議
超文本傳輸協議(HTTP,HyperText Transfer Protocol)是互聯網上應用最為廣泛的一種網絡傳輸協議,所有的WWW文件都必須遵守這個標准。設計HTTP最初的目的是為了提供一種發布和接收HTML頁面的方法。
HTTP服務端
net/http包是對net包的進一步封裝,專門用來處理HTTP協議的數據。
// http server
func sayHi(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w,"你好,ares!")
}
func main() {
http.HandleFunc("/",sayHi)
err := http.ListenAndServe(":8888",nil)
if err != nil{
fmt.Println("Http 服務建立失敗,err:",err)
return
}
}

HTTP客戶端
func main() {
resp, err := http.Get("https://www.baidu.com/")
if err != nil {
fmt.Println("get failed, err:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Printf("%T\n",body)
fmt.Println(string(body))
}
執行之后就能在終端輸出www.baidu.com網站首頁的內容了。
TCP粘包
粘包服務端
//粘包
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [1024]byte
for {
n,err := reader.Read(buf[:])
if err == io.EOF{
break
}
if err != nil{
fmt.Println("讀取客戶端失敗,err",err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client發來的數據:",recvStr)
}
}
func main() {
listen,err := net.Listen("tcp","127.0.0.1:8888")
if err != nil{
fmt.Println("監聽失敗,err",err)
return
}
defer listen.Close()
for {
conn,err := listen.Accept()
if err != nil{
fmt.Println("接受失敗,err",err)
continue
}
go process(conn)
}
}
粘包客戶端
func main() {
conn,err := net.Dial("tcp","127.0.0.1:8888")
if err != nil{
fmt.Println("連接失敗,err",err)
return
}
defer conn.Close()
for i:=0;i<20;i++{
msg := "Ares is a bird!"
conn.Write([]byte(msg))
}
}
先啟動服務端再啟動客戶端,可以看到服務端輸出結果如下:
$go run main.go
收到client發來的數據: Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!
收到client發來的數據: Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!
客戶端分10次發送的數據,在服務端並沒有成功的輸出10次,而是多條數據“粘”到了一起。
TCP為什么會出現粘包
在socket網絡程序中,TCP和UDP分別是面向連接和非面向連接的。因此TCP的socket編程,收發兩端(客戶端和服務器端)都要有成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小、數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。
對於UDP,不會使用塊的合並優化算法,這樣,實際上目前認為,是由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。所以UDP不會出現粘包問題。
粘包產生原因
1發送端需要等緩沖區滿才發送出去,造成粘包
2接收方不及時接收緩沖區的包,造成多個包接收
具體點:
(1)發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一包數據。若連續幾次發送的數據都很少,通常TCP會根據優化算法把這些數據合成一包后一次發送出去,這樣接收方就收到了粘包數據。
(2)接收方引起的粘包是由於接收方用戶進程不及時接收數據,從而導致粘包現象。這是因為接收方先把收到的數據放在系統接收緩沖區,用戶進程從該緩沖區取數據,若下一包數據到達時前一包數據尚未被用戶進程取走,則下一包數據放到系統接收緩沖區時就接到前一包數據之后,而用戶進程根據預先設定的緩沖區大小從系統接收緩沖區取數據,這樣就一次取到了多包數據。
參考:TCP通信粘包問題分析和解決
解決辦法
出現”粘包”的關鍵在於接收方不確定將要傳輸的數據包的大小,因此我們可以對數據包進行封包和拆包的操作。
自定義一個協議,比如數據包的前4個字節為包頭,里面存儲的是發送的數據的長度。
// Encode 將消息編碼
func Encode(message string)([]byte ,error) {
// 讀取消息的長度,轉換成int32類型(占4個字節)
var length = int32(len(message))
var pkg = new(bytes.Buffer)
//寫入消息頭
err := binary.Write(pkg,binary.LittleEndian,length)
if err != nil{
return nil,err
}
//寫入消息實體
err = binary.Write(pkg,binary.LittleEndian,[]byte(message))
if err != nil{
return nil,err
}
return pkg.Bytes(),nil
}
// Decode 消息解碼
func Decode(reader *bufio.Reader)(string,error) {
//讀取消息長度
lengthByte,_ := reader.Peek(4) //讀取前4個字節數據
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
err := binary.Read(lengthBuff,binary.LittleEndian,&length)
if err != nil{
return "",err
}
// Buffered返回緩沖中現有的可讀取的字節數。
if int32(reader.Buffered()) < length+4{
return "",err
}
//讀取真正的消息數據
pack := make([]byte,int(4+length))
_,err = reader.Read(pack)
if err != nil{
return "",err
}
return string(pack[4:]),nil
}
server端:
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
msg,err := proto.Decode(reader)
if err == io.EOF{
return
}
if err != nil{
fmt.Println("decode 失敗,err",err)
return
}
fmt.Println("收到client數據:",msg)
}
}
func main() {
listen,err := net.Listen("tcp","127.0.0.1:8888")
if err != nil{
fmt.Println("監聽失敗,err",err)
return
}
defer listen.Close()
for {
conn,err := listen.Accept()
if err != nil{
fmt.Println("接受失敗,err",err)
continue
}
go process(conn)
}
}
client端:
func main() {
conn,err := net.Dial("tcp","127.0.0.1:8888")
if err != nil{
fmt.Println("dial失敗,err",err)
return
}
defer conn.Close()
for i:=0;i<20;i++{
msg := "Hello Ares!"
data,err := proto.Encode(msg)
if err != nil{
fmt.Println("encode失敗,err",err)
return
}
conn.Write(data)
}
}
先啟動服務端再啟動客戶端,可以看到服務端輸出結果如下:
go run main.go
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!