阅读UNIX网络编程卷1:套接字联网API(第3版)--整理笔记

一、一个基本的tcp客户/服务器回写,server & client 的代码如下:

server:

client:

这边最好是要把2个buffer都memset()下,不然空回车,数据就不对了

本书前5章讲的tcp的并发,都是多进程模式(fork)

等待连接,正在listening
已建立连接

POSXI信号处理

信号:就是告知某个进程发生了某个事件的通知;信号通常是异步发生的,也就是说接受信号的进程不知道信号的准确发生时刻。

信号可以一个进程发送给另外一个进程。也可以是内核发给某个进程

现在是修改客户端代码,使用for循环,连续5次connect服务器,然后ctrl+c。停止客户端程序,这样使得server端产生僵尸进程。



处理僵尸进程方法:

wait 函数:利用 wait 函数销毁僵死进程的原理:父进程主动请求获取子进程的返回值。

waitpid函数:wait 的局限性:调用 wait 函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止。wait 函数不能处理客户端与服务器同时建立多个连接的情况(《UNIX 网络编程》P109-111)

wait 函数会引起程序阻塞,但 waitpid 函数不会阻塞,而且可以指定等待的目标子进程,options 指定为 WNOHANG 时没有终止子进程也不会阻塞。

建立一个信号处理函数并在其中调用wait并不足防止出现僵尸进程。本问题在于:所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为unix信号一般是不排队的,更严重的是,本问提示不确定的。我们刚才运行的例子,客户与服务器在同一个主机上,信号处理函数执行一次,留下4个僵尸进程。但是如果我们在不同的主机上运行客户和服务器,那么信号处理函数一般执行2次,一次是第一个产生的信号引起的,由于另外4个信号在信号出路函数第一次执行时发生,因此该处理函数仅仅再调用一次,从而留下3个僵尸进程。不过有时候,以来于FIN到达服务器主机的时间,信号处理函数可能会执行3次甚至4次。

使用 waitpid 而不是 wait 的原因——UNIX 信号不排队。

一个客户与并发服务器建立 5 个连接时,建立一个信号处理函数并在其中调用的 wait 不足以防止出现僵死进程(只能终止一个进程)。原因:所有 5 个信号都在信号处理函数执行之前产生,而信号处理函数只执行了一次,因为 Unix 信号一般是不排队的。正确的解决方法是调用 waitpid 而不是 wait:在一个循环内调用 waitpid,以获取所有已终止子进程的状态。WHOHANG 告知 waitpid 没有已终止子进程时也不要阻塞。


server

本节的目的是示范我们在网络编程时可能会遇到的三种情况:

          1. 当 fork 子进程时,必须捕获 SIGCHLD 信号

          2.当捕获信号时,必须处理被中断的系统调用

         .3.SIGCHLD 的信号处理函数必须正确编写,应使用 waitpid 函数以免留下僵死进程。

服务器进程停止:

现在杀死服务器的子进程。这是在模拟服务器进程崩溃的情形。我们可以从中查看客户将发生什么。(我们必须小心的区别即将讨论的服务器进程崩溃与将在一下讨论服务器主机崩溃)所发生的步骤如下:

(1)我们在同一个主机上启动服务器和客户端,并在客户上键入遗憾文本,以验证一切正常。正常情况下该行文本由服务器子进程回写给客户。

(2)找到服务器子进程的pid,并执行kill杀死。作为进程终止处理的部分工作,子进程中所有打开着的描述符都被关闭,这就导致向客户发送一个FIN,而客户TCP则响应一个ACK,这个就是TCP连接终止工作的前半部分。

(3)SIGCHLD信号被发生给服务器父进程,并得到正确处理,

(4)客户上没有发生任何特殊之事。客户TCP接收来自服务器TCP的FIN并响应一个ACK,然而问题是客户进程阻塞在fgets调用上,等待从终端接收一行文本。

(5) 此时,在另外窗口上运行netstat cmd,一观察套接字的状态。


(7) 然而客户进程看不到这个RST,因为它在调用writen后,立即调用readline,并且由于第2步中接收的FIN,所调用的readline立即返回0(表示EOF)。我们的客户此时并未预期收到EOF,于是出现“server terminated prematurely”(服务器过早终止)推出。

