Loading...
墨滴

阿酷尔工作室

2021/10/16  阅读:32  主题:默认主题

未命名文章Beej网络编程指南

Beej网络编程指南《二》

6客户端-服务器背景

这是一个客户机-服务器的世界,宝贝。网络上几乎所有的东西都处理客户机进程与服务器进程之间的对话,反之亦然。以telnet为例。当你用telnet(客户机)连接到端口23上的远程主机时,该主机(称为telnetd,服务器)上的一个程序就会活跃起来。它处理传入的telnet连接,为你设置登录提示,等等。

客户端-服务器交互。

客户端和服务器之间的信息交换总结在上图中。

请注意,客户机-服务器对可以说SOCK_STREAMSOCK_DGRAM或其他任何东西(只要他们说的是同样的事情)。一些很好的客户机-服务器对的例子是telnet/telnetdftp/ftpdFirefox/Apache。每次你使用ftp,都有一个远程程序ftpd为你服务。

通常,一台机器上只有一台服务器,该服务器将使用fork()处理多个客户端。基本例程是:服务器将等待连接,接受()它,并使用fork()子进程来处理它。这是我们在下一节中的示例服务器所做的。

6.1一个简单的流服务器

这个服务器所做的就是通过一个流连接发送字符串“你好,世界!”。测试这个服务器所需要做的就是在一个窗口中运行它,并通过以下方式从另一个窗口telnet到它:

    $ telnet remotehostname 3490

其中Remote tehost name是运行它的机器的名称

服务器代码23

/*
** server.c -- a stream socket server demo
*/


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

#define PORT "3490"  // the port users will be connecting to

#define BACKLOG 10   // how many pending connections queue will hold

void sigchld_handler(int s)
{
    // waitpid() might overwrite errno, so we save and restore it:
    int saved_errno = errno;

    while(waitpid(-1NULL, WNOHANG) > 0);

    errno = saved_errno;
}


// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    int sockfd, new_fd;  // listen on sock_fd, new connection on new_fd
    struct addrinfo hints, *servinfo, *p;
    struct sockaddr_storage their_addr; // connector's address information
    socklen_t sin_size;
    struct sigaction sa;
    int yes=1;
    char s[INET6_ADDRSTRLEN];
    int rv;

    memset(&hints, 0sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE; // use my IP

    if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr"getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and bind to the first we can
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("server: socket");
            continue;
        }

        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
                sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }

        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("server: bind");
            continue;
        }

        break;
    }

    freeaddrinfo(servinfo); // all done with this structure

    if (p == NULL)  {
        fprintf(stderr"server: failed to bind\n");
        exit(1);
    }

    if (listen(sockfd, BACKLOG) == -1) {
        perror("listen");
        exit(1);
    }

    sa.sa_handler = sigchld_handler; // reap all dead processes
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }

    printf("server: waiting for connections...\n");

    while(1) {  // main accept() loop
        sin_size = sizeof their_addr;
        new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
        if (new_fd == -1) {
            perror("accept");
            continue;
        }

        inet_ntop(their_addr.ss_family,
            get_in_addr((struct sockaddr *)&their_addr),
            s, sizeof s);
        printf("server: got connection from %s\n", s);

        if (!fork()) { // this is the child process
            close(sockfd); // child doesn't need the listener
            if (send(new_fd, "Hello, world!"130) == -1)
                perror("send");
            close(new_fd);
            exit(0);
        }
        close(new_fd);  // parent doesn't need this
    }

    return 0;
}

如果你好奇的话,我把代码放在一个大的main()函数中,以便(我觉得)语法清晰。如果能让你感觉更好,可以把它分成更小的函数。

(另外,整个sigaction()对你来说可能是新的——没关系。那里代码负责收割在fork()ed子进程退出时出现的僵尸进程。如果你制造了很多僵尸却没有收割它们,你的系统管理员会变得焦虑不安。)

您可以使用下一节中列出的客户端从该服务器获取数据。

6.2一个简单的流客户端

这个人甚至比服务器更容易。这个客户端所做的只是连接到您在命令行中指定的主机,端口3490。它获取服务器发送的字符串。

客户来源24

/*
** client.c -- a stream socket client demo
*/


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

#include <arpa/inet.h>

#define PORT "3490" // the port client will be connecting to 

#define MAXDATASIZE 100 // max number of bytes we can get at once 

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(int argc, char *argv[])
{
    int sockfd, numbytes;  
    char buf[MAXDATASIZE];
    struct addrinfo hints, *servinfo, *p;
    int rv;
    char s[INET6_ADDRSTRLEN];

    if (argc != 2) {
        fprintf(stderr,"usage: client hostname\n");
        exit(1);
    }

    memset(&hints, 0sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr"getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and connect to the first we can
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("client: socket");
            continue;
        }

        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("client: connect");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr"client: failed to connect\n");
        return 2;
    }

    inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
            s, sizeof s);
    printf("client: connecting to %s\n", s);

    freeaddrinfo(servinfo); // all done with this structure

    if ((numbytes = recv(sockfd, buf, MAXDATASIZE-10)) == -1) {
        perror("recv");
        exit(1);
    }

    buf[numbytes] = '\0';

    printf("client: received '%s'\n",buf);

    close(sockfd);

    return 0;
}

请注意,如果在运行客户端之前没有运行服务器,连接()返回“连接拒绝”。非常有用。

6.3数据报套接字

我们已经在上面讨论了sendto()和recvfrom(),涵盖了UDP数据报套接字的基础知识,所以我将只介绍几个示例程序:talker. c和listener. c

监听器坐在一台机器上等待端口4950上的传入数据包。说话者在指定的机器上向该端口发送一个数据包,其中包含用户在命令行上输入的任何内容。

因为数据报套接字是无连接的,只是无情地将数据包发送到以太网中,无视成功,我们将告诉客户端和服务器使用特定的IPv6。这样我们就避免了服务器监听IPv6而客户端发送IPv4的情况;数据根本不会被接收。(在我们连接的TCP流套接字世界中,我们可能仍然存在不匹配,但是一个地址系列的连接()错误会导致我们重试另一个地址。)

这里是听众来源。

/*
** listener.c -- a datagram sockets "server" demo
*/


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define MYPORT "4950"    // the port users will be connecting to

#define MAXBUFLEN 100

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    int sockfd;
    struct addrinfo hints, *servinfo, *p;
    int rv;
    int numbytes;
    struct sockaddr_storage their_addr;
    char buf[MAXBUFLEN];
    socklen_t addr_len;
    char s[INET6_ADDRSTRLEN];

    memset(&hints, 0sizeof hints);
    hints.ai_family = AF_INET6; // set to AF_INET to use IPv4
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_flags = AI_PASSIVE; // use my IP

    if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0) {
        fprintf(stderr"getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and bind to the first we can
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("listener: socket");
            continue;
        }

        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("listener: bind");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr"listener: failed to bind socket\n");
        return 2;
    }

    freeaddrinfo(servinfo);

    printf("listener: waiting to recvfrom...\n");

    addr_len = sizeof their_addr;
    if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN-1 , 0,
        (struct sockaddr *)&their_addr, &addr_len)) == -1) {
        perror("recvfrom");
        exit(1);
    }

    printf("listener: got packet from %s\n",
        inet_ntop(their_addr.ss_family,
            get_in_addr((struct sockaddr *)&their_addr),
            s, sizeof s));
    printf("listener: packet is %d bytes long\n", numbytes);
    buf[numbytes] = '\0';
    printf("listener: packet contains \"%s\"\n", buf);

    close(sockfd);

    return 0;
}

请注意,在调用getaddrinfo()时,我们终于使用了SOCK_DGRAM。另外,请注意,没有必要监听()接受()。这是使用未连接数据报套接字的好处之一!

接下来是talker. c26的源代码:

/*
** talker.c -- a datagram "client" demo
*/


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define SERVERPORT "4950"    // the port users will be connecting to

int main(int argc, char *argv[])
{
    int sockfd;
    struct addrinfo hints, *servinfo, *p;
    int rv;
    int numbytes;

    if (argc != 3) {
        fprintf(stderr,"usage: talker hostname message\n");
        exit(1);
    }

    memset(&hints, 0sizeof hints);
    hints.ai_family = AF_INET6; // set to AF_INET to use IPv4
    hints.ai_socktype = SOCK_DGRAM;

    if ((rv = getaddrinfo(argv[1], SERVERPORT, &hints, &servinfo)) != 0) {
        fprintf(stderr"getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and make a socket
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("talker: socket");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr"talker: failed to create socket\n");
        return 2;
    }

    if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0,
             p->ai_addr, p->ai_addrlen)) == -1) {
        perror("talker: sendto");
        exit(1);
    }

    freeaddrinfo(servinfo);

    printf("talker: sent %d bytes to %s\n", numbytes, argv[1]);
    close(sockfd);

    return 0;
}

这就是它的全部!在一些机器上运行监听器,然后在另一台机器上运行通话器。观察他们的交流!整个核心家庭的有趣的G级兴奋!

这次你甚至不必运行服务器!你可以自己运行talker,它只是愉快地将数据包发送到以太网中,如果没有人准备好在另一边使用recvfrom),数据包就会消失。记住:使用UDP数据报套接字发送的数据不能保证到达!

除了我在过去多次提到的一个小细节:连接的数据报套接字。我需要在这里讨论这个问题,因为我们在文档的数据报部分。假设talker调用连接()并指定侦听器的地址。从那时起,talker只能发送到连接()指定的地址并从连接()指定的地址接收。因此,您不必使用sendto()和recvfrom();您可以简单地使用发送()和recv)。

7个稍微高级的技巧

这些并不是真正的高级,但是它们已经脱离了我们已经讨论过的更基本的水平。事实上,如果你已经走了这么远,你应该认为自己在Unix网络编程的基础上相当有成就!祝贺你!

所以在这里,我们进入了一个勇敢的新世界,一些你可能想了解的关于套接字的更深奥的东西。动手吧!

7.1阻塞

