【问题标题】:Misbehaving TCP client implementation行为不端的 TCP 客户端实现
【发布时间】:2012-10-24 02:43:58
【问题描述】:

我用 C 语言编写了一个简单的应用程序,它派生出一个孩子充当网络服务器,并派出许多孩子充当网络客户端。客户端连接到服务器,并请求数据。代码如下:

#include <stdio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/queue.h>
#include <strings.h>
#include <sys/wait.h>


#define LISTENSOCKET 1519
#define MAXCLIENT 200
#define MAXLINE 80


void client_do_something(int c) 
{
    printf("Process %d, server gave %d\n", getpid(), c);
}


void client_body()
{
    struct sockaddr_in clientaddr;
    struct sockaddr_in localaddr;
    int sockfd;
    int nread;
    int s;
    char serveraddr[20] = "127.0.0.1";
    char laddr[20];
    char command[4] = "GET";
    int c;
    command[3] = '\0';
    socklen_t len;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    clientaddr.sin_family = AF_INET;
    clientaddr.sin_port = htons(LISTENSOCKET);
    inet_pton(AF_INET, serveraddr, &clientaddr.sin_addr);
    printf("Client %d started\n", getpid());
    if ((s=connect(sockfd, (struct sockaddr *) &clientaddr, sizeof(clientaddr))) != 0) {
        perror("conn err:");
        printf("Connect error: pid %d %d\n", getpid(), errno);
        close(sockfd);
        sleep(1);
        exit(1);
    }
    getsockname(sockfd, (struct sockaddr *) &localaddr, &len);
    inet_ntop(AF_INET,  &localaddr.sin_addr, laddr, sizeof(laddr));
    printf("Client %d passed connect (%d), %s:%d \n", getpid(), s, laddr,     ntohs(localaddr.sin_port));
    while(1) {
        send(sockfd, command, 4, 0);
        if ( (nread = recv(sockfd, &c, 4, 0)) < 0 ) {
            if (errno == ENOTCONN) {
                sleep(1);
                continue;
            }
            perror("client recv err:");
            printf("Client %d received error %d ", getpid(), errno);
            exit(1);
        } else if (nread == 0) {
            printf("Pid %d received FIN\n", getpid());
            close(sockfd);
            exit(0);
        }
        client_do_something(c);
    }
}


int start_server() {
    int i, nread, maxi, val, listenfd, connfd, sockfd, maxfd, nready, client[MAXCLIENT];
    socklen_t len;
    char *c;
    struct sockaddr_in servaddr, clientaddr, localaddr;
    fd_set rset, allset;
    char addr[MAXLINE];
    char laddr[INET_ADDRSTRLEN];
    struct timeval timeout;
    printf("Started\n");
    printf("Server PID=%d", getpid());
    val = 0;

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

    if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        perror("socket failed\n");


    if(bind(listenfd, (struct sockaddr *) &servaddr, (socklen_t) sizeof(servaddr)) == -1) 
        perror("listen failed\n");
    getsockname(listenfd, (struct sockaddr *) &localaddr, &len);
    inet_ntop(AF_INET,  &localaddr.sin_addr, laddr, INET_ADDRSTRLEN);
    printf("Server %d passed connect , %s:%d \n", getpid(),  laddr, ntohs(localaddr.sin_port));
    if(listen(listenfd, 10) == -1) 
        perror("listen failed\n");

    maxi = -1;
    for(i=0; i < MAXCLIENT; i++)
    client[i] = -1;
    maxfd = listenfd;
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    bzero(&timeout, sizeof(struct timeval));
    timeout.tv_sec = 15;
    printf("Server process, after listen, sleep 5s before accept\n");
/* Here I purpousely sleep because I want client to initiate connection before accept */
    sleep(5);
    printf("Server porcess, slept for 5s\n");
    while(1) {
    rset = allset;
    nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
    if (FD_ISSET(listenfd, &rset)) {
        len = sizeof(clientaddr);
        bzero(&clientaddr, sizeof(clientaddr));
        if((connfd = accept(listenfd, (struct sockaddr *) &clientaddr, &len)) < 0) {
        perror("accept failed");
        exit(-1);
        }
        for(i=0; i < MAXCLIENT; i++) {
        if(client[i] < 0) {
            client[i] = connfd;
            break;
        }
        } 
        FD_SET(connfd, &allset);
        if(connfd >= maxfd)
        maxfd = connfd;
        if(i > maxi)
        maxi = i;
        if(--nready <= 0) 
        continue;
    }
    for(i=0; i <= maxi; i++) {
        if ( (sockfd = client[i]) < 0)
        continue;
        if(FD_ISSET(sockfd, &rset)) {
        if( (nread = read(sockfd, addr, MAXLINE)) < 0) {
            if(errno == EINTR) 
            nread = 0;
            else {
            printf("Izlazim");
            return -1;
            }
        } else if (nread == 0) {
            close(sockfd);
            FD_CLR(sockfd, &allset);
            client[i] = -1;
                    continue;
        }

        if (strncmp(addr, "GET", 3) == 0) {
                    if(val < 100) {
                        i = send(sockfd, (int *) &val, (size_t) sizeof(val), 0);
                        val++;
                        if (i == -1)
                            printf("nread=%d, errno=%d\n",nread, errno); 
                    } else {
                        FD_CLR(sockfd, &allset);
                        client[i] = -1;
                        close(sockfd);
                    }
            } else {
            c = addr;
                    printf("Poslao je addr=%s\n", addr);
            }
            if(--nready <= 0) 
            break;
        }
    }
    }
}


