前言:
这算是第二次系统地阅读UNPv1,正如副标题,不希望写成书摘、缩写版,尽量多写写个人体会和获得的经验,因此很多地方都会从全书的角度来说明,而不仅仅限于某个章节内部。
SCTP、信号驱动I/O、广播和多播等内容不包括在内。
准备工作:
为了适应在命令行界面编写代码,我先进行了vim环境的配置和Xshell的配置。在读书过程中还学习了gdb调试程序的方法(见这里)。
(1)vim插件搭建
从别人那里拷贝现成的的.vimrc和插件,分别放于/home/用户名 和/home/用户名/vim文件夹下就行了。
(2)Xshell远程登录
下载安装Xshell。由于是用VMware装的Ubuntu,并且此时主机使用PPPoE上网。需要设置使虚拟机能够上网:使用网桥连接,把IP地址与主机设为同一网段,并且可以互相访问。主机开启代理服务器(比如CCProxy),将虚拟机的网络设置为代理上网,这样虚拟机就可以使用sudo apt-get install了。如果是通过局域网上网,那么就更简单,略过。Xshell设置本身很简单(可以参考这篇博文),设置好就能登录到虚拟机上了。
热身:出错处理和包裹函数
UNP中的代码风格有一点是使用包裹函数和作者编写出错处理库相结合,这使得初学者可以把注意力集中在学习主干代码本身,而不是在初学阶段就不得不对所用到的函数考虑出错处理,同时这并不影响代码的稳健型,仍然提供了出错时的提示和处理功能。因此复习的第一步是从出错处理开始,这是UNP附录D.3的内容,几个错误处理函数按照错误的差异决定以abort()、exit(1)、还是return做结尾;而出错信息的处理有一个使用了可变参数列表的函数来完成,它按照需要把出错信息传递给syslog或是标准错误输出。
包裹函数根据其调用函数的出错情况来调用出错处理,并且把全局变量errno预存,处理完再返回,防止其他进程覆盖。
第一章 & 第二章 基础知识
第一章第一个程序daytimetcpcli.c,用127.0.0.1做测试时,需要有daytime服务。开启方法:http://blog.csdn.net/shenyan008/article/details/6997681,当然直接自己编写一个服务器也行,原书的后文提供了相关代码。
对于netstat -i -a -n的各列含义,如果不明白可以使用ifconfig <指定接口>来查看。
本来想了解一下套接字相关函数实现,但是发现它们的代码比较复杂也比较长,没有深入下去,如果读者有兴趣可以自行参考《Linux内核情景分析》。了解了一部分内容:套接字描述符其实是特殊的已打开文件,这形成了一种特殊的文件系统sockfd。与文件系统的关联:i_mode中S_IFSOCK标志位设为1,并将i_sock也设为1。(对文件系统知识的缺乏也是没有深入看下去的原因。)
关于这些函数关系的比喻:“sokcet()是安装电话机,bind()为它指定一个使用的号码,connect()是向某个号码的服务器拨号。”
TCP状态转换图很经典,结合这个图可以对各个函数(bind、listen、connect、accept、close)作出的状态转换(阻塞时的状态、返回时机等)有更好的理解。状态可以用netstat -anp|grep <#port>来查看,在后续的程序运行和调试中使用这个命令能加深对状态和其转换的理解。
习题2.5回顾了MSS和路径MTU发现功能的关系,值得看一看。
第三章 & 第四章 常用函数与数据结构
通用套接字的意义:使用sockaddr的强制转换可以使套接字函数能够处理来自所有支持的任何协议族的套接字结构,这也是为什么它们需要一个这个结构的长度作为参数的原因。
UNP提供readline的原因是read和write不能够按行处理缓冲区。
socket()的参数表比较有意思,后两个参数会相互限定,但是由于这个限制不是唯一的,不可能把三个参数简化成两个参数。这么讲比较抽象,请结合图4-5来理解。
connect()的出错:超时、不可达、拒绝(可达但是不提供请求的服务)。
bind()通常是服务器需要进行而客户不必进行的。事实上客户限定的套接字是由内核决定,绑定非通配地址后,解复用由内核而不是进程完成。
listen()的backlog参数UNP解释的比较含混,准确的解释可以在man里看到,并与具体系统有关。
每个文件或套接字都有一个引用计数(对于python,任何对象都有),这也相对于后面对close()和shutdown()差异的解释(前者引用计数-1,为0时关闭;后者直接关闭并发送TCP连接终止消息)。
第五章 基本TCP编程 & 第八章 基本UDP套接字编程
编写的C/S代码的框架(包括后面的多进程服务器)没什么好说的,但这章开始,信号处理就成了大部分程序中不可缺少的部分,UNP里信号方面主要处理的对象是僵尸进程、SIGPIPE、中断、ALARM计时器这几种。把signal函数的简化定义看懂还是要花点功夫的。这里我做了个引申:#define和typedef的区别是?参考
第六章 I/O复用(select、poll、kqueue、epoll)& 第十六章 与非阻塞I/O
大多数UNIX提供了select和poll,FreeBSD提供了kqueue,Linux提供了epoll。其实它们很相似,如果仔细阅读了select的例子再去看后面三者,是非常好理解的。个人感觉它们相当于一种内核进行的“轮询”(不同于用户实现的轮询,有着更高的效率和更低的开销),我们设置了要关注的描述符(套接字、标准输入输出等)以及我们所关注的状态(是可读了、可写了还是别的什么的),或许还需要设置个flags,一旦出现了这个状态就按我们预设的行为去操作,它们表面上的区别只是在于借助的辅助数据结构的不同。之前写过epoll的代码,可以参考这里。
而对于UNP的第十六章的非阻塞I/O,基本都是通过设置套接字为非阻塞模式并select来实现的。但是非阻塞I/O并没有投入睡眠,而是反复地进行系统调用(6.2 I/O模型)。
同时,6.4~6.6节非常绕口的解释了为什么stdio和select混合使用时非常容易犯错误的:对EOF的处理会导致函数的退出,而这时发送缓冲区可能还有内容要处理。
第七章 套接字选项 & 第十一章 名字地址转换 & 第十四章 高级I/O函数
内容很多,但是没必要一条一条记着,有需要再查就行。
第十一章提供的一些函数比较好用(tcp_listen等),可以作为自己以后用的库函数。
第十三章 守护进程
编写的daemon_init()可以守护进程化当前进程,值得加入函数库。
第三十章 C/S设计范式
这章比较综合,将不同的C/S实现方式做了个综合和比较,包含了迭代模型、进程模型、线程模型、简单的进程/线程池(预先派生,任务分配和均衡由内核完成)、进程互斥方式(文件锁和线程锁)、描述符传递(使用到了UNIX域套接字)等,并且还包含了进程使用的用户/系统时间的测定,非常有价值,建议读者把这部分代码动手实现一下。
另外第十二章的IPv4/v6互操作性简单读了下。
在这次复习中,所看部分的大部分代码都实现并调试运行通过,并且按照unp源码的组织形式进行组织。最后是以一个第二十八章的ping程序的学习和实现作为这次学习的结束。ping的源码在UNP上很完善了,不必多言,如果需要不切换超级用户就能运行,可以参考这篇博文。