为了方便起见,我直接用 python 来学这一章(因为 Cpython 这方面接口基本一致),编写方便!这一章和之前的套接字与传输层的笔记的区别是讲的更多是怎么使用他们,同时配备部分函数对应的 TCP 行为,主要是涉及错误的部分。
socket
这里一般 family 和 type 就决定了 protocol,所以 arg3 直接留 0 就可以自动匹配。但是比如你可能可以用 INET + STREAM 来选用 SCTP 但是我们默认是匹配 TCP,SCTP 的首选是 INET+SEQPACKET。
历史上由于有一种想法是让 TCP 这种协议支持不同的地址 IP 和 XXP 之类的,所以划了一个 AF 和 PF 区分逻辑寻址层和操作系统的端口层。结果根本没实现过。
connect
connect 当错误或握手成功才返回,这里主要讲错误的部分。
- Connection timed out 首先是超时错误,ETIMEOUT 实际会等到很久之后(可能超过一分钟)才返回,这是因为我们前面说过的Karn 算法测量 RTT 超时后指数退避。第一个包也一样要用的。
- Connection refused 然后是 RST,对 connect 来说一般在服务器没有监听端口的时候引发(前一篇也讲了其他情况)。
- No route to host 如果路由器传了一个 ICMP 通告不可达,这是 IP 层的问题,可能是网络拓扑出问题,这种情况还是要重传的。
- 状态图的 SYN_SENT 状态就是说第一个 SYN 都要不断重发的情况,直到握手成功或者完全超时。
bind
- connect 不用 bind 的原因是因为 connect 会引发操作系统绑定一个端口,我们完全可以自己指定端口,先 bind 再 connect。不过实际这种需求很少,因为服务端也不能限制入站端口号 since NAT 会用一些很奇怪的端口。所以实际服务器不 bind 也可以 listen fd,不过这常常是搞笑的。但是 RPC 远程过程调用可以这么做因为他存在一个端口注册器。这样做是因为 RPC 的服务请求处理器可能需要动态创建而不是事先指定端口。
- 客户端也可以不绑定 IP 地址只绑定端口,让内核决定IP地址的好处是多网卡客户端免去手动寻网关的福音。
- bind 不能返回内核选定的套接字地址,所以需要使用 getsockname 函数。
- 服务器绑定非通配地址的用途是:一个服务器可能会为很多不同的机构提供 80 端口的 web 主页,然后他这个服务器又有很多个网卡连接到不同的子网里(不是互联网反向代理的那种)。当然这种情景可以用不同的程序listen,也可以用同一个程序通过判断 ip 包的 dst 地址来 if else,主要区别在于 demultiplex 的行为发生在内核和应用层。
- 注意 Address already in use。
listen
- 为了讲第二个选项参数(即最大允许同时连接数量),先讲解内核缓冲区的实现,就只要有 established 缓冲区(三次握手成功)和 syn_rcvd 缓冲区(第一个 SYN 到达,这种操作系统必须设定超时防止浪费资源),可能是两个加起来小于 backlog,也可能用某种放缩实现。
- accept 返回的事件是某个连接的 state 从 syn rcvd 转换到 established。
- 环境变量编程,通过环境变量得到比头文件常量更好的灵活性,使用 getenv 函数。
- 队列满的时候不应该发送 RST since 队列可能会被 free,这时候保证 client 的 SYN SENT 状态符合语义。
- SYN flooding Dos 攻击防备:Borman 1997c 论文。不要为了防止 SYN flooding 而采用超大 backlog,希望内核能认为 backlog 是一个 established 的限制。
fork
- 父进程返回子进程 pid 是为了 wait
- 子进程返回 0 是因为他总是能用 getppid 查询父进程 id,返回 0 方便判断。(因为 init 的存在,子进程的 pid 永远不会是 0)。
- 为什么不用 fork 来做并发服务器,原因很简单,百万级高并发会引发内核管理进程的调度器即内核内存爆炸。然后是频繁的 context switch 引发的 overhead,
这一点暂时存疑我不知道子进程是否共享程序执行时的页表,应该不会,或者更加引发CoW的trap开销?一般直接马上调用 exec,所以应该是有这部分开销的。
迭代服务器和并发服务器
- 需要非迭代的原因是单次服务可能是长连接,这样无法同时服务多客户。
- 借助系统的 timer interrupt 来调度是一个很好的思路,但是缺点前面说过了。
- fork 的并发服务器必须需要让父进程关闭 fd,这样之后子进程关闭才正常结束程序(注意 close 是对 fd refcnt 保证的)。
getsockname 和 getpeername
- 需要两个函数的原因是因为 socket = <ip:port> :<ip:port>. 然后没有显式 bind port 的话需要查询端口获取某些信息。
- sock 获取本地
- peer 获取外地。
- 如果使用通配 ip bind,一旦 listen 而 accept 之后,可以用来查询 client 地址。(使用 new fd 作为参数)
- 例子: telnet 是由 inetd 守护进程 fork 出来 exec 的,这样他 exec 之后就没有 accept 的数据结构了,无法知道客户是谁,但是他有这个 connfd(作为 exec 的参数传过来或者规定默认把 0 1 2 作为 exec 程序继承的 fd 就行了)。012到底是什么存疑。