首页 > 代码库 > UNIX网络编程:卷1-读书笔记

UNIX网络编程:卷1-读书笔记

1. if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    err_sys("socket error");
作为一种编码风格,作者总是在这样的两个左括号间加一个空格,提示比较运算的左侧同时也是一个赋值运算。

2. bzero不是ANSI C函数,比memset更好记忆(只有2个参数)

3. htons(“主机到网络短整数”),inet_pton("呈现形式到数值")

4. 每当一个套接字函数需要一个指向某个套接字地址结构的指针时,这个指针必须强制类型转换成一个指向通用套接字地址结构的指针。这是因为套接字函数早于ANSI C标准,20世纪80年代早期开发这些函数时,ANSI C的void*指针类型还不可用。

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    error("inet_pton error");
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) // 把struct sockaddr_in转换成struct sockaddr
    error("connect error");
5. Unix在一个进程终止时总是关闭该进程所有打开的描述符,我们的TCP套接字就此被关闭。

6. errno只要Unix函数中有错误发生,全局变量errno就被置为一个指明该错误类型的正值,函数本身通常返回-1。错误码定义在<sys/errno.h>头文件中。

7. 

servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13);
//注:指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户连接
8. 调用listen函数把该套接字转换成一个监听套接字,这样来自客户的外来连接就可以再该套接字上由内核接受

9. 当前时间和日期

time_t ticks;
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
注:库函数time返回自Unix纪元1970年1月1日0点0分0秒以来的秒数。ctime把该整数值转换成直观可读的时间格式

应尽量使用snprintf代替sprintf,因为后者不检查buff是否溢出。snprintf会默认留出最后一个字节‘\0‘。

10. netstat -ni 提供网络接口信息

netstat -nr 展示路由表

查看接口详细信息:ifconfig eth0

找出本地众多主机IP地址的方法之一是,针对从上一步找到的本地接口广播地址执行ping命令

ping -b 206.168.112.127

11. 建立连接:

(1)客户通过调用connect发起主动打开。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只包含有一个IP首部,一个TCP首部及可能的TCP选项(见12)。

(2)服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个字节中发送SYN和对客户SYN的ACK(确认)。

(3)客户必须确认服务器的SYN。

这就是TCP的三路握手。

12. TCP选项:

(1)MSS选项。发送SYN的TCP一端使用本选项通告对端它的最大分节大小。TCP_MAXSEG

(2)窗口规模选项。TCP连接任何一端能够通告对端的最大窗口大小是65535(字段16位)。SO_RCVBUF

(3)时间戳。程序员无需考虑。

13. 终止连接:

(1)某个应用进程先调用close,该端的TCP发送一个FIN分节,表示数据发送完毕。

(2)接收到这个FIN的对端执行被动关闭。这个FIN由TCP确认。FIN意味着接收端应用进程在相应连接上再无额外数据可接收。

(3)一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。

(4)接收这个最终FIN的原发送端TCP确认这个FIN。

SYN和FIN都占一个字节的序列号空间。

从步骤2与步骤3之间,称之为半关闭。

当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。

14. 端口号:

(1)well-known port:0~1023

(2)已登记的端口1024~49151

(3)49152~65535是动态的或私用的端口

15. 如果一台机器是多宿的,有多个ip地址。可以通过在调用bind之前把套接字地址结构中的IP地址字段设置成INADDR_ANY来指定。(见7)

16. TCP无法仅仅通过查看目的端口号来分离外来的分节到不同的端口。它必须查看套接字对的所有4个元素才能确定由哪个端点接收某个到达的分节。

套接字对的4个元素指:客户端ip:port + 服务器ip:port

17. IPv4数据报的最大大小是65535字节,包括IPv4首部。

IPv6数据报的最大大小是65575字节,包括40字节的IPv6首部。

注意:IPv6的净荷长度字段不包括IPv6首部,而IPv4的总长度字段包括IPv4首部。

两个主机之间的路径中的最小MTU称之为“路径MTU”。1500字节的以太网MTU是当今最常见的路径MTU。

IPv4首部的“不分片”位(DF位)若被设置,那么不管是产生它的主机或转发它的路由都不允许对它们分片。路由接到一个超出其外出链路MTU大小且设置了DF位的IPv4数据报时,产生一个ICMPv4(目的不可达)的出错消息。

IPv6的路由器不执行分片,每个IPv6数据报都隐含一个DF位。

DF位可用于路径MTU发现。(此方法是有问题的)

IPv4和IPv6都定义了最小重组缓冲区大小。IPv4为576字节,IPv6是1500字节。

TCP的MSS(最大分节大小):常被设置成MTU减去IP和TCP首部的固定长度。在以太网中IPv4的MSS值为1460,IPv6的MSS值为1440(TCP首部都是20字节,但IPv4首部20字节,IPv6首部却是40字节)

18. UDP的发送缓冲区是虚拟的,当然如果发送一个过大的内核还是会返回EMSGSIZE的错误。

19. 数据链路层有一个输出队列,如果该队列空间不够,那么会返回错误,TCP会捕捉到这个错误,并处理,然后UDP有的实现会返回给应用层这个错误,有的实现不返回,很是蛋疼。

20. IPv4套接字地址结构以sockaddr_in命名,在<netinet/in.h>头文件中。

struct in_addr {
    in_addr_t s_addr; /*32-bit IPv4 addr, network byte ordered*/
};
struct sockaddr_in {
    uint8_t sin_len;  /*通常不用理会*/
    sa_family_t sin_family; /*AF_INET*/
    in_port_t sin_port; /*16-bit*/
    struct in_addr sin_addr; 
    char sin_zero[8]; /*unused*/
};
21. IPv6套接字地址结构,在<netinet/in.h>头文件中。

struct in6_addr {
	uint8_t s6_addr[16]; /*128-bit IPv6 addr*/
};
#define SIN6_LEN /*FOR TEST*/
struct sockaddr_in6 {
	uint8_t 		sin6_len; 			/*len of this struct(28)*/
	sa_family_t 	sin6_family;
	in_port_t		sin6_port;
	uint32_t		sin6_flowinfo;
	struct in6_addr	sin6_addr;
	uint32)t 		sin6_scope_id;		/*set of interface for a scope*/
};
22. 值-结果参数:

(1)从进程到内核传递套接字地址结构的函数:bind, connect, sendto,两个参数一个某个套接字的指针,另一个该结构的整数大小

(2)从内核到进程的函数:accept, recvfrom, getsockname, getpeername,两个参数指向某套接字的指针,指向表示该结构大小的整数变量的指针。
23. 字节排序:

小端:低字节在起始地址

大端:高字节在起始地址

网络协议使用大端字节序来传送。

24. 字节序转换函数:

#include <netinet/in.h>

uint16_t htons(uint16_t);
uint32_t htonl(uint32_t); 

