TCP客户/服务器

来源:互联网 发布:vscode使用教程 编辑:程序博客网 时间:2024/04/27 21:25

《UNIX Network Programming Volume1: The Socket Networking API, Third Edition》
W.Richard Stevens / Bill Fenner / Andrew M.Rudoff

字节排序函数

#include <netinet/in.h>uint16_t htons(uint16_t host16bitvalue);uint32_t htonl(uint32_t host32bitvalue);uint16_t ntohs(uint16_t net16bitvalue);uint32_t ntohl(uint32_t net32bitvalue);

h代表host,n代表network,s代表short(16位值),l代表long(32位值)。事实上,即使在64位主机中,尽管long整型占用64位,htonl和ntohl函数操作的仍然是32位的值。

地址转换函数

#include <arpa/inet.h>int inet_pton(int family, const char *strptr, void *addrptr);const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

这两个函数为IP地址转换函数,在ASCII字符串与网络字节序的二进制值之间转换网际协议。它们是随IPv6出现的,对于IPv4和IPv6地址都适用。函数名中的p和n分别代表表达(presentation)数值(numeric)inet_ntop从数值格式转换到表达格式,len为目标存储单元的大小。为有助于指定这个大小,在<netinet/in.h>中定义了:#define INET_ADDRSTRLEN 16#define INET6_ADDRSTRLEN 46

套接字函数

#include <sys/socket.h>int socket(int family, int type, int protocol);family:协议族。如AF_INET(IPv4)/AP_INET6(IPv6)等。type:套接字类型。如SOCK_STREAM(字节流)/SOCK_DGRAM(数据报)/SOCK_RAW(原始)等。protocol:协议类型。如IPPROTO_CP/IPPROTO_UDP等。(如设为0,用以选择family与type组合的系统默认值)

2

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

此函数将激发TCP的三路握手过程,且仅在连接建立成功或出错时才返回,出错情况如下:

  1. 调用connect函数时,4.4BSD内核发送一个SYN分节,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再发送一个。若总共等了75s后仍未收到响应则返回ETIMEDOUT错误。
  2. 若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在指定的端口上没有进程在等待与之连接。这是一种硬错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误。
  3. 若客户发出的SYN在中间的某个路由器上引发了一个”destination unreachable(目的地不可达)“ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该错误消息后按”1“中所述时间间隔继续发送SYN,75s后仍无响应则返回EHOSTUNREACHENETUNREACH错误。
  4. 若connect失败则该套接字不再可用,必须关闭,不能对这样的套接字再次调用connect函数。当循环调用connect为给定主机尝试IP地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并重新调用socket。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

此函数把一个本地协议地址(IP和端口号)赋予一个套接字。

  • 如果一个TCP客户或服务器未曾调用bind捆绑一个端口(如RPC远程过程调用)或指定端口为0(对于IPv4,INADDR_ANY值一般为0),当调用connect或listen时,内核要为套接字选择一个临时端口。为了得到内核所选择的临时端口值,必须调用函数getsockname来返回协议地址。
  • TCP客户捆绑IP地址到套接字,表示为发送的IP数据报指派了源IP地址(通常不这样做)。TCP服务器捆绑IP地址到套接字,就限定了该套接字只接收那些目的地为这个IP地址的客户连接。
  • bind函数返回的一个常见错误是EADDRINUSE(”Address already in use“)

int listen(int sockfd, int backlog);

此函数仅由TCP服务器调用,将一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。内核为任何一个给定的监听套接字维护两个队列:未完成连接队列已完成连接队列。关于这两个队列,以下几点需要考虑:

  • listen的backlog参数曾被规定为这两个队列总和的最大值,而源自Berkeley的实现给backlog增设了一个模糊因子:把它乘以1.5得到未处理队列最大长度。不要把backlog设为0,因为不同的实现对此有不同的解释。历来沿用的样例代码总将backlog设置为5(实际上允许最多8项在排队)。另外,指定一个比内核能够支持的值还要大的多的backlog也是可接受的,因为内核可把这个偏大值截成自身支持的最大值,而不返回错误。
  • 当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节(不发送RST),等待TCP的正常重传机制重传SYN。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

此函数由TCP服务器调用,用于从已完成连接队列队头(三路握手完成时,TCP在未完成队列创建的新项会移到已完成连接队列的队尾)返回下一个已完成连接。如果已完成连接队列为空,那么进程将被投入睡眠(套接字为阻塞方式时)。参数cliaddraddrlen用来返回已连接的对端客户的协议地址。 sockfd为监听套接字(listening socket)描述符,它在该服务器的生命期内一直存在。accept的返回值为已连接套接字(connected socket)描述符,当服务器完成对某个客户的服务时,相应的已连接套接字就被关闭。

int close(int sockfd);int shutdown(int sockfd, int howto);
  • close将相应描述符的引用计数值减1,仅在该计数变为0时才关闭套接字(终止读和写两个方向的数据传送)。
  • shutdown可以不管引用计数就激发TCP的正常连接终止序列。其行为依赖于howto参数:

    • SHUT_RD:关闭连接的读一半。套接字不再接收数据并将接收缓冲区中的数据丢弃。
    • SHUT_WR:关闭连接的写一半。对于TCP,这称为半关闭(half close)。套接字发送缓冲区中的现有数据会被发送掉,后跟TCP的正常终止序列。
    • SHUT_RDWR:等效于第一次调用SHUT_RD,第二次调用SHUT_WR

    int getsockname(itn sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
    int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);