int main(int argc, char *argv[]) {
    printf("Pid=%d",getpid());
    int i, child_num, status;
    pid_t p, pp;
    child_num = 100;
    printf("%d childs, pid=%d \n", child_num, getpid());
    p = fork();
    if(p == 0) {
        start_server();
        exit(0);
    } else if (p < 0) {
        exit(1);
    }
    pp = p;
    for(i = 0; i < child_num; i++) {
        p = fork();
        if (p < 0) {
            exit(1);
        } else if (p == 0){
            client_body();
            exit(0);
        }
    }
    for(i = 0; i < child_num; i++) {
        wait(&status);
    }
    kill(pp, SIGTERM);
    return 0;
}

问题出在 connect() 系统调用中(在 client_body 函数中);根据手册页,如果连接成功,连接应该返回 0,错误时返回 -1。我注意到,在我的程序中,connect() 返回 0,尽管它没有与服务器建立连接(从服务器接收到 SYN ACK)。后来,在程序中,同一个子进程发出了recv系统调用,产生了errno值为104(ECONNRESET)的错误。我一直在看 wireshark 捕获,我没有注意到服务器发送的任何 TCP 数据包中的 RESET 标志。

有人知道怎么回事吗?我已经在 linux,2.6.38-8-generic kernel 上测试了这段代码。

【问题讨论】:

  • 我在 freebsd 8.1 系统上尝试了此代码,但没有重现 connect() 在不交换 3 次握手的情况下返回 0(无错误)的问题。

标签: c unix network-programming system-calls


【解决方案1】:

您可能需要在 main() 中插入延迟,以确保在客户端尝试连接之前启动服务器。

睡眠(1); // 应该在 2 个分叉之间进行

可以随时重置连接。

对于listen() 积压,您的fork() 速率可能太快了。从 10 增加到 SOMAXCONN。在每个尝试连接的客户端之间插入一个 nanosleep() 亚秒睡眠,也许 10 毫秒就可以了。当您超过此值时,客户端将看到 ECONNRESET。

不知道为什么你在从客户端退出() 之前有 sleep(1)。在线查看 setsocketopt() 逗留设置,了解我认为您正在尝试实现的目标并立即退出。一些链接: https://lists.mindrot.org/pipermail/openssh-unix-dev/2002-September/015275.html(包含 SO_LINGER 使用的 C 代码) What is the difference between calling setSoLinger with a 0 value and not enabling soLinger at all?(面向Java但与相同机制相关)