阻塞。你听说过它——现在它到底是什么?简而言之,“块”是“睡眠”的技术术语。你可能注意到,当你运行上面的监听器时,它就停在那里,直到数据包到达。实际情况是,它叫recvfrom(),没有数据,所以recvfrom()被称为“阻塞”(即在那里睡觉),直到一些数据到达。

许多函数块。接受()块。所有recv()函数都阻塞。他们能这样做的原因是因为他们被允许这样做。当你第一次用套接字()创建套接字描述符时,内核将其设置为阻塞。如果你不想让套接字阻塞,你必须调用fcntl()

#include <unistd.h>
#include <fcntl.h>
.
.
.
sockfd = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.

通过将套接字设置为非阻塞,您可以有效地“轮询”套接字以获取信息。如果您试图从非阻塞套接字读取,但那里没有数据,它不允许阻塞——它将返回-1errno将被设置为EAGAINEWOULDBLOCK

(等等-它可以返回``*EAGAIN或EWOULDBLOCK**?*您检查哪个?规范实际上没有指定您的系统将返回哪个,所以为了便于移植,请检查它们。)

然而,一般来说,这种类型的轮询是个坏主意。如果你让你的程序忙于等待套接字上的数据,你会占用CPU时间,就像它过时了一样。检查是否有数据等待读取的更优雅的解决方案出现在下面关于轮询)的部分。

同步I/O复用

您真正希望能够做的是以某种方式同时监视一堆套接字,然后处理那些已经准备好数据的套接字。这样,您就不必连续轮询所有这些套接字来查看哪些已经准备好读取了。

警告一句:当涉及到大量的连接时,民意测验*(*)非常慢。在这种情况下,您将从事件库中获得更好的性能,例如试图使用系统上可用的最快方法的**libevent27**。

那么,您如何避免轮询呢?颇具讽刺意味的是,您可以通过使用轮询)系统调用来避免轮询。简而言之,我们将要求操作系统为我们做所有的脏活,并且只要让我们知道一些数据何时可以在哪些套接字上读取。与此同时,我们的进程可以进入睡眠状态,从而节省系统资源。

一般的游戏计划是保留一个结构变量数组,其中包含我们想要监控的套接字描述符以及我们想要监控的事件类型的信息。操作系统将阻止轮询()调用,直到其中一个事件发生(例如“套接字准备读取!”)或直到用户指定的超时发生。

有用的是,当一个新的传入连接准备好被接受()ed时,听()ing套接字将返回“准备好阅读”。

玩笑开够了。我们怎么用这个?

    #include <poll.h>
    
    int poll(struct pollfd fds[], nfds_t nfds, int timeout);

fds是我们的信息数组(要监控哪些套接字),nfds是数组中元素的计数,timeout是以毫秒为单位的超时。它返回数组中发生事件的元素数量。

让我们来看看这个结构

    struct pollfd {
        int fd;         // the socket descriptor
        short events;   // bitmap of events we're interested in
        short revents;  // when poll() returns, bitmap of events that occurred
    };

所以我们将有一个数组,我们将看到每个元素的fd字段到我们感兴趣监视的套接字描述符。然后我们将设置事件字段来指示我们感兴趣的事件类型。

事件字段是以下内容的按位或:

MacroDescriptionPOLLINAlert me when data is ready to recv() on this socket.POLLOUTAlert me when I can send() data to this socket without blocking.

一旦你有了你的结构的数组,然后你可以把它传递给投票(),也传递数组的大小,以及一个超时值,以毫秒为单位。(你可以指定一个负超时永远等待。)

在轮询()返回后,您可以检查Revents字段,查看是否设置了POLLIN或POLLOUT,指示事件发生。

(实际上,您可以通过民意测验()调用做更多的事情。有关更多详细信息,请参阅下面的民意测验()手册页。)

这里有一个例子28,我们将等待2.5秒,让数据准备好从标准输入中读取,也就是说,当你点击返回时:

#include <stdio.h>
#include <poll.h>

int main(void)
{
    struct pollfd pfds[1]; // More if you want to monitor more

    pfds[0].fd = 0;          // Standard input
    pfds[0].events = POLLIN; // Tell me when ready to read

    // If you needed to monitor other things, as well:
    //pfds[1].fd = some_socket; // Some socket descriptor
    //pfds[1].events = POLLIN;  // Tell me when ready to read

    printf("Hit RETURN or wait 2.5 seconds for timeout\n");

    int num_events = poll(pfds, 12500); // 2.5 second timeout

    if (num_events == 0) {
        printf("Poll timed out!\n");
    } else {
        int pollin_happened = pfds[0].revents & POLLIN;

        if (pollin_happened) {
            printf("File descriptor %d is ready to read\n", pfds[0].fd);
        } else {
            printf("Unexpected event occurred: %d\n", pfds[0].revents);
        }
    }

    return 0;
}

请再次注意,民意测验()返回pfds数组中发生事件的元素数。它不告诉您数组中的哪些元素(您仍然需要扫描这些元素),但它告诉您有多少条目有一个非零的Revents字段(因此您可以在找到这么多之后停止扫描)。

这里可能会出现几个问题:如何向我传递给轮询()的集合中添加新的文件描述符?为此,只需确保数组中有足够的空间来满足您的所有需求,或者根据需要使用realloc()更多的空间。

如何从集合中删除项?为此,您可以将数组中的最后一个元素复制到要删除的元素之上。然后传入少一个元素作为轮询()的计数。另一个选项是,您可以将任何fd字段设置为负数,轮询()将忽略它。

我们怎样才能把它们都放在一个聊天服务器上,你可以远程登录?

我们要做的是启动一个侦听器套接字,并将其添加到要轮询()的文件描述符集中。(当有传入连接时,它将显示准备就绪。)

然后,我们将添加新的连接到我们的结构图数组。如果空间不足,我们将动态增长它。

当连接关闭时,我们将从数组中删除它。

当一个连接可以读取时,我们将从中读取数据,并将数据发送给所有其他连接,这样他们就可以看到其他用户键入的内容。

所以试试这个轮询服务器29。在一个窗口中运行它,然后从许多其他终端窗口中localhosttelnet 9034。您应该能够在其他窗口中看到您在一个窗口中键入的内容(在您点击返回后)。

不仅如此,如果您点击CTRL-]并键入退出telnet,服务器应该检测到断开,并将您从文件描述符数组中删除。

