Unix getaddrinfo C函数启动设置服务器的用法
Usage of Unix getaddrinfo C function to start set the server
我正在用 C 语言构建一个客户端-服务器应用程序,源代码取自《Unix 环境中的高级编程》一书。
在服务器中它正在执行以下操作:
struct addrinfo hint;
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_STREAM;
hint.ai_addr = NULL;
hint.ai_next = NULL;
....
if ((n = sysconf(_SC_HOST_NAME_MAX))<0)
{
n = HOST_NAME_MAX;
}
if((host = malloc(n)) == NULL)
{
printf("malloc error\n");
exit(1);
}
if (gethostname(host, n)<0)
{
printf("gethostname error\n");
exit(1);
}
...
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)
{
//printf("starting to serve\n");
serve(sockfd);
exit(0);
}
}
据我了解,函数 getaddrinfo
用于在主机上查看套接字地址结构 运行 连接名为 ruptime
且类型为 SOCK_STREAM
的服务.
虽然书中没有指定,但为了工作,我必须 运行 在文件 /etc/services/
中添加一个新条目,其中包含一个未使用的端口和指定的名称 ruptime
:
ruptime 49152/tcp #ruptime Unix System Programming
ruptime 49152/udp #ruptime Unix System Programming
虽然未使用,但建议也添加 UDP 部分。
但是文档中说的是
If the AI_PASSIVE
flag is specified in hints.ai_flags
, and node is
NULL
, then the returned socket addresses will be suitable for
bind(2)ing a socket that will accept(2) connections. The returned
socket address will contain the "wildcard address" (INADDR_ANY
for
IPv4 addresses, IN6ADDR_ANY_INIT
for IPv6 address). The wildcard
address is used by applications (typically servers) that intend to
accept connections on any of the host's network addresses.
所以从这里和其他关于 SO 的讨论中可以看到:
hint.ai_flags |= AI_PASSIVE
...
getaddrinfo(NULL, myserviceport, &hint, &aihint)
好像比较合适
这两种方法到底有什么区别?第二个也在寻找 SOCK_DGM
吗?书中选择第一种方法有什么理由吗?在第二种方式中,因为我在代码中指定了端口,它是否允许避免在 /etc/services/
?
中添加新条目
另一个问题。
我必须向客户传递主机名。我认为环回(客户端和服务器在同一台机器上 运行ning)地址可以。相反,主机名类似于 ./client MBPdiPippo.lan
。什么定义了可以使用主机名而不是环回地址创建连接这一事实?是我将 host
作为第一个参数传递给服务器中的 getaddrinfo
吗?
完整代码
server.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> //_SC_HOST_NAME_MAX
#include<string.h>
#include<netdb.h> //Here are defined AF_INET and the others of the family
#include<syslog.h> //LOG_ERR
#include<errno.h> //errno
#include <sys/types.h>
#include"utilities.h"
#include "error.h"
#define BUFLEN 128
#define QLEN 10
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 156
#endif
int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen);
void serve(int sockfd);
int main(int argc, char* argv[])
{
printf("entered main\n");
struct addrinfo *ailist, *aip, hint;
int sockfd, err, n;
char *host;
if (argc != 1)
{
printf("usage: ruptimed\n");
exit(1);
}
if ((n=sysconf(_SC_HOST_NAME_MAX))<0)
{
n = HOST_NAME_MAX;
}
if((host = malloc(n)) == NULL)
{
printf("malloc error\n");
exit(1);
}
if (gethostname(host, n)<0)
{
printf("gethostname error\n");
exit(1);
}
printf("host: %s\n", host);
printf("Daemonizing\n");
int res = daemonize("ruptimed");
printf("%d\n", res);
printf("Daemonized\n");
memset(&hint, 0, sizeof(hint)); //set to 0 all bytes
printf("hint initialized\n");
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_STREAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
printf("getting addresses\n");
if((err = getaddrinfo(host, "ruptime", &hint, &ailist))!=0)
{
printf("error %s\n", gai_strerror(err));
syslog(LOG_ERR, "ruptimed: getaddrinfo error %s", gai_strerror(err));
exit(1);
}
printf("Got addresses\n");
for (aip = ailist; aip!=NULL; aip = aip->ai_next)
{
if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN))>=0)
{
printf("starting to serve\n");
serve(sockfd);
exit(0);
}
}
exit(1);
}
void serve(int sockfd)
{
int clfd;
FILE *fp;
char buf[BUFLEN];
set_cloexec(sockfd);
for(;;)
{
/*After listen, the socket can receive connect requests. accept
retrieves a connect request and converts it into a connection.
The file returned by accept is a socket descriptor connected to the client that
called connect, haing the same coket type and family type. The original
soket remains available to receive otherconneion requests. If we don't care
about client's identity we can set the second (struct sockaddr *addr)
and third parameter (socklen_t *len) to NULL*/
if((clfd = accept(sockfd, NULL, NULL))<0)
{
/*This generates a log mesage.
syslog(int priority, const char *fformat,...)
priority is a combination of facility and level. Levels are ordered from highest to lowest:
LOG_EMERG: emergency system unusable
LOG_ALERT: condiotin that must be fied immediately
LOG_CRIT: critical condition
LOG_ERR: error condition
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
format and other arguments are passed to vsprintf function forf formatting.*/
syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
exit(1);
}
/* set the FD_CLOEXEC file descriptor flag */
/*it causes the file descriptor to be automatically and atomically closed
when any of the exec family function is called*/
set_cloexec(clfd);
/**pg. 542 Since a common operation is to create a pipe to another process
to either read its output or write its input Stdio has provided popen and
pclose: popen creates pipe, close the unused ends of the pipe,
forks a child and call exec to execute cmdstr and
returns a file pointer (connected to std output if "r", to stdin if "w").
pclose closes the stream, waits for the command to terminate*/
if ((fp = popen("/usr/bin/uptime", "r")) == NULL)
{
/*sprintf copy the string passed as second parameter inside buf*/
sprintf(buf, "error: %s\n", strerror(errno));
/*pag 610. send is similar to write. send(int sockfd, const void *buf, size_t nbytes, it flags)*/
send(clfd, buf, strlen(buf),0);
}
else
{
/*get data from the pipe that reads created to exec /usr/bin/uptime */
while(fgets(buf, BUFLEN, fp)!=NULL)
{
/* clfd is returned by accept and it is a socket descriptor
connected to the client that called connect*/
send(clfd, buf, strlen(buf), 0);
}
/*see popen pag. 542*/
pclose(fp);
}
close(clfd);
}
}
int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen)
{
int fd, err;
int reuse = 1;
if ((fd = socket(addr->sa_family, type, 0))<0)
{
return (-1);
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))<0)
{
goto errout;
}
if(bind(fd, addr, alen)<0)
{
goto errout;
}
if (type == SOCK_STREAM || type == SOCK_SEQPACKET)
{
if(listen(fd, qlen)<0)
{
goto errout;
}
}
return fd;
errout:
err = errno;
close (fd);
errno = err;
return(-1);
}
utilities.c
:包含demonize
和setcloexec
函数。在 daemonize
函数中,我没有关闭文件描述符进行调试。
#include "utilities.h"
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <syslog.h>
#include <sys/time.h>//getrlimit
#include <sys/resource.h>//getrlimit
#include <signal.h> //sigempyset , asigcation (umask?)
#include <sys/resource.h>
#include <fcntl.h> //O_RDWR
#include <stdarg.h>
#include "error.h"
int daemonize(const char *cmd)
{
int fd0, fd1, fd2;
unsigned int i;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
/* *Clear file creation mask.*/
umask(0);
/* *Get maximum number of file descriptors. */
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
{
err_quit("%s: can’t get file limit", cmd);
}
/* *Become a session leader to lose controlling TTY. */
if ((pid = fork()) < 0)
{
err_quit("%s: can’t fork", cmd);
}
else if (pid != 0) /* parent */
{
exit(0); //the parent will exit
}
setsid();
/* *Ensure future opens won’t allocate controlling TTYs. */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
{
err_quit("%s: can’t ignore SIGHUP", cmd);
}
if ((pid = fork()) < 0)
{
err_quit("%s: can’t fork", cmd);
}
else if (pid != 0) /* parent */
{
exit(0);
}
/*
*Change the current working directory to the root so
* we won’t prevent file systems from being unmounted.
*/
if (chdir("/") < 0)
{
err_quit("%s: can’t change directory to /", cmd);
}
/* Close all open file descriptors. */
if (rl.rlim_max == RLIM_INFINITY)
{
rl.rlim_max = 1024;
}
printf("closing file descriptors\n");
/*for (i = 0; i < rl.rlim_max; i++)
{
close(i);
}*/
/* *Attach file descriptors 0, 1, and 2 to /dev/null.*/
//printf not working
/*printf("closed all file descriptors for daemonizing\n");*/
/*fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);*/
/* *Initialize the log file. Daemons do not have a controlling terminal so
they can't write to stderror. We don't want them to write to the console device
because on many workstations the control device runs a windowing system. They can't
write on separate files either. A central daemon error-logging facility is required.
This is the BSD. 3 ways to generate log messages:
1) kernel routines call the log function. These messages can be read from /dev/klog
2) Most user processes (daemons) call syslog to generate log messages. This causes
messages to be sent to the UNIX domain datagram socket /dev/log
3) A user process on this host or on other host connected to this with TCP/ID
can send log messages to UDP port 514. Explicit network programmin is required
(it is not managed by syslog.
The syslogd daemon reads al three of log messages.
openlog is optional since if not called, syslog calls it. Also closelog is optional
openlog(const char *ident, int option, int facility)
It lets us specify ident that is added to each logmessage. option is a bitmask:
LOG_CONS tells that if the log message can't be sent to syslogd via UNIX
domain datagram, the message is written to the console instead.
facility lets the configuration file specify that messages from different
facilities are to be handled differently. It can be specified also in the 'priority'
argument of syslog. LOG_DAEMON is for system deamons
*/
/*
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2)
{*/
/*This generates a log mesage.
syslog(int priority, const char *fformat,...)
priority is a combination of facility and level. Levels are ordered from highest to lowest:
LOG_EMERG: emergency system unusable
LOG_ALERT: condiotin that must be fied immediately
LOG_CRIT: critical condition
LOG_ERR: error condition
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
format and other arguments are passed to vsprintf function forf formatting.*/
/*syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}*/
return 0;
}
/*The function set the FD_CLOEXEC flag of the file descriptor already open that
is passed to as parameter. FD_CLOEXEC causes the file descriptor to be
automatically and atomically closed when any of the exec family function is
called*/
int set_cloexec(int fd)
{
int val;
/* retrieve the flags of the file descriptor */
if((val = fcntl(fd, F_GETFD, 0))<0)
{
return -1;
}
/* set the FD_CLOEXEC file descriptor flag */
/*it causes the file descriptor to be automatically and atomically closed
when any of the exec family function is called*/
val |= FD_CLOEXEC;
return (fcntl(fd, F_SETFD, val));
}
错误函数我用了
/* Fatal error unrelated to a system call.
* Print a message and terminate*/
void err_quit (const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
err_doit (0, 0, fmt, ap);
va_end (ap);
exit(1);
}
/*Print a message and return to caller.
*Caller specifies "errnoflag"*/
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap)
{
char buf [MAXLINE];
vsnprintf (buf, MAXLINE-1, fmt, ap);
if (errnoflag)
{
snprintf (buf+strlen(buf), MAXLINE-strlen(buf)-1, ": %s",
strerror (error));
}
strcat(buf, "\n");
fflush(stdout); /*in case stdout and stderr are the same*/
fputs (buf, stderr);
fflush(NULL); /* flushes all stdio output streams*/
}
首先,吹毛求疵。 getaddrinfo()
代码应合并到initserver()
函数中,并在循环后释放(使用freeaddrinfo()
)套接字结构链表。这使得代码更易于维护;您想将紧密耦合的实现保持在一起。
Exactly what is the difference between these two methods?
绑定到通配符地址(即,在使用 getaddrinfo()
获取合适的套接字描述时使用 NULL
节点和 AI_PASSIVE
标志)意味着套接字作为一个绑定到所有网络接口设置,而不是特定的网络接口。当您绑定到一个特定的节点名称时,您就绑定到一个特定的网络接口。
实际上,这意味着如果额外的网络接口在 运行 时间可用,内核将在路由数据包时考虑它们 to/from 绑定到通配符地址的套接字。
这确实应该由每个系统管理员做出选择,因为在某些用例中服务(您的应用程序)应该监听所有网络接口上的传入连接,但在其他用例中服务应该监听仅特定或某些特定接口上的传入连接。典型的情况是一台机器连接到多个网络。这对于服务器来说非常普遍。对于实际案例,请参见例如如何配置 the Apache web server。
就我个人而言,我会重写 OP 的 initServer()
函数,使其看起来像下面这样:
enum {
/* TCP=1, UDP=2, IPv4=4, IPv6=8 */
SERVER_TCPv4 = 5, /* IPv4 | TCP */
SERVER_UDPv4 = 6, /* IPv4 | UDP */
SERVER_TCPv6 = 9, /* IPv6 | TCP */
SERVER_UDPv6 = 10, /* IPv6 | UDP */
SERVER_TCP = 13, /* Any | TCP */
SERVER_UDP = 14 /* Any | UDP */
};
int initServer(const char *host, const char *port,
const int type, const int backlog)
{
struct addrinfo hints, *list, *curr;
const char *node;
int family, socktype, result, fd;
if (!host || !*host || !strcmp(host, "*"))
node = NULL;
else
node = host;
switch (type) {
case SERVER_TCPv4: family = AF_INET; socktype = SOCK_STREAM; break;
case SERVER_TCPv6: family = AF_INET6; socktype = SOCK_STREAM; break;
case SERVER_TCP: family = AF_UNSPEC; socktype = SOCK_STREAM; break;
case SERVER_UDPv4: family = AF_INET; socktype = SOCK_DGRAM; break;
case SERVER_UDPv6: family = AF_INET6; socktype = SOCK_DGRAM; break;
case SERVER_UDP: family = AF_UNSPEC; socktype = SOCK_DGRAM; break;
default:
fprintf(stderr, "initServer(): Invalid server type.\n");
return -1;
}
memset(&hints, 0, sizeof hints);
hints.ai_flags = AI_PASSIVE;
hints.ai_family = family;
hints.ai_socktype = socktype;
hints.ai_protocol = 0;
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
result = getaddrinfo(node, port, &hints, &list);
if (result) {
/* Fail. Output error message to standard error. */
fprintf(stderr, "initServer(): %s.\n", gai_strerror(result));
return -1;
}
fd = -1;
for (curr = list; curr != NULL; curr = curr->ai_next) {
int reuse = 1;
fd = socket(curr->ai_family, curr->ai_socktype, curr->ai_protocol);
if (fd == -1)
continue;
if (bind(fd, curr->ai_addr, curr->ai_addrlen) == -1) {
close(fd);
fd = -1;
continue;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof (int)) == -1) {
close(fd);
fd = -1;
continue;
}
if (listen(fd, backlog) == -1) {
close(fd);
fd = -1;
continue;
}
break;
}
freeaddrinfo(list);
if (fd == -1) {
fprintf(stderr, "initServer(): Cannot bind to a valid socket.\n");
return -1;
}
return fd;
}
(注意:代码未经测试,甚至未编译;但底层逻辑是合理的。如果您发现任何问题或错误,请在评论中告诉我,以便我在必要时进行审查、检查和修复。 )
这样,您可以从配置文件中读取 host
和 port
。如果 host
为 "*"
、空或 NULL
,该函数将尝试绑定到通配符地址。 (顺便说一下,这应该是默认设置;如果服务器管理员想要限制到特定接口,他们可以提供 IP 地址或与该接口对应的主机名。)
同样,系统管理员可以使用配置文件将port
指定为services
数据库中定义的任何字符串(getent services
),或者指定为十进制数字字符串;在 OP 的情况下,"49152"
和 "ruptime"
都可以。
Since I am specifying the port in the code, does it allow to avoid adding a new entry in the /etc/services/?
services 数据库(运行 getent services
在您的计算机上查看)仅包含服务名称和 TCP 端口号之间的映射 (SOCK_STREAM
) and/or UDP (SOCK_DGRAM
) 协议。
避免将 ruptime 49152/tcp
条目添加到服务数据库的唯一方法是将端口指定为十进制数字字符串,"49152"
而不是名称 "ruptime"
.这会影响服务器和客户端。 (也就是说,即使您的服务器知道 ruptime 是 TCP 套接字的端口 49152,客户端也不会知道,除非他们在自己的服务数据库中有它。)
通常,大多数管理员不会费心编辑服务数据库,而是使用明确的端口号。当您安装了防火墙(以及相关的实用程序,如 fail2ban,我什至在工作站和笔记本电脑上也推荐使用),如果在服务配置文件中清楚地显示端口号,则更容易维护规则。
我自己会使用端口号。
To the client running on the same machine I had to pass the host name. I thought the loopback address would work. What defines the fact that the connection can be created with the hostname but not with the loopback address? Is it that I am passing host as first parameter to the getaddrinfo in the server?
是的。如果将服务绑定到通配符地址,它将响应所有网络接口上的请求,包括环回地址。
如果绑定到特定的主机名,它将只响应对该特定网络接口的请求。
(这是由 OS 内核完成的,并且是网络数据包如何路由到用户空间应用程序的一部分。)
这也意味着绑定到特定主机名(而不是通配符地址)的 "proper" 启用 Internet 的服务应该真正能够侦听多个套接字上的传入连接,而不是只有一个。它可能不是绝对必要的,甚至在 大多数 用例中都不需要,但我可以告诉你,当服务 运行 在跨越多个不同的机器上时,它肯定会派上用场网络,而您只想为其中的一些网络提供服务。幸运的是,您可以使侦听套接字成为非阻塞的(使用 fcntl(fd, F_SETFL, O_NONBLOCK)
——我还建议在定义了 O_CLOEXEC
的系统上使用 fcntl(fd, F_SETFD, O_CLOEXEC)
,这样侦听套接字就不会意外地传递给子进程执行外部二进制文件),然后使用 select()
or poll()
to wait for accept()
可用连接;当连接到达时,每个套接字都变得可读。
我正在用 C 语言构建一个客户端-服务器应用程序,源代码取自《Unix 环境中的高级编程》一书。
在服务器中它正在执行以下操作:
struct addrinfo hint;
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_STREAM;
hint.ai_addr = NULL;
hint.ai_next = NULL;
....
if ((n = sysconf(_SC_HOST_NAME_MAX))<0)
{
n = HOST_NAME_MAX;
}
if((host = malloc(n)) == NULL)
{
printf("malloc error\n");
exit(1);
}
if (gethostname(host, n)<0)
{
printf("gethostname error\n");
exit(1);
}
...
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)
{
//printf("starting to serve\n");
serve(sockfd);
exit(0);
}
}
据我了解,函数 getaddrinfo
用于在主机上查看套接字地址结构 运行 连接名为 ruptime
且类型为 SOCK_STREAM
的服务.
虽然书中没有指定,但为了工作,我必须 运行 在文件 /etc/services/
中添加一个新条目,其中包含一个未使用的端口和指定的名称 ruptime
:
ruptime 49152/tcp #ruptime Unix System Programming
ruptime 49152/udp #ruptime Unix System Programming
虽然未使用,但建议也添加 UDP 部分。
但是文档中说的是
If the
AI_PASSIVE
flag is specified inhints.ai_flags
, and node isNULL
, then the returned socket addresses will be suitable for bind(2)ing a socket that will accept(2) connections. The returned socket address will contain the "wildcard address" (INADDR_ANY
for IPv4 addresses,IN6ADDR_ANY_INIT
for IPv6 address). The wildcard address is used by applications (typically servers) that intend to accept connections on any of the host's network addresses.
所以从这里和其他关于 SO 的讨论中可以看到:
hint.ai_flags |= AI_PASSIVE
...
getaddrinfo(NULL, myserviceport, &hint, &aihint)
好像比较合适
这两种方法到底有什么区别?第二个也在寻找 SOCK_DGM
吗?书中选择第一种方法有什么理由吗?在第二种方式中,因为我在代码中指定了端口,它是否允许避免在 /etc/services/
?
另一个问题。
我必须向客户传递主机名。我认为环回(客户端和服务器在同一台机器上 运行ning)地址可以。相反,主机名类似于 ./client MBPdiPippo.lan
。什么定义了可以使用主机名而不是环回地址创建连接这一事实?是我将 host
作为第一个参数传递给服务器中的 getaddrinfo
吗?
完整代码
server.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> //_SC_HOST_NAME_MAX
#include<string.h>
#include<netdb.h> //Here are defined AF_INET and the others of the family
#include<syslog.h> //LOG_ERR
#include<errno.h> //errno
#include <sys/types.h>
#include"utilities.h"
#include "error.h"
#define BUFLEN 128
#define QLEN 10
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 156
#endif
int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen);
void serve(int sockfd);
int main(int argc, char* argv[])
{
printf("entered main\n");
struct addrinfo *ailist, *aip, hint;
int sockfd, err, n;
char *host;
if (argc != 1)
{
printf("usage: ruptimed\n");
exit(1);
}
if ((n=sysconf(_SC_HOST_NAME_MAX))<0)
{
n = HOST_NAME_MAX;
}
if((host = malloc(n)) == NULL)
{
printf("malloc error\n");
exit(1);
}
if (gethostname(host, n)<0)
{
printf("gethostname error\n");
exit(1);
}
printf("host: %s\n", host);
printf("Daemonizing\n");
int res = daemonize("ruptimed");
printf("%d\n", res);
printf("Daemonized\n");
memset(&hint, 0, sizeof(hint)); //set to 0 all bytes
printf("hint initialized\n");
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_STREAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
printf("getting addresses\n");
if((err = getaddrinfo(host, "ruptime", &hint, &ailist))!=0)
{
printf("error %s\n", gai_strerror(err));
syslog(LOG_ERR, "ruptimed: getaddrinfo error %s", gai_strerror(err));
exit(1);
}
printf("Got addresses\n");
for (aip = ailist; aip!=NULL; aip = aip->ai_next)
{
if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN))>=0)
{
printf("starting to serve\n");
serve(sockfd);
exit(0);
}
}
exit(1);
}
void serve(int sockfd)
{
int clfd;
FILE *fp;
char buf[BUFLEN];
set_cloexec(sockfd);
for(;;)
{
/*After listen, the socket can receive connect requests. accept
retrieves a connect request and converts it into a connection.
The file returned by accept is a socket descriptor connected to the client that
called connect, haing the same coket type and family type. The original
soket remains available to receive otherconneion requests. If we don't care
about client's identity we can set the second (struct sockaddr *addr)
and third parameter (socklen_t *len) to NULL*/
if((clfd = accept(sockfd, NULL, NULL))<0)
{
/*This generates a log mesage.
syslog(int priority, const char *fformat,...)
priority is a combination of facility and level. Levels are ordered from highest to lowest:
LOG_EMERG: emergency system unusable
LOG_ALERT: condiotin that must be fied immediately
LOG_CRIT: critical condition
LOG_ERR: error condition
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
format and other arguments are passed to vsprintf function forf formatting.*/
syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
exit(1);
}
/* set the FD_CLOEXEC file descriptor flag */
/*it causes the file descriptor to be automatically and atomically closed
when any of the exec family function is called*/
set_cloexec(clfd);
/**pg. 542 Since a common operation is to create a pipe to another process
to either read its output or write its input Stdio has provided popen and
pclose: popen creates pipe, close the unused ends of the pipe,
forks a child and call exec to execute cmdstr and
returns a file pointer (connected to std output if "r", to stdin if "w").
pclose closes the stream, waits for the command to terminate*/
if ((fp = popen("/usr/bin/uptime", "r")) == NULL)
{
/*sprintf copy the string passed as second parameter inside buf*/
sprintf(buf, "error: %s\n", strerror(errno));
/*pag 610. send is similar to write. send(int sockfd, const void *buf, size_t nbytes, it flags)*/
send(clfd, buf, strlen(buf),0);
}
else
{
/*get data from the pipe that reads created to exec /usr/bin/uptime */
while(fgets(buf, BUFLEN, fp)!=NULL)
{
/* clfd is returned by accept and it is a socket descriptor
connected to the client that called connect*/
send(clfd, buf, strlen(buf), 0);
}
/*see popen pag. 542*/
pclose(fp);
}
close(clfd);
}
}
int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen)
{
int fd, err;
int reuse = 1;
if ((fd = socket(addr->sa_family, type, 0))<0)
{
return (-1);
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))<0)
{
goto errout;
}
if(bind(fd, addr, alen)<0)
{
goto errout;
}
if (type == SOCK_STREAM || type == SOCK_SEQPACKET)
{
if(listen(fd, qlen)<0)
{
goto errout;
}
}
return fd;
errout:
err = errno;
close (fd);
errno = err;
return(-1);
}
utilities.c
:包含demonize
和setcloexec
函数。在 daemonize
函数中,我没有关闭文件描述符进行调试。
#include "utilities.h"
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <syslog.h>
#include <sys/time.h>//getrlimit
#include <sys/resource.h>//getrlimit
#include <signal.h> //sigempyset , asigcation (umask?)
#include <sys/resource.h>
#include <fcntl.h> //O_RDWR
#include <stdarg.h>
#include "error.h"
int daemonize(const char *cmd)
{
int fd0, fd1, fd2;
unsigned int i;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
/* *Clear file creation mask.*/
umask(0);
/* *Get maximum number of file descriptors. */
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
{
err_quit("%s: can’t get file limit", cmd);
}
/* *Become a session leader to lose controlling TTY. */
if ((pid = fork()) < 0)
{
err_quit("%s: can’t fork", cmd);
}
else if (pid != 0) /* parent */
{
exit(0); //the parent will exit
}
setsid();
/* *Ensure future opens won’t allocate controlling TTYs. */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
{
err_quit("%s: can’t ignore SIGHUP", cmd);
}
if ((pid = fork()) < 0)
{
err_quit("%s: can’t fork", cmd);
}
else if (pid != 0) /* parent */
{
exit(0);
}
/*
*Change the current working directory to the root so
* we won’t prevent file systems from being unmounted.
*/
if (chdir("/") < 0)
{
err_quit("%s: can’t change directory to /", cmd);
}
/* Close all open file descriptors. */
if (rl.rlim_max == RLIM_INFINITY)
{
rl.rlim_max = 1024;
}
printf("closing file descriptors\n");
/*for (i = 0; i < rl.rlim_max; i++)
{
close(i);
}*/
/* *Attach file descriptors 0, 1, and 2 to /dev/null.*/
//printf not working
/*printf("closed all file descriptors for daemonizing\n");*/
/*fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);*/
/* *Initialize the log file. Daemons do not have a controlling terminal so
they can't write to stderror. We don't want them to write to the console device
because on many workstations the control device runs a windowing system. They can't
write on separate files either. A central daemon error-logging facility is required.
This is the BSD. 3 ways to generate log messages:
1) kernel routines call the log function. These messages can be read from /dev/klog
2) Most user processes (daemons) call syslog to generate log messages. This causes
messages to be sent to the UNIX domain datagram socket /dev/log
3) A user process on this host or on other host connected to this with TCP/ID
can send log messages to UDP port 514. Explicit network programmin is required
(it is not managed by syslog.
The syslogd daemon reads al three of log messages.
openlog is optional since if not called, syslog calls it. Also closelog is optional
openlog(const char *ident, int option, int facility)
It lets us specify ident that is added to each logmessage. option is a bitmask:
LOG_CONS tells that if the log message can't be sent to syslogd via UNIX
domain datagram, the message is written to the console instead.
facility lets the configuration file specify that messages from different
facilities are to be handled differently. It can be specified also in the 'priority'
argument of syslog. LOG_DAEMON is for system deamons
*/
/*
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2)
{*/
/*This generates a log mesage.
syslog(int priority, const char *fformat,...)
priority is a combination of facility and level. Levels are ordered from highest to lowest:
LOG_EMERG: emergency system unusable
LOG_ALERT: condiotin that must be fied immediately
LOG_CRIT: critical condition
LOG_ERR: error condition
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
format and other arguments are passed to vsprintf function forf formatting.*/
/*syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}*/
return 0;
}
/*The function set the FD_CLOEXEC flag of the file descriptor already open that
is passed to as parameter. FD_CLOEXEC causes the file descriptor to be
automatically and atomically closed when any of the exec family function is
called*/
int set_cloexec(int fd)
{
int val;
/* retrieve the flags of the file descriptor */
if((val = fcntl(fd, F_GETFD, 0))<0)
{
return -1;
}
/* set the FD_CLOEXEC file descriptor flag */
/*it causes the file descriptor to be automatically and atomically closed
when any of the exec family function is called*/
val |= FD_CLOEXEC;
return (fcntl(fd, F_SETFD, val));
}
错误函数我用了
/* Fatal error unrelated to a system call.
* Print a message and terminate*/
void err_quit (const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
err_doit (0, 0, fmt, ap);
va_end (ap);
exit(1);
}
/*Print a message and return to caller.
*Caller specifies "errnoflag"*/
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap)
{
char buf [MAXLINE];
vsnprintf (buf, MAXLINE-1, fmt, ap);
if (errnoflag)
{
snprintf (buf+strlen(buf), MAXLINE-strlen(buf)-1, ": %s",
strerror (error));
}
strcat(buf, "\n");
fflush(stdout); /*in case stdout and stderr are the same*/
fputs (buf, stderr);
fflush(NULL); /* flushes all stdio output streams*/
}
首先,吹毛求疵。 getaddrinfo()
代码应合并到initserver()
函数中,并在循环后释放(使用freeaddrinfo()
)套接字结构链表。这使得代码更易于维护;您想将紧密耦合的实现保持在一起。
Exactly what is the difference between these two methods?
绑定到通配符地址(即,在使用 getaddrinfo()
获取合适的套接字描述时使用 NULL
节点和 AI_PASSIVE
标志)意味着套接字作为一个绑定到所有网络接口设置,而不是特定的网络接口。当您绑定到一个特定的节点名称时,您就绑定到一个特定的网络接口。
实际上,这意味着如果额外的网络接口在 运行 时间可用,内核将在路由数据包时考虑它们 to/from 绑定到通配符地址的套接字。
这确实应该由每个系统管理员做出选择,因为在某些用例中服务(您的应用程序)应该监听所有网络接口上的传入连接,但在其他用例中服务应该监听仅特定或某些特定接口上的传入连接。典型的情况是一台机器连接到多个网络。这对于服务器来说非常普遍。对于实际案例,请参见例如如何配置 the Apache web server。
就我个人而言,我会重写 OP 的 initServer()
函数,使其看起来像下面这样:
enum {
/* TCP=1, UDP=2, IPv4=4, IPv6=8 */
SERVER_TCPv4 = 5, /* IPv4 | TCP */
SERVER_UDPv4 = 6, /* IPv4 | UDP */
SERVER_TCPv6 = 9, /* IPv6 | TCP */
SERVER_UDPv6 = 10, /* IPv6 | UDP */
SERVER_TCP = 13, /* Any | TCP */
SERVER_UDP = 14 /* Any | UDP */
};
int initServer(const char *host, const char *port,
const int type, const int backlog)
{
struct addrinfo hints, *list, *curr;
const char *node;
int family, socktype, result, fd;
if (!host || !*host || !strcmp(host, "*"))
node = NULL;
else
node = host;
switch (type) {
case SERVER_TCPv4: family = AF_INET; socktype = SOCK_STREAM; break;
case SERVER_TCPv6: family = AF_INET6; socktype = SOCK_STREAM; break;
case SERVER_TCP: family = AF_UNSPEC; socktype = SOCK_STREAM; break;
case SERVER_UDPv4: family = AF_INET; socktype = SOCK_DGRAM; break;
case SERVER_UDPv6: family = AF_INET6; socktype = SOCK_DGRAM; break;
case SERVER_UDP: family = AF_UNSPEC; socktype = SOCK_DGRAM; break;
default:
fprintf(stderr, "initServer(): Invalid server type.\n");
return -1;
}
memset(&hints, 0, sizeof hints);
hints.ai_flags = AI_PASSIVE;
hints.ai_family = family;
hints.ai_socktype = socktype;
hints.ai_protocol = 0;
hints.ai_canonname = NULL;
hints.ai_addr = NULL;
hints.ai_next = NULL;
result = getaddrinfo(node, port, &hints, &list);
if (result) {
/* Fail. Output error message to standard error. */
fprintf(stderr, "initServer(): %s.\n", gai_strerror(result));
return -1;
}
fd = -1;
for (curr = list; curr != NULL; curr = curr->ai_next) {
int reuse = 1;
fd = socket(curr->ai_family, curr->ai_socktype, curr->ai_protocol);
if (fd == -1)
continue;
if (bind(fd, curr->ai_addr, curr->ai_addrlen) == -1) {
close(fd);
fd = -1;
continue;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof (int)) == -1) {
close(fd);
fd = -1;
continue;
}
if (listen(fd, backlog) == -1) {
close(fd);
fd = -1;
continue;
}
break;
}
freeaddrinfo(list);
if (fd == -1) {
fprintf(stderr, "initServer(): Cannot bind to a valid socket.\n");
return -1;
}
return fd;
}
(注意:代码未经测试,甚至未编译;但底层逻辑是合理的。如果您发现任何问题或错误,请在评论中告诉我,以便我在必要时进行审查、检查和修复。 )
这样,您可以从配置文件中读取 host
和 port
。如果 host
为 "*"
、空或 NULL
,该函数将尝试绑定到通配符地址。 (顺便说一下,这应该是默认设置;如果服务器管理员想要限制到特定接口,他们可以提供 IP 地址或与该接口对应的主机名。)
同样,系统管理员可以使用配置文件将port
指定为services
数据库中定义的任何字符串(getent services
),或者指定为十进制数字字符串;在 OP 的情况下,"49152"
和 "ruptime"
都可以。
Since I am specifying the port in the code, does it allow to avoid adding a new entry in the /etc/services/?
services 数据库(运行 getent services
在您的计算机上查看)仅包含服务名称和 TCP 端口号之间的映射 (SOCK_STREAM
) and/or UDP (SOCK_DGRAM
) 协议。
避免将 ruptime 49152/tcp
条目添加到服务数据库的唯一方法是将端口指定为十进制数字字符串,"49152"
而不是名称 "ruptime"
.这会影响服务器和客户端。 (也就是说,即使您的服务器知道 ruptime 是 TCP 套接字的端口 49152,客户端也不会知道,除非他们在自己的服务数据库中有它。)
通常,大多数管理员不会费心编辑服务数据库,而是使用明确的端口号。当您安装了防火墙(以及相关的实用程序,如 fail2ban,我什至在工作站和笔记本电脑上也推荐使用),如果在服务配置文件中清楚地显示端口号,则更容易维护规则。
我自己会使用端口号。
To the client running on the same machine I had to pass the host name. I thought the loopback address would work. What defines the fact that the connection can be created with the hostname but not with the loopback address? Is it that I am passing host as first parameter to the getaddrinfo in the server?
是的。如果将服务绑定到通配符地址,它将响应所有网络接口上的请求,包括环回地址。
如果绑定到特定的主机名,它将只响应对该特定网络接口的请求。
(这是由 OS 内核完成的,并且是网络数据包如何路由到用户空间应用程序的一部分。)
这也意味着绑定到特定主机名(而不是通配符地址)的 "proper" 启用 Internet 的服务应该真正能够侦听多个套接字上的传入连接,而不是只有一个。它可能不是绝对必要的,甚至在 大多数 用例中都不需要,但我可以告诉你,当服务 运行 在跨越多个不同的机器上时,它肯定会派上用场网络,而您只想为其中的一些网络提供服务。幸运的是,您可以使侦听套接字成为非阻塞的(使用 fcntl(fd, F_SETFL, O_NONBLOCK)
——我还建议在定义了 O_CLOEXEC
的系统上使用 fcntl(fd, F_SETFD, O_CLOEXEC)
,这样侦听套接字就不会意外地传递给子进程执行外部二进制文件),然后使用 select()
or poll()
to wait for accept()
可用连接;当连接到达时,每个套接字都变得可读。