《UNIX网络编程》 卷1 笔记

文章目录

第1章

1.internet一词有多种含义。

  1. 一是网际网(internet),采用TCP/IP协议族通信的任何网络都是网际网,因特网就是一个网际网。
  2. 二是因特网(Internet),它是一个专用名词,特指从ARPANET发展而来的连接全球各个ISP的大型网际网。
  3. 三是作为名词性修饰词,这时应根据情况分别译成“因特网”、“网际网”或“网际”。例如,Internet Protocol译成“网际协议”(注意:“Internet Protocol”是“internet protocol”一词名词专用化的结果);Intermet Society则译成“因特网学会”。

应注意区分因特网和网际网这两个概念:因特网只有一个,为了确保其中任何一个节点(主机或路由器)都能寻址到,其寻址规则和地址分配方案是全球统一的;不属于因特网的网际网却可以为其中的节点任意分配地址,譬如说把因特网中的多播地址(224.0.0.0/4)分配用于单播目的也没有问题,因为地址属性(单播、多播、广播、回馈、私用等)是额外配置到TCP/IP协议族上的,并非TCP/IP协议族的本质特征,尽管实际上TCP/IP的各个实现几乎一律采用因特网的寻址规则。虽然国内权威机构已经为“Internet”一词正过中文名(因特网),许多文献仍然沿用“互联网”这个不确切的名称。互联网的说法是相对内联网(intranet)而言的,后者特指使用因特网私用地址寻址各个节点的网际网,因而只是比较特殊的网际网。


第2章

1.TCP/IP协议概况
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述


2.socket接口
在这里插入图片描述

3.tcp状态转移
在这里插入图片描述分组交换:
在这里插入图片描述
如果该连接的整个目的仅仅是发送一个单报文段的请求和接受一个单报文段的应答,那么使用TCP有8个报文段的开销。如果改用UDP,只需要交换两个分组。但同时也丧失了TCP的可靠性,迫使可靠服务的细节从传输层转移到应用进程。


4.TIME_WAIT状态存在理由:
在这里插入图片描述TIME_WAIT状态持续时间是最长报文寿命(MSL)的两倍。

2.11 缓冲区大小及限制

在这里插入图片描述在这里插入图片描述以虚线框展示UDP套接字发送缓冲区,因为它实际上并不存在。任何UDP套接字都有发送缓冲区大小(我们可以使用so_SNDBUF套接字选项更改它),不过它仅仅是可写到该套接字的UDP数据报的大小上限。如果一个应用进程写一个大于套接字发送缓冲区大小的数据报,内核将返回该进程一个EMSGSIZE错误。既然UDP是不可靠的,它不必保存应用进程数据的一个副本,因此无需一个真正的发送缓冲区。

第3章

3.2 套接字地址结构

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

在这里插入图片描述

3.4 字节序转换

在这里插入图片描述套接字地址结构中的端口和IP必须按照网络字节序进行维护:

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY为0实际上无所谓
servaddr.sin_port        = htons(13);	/* daytime server */

3.6 inet_aton、inet_addr和inet_ntoa函数

在这里插入图片描述在这里插入图片描述

3.7 inet_pton和inet_ntop函数

在这里插入图片描述数值格式为in_addr、in6_addr。

小结:
在这里插入图片描述


第4章

4.2 sock函数

在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述


4.3 connect函数

在这里插入图片描述connect在客户接收到对于自己SYN的ACK才返回。
在这里插入图片描述
如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况。
在这里插入图片描述

若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。

原因:POSIX 2001 在一个信息部分说:
如果connect()失败,则未指定套接字的状态。在尝试重新连接之前,应用程序应关闭文件描述符并创建新套接字。
来源


4.3 bind函数

在这里插入图片描述
bind函数用于将一个协议地址赋给一个套接字,如果connect/listen前未bind绑定临时端口,内核会自动给套接字选择一个临时端口。

bind也可以只设置端口或只设置ip:
在这里插入图片描述在这里插入图片描述绑定通配IP地址是在告知系统:要是系统是多宿主机(有多个网卡多个IP地址),则将接受目的地为任何本地接口的连接。

远程过程调用(Remote Procedure Call,RPC)服务器:

​ 通常由内核为它们的监听套接字选择一个临时端口,而该端口随后通过RPC端口映射器进行注册。客户在connect这些服务器之前,必须与端口映射器联系以获取它们的临时端口。这种情况也适用于使用UDP的RPC服务器。


4.5 listen函数

在这里插入图片描述

listen函数将socket创建的默认主动套接字转换为被动套接字,状态从CLOSED转换为LISTEN状态;

同时第二个参数backlog规定了已经建立连接(established)并等待被accept的sockets的队列的最大长度。
TCP监听套接字将在有一个新连接准备好可被接受时变为可读。

当一个客户SYN到达时若队列已满,TCP则忽略该分组,客户TCP等待一段时间后重发SYN以等待空闲位置;

只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接。三次握手完成后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为已连接套接字的接受缓存区大小。
在这里插入图片描述accept函数之前tcp连接已经创建,三次握手已经完成。

SYN泛洪攻击:

1996年,黑客编写了一个以高速率给受害主机发送SYN的程序,用以装填一个或多个TCP端口的未完成连接队列。而且,该程序将每个SYN的源IP地址都置成随机数(称为IP欺骗(IPspoofing)),这样服务器的SYN/ACK就发往不知道什么地方,同时防止受攻击服务器获悉黑客的真实IP地址。这样,通过以伪造的SYN装填未完成连接队列,使合法的SYN排不上队,导致针对合法客户的服务被拒绝(denial ofservice)。


4.6 accept函数

accept函数由TCP服务器调用,用于从已完成连接队列队头返回已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。在这里插入图片描述在这里插入图片描述

在这里插入图片描述

4.7 exec函数

在这里插入图片描述

4.8 并发服务器

Q:并发服务器,父进程accept后创建子进程处理新连接,父进程直接关闭该连接,为什么父进程关闭连接没有终止它与客户的连接?
A:因为每个文件过套接字都有引用计数,只有在计数为0时才真正清理和释放资源,发送FIN终止连接。
如果想直接发送FIN,那么可以改用shutdown函数。


4.9 close函数

close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数。
然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
SO_LINGER套接字选项可以用来改变TCP套接字的这种默认行为。

引用计数:
在这里插入图片描述

4.10 getsockname和getpeername函数

这两个函数或者返回与某个套接字关联的本地协议地址(getsockname),或者返回与某个套接字关联的外地(对端)协议地址(getpeername)。

在这里插入图片描述注:POSIX规范允许getsockname对未绑定的套接字调用getsockname以获取任何套接字地址结构的地址族。

应用:

  • 获取某套接字地质组;

  • 返回由内核赋予的本地IP地址或端口;

  • 当服务器由调用过accept的某个进程调用exec执行程序时,通过getpeername以获取客户身份。服务器在exec后获取套接字描述符的方法:

    • 调用exec的进程将这个描述符格式化为字符串,作为命令行参数传递给新程序。
    • 约定在调用exec之前,总是把某个特定描述符置为已连接套接字描述符,比如0、1、2。

第5章 TCP客户/服务器程序示例

5.6正常启动

显示详细的网络状况:

netstat -anp

-a或–all 显示所有连线中的Socket。
-n或–numeric 直接使用IP地址,而不通过域名服务器。
-p或–programs 显示正在使用Socket的程序识别码和程序名称.

5.8 POSIX信号处理

1.如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解阻塞之后通常只递交一次,也就是说Unix信号默认是不排队的。

2.signal函数不同系统不同实现,最好用sigaction(符合POSIX标准)自定义自己的signal函数。

5.9 处理SIGCHLD信号

设置僵死(zombie)状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。
这些信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间、内存使用量等等)。用ps命令查看资源利用信息。

信号处理函数中使用标准I/O函数是不合适的,因为i/o函数使用了全局变量比如stdout,不可重入(非异步信号安全)。


处理被中断的系统调用

不自动重启的系统调用被信号处理函数中断时,返回EINTR,我们必须对其有所准备。
在这里插入图片描述这段代码所做的事情就是自己重启被中断的系统调用。对于accept以及诸如read、write、select和open之类函数来说,这是合适的。
不过有一个函数我们不能重启:connect。如果该函数返回EINTR,我们就不能再次调用它,否则将立即返回一个错误。当connect被一个捕获的信号中断而且不自动重启时,我们必须调用select来等待连接完成。connect被中断与非阻塞connect属于同一种情况,需要用select来检测该TCP是否可写或可读可写(16.3节)。

源自Berkeley的实现(和POSIX)有关于select和非阻塞connect的以下两个规则:(1)当连接成功建立时,描述符变为可写;(2)当连接建立遇到错误时,描述符变为既可读又可写

5.10 wait和waitpid函数

在这里插入图片描述在这里插入图片描述因为unix信号不排队,所以多个SIGCHLD信号同时到达时,只执行一次wait会漏掉其他SIGCHLD产生僵尸进程,解决办法:

使用非阻塞waitpid来构成SIGCHLD的信号处理函数:

#include	"unp.h"

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

	while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
		printf("child %d terminated\n", pid);
	return;
}