uint16_t ntohs(uint16_t);
uint32_t ntohl(uint32_t);
25. 地址转换函数:
#include <arpa/inet.h>

int inet_aton(const char *strptr, struct in_addr *addrptr);
char *inet_ntoa(struct in_addr inaddr);  // 使用static域来存储返回的地址,所以它是非线程安全的(非可重入的)

in_addr_t inet_addr(const char *strptr); // 已废
建议使用inet_pton和inet_ntop代替

26. 

#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, struct in_addr *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); // error, return NULL
// addrptr是数值格式指针,strptr是表达格式地址,如果成功返回strptr,如果失败返回NULL
27. 若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。在每次connect失败后,都必须close当前的套接字并重新调用socket。

28. 在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。

29. bind可以指定具体ip地址,也可以指定通配地址,对于IPv4通配地址是INADDR_ANY

struct sockaddr_in seraddr;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY值为0,用不用htonl都无所谓
IPv6为:
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any; // 系统预先分配in6addr_any变量并初始化为IN6ADDR_ANY_INIT
如果不指定bind的端口,内核会为套接字选择一个临时的端口号。如果要获取这个端口号,必须调用函数getsockname来返回协议地址。

30. bind表示为一个无名的套接字命名。

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
31. listen函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);

(1)当创建一个socket套接字时,它被假设为一个主动套接字,例如客户端程序,直接用connect去连接。listen函数把这个套接字转换成被动套接字,用来接受连接请求。把套接字从CLOSED状态转换成LISTEN状态。

(2)第二个参数规定了内核应该为相应套接字排队的最大连接个数。

内核为任何一个给定的监听套接字维护2个队列:

(1)未完成连接队列(三次握手未完成SYN_RECV)

(2)已完成连接队列(三次握手已完成ESTABLISHED)

backlog是2个队列之和。它存在的意义是在被监听的套接字的应用程序停止连接的时候,防止内核在该套接字上继续接受新的连接请求!

32. accept函数,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程阻塞(如果阻塞模式)。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
如果第二个和第三个参数都是空,表示对客户端身份不感兴趣。

33. fork的2个经典用法

(1)一个进程创建一个自身的副本。

(2)一个进程想要执行另一个程序。调用fork后在子进程中调用exec函数族中的某一个。

注:进程在调用exec之前打开着的描述符通常跨exec继续保持打开。通过fcntl设置FD_CLOEXEC描述符标志禁止掉。

34. 典型的并发服务器轮廓(多进程)

listenfd = socket(...);
Bind(listenfd, ...);
Listen(listenfd, ...);

for (;;) {
	connfd = Accept(listenfd, ...);
	if ( (pid == fork()) == 0) {
		// child process
		close(listenfd);
		doit(connfd);
		close(connfd);
		exit(0);
	}
	close(connfd);// parent process
}
在子进程中关掉了listenfd,在父进程关掉了connfd。

这样做是因为每个文件或套接字都有一个引用计数。引用计数在文件表项中维护。fork后listenfd和connfd的引用计数都为2,关掉后变成1。等最后运行结束后才会真正的释放。(close会发送一个FIN)

35. close由于有引用计数的存在,如果引用计数并不为0,那么不会发送FIN。如果想强制这么做就改用shutdown函数。

36. getsockname: 返回与某个套接字关联的“本地地址”

getpeername:返回与某个套接字关联的“外地地址”

如果不知道套接字地址结构的类型,我们采用sockaddr_storage这个通用的结构,它能承载系统支持的任何套接字地址结构。

int sockfd_to_family (int sockfd) {
	struct sockaddr_storage ss;
	socklen_t len;

	len = sizeof(ss);
	if (getsockname(sockfd, (struct sockaddr*)&ss, &len) < 0)
		return (-1);

	return ss.ss_family;
}
37. 当子进程运行结束,它会向父进程发送一个SIGCHLD信号,如果不显示处理它,这个子进程就会一直处于僵死状态,设置僵死状态的目的是维护子进程的信息(子进程ID,终止状态,资源利用信息,CPU时间,内存使用等),以便父进程在以后某个时候获取。

如果父进程终止,而该进程的子进程处于僵死状态,那么它的所有僵死子进程的父进程ID将被重置为1(init进程),init进程将wait它们,从而去除它们的僵死状态。

僵死进程会占用内核中的空间,可能导致耗尽进程资源。所以我们要显示的捕获SIGCHLD信号,并wait它。

void sig_chld(int signo) {
	pid_t pid;
	int stat;

	pid = wait(&stat);// 改进-> while( (pid = waitpid(-1, &stat, WNOHANG)) > 0) // 其中WNOHANG告诉内核在没有已终止子进程时不要阻塞
	return;
}
当我们使用这个函数去处理捕获这个信号时,父进程正好阻塞在accept这个慢系统调用上,此时内核就会使accept返回一个EINTR错误(被中断的系统调用)。如果父进程不处理该错误,程序就会中止。

适用于慢系统调用的基本规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。

对于accept,read,write,select,open之类的函数来说,遇到EINTR错误,我们便重启这个系统调用。但是有一个函数:connect,如果它返回EINTR,我们不能再次调用它。当connect被一个捕获的信号中断并且不自动重启时,我们必须调用select来等待连接完成。

38. 如果服务器子进程被中止,那么它会关闭它的套接字,向客户端发送一个FIN,客户端接收到这个FIN会马上回复一个ACK。如果此时客户端第一次继续往这个接收了FIN的套接字里写操作,那么服务器会返回一个RST,如果第二次写操作就会引发SIGPIPE信号,并且write操作返回一个EPIPE错误。

39. Unix系统关机时,init进程通常先给所有进程发送SIGTERM信号(该信号可以被捕获),等待一段固定的时间(往往在5到20秒之间),然后给所有仍在运行的进程发送SIGKILL信号(该信号不可以被捕获)。这么做事留给所有运行的进程一小段时间来清除和终止。

40. select需要2个系统调用,处理单个套接字时可能稍有劣势。select优势在于处理多个描述符。

41. 调用recvfrom如果套接字时非阻塞的,而且此时没有数据的话,就会返回EWOULDBLOCK错误。

42. 信号驱动式I/O模型。

建立SIGIO的信号处理程序,套接字有数据,系统发送SIGIO信号

43. select函数

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
如果三个描述符集都为NULL,那么select就是一个比sleep更精准的定时器。

描述符操作宏:

void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
maxfdpl是我们计算出所关心的最大描述符并+1。在头文件<sys/select.h>中定义的FD_SETSIZE常值是数据类型fd_set中的描述符总数。

44. 描述符就绪条件

(1)满足下列4个条件任何一个时,一个套接字准备好“”。

(a)接收缓冲区数据字节数大于等于此缓冲区低水位标记的大小。可以通过SO_RCVLOWAT来设置。TCP和UDP套接字默认值为1。

