2012-11-03 30 views
1

我在C中寫了一個簡單的應用程序,它讓一個孩子充當網絡服務器,許多孩子充當網絡客戶端。客戶端連接到服務器,並詢問數據。這裏是代碼:行爲不端的TCP客戶端實現

#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-通用內核上測試過這個代碼。

+0

我在freebsd 8上試過這段代碼。1系統,並沒有重現connect()返回0(沒有錯誤)而沒有交換三次握手的問題。 – user1267132

回答

1

您可能需要在main()中插入一個延遲,以確保服務器在客戶端嘗試連接之前啓動。

sleep(1); //應該在2個分叉之間執行

連接可以隨時重置。

您的fork()速率對於listen()積壓可能太快。從10增加到SOMAXCONN。在每個試圖連接的客戶端之間插入一個nanosleep()亞秒級睡眠,也許10ms就會這樣做。當你超過這個時,客戶端會看到ECONNRESET。

不確定爲什麼你在客戶端退出()之前有睡眠(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,套接字將永遠不會恢復)。這些是僅有2例

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

FWIW我知道我沒有直接回答你的問題,但我不相信你根據代碼作出要求。也許如果你要重新組織所有的日誌輸出,所以getpid()在格式化爲%05d的行的開頭,然後運行該程序,然後粘貼生成的日誌文件作爲事件發生順序的證明。

+0

我不知道你在想什麼,他想在退出之前用睡眠來達到什麼目的,SO_LINGER會達到目的,但不會。它隻影響從他已經調用的close()方法返回的時間。您發佈的C鏈接僅包含一些代碼,它不會執行它在其評論中聲稱的操作,並且確實已被刪除。 – EJP

+0

呵呵...... 1)我們不知道想要達到什麼目的,2)根據對SO_LINGER的瞭解以及它的作用,你顯然會重新編寫睡眠代碼(1)。如果是我,我會在關閉套接字之前使用sleep()和SO_LINGER,並關閉(fd,SHUT_RDWR)。我的答案的目標是讓他閱讀並理解它如何影響他的應用程序,而不是爲他做所有的工作。哪個鏈接?我認爲它與如何使用setsockopt()API來啓用SO_LINGER相關。顯然,用戶也應該使用谷歌。 C剪輯提供了示例和條款。 –

+0

除了關閉插座之外,我不會做任何事情。 shutdown()在close()之前立即是多餘的。 C代碼片段提供了一些評論,表明它在解決TIME_WAIT「問題」時不會提供,並且沒有說明SO_LINGER的實際用途。你也不是。整個事情是一個完整的紅鯡魚。 – EJP

1

我以前見過這個。當連接到本地主機時,connect()可能看起來成功,即使它不是真的,直到第一個I/O操作才發現。但是,因爲您忽略了第一個I/O操作中的錯誤,所以send()會在第二個錯誤處收到recv(),您會在其中執行檢查。檢查send()了。

上次我看到這是二十年前,我忘記了我是如何得到它的。你可以嘗試在connect()之後通過getsockopt()來查看SO_ERROR。