我们必须指定WNOHANG选项,它告知waitpid在有尚未终止的子进程在运行时不要阻塞。无法使用wait代替waitpid,因为没有办法防止wait在正运行的子进程尚有未终止时阻塞。

在这里插入图片描述

#include	"unp.h"

int
main(int argc, char **argv)
{
	int					listenfd, connfd;
	pid_t				childpid;
	socklen_t			clilen;
	struct sockaddr_in	cliaddr, servaddr;
	void				sig_chld(int);

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

	Signal(SIGCHLD, sig_chld);	/* must call waitpid() */

	for ( ; ; ) {
		clilen = sizeof(cliaddr);
		if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
			if (errno == EINTR)
				continue;		/* back to for() */
			else
				err_sys("accept error");
		}

		if ( (childpid = Fork()) == 0) {	/* child process */
			Close(listenfd);	/* close listening socket */
			str_echo(connfd);	/* process the request */
			exit(0);
		}
		Close(connfd);			/* parent closes connected socket */
	}
}

避免僵尸进程的另一种方法

在这里插入图片描述

signal(SIGCHLD, SIG_IGN);

5.12 服务器进程终止

如果服务端进程连接客户端时异常终止(被kill命令杀死),则:

  1. 进程终止处理函数关闭所有描述符,服务进程向客户进程发送FIN,客户响应ACK。TCP连接终止序列前半部分已经完成。
  2. 客户端可以继续发送数据,因为正常的半关闭状态允许另一端继续发送数据。
  3. 服务端TCP接收到数据时,响应RST。
  4. 若客户TCP不理会RST而继续发送数据时,会收到SIGPIPE信号。适用于此的规则是:**当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。**该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿地被终止。
    不论该进程是捕获了该信号并从其信号处理函数返回,还是简单地忽略该信号,写操作都将返回EPIPE错误

5.14服务器主机崩溃

如果服务器主机被断开网络,则:

  1. 已有的网络连接上不发出任何东西;
  2. 客户TCP发送数据后没有接收到ACK则会持续重传一段时间;
  3. 如果服务器没有重启,则返回ETIMEDOUT,如果中间路由器判定服务器主机不可达,则响应“unreachable(目标不可达)”ICMP消息,返回EHOSTUNREACH或ENETRUNREACH错误。
  4. 如果希望客户不发送数据也能检测出服务器已经崩溃,则需要设置SO_KEEPALIVE套接字选项。

5.15服务器主机崩溃后重启

客户在服务器崩溃重启后再发送数据:

  1. 当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应以一个RST。
  2. 当客户TCP收到该RST时,客户正阻塞于readline调用,导致该调用返回ECONNRESET错误。

5.16服务器主机关闭

Unix系统关机时,init进程通常先给所有进程发送SIGTERM信号(该信号可被捕获),等待一段固定的时间(往往在5到20秒之间),然后给所有仍在运行的进程发送STGKILL信号(该信号不能被捕获)。这么做留给所有运行的进程一小段时间来清除和终止。如果我们不捕获SIGTERM信号并终止,我们的服务器将由SIGKILL信号终止。当服务器子进程终止时,它的所有打开着的描述符都被关闭。

5.17 TCP程序例子小结

在这里插入图片描述

5.18 数据格式

文本串

在客户和服务器之间传递文本串:无论两者字节序如何,新的服务器程序都能正常工作。

二进制结构

把客户和服务器程序修改为穿越套接字传递二进制值(而不是文本串)存在的问题:
在这里插入图片描述

第6章 I/O复用

6.2 I/O模型

Unix下可用的5种I/O模型:

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O复用
  • 信号驱动式I/O(SIGIO)
  • 异步I/O(POSIX的aio_系列函数)

一个输入操作通常包括两个不同的阶段:

  1. 等待数据准备好;
  2. 从内核向进程复制数据。

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

6.2.1 阻塞式I/O模型

默认情况下,所有套接字都是阻塞的。
在这里插入图片描述

6.2.2 非阻塞式I/O模型

进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
在这里插入图片描述当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统中才有。
内核一旦启动I/O操作的话就不立即返回到进程,而是等到I/O操作完成或遇到错误,所以并非异步I/O。

6.2.3 I/O复用模型

在这里插入图片描述

6.2.4 信号驱动式I/O模型

在这里插入图片描述开启套接字信号驱动式I/O功能步骤:
在这里插入图片描述

6.2.5 异步I/O模型

异步I/O函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。
这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
在这里插入图片描述在这里插入图片描述

比较

在这里插入图片描述

6.2.7 同步和异步I/O

在这里插入图片描述
异步I/O:进程执行I/O系统调用(譬如读或写)告知内核启动某个I/O操作,内核启动I/O操作后立即返回到进程。进程在I/O操作发生期间继续执行。当操作完成或遇到错误时,内核以进程在I/O系统调用中指定的某种方式通知进程。

区别:同步I/O需要自己用户进程自己进行读写,异步I/O让内核来读写。

在这里插入图片描述

6.3 select函数

select函数用来告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。
在这里插入图片描述在这里插入图片描述对于套接字描述符来说,唯一的异常条件是带外数据的到达。

对于回射客户进程,如果服务端崩溃时,客户端阻塞于从标准输入读取数据,则接受不到服务端发送的FIN,不能立即关闭。
select正好能解决这个问题:同时监听标准输入和套接字两个I/O。

6.3.1 描述符就绪准备

在这里插入图片描述在这里插入图片描述注意:当某个套接字上发生错误时,它将由select标记为既可读又可写。
在这里插入图片描述在这里插入图片描述

6.5 批量输入

ping程序时测量RTT(往返时间)的一个简单方法。

在这里插入图片描述

6.6 shutdown函数

在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述我们以批量方式运行用select编写的回射客户程序,发现即使已经遇到了用户输入的结尾,仍可能有数据处于去往或来自服务器的管道中。处理这种情形要求使用shutdowm函数,这使得我们能够用上TCP的半关闭特性。

6.8 TCP回射服务器程序

对于使用select的服务器所能处理的最大客户数目的限制是以下两个值中的较小者:FD_SETSIZE(select的描述符集fd_set能容纳的最大描述符限制) 和 内核允许本进程打开的最大描述符数。

拒绝服务型攻击

在这里插入图片描述

6.9 pselect函数

在这里插入图片描述当有信号需要捕获时,sigmask能让我们避免竞争条件。

6.10 poll函数

在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

比起select,poll函数不受fd_set大小的限制。

6.11 TCP回射服务器程序

确定一个进程任何时刻能够打开的最大描述符数目并不容易。方法之一是以参数_SC_OPEN_MAX调用POSIX的sysconf函数。然而sysconf的可能返回之一是“indeterminate”(不确定),意味着我们仍然不得不猜测一个值。

第7章 套接字选项

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

7.4 套接字状态

对于某些套接字选项,针对套接字的状态,什么时候设置或获取选项有时序上的考虑。我们对受影响的选项论及这一点。
下面的套接字选项是由TCP已连接套接字从监听套接字继承来的(TCPv2第462~463页):SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUE、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。这对TCP是很重要的,因为accept一直要到TCP层完成三路握手后才会给服务器返回已连接套接字。如果想在三路握手完成时确保这些套接字选项中的某一个是给已连接套接字设置的,那么我们必须先给监听套接字设置该选项。

7.5通用套接字选项

7.5.4 SO_ERROR套接字选项

在这里插入图片描述

7.5.5 SO_KEEPALIVE套接字选项

在这里插入图片描述在这里插入图片描述无活动时间可以通过内核参数更改,对所有套接字均有效。
但该选项不能对端主机已崩溃,因而TCP可能会终止一个有效连接。某个中间路由器崩溃15分钟是有可能的,而这段时间正好与主机的11分15秒的保持存活探测周期完全重迭。事实上本功能称为“切断”(make-dead)而不是“保持存活”也许更合适些,因为它可能终止存活的连接。
在这里插入图片描述
TCP的半开连接(half-open)是指TCP连接的一端崩溃,或者在未通知对端的情况下移除socket,不可以正常收发数据,否则会产生RST。
TCP的半关闭是指TCP连接的一端调用shutdown操作使数据只能往一个方向流动,只有一方发送了FIN,仍然可以正常收(或发)数据。
在这里插入图片描述对端进程崩溃会有响应,主机崩溃则完全无响应。

7.5.6 SO_LINGER套接字选项

在这里插入图片描述在这里插入图片描述不推荐用这种方法来避免TIME_WAIT状态。

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述这里有一个基本原则:设置SO_LINGER套接字选项后,close的成功返回只是告诉我们先前发送的数据(和FIN)已由对端TCP确认,而不能告诉我们对端应用进程是否已读取数据。如果不设置该套接字选项,那么我们连对端TCP是否确认了数据都不知道。

告知客户对端应用程序已读取数据

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
倒数第二列指明该选项可以用来发送RST。

7.5.8 SO_RCVBUF 和SO_SNDBUF 套接字选项

