美文网首页LinuxLinux学习之路
APUE读书笔记-16网络通信(7)

APUE读书笔记-16网络通信(7)

作者: QuietHeart | 来源:发表于2020-08-01 16:28 被阅读0次

    面向连接与面向无连接通讯举例

    面向连接的客户端的例子

    下面的程序,显示了一个客户的命令,这个命令和服务进行通信,从系统的uptime命令获取输出。我们将这个服务称作"remote uptime"(或简称"ruptime")。

    这个程序连接服务器,读取服务器发送的字符串,然后打印字符串到标准输出。因为我们使用SOCK_STREAM套接字,我们不能保证我们会在一个recv调用中读取整个字符串,所以我们使用一个循环进行调用,直到它返回0。

    #include <netdb.h>
    #include <errno.h>
    #include <sys/socket.h>
    #define MAXADDRLEN  256
    #define BUFLEN      128
    extern int connect_retry(int, const struct sockaddr *, socklen_t);
    void print_uptime(int sockfd)
    {
        int     n;
        char    buf[BUFLEN];
        while ((n = recv(sockfd, buf, BUFLEN, 0)) > 0)
            write(STDOUT_FILENO, buf, n);
        if (n < 0)
            err_sys("recv error");
    }
    
    int main(int argc, char *argv[])
    {
        struct addrinfo *ailist, *aip;
        struct addrinfo hint;
        int             sockfd, err;
        if (argc != 2)
            err_quit("usage: ruptime hostname");
        hint.ai_flags = 0;
        hint.ai_family = 0;
        hint.ai_socktype = SOCK_STREAM;
        hint.ai_protocol = 0;
        hint.ai_addrlen = 0;
        hint.ai_canonname = NULL;
        hint.ai_addr = NULL;
        hint.ai_next = NULL;
        if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0)
            err_quit("getaddrinfo error: %s", gai_strerror(err));
        for (aip = ailist; aip != NULL; aip = aip->ai_next) {
            if ((sockfd = socket(aip->ai_family, SOCK_STREAM, 0)) < 0)
                err = errno;
            if (connect_retry(sockfd, aip->ai_addr, aip->ai_addrlen) < 0) {
                err = errno;
            } else {
                print_uptime(sockfd);
                exit(0);
            }
        }
        fprintf(stderr, "can't connect to %s: %s\n", argv[1],
        strerror(err));
        exit(1);
    }
    

    如果服务进程支持多个网络接口或者多个网络协议,getaddrinfo函数可能返回多个候选地址。我们一次对每个进行尝试,当找到一个允许我们连接的服务的时候为止。我们使用前面自定义的connect_retry函数来建立和服务器之间的连接。

    面向连接的服务端的例子

    下面的程序给出了提供前面客户端的uptime的服务输出程序。

    #include <netdb.h>
    #include <errno.h>
    #include <syslog.h>
    #include <sys/socket.h>
    
    #define BUFLEN  128
    #define QLEN 10
    
    #ifndef HOST_NAME_MAX
    #define HOST_NAME_MAX 256
    #endif
    
    extern int initserver(int, struct sockaddr *, socklen_t, int);
    
    void serve(int sockfd)
    {
        int     clfd;
        FILE    *fp;
        char    buf[BUFLEN];
    
        for (;;) {
            clfd = accept(sockfd, NULL, NULL);
            if (clfd < 0) {
                syslog(LOG_ERR, "ruptimed: accept error: %s",
                  strerror(errno));
                exit(1);
            }
            if ((fp = popen("/usr/bin/uptime", "r")) == NULL) {
                sprintf(buf, "error: %s\n", strerror(errno));
                send(clfd, buf, strlen(buf), 0);
            } else {
                while (fgets(buf, BUFLEN, fp) != NULL)
                    send(clfd, buf, strlen(buf), 0);
                pclose(fp);
            }
            close(clfd);
        }
    }
    
    int main(int argc, char *argv[])
    {
        struct addrinfo *ailist, *aip;
        struct addrinfo hint;
        int             sockfd, err, n;
        char            *host;
    
        if (argc != 1)
            err_quit("usage: ruptimed");
    #ifdef _SC_HOST_NAME_MAX
        n = sysconf(_SC_HOST_NAME_MAX);
        if (n < 0)  /* best guess */
    #endif
            n = HOST_NAME_MAX;
        host = malloc(n);
        if (host == NULL)
            err_sys("malloc error");
        if (gethostname(host, n) < 0)
            err_sys("gethostname error");
        daemonize("ruptimed");
        hint.ai_flags = AI_CANONNAME;
        hint.ai_family = 0;
        hint.ai_socktype = SOCK_STREAM;
        hint.ai_protocol = 0;
        hint.ai_addrlen = 0;
        hint.ai_canonname = NULL;
        hint.ai_addr = NULL;
        hint.ai_next = NULL;
        if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
            syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s",
              gai_strerror(err));
            exit(1);
        }
        for (aip = ailist; aip != NULL; aip = aip->ai_next) {
            if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
              aip->ai_addrlen, QLEN)) >= 0) {
                serve(sockfd);
                exit(0);
            }
        }
        exit(1);
    }
    

    为了获取地址,服务进程需要获取它所运行的主机的名称。有些系统没有定义_SC_HOST_NAME_MAX常量,所以我们使用HOST_NAME_MAX。如果系统没有定义HOST_NAME_MAX那么我们自己定义它。POSIX.1指明了主机名称的最小值为255字节(不包含结束符号),所以我们定义HOST_NAME_MAX为256字节以便能够包含结束符号。

    服务进程调用gethostname来得到主机的名称,同时查看用于remote uptime服务的地址。可能会返回多个地址,但是我们只是选择第一个来建立套接字末端。

    我们使用前面自己定义的initserver函数来初始化等待连接请求到达处的套接字末端(实际上,我们使用另外一个版本,具体还是参见参考资料吧)。

    另外一个可选面向连接的服务进程例子

    之前我们说过使用文件描述符号可以访问套接字,这一点非常重要,因为这样程序在网络环境下不用了解关于网络相关的信息。下面的程序就展示了这一点。不是读取uptime命令的标准输出并且将它发送到客户端,而是服务进程让uptime命令的标准输出和标准错误输出变成连接到客户端的套接字。

    #include <netdb.h>
    #include <errno.h>
    #include <syslog.h>
    #include <fcntl.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    
    #define QLEN 10
    
    #ifndef HOST_NAME_MAX
    #define HOST_NAME_MAX 256
    #endif
    
    extern int initserver(int, struct sockaddr *, socklen_t, int);
    
    void serve(int sockfd)
    {
        int     clfd, status;
        pid_t   pid;
    
        for (;;) {
            clfd = accept(sockfd, NULL, NULL);
            if (clfd < 0) {
                syslog(LOG_ERR, "ruptimed: accept error: %s",
                  strerror(errno));
                exit(1);
            }
            if ((pid = fork()) < 0) {
                syslog(LOG_ERR, "ruptimed: fork error: %s",
                  strerror(errno));
                exit(1);
            } else if (pid == 0) {  /* 子进程 */
                /* 父进程调用daemonize函数,这样STDIN_FILENO,STDOUT_FILENO,和STDERR_FILENO被打开到/dev/null。
                 * 因此,不需要通过检查clfd是否等于这些值之一来对调用close进行保护。
                 */
                if (dup2(clfd, STDOUT_FILENO) != STDOUT_FILENO ||
                  dup2(clfd, STDERR_FILENO) != STDERR_FILENO) {
                    syslog(LOG_ERR, "ruptimed: unexpected error");
                    exit(1);
                }
                close(clfd);
                execl("/usr/bin/uptime", "uptime", (char *)0);
                syslog(LOG_ERR, "ruptimed: unexpected return from exec: %s",
                  strerror(errno));
            } else {        /* 父进程 */
                close(clfd);
                waitpid(pid, &status, 0);
            }
        }
    }
    
    int main(int argc, char *argv[])
    {
        struct addrinfo *ailist, *aip;
        struct addrinfo hint;
        int             sockfd, err, n;
        char            *host;
        if (argc != 1)
            err_quit("usage: ruptimed");
    #ifdef _SC_HOST_NAME_MAX
        n = sysconf(_SC_HOST_NAME_MAX);
        if (n < 0)  /* best guess */
    #endif
            n = HOST_NAME_MAX;
        host = malloc(n);
        if (host == NULL)
            err_sys("malloc error");
        if (gethostname(host, n) < 0)
            err_sys("gethostname error");
        daemonize("ruptimed");
        hint.ai_flags = AI_CANONNAME;
        hint.ai_family = 0;
        hint.ai_socktype = SOCK_STREAM;
        hint.ai_protocol = 0;
        hint.ai_addrlen = 0;
        hint.ai_canonname = NULL;
        hint.ai_addr = NULL;
        hint.ai_next = NULL;
        if ((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) {
            syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s",
              gai_strerror(err));
            exit(1);
        }
        for (aip = ailist; aip != NULL; aip = aip->ai_next) {
            if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
              aip->ai_addrlen, QLEN)) >= 0) {
                serve(sockfd);
                exit(0);
            }
        }
        exit(1);
    }
    

    不是使用popen来运行uptime命令以及从连接到命令标准输出的管道上面读取输出,我们使用fork创建子进程并且之后使用dup2来让子进程的STDIN_FILENO打开到/dev/null,STDOUT_FILENO和STDERR_FILENO打开到套接字的末端。当我们执行uptime命令的时候,命令向标准输出写,因为标准输出连接到了套接字,数据会被发送会ruptime的客户端命令。

    父进程可以安全地关闭连接到客户端的文件描述符号,因为子进程仍旧打开着它。父进程在继续之前等待子进程完成,这样子进程不会变成僵尸进程。因为uptime命令不会占用太多的时间,所以父进程可以在接收下一条请求之前等待字进程退出。但是如果子进程执行过长的时间的化,可能这个方法就不那么好用了。

    前面的例子使用的是面向连接的套接字。但是我们如何选择合适的类型?什么时候我们使用面向连接的套接字,什么时候我们使用无连接的套接字?这个打开取决于我们需要做多少工作以及我们可以容忍多少的错误。

    对于一个无连接的套接字,对数据包到达的次序没有太多的要求,所以如果我们不能将我们所有的数据放到一个包中,我们就需要考虑次序的问题,包的最大长度是通信协议的特性。另外,对于无连接的套接字,可能会出现丢包的情况。如果我们的应用程序不能容忍丢包的情况,那么我们应该使用面向连接的套接字。

    容忍包的丢失以为着我们可以有两个选择。如果我们使用可靠的通信方式,那么我们需要给我们的包进行编号,当我们检测到包的丢失的时候可以请求重新传送。由于一个包可能会延迟导致好象是丢失了,但是在我们再次请求传输的时候又出现了,所以我们也应当可以辨别重复的包然后忽略它们。

    另外一个选择就是我们可以重新尝试命令来处理错误。对于一个简单的应用程序,这个就足够了,但是对于一个复杂的应用程序,它并不一定可行。所以,一般来说这时候我们最好使用面向连接的套接字服务。

    使用面向连接的套接字的缺点就是,我们在建立连接的时候需要做更多的工作和时间,并且每个连接都会消耗更多的操作系统资源。

    相关文章

      网友评论

        本文标题:APUE读书笔记-16网络通信(7)

        本文链接:https://www.haomeiwen.com/subject/cpijlktx.html