(8) 当客户终止时(通过调用err_quit),它所有打开着的描述符都被关闭

本例子的问题在于:当FIN到达套接字时,客户正阻塞在fgets调用上。客户实际上在应对2个描述符--套接字和用户输入,它不能单纯阻塞在这两个源中某个特定的源输入上(正如目前编写的str_cli函数所为),而应该阻塞在其中在任何一个源 的输入上。事实上这正是selecth和poll这2个函数的目的之一。将在6.4节重新编写str_cli,一旦杀死服务器子进程,客户就会立即被告知已收到FIN。

SIGPIPE信号

SIGPIPE产生的原因是这样的:如果一个 socket 在接收到了 RST packet 之后,程序仍然向这个 socket 写入数据,那么就会产生SIGPIPE信号。

  这种现象是很常见的,譬如说,当 client 连接到 server 之后,这时候 server 准备向 client 发送多条消息,但在发送消息之前,client 进程意外奔溃了,那么接下来 server 在发送多条消息的过程中,就会出现SIGPIPE信号


服务器主机崩溃(SO_KEEPALIVE套接字)

在不同的主机上运行服务器和客户端,先启动服务器,再启动客户端,确定它们正常启动后,从网络上断开服务器主机,并在客户键入一行文本。

当服务器主机崩溃后(不是由操作员执行命令关机),已有的网络连接上不再发出任何东西。

此时客户键入一行文本,文本由 writen 写入内核,再由客户 TCP 作为一个数据分节发出。然后客户阻塞在 readline 调用,等待服务器回射应答。

此时用 tcpdump 就会发现,客户 TCP 持续重传数据分节,试图从服务器上接收一个 ACK。

既然客户阻塞在 readline 调用上,该调用会返回一个错误:

假设服务器已经崩溃,对客户的数据分节根本没有响应,返回错误 ETIMEDOUT;

如果某个中间路由器判定服务器已不可达,则该路由器会响应一个 “destination unreachable” (目的地不可达)ICMP 消息,返回错误为 **EHOSTUNREACH **或 ENETUNREACH。

本例的问题在于:想要知道服务器主机是否崩溃,只能通过客户向服务器主机发送数据来检验。如果想不发送数据就检测出服务器主机是否崩溃,需要使用 SO_KEEPALIVE 套接字选项。

服务器主机关机(使用select poll epoll)

服务器主机被操作员关机将会发生什么:Unix 系统关机时,init 进程会给所有进程发送一个 SIGTERM 信号(该信号可被捕获),等待一段固定时间(5~12s),然后给所有仍在运行的程序发送给一个 SIGKILL(该信号不能被捕获)。这么做的目的是,留出一小段时间给所有运行的进程来清除与终止。

如果不捕获 SIGTERM 信号并终止,服务器将由 SIGKILL 信号终止。当服务器子进程终止时,它的所有打开着的描述符都被关闭,这样又回到了服务器进程终止的问题。

数据格式

在客户与服务器之间传递二进制值时,如果字节序不一样或所支持的长整数的大小不一致,将会出错。

3 个问题:

a.不同的实现以不同的格式存储二进制数——大端字节序与小端字节序。

b.不同的实现在存储相同的 C 数据类型上可能存在差异——大多数 32 位 Unix 系统使用 32 位表示长整数,而 64 位系统一般使用 64 位表示长整数。

c.不同的实现给结构打包的方式存在差异。因此,穿越套接字传送二进制结构绝不是明智的。

解决方法:

1. 把所有的数值数据作为文本串来传递。

2.显式定义所支持数据类型的二进制格式(位数、大端或小端字节序),并以这样的格式在客户与服务器之间传递所有数据。远程过程调用(RPC)通常使用这种技术。

总结

从简单的 echo 服务器开始,解决了以下问题:

处理僵死子进程——采用信号处理(signal,sigaction)。

服务器进程终止时,客户进程收到 FIN 但并不知道终止——使用 select、poll。

服务器主机崩溃时,必须通过客户向服务器发送数据才能检验—— SO_KEEPALIVE 套接字选项。

穿越套接字传送二进制结构绝不是明智的——把所有的数值数据作为文本串来传递。

二、高级IO的函数

在套接字操作上设置时间限制的方法有三个:

a.使用alarm函数和SIGALRM信号


