浅析TCP连接过程中server异常情况处理

#浅析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$

可以简单得出两点:

  1. 在服务器进程重启的情况下如果客户在主机崩溃重启前不主动发送数据,那么客户是不会知道服务器已崩溃。
  2. 在服务器退出后,就算重启,不进行设置直接使用原有套接字发送消息是无法发送成功的。

如果客户对服务器的崩溃情况很关心,即使客户不主动发送数据也这样,这需要进行相关设置(如设置套接口选项SO_KEEPALIVE或要依赖于某些客户/服务器心跳函数)。

3.服务进程崩溃

此时客户端发出数据后,会一直阻塞在套接字的读取响应。但是由于服务器主机已崩溃,TCP客户端会持续重传数据分节,试图从服务器接收一个ACK[一般重传12次(源自Berkeley的实现)]后,客户TCP最终选择放弃,返回给应用经常一个ETIMEDOUT错误;或者是因为中间路由器判定服务器主机不可达,则返回一个目的地不可达的ICMP消息响应,其错误代码为EHOSTUNREACH或ENETUNREACH。

参考

简单代码参考go tcp server-client

http://blog.csdn.net/qq_15437667/article/details/51042366

猜你喜欢

转载自blog.csdn.net/qq_15437667/article/details/70667145