面向连接与面向无连接通讯举例
面向连接的客户端的例子
下面的程序,显示了一个客户的命令,这个命令和服务进行通信,从系统的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命令不会占用太多的时间,所以父进程可以在接收下一条请求之前等待字进程退出。但是如果子进程执行过长的时间的化,可能这个方法就不那么好用了。
前面的例子使用的是面向连接的套接字。但是我们如何选择合适的类型?什么时候我们使用面向连接的套接字,什么时候我们使用无连接的套接字?这个打开取决于我们需要做多少工作以及我们可以容忍多少的错误。
对于一个无连接的套接字,对数据包到达的次序没有太多的要求,所以如果我们不能将我们所有的数据放到一个包中,我们就需要考虑次序的问题,包的最大长度是通信协议的特性。另外,对于无连接的套接字,可能会出现丢包的情况。如果我们的应用程序不能容忍丢包的情况,那么我们应该使用面向连接的套接字。
容忍包的丢失以为着我们可以有两个选择。如果我们使用可靠的通信方式,那么我们需要给我们的包进行编号,当我们检测到包的丢失的时候可以请求重新传送。由于一个包可能会延迟导致好象是丢失了,但是在我们再次请求传输的时候又出现了,所以我们也应当可以辨别重复的包然后忽略它们。
另外一个选择就是我们可以重新尝试命令来处理错误。对于一个简单的应用程序,这个就足够了,但是对于一个复杂的应用程序,它并不一定可行。所以,一般来说这时候我们最好使用面向连接的套接字服务。
使用面向连接的套接字的缺点就是,我们在建立连接的时候需要做更多的工作和时间,并且每个连接都会消耗更多的操作系统资源。
网友评论