go socket close那些事

温故而知新,可以为师矣

基于目前socket在编程中的重要地位,我打算仔细琢磨一下go的socket编程。打造一个属于自己的方便调用的go socket框架。因为毕竟自己有c的socket经验,所以我觉得应该没什么难度。

一路下来也确实没有碰到什么问题,在此就大致记录下编码过程以及新体会吧。


编写go客户端:
  1. 获取服务器ip地址
    ip, err := net.ResolveIPAddr("", "127.0.0.1")
    if err != nil {
      Log("err1=%v\n", err.Error())  
      return
    }
  1. 建立连接
    ipStr := fmt.Sprintf("%s:20003", ip.IP.String())
    conn, err := net.Dial("tcp", ipStr)
    if err != nil {
        Log("err2=%v\n", err.Error())
        return
    }
  1. 数据处理
    defer func() {
        err := conn.Close()
        if err != nil {
            Log("err3=%v\n", err.Error())
            return
        }
    }()

    Log("connected ...\n")
    hello := "Hi,here is glp"
    n, err := conn.Write([]byte(hello))
    if err != nil {
        Log("err4=%v\n", err.Error())
        return
    }
    Log("send server [%d]=%s\n", n, hello)

    <-time.After(time.Second)
编写go服务端:
  1. 首先获取到一个监听的IP地址:
//获取服务器监听ip地址
    ip, err := net.ResolveTCPAddr("", ":20003")
    if err != nil {
        Log("err1=%v\n", err.Error())
        return
    }
  1. 创建监听的socket
//创建一个监听的socket
conn, err := net.ListenTCP("tcp", ip)
if err != nil {
    Log("err2=%v\n", err.Error())
    return
}
  1. 服务器逻辑主循环
clientConn, err := conn.Accept()
if err != nil {
    Log("err3=%v\n", err.Error())
    return
}
Log("new client conn=%v,ip=%v\n", clientConn, clientConn.RemoteAddr())
go handlerClient(&StatusConn{clientConn, StatusNormal})
  1. 对客户端socket的处理逻辑主要片段
buffer := make([]byte, 1024)
err := conn.SetReadDeadline(time.Now().Add(time.Second * 10))
if err != nil {
    Log("handlerClient err2=%v\n", err.Error())
    conn.Status = StatusError
    return
}
n, err := conn.Read(buffer)
if err != nil {
    Log("handlerClient err3=%v\n", err.Error())
    if err == io.EOF {
        conn.Status = StatusClosed
    } else {
        conn.Status = StatusError
    }
    return
}

clientBytes := buffer[:n]
Log("read client [%d]=%s\n", n, string(clientBytes))
hello := fmt.Sprintf("from server back【go】:%s", string(clientBytes))

n, err = conn.Write([]byte(hello))
Log("send client [%d]=%s\n", n, hello)
if err != nil {
    Log("handlerClient err4=%v\n", err.Error())
}
实验

直接启动服务器跟客户端,发现打印如下:
客户端:

2019/05/30 16:35:44.255:connected ...
2019/05/30 16:35:44.269:send server [14]=Hi,here is glp

服务端:

2019/05/30 16:35:39.543:cur goos=windows
2019/05/30 16:35:39.560:server wait...
2019/05/30 16:35:44.255:new client conn=&{{0xc0000862c0}},ip=127.0.0.1:56795
2019/05/30 16:35:44.269:read client [14]=Hi,here is glp
2019/05/30 16:35:44.269:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 16:35:45.269:handlerClient err3=read tcp 127.0.0.1:20003->127.0.0.1:56795: wsarecv: An existing connection was forcibly closed by the remote host.
2019/05/30 16:35:45.269:handlerClient Status=3

是这个报错内容:An existing connection was forcibly closed by the remote host.
意思应该是连接被远端强制关闭,诶。。这个跟io.EOF到底有什么区别呢。我记得我在编写c++ socket的时候好像没碰到这个。印象中强制关闭的判断应该就是判断recv(...)==0。带着这个好奇,我于是编写了c++ socket服务器。

c++服务器 主要逻辑代码:
  1. 服务器主循环