(b)该连接的读半部关闭(就是接收了FIN的tcp连接)。读操作返回0(EOF)

(c)套接字是一个监听套接字,且已完成的连接数不为0

(d)套接字上右一个错误待处理

(2)满足下列4个条件任何一个时,一个套接字准备好“”。

(a)当该套接字的已用空间字节数大于等于套接字发送缓冲区低水位标记的大小。使用SO_SNDLOWAT来设置。TCP和UDP套接字默认值为2048。

(b)该连接的写半部关闭。对这个套接字进行写操作将产生SIGPIPE信号

(c)使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。

(d)有错误发生

注:当某个套接字上发生错误时,它将由select标记为既可读又可写。

45. shutdown函数:

#include <sys/socket.h>
int shutdown(int sockfd, int howto);
两种情况可以使用shutdown函数:

(1)close把描述符的引用计数减1,在变0时才关闭套接字。shutdown不管引用计数,可直接触发正常连接终止序列

(2)close终止读和写两个方向的数据传送。

其中howto参数决定shutdown的行为:

SHUT_RD 关闭连接的读这一半,套接字上的数据都被丢掉。进程不能再对它进行读函数。

SHUT_WR 关闭连接的写这一半,称为“半关闭(half-close)”,不能再对它进行写函数。

SHUT_RDWR 先调SHUT_RD,再调SHUT_WR

46. getsockopt和setsockopt函数

#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
47. 通用套接字选项:

(1)SO_KEEPALIVE:给TCP设置此选项后,如果2小时内在该套接字的任一方都没有数据交换,TCP就自动给对端发送一个保持存活探测分节。可能出现以下三种情况:

(a)对端以期望的ACK响应

(b)对端以RST响应,告知本端TCP:对端已崩溃且已重新启动,本端套接字返回错误ECONNRESET,套接字被关闭

(c)对端没有任何响应。(Berkeley的TCP将发送另外8个探测分节,如果没有得到响应,则放弃)该套接字返回ETIMEOUT错误,套接字被关闭。

通常我们想把检测时间改短,但是改短之后受影响的将是整个主机所有开启了本选项的套接字。

(2)SO_LINGER:

本选项指定close函数如何操作(TCP, UDP),默认操作是close立即返回,如果有数据残留在套接字缓冲区中,系统将试着把这些数据发送给对端。

shutdown, SHUT_RD:在套接字上不能发出接收请求(read),进程仍可以写(write),接收缓冲区数据被丢弃,再接收任何数据由TCP丢弃。

shutdown, SHUT_WR:在套接字上不能发出发送请求(write),进程仍可以读(read),发送缓冲区内容被发送到对端,后跟正常的TCP连接终止序列。

close, l_onoff = 0:不能再读和写,缓冲区内容被发送到对端。如描述符引用计数为0,发完缓冲区数据后,正常TCP终止。

close, l_onoff = 1, l_linger = 0:不能再读和写,如描述符引用计数为0:RST被发送到对端,连接状态被置为CLOSED(无TIME_WAIT状态),套接字接收发送缓冲区数据被丢弃

close, l_onoff = 1, l_linger != 0:不能再读和写,发送缓冲区数据被发送到对端,如果描述符引用计数为0:在发送完缓冲区数据后,以正常TCP终止(FIN),接收缓冲区数据被丢弃,如果在连接变为CLOSED状态之前,延滞时间到,那么close返回EWOULDBLOCK错误。

(3)SO_RCVBUF和SO_SNDBUF套接字选项:

设置缓冲区大小时,函数调用顺序很重要。因为TCP的窗口规模选项是在建立连接时用SYN分节与对端互换得到。所以对于客户端是在调用connect函数前,对于服务器是在listen函数前,因为accept直到TCP的三路握手完成才会创建并返回已连接套接字,新创建的已连接套接字的缓冲区大小是从监听套接字继承而来

TCP套接字缓冲区大小至少应该是相应连接MSS值的四倍(快速恢复使用)。

宽道的容量=宽带(bit/s) * RTT(秒)

如果套接字缓冲区大小小于该值,管道性能也会低于期望。

(4)SO_RCVLOWAT和SO_SNDLOWAT套接字选项:

每个套接字都有一个接收低水位标记和一个发送低水位标记。它们是由select函数使用。

接收低水位标记,对于TCP,UDP,和SCTP默认值为1

发送低水位标记,是让select返回“可写”时套接字发送缓冲区所需的可用空间。对于TCP其默认值通常为2048。UDP的发送缓冲区没啥用,不是由这个东西触发,UDP没有发送缓冲区,而只有发送缓冲区大小这个属性。

(5)SO_RCVTIMEO和SO_SNDTIMEO套接字选项:

这两个选项允许我们给套接字的接收和发送设置一个超时值。通过把timeval的值设置为0s和0us来禁止超时。默认情况下超时都是禁止的。

(6)SO_REUSEADDR和SO_REUSEPORT套接字选项

通常问题是这样碰到的:

a) 启动一个监听服务器;

b) 连接请求到达,派生一个子进程处理

c) 监听服务器终止,但子进程继续为现有连接上的客户提供服务

d) 重启监听服务器

此时,bind会失败,所以应该在socket和bind两个调用之间设置SO_REUSEADDR套接字选项。

SO_REUSEADDR允许同一端口上启动同一个服务器的多个实例,只要每个实例绑定到不同的IP地址。

对于TCP绝对不允许启动绑定相同IP地址和相同端口号的多个服务器:这是完全重复的绑定

对于UDP如果传输协议支持,同样的IP地址和端口还可以绑定到另一个套接字上。

为了防止TIME_WAIT导致重启服务器失败,通常设置SO_REUSEADDR

总结:在所有TCP服务器程序中,在调用bind之前设置SO_REUSEADDR套接字选项。

潜在问题,假设存在一个绑定了通配地址和端口5555的套接字,如果指定SO_REUSEADDR,我们就可以把相同的端口绑定到不同IP地址上,此后目的地为端口5555及新绑定IP地址的数据报将被递送到新的套接字,而不是递送到绑定了通配地址的已有连接。这些数据报可以是TCP的SYN分节,SCTP的INIT块或UDP数据报。

(6)SO_TYPE套接字选项:

返回套接字的类型,诸如:SOCK_STREAM或SOCK_DGRAM之类的值

48. TCP套接字选项:

(1)TCP_MAXSEG套接字选项:

允许我们获取或设置TCP连接的最大分节大小(MSS)。返回值时我们的TCP可以发送给对端的最大数据量,它通常是由使用SYN分节通告MSS。

(2)TCP_NODELAY套接字选项:

开启本选项将禁止TCP的Nagle算法。Nagle算法的目的在于减少广域网(WAN)上小分组的数目。

Nagle算法:如果还有一个分节的数据没确认,就等着它确认后才发送下一分节