/*
** pollserver.c -- a cheezy multiperson chat server
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <poll.h>

#define PORT "9034"   // Port we're listening on

// Get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

// Return a listening socket
int get_listener_socket(void)
{
    int listener;     // Listening socket descriptor
    int yes=1;        // For setsockopt() SO_REUSEADDR, below
    int rv;

    struct addrinfo hints, *ai, *p;

    // Get us a socket and bind it
    memset(&hints, 0sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) {
        fprintf(stderr"selectserver: %s\n", gai_strerror(rv));
        exit(1);
    }
    
    for(p = ai; p != NULL; p = p->ai_next) {
        listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (listener < 0) { 
            continue;
        }
        
        // Lose the pesky "address already in use" error message
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));

        if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) {
            close(listener);
            continue;
        }

        break;
    }

    freeaddrinfo(ai); // All done with this

    // If we got here, it means we didn't get bound
    if (p == NULL) {
        return -1;
    }

    // Listen
    if (listen(listener, 10) == -1) {
        return -1;
    }

    return listener;
}

// Add a new file descriptor to the set
void add_to_pfds(struct pollfd *pfds[], int newfd, int *fd_count, int *fd_size)
{
    // If we don't have room, add more space in the pfds array
    if (*fd_count == *fd_size) {
        *fd_size *= 2// Double it

        *pfds = realloc(*pfds, sizeof(**pfds) * (*fd_size));
    }

    (*pfds)[*fd_count].fd = newfd;
    (*pfds)[*fd_count].events = POLLIN; // Check ready-to-read

    (*fd_count)++;
}

// Remove an index from the set
void del_from_pfds(struct pollfd pfds[], int i, int *fd_count)
{
    // Copy the one from the end over this one
    pfds[i] = pfds[*fd_count-1];

    (*fd_count)--;
}

// Main
int main(void)
{
    int listener;     // Listening socket descriptor

    int newfd;        // Newly accept()ed socket descriptor
    struct sockaddr_storage remoteaddr; // Client address
    socklen_t addrlen;

    char buf[256];    // Buffer for client data

    char remoteIP[INET6_ADDRSTRLEN];

    // Start off with room for 5 connections
    // (We'll realloc as necessary)
    int fd_count = 0;
    int fd_size = 5;
    struct pollfd *pfds = malloc(sizeof *pfds * fd_size);

    // Set up and get a listening socket
    listener = get_listener_socket();

    if (listener == -1) {
        fprintf(stderr"error getting listening socket\n");
        exit(1);
    }

    // Add the listener to set
    pfds[0].fd = listener;
    pfds[0].events = POLLIN; // Report ready to read on incoming connection

    fd_count = 1// For the listener

    // Main loop
    for(;;) {
        int poll_count = poll(pfds, fd_count, -1);

        if (poll_count == -1) {
            perror("poll");
            exit(1);
        }

        // Run through the existing connections looking for data to read
        for(int i = 0; i < fd_count; i++) {

            // Check if someone's ready to read
            if (pfds[i].revents & POLLIN) { // We got one!!

                if (pfds[i].fd == listener) {
                    // If listener is ready to read, handle new connection

                    addrlen = sizeof remoteaddr;
                    newfd = accept(listener,
                        (struct sockaddr *)&remoteaddr,
                        &addrlen);

                    if (newfd == -1) {
                        perror("accept");
                    } else {
                        add_to_pfds(&pfds, newfd, &fd_count, &fd_size);

                        printf("pollserver: new connection from %s on "
                            "socket %d\n",
                            inet_ntop(remoteaddr.ss_family,
                                get_in_addr((struct sockaddr*)&remoteaddr),
                                remoteIP, INET6_ADDRSTRLEN),
                            newfd);
                    }
                } else {
                    // If not the listener, we're just a regular client
                    int nbytes = recv(pfds[i].fd, buf, sizeof buf, 0);

                    int sender_fd = pfds[i].fd;

                    if (nbytes <= 0) {
                        // Got error or connection closed by client
                        if (nbytes == 0) {
                            // Connection closed
                            printf("pollserver: socket %d hung up\n", sender_fd);
                        } else {
                            perror("recv");
                        }

                        close(pfds[i].fd); // Bye!

                        del_from_pfds(pfds, i, &fd_count);

                    } else {
                        // We got some good data from a client

                        for(int j = 0; j < fd_count; j++) {
                            // Send to everyone!
                            int dest_fd = pfds[j].fd;

                            // Except the listener and ourselves
                            if (dest_fd != listener && dest_fd != sender_fd) {
                                if (send(dest_fd, buf, nbytes, 0) == -1) {
                                    perror("send");
                                }
                            }
                        }
                    }
                } // END handle data from client
            } // END got ready-to-read from poll()
        } // END looping through file descriptors
    } // END for(;;)--and you thought it would never end!
    
    return 0;
}

在下一节中,我们将看到一个类似的、更古老的函数,叫做选择()。选择()投票()都提供了相似的功能和性能,只是在使用方式上有很大的不同。选择()可能稍微更容易移植,但在使用中可能有点笨拙。选择你最喜欢的一个,只要你的系统支持它。

7.3 Select()-同步I/O多路复用,老派

这个函数有点奇怪,但它非常有用。假设以下情况:你是一个服务器,你想监听传入的连接,并继续从你已经拥有的连接中读取。

你说,没问题,只是一个接受()和几个recv()。没那么快,小家伙!如果你阻塞了一个接受()调用呢?你要如何同时接收)数据?“使用非阻塞套接字!”没门!你不想成为一个CPU猪。然后呢?

Select)允许您同时监视多个套接字。如果您真的想知道,它会告诉您哪些套接字可以读取,哪些可以写入,哪些套接字引发了异常。

提醒一句:selc*(*)虽然非常便携,但在处理大量连接时速度非常慢。在这种情况下,您将从事件库中获得更好的性能,例如**libevent30**,它试图使用您系统上可用的最快方法。

废话少说,我将提供selc)的概要:

    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int select(int numfds, fd_set *readfds, fd_set *writefds,
               fd_set *exceptfds, struct timeval *timeout)

该函数监视文件描述符的“集合”;特别是readfds、Writefds和exceptfds。如果您想看看是否可以从标准输入和一些套接字描述符(sokfd)中读取,只需将文件描述符0和sokfd添加到集合readfds中。参数值应该设置为最高文件描述符加1的值。在这个例子中,它应该设置为sokfd+1,因为它肯定高于标准输入(0)。

当Select()返回时,readfds将被修改,以反映您选择的哪些文件描述符可以读取。您可以使用下面的宏FD_ISSET()测试它们。

在深入讨论之前,我将讨论如何操作这些集合。每个集合都是fd_set类型的。以下宏对这种类型进行操作:

FunctionDescriptionFD_SET(int fd, fd_set *set);Add fd to the set.FD_CLR(int fd, fd_set *set);Remove fd from the set.FD_ISSET(int fd, fd_set *set);Return true if fd is in the set.FD_ZERO(fd_set *set);Clear all entries from the set.

最后,这个奇怪的结构时间评估是什么?嗯,有时候你不想永远等待别人给你发送一些数据。也许每隔96秒你就想打印“仍然在前进......”到终端,即使什么都没发生。这个时间结构允许你指定超时周期。如果超过时间,并且选择()仍然没有找到任何准备好的文件描述符,它会返回,这样你就可以继续处理了。

结构时间值具有以下字段:

    struct timeval {
        int tv_sec;     // seconds
        int tv_usec;    // microseconds
    }; 

只需将tv_sec设置为等待的秒数,并将tv_usec设置为等待的微秒数。是的,这是_micro_seconds,不是毫秒。一毫秒中有1000微秒,一秒中有1000毫秒。因此,一秒中有100万微秒。为什么是“usec”?“u”应该看起来像我们用来表示“微”的希腊字母μ(Mu)。此外,当函数返回时,超时可能会更新,以显示仍然剩余的时间。这取决于你运行的Unix的风格。

太好了!我们有一个微秒分辨率计时器!好吧,不要指望它。无论您设置的结构时间值有多小,您都可能需要等待标准Unix时间片的一部分。

其他感兴趣的事情:如果你将你的结构timava中的字段设置为0,Select()将立即超时,有效地轮询你集合中的所有文件描述符。如果你将参数超时设置为NULL,它永远不会超时,并且会等到第一个文件描述符准备好。最后,如果你不关心等待某个集合,你可以在调用选择()中将其设置为NULL。

下面的代码片段31等待2.5秒,等待标准输入上出现某些内容:

/*
** select.c -- a select() demo
*/


#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define STDIN 0  // file descriptor for standard input

