1. web 客户程序
-
目标:
-
1) web 客户程序建立与某个 web 服务器的 HTTP 连接,然后获取主页(这只是一个开胃菜,测试我们的 web 程序是否正常工作)。
2) 同时请求多个网络资源,并下载。(看起来很像多线程爬虫,但是我们不使用多线程,而是非阻塞 connect)
我们的 web 客户程序命名为就命名为 web,使用方法如下:
$ ./web <同时允许最大连接数> <主页> <主页文件名> <文件1路径> [文件2路径] ...
例如:
$ ./web 3 www.zcool.com.cn /index.html /img/1.jpg /img/2.jpg
2. 程序设计
- 请示主页部分
1. 建立连接
2. 发送 GET 请求到服务器:
GET /index.html HTTP/1.1
Host: www.zcool.com.cn
Connection: close // 关闭 Keep-Alive,服务器发送完数据后主动关闭连接。
3. 接收服务器数据,并打印到屏幕。
- 下载资源部分
设计结构体:
struct file {
char f_name[256]; // 要下载的资源路径
char f_host[256]; // 主页
int f_fd; // 套接字描述符
int f_flags; // 当前状态,有四种值,分别是 { 0, F_CONNECTING, F_READING, F_DONE }。
};
// 假设我们下载 10 资源
初始化 struct file files[10];
while(1) {
使用非阻塞i/o, 同时建立多个连接,每一个 f_flags = F_CONNECTING.
select 监听套接字
for (f in files) { // 遍历所有文件
if (f.f_flags == F_CONNECTING) {
// 检查连接是否成功或失败。使用我们上一篇文章用到的知识,主要是 getsockopt 函数
如果连接成功,则发起 GET 请求,同时 f_flags = F_READING.
如果连接失败,f_flags = F_DONE;
}
else if (f.f_flags == F_READING) {
// 下载资源
nr = read(f.f_fd, buf);
if (nr == 0) {
对端关闭, f.f_flags = F_DONE;
}
}
}
}
3. 程序代码
本文使用的程序托管在 gitos 上:http://git.oschina.net/ivan_allen/unp
如果你已经 clone 过这个代码了,请使用 git pull
更新一下。本节程序所使用的程序路径是 unp/program/nonblockio/web
.
main 主函数主要思路是按照第 2 节中的结构来写的,程序主要有这几个函数:
- homePage:相当于第 2 节中的请求主页部分
- get:请求资源并打印到屏幕,这个函数只被 homePage 使用
- startConnect:发起非阻塞 connect,无论成功与否,直接返回
- request:发起 GET 请求后立即返回,同时 f_flags = F_READING
4. 实验
实验中为了方便启动程序,我所有命令写到了脚本 run.sh 中:
图1 run.sh 脚本
run.sh 脚本可以传递一个参数,表示同时最大允许的连接数,默认情况下为 3.
4.1 小试牛刀
先运行一下 run.sh 看看:
图2 启动 run.sh,默认最大 3 个连接
可以看到执行完后,耗时为 2.158 秒。
4.2 不同连接数的测试
./run.sh 1
图3 同时只允许 1 个连接,时间:5.297s
./run.sh 2
图4 同时只允许 2 个连接,时间:2.886s
./run.sh 3
图5 同时只允许 3 个连接,时间:2.178s
./run.sh 6
图6 同时只允许 6 个连接,时间:1.336s
./run.sh 12
图7 同时只允许 12 个连接,时间:1.040s
./run.sh 24
图8 同时只允许 24 个连接,时间:0.734s
./run.sh 33
因为我们只同时下载了 33 个文件,所以多余的连接也没什么用。
图9 同时只允许 33 个连接,时间:0.890s
这里使用表格来记录一下,下载 33 个文件,使用不同的连接数所消耗的时间:
最大连接数 | 时间(秒) |
---|---|
1 | 5.297 |
2 | 2.886 |
3 | 2.178 |
6 | 1.336 |
12 | 1.040 |
24 | 0.734 |
33 | 0.858 |
从结果可以看到,连接数超过 6 以后,并没有多大提升速度的余地了。连接数从 1 到 2,速度几乎翻倍。
最后,使用非阻塞版本 connect 所付出的代价就是得编写复杂的程序,因此,比较推荐的是使用多线程版本,在 unp 一书中得出的结论是多线程版本的效率并不比非阻塞 connect 差多少,下载速度上大约就几百毫秒的差距,这并不会给客户带来什么影响。
5. 一些可能会踩的坑
客户端一味的追求并发连接数,可能会导致性能下降,特别是在网络状况不好的时候。前面我们介绍过慢启动和拥塞避免算法的细节。
如果同时发起多个连接,其中某个连接遇到 TCP 报文丢失,很可能已经网络已经拥塞,于是执行乘法减小算法(Multiplicative Decrease),但很可惜的是,其它连接并不会得到通知,这种情况下,其它连接仍然还在无脑的一次发送多个 TCP 报文,接下来导致网络更加拥塞。
6. 总结
- 掌握使用非阻塞 connect 同时发起多个连接的方法
- 客户端并发连接,可能会遇到性能的问题
练习:使用多线程改写 web 客户端。