ACK延滞算法:该算法使TCP在接收到数据后不立即发送ACK,而是等一小段时间(50~200)然后才发送ACK

开启TCP_NODELAY选项可以减少延迟。

49. fcntl函数

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /*int arg*/);
设置套接字为非阻塞式I/O型:F_SETFL,O_NONBLOCK

设置套接字为信号驱动式I/O型:F_SETFL,O_ASYNC

设置套接字属主:F_SETOWN

// 设置
int flags;
if ( (flags = fcntl(fd, F_GETFL, 0)) < 0) 
	error("F_GETFL error");
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0)
	error("F_SETFL error");

// 取消
flags &= ~O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0)
	error("F_SETFL error");
F_SETOWN命令允许我们指定用于接收SIGIO和SIGURG信号的套接字属主(进程ID或进程组ID),其中SIGIO信号试套接字被设置为信号驱动式I/O后产生的,SIGURG信号是在新的带外数据到达套接字时产生的。
50. UDP套接字编程

TCP的应用程序与UDP的应用程序之间存在一些本质差异:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。

写一个长度为0的数据报是可行的。在UDP情况下,这会形成一个只包含一个IP首部(对于IPv4通常是20个字节,对于IPv60通常为40字节)和一个8字节UDP首部而没有数据的IP数据报。就是说recvfrom返回0是可接受的。

51. 一个基本规则是:对于一个UDP套接字,由它引发的异步错误却并不返回给它,除非它已连接。??啥意思

52. 当发生一个ICMP端口不可达错误后,recvfrom可以返回的信息仅有errno值,它没有办法返回出错数据报的目的IP地址和目的UDP端口号。因此做出决定:仅在进程已将其UDP套接字连接到恰恰一个对端后,这些异步错误才返回给进程。

我们可以通过给UDP套接字调用connect,但这样做的结果却与TCP连接大相径庭:没有三路握手过程,内核只是检查是否存在立即可知的错误(ICMP的不可达错误),记录对端的IP地址和端口号(取自传给connect的套接字地址结构),然后立即返回到调用进程。

使用已连接(connected UDP socket)后,发生的变化:

(1)不需再使用sendto,而使用write和send

(2)不需再使用recvfrom,而使用read和recv或recvmsg

(3)由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接的进程不会收到任何异步错误。

53. TCP套接字的connect只能调用一次,UDP的可以多次调用,可能是指定新的IP地址和端口号,也可能是断开套接字

对于断开套接字,把套接字地址结构的地址族成员sin_family或sin6_family设置为AF_UNSPEC。这么做可能会返回EAFNOSUPPORT错误,不过没关系。

54. 在一个未连接的UDP套接字上调用sendto时,内核的步骤:

(1)连接套接字

(2)输出第一个数据报

(3)断开套接字

(4)连接套接字

(5)输出第二个数据报

(6)断开套接字

给一个已连接UDP套接字发送2次信息:

(1)连接套接字

(2)输出第一个数据报

(3)输出第二个数据报

使用了已连接的UDP,可以省两次系统调用。

55. 增大UDP套接字接收缓冲区,可以使丢失情况缓解,但是不能从根本上解决问题。

56. 一个进程同时绑定TCP和UDP时,不需使用SO_REUSEADDR选项,因为TCP端口是独立于UDP端口的。

57. 名字与地址转换

gethostbyname和gethostbyaddr在主机名字与IPv4地址之间进行转换

getservbyname和getservbyport在服务名字和端口号之间进行转换

getaddrinfo和getnameinfo用于主机名字和IP地址之间以及服务名字和端口号之间的转换

58. 文件/etc/resolv.conf通常包含本地名字服务器主机的IP地址

解析器使用UDP向本地名字服务器发出查询。如果本地名字服务器不知道答案,它通常就会使用UDP在整个因特网上查询其他名字服务器。如果答案太长,超出了UDP消息的承载能力,本地名字服务器和解析器会自动切换到TCP。

59. 不使用DNS也可能获取名字和地址信息。常用的替代方法有静态主机文件(通常是/etc/hosts文件),网络信息系统以及轻权目录访问协议。

60. gethostbyname函数:查找主机名,返回一个指向hostent结构的指针,该结构中含有所查找主机的所有IPv4地址。getaddrinfo函数能同时处理IPv4和IPv6地址。

#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
// 返回:若成功则为非空指针,若出错则为NULL且设置h_errno

struct hostent {
	char *h_name;			/*official name of host*/
	char **h_aliases;		/*pointer to array of pointers to alias names*/
	int  h_addrtype;		/*host address type:AF_INET*/
	int  h_length;			/*length of address:4*/
	char **h_addr_list;		/*ptr to array of ptrs with IPv4 addrs*/
};
61. gethostbyaddr函数试图由一个二进制的IP地址找到相应的主机名。
#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);
// 注意:addr参数实际上不是char *类型,而是一个指向存放IPv4地址的某个in_addr结构的指针,len参数是这个结构的大小:对于IPv4地址为4。family参数为AF_INET
62. getservbyname和getservbyport函数:获取服务

63. getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个sockaddr结构。

<pre name="code" class="cpp">#include <netdb.h>
int getaddrinfo(const char *hostname, const char *service,
				const struct addrinfo *hints, struct addrinfo **result);

struct addrinfo {
	int 		ai_flags;		/*AI_PASSIVE, AI_CANONNAME*/
	int 		ai_family;		/*AF_xxx*/
	int 		ai_socktype;	/*SOCK_xxx*/
	int 		ai_protocol;	/*0 or IPPROTO_xxx for IPv4 and IPv6*/
	socklen_t	ai_addrlen;		/*length of ai_addr*/
	char		*ai_canonname;	/*ptr to canonical name for host*/
	struct sockaddr *ai_addr;	/*ptr to socket address structure*/
	struct addrinfo	*ai_next;	/*ptr to next structure in linked list*/
};


getaddrinfo解决了把主机名和服务名转换成套接字地址结构的问题。

getnameinfo解决了把套接字地址结构转换成主机名和服务名。64. freeaddrinfo函数:getaddrinfo返回的所有存储空间都是动态获取的,如addrinfo结构,ai_addr结构和ai_canonname字符串。这些存储空间通过调用freeaddrinfo返回给系统。

#include <netdb.h>
void freeaddrinfo(struct addrinfo *ai);
ai参数指向由getaddrinfo返回的第一个addrinfo结构

65. 不可重入函数

gethostbyname和gethostbyaddr,getservbyname和getservbyport是不可重入函数,是非线程安全的,因为它会返回一个指向同一个static的结构体指针。

inet_pton和inet_ntop总是可重入的。

getaddrinfo可重入的前提是由它调用的函数都可重入,就是说它应该调用可重入版本的gethostbyname和getservbyname。同理getnameinfo

gethostbyname_r和gethostbyaddr_r是可重入版本的gethostbyname和gethostbyaddr。

