Overview
前言
最近有个重要的性能问题要解决,
在“非常非常”低配的嵌入式 LINUX 设备上,动不动就会给你出现平时很难出现的性能问题
涉及到“类” DDOS 攻击,需要对 HTTP server 做一些修改,定制。
所幸之前看了 《TCP/IP 网络编程》((韩)尹圣雨) 一书,另加上先前看过 HTTP server 的源码并且做过了一点点根据客户需求的定制(增加一些安全性相关的 feature),所以“感觉上”不会很困难,但是预期修改幅度并不会小。
其实 HTTP server 也就是
mini_httpd
这一开源软件。在规模上已经算是很小的软件了。
之所以有这篇前言,是觉得在传统上,应用层 的程序员是要有过 socket 编程这一练习的,手撕一个简单的 HTTP server – 应该的。
既然本文是在 C 语言下,所以 C 语言主要在 驱动层,应用层的区分还是要说明清楚的,毕竟让驱动层的程序员对 socket,HTTP 通信研究地清楚、熟练;不是很有必要,略有了解就行了。
但是 socket 编程在当下,也算是相对较为底层的了,在更高级的语言上应该更少用,除了小部分需要自己写 socket 通信的应用场景。
所以,需不需要对 socket 编程熟练掌握,甚至理解地清楚呢?
个人的观点是从最终的结果来看,答案是需要的。但是这中间的过程,或许 socket 编程可以稍稍往后放一点点再学也不急。
但是越早练习和学习越好,因为这个也很重要,其不仅仅是 socket 编程这一个点。socket 编程实际上已经是“系统编程”了,在更高级的语言上,“系统编程”这一点略有弱化,但是用 C 语言编程 socket,这是属于“系统编程”范畴无误了。
最后,这一篇博客不会具体到完整地代码来编程网络套接字,也不会再次提到这节前言中提到的我要解决的问题,更不会说明套接字客户端如何编程(因为客户端的情况并不复杂)。
而是从全局地总结网络套接字编程和部分编程要点,以及提出和解答几个非常关键的问题。
注:本文 TCP/IP 层默认基于 TCP 协议(及 IPv4,不过不会到 IP 层)讨论。
1. socket 服务端编程
在《Head First C》上使用 BLAB 形容了套接字编程1。
即:
B: bind
L: listen
A: accept
B: begin
一个简单的套接字编程只要按上述做函数调用即可。
1.1 accept -> fork 并发模型
我们知道网络 I/O 远远慢于 CPU 计算、运行2,所以可以想象的到,仅是“在系统处理完一个客户端地请求之后,在网络 I/O 上发送出去,到接收下一个客户端的链接请求”这一情况而言,这段时间 CPU 就是很空闲的。因此我们应当使用并发的编程方式。
这里是一份相对简单的并发处理客户端请求的程序逻辑:
这里值得一提的是,图示中 close 出现了三个。如果按照图示编程(和各个书籍上的例子)是没有问题的,但是这里有个比较简单形象的图示3可以解释为什么要有这么多 close
:
要真正理解这个 close
的情况,和看懂这张图,就要回答现在提出的本博文第一个问题:
listen
函数系统调用修改的 listen_fd
(提示:由 sockt
函数创建)和 accept
函数返回的 conn_fd
各自含义与区别是什么?4
本节详细的 close
使用和理解可以参考 《UNIX网络编程卷1:套接字联网API》&4.8 ~ 4.9 之 描述符引用计数。
基础问题:
图示并发服务器端程序框图中,在哪些位置会发生阻塞?
1.2 补充
-
setsockopt
-
僵尸进程 -
SIGCHLD
捕获与处理 -
accept
无法实现信号终端,使用select
代替accept
阻塞
2. I/O 复用
对于一个实现基本功能的简单(HTTP)服务器,上面 1. 节中的内容已经足够。
本节具体内容暂略。
问题:
select
函数对服务器编程逻辑、性能提升是否有帮助?如果有,有何帮助?
只是监听一个 80 端口的 HTTP 服务器是否还有必要使用 I/O 复用技术(select
/epoll
)?
3. 操作系统层(协议栈)handle 的通信细节
“客户端链接被重置”5,“无法访问此网站”,等等。
正如前言所提到的,服务器端编程情况比客户端编程要复杂地多。
关于这方面不同的 socket 通信发生情况参见 《UNIX网络编程卷1:套接字联网API》一书有详细讨论。这节主要说明几个我了解到的重要要点。
3.1 FIN 与 RST
服务器没有“安全关闭”(主动,直接调用
close
)是浏览器出现“客户端链接被重置”的原因。
在 socket 关闭时,基于 TCP 编程的 socket 直接调用 close
称不上“健壮”。但是这里不谈如何安全地关闭 socket,而是谈谈安全地关闭 socket 和不安全关闭 socket 的不同之处。
我们知道 TCP 在建立链接的时候有“三次握手”,在断开的时候有“四次握手”。这里只谈断开,所以用下图6再来唤醒四次握手的记忆:
现在将上图中的“服务器”和“客户”对调,考虑由服务器这一段“主动关闭”的情况。
对于操作系统来说,当(服务器端)程序调用了 close
系统调用函数之后,系统的 TCP 协议栈发送 FIN 封包,随后期望链接对端发送 ACK,FIN,此时操作系统还未完全扔掉这个 socket,但是应用层已经无法操作这个 socket 了5,如果对端确实发送了 ACK + FIN,那么操作系统最后响应 ACK,最终这条 TCP 通道完全关闭。
但是,如果程序调用了 close
之后,对端(客户端)仍然发送其它数据包,此时程序已经无法操作 socket 的 fd,完全由操作系统自动发送 RST 给对端(客户端)7;因此,一旦对端(客户端)调用了 recv
函数后,将返回错误,我们会在对端上看到 Connection reset by peer 错误提示8。
3.2 backlog 与最大同时处理的 conn_fd 数
因为 CSDN 服务器重置连接导致浏览器崩溃,本节原本大约 3000 字的内容无法找回。
应该再也写不出原本那么详细了,所以以下仅提及要点。
无法访问此网站 是因为服务器端的连接请求等待队列占满之后发生的。
但是勿以为调大等待队列就能增大并发响应请求数。
3.2.1 关于 backlog
父进程 accept -> fork 重复过程
理解 backlog:
单进程,单线程下,进程在其它位置 - 并非进入 accept 函数调用,由系统代为处理连接请求,将连接请求放入等待队列。等待队列满,系统(服务器)拒绝新到的客户端连接请求。
注:单进程,单线程 + not in accept,等待队列满,客户端发生的情况:
假设 backlog 值设定为 5;
多进程服务器逻辑下,理论上可视为客户端连接请求到来的时刻,服务器进程总是处于 accept 函数调用的状态。所以即使 backlog 很小也没有关系,客户端请求总是能够马上被接受处理。
理论上服务器进程能够同时处理的客户端请求没有限制(仅受系统资源耗尽限制)。
3.2.2 一个 listen 的 socket 可以建立(accept)的最大连接数
理论上系统中同时正在运行的子进程数量“不影响父进程调用 accept 函数允许客户端连接”。
陷阱:
但是在使用上 select 之后的服务器端,会诡异地在 select 处阻塞。
实际上观察到的现象,Ubuntu 系统环境 + mini_httpd 进程 最多有 6 个子进程 handle client(accept 了 6 个 conn_fd),然后父进程阻塞在 select 调用的地方 - 无法调用 accept。
fork 6 个同时正在运行的子进程之后,在调用 select 的位置阻塞!!!
删除 select
函数,如预期在 accept
处阻塞。但是当 fork 的子进程达到 9 个在同时运行,进程就在 accept
阻塞,即使有新的 connect 在连接队列中也不会 accept,直到开始有子进程退出,才会 accept!
– 原因未知!
可能和惊群效应有关?accept 只唤醒一个进程?
在“使用只读方式open
文件得到 fd,select 判断 fd 是否可读,可读即 fork 的测试”,没有 fork 出来同时运行的子进程数限制。
所以,这或许和 连接等待队列中的 connect,到“可读状态”中间有着系统级的实现细节问题。
更新之后,以上分析最后结论是错误的,但是依然保留。因为“惊群效应”在其它 socket 并发模型中是需要考虑的。
上述现象原因与测试的时候如果是使用浏览器多标签打开网站并发测试有关;参见下文。
问题:
一个(服务器)进程能够“同时”处理的最大套接字数?
相关原因9 ?:
如果不知道浏览器的这个行为,在调试的时候将会十分诡异!
注意到本文上面说到删除 select,直接使用 accept 阻塞 -> fork 模型,最大 9 个后台同时运行的进程。现在回想起来,那是因为使用了 firefox 浏览器。
因此 firefox 其实也有这一限制。在调试时要十分小心。
如果手写一份 client 用于测试并发应该就不会出现这一问题。
具体答案与分析10 ?:
注:受操作系统设置和内存相关关系限制。
x. 其它
client socket 编程可以指定(绑定)自己使用的端口吗?
– 答案是可以的,调用 bind 函数即可,但是通常不会这么干,而是在 connect 时由操作系统分配可用的 port。
y. 调试技巧与测试 -n/a
y.1 wireshark - n/a
y.2 tcpdump - n/a
y.3 关于测试
测试并发不要使用浏览器来手动测试。因为浏览器有其限制性行为(见 3.2 节)。
写出 client 测试代码来运行测试!
附
socket 编程与TCP 的三路握手关系:
Reference
对于网络编程这一块,前面已经提到相关书籍,这里做一下整理:
-
《Head First C》
-
《UNIX环境高级编程》
-
《TCP/IP 网络编程》
-
《UNIX网络编程卷1:套接字联网API》(第3版)
其中 1 和 2 是 C 语言基础和 *nix11 环境编程基础。这是在学习网络编程的前提。
注:在我这里不会看到任何在 Windows 上编程的相关内容,所以个人的博客和推荐书籍皆指 *NIX 系统环境。