因为一个套接字终端作为一个文件描述符表示,所以我们可以使用read和write来和一个套接字通信,只要它被连接。回想一个数据报套接字可以是
“连接的”如果我们使用connect函数设置默认伙伴地址。使用read和write操作套接字描述符是很有意义的,因为它表示我们可以传递套接字描述
符到一个函数,它最初被设计为工作在本地文件上。我们可以安排把套接字描述符传递给执行程序的对套接字一无所知的子进程。
尽管我们可以使用read和write交换数据,但是这大概是我们用这两个函数能做的所有事情。如果我们想指定选项,从多个客户接收包,或发送不同频道的数据,那么我们需要使用为数据传输设计的6个套接字函数中的某个。
三个函数可用来发送数据,而三个可用来接收数据。首先,我们将看下用来发送数据的那些。
最简单的一个是send。它和write相似,但是允许我们指定标志来改变我们想要的数据是如何被对待的。
- #include <sys/socket.h>
- ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
- 成功返回发送的字节数,错误返回-1。
像write一样,套接字必须被连接才能使用send。buf和nbytes参数有和在write里时相同的意义。
然而,不像write一样,send支持第4个标志参数。两个标志被SUS定义,但是实现普遍支持补充的。它们在下表汇总。
send套接字调用使用的标志标志 | 描述 | POSIX.1 | FreeBSD 5.2.1 | Linux 2.4.22 | Mac OS X 10.3 | Solaris 9 |
---|
MSG_DONTROUTE | 不要路由本地网络之外的包 |
| * | * | * | * |
MSG_DONTWAIT | 启用非阻塞操作(等价于使用O_NONBLOCK)。 |
| * | * | * |
|
MSG_EOR | 如果被协议支持,这是记录末尾 | * | * | * | * |
|
MSG_OOB | 发送out-of-band数据,如果被协议支持(16.7节) | * | * | * | * | * |
如果send返回成功,它也不必表示连接的另一端的进程收到了数据。所有我们被保证的是当send成功时,数据被分发给网络驱动器而没有错误。
使用一个支持消息边界的协议时,如果我们尝试发送单个比协议支持的最大值还要大的消息,那么send会失败并设置errno为EMSGSIZE。使用一个基于流的协议时,send会阻塞,直到整个数据被传送。
sendto函数和send相似。区别在于sendto允许我们指定一个用于无连接套接字的目的地址。
- #include <sys/socket.h>
- ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
- 成功返回发送的字节量,错误返回-1。
使用一个面向连接的套接字时,目的地址被忽略,因为目的地被连接隐含。使用一个无连接的套接字时,我们不能使用send,除非目的地址事先通过调用connect设置,所以sendto给我们发送一个消息的另一种方式。
我们在通过套接字传输数据时还有一个选择。我们调用sendmsg,使用一个msghdr结构体来指明多个缓冲,从这些缓冲来传输数据,和writev函数相似(14.7节)。
- #include <sys/socket.h>
- ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
- 成功返回字节数,错误返回-1。
POSIX.1定义msghdr结构体,包含至少以下成员:
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* address size in bytes */
struct iovec *msg_iov; /* array of I/O buffers */
int msg_iovlen; /* number of elements in array */
void *msg_control; /* ancillary data */
socklen_t msg_controllen; /* number of ancillary bytes */
int msg_flags; /* flags for received message */
...
};
我们在14.7节看到iovec结构体。我们将在17.4.2节看到补充数据的使用。
recv函数和read相似,但允许我们指定一个选项来控制我们如何收到数据。
- #include <sys/socket.h>
- ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
- 返回消息的字节尺寸,如果没有可用的消息且同伴执行了一个有规则的关闭则返回0,错误返回-1。
可以传递给recv的标志在下表汇总。只有三个被定义在SUS里。
recv套接字调用使用的标志标志 | 描述 | POSIX.1 | FreeBSD 5.2.1 | Linux 2.4.22 | Mac OS X 10.3 | Solaris 9 |
MSG_OOB | 如果被协议支持则接受不在同一频道的数据(16.7节) | * | * | * | * | * |
MSG_PEEK | 返回包内容而不消费包。 | * | * | * | * | * |
MSG_TRUNC | 请求被返回的包的真实长度,即使它被裁切 |
|
| * |
|
|
MSG_WAITALL | 等待直到数据可用(只用于SOCK_STREAM) | * | * | * | * | * |
当我们指定MSG_PEEK标志时,我们可以瞟一下下一个要读的数据,而不真正消费它。下一个read调用或某个recv函数会返回我们瞟过的相同的数据。
使
用SOCK_STREAM套接字时,我们可以接收比我们请求更少的数据。MSG_WAITALL标志抑制这种行为,避免recv返回,直到所有我们请求的
数据被接收到。使用SOCK_DGRAM和SOCK_SEQPACKET套接字时,MSG_WAITALL标志在行为上没有提供改变,因为这些基于消息的
套接字类型在单个读里已经返回整个消息了。
如果发送者已经调用了shutdown(16.2节)来结束传输,或网络协议默认支持有规则的关闭且发送者已经关闭这个套接字,那么recv将返回0,当我们已收到所有数据时。
- #include <sys/socket.h>
- ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
- 返回消息的字节长度,如果没有消息可用且同伴已经执行有规则的关闭则返回0,错误返回-1。
如果addr为非空,那么它将包含套接字终端的地址,数据从那里发送过来。当调用recvfrom时,我们需要设置addrlen参数来指向一个包含addr指向的套接字缓冲字节尺寸的整型。在返回时,这个整型被设置为地址真实的字节尺寸。
因为我们被允许得到发送者的地址,所能recvfrom通常用在无连接的套接字上。否则,recvfrom和recv的行为一样。
为了接收数据到多个缓冲里,和readv类似(14.7节),或者我们想接收附加数据(17.4.2节),那么我们可以使用recvmsg。
- #include <sys/socket.h>
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
- 返回消息的字节长度,如果没有消息可用且同伴已经执行有规则的关闭则返回0,错误返回-1。
msghdr
结构体(我们在sendmsg那里看过了)被recvmsg用来指定用来接收数据的输入缓冲。我们可以设置flags参数来改变recvmsg的默认行
为。在返回时,msghdr结构体的msg_flags域被设置来指定收到的数据的各种特性。(msg_flags域在recvmsg的入口被忽略)。在
从recvmsg返回时的可能值在下表汇总。我们将看到一个使用recvmsg的例子,在17章。
recvmsg返回的msg_flags的标志标志 | 描述 | POSIX.1 | FreeBSD 5.2.1 | Linux 2.4.22 | Mac OS X 10.3 | Solaris 9 |
---|
MSG_CTRUNC | 控制数据被裁切 | * | * | * | * | * |
MSG_DONTWAIT | recvmsg以非阻塞模式被调用 |
|
| * |
| * |
MSG_EOR | 记录末尾被收到 | * | * | * | * | * |
MSG_OOB | 不同频道的数据被收到 | * | * | * | * | * |
MSG_TRUNC | 普通数据被裁切 | * | * | * | * | * |
下面的代码展示了一个和服务器通信来获得一个系统uptime命令的输出的客户端命令。我们称这个服务为“远程uptime”(或“ruptime”)。
- #include <netdb.h>
- #include <errno.h>
- #include <sys/socket.h>
- #include <unistd.h>
- #include <stdio.h>
- #define MAXDDRLEN 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) {
- printf("recv error\n");
- exit(1);
- }
- }
- int
- main(int argc, char *argv[])
- {
- struct addrinfo *ailist, *aip;
- struct addrinfo hint;
- int sockfd, err;
- if (argc != 2) {
- printf("usage: ruptime hostanme");
- exit(1);
- }
- 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) {
- printf("getaddrinfo error: %s\n", gai_strerror(err));
- exit(1);
- }
- 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);
- }
这个程序连接到一个服务器,读取从服务器发送的字符串,并把字符串打印到标准输出。因为我们正使用一个SOCK_STREAM套接字,所以我们不能被保证我们将在一次recv调用里读取到整个字符串,所以我们需要重复这个调用,直到它返回0。
getaddrinfo函数可能为我们返回多个可使用的候选地址,如果服务器支持多个网络接口或多个网络协议。我们依次尝试每一个,当找到一个允许我们连接到服务时便放弃。我们使用connect_retry函数来建立一个到服务器的连接。
下面的代码展示了提供uptime命令的输出给上面的客户程序的服务器。
- #include <netdb.h>
- #include <stdio.h>
- #include <syslog.h>
- #include <errno.h>
- #define BUFLEN 128
- #define QLEN 10
- #ifndef HOST_NAME_MAX
- #define HOST_NAME_MAX 256
- #endif
- extern void daemonize(const char *);
- extern int initserver(int, struct sockaddr *, socklen_t, int);
- void
- serve(int sockfd)
- {
- int clfd;
- FILE *fp;
- char buf[BUFLEN];
- for (;;) {
- syslog(LOG_INFO, "ruptimed: start to accept");
- clfd = accept(sockfd, NULL, NULL);
- if (clfd < 0) {
- syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
- exit(1);
- }
- else {
- syslog(LOG_INFO, "ruptimed: succeed to accept");
- }
- 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) {
- printf("usage: ruptimed1");
- exit(1);
- }
- #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) {
- printf("malloc error");
- exit(1);
- }
- if (gethostname(host, n) < 0) {
- printf("gethostname error\n");
- exit(1);
- }
- 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_NAME。如果系统没有定义HOST_NAME_MAX,那么我们自己定义。POSIX.1指定主机名的最小值是255字节,不包括
最后的终止空字符,所以我们定义HOST_NAME_MAX为256来包含终止符。
服务器通过调用gethostname得到主机名并查找远程uptime服务器的地址。可能有多个地址返回,但是我们简单挑选第一个我们可以为其建立一个被动套接字终端的那个。
我们使用16.4节的initserver函数来初始化套接字终端,在它上面我们将等待连接请求的到达。(事实上, 我们使用16.6节的版本,我们将在讨论套接字选项时看到为什么。)
(要让上面的代码正常工作,首先确保/etc/services文件有ruptime这个服务,没有的话需要加上这项。然后,在客户端程序的命令行里必须指定主机的真实名字,而不能用localhost这样的。)
在
前面,我们指定使用文件描述符来访问套接字是意义重大的,因为它允许不知道网络的程序在网络环境下运行。下面的代码是上面服务器的另一个版本,它演示这
点。作为从uptime命令的输出读并发送它到客户端的替代,服务器把uptime命令的标准输出和标准错误安排到和客户端连接的套接字终端。
- #include <netdb.h>
- #include <syslog.h>
- #include <errno.h>
- #include <unistd.h>
- #define QLEN 10
- #ifndef HOST_NAME_MAX
- #define HOST_NAME_MAX 256
- #endif
- extern void daemonize(const char *);
- 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) { /* child */
- /*
- * The parent called daemonize, so
- * STDIN_FILENO, STDOUT_FILENO, and STDERROR_FILENO
- * are already open to /dev/null. Thus, the call to
- * close doesn't need to be protected by checks that
- * clfd isn't already equal to one of these values.
- */
- 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 { /* parent */
- 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) {
- printf("usage: ruptimed1");
- exit(1);
- }
- #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) {
- printf("malloc error");
- exit(1);
- }
- if (gethostname(host, n) < 0) {
- printf("gethostname error\n");
- exit(1);
- }
- 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的客户命令。
父
进程可以安全地关闭和客户连接的文件描述符,因