这两个函数返回与某个套接字关联的本地协议地址(getsockname)或外地协议地址(getpeername)。

read和write

字节流套接字(TCP)上的read和write函数所表现的行为不同于通常的文件I/O。字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。这个现象的原因在于内核中用于套接字的缓冲区可能已达到了极限。此时所需的是调用者再次调用read或write函数,以输入或输出剩余的字节。当套接字标志为阻塞时,对于read调用,如果TCP接收缓冲区没有数据read函数就会阻塞住,如果接收缓冲区中有20字节,请求读100个字节,read就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回,如果write中得套接字标志为非阻塞,则直接返回20。
当进程调用read且没有数据返回时,如果套接字的so_error变量为非0值,那么read返回-1且errno被置为so_error的值,随后so_error被复位为0。如果该套接字上有数据在排队等待读取,那么read返回那些数据而不是返回错误条件。如果在进程调用write时套接字的so_error变量为非0值,那么write返回-1且errno被置为so_error的值,随后so_error被复位为0。

简单的TCP客户/服务器

daytimetcpsrv.c:对于时间获取服务,端口号一般为13。由服务器关闭连接表征记录的结束,HTTP 1.0版本也采用这种技术。

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <time.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/types.h>#include <sys/socket.h>#define MAXLINE 32#define error_exit(msg) \    do { perror(msg); exit(EXIT_FAILURE); } while (0)int main(int argc, char **argv) {    int listenfd, connfd;    socklen_t clilen;    struct sockaddr_in cliaddr, servaddr; // 网际套接字地址结构    char buff[MAXLINE];    time_t ticks;    // 创建网际(AF_INET)字节流(SOCK_STREAM)套接字(TCP)    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)        error_exit("socket");     // 绑定端口到上述套接字    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET; // 地址族    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY指定IP地址为0.0.0.0(所有地址)    servaddr.sin_port = htons(13); // 端口号(主机字节序到网络字节序)    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)        error_exit("bind");     // 将上述套接字转换成监听套接字(监听描述符),这样客户的连接就可在该套接字上由内核接受    if (listen(listenfd, 1024) < 0) // 指定系统内核允许在该监听描述符上排队的最大客户连接数        error_exit("listen");    for ( ; ; ) {        // 睡眠等待客户的连接到达并被内核接受(三路握手)。握手完毕返回已连接描述符        clilen = sizeof(cliaddr);        if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0)             error_exit("accept");                           printf("connection from %s, port %d\n",             inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),             ntohs(cliaddr.sin_port));           ticks = time(NULL);        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));        // 发送应答(将当前时间字符串写给客户)        if (write(connfd, buff, strlen(buff)) != strlen(buff))            error_exit("write");        // 终止连接(引发正常的终止序列:每个方向上发送一个FIN,每个FIN又由各自的对端确认)        close(connfd);     }    return 0;}

daytimetcpcli.c

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <arpa/inet.h>#include <sys/types.h>#include <sys/socket.h>#define MAXLINE 32#define error_exit(msg) \    do { perror(msg); exit(EXIT_FAILURE); } while (0)int main(int argc, char **argv) {    int sockfd, n;    char recvline[MAXLINE + 1];    struct sockaddr_in servaddr; // 网际套接字地址结构    if (argc != 2)         error_exit("usage: a.out <IPaddress>");    // 创建网际(AF_INET)字节流(SOCK_STREAM)套接字(TCP)    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)         error_exit("socket");    // 建立连接:connect参数2强制转换成指向"通用套接字地址结构"的指针(开发此函数时,void*还不可用)       bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET; // 地址族    servaddr.sin_port = htons(13); // 端口号(主机字节序到网络字节序)    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) // IP地址(呈现形式到数值)        error_exit("inet_pton");    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)        error_exit("connect");    /* read函数读取服务器的应答,并用fputs输出结果“Thu Jan 12 10:45:52 2017\r\n”(26个字符)。     * 因为TCP是一个没有记录边界的字节流协议,这26个字节可以有多种返回方式(包含26个字节的单个TCP分节、每个分节只含1个字节的26个分节等)。     * 如果数据量很大,就不能确保一次read调用能返回服务器的整个应答,因此总把read编写在某个循环中。 */    while ((n = read(sockfd, recvline, MAXLINE)) > 0) { // read()返回0:表明对端关闭连接        recvline[n] = 0;        if (fputs(recvline, stdout) == EOF)             error_exit("fputs");    }    if (n < 0) // read()返回负值:表明发生错误        error_exit("read");    exit(0); // Unix在一个进程终止时总是关闭该进程所有打开的描述符(TCP套接字就此被关闭)}

结果:

$ ./daytimetcpcli 123.3.4.5connect: Connection timed out (约75s后返回)

使用SIGALRM为connect设置超时

因为在多线程程序中正确使用信号非常困难,建议只是在未线程化或单线程化的程序中使用此技术。
(改写daytimetcpcli.c中connect调用如下:)