b. 使用由select提供的时间限制
c. 使用较新的SO_RCVTIMEO和SO_SNDTIMEO套接字选项


22.5 给UDP应用增加可靠性                                                                                                                                                                                       那么必须在客户程序种中增加以下2个特性:                                                                                                                                                               (1)超时和重传:用于处理丢失的数据报                                                                                                                                                                     (2)序列号:供客户验证一个应答是否匹配相应的请求

22.7 并发UDP服务器

(1)第一种UDP服务器比较简单,读入一个客户请求并发送一个应答后,与这个客户就不再相关了。这种情况下,读入客户请求的服务器可以fork一个子进程并让子进程取处理该请求。该“请求”(即请求数据报的内容以及含有客户协议地址的套接字地址结构)通过由fork复制的内存映像传递给子进程。然后子进程吧它的应答直接发送给客户。

(2)第二种UDP服务器与客户交换多个数据包。问题事客户知道的服务器端口号只有服务器的一个众所周知的端口。一个客户发送其请求的第一个数据报到这个端口,但是服务器如何区分这是来自该客户同一个请求的后续数据报还是来自其它客户请求的数据报呢?这个问题的典型解决办法就是让服务器为每个客户创建一个新的套接字,在其上bind一个临时端口,然后使用该套接字发送对该客户的所有应答。这个办法要求客户查看服务器第一个应答中的源端口号,并把请求的后续数据报发送到该端口。

第二种类型的UDP服务器的一个例子就是TFTP.

https://github.com/ideawu/tftpx

https://blog.csdn.net/leon_zeng0/article/details/106883498

https://gitee.com/ivan_allen/unp   udp并发编程

https://blog.csdn.net/q1007729991/article/details/79251579

https://mirrors.edge.kernel.org/pub/software/network/tftp/tftp-hpa/    tftp-hpa源码

tftpx的源代码:


创建个线程去新建一个新的socket

为客户端新建一个新的套接字与客户端通信,就是上面说的udp并发的第二种方式


停止等待机制作为数据传输的基本机制, 是网络编程必须要掌握的技能. TFTP 协议使用基于UDP的停止等待机制来实现文件的可靠传输.

独立运行的UDP并发服务器所涉及的步骤

24 带外数据

许多传输层有带外数据的概念,它有时也称为经加速数据。其想法就是一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端。这里的迅速意味着这种通知应该在已经排队等待发送的任何“普通”数据之前发送。也就是说,带外数据被认为具有比普通数据更高的优先级。带外数据并不要求在客户和服务器之间再使用一个连接,二十被映射到已有的连接中。(telnet rlogin ftp使用带外数据)。

TCP并没有真正的带外数据,不过提供了我们接着讲解的紧急模式。假设一个进程已经往一个TCP套接字写出N字节数据,而且TCP把这些数据排队再该套接字的发送缓冲区中,等着发送到对端。如下图展示了这样的套接字发送换种去,并且标记了从1到N的数据字节。

含有待发送数据的套接字发送缓冲区

该进程接着以MSG_OOB标志调用send函数写出一个含有ASCII字符a的单字节带外数据:send(fd, "a", 1, MSG_OOB)

TCP把这个数据放置再该套接字发送缓冲区的下一个可用位置,并把该连接的TCP紧急指针设置成再下一个可用的位置,图下图展示了此时的套接字发送缓冲区,并且把带外字节标记为“OOB”

应用进程写入1字节带外数据后的套接字发送缓冲区

发送和接收带外数据的小例子,使用SIGURG的简单例子

简单的带外发送程序


简单的带外接收程序

也可以使用select方式接收带外数据:

正确的select异常条件的

带外数据也可以使用sockatmark函数。

24.5 客户/服务器心搏函数

使用带外数据的客户/服务器心搏机制

这2个函数代码引用图5-4的,分别在2个for循环之前,调用heartbeat_cli函数与heartbeat_serv()

客户端心搏函数


服务器端心搏函数

说明:在这个例子中,客户每隔1s向服务器发送一个带外字节,服务器接收该字节将导致它向客户发送回一个带外字节。每端都需要知道对端是否不复存在或者不再到达。客户和服务器每1s递增它们的cnt变量一次,每收到一个带外字节又把该变量重置为0.如果该计数器达到5,那就认定连接失效。当有带外字节到达时,客户和服务器都使用SIGURG信号得以通知。我们在该图中间指出:数据、会送数据和带外字节都通过单个TCP连接交换。