int main(void)
{
    struct timeval tv;
    fd_set readfds;

    tv.tv_sec = 2;
    tv.tv_usec = 500000;

    FD_ZERO(&readfds);
    FD_SET(STDIN, &readfds);

    // don't care about writefds and exceptfds:
    select(STDIN+1, &readfds, NULLNULL, &tv);

    if (FD_ISSET(STDIN, &readfds))
        printf("A key was pressed!\n");
    else
        printf("Timed out.\n");

    return 0;

如果你在一个线路缓冲终端上,你按下的键应该是返回,否则它会超时。

现在,你们中的一些人可能认为这是在数据报套接字上等待数据的好方法——你是对的:可能*是这样。*一些单位可以以这种方式使用选择,一些不能。如果你想尝试,你应该看看你的本地手册页面在这个问题上说了什么。

一些Unices会更新你的结构时间值,以反映超时前剩余的时间。但其他人不会。如果你想便携,不要依赖这种情况。(如果你需要跟踪时间流逝,请使用gettimeofday()。我知道这很糟糕,但事实就是如此。)

如果读取集中的套接字关闭连接会发生什么?在这种情况下,选择()返回,套接字描述符设置为“准备读取”。当您实际执行recv()时,recv()将返回0。这就是您如何知道客户端已经关闭了连接。

关于selc()还有一个有趣的注意事项:如果您有一个套接字正在监听()ing,您可以通过将该套接字的文件描述符放入readfds集中来检查是否有新的连接。

我的朋友们,这是对全能选择()函数的快速概述。

但是,根据大众的要求,这里有一个深入的例子。不幸的是,上面这个非常简单的例子和这里这个例子之间的区别很大。但是看一看,然后阅读下面的描述。

这个程序32就像一个简单的多用户聊天服务器。在一个窗口中启动它,然后从多个其他窗口远程登录到它(“远程登录主机名9034”)。当你在一个远程登录会话中键入某些内容时,它应该会出现在所有其他窗口中。

/*
** selectserver.c -- a cheezy multiperson chat server
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define PORT "9034"   // port we're listening on

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    fd_set master;    // master file descriptor list
    fd_set read_fds;  // temp file descriptor list for select()
    int fdmax;        // maximum file descriptor number

    int listener;     // listening socket descriptor
    int newfd;        // newly accept()ed socket descriptor
    struct sockaddr_storage remoteaddr; // client address
    socklen_t addrlen;

    char buf[256];    // buffer for client data
    int nbytes;

    char remoteIP[INET6_ADDRSTRLEN];

    int yes=1;        // for setsockopt() SO_REUSEADDR, below
    int i, j, rv;

    struct addrinfo hints, *ai, *p;

    FD_ZERO(&master);    // clear the master and temp sets
    FD_ZERO(&read_fds);

    // get us a socket and bind it
    memset(&hints, 0sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) {
        fprintf(stderr"selectserver: %s\n", gai_strerror(rv));
        exit(1);
    }
    
    for(p = ai; p != NULL; p = p->ai_next) {
        listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (listener < 0) { 
            continue;
        }
        
        // lose the pesky "address already in use" error message
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));

        if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) {
            close(listener);
            continue;
        }

        break;
    }

    // if we got here, it means we didn't get bound
    if (p == NULL) {
        fprintf(stderr"selectserver: failed to bind\n");
        exit(2);
    }

    freeaddrinfo(ai); // all done with this

    // listen
    if (listen(listener, 10) == -1) {
        perror("listen");
        exit(3);
    }

    // add the listener to the master set
    FD_SET(listener, &master);

    // keep track of the biggest file descriptor
    fdmax = listener; // so far, it's this one

    // main loop
    for(;;) {
        read_fds = master; // copy it
        if (select(fdmax+1, &read_fds, NULLNULLNULL) == -1) {
            perror("select");
            exit(4);
        }

        // run through the existing connections looking for data to read
        for(i = 0; i <= fdmax; i++) {
            if (FD_ISSET(i, &read_fds)) { // we got one!!
                if (i == listener) {
                    // handle new connections
                    addrlen = sizeof remoteaddr;
                    newfd = accept(listener,
                        (struct sockaddr *)&remoteaddr,
                        &addrlen);

                    if (newfd == -1) {
                        perror("accept");
                    } else {
                        FD_SET(newfd, &master); // add to master set
                        if (newfd > fdmax) {    // keep track of the max
                            fdmax = newfd;
                        }
                        printf("selectserver: new connection from %s on "
                            "socket %d\n",
                            inet_ntop(remoteaddr.ss_family,
                                get_in_addr((struct sockaddr*)&remoteaddr),
                                remoteIP, INET6_ADDRSTRLEN),
                            newfd);
                    }
                } else {
                    // handle data from a client
                    if ((nbytes = recv(i, buf, sizeof buf, 0)) <= 0) {
                        // got error or connection closed by client
                        if (nbytes == 0) {
                            // connection closed
                            printf("selectserver: socket %d hung up\n", i);
                        } else {
                            perror("recv");
                        }
                        close(i); // bye!
                        FD_CLR(i, &master); // remove from master set
                    } else {
                        // we got some data from a client
                        for(j = 0; j <= fdmax; j++) {
                            // send to everyone!
                            if (FD_ISSET(j, &master)) {
                                // except the listener and ourselves
                                if (j != listener && j != i) {
                                    if (send(j, buf, nbytes, 0) == -1) {
                                        perror("send");
                                    }
                                }
                            }
                        }
                    }
                } // END handle data from client
            } // END got new incoming connection
        } // END looping through file descriptors
    } // END for(;;)--and you thought it would never end!
    
    return 0;
}

请注意,我在代码中有两个文件描述符集:masterread_fds。第一个,master,保存当前连接的所有套接字描述符,以及正在监听新连接的套接字描述符。

我之所以有主集,是因为Select()``*实际上改变了*``您传递给它的集,以反映哪些套接字可以读取。因为我必须跟踪从一个选择()调用到下一个调用的连接,所以我必须把它们安全地存储在某个地方。在最后一分钟,我将主文件复制到read_fds,然后调用Select()。

但是这难道不意味着每次我得到一个新连接,我都必须把它添加到主集中吗是的。每次连接关闭,我都必须把它从主集中删除?是的。

请注意,我检查侦听器套接字何时准备好读取。当它准备好时,这意味着我有一个新的连接挂起,我接受()它并将其添加到主集中。类似地,当客户端连接准备好读取时,recv()返回0,我知道客户端已经关闭了连接,我必须将其从主集中删除。

但是,如果客户端recv)返回非零,我知道已经收到了一些数据。所以我得到了它,然后通过主列表并将这些数据发送给所有其他连接的客户端。

我的朋友们,这是对全能选择()函数的不简单概述。

快速提醒所有Linux爱好者:有时,在极少数情况下,Linux的Select()可以返回“准备好阅”,但实际上并没有准备好阅读!这意味着它会在Select()说不会后阻止读取()!为什么你这么小-!无论如何,解决方案是在接收套接字上设置O_NONBLOCK标志,这样它就会出现EWOULDBLOCK错误(如果发生这种情况,你可以安全地忽略它)。有关将套接字设置为非阻塞的更多信息,请参阅fcntl()参考页面

此外,这里还有一个额外的事后想法:还有另一个叫做轮询()的函数,它的行为与选择()几乎相同,但是有一个不同的系统来管理文件描述符集。看看吧

7.4处理部分发送

还记得在上面关于发送()的部分,我说过发送()可能不会发送你要求它发送的所有字节吗?也就是说,你希望它发送512个字节,但它返回412个。剩下的100个字节呢?

嗯,它们还在你的小缓冲区里等着被发送出去。由于你无法控制的情况,内核决定不把所有的数据集中在一个块中发送出去,现在,我的朋友,由你来获取数据。

你也可以写一个这样的函数来完成它:

#include <sys/types.h>
#include <sys/socket.h>

int sendall(int s, char *buf, int *len)
{
    int total = 0;        // how many bytes we've sent
    int bytesleft = *len; // how many we have left to send
    int n;

    while(total < *len) {
        n = send(s, buf+total, bytesleft, 0);
        if (n == -1) { break; }
        total += n;
        bytesleft -= n;
    }

    *len = total; // return number actually sent here

    return n==-1?-1:0// return -1 on failure, 0 on success

在本例中,s是要将数据发送到的套接字,buf是包含数据的缓冲区,len是指向包含缓冲区中字节数的int的指针。

该函数在错误时返回-1(并且errno仍然从调用发送()中设置)。此外,在len中返回实际发送的字节数。这将与您要求它发送的字节数相同,除非有错误。sendall()将尽最大努力,气喘吁吁地发送数据,但如果有错误,它会立即返回给您。

为了完整起见,下面是对函数的示例调用:

char buf[10] = "Beej!";
int len;

len = strlen(buf);
if (sendall(s, buf, &len) == -1) {
    perror("sendall");
    printf("We only sent %d bytes because of the error!\n", len);

当数据包的一部分到达时,接收者端会发生什么?如果数据包是可变长度的,接收者如何知道一个数据包何时结束,另一个数据包何时开始?是的,现实世界的场景是驴子们的大麻烦。你可能必须封装(还记得开头的数据封装部分吗?)继续阅读细节!

7.5序列化-如何打包数据

你发现,在网络上发送文本数据很容易,但是如果你想发送一些“二进制”数据,比如int或浮动,会发生什么呢事实证明你有几个选择。

  1. 将数字转换为文本,然后发送文本。接收者将使用strtol()等函数将文本解析回数字。
  2. 只需发送原始数据,传递一个指针到要发送的数据()。
  3. 将数字编码成便携式二进制形式。接收器将解码它。

偷偷预演!只有今晚!

[帷幕升起]

Beej说,“我更喜欢上面的方法三!”

[结束]

(在我认真开始这一部分之前,我应该告诉你,有很多库可以做这件事,滚动你自己的库并保持可移植性和无错误性是一个相当大的挑战。所以在决定自己实现这些东西之前,四处寻找并做足功课。我在这里提供了一些信息,供那些对这种东西是如何工作的好奇的人使用。)

事实上,上面所有的方法都有它们的缺点和优点,但是,就像我说的,总的来说,我更喜欢第三种方法。不过,首先让我们谈谈其他两种方法的一些缺点和优点。

第一种方法,在发送前将数字编码为文本,其优点是您可以轻松打印和读取通过线路传输的数据。有时,人类可读的协议非常适合在非带宽密集型情况下使用,例如互联网中继聊天(IRC)33。然而,它的缺点是转换速度慢,结果几乎总是比原始数字占用更多空间!

方法二:传递原始数据。这个很简单(但是很危险!):只需要一个指向要发送的数据的指针,然后用它调用发送。

    double d = 3490.15926535;
    
    send(s, &d, sizeof d, 0);  /* DANGER--non-portable! */

接收器得到它像这样:

    double d;
    
    recv(s, &d, sizeof d, 0);  /* DANGER--non-portable! */

快速,简单——有什么不喜欢的呢?嗯,事实证明,并不是所有的体系结构都用相同的位表示甚至相同的字节顺序来表示双精度(或者int)!代码显然是不可移植的。(嘿——也许你不需要可移植性,在这种情况下,这很好也很快。)

当打包整数类型时,我们已经看到hton()类函数如何通过将数字转换为网络字节顺序来帮助保持数据的可移植性,以及这是如何做的正确的事情。不幸的是,对于浮点类型没有类似的函数。所有的希望都破灭了吗?

不要害怕!(你有一秒钟害怕吗?没有?一点也不?)我们可以做一些事情:我们可以将数据打包(或“列表”,或“序列化”,或一百万个其他名称中的一个)成一种已知的二进制格式,接收者可以在远程端解压缩。

我所说的已知二进制格式是什么意思?嗯,我们已经见过hton()的例子了,对吗?它将(或者“编码”,如果你想这样想的话)一个数字从任何主机格式转换成网络字节顺序。为了逆转(解开编码)这个数字,接收者调用ntohs()

但是我不是刚刚说完了其他非整数类型没有这样的函数吗?是的。我做到了。因为在C中没有标准的方法来做到这一点,所以这有点棘手(对你们这些Python爱好者来说,这是一个免费的双关语)。

要做的事情是将数据打包成一种已知的格式,并通过电线发送以进行解码。例如,要打包浮动,这里有一些快速而肮脏的东西,还有很大的改进空间34:

#include <stdint.h>

uint32_t htonf(float f)
{
    uint32_t p;
    uint32_t sign;

    if (f < 0) { sign = 1; f = -f; }
    else { sign = 0; }
        
    p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign
    p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff// fraction

    return p;
}

float ntohf(uint32_t p)
{
    float f = ((p>>16)&0x7fff); // whole part
    f += (p&0xffff) / 65536.0f// fraction

    if (((p>>31)&0x1) == 0x1) { f = -f; } // sign bit set

    return f;
}

上面的代码是一种简单的实现,它将浮点数存储32位数字中。高位(31)用于存储数字的符号(“1”表示负数),接下来的7位(30-16)用于存储浮点数的整个数字部分。最后,剩余的位(15-0)用于存储数字的小数部分。

用法相当简单:

#include <stdio.h>

int main(void)
{
    float f = 3.1415926, f2;
    uint32_t netf;

    netf = htonf(f);  // convert to "network" form
    f2 = ntohf(netf); // convert back to test

    printf("Original: %f\n", f);        // 3.141593
    printf(" Network: 0x%08X\n", netf); // 0x0003243F
    printf("Unpacked: %f\n", f2);       // 3.141586

    return 0;
}

从好的方面来看,它小、简单、快速。从负的方面来看,它没有有效地利用空间,范围也受到严重限制——试着在那里存储一个大于32767的数字,它不会很开心!你也可以在上面的例子中看到最后几个小数位没有正确保存。

我们能做什么呢?存储浮点数的标准被称为IEEE-75435。大多数计算机在内部使用这种格式进行浮点数学运算,所以严格来说,在这种情况下,不需要进行转换。但是如果你希望你的源代码是可移植的,这是一个你不一定能做的假设。(另一方面,如果你想事情变得快速,你应在不需要这样做的平台上优化它!这就是hton()及其同类产品所做的。)

这里有一些编码浮点数并加倍为IEEE-754格式的代码。(主要是——它不编码NaN或无限,但可以修改为这样。)

#define pack754_32(f) (pack754((f), 32, 8))
#define pack754_64(f) (pack754((f), 64, 11))
#define unpack754_32(i) (unpack754((i), 32, 8))
#define unpack754_64(i) (unpack754((i), 64, 11))

uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
    long double fnorm;
    int shift;
    long long sign, exp, significand;
    unsigned significandbits = bits - expbits - 1// -1 for sign bit

    if (f == 0.0return 0// get this special case out of the way

    // check sign and begin normalization
    if (f < 0) { sign = 1; fnorm = -f; }
    else { sign = 0; fnorm = f; }

    // get the normalized form of f and track the exponent
    shift = 0;
    while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
    while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
    fnorm = fnorm - 1.0;

    // calculate the binary form (non-float) of the significand data
    significand = fnorm * ((1LL<<significandbits) + 0.5f);

    // get the biased exponent
    exp = shift + ((1<<(expbits-1)) - 1); // shift + bias

    // return the final answer
    return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}

long double unpack754(uint64_t i, unsigned bits, unsigned expbits)
{
    long double result;
    long long shift;
    unsigned bias;
    unsigned significandbits = bits - expbits - 1// -1 for sign bit

    if (i == 0return 0.0;

    // pull the significand
    result = (i&((1LL<<significandbits)-1)); // mask
    result /= (1LL<<significandbits); // convert back to float
    result += 1.0f// add the one back on

    // deal with the exponent
    bias = (1<<(expbits-1)) - 1;
    shift = ((i>>significandbits)&((1LL<<expbits)-1)) - bias;
    while(shift > 0) { result *= 2.0; shift--; }
    while(shift < 0) { result /= 2.0; shift++; }

    // sign it
    result *= (i>>(bits-1))&1-1.01.0;

    return result;
}

我在顶部放了一些方便的宏,用于打包和解包32位(可能是浮点数和64位(可能是双倍数)的数字,但是Pack754()函数可以直接调用,并被告知对价的数据进行编码(其扩展位保留给标准化数字的指数)。

以下是示例用法:

#include <stdio.h>
#include <stdint.h> // defines uintN_t types
#include <inttypes.h> // defines PRIx macros

int main(void)
{
    float f = 3.1415926, f2;
    double d = 3.14159265358979323, d2;
    uint32_t fi;
    uint64_t di;

    fi = pack754_32(f);
    f2 = unpack754_32(fi);

    di = pack754_64(d);
    d2 = unpack754_64(di);

    printf("float before : %.7f\n", f);
    printf("float encoded: 0x%08" PRIx32 "\n", fi);
    printf("float after  : %.7f\n\n", f2);

    printf("double before : %.20lf\n", d);
    printf("double encoded: 0x%016" PRIx64 "\n", di);
    printf("double after  : %.20lf\n", d2);

    return 0;
}

上面的代码产生这个输出:

    float before : 3.1415925
    float encoded: 0x40490FDA
    float after  : 3.1415925
    
    double before : 3.14159265358979311600
    double encoded: 0x400921FB54442D18
    double after  : 3.14159265358979311600

你可能会问的另一个问题是如何打包结构?对你来说不幸的是,编译器可以自由地将填充放在结构中的所有地方,这意味着你不能将整个东西打包成一个块。(你是不是厌倦了听到“不能做这个”、“不能做那个”?抱歉!引用一个朋友的话,“每当出问题时,我总是责怪微软。”诚然,这可能不是微软的错,但我朋友的说法完全正确。)

回到正题:在导线上发送结构的最佳方式是独立打包每个字段,然后当它们到达另一边时将它们解压缩到结构中。

这是一个很大的工作,这就是你所想的。是的,是的。你可以做的一件事是编写一个助手函数来帮助你打包数据。这会很有趣的!真的!

在Kernighan和Pike*的《编程实践37*》一书中,他们实现了类似printf()的函数,称为pack()和unpack(),正是这样做的。我想链接到他们,但显然这些函数与书中的其他来源不在线。

《编程实践》是一本很好的读物。每次我推荐它,宙斯都会救一只小猫。)

在这一点上,我将删除一个指针,指向C38中的一个协议缓冲区实现,我从未使用过,但看起来完全值得尊敬。Python和Perl程序员会想检查他们语言的pack()和unpack)函数来完成同样的事情。Java有一个大的ol“序列化接口,可以用类似的方式使用。

但是如果你想用C语言编写你自己的打包实用程序,K&P的诀窍是使用可变参数列表来创建类似printf()的函数来构建数据包。这是我自己根据这个版本制作的一个版本,希望它足以让你了解这种东西是如何工作的。

(这段代码引用了上面的Pack754()函数。Packi*()函数的操作类似于我们熟悉的hton(系列,只是它们打包成一个char数组而不是另一个整数。)

#include <stdio.h>
#include <ctype.h>
#include <stdarg.h>
#include <string.h>

/*
** packi16() -- store a 16-bit int into a char buffer (like htons())
*/
 
void packi16(unsigned char *buf, unsigned int i)
{
    *buf++ = i>>8; *buf++ = i;
}

/*
** packi32() -- store a 32-bit int into a char buffer (like htonl())
*/
 
void packi32(unsigned char *buf, unsigned long int i)
{
    *buf++ = i>>24; *buf++ = i>>16;
    *buf++ = i>>8;  *buf++ = i;
}

/*
** packi64() -- store a 64-bit int into a char buffer (like htonl())
*/
 
void packi64(unsigned char *buf, unsigned long long int i)
{
    *buf++ = i>>56; *buf++ = i>>48;
    *buf++ = i>>40; *buf++ = i>>32;
    *buf++ = i>>24; *buf++ = i>>16;
    *buf++ = i>>8;  *buf++ = i;
}

/*
** unpacki16() -- unpack a 16-bit int from a char buffer (like ntohs())
*/
 
int unpacki16(unsigned char *buf)
{
    unsigned int i2 = ((unsigned int)buf[0]<<8) | buf[1];
    int i;

    // change unsigned numbers to signed
    if (i2 <= 0x7fffu) { i = i2; }
    else { i = -1 - (unsigned int)(0xffffu - i2); }

    return i;
}

/*
** unpacku16() -- unpack a 16-bit unsigned from a char buffer (like ntohs())
*/
 
unsigned int unpacku16(unsigned char *buf)
{
    return ((unsigned int)buf[0]<<8) | buf[1];
}

/*
** unpacki32() -- unpack a 32-bit int from a char buffer (like ntohl())
*/
 
long int unpacki32(unsigned char *buf)
{
    unsigned long int i2 = ((unsigned long int)buf[0]<<24) |
                           ((unsigned long int)buf[1]<<16) |
                           ((unsigned long int)buf[2]<<8)  |
                           buf[3];
    long int i;

    // change unsigned numbers to signed
    if (i2 <= 0x7fffffffu) { i = i2; }
    else { i = -1 - (long int)(0xffffffffu - i2); }

    return i;
}

/*
** unpacku32() -- unpack a 32-bit unsigned from a char buffer (like ntohl())
*/
 
unsigned long int unpacku32(unsigned char *buf)
{
    return ((unsigned long int)buf[0]<<24) |
           ((unsigned long int)buf[1]<<16) |
           ((unsigned long int)buf[2]<<8)  |
           buf[3];
}

/*
** unpacki64() -- unpack a 64-bit int from a char buffer (like ntohl())
*/
 
long long int unpacki64(unsigned char *buf)
{
    unsigned long long int i2 = ((unsigned long long int)buf[0]<<56) |
                                ((unsigned long long int)buf[1]<<48) |
                                ((unsigned long long int)buf[2]<<40) |
                                ((unsigned long long int)buf[3]<<32) |
                                ((unsigned long long int)buf[4]<<24) |
                                ((unsigned long long int)buf[5]<<16) |
                                ((unsigned long long int)buf[6]<<8)  |
                                buf[7];
    long long int i;

    // change unsigned numbers to signed
    if (i2 <= 0x7fffffffffffffffu) { i = i2; }
    else { i = -1 -(long long int)(0xffffffffffffffffu - i2); }

    return i;
}

/*
** unpacku64() -- unpack a 64-bit unsigned from a char buffer (like ntohl())
*/
 
unsigned long long int unpacku64(unsigned char *buf)
{
    return ((unsigned long long int)buf[0]<<56) |
           ((unsigned long long int)buf[1]<<48) |
           ((unsigned long long int)buf[2]<<40) |
           ((unsigned long long int)buf[3]<<32) |
           ((unsigned long long int)buf[4]<<24) |
           ((unsigned long long int)buf[5]<<16) |
           ((unsigned long long int)buf[6]<<8)  |
           buf[7];
}

/*
** pack() -- store data dictated by the format string in the buffer
*
**   bits |signed   unsigned   float   string
**   -----+----------------------------------
**      8 |   c        C         
**     16 |   h        H         f
**     32 |   l        L         d
**     64 |   q        Q         g
**      - |                               s
*
**  (16-bit unsigned length is automatically prepended to strings)
*/
 

unsigned int pack(unsigned char *buf, char *format, ...)
{
    va_list ap;

    signed char c;              // 8-bit
    unsigned char C;

    int h;                      // 16-bit
    unsigned int H;

    long int l;                 // 32-bit
    unsigned long int L;

    long long int q;            // 64-bit
    unsigned long long int Q;

    float f;                    // floats
    double d;
    long double g;
    unsigned long long int fhold;

    char *s;                    // strings
    unsigned int len;

    unsigned int size = 0;

    va_start(ap, format);

    for(; *format != '\0'; format++) {
        switch(*format) {
        case 'c'// 8-bit
            size += 1;
            c = (signed char)va_arg(ap, int); // promoted
            *buf++ = c;
            break;

        case 'C'// 8-bit unsigned
            size += 1;
            C = (unsigned char)va_arg(ap, unsigned int); // promoted
            *buf++ = C;
            break;

        case 'h'// 16-bit
            size += 2;
            h = va_arg(ap, int);
            packi16(buf, h);
            buf += 2;
            break;

        case 'H'// 16-bit unsigned
            size += 2;
            H = va_arg(ap, unsigned int);
            packi16(buf, H);
            buf += 2;
            break;

        case 'l'// 32-bit
            size += 4;
            l = va_arg(ap, long int);
            packi32(buf, l);
            buf += 4;
            break;

        case 'L'// 32-bit unsigned
            size += 4;
            L = va_arg(ap, unsigned long int);
            packi32(buf, L);
            buf += 4;
            break;

        case 'q'// 64-bit
            size += 8;
            q = va_arg(ap, long long int);
            packi64(buf, q);
            buf += 8;
            break;

        case 'Q'// 64-bit unsigned
            size += 8;
            Q = va_arg(ap, unsigned long long int);
            packi64(buf, Q);
            buf += 8;
            break;

        case 'f'// float-16
            size += 2;
            f = (float)va_arg(ap, double); // promoted
            fhold = pack754_16(f); // convert to IEEE 754
            packi16(buf, fhold);
            buf += 2;
            break;

        case 'd'// float-32
            size += 4;
            d = va_arg(ap, double);
            fhold = pack754_32(d); // convert to IEEE 754
            packi32(buf, fhold);
            buf += 4;
            break;

        case 'g'// float-64
            size += 8;
            g = va_arg(ap, long double);
            fhold = pack754_64(g); // convert to IEEE 754
            packi64(buf, fhold);
            buf += 8;
            break;

        case 's'// string
            s = va_arg(ap, char*);
            len = strlen(s);
            size += len + 2;
            packi16(buf, len);
            buf += 2;
            memcpy(buf, s, len);
            buf += len;
            break;
        }
    }

    va_end(ap);

    return size;
}

/*
** unpack() -- unpack data dictated by the format string into the buffer
*
**   bits |signed   unsigned   float   string
**   -----+----------------------------------
**      8 |   c        C         
**     16 |   h        H         f
**     32 |   l        L         d
**     64 |   q        Q         g
**      - |                               s
*
**  (string is extracted based on its stored length, but 's' can be
**  prepended with a max length)
*/

void unpack(unsigned char *buf, char *format, ...)
{
    va_list ap;

    signed char *c;              // 8-bit
    unsigned char *C;

    int *h;                      // 16-bit
    unsigned int *H;

    long int *l;                 // 32-bit
    unsigned long int *L;

    long long int *q;            // 64-bit
    unsigned long long int *Q;

    float *f;                    // floats
    double *d;
    long double *g;
    unsigned long long int fhold;

    char *s;
    unsigned int len, maxstrlen=0, count;

    va_start(ap, format);

    for(; *format != '\0'; format++) {
        switch(*format) {
        case 'c'// 8-bit
            c = va_arg(ap, signed char*);
            if (*buf <= 0x7f) { *c = *buf;} // re-sign
            else { *c = -1 - (unsigned char)(0xffu - *buf); }
            buf++;
            break;

        case 'C'// 8-bit unsigned
            C = va_arg(ap, unsigned char*);
            *C = *buf++;
            break;

        case 'h'// 16-bit
            h = va_arg(ap, int*);
            *h = unpacki16(buf);
            buf += 2;
            break;

        case 'H'// 16-bit unsigned
            H = va_arg(ap, unsigned int*);
            *H = unpacku16(buf);
            buf += 2;
            break;

        case 'l'// 32-bit
            l = va_arg(ap, long int*);
            *l = unpacki32(buf);
            buf += 4;
            break;

        case 'L'// 32-bit unsigned
            L = va_arg(ap, unsigned long int*);
            *L = unpacku32(buf);
            buf += 4;
            break;

        case 'q'// 64-bit
            q = va_arg(ap, long long int*);
            *q = unpacki64(buf);
            buf += 8;
            break;

        case 'Q'// 64-bit unsigned
            Q = va_arg(ap, unsigned long long int*);
            *Q = unpacku64(buf);
            buf += 8;
            break;

        case 'f'// float
            f = va_arg(ap, float*);
            fhold = unpacku16(buf);
            *f = unpack754_16(fhold);
            buf += 2;
            break;

        case 'd'// float-32
            d = va_arg(ap, double*);
            fhold = unpacku32(buf);
            *d = unpack754_32(fhold);
            buf += 4;
            break;

        case 'g'// float-64
            g = va_arg(ap, long double*);
            fhold = unpacku64(buf);
            *g = unpack754_64(fhold);
            buf += 8;
            break;

        case 's'// string
            s = va_arg(ap, char*);
            len = unpacku16(buf);
            buf += 2;
            if (maxstrlen > 0 && len >= maxstrlen) count = maxstrlen - 1;
            else count = len;
            memcpy(s, buf, count);
            s[count] = '\0';
            buf += len;
            break;

        default:
            if (isdigit(*format)) { // track max str len
                maxstrlen = maxstrlen * 10 + (*format-'0');
            }
        }

        if (!isdigit(*format)) maxstrlen = 0;
    }

    va_end(ap);
}

这里是上面代码的一个演示程序40,它将一些数据打包到buf中,然后将其解压缩到变量中。请注意,当使用字符串参数(格式说明符"s")调用unpack()时,明智的做法是在它前面放一个最大长度计数,以防止缓冲区溢出,例如"96s"。在解包你通过网络获得的数据时要小心——恶意用户可能会发送构造不当的数据包来攻击你的系统!

#include <stdio.h>

// various bits for floating point types--
// varies for different architectures
typedef float float32_t;
typedef double float64_t;

int main(void)
{
    unsigned char buf[1024];
    int8_t magic;
    int16_t monkeycount;
    int32_t altitude;
    float32_t absurdityfactor;
    char *s = "Great unmitigated Zot! You've found the Runestaff!";
    char s2[96];
    int16_t packetsize, ps2;

    packetsize = pack(buf, "chhlsf", (int8_t)'B', (int16_t)0, (int16_t)37
            (int32_t)-5, s, (float32_t)-3490.6677);
    packi16(buf+1, packetsize); // store packet size in packet for kicks

    printf("packet is %" PRId32 " bytes\n", packetsize);

    unpack(buf, "chhl96sf", &magic, &ps2, &monkeycount, &altitude, s2,
        &absurdityfactor);

    printf("'%c' %" PRId32" %" PRId16 " %" PRId32
            " \"%s\" %f\n", magic, ps2, monkeycount,
            altitude, s2, absurdityfactor);

    return 0;
}

无论你是滚动自己的代码还是使用别人的代码,为了控制错误,最好有一套通用的数据打包例程,而不是每次都手工打包。

打包数据时,使用哪种格式比较好?问得好。幸运的是,外部数据表示标准RFC 450641已经为许多不同类型定义了二进制格式,如浮点类型、整数类型、数组、原始数据等。如果你要自己滚动数据,我建议你遵循这一标准。但你没有义务这么做。分组警察不会就在你的门外。至少,我认为他们不会。

无论如何,在发送数据之前以某种方式编码数据是正确的做事方式!

7.6数据封装之子

不管怎样,封装数据到底意味着什么?在最简单的情况下,这意味着你要在上面贴一个标头,上面有一些识别信息或数据包长度,或者两者兼而有之。

你的标题应该是什么样子的?嗯,它只是一些二进制数据,代表你认为完成项目所必需的任何东西。

哇。太含糊了。

好的。例如,假设你有一个使用SOCK_STREAMs的多用户聊天程序。当用户键入(“说”)某事时,需要向服务器传输两条信息:说了什么和谁说了什么。

到目前为止还好吗?“有什么问题?”你在问。

问题是信息的长度可能不同。一个叫“汤姆”的人可能会说,“嗨”,另一个叫“本杰明”的人可能会说,“嘿,伙计们,怎么了?”

所以你把所有这些东西都发送给客户端。你的输出数据流看起来像这样:

    t o m H i B e n j a m i n H e y g u y s w h a t i s u p ?

等等。客户端如何知道一条消息何时开始,另一条消息何时停止?如果你愿意,你可以让所有消息都一样长,只调用上面我们实现的sendall()。但是这浪费带宽!我们不想发送()1024字节,这样“tom”就可以说“嗨”。

因此,我们将数据封装在一个微小的头和包结构中。客户端和服务器都知道如何打包和解包(有时称为“列表”和“散集”)这些数据。现在不要看,但是我们开始定义一个描述客户端和服务器如何通信的协议

在这种情况下,让我们假设用户名是固定长度的8个字符,填充有\0。然后让我们假设数据是可变长度的,最多128个字符。让我们看看在这种情况下我们可能使用的示例包结构:

  1. Len(1字节,无符号)-数据包的总长度,包括8字节的用户名和聊天数据。
  2. name(8字节)-用户的名称,如果需要,添加NUL。
  3. 聊天数据*(n-*bytes)-数据本身,不超过128字节。数据包的长度应该计算为该数据的长度加上8(上面的名称字段的长度)。

为什么我要为字段选择8字节和128字节的限制?我把它们从空中拉出来,假设它们足够长。但是,也许8字节对您的需求来说限制太大了,您可以有一个30字节的名称字段,或者其他什么。选择取决于您。

使用上述数据包定义,第一个数据包将由以下信息组成(十六进制和ASCII):

       0A     74 6F 600 00 00 00 00      48 69
    (length)  T  o  m    (padding)         H  i

第二个是相似的:

       18     42 65 6661 669 6E      48 65 79 20 67 75 79 73 20 77 ...
    (length)  B  e  n  j  a  m  i  n       H  e  y     g  u  y  s     w  ...

(当然,长度是以网络字节顺序存储的。在这种情况下,它只有一个字节,所以没关系,但是通常来说,你会希望你所有的二进制整数都存储在数据包的网络字节顺序中。)

当您发送这些数据时,您应该是安全的,并使用类似于上面sendall()的命令,这样您就知道所有的数据都被发送了,即使需要多次调用发送()才能全部发送出去。

同样,当您接收这些数据时,您需要做一些额外的工作。为了安全起见,您应该假设您可能会收到一个部分数据包(比如上面我们从Benjamin那里收到“18 42 65 6E 6A”,但这就是我们在调用recv()时得到的全部)。我们需要一遍又一遍地调用recv(),直到数据包被完全接收。

但是怎么做呢?我们知道要完成数据包,我们总共需要接收多少字节,因为这个数字贴在数据包的前面。我们还知道最大数据包大小是1+8+128,或137字节(因为这是我们定义数据包的方式)。

这里实际上有几件事可以做。因为您知道每个包都以一个长度开始,所以您可以调用recv()来获取包的长度。然后一旦您有了它,您可以再次调用它,指定包的剩余长度(可能重复以获取所有数据),直到您有完整的包。这种方法的优点是您只需要一个足够大的缓冲区来容纳一个包,而缺点是您需要至少两次调用recv()来获取所有的数据。

另一个选择是调用recv),说你愿意接收的量是数据包中的最大字节数。然后不管你得到什么,把它贴在缓冲区的后面,最后检查数据包是否完整。当然,你可能会得到下一个数据包,所以你需要有空间。

您可以做的是声明一个足够容纳两个数据包的数组。这是您的工作数组,您将在数据包到达时重建它们。

每次recv()数据时,您都会将其附加到工作缓冲区中,并检查数据包是否完整。也就是说,缓冲区中的字节数大于或等于标头中指定的长度(+1,因为标头中的长度不包括长度本身的字节)。如果缓冲区中的字节数小于1,显然数据包不完整。不过,您必须对此进行特殊处理,因为第一个字节是垃圾,您不能依赖它来获得正确的数据包长度。

一旦数据包完成,您可以随意处理它。使用它,并从工作缓冲区中删除它。

咻!你在脑子里玩这个了吗?嗯,这是“一两拳”的第二个:你可能已经在一次recv()调用中读取了一个数据包的末尾,然后读取到了下一个数据。也就是说,你有一个包含一个完整数据包的工作缓冲区,以及下一个数据包的不完整部分!真该死。(但这就是为什么你让你的工作缓冲区大到足以容纳两个数据包——以防发生这种情况!)

由于您从报头中知道第一个数据包的长度,并且一直在跟踪工作缓冲区中的字节数,因此您可以减去并计算工作缓冲区中属于第二个(不完整)数据包的字节数。处理完第一个包后,您可以将其从工作缓冲区中清除出来,并将第二个部分数据包向下移动到缓冲区的前面,以便为下一个recv)做好准备。