#include <signal.h>void* Signal(int signo, void (*func)(int)) {    struct sigaction act, oact;    act.sa_handler = func;     sigemptyset(&act.sa_mask);     act.sa_flags = 0;#ifdef SA_INTERRUPT     if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;#endif#ifdef SA_RESTART     if (signo != SIGALRM) act.sa_flags |= SA_RESTART; #endif    if (sigaction(signo, &act, &oact) < 0)         return SIG_ERR;    return oact.sa_handler; // 返回信号的旧行为}       static void sig_alarm(int signo) {    return; /* just interrupt the connect() */}   int connect_timeo(int sockfd, const struct sockaddr *saptr, socklen_t salen, int nsec) {    void *sigfunc;    int n;    sigfunc = Signal(SIGALRM, sig_alarm);    /* 设置报警时钟。如果此前本进程已设置过,alarm返回当前剩余秒数,否则返回0 */    if (alarm(nsec) != 0)        printf("connect_timeo: alarm was already set");    /* 调用connect。如果被SIGALRM中断返回EINTR,把errno改设为ETIMEDOUT */    if ((n = connect(sockfd, saptr, salen)) < 0) {        close(sockfd); // 超时关闭套接字,以防三路握手继续进行        if (errno == EINTR)            errno = ETIMEDOUT;    }    /* 关闭alarm并恢复原来的信号处理函数 */    alarm(0);    Signal(SIGALRM, sigfunc);    return n;}
    if (connect_timeo(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr), 3) < 0)         error_exit("connect");

结果:

$ ./daytimetcpcli 123.3.4.5connect: Connection timed out (约3s后返回)  

非阻塞connect与select设置超时

int connect_nonb(int sockfd, const struct sockaddr *saptr, socklen_t salen, int nsec) {    int             flags, n, error;    socklen_t       len;    fd_set          rset, wset;    struct timeval  tval;    /* 设置套接字为非阻塞 */    flags = fcntl(sockfd, F_GETFL, 0);    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);    error = 0;    if ((n = connect(sockfd, saptr, salen)) < 0)        if (errno != EINPROGRESS) // EINPROGRESS错误表示连接建立已经启动但是尚未完成            return -1;    if (n == 0) // 非阻塞connect连接建立完成,通常发生在服务器和客户在同一个主机上的情况        goto done;    /* 调用select判断connect连接是否完成建立。关于此有一下说明:     * 1.当连接成功建立时,描述符变为可写;     * 2.当连接建立遇到错误时,描述符变为既可读又可写;     * 3.select调用之前有可能连接已经建立并有对端数据到达,描述符也变为既可读又可写。*/    FD_ZERO(&rset);    FD_SET(sockfd, &rset);    wset = rset;    tval.tv_sec = nsec;    tval.tv_usec = 0;    if ((n = select(sockfd+1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0) { // select返回0,表示超时发生        close(sockfd);          errno = ETIMEDOUT;        return -1;    }    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {        len = sizeof(error);        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) // 检查套接字上是否存在待处理错误,如果连接成功建立,返回0            return -1;    } else        error_exit("select error: sockfd not set");done:    fcntl(sockfd, F_SETFL, flags); // 恢复套接字的文件状态标志    if (error) {        close(sockfd);      /* just in case */        errno = error;        return -1;    }    return 0;}if (connect_nonb(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr), 3) < 0)    error_exit("connect");

结果同上。

多进程并发TCP客户/服务器

信号:异步发生,有时称为软件中断(software interrupt)。通常通过POSIX规定了信号语义的sigaction函数设定一个信号的行为。有三种行为可选择:

  1. 捕获信号行为:使用信号处理函数(signal handler,原型为void handler(int signo);)。注意SIGKILL和SIGSTOP不能被捕获。
  2. 忽略信号行为:把某个信号的处置设定为SIG_IGN来忽略它。注意SIGKILL和SIGSTOP不能被忽略。
  3. 默认行为:把某个信号的处置设定为SIG_DFL来启用它的默认处置(通常是在收到信号后终止进程)。SIGCHLD和SIGURG的默认处置为忽略。

僵死进程:Unix设置僵死(zombie)状态的目的是维护子进程的信息,以便父进程在以后某个时候获取,但留存僵死进程,会占用内核中的空间,最终可能导致耗尽进程资源。内核在任何一个进程终止时发给它的父进程一个SIGCHLD信号,父进程可捕获此信号并wait子进程,以防止子进程变成僵死进程。