int main() {
    Env env = Env();

    SOCKET listenFd = -1;
    listenFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//socket 描述符
    if (INVALID_SOCKET == listenFd)
    {
        Printf("create socket error %d\n", errno);
        return -1;
    }
    Printf("服务器创建网络描述符 fd=%d\n", listenFd);

    if (setAddrReuse(listenFd) != 0) {
        Printf("setAddrReuse error %d\n", errno);
        return -1;
    }
    Printf("服务器设置地址重用成功\n");

    struct sockaddr_in ipAddr = { 0 };
    ipAddr.sin_family = AF_INET;
    ipAddr.sin_port = htons(20003);
    ipAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (0 != bind(listenFd, (struct sockaddr*) & ipAddr, sizeof(ipAddr)))
    {
        Printf("socket bind error %d\n", errno);
        return -1;
    }
    Printf("服务器绑定地址成功\n");

    if (::listen(listenFd, SOMAXCONN) != 0) {
        Printf("socket listen error %d\n", errno);
        return -1;
    }
    Printf("服务器监听成功\n");

    sockaddr addr;
#ifdef WIN32
    int len = sizeof(addr);
#else
    socklen_t len = sizeof(addr);
#endif
    while (true) {
        SOCKET clientSocket = accept(listenFd, &addr, &len);
        if (clientSocket == INVALID_SOCKET) {
            Printf("socket accept error %d\n", errno);
            return -1;
        }

        Printf("客户端新连接:接入 fd = %d,ip = %s\n", clientSocket, GetPeerIp(clientSocket));
        auto clientThread = std::thread(handlerClient, clientSocket);
        clientThread.detach();
    }
}
  1. 客户端连接处理片段
        char buf[65535] = { 0 };
        int len = (int) ::recv(fd, buf, sizeof(buf), 0);//接收网络数据
        if (len == 0) {
            Printf("客户端连接被主动关闭\n");
            return;
        }

        if (len == SOCKET_ERROR) {
            int errorcode;
#ifdef WIN32
            errorcode = GetLastError();//这里的错误码,必须用windows的GetLastError获取。
            if (errorcode != WSAEWOULDBLOCK) {
#else//Linux
            errorcode = errno;
            if (errorcode != EAGAIN) {
#endif
                Printf("client[%d] recv err=%d\n", fd, errorcode);
                return;
            }
            continue;
        }

        Printf("client[%d] recv[%d]=%s\n", fd, len, buf);
        char hello[65535] = { 0 };
        sprintf(hello, "from server back[c++]:%s", buf);
        Printf("send to client[%d]=%s\n", fd, hello);
        ::send(fd, hello, strlen(hello), 0);
实验2,用go客户端连接这个c++服务器看打印结果

客户端打印

2019/05/30 16:30:30.380:connected ...
2019/05/30 16:30:30.393:send server [14]=Hi,here is glp

c++服务器打印

2019-05-30 16:30:20.499:服务器创建网络描述符 fd=636
2019-05-30 16:30:20.499:服务器设置地址重用成功
2019-05-30 16:30:20.499:服务器绑定地址成功
2019-05-30 16:30:20.499:服务器监听成功
2019-05-30 16:30:30.381:客户端新连接:接入 fd = 640,ip = 127.0.0.1
2019-05-30 16:30:30.393:client[640] recv[14]=Hi,here is glp
2019-05-30 16:30:30.393:send to client[640]=from server back[c++]:Hi,here is glp
2019-05-30 16:30:31.393:client[640] recv err=10054

特地去查了下10054:

//
// MessageId: WSAECONNRESET
//
// MessageText:
//
// An existing connection was forcibly closed by the remote host.
//
#define WSAECONNRESET                    10054L

发现跟go服务器打印的错误信息一致。。。
c++的recv函数在这里也确实返回了-1,看来并不是所有的客户端socket close,都可以让服务器的recv得到0。看来是我自己以前没怎么关注这块只顾写逻辑去了。。go的底层socket代码应该是跟c++保持一致的。

实验3 linux服务器

上面的服务器我都是在本地windows运行的,现在尝试在linux上看看。
我把编写的go服务器代码编译发布到服务器上再看服务器打印日志:

2019/05/30 16:45:00.012:cur goos=linux
2019/05/30 16:45:00.012:server wait...
2019/05/30 16:46:36.104:new client conn=&{{0xc00009a080}},ip=115.192.79.216:56919
2019/05/30 16:46:36.132:read client [14]=Hi,here is glp
2019/05/30 16:46:36.132:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 16:46:37.133:handlerClient err3=read tcp 172.16.200.43:20003->115.192.79.216:56919: read: connection reset by peer
2019/05/30 16:46:37.133:handlerClient Status=3

到了linux这边就变成了connection reset by peer.
可以看到windows和linux返回的错误提示是不同的。不过他们想表达的意思应该是一致的。

实验4 尝试读取服务器发来的数据再关闭socket

上面的go客户端是没有读取服务器返回的数据,自己等待一秒钟就关闭了。现在把客户端逻辑改成:

    Log("connected ...\n")
    hello := "Hi,here is glp"
    n, err := conn.Write([]byte(hello))
    if err != nil {
        Log("err4=%v\n", err.Error())
        return
    }
    Log("send server [%d]=%s\n", n, hello)
//新增读取服务器数据代码 begin
    buffer := make([]byte, 1024)
    n, err = conn.Read(buffer)
    if err != nil {
        Log("err5=%v\n", err.Error())
        return
    }
    Log("read server [%d]=%s\n", n, string(buffer))
//新增读取服务器数据代码 end

    <-time.After(time.Second)

再去请求go服务器和c++服务器
go服务器打印(linux):

2019/05/30 17:02:28.809:cur goos=linux
2019/05/30 17:02:28.809:server wait...
2019/05/30 17:02:43.239:new client conn=&{{0xc00009a080}},ip=115.192.79.216:57079
2019/05/30 17:02:43.253:read client [14]=Hi,here is glp
2019/05/30 17:02:43.253:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:02:44.259:handlerClient err3=EOF
2019/05/30 17:02:44.259:handlerClient Status=1

go服务器打印(windows):

2019/05/30 17:03:21.963:cur goos=windows
2019/05/30 17:03:21.976:server wait...
2019/05/30 17:03:31.903:new client conn=&{{0xc00008c2c0}},ip=127.0.0.1:57090
2019/05/30 17:03:31.922:read client [14]=Hi,here is glp
2019/05/30 17:03:31.922:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:03:32.923:handlerClient err3=EOF
2019/05/30 17:03:32.923:handlerClient Status=1

c++服务器打印(windows):

2019-05-30 17:06:08.737:服务器创建网络描述符 fd=644
2019-05-30 17:06:08.737:服务器设置地址重用成功
2019-05-30 17:06:08.737:服务器绑定地址成功
2019-05-30 17:06:08.737:服务器监听成功
2019-05-30 17:06:29.437:客户端新连接:接入 fd = 604,ip = 127.0.0.1
2019-05-30 17:06:29.449:client[604] recv[14]=Hi,here is glp
2019-05-30 17:06:29.450:send to client[604]=from server back[c++]:Hi,here is glp
2019-05-30 17:06:30.451:客户端连接被主动关闭

经过这步实验证明确实是因为客户端没有读取服务器发来的数据就关闭导致服务器的read或者recv报错了

经过调试我发现我可以通过如下步骤找到对应的error

if err1, ok := err.(*net.OpError); ok {
    if err2, ok := err1.Err.(*os.SyscallError); ok {
        if err3, ok := err2.Err.(syscall.Errno); ok {
            if err3 == syscall.WSAECONNRESET {
                Log("err3 == syscall.WSAECONNRESET True\n")
            }
        }
    }
}

go定义的Errno

但是syscall.WSAECONNRESET不能用于linux,在编译linux的时候会报错:

src\study\testServer.go:68:18: undefined: syscall.WSAECONNRESET

如果想要把没有读取数据的强制关闭也要归结为close而不是error的时候,用上面的办法只能用于windows。而我需要跨平台解决方案
在提出最后的解决方案之前,我们还有一个实验需要做一下就是:

socket 10053

实验5 socket出现10053的原因

改写我们的go客户端:

Log("connected ...\n")
hello := "Hi,here is glp"
n, err := conn.Write([]byte(hello))
if err != nil {
    Log("err4=%v\n", err.Error())
    return
}
Log("send server [%d]=%s\n", n, hello)

//buffer := make([]byte, 1024)
//n, err = conn.Read(buffer)
//if err != nil {
//  Log("err5=%v\n", err.Error())
//  return
//}
//Log("read server [%d]=%s\n", n, string(buffer))
//
//<-time.After(time.Second)

把读取和等待的代码都注释了,查看服务器打印:
go服务器打印(linux):

2019/05/30 17:26:09.186:cur goos=linux
2019/05/30 17:26:09.187:server wait...
2019/05/30 17:26:12.750:new client conn=&{{0xc0000b4080}},ip=115.192.79.216:57420
2019/05/30 17:26:12.760:read client [14]=Hi,here is glp
2019/05/30 17:26:12.760:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:26:12.760:handlerClient err3=EOF
2019/05/30 17:26:12.760:handlerClient Status=1

go服务器打印(windows):

2019/05/30 17:25:42.506:cur goos=windows
2019/05/30 17:25:42.520:server wait...
2019/05/30 17:25:48.386:new client conn=&{{0xc00008c2c0}},ip=127.0.0.1:57409
2019/05/30 17:25:48.400:read client [14]=Hi,here is glp
2019/05/30 17:25:48.400:send client [39]=from server back【go】:Hi,here is glp
2019/05/30 17:25:48.400:handlerClient err3=read tcp 127.0.0.1:20003->127.0.0.1:57409: wsarecv: An established connection was aborted by the software in your host machine.
2019/05/30 17:25:48.400:handlerClient Status=3

c++服务器打印(windows):

2019-05-30 17:26:23.847:服务器创建网络描述符 fd=628
2019-05-30 17:26:23.847:服务器设置地址重用成功
2019-05-30 17:26:23.847:服务器绑定地址成功
2019-05-30 17:26:23.848:服务器监听成功
2019-05-30 17:26:53.961:客户端新连接:接入 fd = 640,ip = 127.0.0.1
2019-05-30 17:26:53.973:client[640] recv[14]=Hi,here is glp
2019-05-30 17:26:53.973:send to client[640]=from server back[c++]:Hi,here is glp
2019-05-30 17:26:53.973:client[640] recv err=10053

笔者多实验了几次,windows偶尔会出现10054,但是以10053居多。而linux一直是EOF。
看来这个错误码只出现在windows平台。情况是:服务器向客户端发送数据的时候客户端已经close了。
Linux应该把这种情况当EOF处理了。

解决方案:屏蔽强制关闭的错误

有时候我们服务器需要对socket的错误的原因进行分析,但是像这种客户端直接close的,我们就不需要分析了,最好在日志层面就把它过滤掉。
但是我们又不能像上面提到的直接用常量。这样我们就没办法编译跨平台的服务器,至少要去改代码才行。而笔者想要的就是不动源码的基础上分辨是否是强制关闭。考虑良久,虽然不是很好但是比较实用的go代码:

n, err := conn.Read(buffer)
if err != nil {
    Log("handlerClient err3=%v\n", err.Error())
    errStr := err.Error()
    err1 := err.(*net.OpError)
    if err == io.EOF || (runtime.GOOS == "windows" &&
        strings.Contains(errStr, "An existing connection was forcibly closed by the remote host") ||
        strings.Contains(errStr, "An established connection was aborted by the software in your host machine")) ||
        strings.Contains(errStr, "connection reset by peer") {
        /*
            1.io.EOF
                正常关闭.指客户端读完服务器发送的数据然后close

            2.
            connection reset by peer(linux)
            An existing connection was forcibly closed by the remote host(windows)
                表示客户端 【没有读取/读取部分】就close

            3.An established connection was aborted by the software in your host machine(windows)
                表示服务器发送数据,客户端已经close,这个经过测试只有在windows上才会出现。linux试了很多遍都是返回io.EOF错误
                解决办法就是客户端发送数据的时候需要wait一下,然后再close,这样close的结果就是2了
        */
        conn.Status = StatusClosed
    } else if err1 != nil && err1.Timeout() {
        conn.Status = StatusTimeout
    } else {
        conn.Status = StatusError
    }
    return
}

go服务器完整代码:

package main

import (
    "fmt"
    "io"
    "net"
    "runtime"
    "strings"
    "time"
)

const (
    TimeFmt = "2006/01/02 15:04:05.000" //毫秒保留3位有效数字
)

func Log(format string, args ...interface{}) {
    fmt.Printf(fmt.Sprintf("%s:%s", time.Now().Format(TimeFmt), format), args...)
}

type StatusNO int32

const (
    StatusNormal  StatusNO = iota //0 正常
    StatusClosed                  //1 对方主动关闭
    StatusTimeout                 //2 超时连接断开
    StatusError                   //3 其他异常断开
)

type StatusConn struct {
    net.Conn
    //StatusNormal
    Status StatusNO
}

func handlerClient(conn *StatusConn) {
    defer func() {
        Log("handlerClient Status=%v\n", conn.Status)
        err := conn.Close()
        if err != nil {
            Log("handlerClient err1=%v\n", err)
        }
    }()

    for {
        buffer := make([]byte, 1024)
        err := conn.SetReadDeadline(time.Now().Add(time.Second * 10))
        if err != nil {
            Log("handlerClient err2=%v\n", err.Error())
            conn.Status = StatusError
            return
        }
        n, err := conn.Read(buffer)
        if err != nil {
            Log("handlerClient err3=%v\n", err.Error())
            errStr := err.Error()
            err1 := err.(*net.OpError)
            if err == io.EOF || (runtime.GOOS == "windows" &&
                strings.Contains(errStr, "An existing connection was forcibly closed by the remote host") ||
                strings.Contains(errStr, "An established connection was aborted by the software in your host machine")) ||
                strings.Contains(errStr, "connection reset by peer") {
                /*
                    1.io.EOF
                        正常关闭.指客户端读完服务器发送的数据然后close

                    2.
                    connection reset by peer(linux)
                    An existing connection was forcibly closed by the remote host(windows)
                        表示客户端 【没有读取/读取部分】就close

                    3.An established connection was aborted by the software in your host machine(windows)
                        表示服务器发送数据,客户端已经close,这个经过测试只有在windows上才会出现。linux试了很多遍都是返回io.EOF错误
                        解决办法就是客户端发送数据的时候需要wait一下,然后再close,这样close的结果就是2了
                */
                conn.Status = StatusClosed
            } else if err1 != nil && err1.Timeout() {
                conn.Status = StatusTimeout
            } else {
                conn.Status = StatusError
            }
            return
        }

        clientBytes := buffer[:n]
        Log("read client [%d]=%s\n", n, string(clientBytes))
        hello := fmt.Sprintf("from server back【go】:%s", string(clientBytes))

        n, err = conn.Write([]byte(hello))
        Log("send client [%d]=%s\n", n, hello)
        if err != nil {
            Log("handlerClient err4=%v\n", err.Error())
        }

    }
}

func main() {
    //服务器例子
    Log("cur goos=%s\n", runtime.GOOS)

    //获取服务器监听ip地址
    ip, err := net.ResolveTCPAddr("", ":20003")
    if err != nil {
        Log("err1=%v\n", err.Error())
        return
    }

    //创建一个监听的socket
    conn, err := net.ListenTCP("tcp", ip)
    if err != nil {
        Log("err2=%v\n", err.Error())
        return
    }

    Log("server wait...\n")
    for {
        clientConn, err := conn.Accept()
        if err != nil {
            Log("err3=%v\n", err.Error())
            return
        }
        Log("new client conn=%v,ip=%v\n", clientConn, clientConn.RemoteAddr())
        go handlerClient(&StatusConn{clientConn, StatusNormal})
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350