(你们中的一些读者会注意到,实际上将部分第二个数据包移动到工作缓冲区的开头需要时间,并且可以通过使用循环缓冲区对程序进行编码以使其不需要这样做。不幸的是,对其他人来说,关于循环缓冲区的讨论超出了本文的范围。如果你仍然好奇,拿一本数据结构书,然后从那里开始。)

我从没说过这很容易。好吧,我确实说过这很容易。的确如此;你只需要练习,很快你就会自然而然地学会。我以神剑发誓!

7.7广播包-你好,世界!

到目前为止,本指南已经谈到了将数据从一个主机发送到另一个主机。但是,我坚持认为,如果有适当的权限,您可以同时向多个主机发送数据!

对于UDP(只有UDP,没有TCP)和标准IPv4,这是通过一种叫做广播的机制实现的。对于IPv6,广播是不支持的,你必须求助于通常更高级的多播技术,遗憾的是,我现在不会讨论这个。但是对未来的幻想已经够多了——我们被困在32位的现在。

但是等等!你不能不管三七二十一就跑出去开始广播;你必须把插座选项设置SO_BROADCAST才能在网络上发送广播包。这就像他们在导弹发射开关上盖上的一个小塑料盖!这就是你手中的力量!

但说真的,使用广播包有一个危险,那就是:每个接收广播包的系统都必须撤消所有洋葱皮层的数据封装,直到它找到数据的目的地。然后它将数据移交或丢弃。在这两种情况下,每台接收广播包的机器都要做很多工作,因为它们都在本地网络上,所以很多机器可能会做很多不必要的工作。游戏《毁灭战士》刚出来的时候,这是对其网络代码的抱怨。

