网络编程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_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("绑定port失败,errno = %d\n",errno);
        return -1;
    }
    else
    {
        printf("绑定port成功\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;
    }
    
    printf("client said:%s\n",recvMsg);
       
    while (1)
    {       
        write(connfd, replyMsg, sizeof(replyMsg));      
    }
    
    close(connfd);

    return 0;
}


首先执行server程序,再执行client。然后关闭服务端后立刻再打开服务端,就会打印例如以下信息:48相应EADDRINUSE错误码

绑定port失败,errno 48


这里要说明一个问题:

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


非常明显server已经关闭了。为什么会绑定port失败呢。以下是TCP连接终止的四个分节




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


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

这里client是被动关闭的一端,收到服务端的FIN之后状态进入CLOSE_WAIT,这个时候read方法会返回0,然后发送对第一个分节的确认。此时client调用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,这个时候client已经是CLOSED了,就会响应一个RST,这个RST就会被server解释成一个错误。

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


这里另一种情况:打开clientwhile里面的sleep,(或者屏蔽掉client以下的代码)然后再先执行server程序。再执行client,然后关闭服务端后立刻再打开服务端,仍然会绑定失败,此时的状态和之前的有点不一样

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

我们从终端信息打印例如以下,此时server处于FIN_WAIT_2状态,就如上面说的由于client还没有关闭连接,没有发送第三个FIN分节,此时client由于已经收到来自服务端的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>

server重新启动监听时,试图捆绑现有连接上的port会失败,(另一种情况可能之前派生出来的子进程还处理着连接)假设设置了SO_REUSEADDR套接字选项。就会bind成功,全部的TCPserver都应该指定SO_REUSEADDR套接字选项,以同意server在这样的情形下被又一次启动;SO_REUSEADDR同意在同一port上启动同一server的多个实例,仅仅要每一个实例捆绑一个不同的本地IP地址就可以。


对于TCP,我们绝不可能启动捆绑同样IP地址和同样port号的多个server。


參考:

UNIX Network ProgrammingVolume 1, Third Edition: TheSockets Networking API


版权声明:本文博客原创文章,博客,未经同意,不得转载。

posted on 2015-07-30 10:50  gcczhongduan  阅读(200)  评论(0编辑  收藏  举报