每个套接字都有一个发送缓冲区和一个接收缓冲区。我们在图2-15、图2-16中分别描述了TCP、UDP套接字中发送缓冲区的操作。(UDP实际上并没有发送缓冲区,只有发送缓冲区大小这个属性)
对于TCP来说,套接字接收缓冲区中可用空间的大小限定了TCP通告对端的窗口大小。TCP套接字接收缓冲区不可能溢出,因为不允许对端发出超过本端所通告窗口大小的数据。这就是TCP的流量控制,如果对端无视窗口大小而发出了超过该窗口大小的数据,本端TCP将丢弃它们。
**然而对于UDP来说,当接收到的数据报装不进套接字接收缓冲区时,该数据报就被丢弃。**回顾一下,UDP是没有流量控制的:较快的发送端可以很容易地淹没较慢的接收端,导致接收端的UDP丢弃数据报,我们在8.13节将展示这一点。事实上较快的发送端甚至可以淹没本机的网络接口,导致数据报被本机丢弃。

ubuntu16.04:
TCP:
SO_RCVBUF: default = 131072
SO_SNDBUF: default = 16384
DUP:
SO_RCVBUF: default = 212992
SO_SNDBUF: default = 212992

当设置TCP套接字接收缓冲区的大小时,函数调用的顺序很重要。这是因为TCP的窗口规模选项(2.6节)是在建立连接时用SYN分组与对端互换得到的。对于客户,这意味着SO_RCVBUF选项必须在调用connect之前设置;对于服务器,这意味着该选项必须在调用Listen之前给监听套接字设置。给已连接套接字设置该选项对于可能存在的窗口规模选项没有任何影响,因为accept直到TCP的三路握手完成才会创建并返回已连接套接字。这就是必须给监听套接字设置本选项的原因。(套接字缓冲区的大小总是由新创建的已连接套接字从监听套接字继承而来)

在这里插入图片描述

7.5.10 SO_RCVTIMEO和SO_SNDTIMEO套接字选项

在这里插入图片描述超时errno=EWOULDBLOCK(等价于EAGAIN)

7.5.11 SO_REUSEADDR和SO_REUSEPORT 套接字

SO_REUSEADDR套接字选项有4个不同的功用,最主要的是前两种:
在这里插入图片描述在这里插入图片描述在这里插入图片描述(3)SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。

(4) SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。

SO_REUSEPORT 允许端口重复bind,便于多个相同服务器进程同时监听端口,负载均衡


7.5.12 SO_TYPE 套接字选项

本选项返回套接字的类型,返回的整数值是一个诸如SOCK_STREAM或SOCK_DGRAM之类的值。本选项通常由启动时继承了套接字的进程使用。

7.6 IPv4套接字选项

在这里插入图片描述LINUX中IP_RECVDSTADDR被IP_RECVORIGDSTADDR代替。

在这里插入图片描述
在这里插入图片描述

7.9 TCP套接字选项

在这里插入图片描述在这里插入图片描述

7.9.2 TCP_NODELAY套接字选项

在这里插入图片描述

Nagle算法

在这里插入图片描述

在这里插入图片描述

7.11 fcntl函数

与代表“file control”(文件控制)的名字相符,fcntl函数可执行各种描述符控制操作。
在这里插入图片描述套接字属主即为进程ID或进程组ID,获取或设置套接字进程ID或进程组ID。

fcntl函数提供了与网络编程相关的如下特性:
在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

7.12 小结

在这里插入图片描述


第8章 基本UDP套接字编程

UDP数据报大小

对于ipv4:
UDP数据报大小=min(udp发送缓冲区大小,65536)
65536(2^16)来自ipv4分组头部长度字段的16位
ipv6:
可以选择超大净荷以发送超过65536大小的数据。

8.2 recvfrom和sendto函数

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

8.4 UDP回射服务器程序

一般来说,大多数TCP服务器是并发的,而大多数UDP服务器是迭代的。在这里插入图片描述在这里插入图片描述

8.7 数据报的丢失

在这里插入图片描述在这里插入图片描述

8.8 验证接收到的响应

在这里插入图片描述如果服务器运行在一个只有单个IP地址的主机上,那么这个新版本的客户工作正常。然而如果服务器主机是多宿的,该客户就有可能失败。
多宿主机:拥有多个接口(多个IP地址)的主机。
比如,客户端发送数据报的目的地IP接口不同于服务端响应的IP接口。

大多数IP实现接受目的地址为本主机任一IP地址的数据报,而不管数据报到达的接口(TCPv2第217-219页)。RFC1122[Braden1989]称之为弱端系统模型(weak end system model)。如果一个系统实现的是该RFC中所说的强端系统模型(strong end system model),那么它将只接受到达接口与目的地址一致的数据报。

在这里插入图片描述

8.9 服务器进程未运行

我们下一个要检查的情形是在不启动服务器的前提下启动客户。如果我们这么做后在客户上键入一行文本,那么什么也不发生。客户永远阻塞于它的recvfrom调用,等待一个永不出现的服务器应答。

使用tcpdump后可以发现,服务器主机响应的是一个“port unreachable”(端口不可达)ICMP消息。不过这个ICMP错误不返回给客户进程。

我们称这个ICMP错误为异步错误(asynchronous error)。该错误由sendto引起,但是sendto本身却成功返回。回顾2.11节,我们知道从UDP输出操作成功返回仅仅表示在接口输出队列中具有存放所形成IP数据报的空间。该ICMP错误直到后来才返回(图8-10所示为4ms之后),这就是称其为异步的原因。
一个基本规则是:对于一个UDP套接字,由它引发的异步错误却并不返回给它,除非它已连接
在这里插入图片描述

8.10 UDP程序例子小结

在这里插入图片描述

客户必须给sendto调用指定服务器的IP地址和端口号。一般来说,客户的IP地址和端口号都由内核自动选择,尽管我们提到过,客户也可以调用bind指定它们。客户的临时端口在第一次调用sendto时一次性选定,不能改变;然而客户的IP地址却可以随客户发送的每个UDP数据报而变动(假设客户没有捆绑一个具体的IP地址到其套接字上)。其原因如图8-11所示:如果客户主机是多宿的,客户有可能在两个目的地之间交替选择,其中一个由左边的数据链路外出,另一个由右边的数据链路外出。在这种最坏的情形下,由内核基于外出数据链路选择的客户IP地址将随每个数据报而改变。

如果客户bind一个IP地址到它的套接字上,但是发送一个从其他接口外出的数据报,那么该数据报仍然包含绑定在该套接字上的IP地址,即使该IP地址与该数据报的外出接口并不相符也不管

获取地址信息

服务器可能想从到达的IP数据报上取得至少四条信息:源IP地址、目的IP地址、源端口号和目的端口号。图8-13给出了从TCP服务器或UDP服务器返回这些信息的函数调用。
在这里插入图片描述在这里插入图片描述

8.11 UDP的connect函数

给UDP套接字调用connect没有三路握手过程。内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地),保存对端的IP地址和端口号(取自传递给connect的套接字地址结构,用于以后的读写),然后立即返回到调用进程。

PS:因为没有三路握手,所以在连接被拒绝时(服务器进程未运行),connect并不返回错误,而是在read时才返回。

在这里插入图片描述

  1. 我们再也不能给输出操作指定目的IP地址和端口号。使用write或send代替sendto,如果继续使用sento,则指明目的地址的结构的参数必须为空指针。否则会返回EISCONN错误。
  2. 我们不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报。这样就限制一个已连接UDP套接字能且仅能与一个IP地址交换数据报。
  3. 由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。
    在这里插入图片描述在这里插入图片描述

8.11.1 UDP多次调用connect

在这里插入图片描述在这里插入图片描述

8.11.2 性能

在这里插入图片描述

8.13 UDP缺乏流量控制

查看接受到的数据报总数:

netstat -su

-s 按各个协议进行统计
-u (udp) 仅显示udp相关选项

让客户循环发送大量数据,服务端循环接收,不回射。有两种结果:

  1. 客户端在较快的主机上运行,服务端在慢速的主机上运行时,服务器只接收到一个缓冲区的数据,其他数据报都因为缓冲区满被而丢弃。
  2. 客户运行在慢速主机上,服务器运行在较快主机上,则没有数据报丢失。

8.14 UDP中的外出接口的确定

在这里插入图片描述connect会为udp套接字选择合适的源IP(外出接口)。

8.15 使用select函数的TCP和UDP回射服务器程序

UDP套接字bind与TCP套接字相同的端口时,无需设置SO_REUSEADDR选项,因为TCP端口独立于UDP端口



第11章 名字与地址转换

11.2 域名系统

在这里插入图片描述

11.2.1 资源记录

在这里插入图片描述在这里插入图片描述在这里插入图片描述

11.2.2 解析器和名称服务器 (Name Server)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
DNS服务一般使用UDP,但如果消息太长,会改为TCP。

11.2.3 DNS替代方法

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

11.3 gethostname函数

用于将主机名映射为IPv4地址,无法用于IPv6。
在这里插入图片描述在这里插入图片描述

11.4 gethostbyaddr函数

由二进制的IPv4地址找到相应主机名。
在这里插入图片描述

11.5 getservebyname和getservebyport函数

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
PS:s_port为网络字节序

在这里插入图片描述
在这里插入图片描述

11.6 getaddrinfo 函数