现在,剥猫皮的方法不止一种......等一下。真的有不止一种剥猫皮的方法吗?那是一种什么样的表达?呃,同样,发送广播包的方法也不止一种。所以,说到整件事情的核心:如何为广播消息指定目标地址?有两种常见的方法:

  1. 将数据发送到特定子网的广播地址。这是子网的网络号,地址的主机部分设置了所有一位数。例如,在家里,我的网络是192.168.1.0的,我的网络掩码是255.255.255.0的,所以地址的最后一个字节是我的主机号(因为根据网络掩码,前三个字节是网络号)。所以我的广播地址是192.168.1.255。在Unix下,ifconfig命令实际上会给你所有这些数据。(如果你好奇,获取广播地址的按位逻辑是network_number或(不是网络掩码)。)您可以将这种类型的广播数据包发送到远程网络以及您的本地网络,但您面临数据包被目标路由器丢弃的风险。(如果他们没有丢弃它,那么一些随机的smurf可能会开始用广播流量淹没他们的局域网。)
  2. 将数据发送到“全局”广播地址。这是255.255.255.255,也就是INADDR_BROADCAST。许多机器会自动将此与您的网络号按位并用,以将其转换为网络广播地址,但有些不会。情况各不相同。讽刺的是,路由器不会从您的本地网络转发这种类型的广播数据包。