66. IPv4客户与IPv6服务器

(1)如果收到一个目的地为某个IPv4套接字的IPv4数据报,那么无需任何特殊处理。

(2)如果收到一个目的地为某个IPv6套接字的IPv6数据报,那么无需任何特殊处理。

(3)如果收到一个目的地为某个IPv6套接字的IPv4数据报,那么内核把与该数据报的源IPv4地址对饮的IPv4映射的IPv6地址作为由accept(TCP)或recvfrom(UDP)返回的对端IPv6地址。因为任何一个IPv4地址总能表示成一个IPv6地址。客户和服务器之间交换的是IPv4数据报。

(4)上一点相反面却不成立:一般来说,一个IPv6地址无法表示成一个IPv4地址

大多数双栈主机在处理监听套接字时应该使用以下规则:

(1)IPv4监听套接字只能接受来自IPv4客户的外来连接

(2)如果服务器有一个绑定了通配地址的IPv6监听套接字,而且该套接字未设置IPV6_V6ONLY套接字选项,那么该套接字既能接受来自IPv4客户的外来连接,又能接受来自IPv6客户的外来连接

(3)如果服务器有一个IPv6监听套接字,而且绑定在其上的是除IPv4映射的IPv6地址之外的某个非通配IPv6地址,或者绑定在其上的是通配地址,不过还设置了IPV6_V6ONLY套接字选项,那么该套接字只能接受来自IPv6客户的外来连接。

67. 守护进程:在后台运行且不与任何控制终端关联的进程。

守护进程启动的方法有很多:

(1)在系统启动时,位于/etc目录或以/etc/rc开头的某个目录中

(2)有inetd超级服务器启动,inetd监听网络请求(Telnet, FTP等)

(3)cron守护进程

(4)at命令指定某个时刻执行。时间到时通常由cron守护进程启动

(5)从终端启动

68. 套接字超时:

(1)调用alarm,指定超时时期满时产生SIGALARM信号。信号处理在不同实现上存在差异,而且可能干扰进程中现有的alarm调用

(2)在select中阻塞等待I/O,以此代替直接阻塞在read或write调用上

(3)使用SO_RCVTIMEO和SO_SNDTIMEO套接字选项。并非所有实现都支持这两个套接字选项

TCP内置的connect超时相当长(典型为75秒)。

69. 使用SIGALRM为connect设置超时

尽管书中的例子相当简单,但在多线程化程序中正确使用信号却非常困难,因此只建议在未线程化或单线程化的程序中使用此技术。

70. 使用select

#include "unp.h"

int 
readable_timeo(int fd, int sec) {
	fd_set 	rset;
	struct timeval tv;

	FD_ZERO(&rset);
	FD_SET(fd, &rset);

	tv.tv_sec = sec;
	tv.tv_usec = 0;

	return (select(fd+1, &rset, NULL, NULL, &tv));
}
本函数不执行读操作,它只是等待给定描述符变为可读。本函数适用于任何类型的套接字,既可以是TCP也可以是UDP。

71. 使用SO_RCVTIMEO套接字和SO_SNDTIMEO选项两者都不能用于为connect设置超时。并非所有实现都支持它们。

72. recv和send函数:

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
flags参数值:

MSG_DONTROUTE		绕过路由表查找
MSG_DONTWAIT		仅本操作非阻塞
MSG_OOB			发送或接收带外数据
MSG_PEEK		窥看外来消息(在recv或recvfrom后不丢弃数据)
MSG_WAITALL		直到读到指定数量数据才返回,省掉readn函数
			但是发生(a)捕获一个信号(b)连接被终止(c)套接字发生错误,导致返回比所请求数据少
73. readv和writev函数,这两个函数类似read和write,不过允许单个系统调用读入或写出自一个或多个缓冲区。这些操作叫做分散读集中写
#include <sys/uio.h>

ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

struct iovec {
	void *iov_base;
	size_t iov_len;
};
iovec结构数组中元素的数目存在某个限制,具体取于实现(4.3BSD和linux最多允许1024个,而HP-UX最多允许2100个),POSIX要求在<sys/uio.h>中定义IOV_MAX常值,而且其值至少为16。
readv和writev这两个函数可用于任何描述符,而不仅限于套接字。另外writev是一个原子操作,意味着对于一个基于记录的协议(例如UDP)而言,一次writev调用只产生一个UDP数据报。

在之前讲TCP_NODELAY套接字选项提到过writev的一个用途。当时我们说一个4字节的write跟一个396字节的write可能触发Nagle算法,首选办法之一是针对这两个缓冲区调用writev。

74. recvmsg和sendmsg函数:是最通用的I/O函数,可以把所有read,readv,recv和recvfrom替换成recvmsg。类似各种输出函数调用也可以替换成sendmsg调用。

75. 排队的数据量

(1)如果我们既想查看数据,又想数据仍然留在接收队列中,那么可以使用上边提到的recv的flags的MSG_PEEK标志。

(2)一些实现支持ioctl的FIONREAD命令。该命令的第三个参数是指向某个整数的一个指针。注意源自Berkeley的实现中,为UDP套接字返回的值还包括一个套接字地址结构空间,其中包含发送者的IP地址和端口号。(IPv4为16个字节,对于IPv6为24个字节)

76. 套接字和标准I/O

执行I/O的另一个方法是使用标准I/O函数库。标准I/O函数库处理我们直接使用Unix I/O函数时必须考虑的一些细节,譬如自动缓冲输入流和输出流。

TCP和UDP套接字是全双工的。标准I/O也可以是全双工的:只要以r+类型打开流即可,r+意味着读写。然后在这样的流上,我们必须在调用一个输出函数之后插入一个fflush,fseek,fsetpos或rewind调用才能接着调用一个输入函数。类似的调用一个输入函数后也必须插入一个fseek,fsetpos或rewind调用才能调用一个输出函数,除非输入函数遇到一个EOF。fseek,fsetpos和rewind这3个函数的问题是它们都调用lseek,而lseek用在套接字上只会失败,解决上述读写问题的最简单办法是为一个给定的套接字打开两个标准I/O流,一个用于读,一个用于写

77. 标准I/O函数库执行以下三类缓冲。

(1)完全缓冲:缓冲区满,进程显示调fflush,或进程调用exit终止自身。标准I/O缓冲区的通常大小为8192字节。

(2)行缓冲:碰到一个换行符

(3)不缓冲

标准I/O函数库的大多数Unix实现使用规则:

标准错误输出总是不缓冲

标准输入和标准输出完全缓冲,除非它们指代终端设备(这种情况下使用行缓冲)

所有其他I/O流都是完全缓冲,除非它们指代终端设备(这种情况下使用行缓冲)

然而,最好解决套接字输出问题的办法是彻底避免在套接字上使用标准I/O函数库

78. 轮询技术

