概述
本章主要实现的程序模型:
2 TCP回射服务器程序
服务器与客户程序约定一个固定的端口,要比5000大,比49152小。
fork后子进程第一件事就是关掉listenfd,父进程的第一件事是关掉connfd。
在等待客户的read调用返回出错后,如果是因为被信号打断,要重新调用read。
正常情况
正常启动
监听套接字处于LISTEN状态。
客户的connect在三路握手的第二个分节就返回了,而服务器要直到第三个分节才返回,即客户的connect返回时服务器还没有accept。
服务器的连接套接字处于ESTABBLISHED状态。
正常终止
客户端主动关闭时,客户TCP发送一个FIN给服务器,服务器TCP响应ACK,此时服务器处于CLOSE_WAIT状态,客户处于FIN_WAIT_2状态。
服务器关闭时,服务器发送FIN给客户,客户发送ACK给服务器,连接完全终止,客户进入TIME_WAIT状态。
服务器子进程终止时给父进程发送一个SIGCHLD信号。默认行为是忽略,但我们必须捕捉此信号,清理僵死进程。
POSIX信号处理
信号是某个进程发生了某个事件的通知,有时也称为软件中断,通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。
信号可以:
由一个进程发给另一个进程
由内核发给某个进程
每个信号都有一个与之关联的处置也称行为。
处理SIGCHLD信号
多进程下父进程必须捕捉SIGCHLD信号以回收终止状态的子进程资源,否则进程处于僵尸状态。可以在信号处理函数中用wait或waitpid。慢速系统调用会被信号处理函数打断,可能会返回EINTR错误,也可能会自动重启。我们编写捕获信号的程序时,必须对此有所准备。例如,对accept的处理,connect被打断后就不能被使用了。
wait和waitpid函数
通过wait和waitpid都可以获得终止的子进程的pid和状态,waitpid还能指定想等待的pid,options参数允许指定附加选项,最常用的是WNOHANG,在没有终止子进程时不阻塞。
信号阻塞期间如果该信号产生了多次,解除阻塞后只能接收到一次,因此要用waitpid(-1,*,WNOHANG)来循环回收所有结束的子进程。
accept返回前连接中止
三路握手完成,连接建立后,客户TCP发送了RST,服务器端在调用accept前收到了这个RST。
如何处理这种中止依赖于不同的实现。BSD的实现是在内核中处理,服务器的accept继续阻塞,SVR4的实现是返回一个错误给进程。如果返回了一个错误,再次调用accept就行。
服务器进程终止
客户与服务器连接成功后,服务器进程如果终止(被动),套接字被关闭,向客户发送FIN,客户响应ACK,此时客户进程可能阻塞在用户输出上,看不到这个RST,此时如果进行write,再read,就会收到预期外的EOF。
SIGPIPE信号
写一个已收到FIN的套接字会收到RST,写一个已收到RST的套接字会产生SIGPIPE信号。
如果没有特殊的事情要做,就将SIGPIPE设置为SIG_IGN,忽略它,并在后面的读写操作中检查返回的错误。如果需要采取特殊措施(如写入日志),就要捕捉该信号。但如果用了多个套接字,信号处理程序无法分辨是哪个套接字出的错。如果需要知道出错的位置,要么不理会该信号,要么从信号处理函数返回后再处理write的EPIPE。
服务器主机崩溃 服务器主机关机
服务器主机崩溃时,已有的网络连接上不发出任何东西(如FIN)。客户对服务器的写操作会持续重传数据,试图接收一个ACK,直到超时。客户随后的readline调用会返回一个错误。
可以对readline设置一个超时。如果不主动向服务器发送数据也想检测出服务器主机的崩溃,需要SO_KEEPALIVE套接字选项。
服务器主机崩溃后重启
服务器主机在崩溃后客户发数据前重启完成,客户不知道服务器主机的崩溃,发送数据,但服务器TCP丢失了之前的连接信息,因此响应RST,客户TCP收到RST时,客户进程正阻塞于readline调用,该调用返回一个错误。
数据格式
例子:在客户与服务器之间传递文本串
用sscanf获取文本中的指定数据,再用snprintf把结果转换为文本串。
例子:在客户与服务器之间传递二进制结构
当这样的客户和服务器程序运行在字节序不一样的或某些类型长度不一致的两个主机上时,工作将失常。
不同的实现在存储二进制数据的格式上(大端小端)、相同类型的长度上、给结构打包的方式(对齐)上都可能不同,因此直接传送二进制结构绝不明智。
解决方法:
所有的数值数据作为文本串来传递。
显式定义所支持数据类型的进进制格式,并以这样的格式在客户与服务器间传递所有数据。