当编写fork子进程处理连接的服务器程序时,子进程退出会给父进程产生SIGCHLD信号,父进程若不处理该信号会导致僵尸进程。
处理SIGCHLD信号,使用waitpid调用,不能使用wait简单处理。一般的处理方法如下(信号处理函数):
void
sig_chld(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 )
continue;
return;
}
信号处理可能会中断慢系统调用,所以我们必须对慢系统调用返回EINTR错误做准备。一般处理方法如下(以accept为例):
for (;;) {
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen) < 0) {
if (errno == EINTR)
continue;
else
err_sys("accept error");
}
此法对accept、read、write、select、open等合适;但connect调用不能重启,若connect返回EINTR,我们不能再调用它,否则立即返回错误。
连接在listen后建立,在accept前夭折,此时accept可能会返回错误,对此必须处理。
但对于夭折的连接处理依赖于实现,源自Berkekey的实现在内核中处理夭折的连接,服务器进程根本看不到,若当前没有新连接,则accept阻 塞;大多数SVR4实现会返回一个错误给进程,作为accept的返回,此值可能是EPROTO(“协议错”)errno值;Posix.1g要求返回 ECONNABORTED(“软件引起的连接夭折”)。
所以,在ECONNABORTED错误情况下,服务器可以忽略错误而简单的再调用accept一次。
经过测试,在Solaris8和AIX4.3下返回ECONNABORTED,但在SCO Openserver 5.05下返回EINVAL。
当工作在有两个或多个描述字的情况下,不能只阻塞于某个特定源输入中,而是应该阻塞于任一个源的输入中。否则在网络描述字中会出现有异常数据而没有读到,导致出错的情况。
方法是使用select或poll。
在一个服务器进程终止的情况下,终止关闭了描述字,此时客户对此描述字写,服务器TCP接收到来自客户的数据,由于先前打开那个套接口的进程已经终 止,所以以RST响应。客户端可能会收到先前关闭时的FIN,也可能收到后面的RST,这依赖于当时的具体情况。因为同时有FIN和RST时,RST优先 于FIN。
当进程向接收了RST的套接口进行写操作时,内核个该进程发一个SIGPIPE信号,此信号的缺省行为是终止进程,所以,进程必须捕获该信号以免不情愿的被终止。
进程不论是捕获了该信号并从其信号处理程序返回,还是不理会该信号,写操作都会返回EPIPE错误。
写一个已接收FIN的套接口是可行的,但写一个已接收了RST的套接口则是错误的。
处理SIGPIPE的建议方法取决于它发生时应用想做什么。如果没有什么特殊的情况处理,可将信号处理方法简单的设置为SIG_IGN,并假设后续 的写操作将捕捉EPIPE错误并终止。若在信号处理程序中处理,要注意的是,如果使用了多个套接口,该信号的递交无法知道是哪个套接口出的错。
假设服务器主机已经崩溃,客户此时写数据,客户TCP会持续重传数据分节,若客户的数据分节根本没有响应,则错误为ETIMEDOUT;若某些中间 路由器判定服务器主机不可达,且以一个目的地不可达饿ICMP消息响应,则错误是EHOSTUNREACH或ENETUNREACH。
以上情况只有向服务器主机发送数据时,才能检测出它已经崩溃。如果不主动发送也想检测出崩溃情况,则需要设置套接口选项SO_KEEPALIVE。
当服务器主机崩溃并重启后,客户向服务器发送数据,由于服务器重启,它的TCP丢失了崩溃前的所有连接信息,所以服务器TCP对接收到的客户数据分节以RST响应。若客户阻塞于读,则返回ECONNRESET错误。
服务器关机会终止所有进程,进程退出时会关闭描述字,所以该情况的处理类似于SIGPIPE中服务器进程终止的讨论。
历史上,gethostbyname、gethostbyname2、gethostbyaddr、getservbyname和 getservbyport是不可重入的。一些支持线程的实现提供了相应的可重入版本(以_r结尾),也有些实现提供了这些函数的使用线程特定数据的可重 入版本。
inet_pton和inet_ntop总是可重入的。
历史上inet_ntoa是不可重入的,但一些实现提供了使用线程特定数据的可重入版本。
getaddrinfo只有在它自己调用的是可重入函数时才是可重入的。
getnameinfo只有在它自己调用的是可重入函数时才是可重入的。
有三种方法:
在sendmsg和recvmsg中可以使用msghdr结构中的msg_control和msg_controllen成员发送和接收辅助数据(ancillary data)。辅助数据的另一个叫法是控制信息(control information)。
辅助数据的各种用法如下:
协议 | cmsg_level | cmsg_type | 说明 |
---|---|---|---|
IPv4 | IPPROTO_IP | IP_RECVDSTADDR | 接收UDP数据报的目的地址 |
IP_RECVIF | 接收UDP数据报的接口索引 | ||
IPv6 | IPPROTO_IPV6 | IPV6_DSTOPTS | 指定/接收目标选项 |
IPV6_HOPLIMIT | 指定/接收跳限 | ||
IPV6_HOPOPTS | 指定/接收步跳选项 | ||
IPV6_NEXTHOP | 指定下一跳地址 | ||
IPV6_PKTINFO | 指定/接收分组信息 | ||
IPV6_RTHDR | 指定/接收路由头部 | ||
Unix域 | SOL_SOCKET | SCM_RIGHTS | 发送/接收描述字 |
SCM_CREDS | 发送/接收用户凭证 |
辅助数据由一个或多个辅助数据对象组成,每个对象由一个cmsghdr结构开头,该结构在<sys/socket.h>中定义如下:
struct cmsghdr {
socklen_t cmsg_len;
/* length in bytes, including this structure */
int cmsg_level;
/* originating protocol */
int cmsg_type;
/* protocol-specific type followed by unsigned char cmsg_data[] */
};
实际数据在cmsghdr结构后面,msg_control指向的辅助数据必须按cmsghdr结构进行对齐,所以在cmsg_type成员和实际数据之间可能有填充字节,在数据之后,下一个对象之前也可能有填充字节。使用如下的宏屏蔽可能出现的填充字节:
#include <sys/socket.h>
#include <sys/param.h>
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdrptr);
返回:指向第一个cmsghdr结构的指针,无辅助数据时为NULL
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr, \
struct cmsghdr *cmsgptr);
返回:指向下一个cmsghdr结构的指针,不再有辅助数据对象时为NULL
unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr);
返回:指向与cmsghdr结构关联的数据的第一个字节的指针
unsigned int CMSG_LEN(unsigned int length);
返回:给定数据量下存储在cmsg_len中的值
unsigned int CMSG_SPACE(unsigned int length);
返回:给定数据量写一个辅助数据对象的总大小
CMSG_LEN和CMSG_SPACE的区别在于,前者不将填充字节计算在内,因此等于cmsg_len的值,但后者将这些填充字节都计算在内,因此在动态申请辅助数据对象的数据空间时使用这个值。
有三种方法:
UNIX域协议并不是一个实际的协议族,它只是在同一台主机上进行C/S通信时,使用与在不同主机上的C/S通信时相同的API的一种方法。
UNIX域提供了两种类型的套接口:字节流套接口(与TCP类似)和数据报套接口(与UDP类似)。
使用UNIX域协议有三个原因:
UNIX域套接口地址结构如下:
#include
struct sockaddr_un {
uint8_t sun_len;
sa_family_t sun_family; /* AF_LOCAL */
char sun_path[104]; /* null_terminated pathname */
};
sun_path数组中存放的路径名必须以空字符结尾。系统提供了SUN_LEN宏,他以一个指向sockaddr_un结构的指针为参数,返回该 结构的长度,长度里包括路径名中的非空字节数。未指定地址以空字符串的路径名表示,这是UNIX域中与IPv4的INADDR_ANY常值或IPv6的 IN6ADDR_ANY_INIT常值等价的一个地址。
注意:给UNIX域套接口bind的路径名如果在文件系统中已经存在,则bind将失败。为预防此种情况,可以先调用unlink。
下面是Posix.1g的一些要求,并不是所有的实现都做到了这一级:
使用UNIX域套接口可以在两个没有关系的进程之间传递描述字。4.4BSD的技术允许一次sendmsg传递多个描述字,而SVR4的技术一次只能传递一个描述字。
在两个进程之间传递描述字的步骤如下:
缺省状态下,套接口是阻塞方式的,当一个套接口调用不能立即完成时,进程进入睡眠状态,等待操作完成。可能阻塞的套接口调用分为如下四种:
对于非阻塞I/O不能马上完成返回的错误码不同实现有差异:系统V返回EAGAIN,而Berkeley返回EWOULDBLOCK,幸好这两个错误码在多数实现中值相等。
非阻塞connect有如下三种用途:
处理非阻塞connect有一些细节需要处理:
非阻塞connect有许多移植性的问题,如getsockopt等,需要注意。
一个被中断的阻塞connect不自动重启的情况下,它会返回EINTR,此时我们不能再调用connect等待连接建立完成,这样做将返回EADDRINUSE错误。
在这种情况下要做的是要么关闭套接口,重新调用socket和connect;要么调用select,和非阻塞connect一样处理,使select在连接建立成功(使套接口可写)或连接失败(使套接口科可读又可写)时返回。
如前“accept返回前连接夭折”所述,在服务器调用accept之前客户如果放弃这个连接,源自Berkeley的实现不对服务器返回这个夭折的连接,而其他实现应返回ECONNABORTED错误,但一般返回EPROTO错误。
如果在accept之前使用select来测试连接是否准备好,要注意的是在select和accept之间如果连接夭折,源自Berkeley的实现会导致accept阻塞,直到其他某个客户建立一个连接为止。所以使用select并不能保证accept不会阻塞。
解决方法是:
服务器程序设计方法列出如下9种:
有如下结论:
在winsock中套接口描述符用SOCKET而不是int类型定义。
尽量避免在套接口字上使用read和write函数,因为在WIN32中不支持它们。我们一般用recv、recvfrom和recvmsg来表示“读”,用send、sendto和sendmsg表示“写”。
软件应编写成能够处理任何想象的到的错误,不管该错误可能发生的概率是如何的小。
在网络编程中,应当牢记的规则是:不能假设对等方会严格遵守应用程序协议,甚至是在我们实现双方协议的时候也是这样。
如在长链中检查客户端是否终止,检查输入的有效性,注意缓冲区的溢出和指针失控。
大多数的TCP/IP应用程序属于下面四种之一:TCP服务器、TCP客户端、UDP服务器、UDP客户端。
每类应用程序都有类似的代码来完成初始化工作,所以可以定义一些程序框架(模板)。使用时根据情况稍做修改即可。
当应用程序启用TCP的keep-alive机制时,TCP就会在连接已经空闲了一定时间间隔后发送一个特殊的段给对等方。如果对等方主机可以到达 而且对等方应用程序依然运行,对等方TCP就会响应一个ACK应答,此时通讯正常,TCP将发送keep-alive空闲时间重置为0,应用程序不会接收 到消息交换的任何通知。
如果对等方主机可以到达,但对等方应用程序没有运行,则对等方TCP响应RST,发送方的TCP撤消连接并返回ECONNRESET错误给应用程序。
如果对等方主机没有响应ACK或RST消息,发送方TCP继续发送keep-alive探询消息,直到它认为对等方不可达到或已经崩溃。此时撤消连 接并通知应用ETIMEDOUT错误。如果路由器已经返回主机或网络不可到达的ICMP消息的话,则返回EHOSTUNREACH或 ENETUNREACH错误。
TCP的keep-alive机制的问题是检测的空闲时间太长(至少是2小时),而且它不仅检测死连接,同时也撤消它们,有时这并不是应用程序所期望的。
可以在应用程序中实现类似的机制来解决keep-alive的问题,通过编写心博(heartbeat)函数。
应用程序写操作成功并不表示数据已经发送到对等方。写操作把数据放到发送缓冲区中,除非TCP发送缓冲区已满,否则写操作是不会阻塞的,这意味着写操作几乎总是立即返回,而且如果它返回了,也不能保证所写数据的位置。
实际上,当写操作返回时,写入的部分或全部数据可能仍旧在排队等待传输,如果此时对等方主机或应用程序崩溃的话,数据将会丢失。
因为写操作可能在数据实际发送之前就已经返回,所以当传输发生错误时,该错误通常是从下一个操作返回。写操作返回的错误仅仅是那些在调用写操作时就已经发生的错误,除了EPIPE错误。
利用select来实现一个通用的分时机制,允许在一定时间间隔后指定一个必须发生的事件,而且使该事件在指定的时间上异步发生。
TIME-WAIT状态是TCP可靠性中很重要的一部分,程序员不应该试图绕过它,虽然可以使用SO_LINGER套接口选项来取消它。
对于TCP服务器总是应该设置SO_REUSEADDR选项,否则没法重新启动一个在TIME-WAIT状态中还存在连接的服务器。
该选项必须在bind()之前调用setsockopt()设置。
使用大型写操作的原因除了避免上下文切换外,主要目的是避免因过多的小规模写操作导致Nagle算法的影响,从而对程序性能造成极大的负面影响。
虽然可以使用套接口选项TCP_NODELAY来禁止Nagle算法,但不是解决问题的方法。
所以要减少小规模的写操作,多次写合并成一次写。实现的一种方法是使用聚集写:readv和writev。在Winsock中则使用不同但类似的接口:WSAsend。
把connect设置为非阻塞的主要用途是使connect具有超时机制。使用select是实现的一种方法。但使用该法有比较多的可移植问题。
程序的关键是在判断连接是否成功的地方。下面是UNIX版本:
int isconnected(SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex)
{
int err;
int len = sizeof(err);
errno = 0; /* assume no error */
if (!FD_ISSET(s, rd) && !FD_ISSET(s, wr))
return 0;
if (getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
return 0;
errno = err; /* in case we're not connected */
return err == 0;
}
在UNIX中,连接一旦建立,则套接口就可以执行写操作;如果发生了错误,套接口就既可读又可写。但我们不能依据这些来判断是否成功,要使用getsockopt来取错误状态。
但getsockopt也有问题,在UNIX的有些实现中,如果套接口发生了错误则getsockopt返回-1;而其他实现仅返回套接口的错误状态而让调用者来检查。所以需要对两种情况都考虑。
下面是Winsock的版本:
int isconnected(SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex)
{
WSASetLastError(0);
if (!FD_ISSET(s, rd) && !FD_ISSET(s, wr))
return 0;
if (FD_ISSET(s, ex))
return 0;
return 1;
}
在Winsock下,当使用select时,在非阻塞套接口上调用connect返回的错误有异常事件来指示。
在许多网络程序中,把数据从一个缓冲区拷贝到另一个缓冲区的的操作占用了大多数的处理时间。所以,在一个进程里避免这种拷贝是一个好的程序设计习惯。
一种方法是预留空间。如:
rc = read(fd, buf + sizeof(struct hrd),
sizeof(buf) - sizeof(struct hdr));
在多进程环境中,可以使用共享内存区来避免数据拷贝。在UNIX下使用shmget等调用,在Windows下使用类似于文件映射的机制来实现。
通常我们只使用sockaddr-in结构中的三个域:sin_family、sin_port和sin_addr,但许多实现还包括了其他的域,这些域的值影响着套接口操作。所以,在使用该结构之前把它初始化为0是一个很好的习惯。
TCP缓冲区的大小依赖于应用程序,对于一个交互式的应用程序如telnet,小的缓冲区就足够了。
对于非交互式的应用,应当设置它们的发送缓冲区至少为3倍的MSS大小,这要在listen或connect之前执行。