gethostbyname和gethostbyaddr这两个函数仅仅支持IPv4。支持IPv6的函数getaddrinfo允许将一个主机名字和服务名字映射到一个地址。返回的是一个sockaddr结构而不是一个地址列表。这些sockaddr结构随后可由套接字函数直接使用。如此一来,getaddrinfo函数把协议相关性完全隐藏在这个库函数内部。应用程序只需处理由getaddrinfo填写的套接字地址结构。该函数在POSIX规范中定义。
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

11.7 gai_strerror 函数

在这里插入图片描述

11.8 freeaddrinfo 函数

在这里插入图片描述

11.9 getaddrinfo 函数:IPv6

在这里插入图片描述

下面的函数皆为协议无关

11.12 tcp_connect函数

tcp_connect执行客户的通常步骤:创建一个TCP套接字并连接到一个服务器。
host: <hostname/IPaddress>
serv:<service/port>

#include	"unp.h"

int
tcp_connect(const char *host, const char *serv)
{
	int				sockfd, n;
	struct addrinfo	hints, *res, *ressave;

	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;

	if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
		err_quit("tcp_connect error for %s, %s: %s",
				 host, serv, gai_strerror(n));
	ressave = res;

	do {
		sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd < 0)
			continue;	/* ignore this one */

		if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
			break;		/* success */

		Close(sockfd);	/* ignore this one */
	} while ( (res = res->ai_next) != NULL);

	if (res == NULL)	/* errno set from final connect() */
		err_sys("tcp_connect error for %s, %s", host, serv);

	freeaddrinfo(ressave);

	return(sockfd);
}

socket调用失败不是致命的错误,因为如果返回地址中有IPv6地址而主机内核并不支持IPv6,这种失败就可能发生。

11.13 tcp_listen函数

在这里插入图片描述若hostname为NULL,则在双栈主机上默认使用IPv6;
如果hostname=“0.0.0.0”,则显式指定IPv4;
如果hostname=“0::0”,则显式指定IPv6。
但,运行在双栈主机上的IPv6服务器技能处理IPv6客户,也能处理IPv4客户。正如12.2节将讨论的那样,IPv4客户主机的地址作为IPv4映射的IPv6地址传递给IPv6服务器。

#include	"unp.h"

int
tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
	int				listenfd, n;
	const int		on = 1;
	struct addrinfo	hints, *res, *ressave;

	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_flags = AI_PASSIVE;
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;

	if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
		err_quit("tcp_listen error for %s, %s: %s",
				 host, serv, gai_strerror(n));
	ressave = res;

	do {
		listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (listenfd < 0)
			continue;		/* error, try next one */

		Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
		if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
			break;			/* success */

		Close(listenfd);	/* bind error, close and try next one */
	} while ( (res = res->ai_next) != NULL);

	if (res == NULL)	/* errno from final socket() or bind() */
		err_sys("tcp_listen error for %s, %s", host, serv);

	Listen(listenfd, LISTENQ);

	if (addrlenp)
		*addrlenp = res->ai_addrlen;	/* return size of protocol address */

	freeaddrinfo(ressave);

	return(listenfd);
}

11.14 udp_client函数

在这里插入图片描述

#include	"unp.h"

int
udp_client(const char *host, const char *serv, SA **saptr, socklen_t *lenp)
{
	int				sockfd, n;
	struct addrinfo	hints, *res, *ressave;

	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;

	if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
		err_quit("udp_client error for %s, %s: %s",
				 host, serv, gai_strerror(n));
	ressave = res;

	do {
		sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd >= 0)
			break;		/* success */
	} while ( (res = res->ai_next) != NULL);

	if (res == NULL)	/* errno set from final socket() */
		err_sys("udp_client error for %s, %s", host, serv);

	*saptr = Malloc(res->ai_addrlen);
	memcpy(*saptr, res->ai_addr, res->ai_addrlen);
	*lenp = res->ai_addrlen;

	freeaddrinfo(ressave);

	return(sockfd);
}

11.15 udp_connect函数

在这里插入图片描述

#include	"unp.h"

int
udp_connect(const char *host, const char *serv)
{
	int				sockfd, n;
	struct addrinfo	hints, *res, *ressave;

	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;

	if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
		err_quit("udp_connect error for %s, %s: %s",
				 host, serv, gai_strerror(n));
	ressave = res;

	do {
		sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd < 0)
			continue;	/* ignore this one */

		if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
			break;		/* success */

		Close(sockfd);	/* ignore this one */
	} while ( (res = res->ai_next) != NULL);

	if (res == NULL)	/* errno set from final connect() */
		err_sys("udp_connect error for %s, %s", host, serv);

	freeaddrinfo(ressave);

	return(sockfd);
}
/* end udp_connect */

在这里插入图片描述

11.16 udp_server 函数

在这里插入图片描述

#include	"unp.h"

int
udp_server(const char *host, const char *serv, socklen_t *addrlenp)
{
	int				sockfd, n;
	struct addrinfo	hints, *res, *ressave;

	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_flags = AI_PASSIVE;
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;

	if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
		err_quit("udp_server error for %s, %s: %s",
				 host, serv, gai_strerror(n));
	ressave = res;

	do {
		sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd < 0)
			continue;		/* error - try next one */

		if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0)
			break;			/* success */

		Close(sockfd);		/* bind error - close and try next one */
	} while ( (res = res->ai_next) != NULL);

	if (res == NULL)	/* errno from final socket() or bind() */
		err_sys("udp_server error for %s, %s", host, serv);

	if (addrlenp)
		*addrlenp = res->ai_addrlen;	/* return size of protocol address */

	freeaddrinfo(ressave);

	return(sockfd);
}

在这里插入图片描述

11.17 getnameinfo函数

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

11.18 可重入函数

使用了static变量的函数无法被信号处理函数安全调用(如果一个函数在使用static变量的同时被信号中断,并且信号处理函数调用该函数再次使用该static变量,则可能对原数据造成破坏),是不可重入函数(非异步信号安全),比如gethostbyname、gethostbyaddr、getservbyname、getservbyport和inet_ntoa。
比较新的函数,比如inet_pton、inet_ntop、getaddrinfo和getnameinfo,可重入的前提是:它们或者由调用者预先分配存储空间,或者动态分配存储空间,同时由其调用的函数都是可重入的。

errno作为全局变量也存在类似问题。

在这里插入图片描述

11.19 非重入改为重入

有两种方法可以把诸如gethostbyname之类不可重入的函数改为可重入函数。

  1. 把由不可重入函数填写并返回静态结构的做法改为由调用者分配再由可重入函数填写结构。
  2. 由可重入函数调用malloc以动态分配内存空间。这是getaddrinfo使用的技巧。这种方法的问题是调用该函数的应用进程必须调用freeaddrinfo释放动态分配的内存空间。如果不这么做就会导致内存空间泄漏:进程每调用一次动态分配内存空间的函数,所用内存量就相应增长。如果进程长时间运行(网络服务器的公共特性之一),那么内存耗用量就随时间不断增加。

第12章 IPv4和IPv6的互操作性

双栈就是指在一台设备上同时启用IPv4协议栈和IPv6协议栈。这样,这台设备既能和IPv4网络通信,又能和IPv6网络通信。

12.2 IPv4客户与IPv6服务器

在这里插入图片描述在这里插入图片描述前提:双栈主机既有一个IPv4地址,也有一个IPv6地址。

在这里插入图片描述在这里插入图片描述
在这里插入图片描述IPv4可以映射为IPv6,但反之不可以。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

12.3 IPv6客户与IPv4服务器

在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述

第13章 守护进程和inetd超级服务器

13.1 概述

守护进程(daemon)是在后台运行且不与任何控制终端关联的进程。
在这里插入图片描述

13.2 syslogd 守护进程

在这里插入图片描述在这里插入图片描述在这里插入图片描述新版linux使用rsyslogd(增强版syslogd)代替了syslogd。

13.3 syslog函数

在这里插入图片描述在这里插入图片描述在这里插入图片描述
rsyslogd的配置文件在/etc/rsyslog.d中
在这里插入图片描述

13.4 daemon_init函数

Linux通过调用daemon函数(通常从服务器程序中),我们能够把一个普通进程转变为守护进程。

 #include <unistd.h> 

  int daemon(int nochdir,int noclose) 

nochdir为0时,即可将工作目录修改为根目录;
noclose为0时,输入,输出以及错误输出重定向到/dev/null 。

启动任意一个程序并让它作为守护进程运行需要以下步骤:
调用fork以转到后台运行,调用setsid建立一个新的POSIX会话并成为会话头进程,再次fork以避免无意中获得新的控制终端,改变工作目录和文件创建模式掩码,最后关闭所有非必要的描述符。

大概实现:

int
daemon_init(const char *pname, int facility)
{
	int		i;
	pid_t	pid;

	if ( (pid = Fork()) < 0)
		return (-1);
	else if (pid)
		_exit(0);			/* parent terminates */

	/* child 1 continues... */

	if (setsid() < 0)			/* become session leader */
		return (-1);

	Signal(SIGHUP, SIG_IGN);
	if ( (pid = Fork()) < 0)
		return (-1);
	else if (pid)
		_exit(0);			/* child 1 terminates */

	/* child 2 continues... */

	daemon_proc = 1;			/* for err_XXX() functions */

	chdir("/");				/* change working directory */

	/* close off file descriptors */
	for (i = 0; i < MAXFD; i++)
		close(i);

	/* redirect stdin, stdout, and stderr to /dev/null */
	open("/dev/null", O_RDONLY);
	open("/dev/null", O_RDWR);
	open("/dev/null", O_RDWR);

	openlog(pname, LOG_PID, facility);

	return (0);				/* success */
}

创建新会话:断开与原终端的联系。

再次fork的目的:确保本守护进程将来即使打开了一个终端设备,也不会自动获得控制终端。当没有控制终端的一个会话首进程打开一个终端设备时(该终端不会是当前某个其他会话的控制终端),该终端自动成为这个会话首进程的控制终端。然而再次调用fork之后,我们确保新的子进程不再是一个会话首进程,从而不能自动获得一个控制终端。

必须忽略SIGHUP信号:因为当会话头进程(即首次fork产生的子进程)终止时,其会话中的所有进程(即再次fork产生的子进程)都收到SIGHUP信号。

打开/dev/null作为本守护进程的标准输入、标准输出和标准错误输出:这一点保证这些常用描述符是打开的,针对它们的read系统调用返回0(EOF),write系统调用则由内核丢弃所写数据。打开这些描述符的理由在于,守护进程调用的那些假设能从标准输入读或者往标准输出或标准错误输出写的库函数将不会因这些描述符未打开而失败。这种失败是一种隐患。要是一个守护进程未打开这些描述符,却作为服务器打开了与某个客户关联的一个套接字,那么这个套接字很可能占用这些描述符(譬如标准输出或标准错误输出的描述符1或2),这种情况下如果守护进程调用诸如perror之类函数,那就会把非预期的数据发送给那个客户。

13.5 inetd守护进程

在这里插入图片描述


第14章 高级I/O函数

14.1 概述

在这里插入图片描述

14.2 套接字超时

在这里插入图片描述

4.3 recv和send函数

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述
Linux额外send flag(摘自《Linux/UNIX系统编程手册 下》):

在这里插入图片描述

14.4 readv和writev函数

分散读和集中写
在这里插入图片描述
在这里插入图片描述

14.5 recvmsg 和sendmsg函数

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述

14.6 辅助数据

在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述CMSG_LEN 和 CMSG_SPACE的参数为需存放的数据字节数。
在这里插入图片描述

14.7 排队的数据量

在这里插入图片描述

14.8 套接字和标准 I/O

在这里插入图片描述标准I/O函数库执行以下三类缓冲:
在这里插入图片描述在这里插入图片描述
因为标准I/O缓冲区容易和套接字造成混乱,所以几乎不用。



第15章 Unix域协议

15.1 概述

Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,所用API就是在不同主机上执行客户/服务器通信所用的API(套接字API)。可视为一种IPC。
Unix域提供两类套接字:字节流套接字(类似TCP)和数据报套接字(类似DUP)。
在这里插入图片描述路径名必须是绝对路径,并且必须是不存在的路径,否则会失败。
UNIX域套接字使用路径名代替普通套接字的地址。

15.2 Unix 域套接字地址结构

在这里插入图片描述在这里插入图片描述例子:

#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd;
	socklen_t			len;
	struct sockaddr_un	addr1, addr2;

	if (argc != 2)
		err_quit("usage: unixbind <pathname>");

	sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);

	unlink(argv[1]);		/* OK if this fails */

	bzero(&addr1, sizeof(addr1));
	addr1.sun_family = AF_LOCAL;
	strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path)-1);
	Bind(sockfd, (SA *) &addr1, SUN_LEN(&addr1));//或者sizeof(addr1)

	len = sizeof(addr2);
	Getsockname(sockfd, (SA *) &addr2, &len);
	printf("bound name = %s, returned len = %d\n", addr2.sun_path, len);
	
	exit(0);
}

在这里插入图片描述

15.3 socketpair 函数

在这里插入图片描述

15.4 套接字函数

在这里插入图片描述

使用udp的unix套接字如果想要得到响应,必须先绑定路径名。


15.7 描述符传递

在这里插入图片描述实际上是内核帮助我们无视接收方权限,打开文件。
可用于打开没有权限打开的文件。

在这里插入图片描述在这里插入图片描述

15.8 接收发送者的凭证

LINUX凭证数据结构

struct ucred
{
  pid_t pid;			/* PID of sending process.  */
  uid_t uid;			/* UID of sending process.  */
  gid_t gid;			/* GID of sending process.  */
};

该数据结构可以作为辅助数据由内核自动填写,随sendmsg发送给其他进程,用来验证进程身份。
https://blog.csdn.net/q1007729991/article/details/71268949



第16章 非阻塞式I/0

在这里插入图片描述

对于一个非阻塞的TCP套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个EWOULDBLOCK错误。如果其发送缓冲区中有一些空间,返回值将是内核能够复制到该缓冲区中的字节数。这个字节数也称为不足计数(short count)。
我们还在2.11节说过,UDP套接字不存在真正的发送缓冲区。内核只是复制应用进程数据并把它沿协议栈向下传送,渐次冠以UDP首部和IP首部。因此对一个阻塞的UDP套接字(默认设置),输出函数调用将不会因与TCP套接字一样的原因而阻塞,不过有可能会因其他的原因而阻塞。
在这里插入图片描述
(4)发起外出连接,即用于TCP的connect函数。(回顾一下,我们知道connect同样可用于UDP,不过它不能使一个“真正”的连接建立起来,它只是使内核保存对端的IP地址和端口号。)我们已在2.6节展示过,TCP连接的建立涉及一个三路握手过程,而且connect函数一直要等到客户收到对于自己的SYN的ACK为止才返回。这意味着TCP的每个connect总会阻塞其调用进程至少一个到服务器的RTT时间
如果对一个非阻塞的TCP套接字调用connect,并且连接不能立即建立,那么连接的建立能照样发起(譬如送出TCP三路握手的第一个分组),不过会返回一个EINPROGRESS错误。注意这个错误不同于上述三个情形中返回的错误。另请注意有些连接可以立即建立,通常发生在服务器和客户处于同一个主机的情况下。因此即使对于一个非阻塞的connect,我们也得预备connect成功返回的情况发生。

步骤:

int val;
val = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);

16.2 非阻塞读和写

在这里插入图片描述
不过,非阻塞式IVO的加入让本函数的缓冲区管理显著地复杂化了。

速度提升了,但复杂度大大增加,这种努力并不值得。
每当我们发现需要使用非阻塞式I/O时,更简单的办法通常是把应用程序任务划分到多个进程(使用fork)或多个线程。
在这里插入图片描述

16.3 非阻塞connect

在这里插入图片描述
在这里插入图片描述
注意:如果描述符建立后对端数据已到达,则描述符也是可读可写,此时需要使用以SO_ERROR调用getsockopt检查套接字上是否有待处理错误。

当套接字有错误待处理时,对这样的套接字的读操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

使用非阻塞connect:

#include	"unp.h"

int
connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
	int				flags, n, error;
	socklen_t		len;
	fd_set			rset, wset;
	struct timeval	tval;

	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)
			return(-1);

	/* Do whatever we want while the connect is taking place. */

	if (n == 0)
		goto done;	/* connect completed immediately */

	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);		/* timeout */
		errno = ETIMEDOUT;
		return(-1);
	}

	if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
		len = sizeof(error);
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
			return(-1);			/* Solaris pending error */
	} else
		err_quit("select error: sockfd not set");

done:
	Fcntl(sockfd, F_SETFL, flags);	/* restore file status flags */

	if (error) {
		close(sockfd);		/* just in case */
		errno = error;
		return(-1);
	}
	return(0);
}

在这里插入图片描述
在这里插入图片描述

非阻塞connect应用:
在这里插入图片描述
副作用:各个连接之间无通信,对于拥塞的网络不利。



第17章 ioctl操作

网络程序(特别是服务器程序)经常在程序启动执行后使用ioct1获取所在主机全部网络接口的信息,包括:接口地址、是否支持广播、是否支持多播,等等。

在这里插入图片描述
我们可以把和网络相关的请求(request)划分为5类:

  • 套接字操作;
  • 文件操作;
  • 接口操作;
  • ARP高速缓存操作;
  • 路由表操作;
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述FIONREAD:通过由ioct1的第三个参数指向的整数返回当前在本套接字接收缓冲区中的字节数。本特性同样适用于文件、管道和终端。

linux获取主机所有ip地址

通过gethostname()和gethostbyname():

#include <stdio.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
int main() 
{
    char hname[128];
    struct hostent *hent;
    int i;
 
    gethostname(hname, sizeof(hname));
 
    //hent = gethostent();
    hent = gethostbyname(hname);
 
    printf("hostname: %s/naddress list: ", hent->h_name);
    for(i = 0; hent->h_addr_list[i]; i++) 
    {
        printf("%s/t", inet_ntoa(*(struct in_addr*)(hent->h_addr_list[i])));
    }
    return 0;
}

获取网络接口信息的命令:
ifconfig