(1)/dev/poll接口:solaris上名为/dev/poll的特殊文件,提供了一个可扩展的轮询大量描述符的方法

int wfd = open("/dev/poll", O_RDWR, 0);

struct pollfd pollfd[2];
pollfd[0].fd = n;
pollfd[0].events = POLLIN;
pollfd[0].revents = 0;

pollfd[1].fd = n+1;
pollfd[1].events = POLLIN;
pollfd[1].revents = 0;

write(wfd, pollfd, sizeof(struct pollfd) * 2);

for (;;) {
	struct dvpoll dopoll;
	dopoll.dp_timeout = -1;
	dopoll.dp_nfds = 2;
	dopoll.dp_fds = pollfd; //指向一个数组
	result = ioctl(wfd, DP_POLL, &dopoll);
	for (i = 0; i < result; i++) {
		....
	}
}
有点类似epoll的用法

(2)kqueue接口:FreeBSD引入了kqueue接口。本接口允许进程向内核注册描述所关注kqueue事件的事件过滤器(event filter)。事件除了与select所关注类似的文件I/O和超时外,还有异步I/O,文件修改通知(文件被删除或修改时发出的通知),进程跟踪(例如进程调用exit或fork时发出的通知)和信号处理。

#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>

int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges,
	struct kevent *eventlist, int nevents,
	const struct timespec *timeout);
void EV_SET(struct kevent *kev, uintptr_t ident, short filter,
	u_short flags, u_int fflags, intptr_t data, void *udata);

struct kevent {
	uintptr_t 	ident;		/*identifier (e.g., file descripter)*/
	short 		filter;		/*filter type (e.g, EVFILT_READ)*/
	u_short 	flags;		/*action flags (e.g, EV_ADD)*/
	u_int		fflags;		/*filter-specific flags*/
	intptr_t	data;		/*filter-specific data*/
	void 		*udata;		/*opaque user data*/
};
kqueue函数返回的新的kqueue描述符,用于后续的kevent调用。kevent函数即可用于注册所关注的事件,也用于确定是否有所关注事件发生。

struct kevent kev[2];

EV_SET(&kev[0], fileno(fp), EVFILT_READ, EV_ADD, 0, 0, NULL);
EV_SET(&kev[1], sockfd, EVFILT_READ, EV_ADD, 0, 0, NULL);

kq = kqueue();
ts.tv_sec = ts.tv_nsec = 0; // 不阻塞,直接返回
kevent(kq, kev, 2, NULL, 0, &ts); // 注册感兴趣的事件

for(;;) {
	nev = kevent(kq, NULL, 0, kev, 2, NULL);

	for (i = 0; i < nev; i++) {
		....
	}
}
79. T/TCP:事务目的TCP
T/TCP能够把SYN,FIN和数据组合到单个分节中,前提是数据的大小小于MSS

T/TCP优势在于TCP的所有可靠性(序列号,超时,重传,等等)得以保留,而不像UDP那样把可靠性推给应用程序去实现。T/TCP同样维持TCP的慢启动和拥塞避免措施,UDP却往往缺乏这些特性。

80. Unix域协议并不是一个实际的协议族,它可作为IPC(进程间通信)的一种。使用Unix域套接字有以下3个理由:

(1)比TCP套接字快一倍

(2)可用于在同一个主机上的不同进程之间传递描述符

(3)把客户的用户ID和组ID提供给服务器,从而能提供额外的安全检查措施。

Unix域中用于标识客户和服务器的协议地址是普通文件系统中的路径名。

#include <sys/un.h>
struct sockaddr_un {
	sa_family_t sun_family;
	char 		sun_path[104];
};
81. socketpair函数:创建两个随后连接起来的套接字。仅适用于Unix套接字。
#include <sys/socket.h>
int socketpair(int family, int type, int protocol, int sockfd[2]);
family参数必须为AF_LOCAL, protocol参数必须为0。type参数既可以是SOCK_STREAM也可以是SOCK_DGRAM。新创建的描述符作为sockfd[0],sockfd[1]返回。

指定type参数为SOCK_STREAM得到的结果为流管道,与pipe创建的普通Unix管道类似,差别在于流管道是全双工的。POSIX不要求全双工管道,SVR4上pipe返回两个全双工描述符,而Berkeley的内核返回两个半双工的描述符。

注:socketpair又增加了一个可以在同一个进程内部同通信的办法,以前的是pipe,并且socketpair是全双工的

82. Unix域套接字与其他套接字的差异和限制。

(1)由bind创建的路径名默认权限应为0777,并参照当前umask值进行修正

(2)关联路径名应该是一个绝对路径名

(3)在connect调用中指定的路径名必须是一个当前绑定在某个打开的Unix域套接字上的路径名,套接字类型也必须一致

(4)调用connect连接一个Unix域涉及的权限测试等同于调用open以只写方式访问

(5)Unix字节流套接字类似于TCP套接字:无记录边界的字节流接口

(6)监听套接字的队列已满,调用就立即返回一个ECONNREFUSED错误。这点与TCP不同:如果TCP监听套接字的队列已满,TCP监听端就忽略新到达的SYN,而TCP发起端将数次发送SYN进行重试。

(7)Unxi数据报套接字类似于UDP套接字

(8)在一个未绑定的Unix域套接字上发送数据报不会自动给这个套接字绑定一个路径名

83. 描述符传递

从一个进程到另一个进程传递打开的描述符时,通常:

fork调用返回后,子进程共享父进程的所有打开的描述符

exec调用执行后,所有描述符通常保持打开状态不变

Unix提供了一个从一个进程向任何一其他进程传递任一打开的描述符的方法。

84. 对于不能被满足的非阻塞式I/O操作,System V会返回EAGAIN错误,而源自Berkeley的实现则返回EWOULDBLOCK错误。幸运的是,大多数当前的系统把这两个错误码定义成相同的值。

85. 在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三路握手继续进行。

注:如果调用connect时,连接被立即建立,这是connect返回值为0,表示立即建立

86. 处理非阻塞connect时需要注意的一些细节:

(1)尽管套接字时非阻塞的,如果连接到的服务器在同一个主机上,那么我们调用connect时,连接通常立刻建立。

(2)当连接成功建立时,描述符变为可写,当连接建立遇到错误时,描述符变为既可读又可写。

int connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec) {
	int flags = fcntl(sockfd, F_GETFL, 0);
	fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

	error = 0;
	if ( (n = connect(sockfd, saptr, salen)) < 0)
		if (errno != EINPROGRESS)// 对于非阻塞connect,调用会返回EINPROGRESS是正确的
			return (-1);

	if (n == 0) // 表示连接立即建立
		goto done;

	FD_ZERO(&rset);
	FD_SET(sockfd, &rset);
	wset = rset;
	tval.tv_sec = nsec;
	tval.tv_usec = 0;

	if ( (n = select(sockfd+1, &rset, &wset, NULL, nsec?&tval:NULL)) == 0) {
		close(sockfd);// 用户设置的nsec时间到
		errno = ETIMEOUT;
		return (-1);
	}

	if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {// 当连接成功建立时,描述符可写,当连接遇到错误时,描述符既可读又可写,也有可能是连接建立而且对方已发来数据
		len = sizeof(error);
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)// 获取套接字的待处理错误(SO_ERROR)
			return (-1);
	} else 
		err_quit("select error: sockfd not set");