无需在服务器上休眠(5)。你可以马上听。这 5 秒的延迟会将 listen() 积压设置为 10,以便在 connect() 期间查看 ECONNRESET(),因为服务器端的传入连接超载。

作为一项改进,recv() 上的大多数错误都是终止连接的原因。除了 EWOULDBLOCK/EAGAIN 和 EINTR。即 ENOTCONN 是一个应该终止使用套接字的错误(如果你是 UDP,则有可能继续保持 fd 打开,但不是 TCP,套接字永远不会恢复,你看到了吗)。这是仅有的两种情况

if(recv(fd, ...) < 0) {
  if(errno != EWOULDBLOCK && errno != EINTR) {
    // print out
    close(fd);
    exit(1);
  }
}

FWIW 我知道我没有直接回答您的问题,但我不相信您根据代码做出的声明。也许如果您要重新组织所有日志输出,使 getpid() 位于格式为 %05d 的行的开头,然后运行程序,然后粘贴生成的日志文件作为事情发生顺序的证明。

【讨论】:

  • 我不知道你认为他在退出之前尝试通过使用 SO_LINGER 来实现的睡眠会达到什么目的,但它不会。它只影响从他已经调用过的 close() 方法返回的时间。您发布的 C 链接仅包含一些代码,这些代码没有执行它声称在其 cmets 中执行的操作,并且确实应该被删除。
  • 嘿... 1)我们不知道要实现什么,2)您显然会根据了解 SO_LINGER 和什么来重新编写 sleep(1) 周围的代码确实如此。如果是我,我既不使用 sleep() 也不使用 SO_LINGER,而在关闭套接字之前只使用 shutdown(fd, SHUT_RDWR) 。我回答的目的是让他阅读它,然后思考它如何影响他的应用程序,而不是为他做所有的工作。哪个链接?我认为它与如何使用 setsockopt() API 来启用 SO_LINGER 相关。显然用户也应该使用谷歌。 C 剪断的内容提供了示例和术语。
  • 除了关闭套接字我什么都不会做。 shutdown() 在 close() 之前是多余的。 C sn-p 提供的 cmets 说它正在解决 TIME_WAIT '问题',但它没有提供,并且没有说明 SO_LINGER 的实际用途。你也没有。整个事情是一个完整的红鲱鱼。
  • 服务器上不需要 sleep(5) 是对的,但我故意把它放在那里。我希望我的客户在接受服务器调用之前尝试连接到服务器。即使服务器上没有 sleep(5) 也可能发生这种情况,而且,这也是我制作错误条件的方式。问题是在 client_body 中,connect 返回 0,虽然我在 tcpdump 捕获中没有看到 3 次握手:
  • 例如,我在 tcpdump 捕获中只看到这个:tcpdump -r log1.cap -ln port 38640 11:45:17.531036 IP 127.0.0.1.38640 > 127.0.0.1.1519: Flags [S ], seq 3043005227, win 32792, options [mss 16396,sackOK,TS val 1331905 ecr 0,nop,wscale 6], 长度 0 11:45:26.553459 IP 127.0.0.1.38640 > 127.0.0.1.1519: 标志 [P .], seq 3043005228:3043005232, ack 14362430, win 513, options [nop,nop,TS val 1334160 ecr 1334102], 长度 4
【解决方案2】:

我以前见过这个。当连接到 localhost 时,connect() 看起来会成功,即使它实际上并没有成功,并且直到第一次 I/O 操作才发现。但是,因为您忽略了第一个 I/O 操作 send() 中的错误,所以您会在第二个操作 recv() 中得到它,您可以在其中进行检查。也检查 send()。

我上次看到这是二十年前的事了,我忘记了我是如何绕过它的。您可以尝试在 connect() 之后通过 getsockopt() 查看 SO_ERROR。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-09-28
    • 1970-01-01
    • 2010-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多