美文网首页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)

    面向连接与面向无连接通讯举例 面向连接的客户端的例子 下面的程序,显示了一个客户的命令,这个命令和服务进行通信,从...

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

    3、寻址 在前面的小节里面,我们学到了如何创建和销毁一个套接字。在我们使用套接字之前,我们需要知道如何定位到我们交...

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

    4、建立连接 如果我们处理一个面向连接的网络服务(SOCK_STREAM或者SOCK_SEQPACKET),那么在...

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

    6、套接字选项 套接字机制提供了两个套接字选项接口来控制套接字的行为。一个接口用来设置选项,另外一个接口用来允许我...

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

    (4)将地址和套接字进行关联 客户的套接字关联的地址并不是重点,我们可以让系统选择一个默认的地址。但是对于一个服务...

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

    7、带外数据 带外数据是一个被一些通信协议支持的可选的特性,它允许比普通数据优先级高的数据被传送。带外数据会被优先...

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

    1、简介 在前面的章节,我们查看了pipes,FIFOs,消息队列,信号量,和共享内存这些unix提供的经典的IP...

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

    5、数据传输 由于socket末端使用文件描述符号来替代,当被连接起来的时候,我们就可以使用read和write来...

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

    (3)地址查询 理想情况,一个应用程序不用知道套接字地址的内部结构。如果一个应用程序简单地把套接字地址作为sock...

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

    无连接客户的例子 下面的程序就是使用数据包接口的uptime客户端程序。 基于数据包的客户端的main函数和面向连...

网友评论

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

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