done:
	fcntl(sockfd, F_SETFL, flags);// 恢复套接字状态

	if (error) {
		close(sockfd);
		errno = error;
		return (-1);
	}

	return (0);
}
有一个移植性问题,怎样判断连接建立是否成功?最简单的解决移植性问题的办法是为每个连接创建一条处理线程
对于一个正常的阻塞式套接字,如果其上的connect调用在TCP三次握手完成前被中断(譬如捕获了某个信号),将会发生什么呢?假设被中断的connect调用不由内核自动重启,那么它将会返回EINTR。我们不能再次调用connect等待未完成的连接继续完成。这样做将会导致EADDRINUSE错误。

87. 广播和多播要求用于UDP或原始IP,它们不能用于TCP

广播的用途之一:在本地子网定位一个服务器主机,前提是已知或确定这个服务器主机位于本地子网,但是不知道它的单播IP地址。

另一个用途:在有多个客户主机与单个服务器主机通信的局域网环境中尽量减少分组流通。

88. 除非显示的告诉内核我们准备发送广播数据报,否则系统不允许我们这么做。我们通过设置SO_BROADCAST套接字选项来做到这一点。然后sendto一个多播地址就可以了。

89. 有的系统对广播不支持IP分片,所以为了移植,先通过SIOCGIFMTU ioctl确定外出接口的MTU,从中扣除IP首部和UDP首部的长度得到最大净荷大小。

90. 竞争状态:

信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); //显示通知内核多播,否则会返回错误

signal(SIGALRM, recvfrom_alarm); // 注册信号

while(fgets(sendline, MAXLINE, fp) != NULL) {
	sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

	alarm(1); // 定时1秒
	for(;;) {
		len = servlen;
		n = recvfrom(sockfd, recvline, MAXLINE, 0, prely_addr, &len);
		if (n < 0) {
			if (errno == EINTR) // 被信号中断,跳出循环
				break;
			else 
				err_sys("recvfrom error");
		} else {
			recvline[n] = 0;
			sleep(1);
			printf("from %s:%s", sock_ntop_host(preply_addr, len), recvline);
		}
	}
}
free(preply_addr);
上面的代码,使用alarm是为了设置超时,当时间到时内核递交SIGALARM信号,作者的原意是时间到通过alarm发出的SIGALARM信号中断recvfrom函数,然后跳出。但是当程序阻塞在sleep上的时候,此时sleep被中断,然后recvfrom可能就会被一直阻塞。
解决方法1:(不正确的)
sigset_t sigset_alrm; // 信号集

setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); //显示通知内核多播,否则会返回错误

sigemptyset(&sigset_alrm); // 初始化清空信号集
sigaddset(&sigset_alrm, SIGALRM); // 注册SIGALRM
signal(SIGALRM, recvfrom_alarm); // 注册信号

while(fgets(sendline, MAXLINE, fp) != NULL) {
	sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

	alarm(5); // 定时1秒
	for(;;) {
		len = servlen;
		sigprocmask(SIG_UNBLOCK, &sigset_alrm, NULL); // 解阻塞
		n = recvfrom(sockfd, recvline, MAXLINE, 0, prely_addr, &len);
		sigprocmask(SIG_BLOCK, &sigset_alrm, NULL); // 阻塞:在阻塞期间,被阻塞信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。
		if (n < 0) {
			if (errno == EINTR) // 被信号中断,跳出循环
				break;
			else 
				err_sys("recvfrom error");
		} else {
			recvline[n] = 0;
			sleep(1);
			printf("from %s:%s", sock_ntop_host(preply_addr, len), recvline);
		}
	}
}
free(preply_addr);

此方法可能在大多数情况下运行正常!但是仍然存在一个问题,解阻塞信号,调用recvfrom和阻塞信号都是相互独立的系统调用。如果SIGALRM信号恰在recvfrom返回最后一个应答数据之后与接着阻塞该信号之间递交,那么下次调用recvfrom将永远阻塞。我们虽然已经缩小了出错的窗口,但是问题依然存在。

解决方法2:pselect

sigset_t sigset_alrm; // 信号集
sigset_t sigset_empty; // 信号集

setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); //显示通知内核多播,否则会返回错误

sigemptyset(&sigset_alrm); // 初始化清空信号集
sigemptyset(&sigset_empty); // 初始化清空信息化集

sigaddset(&sigset_alrm, SIGALRM); // 注册SIGALRM

signal(SIGALRM, recvfrom_alarm); // 注册信号

while(fgets(sendline, MAXLINE, fp) != NULL) {
	sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

	sigprocmask(SIG_BLOCK, &sigset_alrm, NULL); // 阻塞:在阻塞期间,被阻塞信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。
	alarm(5); // 定时1秒
	for(;;) {
		FD_SET(sockfd, &rset);
		n = pselect(sockfd + 1, &rset, NULL, NULL, NULL, &sigset_empty); // 在pselect阻塞期间,解阻塞所有的信号,在pselect返回时,恢复之前的状态(阻塞SIG_ALRM信号)
		if (n < 0) {
			if (errno == EINTR)	// 被中断跳出循环
				break;		
		}

		len = servlen;
		sigprocmask(SIG_UNBLOCK, &sigset_alrm, NULL); // 解阻塞
		n = recvfrom(sockfd, recvline, MAXLINE, 0, prely_addr, &len);
		
		recvline[n] = 0;
		sleep(1);
		printf("from %s:%s", sock_ntop_host(preply_addr, len), recvline);
	}
}
free(preply_addr);
这里的关键点是:设置信号掩码,测试描述符以及恢复信号掩码这3个操作在调用进程看来是原子操作。

解决方法3:使用sigsetjmp和siglongjmp

static sigjmp_buf jmpbuf;

setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); //显示通知内核多播,否则会返回错误

sigemptyset(&sigset_alrm); // 初始化清空信号集

signal(SIGALRM, recvfrom_alarm); // 注册信号

while(fgets(sendline, MAXLINE, fp) != NULL) {
	sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

	alarm(5); // 定时5秒
	for(;;) {
		if (sigsetjmp(jmpbuf, 1) != 0) // 信号处理函数会返回到这里
			break;

		len = servlen;
		n = recvfrom(sockfd, recvline, MAXLINE, 0, prely_addr, &len);
		recvline[n] = 0;
		printf("from %s:%s", sock_ntop_host(preply_addr, len), recvline);
	}
}
free(preply_addr);

