libcurl 撸记

废话不讲,直奔主题

1. 基本流程
首先在使用这个库的任何操作前,最先进行的一个操作就是全局初始化。当然,相对的,在结束后也需要全局的销毁。这两个函数在开始的时候最好就直接先拍上,肯定不会浪费的。

然而,这似乎并不是必须的,从 libcurl 提供的 examples 来看,并不是所有情况都有用这两个函数。具体情况就不太深究了,写上也没什么损失。根据官方资料,每个程序(application)都要执行一次。

2. 同步请求
同步请求就是使用 easy 系列函数,基本的操作流程就是:
初始化 → 设置属性 → 执行 → 获得返回数据 → 销毁
这里每一步都是阻塞式的,perform 这步实际发出 http 请求,收到回复前都会阻塞住。

3. 异步请求

异步请求就要复杂了,需要同时使用 easy 系列函数和 multi 系列函数。
multi 负责管理所有的添加进来的 easy 指针,然而它主要做的工作是异步的把所有加进来的 easy 都 perform 掉。最终取数据的时候,它会返回 easy 的指针,所以取数据还是要使用同步方式的操作。并且,需要自己手动调用 select 来执行异步操作的。
最终可以实现所有操作不会把线程阻塞住。
因为异步,所以流程并不固定,大概可以这样描述:
固定流程:初始化 → 循环[ 开始请求 → 取 fd_set → 执行 select → 取消息 ] → 销毁
添加请求:生成一个 easy 请求句柄 → 将指针添加给 multi
获取返回:通过 multi 取出的消息拿到 easy 指针 → 通过这个 easy 指针读取返回数据
这样生成请求和读取返回信息可以是完全异步完成的。

4. 返回数据接收
通过 curl_easy_getinfo 只能获取到返回的 response code 不能获得返回的数据。如果需要通过 http 请求获得数据返回或者下载文件的话,需要在 CURL 指针的设置中,挂一个接收数据的函数。

这个函数的声明是这样的:

buffer 数据指针
size 指针单个偏移的大小
nitems 指针能偏移多少个
outstream 由用户传入的指针 CURLOPT_WRITEDATA
返回值:本次接收的数据大小

然后我们在这个函数中来处理返回来的数据。
这里需要注意的是这个函数不一定是一次性返回全部的数据,所以要妥善处理才行,直到 curl_easy_perform 返回或者 curl_multi_info_read 拿到这个句柄才能认为返回数据已经写入完毕,否则接收到的数据都可能是不完整的。
下面是我下载部分的处理:

不过如果不需要特殊处理,可以不需要 CURLOPT_WRITEFUNCTION 传入一个函数,只需要 CURLOPT_WRITEDATA 传入一个 FILE* ,下载的数据会自动写入文件。(至少我使用的版本是这样的)

5. POST 和 GET
libcurl 默认的模式是 GET ,传参数只需要写在 url 后边就可以了,只要不做特殊的设置,就是以 GET 形式向服务器发请求。
那么如果我们要发 POST 呢?
首先,我们要显示的设置我们这个请求是一个 POST 请求:

POST 的参数不是接在 url 后边的,而是要单独传递:

这样设置好,perform 后就向服务器发送 POST 请求了。

6. 分块下载
使用 http 协议下载文件,数据是绝对顺序传输的,如果客户端或者服务器端在传输过程中发生了一些问题,就会导致 TCP 层重试,导致下载效率降低。所以如果只是直接单纯的顺序传输下载,效果是比较差的。因此同时开多个连接,同时下载文件的不同部分,会对网络的利用率更高一些。
下载文件的一个块,在 http1.1 中引入了 Range 参数。这个参数就指定了这个连接负责下载文件的哪个部分。

写文件的部分只需要每个连接各自拿一个文件句柄 seek 到起始位置就可以了。

然而如何让所有连接同时工作起来呢?
有两个选择:1) 每个连接各自使用 curl_easy_perform 发起请求;2) 加入到 multi 中异步请求。
1) 因为是会阻塞,所以单线程是无法完成一起工作的,所以如果选择这个种简单的方式,就需要手动开多个线程来完成。这里需要注意的是每个线程使用自己的 CURL* 基本上也不会出什么乱子。
2) 这种方式会比较省心一些,不用处理复杂的线程同步问题。只是 multi 异步架构起来会稍微复杂。
我个人倾向于选择方案2,因为没有太大必要折腾多线程来处理下载的问题。线程开的再多也并不会使网线带宽变大,所以也没必要花心思处理多线程数据同步问题。
方案2,只要处理好最大连接数,以及下载结束后的收尾,在性能和稳定性方面还是更靠谱一些的。

7. 断点续传
要实现断点续传首先我们应该确定断点,必须知道下载的文件断在了什么地方才能知道从哪里开始继续下载。
这里可以分两种情况来考虑
1) 不分块顺序下载,那么文件已经下载好的部分就是断点。那么下一次只需要取出已下载文件的大小,从这里开始继续下就可以了。这可以不必使用 Range 参数,可以使用 RESUME_FROM 设置起始点。不过对于大文件,不适用分块的话效率会比较低。
2) 分块下载,对于分块下载到同一个文件的情况,读取文件已完成部分大小的方式就显然不可行了。这里就需要借助另外一个文件来记录文件每块的下载进度了。在写被下载文件的同时再写一个记录进度的 log 文件,如果正常完成了下载,就可以将 log 文件删除了,如果没有完成,下一次可以先读取 log 文件,通过 log 文件中的记录继续剩余分块的下载。
那么再写一个 log 文件是不是会有性能问题呢?答案是肯定会多消耗一些磁盘的写带宽,但是对于下载这件事的影响有多大呢?理论上不会有什么影响,毕竟磁盘的写入带宽要远远大于网络传输带宽,所以对实际效果的影响应该是不大的。
据我观察迅雷等下载软件也是会给每个下载的文件多写一个 log 文件的。当然我并不确定它是不是用这个 log 文件作为断点记录。

写在最后
这篇贴出来的代码都是小片段,因为自己撸出来的代码确实挺丑,就不拿出来丢人了。贴出来除了多占篇幅,也起不到太大作用。
我封装了一个 http client 放到了 github 有需要的同学自取。实现了 http post 、http get 以及分块下载和断点续传。
点击前往:ftxHttpClient

发表评论

电子邮件地址不会被公开。