那么,如果您尝试在没有首先设置SO_BROADCAST套接字选项的情况下发送广播地址上的数据,会发生什么呢?好吧,让我们启动好的老谈话听众,看看会发生什么。

    $ talker 192.168.1.2 foo
    sent 3 bytes to 192.168.1.2
    $ talker 192.168.1.255 foo
    sendto: Permission denied
    $ talker 255.255.255.255 foo
    sendto: Permission denied

是的,一点也不开心...因为我们没有设置SO_BROADCAST套接字选项。这样做,现在你可以在任何你想要的地方发送()

事实上,这是UDP应用程序可以广播和不能广播的唯一区别。所以让我们使用旧的talker应用程序,添加一个设置SO_BROADCAST套接字选项的部分。我们将这个程序称为广播器。c42

/*
** broadcaster.c -- a datagram "client" like talker.c, except
**                  this one can broadcast
*/


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define SERVERPORT 4950 // the port users will be connecting to

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_in their_addr; // connector's address information
    struct hostent *he;
    int numbytes;
    int broadcast = 1;
    //char broadcast = '1'; // if that doesn't work, try this

    if (argc != 3) {
        fprintf(stderr,"usage: broadcaster hostname message\n");
        exit(1);
    }

    if ((he=gethostbyname(argv[1])) == NULL) {  // get the host info
        perror("gethostbyname");
        exit(1);
    }

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // this call is what allows broadcast packets to be sent:
    if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast,
        sizeof broadcast) == -1) {
        perror("setsockopt (SO_BROADCAST)");
        exit(1);
    }

    their_addr.sin_family = AF_INET;     // host byte order
    their_addr.sin_port = htons(SERVERPORT); // short, network byte order
    their_addr.sin_addr = *((struct in_addr *)he->h_addr);
    memset(their_addr.sin_zero, '\0'sizeof their_addr.sin_zero);

    if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
             (struct sockaddr *)&their_addr, sizeof their_addr)) == -1) {
        perror("sendto");
        exit(1);
    }

    printf("sent %d bytes to %s\n", numbytes,
        inet_ntoa(their_addr.sin_addr));

    close(sockfd);

    return 0;
}

普通的UDP客户端/服务器情况有什么不同?什么都没有!(除了在这种情况下允许客户端发送广播包。)因此,继续在一个窗口运行旧的UDP监听器程序,在另一个窗口运行广播程序。您现在应该能够执行上面所有失败的发送。

    $ broadcaster 192.168.1.2 foo
    sent 3 bytes to 192.168.1.2
    $ broadcaster 192.168.1.255 foo
    sent 3 bytes to 192.168.1.255
    $ broadcaster 255.255.255.255 foo
    sent 3 bytes to 255.255.255.255

您应该会看到监听器响应它得到了数据包。(如果监听器没有响应,可能是因为它绑定了IPv6地址。尝试将listener. c中的AF_UNSPEC更改为AF_INET强制IPv4。)

嗯,这有点令人兴奋。但是现在在同一网络上你旁边的另一台机器上启动监听器,这样你就有两个副本,每台机器一个,然后用你的广播地址再次运行广播公司...嘿!两个监听器都得到数据包,即使你只调用了一次sendto()!酷!
如果监听器收到了你直接发送给它的数据,但没有广播地址上的数据,那可能是你在本地机器上有防火墙阻止了数据包。(是的,帕特和巴珀,谢谢你们在我之前意识到这就是我的示例代码不起作用的原因。我告诉过你我会在指南中提到你,给你。所以``*nyah*``。)
同样,要小心广播数据包。由于局域网上的每台机器都将被迫处理数据包,无论它是否回收,它都会给整个计算网络带来相当大的负担。它们肯定要谨慎和适当地使用。

8个常见问题

我在哪里可以得到那些头文件?

如果您的系统中还没有它们,您可能不需要它们。请查看特定平台的手册。如果您正在为Windows构建,您只需要#包含<winsok. h>

当bind**(**)报告地址已在使用时该怎么办?

您必须在侦听套接字的SO_REUSEADDR选项中使用setsokbet()。例如,请参阅关于bind()的部分和选择()部分。

如何获得系统上打开的套接字列表?

使用netstat。查看手册页面了解完整的细节,但是你应该会得到一些好的输出,只需输入:

    $ netstat

唯一的技巧是确定哪个套接字与哪个程序相关联。:-

如何查看路由表?

运行路由命令(在大多数Linux上 /sbin)或命令netstat-r

如果我只有一台计算机,我如何运行客户端和服务器程序?我不需要网络来编写网络程序吗?

幸运的是,几乎所有的机器都实现了位于内核中的环回网络“设备”,并假装是网卡。(这是路由表中列出的“lo”接口。)

假设你登录了一台名为“山羊”的机器。在一个窗口运行客户端,在另一个窗口运行服务器。或者在后台启动服务器(“服务器&”),在同一个窗口运行客户端。环回设备的结果是,你可以客户端山羊或客户端localhost(因为“localhost”可能在你的 /etc/hosts文件中定义),你会让客户端在没有网络的情况下与服务器对话!

简而言之,不需要对任何代码进行任何更改,就可以使其在单个非联网机器上运行!

如何判断远程端是否已关闭连接?

可以判断,因为recv)将返回0

如何实现“ping”实用程序?什么是ICMP?我在哪里可以找到更多关于原始套接字和**SOCK_RAW**的信息?

所有原始套接字问题都将在W. Richard Stevens的UNIX网络编程书籍中得到解答。此外,请查看Stevens的UNIX网络编程源代码中的ping/子目录,可在线获得43。

如何更改或缩短调用的超时**连接()**

与其给出W. Richard Stevens会给您的完全相同的答案,我将只向您推荐UNIX网络编程源代码44中的lib/connect_nonb. c