static void recvfrom_alarm(int signo) {
	siglongjmp(jmpbuf, 1);	// 触发信号时,执行跳转
}
不过这还有一个潜在的问题,就是不能保证alarm与sigsetjmp之间小于5秒。

解决方法4:使用IPC

使用pipe生成一对描述符,用select监控sockfd和pipefd[0],如果发送SIGALARM信号,就在信号处理函数里向pipefd[1]写入一个字符。

static int pipefd[2];

setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); //显示通知内核多播,否则会返回错误

signal(SIGALRM, recvfrom_alarm); // 注册信号

pipe(pipefd); // 创建管道
maxfdp1 = max(sockfd, pipefd[0]) + 1;

FD_ZERO(&rset);

while(fgets(sendline, MAXLINE, fp) != NULL) {
	sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

	alarm(5); // 定时1秒
	for(;;) {
		FD_SET(sockfd, &rset);
		FD_SET(pipefd[0], &rset);
		if ( (n = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {
			if (errno == EINTR)
				continue;
		}

		if (FD_ISSET(sockfd, &rset)) {
			len = servlen;
			n = recvfrom(sockfd, recvline, MAXLINE, 0, prely_addr, &len);
			recvline[n] = 0;
			printf("from %s:%s", sock_ntop_host(preply_addr, len), recvline);
		}
		if (FD_ISSET(pipefd[0], &rset)) {
			read(pipefd[0], &n, 1); // 时间到
			break;
		}

	}
}
free(preply_addr);

static void recvfrom_alarm(int signo) {
	write(pipefd[1], "", 1); //写入
}
91. 广播只能用于局域网,多播既可用于局域网,也可用于广域网。IPv4的D类地址(从224.0.0.0到239.255.255.255)是IPv4多播地址,D类地址的低序28位构成多播组ID,整个32位地址则称组地址

92. 何时用UDP代替TCP

(1)UDP支持广播和多播。就必须使用UDP。

(2)UDP没有连接建立和拆除。UDP只需要2个分组就能交换一个请求和一个应答。TCP却需要大约20个分组(假设为每次请求-应答交换建立一个新的TCP连接)。就是在请求应答次数比较少的时候 可以考虑使用UDP。譬如DNS

还有一些UDP无法提供的TCP特性:

(1)正面确认,丢失分组重传,重复分组检测,给被网络打乱次序的分组排序。TCP确认所有数据,以便检测出丢失的分组。这些特性要求每个TCP数据分节都包含一个能被对端确认的序列号。这些特性还要求TCP为每个连接估算重传超时值,该值应随着两个端系统之间分组流通的变化持续更新。

(2)窗口式流量控制。接收端TCP告知发送端自己已为接收数据分配了多大的缓冲区空间,发送端不能发送超过这个大小的数据。也就是说,发送端的未确认数据量不能超过接收端告知的窗口。

(3)慢启动和拥塞避免。这是由发送端实施的一种流量控制形式,它通过检测当前的网络容量来应对阵发的拥塞。当前所有的TCP必须支持这两个特性。注:那些面临拥塞而不“后退”的协议只会导致拥塞变得更糟糕。

作为总结,建议如下:

(1)对于广播或多播应用程序必须使用UDP。

(2)对于简单的请求应答应用程序可以使用UDP,不过错误检测功能必须加到应用程序内部。错误检测至少涉及确认,超时和重传。流量控制对于合理大小的请求和应答往往不成问题。

(3)对于海量数据传输(例如文件传输)不应该使用UDP。

93. 线程:

线程的创建可能比进程的创建快10~100倍。

同一进程内的所有线程除了共享全局变量外还共享:
进程指令,大多数数据,打开的文件(即文件描述符),信号处理函数和信号处置,当前工作目录,用户ID和组ID

不过每个线程有各自的:
线程ID,寄存器集合(包括程序计数器和栈指针),栈(用于存放局部变量和返回地址),errno,信号掩码,优先级

(1)线程初始化

#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
		void *(*func)(void*), void *arg);
//注:每个线程都有许多属性(attribute):优先级,初始栈大小,是否应该成为一个守护线程,等,通常情况下我们采纳默认设置,把attr参数指定为NULL
(2)等待线程结束
#include <pthread.h>
int pthread_join(pthread_t *tid, void **status);
// 通过调用pthread_join等待一个给定的线程终止。如果status指针非空,来自所等待线程的返回值(一个指向某个对象的指针)将存入由status指向的位置。
(3)获取自身ID
#include <pthread.h>
pthread_t pthread_self();
(4)脱离线程
#include <pthread.h>
int pthread_detach();
// 注:与pthread_join相反,当一个joinable线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。而detached线程却像守护进程,当它们终止时,所有相关资源都被释放,我们不能等待它们终止。如果一个线程需要知道另一个线程什么时候终止,那就最好保持第二个线程的可汇合(joinable)状态.
(5)pthread_exit, pthread_cancel 这两个就不说了,详见这里

(6)特定数据

/////////////////////////////////////////////////////////////
#include <pthread.h>
int pthread_once(pthread_once_t *onceptr, void (*init)(void));
int pthread_key_create(pthread_key_t *keyptr, void (*destructor)(void *value));

void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);

/////////////////////////////////////////////////////////////


pthread_key_t rl_key;
pthread_once_t rl_once = PTHREAD_ONCE_INIT;

void
readline_destructor(void *ptr) {
	free(ptr); // 线程结束时调用
}

void 
readline_once(void) {
	pthread_key_create(&rl_key, readline_destructor);	// 创建线程特定数据,并注册结束处理函数
}

ssize_t
readline(...) {
	...
	pthread_once(&rl_once, readline_once); // 创建线程特定数据,确保只运行一次

	if ( (ptr = pthread_getspecific(rl_key)) == NULL) {
		ptr = malloc(...);
		pthread_setspecific(rl_key, ptr); // 设置特定数据
	}
}
每个系统支持有限数量的线程特定数据元素。POSIX要求这个限制不小于128(每个进程)。
详细的测试例子在这里
94. 互斥锁:
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
如果某个互斥锁变量是静态分配的,我们就必须把它初始化为常量PTHREAD_MUTEX_INITIALIZER。如果我们在共享内存区分配一个互斥锁,那么必须通过调用pthread_mutex_init函数在运行时把它初始化。
95. 条件变量
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
pthread_cond_signal通常唤醒等在相应条件变量上的单个线程。有时候一个线程应该唤醒多个线程,这种情况下它可以调用pthread_cond_broadcast唤醒等在相应条件变量上的所有线程。
#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
pthread_cond_timewait的时间是绝对时间。这里有一个关于pthread_cond_timedwait使用时间导致CPU负载过高的错误,见这里


真是经典中的经典。

UNIX网络编程:卷1-读书笔记