网络编程Socket之TCP之TIME_WAIT状态详解

下面我们用最简单的一对一的客户服务器模型来重现编程中遇到的一些问题:

初学socket的时候在编写socket程序的时候会遇到很多莫名其妙的问题,比如说bind函数返回的常见错误是EADDRINUSE

使用下面的程序重现这个状态:

client:

int main(int argc, const char * argv[])
{

    struct sockaddr_in serverAdd;

    bzero(&serverAdd, sizeof(serverAdd));
    serverAdd.sin_family = AF_INET;
    serverAdd.sin_addr.s_addr = inet_addr(SERV_ADDR);
    serverAdd.sin_port = htons(SERV_PORT);

    int connfd = socket(AF_INET, SOCK_STREAM, 0);

    int connResult = connect(connfd, (struct sockaddr *)&serverAdd, sizeof(serverAdd));
    if (connResult < 0) {
        printf("连接失败\n");
        close(connfd);
        return -1;
    }

    ssize_t writeLen;
    ssize_t readLen;
    char recvMsg[65535] = {0};
    char sendMsg[20] = "I am client";

    writeLen = write(connfd, sendMsg, sizeof(sendMsg));
    if (writeLen < 0) {
        printf("发送失败\n");
        close(connfd);
        return -1;
    }
    else
    {
        printf("发送成功\n");
    }

    while (1) {

//        sleep(1);

        readLen = read(connfd, recvMsg, sizeof(recvMsg));
        if (readLen < 0) {
            printf("读取失败\n");
            close(connfd);
            return -1;
        }
        if (readLen == 0) {
            printf("服务器关闭\n");
            close(connfd);
            return -1;
        }

        printf("server said:%s\n",recvMsg);

    }

    close(connfd);
    return 0;
}

server:

int main(int argc, const char * argv[])
{

    struct sockaddr_in serverAdd;
    struct sockaddr_in clientAdd;

    bzero(&serverAdd, sizeof(serverAdd));
    serverAdd.sin_family = AF_INET;
    serverAdd.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAdd.sin_port = htons(SERV_PORT);

    socklen_t clientAddrLen;

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);

    if (listenfd < 0) {
        printf("创建socket失败\n");
        close(listenfd);
        return -1;
    }

    int bindResult = bind(listenfd, (struct sockaddr *)&serverAdd, sizeof(serverAdd));
    if (bindResult < 0) {
        close(listenfd);
        printf("绑定端口失败,errno = %d\n",errno);
        return -1;
    }
    else
    {
        printf("绑定端口成功\n");
    }

    listen(listenfd, 20);

    int connfd;
    unsigned char recvMsg[65535];
    char replyMsg[20] = "I am server";

    clientAddrLen = sizeof(clientAdd);
    connfd = accept(listenfd,(struct sockaddr *)&clientAdd,&clientAddrLen);
    if (connfd < 0) {
        close(listenfd);
        printf("连接失败\n");
        return -1;
    }
    else
    {
        printf("连接成功\n");
    }

    ssize_t readLen = read(connfd, recvMsg, sizeof(recvMsg));
    printf("readLen:%ld\n",readLen);
    if (readLen < 0) {
        printf("读取失败\n");
        return -1;
    }
    else if (readLen == 0) {
        printf("读取完成\n");
        close(listenfd);
        return 0;
    }
    replyMsg[readLen] = '\0';
    printf("client said:%s\n",replyMsg);

    while (1)
    {
        write(connfd, replyMsg, sizeof(replyMsg));
    }

    close(connfd);

    return 0;
}

首先运行服务器程序,再运行客户端,然后关闭服务端后立马再打开服务端,就会打印如下信息:48对应EADDRINUSE错误码

绑定端口失败,errno
48

这里要说明一个问题:

