#浅析TCP连接过程中server异常处理
基础环境:腾讯云ubuntu虚拟机
前置基础:对TCP连接有一个基本认识,能写进行简单socket编程
先简单介绍一下TCP编程流程
这个是最简单的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消息,并结束进程。
TCPserver主要有三种异常场景,分别为服务器主机崩溃、服务器主机崩溃后重启、服务器主机关机。
服务器进程退出,会关闭对应文件描述符,向连接上的客户端发送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消息。
依然使用上述的测试进程。
启动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或要依赖于某些客户/服务器心跳函数)。
此时客户端发出数据后,会一直阻塞在套接字的读取响应。但是由于服务器主机已崩溃,TCP客户端会持续重传数据分节,试图从服务器接收一个ACK[一般重传12次(源自Berkeley的实现)]后,客户TCP最终选择放弃,返回给应用经常一个ETIMEDOUT错误;或者是因为中间路由器判定服务器主机不可达,则返回一个目的地不可达的ICMP消息响应,其错误代码为EHOSTUNREACH或ENETUNREACH。
简单代码参考go tcp server-client
http://blog.csdn.net/qq_15437667/article/details/51042366