慢系统调用(slow system call):该术语适用于那些可能阻塞的系统调用。多数网络支持函数都属于此类。适用于慢系统调用的基本原则是当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。某些内核自动重启某些被中断的系统调用,不过为了便于移植,当编写捕获信号的程序时,必须对慢系统调用返回EINTR错误有所准备。(注意connect函数返回EINTR时,不能再次调用它,否则将立即返回一个错误。

tcpserv.c:”多进程回射服务器“

#include <errno.h>#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/types.h>#include <sys/socket.h>#include <sys/wait.h>#define SERV_PORT 8755#define MAXLINE 32#define error_exit(msg) \    do { perror(msg); exit(EXIT_FAILURE); } while (0)void sig_chld(int signo) {    pid_t pid;    int stat;    /* 当多个子进程同时终止(多个SIGCHLD信号)时,在一个循环内调用waitpid,以获取所有已终止子进程的状态。     * (上述情况如果使用wait,可能会留下僵死进程,而且wait在正运行的子进程尚未终止时必定会阻塞。)     * WNOHANG选项告知waitpid在有尚未终止的子进程在运行时不要阻塞。      * waitpid的返回值共有3种情况:     *     1.当正常返回的时候,waitpid返回收集到的子进程的进程ID;     *     2.如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;     *     3.如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。 */    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)        printf("child %d terminated\n", pid);    return;}void* Signal(int signo, void (*func)(int)) {    struct sigaction act, oact;    /* 设定信号处理函数 */    act.sa_handler = func;     /* 在一个信号处理函数运行期间,正被递交的信号是阻塞的。将sa_mask置为空集,意味着除了被捕获的信号外,没有额外的信号被阻塞。*/    sigemptyset(&act.sa_mask);     /* SIGALRM通常在I/O操作超时时产生,这种情况不希望系统调用被重启.      * 对于默认不自动重启被中断的系统调用的系统,如果设置了SA_RESTART标志,则内核自动重启被信号中断的系统调用.      * 对于默认自动重启被中断的系统调用的系统,如果设置了SA_INTERRUPT标志,则该系统调用被某个信号中断后不重启. */        act.sa_flags = 0;#ifdef SA_INTERRUPT     if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;#endif#ifdef SA_RESTART     if (signo != SIGALRM) act.sa_flags |= SA_RESTART; #endif    if (sigaction(signo, &act, &oact) < 0)         return SIG_ERR;    return oact.sa_handler; // 返回信号的旧行为}       ssize_t writen(int fd, const void *vptr, size_t n) {    size_t      nleft;    ssize_t     nwritten;    const char  *ptr;    ptr = vptr;    nleft = n;    while (nleft > 0) {        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {            if (nwritten < 0 && errno == EINTR)                nwritten = 0; /* and call write() again */            else                return(-1);         }        nleft -= nwritten;        ptr   += nwritten;    }    return n;}       void doit(int sockfd) {    ssize_t     n;    char        buf[MAXLINE];    /* read从套接字读入数据,如果客户关闭连接,将导致read函数返回0 */again:    while ((n = read(sockfd, buf, MAXLINE)) > 0)         writen(sockfd, buf, n); // 保证写n个字节,否则失败    if (n < 0 && errno == EINTR)        goto again;    else if (n < 0)        error_exit("read");}int main(int argc, char **argv) {    int                 listenfd, connfd;    pid_t               childpid;    socklen_t           clilen;    struct sockaddr_in  cliaddr, servaddr;    char buff[MAXLINE];     listenfd = socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family      = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port        = htons(SERV_PORT);    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));    listen(listenfd, 5);    /* 当fork子进程时,必须捕获SIGCHLD信号 */    Signal(SIGCHLD, sig_chld);    for ( ; ; ) {        clilen = sizeof(cliaddr);        if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {            if (errno == EINTR) // 当捕获信号时,必须处理EINTR错误(被信号中断的系统调用)                continue;            else if (errno == ECONNABORTED) // 当accept返回前连接被客户终止(RST),POSIX指出返回的error必须是ECONNABORTED(software caused connection abort)                continue;            else                error_exit("accept");        }        printf("connection from %s, port %d\n",             inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),             ntohs(cliaddr.sin_port));           /* socket成功返回后,listenfd的引用计数为1;          * accept成功返回后,connfd的引用计数为1。         * fork返回后,上述两个描述符在父进程和子进程中共享(被复制),因此两个描述符的引用计数均为2. */        if (0 == (childpid = fork())) {             close(listenfd); // 子进程关闭listenfd,使得listenfd的引用计数减1            doit(connfd); // 子进程服务客户所需的所有操作            exit(0); // 子进程终止时会自动关闭所有由内核打开的描述符——close(connfd)此时自动调用        }        close(connfd); // 父进程关闭connfd,使得connfd的引用计数减1,然后等待另一个客户连接    }}

tcpcli.c:”回射客户“

#include <errno.h>#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/types.h>#include <sys/socket.h>#define SERV_PORT 8755#define MAXLINE 32#define error_exit(msg) \    do { perror(msg); exit(EXIT_FAILURE); } while (0)ssize_t writen(int fd, const void *vptr, size_t n) {    size_t      nleft;    ssize_t     nwritten;    const char  *ptr;    ptr = vptr;    nleft = n;    while (nleft > 0) {        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {            if (nwritten < 0 && errno == EINTR)                nwritten = 0; /* and call write() again */            else                return(-1);         }        nleft -= nwritten;        ptr   += nwritten;    }    return n;}   ssize_t readline(int fd, void *vptr, size_t maxlen) {    ssize_t n, rc;    char c, *ptr;    ptr = vptr;    for (n = 1; n < maxlen; n++) {again:        if ((rc = read(fd, &c, 1)) == 1) { // 一次读一个字节,对于大量数据将执行非常慢            *ptr++ = c;            if (c == '\n')                break; // newline is stored, like fgets()        } else if (rc == 0) {            *ptr = 0;            return (n-1); // EOF, n-1 bytes were read        } else {            if (errno == EINTR)                goto again;            return (-1);         }    }    *ptr = 0;     return(n);}void doit(FILE *fp, int sockfd) {    char sendline[MAXLINE], recvline[MAXLINE];    /* 1.当服务器进程终止(RST)时,客户进程可能由于正阻塞于fgets而未接收到该通知。(select或poll函数可处理这种情形)      *   导致第一次写操作引发RST,第二次写操作引发SIGPIPE信号,该信号默认行为是终止进程。     *   (规则:当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。)     * 2.服务器主机崩溃的情形要等到客户主动向服务器发送了数据才能检测到。(或者使用SO_KEEPALIVE套接字选项)      * 3.服务器主机关机的情形类型与“1” */    signal(SIGPIPE, SIG_IGN); // 更改SIGPIPE的行为为忽略,使客户不会终止在write函数上。    while (fgets(sendline, MAXLINE, fp) != NULL) { // 从标准输入读入一行文本        writen(sockfd, sendline, 1);         writen(sockfd, sendline+1, strlen(sendline)-1); // writen保证发送strlen(sendline)-1个字节,否则失败        if (readline(sockfd, recvline, MAXLINE) == 0)            error_exit("server terminated prematurely");        fputs(recvline, stdout); // 写服务端回射行到标准输出    }}int main(int argc, char **argv) {    int                 sockfd;    struct sockaddr_in  servaddr;    if (argc != 2)        error_exit("usage: tcpcli <IPaddress>");    sockfd = socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_port = htons(SERV_PORT);    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));    doit(stdin, sockfd); // 客户的所有请求    exit(0); // 进程终止时会自动关闭所有由内核打开的描述符——close(sockfd)此时自动调用}

结果:
3

4

5

I/O复用:select和poll函数

#include <sys/select.h>#include <sys/time.h>int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout, const sigset_t *sigmask);

select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。<sys/select.h>中定义的FD_SETSIZE常值(通常为1024)是数据类型fd_set中的描述符总数。其参数说明如下:

  • timeout参数:此参数导致select有三种行为。
    1. 永远等待下去(设为空指针):select仅在有一个描述符准备好I/O时才返回。
    2. 根本不等待(定时器值为0):select检查描述符后立即返回,这称为轮询(polling)。
    3. 等待一段固定时间(定时器值不为0):select在有一个描述符准备好I/O时返回,但是不超过定时器值。
  • readsetwritesetexceptset参数:分别指定要让内核测试读、写和异常条件的描述符集合(fd_set)。使用宏FD_ZERO清空)、FD_SET打开)、FD_CLR关闭)和FD_ISSET是否被设置)设置或测试描述符集合中的每一位,fd_set之间可以使用C语言中的赋值语句。这三个参数都是“值-结果”参数,select函数会将这三个描述符集内任何与未就绪描述符对应的位返回时均清成0。(注意,每次重新调用select函数时,都得再次把所有描述符集内所关心的位均置为1。
  • maxfdp1参数:指定待测试的描述符个数,它的值是待测试的最大描述符加1。
  • select返回值:表示跨所有描述符集的已就绪的总位数。返回0表示定时器到时,返回-1表示出错。
#include <poll.h>int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

支持select的系统比支持poll的系统多,而且POSIX没有为poll定义类似pselect的东西。

select阻塞读和写:改进回射客户

tcpcli.c客户存在一个问题:当服务器先终止时,FIN分节到达客户套接字时,由于客户正阻塞在fgets调用上,客户不能立即处理终止。客户实际上在应对两个描述符:套接字描述符和用户输入描述符,这就是select和poll函数存在的目的。改进后的doit函数如下:

void doit(FILE *fp, int sockfd) {    char buf[MAXLINE];    int maxfdp1, stdineof;    fd_set rset;    int n;    signal(SIGPIPE, SIG_IGN);    stdineof = 0;    FD_ZERO(&rset);    for ( ; ; ) {        if (stdineof == 0)            FD_SET(fileno(fp), &rset);        FD_SET(sockfd, &rset);        maxfdp1 = MAX(fileno(fp), sockfd) + 1;        if (select(maxfdp1, &rset, NULL, NULL, NULL) < 0)            error_exit("select");;        /* 改用read和write对缓冲区进行操作,使得select能够如期地工作 */        if (FD_ISSET(sockfd, &rset)) { // sockfd可读            if ((n = read(sockfd, buf, MAXLINE)) == 0) { // read读完接收缓存区数据后返回0                if (stdineof == 1) // 如果已处理完所有客户请求就正常返回                    return; /* normal termination */                else                    error_exit("server terminated prematurely");            }            write(fileno(stdout), buf, n);        }        if (FD_ISSET(fileno(fp), &rset)) { // input可读            if ((n = read(fileno(fp), buf, MAXLINE)) == 0) { // read读到文件尾时返回0                stdineof = 1; // fp描述符到达文件尾                FD_CLR(fileno(fp), &rset); // slect不再等待fp描述符可读事件                                shutdown(sockfd, SHUT_WR); // 关闭套接字写一半,发送FIN                 continue;            }            writen(sockfd, buf, n); // SHUT_WR后所有写函数不可用        }    }}

select非阻塞读和写:改进回射客户

select阻塞读和写版本中,如果套接字发送缓冲区已满,writen调用将会阻塞。在进程阻塞于writen调用期间,可能有来自套接字接收缓冲区的数据可供读取。使用非阻塞式I/O可以防止进程在任何有效工作期间发生阻塞。然而,非阻塞式I/O的加入使得缓冲区的管理复杂化了。

#include <sys/time.h>static char* gf_time(void) {    struct timeval tv;    static char str[30];    char *ptr;    if (gettimeofday(&tv, NULL) < 0)        error_exit("gettimeofday error\n");    ptr = ctime(&tv.tv_sec);    strcpy(str, &ptr[11]);    snprintf(str+8, sizeof(str)-8, ".%06ld", tv.tv_usec);    return str;}void doit(FILE *fp, int sockfd) {    int maxfdp1, val, stdineof;    ssize_t n, nwritten;    fd_set rset, wset;    char to[MAXLINE]; // to缓冲区容纳从标准输入(stdin)到服务器(sockfd)的数据    char fr[MAXLINE]; // fr缓冲区容纳自服务器(sockfd)到标准输出(stdout)的数据    char *toiptr;     // toiptr指向从标准输入读入的数据可以存放的下一个字节,可从stdin读入的字节数是(&to[MAXLINE]-toiptr)    char *tooptr;     // tooptr指向下一个必须写到套接字的字节,有(toiptr-tooptr)个字节需写到sockfd    char *friptr;     // friptr    char *froptr;     // froptr    /* 把描述符设置为非阻塞 */    val = fcntl(sockfd, F_GETFL, 0);    fcntl(sockfd, F_SETFL, val | O_NONBLOCK);    val = fcntl(STDIN_FILENO, F_GETFL, 0);    fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);    val = fcntl(STDOUT_FILENO, F_GETFL, 0);    fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);    /* 初始化缓冲区指针 */    toiptr = tooptr = to;    friptr = froptr = fr;    stdineof = 0;    /* 准备调用select */    maxfdp1 = MAX(MAX(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;    for ( ; ; ) {        FD_ZERO(&rset);        FD_ZERO(&wset);        if (stdineof == 0 && toiptr < &to[MAXLINE]) // 在stdin上尚未读到EOF,而且to缓冲区有至少一个字节可用空间            FD_SET(STDIN_FILENO, &rset);            if (friptr < &fr[MAXLINE]) // fr缓冲区至少一个字节可用空间            FD_SET(sockfd, &rset);          if (tooptr != toiptr) // to缓冲区中有要写到套接字的数据            FD_SET(sockfd, &wset);          if (froptr != friptr) // fr缓冲区中有要写到标准输出的数据            FD_SET(STDOUT_FILENO, &wset);           /* 调用select */        select(maxfdp1, &rset, &wset, NULL, NULL);        /* 从标准输入read */        if (FD_ISSET(STDIN_FILENO, &rset)) {            if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {                if (errno != EWOULDBLOCK) // 忽略非阻塞错误EWOULDBLOCK(select已告知可读,read实际上不会返回此错误)                    error_exit("read error on stdin");            } else if (n == 0) { // 如果read返回0,那么设置stdineof标记结束标准输入处理                stdineof = 1;                if (tooptr == toiptr) // 如果to缓冲区不再有数据要发送,那么调用shutdown发送FIN                    shutdown(sockfd, SHUT_WR);            } else {                printf("%s: read %d bytes from stdin\n", gf_time(), n);                toiptr += n;                FD_SET(sockfd, &wset);            }        }        /* 从套接字read */              if (FD_ISSET(sockfd, &rset)) {            if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {                if (errno != EWOULDBLOCK)                    error_exit("read error on socket");            } else if (n == 0) {                if (stdineof)                    return;                 else                    error_exit("str_cli: server terminated prematurely");            } else {                fprintf(stderr, "%s: read %d bytes from socket\n", gf_time(), n);                friptr += n;                FD_SET(STDOUT_FILENO, &wset);            }        }        /* write到标准输出 */                    if (FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0)) { // stdout可写且要写的字节数大于0            if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0) {                if (errno != EWOULDBLOCK)                    error_exit("write error to stdout");            } else {                fprintf(stderr, "%s: wrote %d bytes to stdout\n", gf_time(), nwritten);                froptr += nwritten;                if (froptr == friptr) // 一旦输出(froptr)追上输入(friptr),则两个指针同时恢复为指向缓冲区开始处                    froptr = friptr = fr;            }        }        /* write到套接字 */        if (FD_ISSET(sockfd, &wset) && ((n = toiptr - tooptr) > 0)) {             if ((nwritten = write(sockfd, tooptr, n)) < 0) {                if (errno != EWOULDBLOCK)                    error_exit("write error to socket");            } else {                fprintf(stderr, "%s: wrote %d bytes to socket\n", gf_time(), nwritten);                tooptr += nwritten;                if (tooptr == toiptr) { // 一旦tooptr移动到toiptr,这两个指针就一起恢复到缓冲区开始处                    toiptr = tooptr = to;                    if (stdineof) // 如果已经在标准输入上遇到EOF就发送FIN到服务器                        shutdown(sockfd, SHUT_WR);  /* send FIN */                }            }        }    }}

多进程:改进回射客户

每当发现需要使用非阻塞式I/O时,更简单的办法通常是把应用程序任务划分到多个进程或多个线程。

void doit_fork(FILE *fp, int sockfd) {    pid_t pid;    char sendline[MAXLINE], recvline[MAXLINE];    /* 父子进程共享同一个套接字,接收缓冲区和发送缓冲区也分别只有一个,而有两个描述符在引用这个套接字 */    if ((pid = fork()) == 0) {              while (readline(sockfd, recvline, MAXLINE) > 0)            fputs(recvline, stdout);        kill(getppid(), SIGTERM); // 服务器过早终止时,子进程向父进程发送一个SIGTERM信号,以防止父进程仍在运行        exit(0);    }    while (fgets(sendline, MAXLINE, fp) != NULL)        writen(sockfd, sendline, strlen(sendline));    shutdown(sockfd, SHUT_WR);    pause();    return;}

select代替多进程回射服务器

重写TCP回射服务器程序tcpserv.c,把它重写成使用select来处理任意个客户的单进程程序。

int main(int argc, char **argv) {    int                 i, maxi, maxfd;    int                 listenfd, connfd, sockfd;    int                 nready, client[FD_SETSIZE];    ssize_t             n;    fd_set              rset, allset;    socklen_t           clilen;    struct sockaddr_in  cliaddr, servaddr;    char                buf[MAXLINE];       listenfd = socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family      = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port        = htons(SERV_PORT);    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));    listen(listenfd, 5);    maxfd = listenfd;    maxi = -1;    for (i = 0; i < FD_SETSIZE; i++)         client[i] = -1;    FD_ZERO(&allset); // 将allset中的所有项置为0    FD_SET(listenfd, &allset); // allset中唯一的非0项是listenfd的项    for ( ; ; ) {        /* select等待allset中描述符可读(建立连接、数据/FIN/RST到达) */        rset = allset;        nready = select(maxfd+1, &rset, NULL, NULL, NULL);        /* 当listenfd监听描述符可读时,接受一个客户的连接。         * 客户连接成功后,服务器保存新的已连接描述符connfd,并把它添加到allset中。 */        if (FD_ISSET(listenfd, &rset)) { // 当客户与服务器建立连接时,监听套接字变为可读            clilen = sizeof(cliaddr);            if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {                if (errno == ECONNABORTED) // 当accept返回前连接被客户终止(RST),POSIX指出返回的error必须是ECONNABORTED(software caused connection abort)                    continue;                else                    error_exit("accept");            }            printf("connection from %s, port %d\n",                 inet_ntop(AF_INET, &cliaddr.sin_addr, buf, sizeof(buf)),                 ntohs(cliaddr.sin_port));            for (i = 0; i < FD_SETSIZE; i++) {                if (client[i] < 0) {                    client[i] = connfd; // 保存每个新的已连接描述符                    break;                }            }            if (i == FD_SETSIZE)                 error_exit("too many clients");            FD_SET(connfd, &allset); // connfd添加到allset中            if (connfd > maxfd) maxfd = connfd; // 更新select参数1——最大描述符            if (i > maxi) maxi = i; // 更新client[FD_SETSIZE]中保存的已连接描述符最大索引            if (--nready <= 0)                 continue;        }        /* 当已连接描述符(client[maxi]中的一个或多个)可读时,处理客户的连接请求。 */        for (i = 0; i <= maxi; i++) {            if ((sockfd = client[i]) < 0)                 continue;            if (FD_ISSET(sockfd, &rset)) {                if ((n = read(sockfd, buf, MAXLINE)) == 0) { // 如果客户终止了连接,read这个套接字将返回0                    close(sockfd); // 关闭该连接描述符                    FD_CLR(sockfd, &allset); // 该连接描述符被移除出allset                    client[i] = -1; // 更新client[i]中该连接描述符值为初始值-1                }                else                    writen(sockfd, buf, n);                if (--nready <= 0) // 限定只处理已就绪的描述符                    break;            }        }    }    return 0;}

结果:
1

2

3

epoll代替多进程回射服务器

epoll机制:epoll_create、epoll_ctl、epoll_wait、close
在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,linux/posix_types.h头文件有这样的声明:#define __FD_SETSIZE 1024 (通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本)。

#include <errno.h>#include <fcntl.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <netinet/in.h>#include <sys/epoll.h>#include <sys/socket.h>#define MAXLINE 32#define MAX_EVENTS 20#define error_exit(msg) \    do { perror(msg); exit(EXIT_FAILURE); } while (0)ssize_t writen(int fd, const void *vptr, size_t n) {    size_t      nleft;    ssize_t     nwritten;    const char  *ptr;    ptr = vptr;    nleft = n;    while (nleft > 0) {        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {            if (nwritten < 0 && errno == EINTR)                nwritten = 0; /* and call write() again */            else                return(-1);         }        nleft -= nwritten;        ptr   += nwritten;    }    return n;}   ssize_t readn(int fd, void *vptr, size_t n) {    size_t  nleft;    ssize_t nread;    char    *ptr;    ptr = vptr;    nleft = n;    while (nleft > 0) {        if ((nread = read(fd, ptr, nleft)) < 0) {            if (errno == EINTR)                nread = 0; // and call read() again             else if (errno == EAGAIN) // 非阻塞模式下,errno为EAGAIN时表示当前缓冲区已无数据可读                break;            else                return(-1);        } else if (nread == 0)            break; // EOF         nleft -= nread;        ptr   += nread;    }    return (n - nleft); // return >= 0 }int main(int argc, char* argv[]) {    int listenfd, connfd, sockfd;    int epfd, i,j, nfds, port, val;    ssize_t n;    char line[MAXLINE], buf[MAXLINE];    socklen_t clilen;    struct sockaddr_in cliaddr;    struct sockaddr_in servaddr;        struct epoll_event ev, events[MAX_EVENTS];      if (2 == argc)         port = atoi(argv[1]);    else {        fprintf(stderr, "usage: %s <port>\n", argv[0]);        exit(0);    }    listenfd = socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_port = htons(port);        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));    listen(listenfd, 5);    if ((epfd = epoll_create(MAX_EVENTS)) == -1) // 创建一个epoll实例返回指向此实例的文件描述符            error_exit("epoll_create");    ev.events = EPOLLIN; // LT是缺省的工作方式,并且同时支持block和nonblock socket    ev.data.fd = listenfd;      if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) // 将监听描述符在epoll实例中注册           error_exit("epoll_ctl : listenfd");    for ( ; ; ) {        if ((nfds = epoll_wait(epfd, events, MAX_EVENTS, -1)) == -1) // 阻塞等待I/O events,返回发生事件数,如返回0表示已超时。                error_exit("epoll_wait");        for (i = 0; i < nfds; i++) {            if (events[i].data.fd == listenfd) {                 clilen = sizeof(cliaddr);                if ((connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen)) < 0) {                    if (errno == ECONNABORTED) // 当accept返回前连接被客户终止(RST)                        continue;                    else                        error_exit("accept");                               }                printf("connection from %s, port %d\n",                     inet_ntop(AF_INET, &cliaddr.sin_addr, buf, sizeof(buf)),                     ntohs(cliaddr.sin_port));                val = fcntl(connfd, F_GETFL, 0);                fcntl(connfd, F_SETFL, val | O_NONBLOCK);                   ev.events = EPOLLIN | EPOLLET; // epoll工作在ET模式的时候,必须使用非阻塞套接字                            ev.data.fd = connfd;                if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev) == -1)                    error_exit("epoll_ctl : connfd");            }             else if (events[i].events & EPOLLIN) { // 可读                sockfd = events[i].data.fd;                if ((n = readn(sockfd, line, MAXLINE)) == 0) {                    close(sockfd);                    ev.data.fd = sockfd;                    ev.events = EPOLLOUT | EPOLLET;                    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, &ev);                    continue;                }                line[n] = 0;                printf("#%d: %s", sockfd, line);                ev.data.fd = sockfd;                ev.events = EPOLLOUT | EPOLLET;                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);            }             else if (events[i].events & EPOLLOUT) { // 可写                sockfd = events[i].data.fd;                writen(sockfd, line, n);                ev.data.fd = sockfd;                ev.events = EPOLLIN | EPOLLET;                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);            }        }    }    close(epfd);    close(listenfd);    return 0;}

多线程回射客户/服务器

回射客户

#include <pthread.h>static int g_sockfd;static FILE *g_fp;static int g_done;void* copyto(void *arg) {    char sendline[MAXLINE];    while (fgets(sendline, MAXLINE, g_fp) != NULL)        writen(g_sockfd, sendline, strlen(sendline));    shutdown(g_sockfd, SHUT_WR);    /* EOF on stdin, send FIN */    g_done = 1;    return(NULL);}void doit(FILE *fp_arg, int sockfd_arg) {    char recvline[MAXLINE];    pthread_t tid;    g_sockfd = sockfd_arg;    g_fp = fp_arg;    pthread_create(&tid, NULL, copyto, NULL);    while (readline(g_sockfd, recvline, MAXLINE) > 0)        fputs(recvline, stdout);    if (g_done == 0)        error_exit("server terminated prematurely");}int main(int argc, char **argv) {    int                 sockfd;    struct sockaddr_in  servaddr;    if (argc != 2)        error_exit("usage: tcpcli <IPaddress>");    sockfd = socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_port = htons(SERV_PORT);    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));    doit(stdin, sockfd); // 客户的所有请求    exit(0); // 进程终止时会自动关闭所有由内核打开的描述符——close(sockfd)此时自动调用}

回射服务器:每个客户使用一个线程,使用tcp_listen函数使得该程序与协议无关

#include <netdb.h>#include <pthread.h>void doit(int sockfd) {    ssize_t     n;    char        buf[MAXLINE];    /* read从套接字读入数据,如果客户关闭连接,将导致read函数返回0 */again:    while ((n = read(sockfd, buf, MAXLINE)) > 0)         writen(sockfd, buf, n); // 保证写n个字节,否则失败    if (n < 0 && errno == EINTR)        goto again;    else if (n < 0)        error_exit("read");}static void* thread_func(void *arg) {    int connfd;    connfd = *((int *)arg);    free(arg);    pthread_detach(pthread_self());    doit(connfd);       close(connfd);    return(NULL);}int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp) {    int listenfd, n;    const int on = 1;    struct addrinfo hints, *res, *ressave;    bzero(&hints, sizeof(struct addrinfo));    hints.ai_flags = AI_PASSIVE;    hints.ai_family = AF_UNSPEC;    hints.ai_socktype = SOCK_STREAM;    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {        printf("tcp_listen error for %s, %s: %s", host, serv, gai_strerror(n));             exit(0);    }    ressave = res;    do {        listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);        if (listenfd < 0)            continue; // error, try next one         setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));        if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)            break; // success         close(listenfd); // bind error, close and try next one     } while ((res = res->ai_next) != NULL);    if (res == NULL) { // errno from final socket() or bind()         printf("tcp_listen error for %s, %s", host, serv);        exit(0);    }    listen(listenfd, 5);    if (addrlenp)        *addrlenp = res->ai_addrlen;    freeaddrinfo(ressave);    return(listenfd);}int main(int argc, char **argv) {    int listenfd, *iptr;    pthread_t tid;    socklen_t addrlen, len;    struct sockaddr *cliaddr;    if (argc == 2)        listenfd = tcp_listen(NULL, argv[1], &addrlen);    else if (argc == 3)        listenfd = tcp_listen(argv[1], argv[2], &addrlen);    else        error_exit("usage: tcpserv [ <host> ] <service or port>");    cliaddr = malloc(addrlen);    for ( ; ; ) {        len = addrlen;        iptr = malloc(sizeof(int));        *iptr = accept(listenfd, cliaddr, &len);        pthread_create(&tid, NULL, &thread_func, iptr);    }}
0 0
原创粉丝点击