25 信号驱动式I/O

信号驱动式IO是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。他在历史上曾被称为异步IO,不过我们讲解的信号驱动式IO不是真正的异步IO.后者通常定义为进程执行IO系统调用(譬如读或写)告知内核启动某个IO操作,内核启动IO操作后立即返回到进程。进程在IO操作发生期间继续执行。当操作完成或遇到错误时,内核以进程在IO系统调用中指定的某种方式通知进程。

注意。我们在第16章讲解过的非阻塞式IO同样不是异步IO.对于非阻塞式IO,内核一旦启动Io操作就不像异步IO那样立即回到进程,而是等到Io操作完成或遇到错误;内核立即返回的唯一条件式IO操作的完成不得不把进程投入睡眠,这种情况下内核不启动Io操作。

针对一个套接字使用信号驱动式IO(SIGIO)要求进程执行以下3个步骤

(1) 建立SIGIO信号的信号处理函数

(2) 设置该套接字的属性,通常使用fcntl的F_SETOWN命令设置

(3) 开启该套接字的信号驱动式IO,通常通过使用fcntl的F_SETFL命令打开O_ASYNC标志完成。

小结:

信号驱动式IO就是让内核在套接字上发生“某事”时使用SIGIO信号通知进程

a. 对于已连接TCP套接字,可以导致这种通知的条件为数众多,反而使得整个特性几近无用

b. 对于监听TCP套接字,这种通知发送在有一个新连接已准备好接受之时

c. 对于UDP套接字,这种通知意味着或者到达一个数据报,或者到达一个异步错误,着两种情况下使用revfrom

28 原始套接字

原始套接字提供普通的TCP和UDP套接字所不提供的以下3个能力:

(1) 有了原始套接字,进程可以读/写ICMPv4、IGMPv4和ICMPv6等分组。比如ping 、traceroute等。

(2) 有了原始套接字,进程可以读写内核不处理其协议字段的IPv4数据报。

(3) 有了原始套接字,进程还可以使用IP_HDRINCL套接字选项自行构造IPv4首部,这个能力可用于构造譬如说TCP或UDP分组。

创建一个原始套接字涉及以下步骤:

a. 把第二个参数指定为SOCK_RAW并调用socket函数,以创建一个原始套接字,第三个参数(协议)通常不为0,如下:

int sockfd; sockfd = socket(AF_INET,SOCK_RAW, protocol),protocol参数是形入IPPROTO_xxx的某个常值,定义在头文件中,只有超级用户才能创建原始套接字,这么做可防止普通用户往网络写出它们自行构造的IP数据报。

b. 可以在这个原始套接字上按以下方式开启IP_HDRINCL套接字选项:

const int on=1; setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on))

原始套接字输出

原始套接字的输出遵循以下规则:

a. 普通输出通过调用sendto或sendmsg并指定目的IP地址完成,如果套接字已经连接,那么也可以调用write writev或send

b. 如果IP_HDRINCL套接字选项未开启,那么由进程让内核发送的数据的起使地址指的是IP首部之后的第一个字节,因为内核将构造的Ip首部并把它置于来自进程的数据之前。内核把所构造IPv4首部的协议字段设置成来自socket调用的第三个参数。

c. 如果IP_HDRINCL套接字已开启,那么由进程让内核发送的数据的起始地址指的是IP首部的第一个字节。进程调用输出函数写出的数据量必须包括IP首部的大小。整个IP首部由进程构造,不过(a)IPv4标识字段可置为0,从而告知内核设置该值,(b)IPV4首部校验和字段总是由内核计算并存贮,(c)IPv4选项字段是可选的,

d. 内核会对超出外出接口MTU的原始分组执行分片。

ping程序:大体思想就是定时器定1s,发送一个icmp数据包,然后接收解析。使用定时器,函数指针。

数据结构
main函数
loop循环,发送是定时器每1s定时
recv函数
定时发送
构造数据包发送
计算校验和

运行如下:

可以再加个信号接收处理,不如ctrl +c 等,最后可以统计send多少和recv多少

你可能感兴趣的:(阅读UNIX网络编程卷1:套接字联网API(第3版)--整理笔记)