第18章 路由套接字

在这里插入图片描述
在这里插入图片描述

18.6 接口名字和索引函数

每个网络接口都有一个唯一的名字和一个唯一的正值索引(0从不用做索引)。
在这里插入图片描述


第19章 密钥管理套接字

在这里插入图片描述
在这里插入图片描述


第20章 广播

使用广播功能,需先设置SO_BROADCAS套接字选项
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述广播的劣势
1. 在于同一子网上的所有主机都必须处理数据报,若是UDP数据报则需沿协议栈向上一直处理到UDP层,即使不参与广播应用的主机也不能幸免。要是运行诸如音频、视频等以较高数据速率工作的应用,这些非必要的处理会给这些主机带来过度的处理负担。
2. 广播只适用于局域网,多播还适用于广域网。

20.2 广播地址

在这里插入图片描述
在这里插入图片描述

因为不知道本地子网,所以只能使用受限广播地址代替子网定向广播地址。

20.3 单播和广播的比较

UDP单播传输的完整步骤:
在这里插入图片描述   图中以太网子网地址为192.168.42/24,其中24位作为子网ID,剩下8位作为主机ID。左侧的应用进程在一个UDP套接字上调用sendto往IP地址192.168.42.3端口7433发送一个数据报。UDP层对它冠以一个UDP首部后把UDP数据报传递到IP层。IP层对它冠以一个IPv4首部,确定其外出接口,在以太网情况下还激活ARP把目的IP地址映射成相应的以太网地址
00:0a:95:79:bc:b4。该分组然后作为一个目的以太网地址为这个48位地址的以太网帧发送出去。该以太网帧的帧类型字段值为表示IPv4分组的0x0800。IPv6分组的帧类型为0x86dd。
  中间主机的以太网接口看到该帧后把它的目的以太网地址与自己的以太网地址(00:04:ac:17:bf:38)进行比较。既然它们不一致,该接口于是忽略这个帧。可见单播帧不会对该主机造成任何额外开销,因为忽略它们的是接口而不是主机。
  右侧主机的以太网接口也看到该帧,当它比较该帧的目的以太网地址和自己的以太网地址时,会发现它们相同。该接口于是读入整个帧,读入完毕后可能产生一个硬件中断,致使相应设备驱动程序从接口内存中读取该帧。既然帧类型为0x0800,该帧承载的分组于是被置于P的输入队列。
  当IP层处理该分组时,它首先比较该分组的目的IP地址(192.168.42.3)和自己所有的IP地址。(我们知道主机可以多宿,另外回顾一下我们在8.8节就强端系统模型和弱端系统模型进行的讨论。)既然这个目的地址是本主机自己的IP地址之一,该分组于是被接受。
  IP层接着查看该分组IPv4首部中的协议字段,其值为表示UDP的17。该分组承载的UDP数据报于是被传递到UDP层。
  UDP层检查该UDP数据报的目的端口(如果其UDP套接字已经连接,那么还检查源端口),接着在本例子中把该数据报置于相应套接字的接收队列。必要的话UDP层作为内核一部分唤醒阻塞在相应输入操作上的进程,由该进程读取这个新收取的数据报。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
本例展示了广播存在的根本问题,子网上未参加相应广播应用的所有主机也不得不沿协议栈一路向上完整地处理收取的UDP广播数据报,直到该数据报历经UDP层时被丢弃为止。另外,子网上所有非IP的主机(例如运行NovellIPX的主机)也不得不在数据链路层接收完整的帧,然后再丢弃它(假设这些主机不支该帧的帧类型,对于IPv4分组就是0x0800)。要是运行着以较高速率产生IP数据报的应用(例如音频、视频应用),这些非必要的处理有可能严重影响子网上这些其他主机的工作。我们将在下一章看到多播是如何在一定程度上解决本问题的。


20.5 竞争状态

图20-5竞争解决办法:

  1. 使用pselect(在pselect前将信号阻塞,pselect阻塞时原子地将信号掩码更改为信号掩码参数(无阻塞),当pselect返回时,恢复信号掩码),因此只会在pselect阻塞期间对信号进行处理。
  2. 使用IPC(管道)通知主函数定时器超时,select同时检查管道是否可读。



第 21章 多播

单播地址标识单个IP接口,广播地址标识某个子网的所有IP接口,多播地址标识一组IP接口。单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折衷方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。另外,广播一般局限于局域网内使用,而多播则既可用于局域网,也可跨广域网使用。事实上,基于MBone(B.2节)的应用系统每天都在跨整个因特网多播。

在这里插入图片描述在这里插入图片描述

在这里插入图片描述

21.3 局域网上多播和广播的比较

在这里插入图片描述
  右侧主机上的接收应用进程启动,并创建一个UDP套接字,捆绑端口123到该套接字上,然后加入多播组224.0.1.1。我们不久将看到这种“加入”(joining)操作通过调用setsockopt完成。上述操作完成之后,IPv4层内部保存这些信息,并告知合适的数据链路接收目的以太网地址为01:00:5e:00:01:01的以太网帧(TCPv2的12.11节)。该地址是与接收应用进程刚加入的多播地址对应的以太网地址,其中所用映射方法如图21-1所示。
  下一个步骤是左侧主机上的发送应用进程创建一个UDP套接字,往IP地址224.0.1.1的123端口发送一个数据报。发送多播数据报无需任何特殊处理;发送应用进程不必为此加入多播组。发送主机把该IP地址转换成相应的以太网目的地址,再发送承载该数据报的以太网帧。注意该帧中同时含有目的以太网地址(由接口检查)和目的IP地址(由IP层检查)。
  我们假设中间主机不具备IPv4多播能力(因为IPv4多播支持是可选的)。它将完全忽略该帧,因为(1)该帧的目的以太网地址不匹配该主机的接口地址,(2)该帧的目的以太网地址不是以太网广播地址,(3)该主机的接口未被告知接收任何组地址(高序字节的低序位被置为1的以太网地址,如图21-1所示)。
  该帧基于我们所称的不完备过滤(imperfect filtering)被右侧主机的数据链路接收,其中的过滤操作由相应接口使用该帧的以太网目的地址执行。我们之所以说这种过滤不完备是因为尽管我们告知该接口接收以某个特定以太网组地址为目的地址的帧,通常它也会接收以其他以太网组地址为目的地址的帧。
  在这里插入图片描述
在这里插入图片描述
  右侧主机的数据链路收取该帧后,把由该帧承载的分组传递到IP层,因为该以太网帧的类型为Pv4。既然收到的分组以某个多播IP地址作为目的地址,IP层于是比较该地址和本机的接收应用进程已经加入的所有多播地址,根据比较结果确定是接受还是丢弃该分组。我们称这个操作为完备过滤(perfect filtering),因为它基于IPv4报头中完整的32位D类地址执行。在本例子中,IP层接受该分组并把承载在其中的UDP数据报传递到UDP层,UDP层再把承载在UDP数据报中的应用数据报传递到绑定了端口123的套接字。
  

21.4 广域网上的多播

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述路由器通过多播路由协议来互相通信,以判断自己是否需要接受某个多播分组。
在这里插入图片描述

21.12 小结

在这里插入图片描述


第22章 高级UDP套接字编程

22.3 数据报截断

LINUX中,当到达的一个UDP数据报超过应用进程提供的缓冲区容量时,该数据报被截断,并且recvmsg在其msghar结构的msg_flags成员上设置MSG_TRUNC标志。

22.4 何时用UDP代替TCP

在这里插入图片描述

22.5 给UDP应用增加可靠性

关键在于两点,从应用层角度考虑:

  1. 提供超时重传,能避免数据报丢失。

  2. 提供确认序列号,可以对数据报进行确认和排序。
    在这里插入图片描述
    超时重传则需要用到Jacobson算法:
    在这里插入图片描述

但是,该算法有个“重传二义性问题”,可以用如下办法解决:
除了为每个请求冠以一个服务器必须回射的序列号外,还为每个请求冠以一个服务器同样必须回射的时间截(timestamp)。每次发送一个请求时,我们把当前时间保存在该时间戳中。当收到一个应答时,我们从当前时间减去由服务器在其应答中回射的时间戳就算出RTT.既然每个请求携带一个将由服务器回射的时间戳,我们可以如此算出所收到的每个应答的RTT.采用本办法不再有任何二义性。此外,既然服务器所做的只是回射客户的时间戳,因此客户可以给时间戳使用任何期望的时间单位,而且客户和服务器根本不需要为此拥有同步的时钟。

简要概括:
为每个请求冠以序列号和时间戳(当前时间),服务端响应将该请求上的序列号和时间戳原样返回。发送端收到响应后使用当前时间减去响应中的时间戳得到RTT,根据该RTT和jacobson算法可算出合适的重传超时(RTO),同时应用指数回退(第一个RTO为2秒,期间为收到应答,则下一个RTO为4秒)。

22.7 并发UDP服务器

在这里插入图片描述


第24章 带外数据

udp没有带外数据。

24.2 TCP带外数据

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述同一时间,只能有一个带外数据,旧的未读取的带外数据会被新的带外数据顶替掉(非在线)或者仅取代标记(在线留存)。
在这里插入图片描述

