打開鏈接
TCP Socket的連接的建立需要經(jīng)歷客戶端和服務(wù)端的三次握手的過程核偿。
連接建立過程中闻坚,服務(wù)端是一個標準的Listen + Accept的結(jié)構(gòu)(可參考上面的代碼),而在客戶端Go語言使用net.Dial或DialTimeout進行連接建立:
阻塞Dial:
conn, err := net.Dial("tcp", "google.com:80")
超時機制的Dial:
conn, err := net.DialTimeout("tcp", ":8080", 2 * time.Second)
客戶端連接的建立會遇到如下幾種情形
1 網(wǎng)絡(luò)不可達或?qū)Ψ椒?wù)未啟動
如果傳給Dial的Addr是可以立即判斷出網(wǎng)絡(luò)不可達急黎,或者Addr中端口對應(yīng)的服務(wù)沒有啟動疏橄,端口未被監(jiān)聽,Dial會幾乎立即返回錯誤奶浦,比如:
2 對方服務(wù)的listen backlog滿
還有一種場景就是對方服務(wù)器很忙兄墅,瞬間有大量client端連接嘗試向server建立,server端的listen backlog隊列滿澳叉,server accept不及時((即便不accept隙咸,那么在backlog數(shù)量范疇里面,connect都會是成功的成洗,因為new conn已經(jīng)加入到server side的listen queue中了五督,accept只是從queue中取出一個conn而已),這將導致client端Dial阻塞瓶殃。我們還是通過例子感受Dial的行為特點:
3 網(wǎng)絡(luò)延遲較大充包,Dial阻塞并超時
如果網(wǎng)絡(luò)延遲較大,TCP握手過程將更加艱難坎坷(各種丟包)碌燕,時間消耗的自然也會更長误证。
Dial這時會阻塞继薛,如果長時間依舊無法建立連接修壕,則Dial也會返回“ getsockopt: operation timed out”錯誤。
在連接建立階段遏考,多數(shù)情況下慈鸠,Dial是可以滿足需求的,即便阻塞一小會兒灌具。
但對于某些程序而言青团,需要有嚴格的連接時間限定,如果一定時間內(nèi)沒能成功建立連接咖楣,程序可能會需要執(zhí)行一段“異扯桨剩”處理邏輯,為此我們就需要DialTimeout了诱贿。
下面的例子將Dial的最長阻塞時間限制在2s內(nèi)娃肿,超出這個時長,Dial將返回timeout error.
服務(wù)端讀的行為特點
1 Socket中無數(shù)據(jù)
連接建立后珠十,如果對方未發(fā)送數(shù)據(jù)到socket料扰,接收方(Server)會阻塞在Read操作上。執(zhí)行該Read操作的goroutine也會被掛起焙蹭。runtime會監(jiān)視該socket晒杈,直到其有數(shù)據(jù)才會重新
調(diào)度該socket對應(yīng)的Goroutine完成read。
2 Socket中有部分數(shù)據(jù)
如果socket中有部分數(shù)據(jù)孔厉,且長度小于一次Read操作所期望讀出的數(shù)據(jù)長度拯钻,那么Read將會成功讀出這部分數(shù)據(jù)并返回帖努,而不是等待所有期望數(shù)據(jù)全部讀取后再返回。
3 Socket中有足夠數(shù)據(jù)
如果socket中有數(shù)據(jù)粪般,且長度大于等于一次Read操作所期望讀出的數(shù)據(jù)長度然磷,那么Read將會成功讀出這部分數(shù)據(jù)并返回。
4 Socket關(guān)閉
如果client端主動關(guān)閉了socket刊驴,那么Server的Read分為“有數(shù)據(jù)關(guān)閉”和“無數(shù)據(jù)關(guān)閉”姿搜。
“有數(shù)據(jù)關(guān)閉”是指在client關(guān)閉時,socket中還有server端未讀取的數(shù)據(jù)捆憎,Read返回“EOF error“舅柜。
“無數(shù)據(jù)關(guān)閉”情形下的結(jié)果,那就是Read直接返回EOF error躲惰。
5 讀取操作超時
有些場合對Read的阻塞時間有嚴格限制致份,在這種情況下,會反復執(zhí)行了多次础拨,沒能出現(xiàn)“讀出部分數(shù)據(jù)且返回超時錯誤”的情況氮块。
服務(wù)端寫的行為特點
1 成功寫
client端在Write時并未判斷Write的返回值。
所謂“成功寫”指的就是Write調(diào)用返回的n與預期要寫入的數(shù)據(jù)長度相等诡宗,且error = nil滔蝉。
2 寫阻塞
TCP連接通信兩端的OS都會為該連接保留數(shù)據(jù)緩沖,一端調(diào)用Write后塔沃,實際上數(shù)據(jù)是寫入到OS的協(xié)議棧的數(shù)據(jù)緩沖的蝠引。
TCP是全雙工通信,因此每個方向都有獨立的數(shù)據(jù)緩沖蛀柴。當
發(fā)送方將對方的接收緩沖區(qū)以及自身的發(fā)送緩沖區(qū)寫滿后螃概,Write就會阻塞。當接收方讀取的時候鸽疾,緩沖區(qū)騰出了空間吊洼,客戶端就又可以寫入了。
3 寫入部分數(shù)據(jù)
Write操作存在寫入部分數(shù)據(jù)的情況制肮,此時服務(wù)端關(guān)閉冒窍,但是寫入的緩沖區(qū)不會阻塞。而是后續(xù)又寫入部分數(shù)據(jù)后發(fā)生了阻塞弄企,程序需要對這部分寫入的部分字節(jié)做特定處理超燃。
4 寫入超時
如果非要給Write增加一個期限,那我們可以調(diào)用SetWriteDeadline
方法拘领。
可以看到在寫入超時時意乓,依舊存在部分數(shù)據(jù)寫入的情況。
Socket屬性
原生Socket API提供了豐富的sockopt設(shè)置接口,但Golang有自己的網(wǎng)絡(luò)架構(gòu)模型届良,golang提供的socket必要的屬性設(shè)置笆凌。
SetKeepAlive 是否開啟長連接
SetKeepAlivePeriod 設(shè)置長連接的周期,超出會斷開
SetLinger 設(shè)定當連接中仍有數(shù)據(jù)等待發(fā)送或接受時的Close方法的行為士葫。
SetNoDelay (默認no delay) 設(shè)定操作系統(tǒng)是否應(yīng)該延遲數(shù)據(jù)包傳遞乞而,以便發(fā)送更少的數(shù)據(jù)包(Nagle's算法)。默認為真慢显,即數(shù)據(jù)應(yīng)該在Write方法后立刻發(fā)送爪模。
SetWriteBuffer 連接的系統(tǒng)發(fā)送緩沖
SetReadBuffer 連接的系統(tǒng)接收緩沖
關(guān)閉連接
由于socket是全雙工的,client和server端在己方已關(guān)閉的socket和對方關(guān)閉的socket上操作的結(jié)果有不同荚藻。
從client的結(jié)果來看屋灌,在己方已經(jīng)關(guān)閉的socket上再進行read和write操作,會得到”use of closed network connection” error应狱;
從server1的結(jié)果來看共郭,在對方關(guān)閉的socket上執(zhí)行read操作會得到EOF error,但write操作會成功疾呻,因為數(shù)據(jù)會成功寫入己方的內(nèi)核socket緩沖區(qū)中除嘹,
即便最終發(fā)不到對方socket緩沖區(qū)了,因為己方socket并未關(guān)閉岸蜗。因此當發(fā)現(xiàn)對方socket關(guān)閉后尉咕,己方應(yīng)該正確合理處理自己的socket,再繼續(xù)write已經(jīng)無任何意義了散吵。
Tcp編程常見問題及解決方法總結(jié)
問題1龙考、粘包問題
解決方法一:TCP提供了強制數(shù)據(jù)立即傳送的操作指令push,TCP軟件收到該操作指令后矾睦,就立即將本段數(shù)據(jù)發(fā)送出去,而不必等待發(fā)送緩沖區(qū)滿炎功;
解決方法二:發(fā)送固定長度的消息
解決方法三:把消息的尺寸與消息一塊發(fā)送
解決方法四:雙方約定每次傳送的大小
解決方法五:雙方約定使用特殊標記來區(qū)分消息間隔
解決方法六:標準協(xié)議按協(xié)議規(guī)則處理枚冗,如Sip協(xié)議
問題2、字符串編碼問題
將中文字符串用utf8編碼格式轉(zhuǎn)換為字節(jié)數(shù)組發(fā)送時蛇损,一個中文字符可能會占用2~4個字節(jié)(假設(shè)為3個字節(jié))赁温,這3個字節(jié)可能分3次接收,接收端每次接收完后用utf8編碼格式轉(zhuǎn)換為字符串淤齐,就會出現(xiàn)亂碼股囊,并導致接收長度計算錯誤的情況。
解決方法一:以字節(jié)數(shù)做為消息長度的計算單位更啄,而不是字符個數(shù)稚疹。
解決方法二:發(fā)送方和接收方都采用unicode編碼格式。
問題3祭务、長連接的蹦诠罚活問題
標準TCP層協(xié)議里把對方超時設(shè)為2小時怪嫌,若服務(wù)器端超過了2小時還沒收到客戶的信息,它就發(fā)送探測報文段柳沙,若發(fā)送了10個探測報文段(每一個相隔75S)還沒有收到響應(yīng)岩灭,就假定客戶出了故障,并終止這個連接赂鲤。因此應(yīng)對tcp長連接進行痹刖叮活。
以下是異步通信時會遇到的問題:
問題4数初、緩沖區(qū)臟數(shù)據(jù)問題
同步發(fā)送的拷貝熄云,是直接拷貝數(shù)據(jù)到基礎(chǔ)系統(tǒng)緩沖區(qū),拷貝完成后返回妙真;
異步發(fā)送消息的拷貝缴允,是將Socket自帶的Buffer空間內(nèi)的所有數(shù)據(jù),拷貝到基礎(chǔ)系統(tǒng)發(fā)送緩沖區(qū)珍德,并立即返回练般;
因此異步發(fā)送時緩沖區(qū)設(shè)置不好會導致接收到臟數(shù)據(jù)的問題,如下所示:
第一次發(fā)送數(shù)據(jù):1234567890
第一次接受數(shù)據(jù):1234567890
第二次發(fā)送數(shù)據(jù):abc
第二次接受數(shù)據(jù):abc4567890
請參考:http://www.cnblogs.com/tianzhiliang/archive/2010/09/08/1821623.html
解決方法一:將緩沖區(qū)的大小設(shè)置為實際發(fā)送數(shù)據(jù)的大小锈候。
問題5薄料、內(nèi)存碎片問題
頻繁的申請緩沖區(qū)會導致內(nèi)存碎片的問題。
解決方法一:使用對象池和內(nèi)存池泵琳。
請參考MSDN:http://msdn.microsoft.com/zh-cn/library/bb517542(v=vs.100).aspx
問題6摄职、亂序問題
多個線程使用異步通信方式向同一個接收端(socket)同時發(fā)送數(shù)據(jù),會導致接收端接收的數(shù)據(jù)混亂获列。如下所示:
線程1第一次發(fā)送:123456789谷市,假設(shè)未發(fā)送完,只發(fā)送了123
線程2第一次發(fā)送:abcdefgh击孩,假設(shè)未發(fā)送完迫悠,只發(fā)送了abc
線程1第二次發(fā)送:456789,發(fā)送完成
線程2第二次發(fā)送:defgh巩梢,發(fā)送完成
接收端最終接收的數(shù)據(jù)為:123abc456789defgh创泄。
解決方法一:一個連接的發(fā)送端線程排隊發(fā)送數(shù)據(jù)。
代碼示例
服務(wù)端:
package main
import (
"fmt"
"net"
"strings"
)
// 讀取數(shù)據(jù)
func handleConnection(conn net.Conn) {
for {
buf := make([]byte, 1024)
if _,err := conn.Read(buf);err == nil {
result := strings.Replace(string(buf),"\n","",1)
fmt.Println(result)
}else{
fmt.Println(err)
}
}
}
func main() {
/*
Listen: 返回在一個本地網(wǎng)絡(luò)地址laddr上監(jiān)聽的Listener括蝠。網(wǎng)絡(luò)類型參數(shù)net必須是面向流的網(wǎng)絡(luò): "tcp"鞠抑、"tcp4"、"tcp6"忌警、"unix"或"unixpacket"搁拙。
*/
listener, err := net.Listen("tcp", "localhost:9999")
if err != nil {
fmt.Println("listen error:", err)
return
}
//todo 限速算法
fmt.Println("server listen success")
for {
//等待客戶端接入
conn, err := listener.Accept()
if err != nil {
fmt.Println("accept error:", err)
break
}
// 使用協(xié)程
go handleConnection(conn)
}
}
客戶端:
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
"strings"
"time"
)
func main() {
//阻塞Dial
/*
Dial:
在網(wǎng)絡(luò)network上連接地址address,并返回一個Conn接口。
可用的網(wǎng)絡(luò)類型有:"tcp"感混、"tcp4"端幼、"tcp6"、"udp"弧满、"udp4"婆跑、"udp6"、"ip"庭呜、"ip4"滑进、"ip6"、"unix"募谎、"unixgram"扶关、"unixpacket"
對TCP和UDP網(wǎng)絡(luò),地址格式是host:port或[host]:port
*/
//conn, err := net.Dial("tcp", "localhost:7777")
//超時
conn, err := net.DialTimeout("tcp", "localhost:9999",time.Second*2)
if err != nil {
log.Println("dial error:", err)
return
}
fmt.Println("client dial success")
inputReader := bufio.NewReader(os.Stdin)
for {
fmt.Println("Please enter a message? 'quit' exit")
//讀取消息
input, _ := inputReader.ReadString('\n')
msg := strings.Trim(input, "\r\n")
//quit 退出
if msg == "quit" {
fmt.Println("quit")
conn.Write([]byte("client quit "))
return
}
_, err = conn.Write([]byte( msg))
}
}