当一个Unix进程无论自愿的(调用exit或者从main函数返回)还是非自愿的(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也将导致仍然打开的任何TCP连接上发出一个FIN。

很明显服务器已经关闭了,为什么会绑定端口失败呢,下面是TCP连接终止的四个分节:

某些情况下第一个分节的FIN随数据一起发送,另外,第二个和第三个分节有可能被合并成一个分节;

这里我们的服务器是主动关闭的一端,当主动发送FIN分节以后等待确认,状态变为FIN_WAIT_1,收到确认以后状态变为FIN_WAIT_2,收到客户端的FIN分节以后状态变为TIME_WAIT状态,这里在TIME_WAIT状态会停留2MSL后才会进入CLOSED状态;所以我们再立马启动服务器的时候,之前的连接还没有处于CLOSED状态,还存在者,所以就会绑定失败了;后面会讲到为什么会存在TIME_WAIT状态;

这里客户端是被动关闭的一端,收到服务端的FIN之后状态进入CLOSE_WAIT,这个时候read方法会返回0,然后发送对第一个分节的确认,此时客户端调用close方法发送FIN分节给服务端进入LAST_ACK状态,等待确认到达,收到确认以后连接状态变为CLOSED;

TIME_WAIT状态:停留在该状态的持续时间是最长分节生命期(maximum segment lifetime,MSL)的两倍,有时候称为2MSL。MSL是任何IP数据报能够在因特网中存活的最长时间,最大值为255,这是一个跳数限制而不是真正的时间限制;

TIME_WAIT状态存在理由:

可靠地实现TCP全双工连接的终止:可能不得不重传最终那个ack,TIME_WAIT后是CLOSED,如果没有TIME_WAIT的2MSL,直接CLOSED,那么如果最后一个ACK丢失了,是不会重新再发送ACK的,那服务端收不到ACK就会重新发送最终那个FIN,这个时候客户端已经是CLOSED了,就会响应一个RST,这个RST就会被服务器解释成一个错误。

允许来的重复分节在网络中消逝:保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已经在网络中消逝了,从而不会被误解成新连接的分组。

这里还有一种情况:打开客户端while里面的sleep,(或者屏蔽掉客户端下面的代码)然后再先运行服务器程序,再运行客户端,然后关闭服务端后立马再打开服务端,仍然会绑定失败,此时的状态和之前的有点不一样

if (readLen == 0) {
            printf("服务器关闭\n");
            close(connfd);
            return -1;
        }

我们从终端打印信息如下,此时服务器处于FIN_WAIT_2状态,就如上面说的因为客户端还没有关闭连接,没有发送第三个FIN分节,此时客户端因为已经收到来自服务端的FIN分节而处于CLOSE_WAIT状态;

wanglijuntekiMac-mini:~ wanglijun$ netstat -an |grep 8000

tcp4       0      0  192.168.1.103.8000     192.168.1.103.49632    FIN_WAIT_2

tcp4  290960      0  192.168.1.103.49632    192.168.1.103.8000     CLOSE_WAIT

解决方法:

这里有一个SO_REUSEADDR套接字选项,打开之后就能解决如上的问题,我们在band之前添加如下设置代码:

<span style="font-size:12px;">int yes = 1;
    setsockopt(listenfd,
               SOL_SOCKET, SO_REUSEADDR,
               (void *)&yes, sizeof(yes));</span>

服务器重启监听时,试图捆绑现有连接上的端口会失败,(还有一种情况可能之前派生出来的子进程还处理着连接)如果设置了SO_REUSEADDR套接字选项,就会bind成功,所有的TCP服务器都应该指定SO_REUSEADDR套接字选项,以允许服务器在这种情形下被重新启动;SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;

对于TCP,我们绝不可能启动捆绑相同IP地址和相同端口号的多个服务器。

参考:

《UNIX Network ProgrammingVolume 1, Third Edition: TheSockets Networking API》

时间: 2024-08-07 00:18:37

网络编程Socket之TCP之TIME_WAIT状态详解的相关文章

网络编程Socket之TCP之close/shutdown详解(续)

接着上一篇网络编程Socket之TCP之close/shutdown详解 现在我们看看对于不同情况的close的返回情况和可能遇到的一些问题: 1.默认操作的close 说明:我们已经知道write操作返回成功只能说明数据已经发送到套接字的发送缓冲区,不能代表对端已经成功收到数据,close的默认返回成功也只是成功发出了一个FIN分节,也不代表对端已经确认 问题1:如果中途网络发生故障,很有可能服务端接收不到这个来自客户端的FIN分节: 问题2:假设服务器忙,那么来自客户端的数据由TCP加入到套

网络编程Socket之TCP之close/shutdown详解

close: 当套接字的引用计数为0的时候才会引发TCP的四分组连接终止序列: shutdown: 不用管套接字的引用计数就激发TCP的正常连接终止序列: 这里由一个SO_LINGER套接字选项 struct linger { int l_onoff; /* 0 = off, nozero = on */ int l_linger; /* linger time,POSIX specifies units as seconds */ }; shutdown:SHUT_RD 关闭连接的读这一半,进

网络编程Socket它TCP它TIME_WAIT国家具体解释

下面我们用最简单的一对一的客户server编程模型重现遇到的一些问题: 初学者socket当写作socket名其妙的问题.比方说bind函数返回的常见错误是EADDRINUSE 使用以下的程序重现这个状态: client: int main(int argc, const char * argv[]) { struct sockaddr_in serverAdd; bzero(&serverAdd, sizeof(serverAdd)); serverAdd.sin_family = AF_IN

java网络编程socket\server\TCP笔记(转)

java网络编程socket\server\TCP笔记(转) 2012-12-14 08:30:04|  分类: Socket |  标签:java  |举报|字号 订阅 1 TCP的开销 a  连接协商三次握手,c->syn->s,s->syn ack->c, c->ack->s b  关闭协商四次握手,c->fin->s, s->ack-c,s->fin->c,c->ack->s c  保持数据有序,响应确认等计算开销 d

TCP/IP中TIME_WAIT状态详解

TIME_WAIT状态原理: 通信双方建立连接后,主动关闭连接的一方就会进入TIME_WAIT状态. 客户端主动关闭连接时,会发送最后一个ACK确认,然后就会进入TIME_WAIT状态,再停留2MSL,就会进入CLOSED状态. 接下来我们看一张图,来说明这一过程: 上图是TCP"四次挥手"的过程,相信你们都会很了解,下面我们来说说为什么要存在TIME_WAIT状态 TIME_WAIT状态存在的理由如下: (1)可靠地实现TCP全双工连接的终止 TCP协议在关闭连接的四次挥手中,最终的

TCP连接的状态详解以及故障排查

转载自CSDN博客:http://blog.csdn.net/hguisu/article/details/38700899 TCP状态 TCP状态迁移路线图 TCP连接建立三次握手 TCP连接的终止四次握手释放 同时打开 同时关闭 TCP通信中服务器处理客户端意外断开 Linux错误信息errno列表 我们通过了解TCP各个状态,可以排除和定位网络或系统故障时大有帮助.(总结网络上的内容) 1.TCP状态 了解TCP之前,先了解几个命令:   linux查看tcp的状态命令: 1).netst

转载:TCP连接的状态详解以及故障排查

FROM:http://blog.csdn.net/hguisu/article/details/38700899 该博文的条理清晰,步骤明确,故复制到这个博文中收藏,若文章作者看到且觉得不能装载,麻烦请告知,谢谢. 我们通过了解TCP各个状态,可以排除和定位网络或系统故障时大有帮助.(总结网络上的内容) 1.TCP状态 linux查看tcp的状态命令: 1).netstat -nat  查看TCP各个状态的数量 2).lsof  -i:port  可以检测到打开套接字的状况 3).  sar

iOS开发——网络编程Swift篇&amp;(八)SwiftyJSON详解

SwiftyJSON详解 最近看了一些网络请求的例子,发现Swift在解析JSON数据时特别别扭,总是要写一大堆的downcast(as?)和可选(Optional),看?号都看花了.随后发现了这个库SwiftyJSON,问题迎刃而解,灰常优雅和Swifty! 简单介绍下这个库(内容译自SwiftyJSON的README): 为什么典型的在Swift中处理JSON的方法不好? Swift语言是一种严格的类型安全语言,它要求我们显示的设置类型,并帮助我们写出更少bug的代码.但是当处理JSON这种

网络编程Socket之TCP之connect详解

对TCP套接字调用connect会激发三次握手,如下: 客户端是主动打开连接的一端,会发送第一个SYN分节,然后等待确认,此时连接状态为SYN_SENT,当收到服务端的确认后连接建立,状态变为ESTABLISHED: 服务器是被动打开连接的一端,调用listen导致套接字从CLOSED状态变为LISTEN状态,当收到来自客户端的SYN分节以后状态变为SYN_RCVD,然后发送第二个SYN分节,等待客户端的确认,收到客户端的确认以后连接建立,状态变为ESTABLISHED: 三次握手中的两个SYN