带外数据的读取:

1. 使用SIGURG
void sig_urg(int signo)
{
	int		n;
	char	buff[100];

	printf("SIGURG received\n");
	n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB);
	buff[n] = 0;		/* null terminate */
	printf("read %d OOB byte: %s\n", n, buff);
}
int main(int argc, char **argv)
{
	...
	Signal(SIGURG, sig_urg);
	Fcntl(connfd, F_SETOWN, getpid());//
	...
}
2. 使用select

select的异常条件包括带外数据到达,不过有个注意点:select会一直指示一个异常条件,直到进程的读入越过带外数据(直到带外数据之后的数据被读入,异常条件才消失,只是读入带外数据的话异常条件继续显示)。
因此需要一个变量来指示刚刚是否读过带外数据。

#include	"unp.h"

int main(int argc, char **argv)
{
	int		listenfd, connfd, n, justreadoob = 0;
	char	buff[100];
	fd_set	rset, xset;

	if (argc == 2)
		listenfd = Tcp_listen(NULL, argv[1], NULL);
	else if (argc == 3)
		listenfd = Tcp_listen(argv[1], argv[2], NULL);
	else
		err_quit("usage: tcprecv03 [ <host> ] <port#>");

	connfd = Accept(listenfd, NULL, NULL);

	FD_ZERO(&rset);
	FD_ZERO(&xset);
	for ( ; ; ) {
		FD_SET(connfd, &rset);
		if (justreadoob == 0)
			FD_SET(connfd, &xset);

		Select(connfd + 1, &rset, NULL, &xset, NULL);

		if (FD_ISSET(connfd, &xset)) {
			n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB);
			buff[n] = 0;		/* null terminate */
			printf("read %d OOB byte: %s\n", n, buff);
			justreadoob = 1;
			FD_CLR(connfd, &xset);
		}

		if (FD_ISSET(connfd, &rset)) {
			if ( (n = Read(connfd, buff, sizeof(buff)-1)) == 0) {
				printf("received EOF\n");
				exit(0);
			}
			buff[n] = 0;	/* null terminate */
			printf("read %d bytes: %s\n", n, buff);
			justreadoob = 0;
		}
	}
}


24.3 sockatmark函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们现在给出一个简单的例子说明带外标记的以下两个特性:
(1)带外标记总是指向普通数据最后一个字节紧后的位置。这意味着,如果带外数据在线接收,那么如果下一个待读入的字节是使用MSG_OOB标志发送的,sockatmark就返回真。而如果SO_OOBINLINE套接字选项没有开启,那么,若下一个待读入的字节是跟在带外数据后发送的第一个字节,sockatmark就返回真。
(2)读操作总是停在带外标记上(TCPv2第519~520页),也就是说,如果在套接字接收缓冲区中有100个字节,不过在带外标记之前只有5个字节,而进程执行一个请求100个字节的read调用,那么返回的是带外标记之前的5个字节。这种在带外标记上强制停止读操作的做法使得进程能够调用sockatmark确定缓冲区指针是否处于带外标记。

在这里插入图片描述

24.5 客户/服务器心搏函数

在这里插入图片描述
在这里插入图片描述


第25章 信号驱动式I/O

25.1 概述

信号驱动式I/O是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。历史上曾被称为异步I/O,但并不是真正的异步I/O。

25.2 套接字的信号驱式I/O

在这里插入图片描述
(1)、(2)的顺序可以颠倒。
在这里插入图片描述

25.2.1 对于UDP套接字的SIGIO信号

在这里插入图片描述

25.2.2 对于TCP套接字的SIGIO信号

在这里插入图片描述
SIGIO可以为到达数据报提供精确的时间戳。
Linux提供SIOCGSTAMP ioctl,它返回一个含有数据报接收时刻的timeval结构。

在这里插入图片描述


第26章 线程

26.1 概述

多进程的问题:
在这里插入图片描述
在这里插入图片描述

26.2 基本线程函数:创建和终止

26.2.1 pthread_create函数

在这里插入图片描述

在这里插入图片描述

26.2.2 pthread_join函数

在这里插入图片描述

26.2.3 pthread_self函数

在这里插入图片描述

26.2.4 pthread_detach函数

在这里插入图片描述

26.2.5 pthread_exit函数

在这里插入图片描述

如果本线程未曾脱离,它的线程ID和退出状态将一直留存到调用进程内的某个其他线程对它调用pthreadjoin。
指针status不能指向局部于调用线程的对象,因为线程终止时这样的对象也消失。
在这里插入图片描述

26.4 使用线程的TCP回射服务器程序

在这里插入图片描述
在这里插入图片描述
线程函数在返回前必须关闭已连接的套接字(对于使用fork的情形,子进程就不必close已连接套接字,因为子进程旋即终止,而所有打开的描述符在进程终止时都将被关闭),因为本线程和主线程共享所有的描述符,如果不关闭会一直不释放。

在这里插入图片描述
在这里插入图片描述

26.4.1 给新线程传递参数

不能给线程传递局部变量指针,最好传递一个自己malloc的变量的指针。

malloc和free有线程安全和线程不安全两种版本,只要使用了线程相关的函数,在编译后的文件中使用的malloc函数就是线程安全的版本(使用锁来实现线程安全),但仍然是不可重入的。

在这里插入图片描述
在这里插入图片描述

26.5 线程特定数据

把一个未线程化的程序转换成使用线程的版本时,有时会碰到因其中有函数使用静态变量而引起的一个常见编程错误。使用线程特定数据是使得现有函数变为线程安全的一个常用技巧。
在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述*onceptr 作为全局变量被初始化为 PTHREAD_ONCE_INTR,
pthread_key_create创建一个被所有线程存放线程特定数据的的键,所以第一个线程创建即可。
在这里插入图片描述
这四个函数的典型用法如下所示(不考虑出错返回):
在这里插入图片描述

26.7 互斥锁

在这里插入图片描述

26.8 条件变量

互斥锁适合于防止同时访问某个共享变量,但是我们需要另外某种在等待某个条件发生期间能让我们进入睡眠的东西。条件变量(condition variable)结合互斥锁能够提供这个功能。互斥锁提供互斥机制,条件变量提供信号机制。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意,主循环仍然只是在持有互斥锁期间检查ndone变量。然后,如果发现无事可做,那就调用pthread_cond_wait。该函数(原子地)把调用线程投入睡眠并释放调用线程持有的互斥锁。此外,当调用线程后来从pthread-cond-wait返回时(其他某个线程发送信号到与ndone关联的条件变量之后),该线程再次持有该互斥锁。

为什么每个条件变量都要关联一个互斥锁呢?因为“条件”通常是线程之间共享的某个变量的值。

同样的理由要求pthread-cona-wait被调用时其所关联的互斥锁必须是上锁的,该函数作为单个原子操作解锁该互斥锁并把调用线程投入睡眠也是出于这个理由。
在这里插入图片描述在这里插入图片描述
这个时间值是一个绝对时间(absolute time),而不是一个时间增量(time delta)。也就是说abstime参数是函数应该返回时刻的系统时间-从1970年1月1日UTC时间以来的秒数和纳秒数。这一点不同于select和pselect,它们指定的是从调用时刻开始到函数应该返回时刻的秒数和微秒数(对于pselect为纳秒数),通常采用的过程是:调用clock_gettime函数获取当前时间(作为一个timespec结构),再加上期望的时间限制。

使用绝对时间取代增量时间的优点是,如果该函数过早返回(可能是因为捕获了某个信号),那么不必改动timespec结构参数的内容就可以再次调用该函数,缺点是首次调用该函数之前不得不调用时间获取函数。


第27章 IP选项

27.1 概述

IPV4允许在20字节首部固定部分之后跟以最多共40个字节的选项。尽管已经定义的IPV4选项共有10种,最常用的却是源路径选项。这些选项的访问途径是存取IP_OPTIONS套接字选项,我们将以一个使用源路由的例子展示这个访问方式。

IPV6允许在固定长度的40字节IPv6首部和传输层首部(例如ICMPV6,TCP或UDP)之间出现扩展首部(extension header),目前定义了6种不同的扩展首部。不同于IPv4的是,IPv6扩展首部的访问途径是函数接口,而不是强求用户理解这些首部如何呈现在IPv6分组中的真实细节。

27.2 IPv4选项

A.2 IPv4 首部

在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

27.4 IPv6扩展首部

A.3 IPv6首部

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述


第28章 原始套接字

在这里插入图片描述

28.2 原始套接字创建

创建一个原始套接字涉及如下步骤:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

28.5 ping程序

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

28.6 traceroute程序

traceroute允许我们确定IP数据报从本地主机游历到某个远程主机所经过的路径。traceroute使用IPv4的TTL字段或IPv6的跳限字段以及两种ICMP消息。它一开始向目的地发送一个TTL(或跳限)为1的UDP数据报。这个数据报导致第一跳路由器返送一个ICMP"time exceeded in transmit"(传输中超时)错误。接着它每递增TTL一次发送一个UDP数据报,从而逐步确定下一跳路由器。当某个UDP数据报到达最终目的地时,目标是由这个主机返送一个ICMP"port unreachable(端口不可达)”错误。这个目标通过向一个随机选取的(但愿)未被目的主机使用的端口发送UDP数据报得以实现。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


