#淺析TCP連接過程中server異常處理
基礎環境:騰訊雲ubuntu虛擬機
前置基礎:對TCP連接有一個基本認識,能寫進行簡單socket編程
先簡單介紹一下TCP編程流程
TCP簡易編程流程
1.TCP服務器端編程流程如下:
- 創建套接字socket;
- 綁定套接字bind;
- 設置套接字爲監聽模式,進入被動接受連接狀態listen;
- 接受請求,建立連接accpet;
- 讀寫數據read/write;
- 終止連接close。
2.TCP客戶端編程流程如下:
- 創建套接字socket;
- 與遠程服務器建立連接connect;
- 讀寫數據read/write;
- 終止連接close。
使用GO的示例代碼
這個是最簡單的server client模式:
server是Listen、Accept、Read/Write;client是Dial、Read/Write
client的Dial即包含了自動創建一個socket並connect的操作)
package main
import (
"fmt"
"net"
"os"
)
func checkError(err error){
if err != nil {
fmt.Println("Error: %s", err.Error())
os.Exit(1)
}
}
func recvConnMsg(conn net.Conn) {
// var buf [50]byte
buf := make([]byte, 50)
defer conn.Close()
for {
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn closed")
return
}
//fmt.Println("recv msg:", buf[0:n])
fmt.Println("recv msg:", string(buf[0:n]))
}
}
func main() {
listen_sock, err := net.Listen("tcp", "localhost:10000")
checkError(err)
defer listen_sock.Close()
for {
new_conn, err := listen_sock.Accept()
if err != nil {
continue
}
go recvConnMsg(new_conn)
}
}
package main
import (
"fmt"
"net"
"os"
)
func checkError(err error){
if err != nil {
fmt.Println("Error: %s", err.Error())
os.Exit(1)
}
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:10000")
checkError(err)
defer conn.Close()
conn.Write([]byte("Hello world!"))
fmt.Println("send msg")
}
這是一個很簡單的hello world程序,server監聽本地10000端口,client連接本地10000端口,然後發送hello world消息,並結束進程。
server異常場景
TCPserver主要有三種異常場景,分別爲服務器主機崩潰、服務器主機崩潰後重啓、服務器主機關機。
1.服務器進程退出/服務器主機關機/
服務器進程退出,會關閉對應文件描述符,向連接上的客戶端發送FIN消息,通知其關閉連接。且此時消息無法發送出去。
client稍作修改
package main
import (
"fmt"
"net"
"os"
"time"
)
func checkError(err error){
if err != nil {
fmt.Println("Error: %s", err.Error())
os.Exit(1)
}
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:10000")
checkError(err)
defer conn.Close()
for {
time.Sleep(1000 * time.Millisecond)
ret, err := conn.Write([]byte("Hello world!"))
fmt.Println("send msg len is %d", ret)
if err != nil {
fmt.Println(err)
}
}
}
串口輸出,在client連接好之後關閉server,這時候發現它你的消息發不出去,且會給你返回broken pipe的錯誤
feiqianyousadeMacBook-Pro:go yousa$ go run client.go
send msg len is %d 12
send msg len is %d 12
send msg len is %d 12
send msg len is %d 12
send msg len is %d 12
send msg len is %d 12
send msg len is %d 0
write tcp 127.0.0.1:52732->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52732->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52732->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52732->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52732->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
那麼再來看收,在連接建立好之後,server退出,client這邊會收到什麼呢?
修改下client
package main
import (
"fmt"
"net"
"os"
)
func checkError(err error){
if err != nil {
fmt.Println("Error: %s", err.Error())
os.Exit(1)
}
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:10000")
buf := make([]byte, 50)
checkError(err)
defer conn.Close()
/*time.Sleep(1000 * time.Millisecond)
ret, err := conn.Write([]byte("Hello world!")) */
n, err := conn.Read(buf)
fmt.Println("recv msg is %s\n recv msg len is %d", buf, n)
if err != nil {
fmt.Println(err)
}
}
串口輸出,在client連接好之後關閉server
feiqianyousadeMacBook-Pro:go yousa$ go run client.go
recv msg is %s
recv msg len is %d [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] 0
EOF
可以看到收到的消息是空,收到的消息長度爲0,且收到EOF連接斷開的錯誤
服務器關機,這裏一般是指正常關機,系統會向各個進程發送SIGTERM和SIGKILL信號,server進程會退出,然後關閉相應文件描述符,給對應client發送FIN消息。
2.服務器進程退出後重啓/服務器主機崩潰後重啓
依然使用上述的測試進程。
啓動server,打開client,關閉server,打開server
server側打印
feiqianyousadeMacBook-Pro:go yousa$ ./server
recv msg: Hello world!
recv msg: Hello world!
recv msg: Hello world!
^C
#重啓server
feiqianyousadeMacBook-Pro:go yousa$ ./server
client側打印
feiqianyousadeMacBook-Pro:go yousa$ go run client.go
send msg len is %d 12
send msg len is %d 12
send msg len is %d 12
send msg len is %d 12
#server進行重啓
send msg len is %d 0
write tcp 127.0.0.1:52934->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52934->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52934->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52934->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52934->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52934->127.0.0.1:10000: write: broken pipe
send msg len is %d 0
write tcp 127.0.0.1:52934->127.0.0.1:10000: write: broken pipe
^Csignal: interrupt
feiqianyousadeMacBook-Pro:go yousa$
可以簡單得出兩點:
- 在服務器進程重啓的情況下如果客戶在主機崩潰重啓前不主動發送數據,那麼客戶是不會知道服務器已崩潰。
- 在服務器退出後,就算重啓,不進行設置直接使用原有套接字發送消息是無法發送成功的。
如果客戶對服務器的崩潰情況很關心,即使客戶不主動發送數據也這樣,這需要進行相關設置(如設置套接口選項SO_KEEPALIVE或要依賴於某些客戶/服務器心跳函數)。
3.服務進程崩潰
此時客戶端發出數據後,會一直阻塞在套接字的讀取響應。但是由於服務器主機已崩潰,TCP客戶端會持續重傳數據分節,試圖從服務器接收一個ACK[一般重傳12次(源自Berkeley的實現)]後,客戶TCP最終選擇放棄,返回給應用經常一個ETIMEDOUT錯誤;或者是因爲中間路由器判定服務器主機不可達,則返回一個目的地不可達的ICMP消息響應,其錯誤代碼爲EHOSTUNREACH或ENETUNREACH。
參考
簡單代碼參考go tcp server-client