它的要点是,你用套接字()创建一个套接字描述符,将其设置为非阻塞,调用连接(),如果一切顺利,连接()将立即返回-1,errno将被设置为EINPROGRESS。然后你用你想要的超时调用选择(),在读写集中传递套接字描述符。如果它没有超时,这意味着连接()调用完成。在这一点上,您将不得不使用getsokopt()SO_ERROR选项来获取连接()调用的返回值,如果没有错误,返回值应该为零。

最后,在开始通过套接字传输数据之前,您可能希望将套接字设置为再次阻塞。

请注意,这有一个额外的好处,即允许您的程序在连接时做其他事情。例如,您可以将超时设置为较低的值,例如500毫秒,并在每次超时时更新屏幕上的指示器,然后再次调用selece()。当您调用selece()和timeed-out(例如,20次)时,您将知道是时候放弃连接了。

就像我说的,看看史蒂文斯的消息来源,找到一个非常好的例子。

如何为Windows构建?

首先,删除Windows并安装Linux或BSD.};-). 不,实际上,请参阅介绍中关于构建Windows的部分。

我如何为Solaris/SunOS构建?当我试图编译时,我总是得到链接器错误!

链接器错误的发生是因为Sun框没有在套接字库中自动编译。有关如何执行此操作的示例,请参阅介绍中关于构建Solaris/SunOS的部分。

为什么**选择()**总是在信号上出现问题?

errno设置EINTR时,信号往往会导致阻塞的系统调用返回-1。当您使用sigaction()设置信号处理程序时,您可以将标志设置SA_RESTART,这应该在系统调用中断后重新启动。

自然,这并不总是奏效。

我最喜欢的解决方法是使用goto语句。你知道这会让你的教授非常恼火,所以去做吧!

select_restart:
if ((err = select(fdmax+1, &readfds, NULLNULLNULL)) == -1) {
    if (errno == EINTR) {
        // some signal just interrupted us, so restart
        goto select_restart;
    }
    // handle the real error here:
    perror("select");

当然,在这种情况下,您不需要使用go``*to*``;您可以使用其他结构来控制它。但是我认为goto语句实际上更干净。

如何实现对recv**(**)调用的超时?

使用Select)!它允许您为要读取的套接字描述符指定超时参数。或者,您可以将整个功能包装在一个函数中,如下所示:

#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>

int recvtimeout(int s, char *buf, int len, int timeout)
{
    fd_set fds;
    int n;
    struct timeval tv;

    // set up the file descriptor set
    FD_ZERO(&fds);
    FD_SET(s, &fds);

    // set up the struct timeval for the timeout
    tv.tv_sec = timeout;
    tv.tv_usec = 0;

    // wait until timeout or data received
    n = select(s+1, &fds, NULLNULL, &tv);
    if (n == 0return -2// timeout!
    if (n == -1return -1// error

    // data must be here, so do a normal recv()
    return recv(s, buf, len, 0);
}
.
.
.
// Sample call to recvtimeout():
n = recvtimeout(s, buf, sizeof buf, 10); // 10 second timeout

if (n == -1) {
    // error occurred
    perror("recvtimeout");
}
else if (n == -2) {
    // timeout occurred
else {
    // got some data in buf
}
.
.

注意recvtimeout()在超时的情况下返回-2。为什么不返回0呢?如果你回想一下,在调用recv)时返回值为0意味着远程端关闭了连接。所以返回值已经被指定,-1意味着“错误”,所以我选择-2作为我的超时指示器。

在通过套接字发送数据之前,如何加密或压缩数据?

一种简单的加密方法是使用SSL(安全套接字层),但这超出了本指南的范围。(查看OpenSSL项目45了解更多信息。)

但是假设你想插入或实现你自己的压缩器或加密系统,这只是把你的数据想象成在两端之间运行一系列步骤的问题。每一步都以某种方式改变数据。

  1. 服务器从文件(或任何地方)读取数据
  2. 服务器加密/压缩数据(您添加此部分)
  3. 服务器发送()s加密数据

现在反过来说:

  1. 客户端recv()的加密数据
  2. 客户端解密/解压缩数据(您添加此部分)
  3. 客户端将数据写入文件(或任何地方)

如果你要压缩和加密,只要记住先压缩。:-

只要客户端正确地撤消服务器所做的工作,无论您添加多少中间步骤,数据最终都会没事。

因此,使用我的代码所需要做的就是找到数据被读取和数据通过网络发送(使用mail())之间的位置,并在其中插入一些执行加密的代码。

我一直看到的这个**“PF_INET”是什么?和AF_INET**有关吗?

是的,是的。有关详细信息,请参阅套接字()部分。

如何编写从客户端接受shell命令并执行它们的服务器?

为了简单起见,假设客户端连接()s发送()s关闭()s连接(也就是说,如果客户端没有再次连接,就没有后续的系统调用)。

客户端遵循的流程如下:

  1. 连接到服务器
  2. 发送(/sbin/ls> /tmp/client.out)
  3. 关闭()连接

同时,服务器正在处理数据并执行它:

  1. 从客户端接受()连接
  2. recv(str)命令字符串
  3. 关闭()连接
  4. 系统(str)运行命令

当心*!*让服务器执行客户端所说的就像给远程shell访问一样,当人们连接到服务器时,他们可以对你的帐户做一些事情。例如,在上面的例子中,如果客户端发送“rm-rf~”会怎么样?它会删除你账户中的所有东西,就是这样!

因此,您可以明智地阻止客户端使用任何工具,除了您知道是安全的几个实用程序,如fobar实用程序:

    if (!strncmp(str, "foobar"6)) {
        sprintf(sysstr, "%s > /tmp/server.out", str);
        system(sysstr);
    } 

但是不幸的是,你仍然不安全:如果客户端输入“脚栏;rm-rf~”呢?最安全的做法是编写一个小例程,在命令参数中所有非字母数字字符(如果合适的话,包括空格)前面放置转义 (“\”) 字符。

如您所见,当服务器开始执行客户端发送的内容时,安全性是一个相当大的问题。

我正在发送大量数据,但是当我recv()时,它一次只能接收536字节或1460字节。但是如果我在本地机器上运行**它,**它会同时接收所有的数据。这是怎么回事?

你达到了MTU——物理介质可以处理的最大大小。在本地机器上,你使用的是可以处理8K或更多的环回设备,没有问题。但是在以太网上,它只能处理1500字节的头,你达到了这个极限。在调制解调器上,有576 MTU(同样是带头),你达到了甚至更低的极限。

首先,您必须确保所有数据都被发送。(详细信息请参见sendall()函数实现。)一旦您确定了这一点,那么您需要在循环中调用recv(),直到读取所有数据。

阅读数据封装之子一节,了解使用多个调用recv)接收完整数据包的详细信息。

我在一个Windows框上,我没有**fork()系统**调用或任何类型的**结构sigaction**。该怎么办?

如果它们在任何地方,它们都可能在编译器附带的POSIX库中。因为我没有Windows框,我真的不能告诉你答案,但是我似乎记得微软有一个POSIX兼容性层,那就是fork()所在的地方。(甚至可能是sigaction。)

在VC++附带的帮助中搜索“叉子”或“POSIX”,看看它是否能给你任何线索。

如果这根本不起作用,抛弃fork()/sigaction的东西,用Win32等效的创建过程()代替它。我不知道如何使用创建过程()——它需要无数个参数,但它应该在VC++附带的文档中涵盖。

我在防火墙后面——我如何让防火墙之外的人知道我的IP地址,以便他们可以连接到我的机器?

不幸的是,防火墙的目的是防止防火墙外的人连接到防火墙内的机器,所以允许他们这样做基本上被认为是对安全的破坏。

这并不是说一切都失去了。首先,如果防火墙在做某种伪装或NAT或类似的事情,你仍然可以通过防火墙进行连接。只要设计你的程序,让你总是发起连接的人,你就会没事。

如果这不能令人满意,你可以要求你的系统管理员在防火墙上戳一个洞,这样人们就可以连接到你。防火墙可以通过它的NAT软件或代理或类似的东西转发给你。

请注意,防火墙上的漏洞不能掉以轻心。你必须确保不让坏人访问内部网络;如果你是初学者,让软件安全比你想象的要困难得多。

不要让你的系统管理员生我的气。;-

如何编写数据包嗅探器?如何将以太网接口置于混杂模式?

对于那些不知道的人来说,当网卡处于“混杂模式”时,它会将所有数据包转发给操作系统,而不仅仅是发送给这台特定机器的数据包。(我们这里谈论的是以太网层地址,而不是IP地址——但是因为以太网比IP低一层,所以所有的IP地址也被有效转发。更多信息请参见低级废话和网络理论部分。)

这是数据包嗅探器工作的基础。它将接口置于混杂模式,然后操作系统获取线路上经过的每一个数据包。你将有一个某种类型的套接字,你可以从中读取这些数据。

不幸的是,这个问题的答案因平台而异,但是如果你在谷歌上搜索“窗口混杂的ioctl”,你可能会有所收获。对于Linux,还有一个看起来像有用的堆栈溢出线程46

如何为TCP或UDP套接字设置自定义超时值?

这取决于您的系统。您可以在网络上搜索SO_RCVTIMEOSO_SNDTIMEO(用于setsokpt()),看看您的系统是否支持这些功能。

Linux手册页建议使用警报()或setitimer)作为替代。

我如何知道哪些端口可以使用?是否有“官方”端口号列表?

通常这不是问题。如果你正在编写一个网络服务器,那么在你的软件中使用众所周知的端口80是个好主意。如果你只编写你自己的专用服务器,那么随机选择一个端口(但大于1023)并试一试。

如果该端口已在使用中,则在尝试绑定()时会出现地址已在使用错误。请选择另一个端口。允许软件用户使用配置文件或命令行开关指定替代端口是个好主意。)

有一个由互联网分配号码管理局(IANA)维护的官方端口号列表47。仅仅因为某个东西(超过1023)在该列表中并不意味着你不能使用该端口。例如,id Software的DOOM使用与“mdqs”相同的端口,不管那是什么。重要的是,当你想使用该端口时,同一台机器上没有其他人在使用它。

阿酷尔工作室

2021/10/16  阅读:32  主题:默认主题

作者介绍

阿酷尔工作室

恒生研究院