第29章 数据链路访问

在这里插入图片描述

29.2 BPF:BSD分组过滤器

在这里插入图片描述

在这里插入图片描述在这里插入图片描述


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


第30章 客户/服务器程序设计范式

在这里插入图片描述在这里插入图片描述

30.2 TCP客户程序设计范式

在这里插入图片描述

30.3 TCP测试用客户程序

程序略
在这里插入图片描述
在这里插入图片描述

30.5 TCP迭代服务器程序

显示cpu时间函数:

#include	"unp.h"
#include	<sys/resource.h>

#ifndef	HAVE_GETRUSAGE_PROTO
int		getrusage(int, struct rusage *);
#endif

void
pr_cpu_time(void)
{
	double			user, sys;
	struct rusage	myusage, childusage;

	if (getrusage(RUSAGE_SELF, &myusage) < 0)
		err_sys("getrusage error");
	if (getrusage(RUSAGE_CHILDREN, &childusage) < 0)
		err_sys("getrusage error");

	user = (double) myusage.ru_utime.tv_sec +
					myusage.ru_utime.tv_usec/1000000.0;
	user += (double) childusage.ru_utime.tv_sec +
					 childusage.ru_utime.tv_usec/1000000.0;
	sys = (double) myusage.ru_stime.tv_sec +
				   myusage.ru_stime.tv_usec/1000000.0;
	sys += (double) childusage.ru_stime.tv_sec +
					childusage.ru_stime.tv_usec/1000000.0;

	printf("\nuser time = %g, sys time = %g\n", user, sys);
}

getrusage函数被调用了两次,分别返回调用进程(RUSAGESELF)和它的所有已终止子进程(RUSAGECHILDREN)的资源利用统计。所显示的值包括总的用户时间(耗费在执行用户进程上的CPU时间)和总的系统时间(内核在代表调用进程执行系统调用上耗费的CPU时间)。


现场fork子进程

实验结果:
user time = 20.1195, sys time = 13.3638

30.6 TCP预先派生子进程服务器程序,accept无上锁保护

在这里插入图片描述这种技术的优点在于无须引入父进程执行fork的开销就能处理新到的客户。缺点则是父进程必须在服务器启动阶段猜测需要预先派生多少子进程。如果某个时刻客户数恰好等于子进程总数,那么新到的客户将被忽略,直到至少有一个子进程重新可用。然而回顾4.5节,我们知道这些客户并未被完全忽略。内核将为每个新到的客户完成三路握手,直到达到相应套接字上listen调用的backlog数为止,然后在服务器调用accept时把这些已完成的连接传递给它。这么一来客户就能觉察到服务器在响应时间上的恶化,因为尽管它的connect调用可能立即返回,但是它的第一个请求可能是在一段时间之后才被服务器处理。
在这里插入图片描述
实验结果:
user time = 0.19112, sys time = 12.2664

惊群效应

在这里插入图片描述
历史上,Linux的accpet确实存在惊群问题,但现在的内核都解决该问题了。即,当多个进程/线程都阻塞在对同一个socket的接受调用上时,当有一个新的连接到来,内核只会唤醒一个进程,其他进程保持休眠,压根就不会被唤醒。

30.6.4 select冲突

当多个进程在引用同一个套接字的描述符上调用select时就会发生冲突,因为在socket结构中为存放本套接字就绪之时应该唤醒哪些进程而分配的仅仅是一个进程ID的空间。如果有多个进程在等待同一个套接字,那么内核必须唤醒的是阻塞在select调用中的所有进程,因为它不知道哪些进程受刚变得就绪的这个套接字影响。

我们可以得出如下经验:如果有多个进程阻塞在引用同一个实体(例如套接字或普通文件,由file结构直接或间接描述)的描述符上,那么最好直接阻塞在诸如accept之类的函数而不是select之中。

30.7 TCP预先派生子进程服务器程序,accept使用文件上锁保护

在这里插入图片描述
linux的accept属于系统调用(man 2),应该不需要上锁。

实验结果:
user time = 0.166881, sys time = 18.5263

30.7.1 子进程过多的影响

在这里插入图片描述

30.8 TCP预先派生子进程服务器程序,accept使用线程上锁保护

在这里插入图片描述实验结果:
user time = 0.24829, sys time = 14.269

30.9 TCP预先派生子进程服务器程序,传递描述符

在这里插入图片描述
如果父进程accept新连接,则向一个空闲子进程传递描述符;
如果一个子进程恢复空闲,则向父进程随便发送一个字节信息。

缺点:比线程上锁、文件锁更费时。

实验结果:
user time = 0.428096, sys time = 22.1868

30.10 TCP并发服务器程序,每个客户一个线程

图30-1表明这个简单的创建线程版本在Solaris和Digital Unix上都快于所有预先派生子进程的版本。这个为每个客户现场创建一个线程的版本比为每个客户现场派生一个子进程的版本(行1)快许多倍。

实验结果:
user time = 0.537654, sys time = 25.363

30.11 TCP预先创建线程服务器程序,每个线程各自accept

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

实验结果:
无锁版本:
user time = 0.120954, sys time = 11.3525

accept线程锁版本:
user time = 0.134038, sys time = 11.6781


30.12 TCP预先创建线程服务器程序,主线程统一accept

本设计范式的问题在于主线程如何把一个已连接套接字传递给线程池中某个可用线程。这里有多个实现手段。我们原本可以如前使用描述符传递,不过既然所有线程和所有描述符都在同一个进程之内,我们没有必要把一个描述符从一个线程传递到另一个线程。接收线程只需知道这个已连接套接字描述符的值,而描述符传递实际传递的并非这个值,而是对这个套接字的一个引用,因而将返回一个不同于原值的描述符(该套接字的引用计数也被递增)。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
实验结果:
user time = 0.194283, sys time = 15.7442

30.13 小结

环境:Ubuntu16.04
处理器:4核单处理器
内存:4GB
网络:局域网

客户端:10个子进程各自发起5000次连接。每个连接请求10000字节数据。
服务端进程/线程数:15

编号服务器描述CPU时间/s
1并发服务器,为每个客户请求fork一个子进程33
2预先派生子进程,每个子进程调用accept12
3预先派生子进程,文件上锁保护accept19
4预先派生子进程,线程互斥锁保护accept15
5预先派生子进程,由父进程向子进程传递套接字描述符23
6并发服务器,为每个客户请求创建一个线程26
7预先创建线程,每个子线程调用accept11
8预先创建线程,线程互斥锁保护accept12
9预先创建线程,由主线程调用accept16

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

在linux中,多线程进程fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

The child process is created with a single thread–the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.


附录

A.4 IPv4 地址

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

A.4.2 环回地址

在这里插入图片描述在这里插入图片描述在这里插入图片描述

A.4.5 多宿与地址别名

在这里插入图片描述在这里插入图片描述

A.5 IPv6地址

在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

A.6 ICMPv4和ICMPv6:网际网控制消息协议

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
tcp服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define MAXLINE 4096

int main()
{
    int listenfd,connfd;
    struct sockaddr_in servaddr;
    char buff[4096];
    int n;

    //创建一个TCP的socket
    if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {
        printf(" create socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    }
	
    //先把地址清空,检测任意IP
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666);

	
    //地址绑定到listenfd
    if ( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        printf(" bind socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    }

    //监听listenfd
    if( listen(listenfd,10) == -1) {
        printf(" listen socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    }

    printf("====waiting for client's request=======\n");
    //accept 和recv,注意接收字符串添加结束符'\0'
    while(1)
    {

        if( (connfd = accept(listenfd, (struct sockaddr *)NULL, NULL))  == -1) {
            printf(" accpt socket error: %s (errno :%d)\n",strerror(errno),errno);
            return 0;
        }
        n = recv(connfd,buff,MAXLINE,0);
        buff[n] = '\0';
        printf("recv msg from client:%s\n",buff);
        close(connfd);
    }
    close(listenfd);
    return 0;
}

tcp客户端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXLINE 4096

int main(int argc, char**argv)
{
    int sockfd,n;
    char recvline[4096],sendline[4096];
    struct sockaddr_in servaddr;

    if(argc !=2)
    {
        printf("usage: ./client <ipaddress>\n");
        return 0;
    }
    //创建socket
    if( (sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {
        printf(" create socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    }
    
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    //IP地址从“点分十进制”转换到“二进制整肃”
    if( inet_pton(AF_INET,argv[1], &servaddr.sin_addr) <=0 ) {
        printf("inet_pton error for %s\n",argv[1]);
        return 0;
    }
    //连接
    if( connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) <0) {
        printf(" connect socket error: %s(errno :%d)\n",strerror(errno),errno);
        return 0;
    }

    printf("send msg to server:\n");
    fgets(sendline,4096,stdin);
    //send发送
    if ( send(sockfd,sendline,strlen(sendline),0) <0) {
        printf("send msg error: %s(errno :%d)\n",strerror(errno),errno);
        return 0;
    }

    close(sockfd);
    return 0;
}

参考: 《UNIX网络编程 卷1》
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值