libcurl 撸记

废话不讲,直奔主题

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

// 全局初始化 
curl_global_init(CURL_GLOBAL_ALL); 

// your code ... 

// 全局清理 
curl_global_cleanup();

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

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

// 初始化
CURL *handle = curl_easy_init();

// 设置
curl_easy_setopt (handle, CURLOPT_URL, url);
curl_easy_setopt (handle, CURLOPT_HEADER, 0L);
curl_easy_setopt (handle, CURLOPT_NOBODY, 1L);
curl_easy_setopt (handle, CURLOPT_NOSIGNAL, 1L);

// 执行
if (curl_easy_perform (handle) == CURLE_OK) {
    // 获得返回数据
    curl_easy_getinfo (handle, CURLINFO_RESPONSE_CODE, &responseCode);
    curl_easy_getinfo (handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &file_length);
}

// 销毁
curl_easy_cleanup (handle);

3. 异步请求

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

/******* 固定流程结构 *******/

// 初始化
CURLM* curlm = nullptr;
curlm = curl_multi_init();

// 开始异步请求 (不阻塞)
curl_multi_perform(curlm, &handles);

// 取 fd set
FD_ZERO(&read_fd);
FD_ZERO(&write_fd);
FD_ZERO(&exec_fd);
curl_multi_fdset(curlm, &read_fd, &write_fd, &exec_fd, &max_fd);

// 执行 select
select(max_fd+1, &read_fd, &write_fd, &exec_fd, &T);

// 取消息
CURLMsg* msg;
msg = curl_multi_info_read(curlm, &msgs);

// 销毁
curl_multi_cleanup(curlm);
/******* 添加请求 *******/ 

// 初始化 
CURL* curl = curl_easy_init(); 

/* your code ... */ 

// 添加 
curl_multi_add_handle(curlm, curl);
/******* 取返回的数据 *******/

/* msg = curl_multi_info_read(curlm, &msgs); */

// 取出 easy 指针
CURL *e = msg->easy_handle;

// 读取返回数据
curl_easy_getinfo(e, CURLINFO_PRIVATE, &opt);
curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &responseCode);

/* your code ... */

// 清理
curl_multi_remove_handle(curlm, e);
curl_easy_cleanup(e);

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

static size_t requestWriteData(void *ptr, size_t size, size_t nmemb, void *stream)
{
    /* your code ... */
}
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, requestWriteData);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, stream);

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

/* 回调函数指针的声明 */
typedef size_t (*curl_write_callback)(char *buffer, size_t size, size_t nitems, void *outstream);

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

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

static size_t downloadWriteData(void *ptr, size_t size, size_t nmemb, void *stream)
{
    DownloadBlock *block = (DownloadBlock *) stream;
    size_t written = fwrite(ptr, size, nmemb, block->fd);
    return written;
}

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

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

curl_easy_setopt(curl, CURLOPT_POST, 1L);

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

curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "a=1&b=22&c=3333");

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

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

/* 100-1000 */ 
sprintf(range, "%ld-%ld", start, end); 
curl_easy_setopt(curl, CURLOPT_RANGE, 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

libgit2使用教程(三) git commit

首先去看 API 中关于 commit 的部分都有哪个函数比较像,但是意外的发现了 API 中并没有官方 101-samples 中的 git_commit_create 函数,搞得我好意外。不过据我所知,实现 commit 操作肯定是要使用这个 git_commit_create 函数了。那咱们先去头文件看看它需要什么参数:

/**
* Create new commit in the repository from a list of `git_object` pointers
*
* The message will **not** be cleaned up automatically. You can do that
* with the `git_message_prettify()` function.
*
* @param id Pointer in which to store the OID of the newly created commit
*
* @param repo Repository where to store the commit
*
* @param update_ref If not NULL, name of the reference that
* will be updated to point to this commit. If the reference
* is not direct, it will be resolved to a direct reference.
* Use "HEAD" to update the HEAD of the current branch and
* make it point to this commit. If the reference doesn't
* exist yet, it will be created. If it does exist, the first
* parent must be the tip of this branch.
*
* @param author Signature with author and author time of commit
*
* @param committer Signature with committer and * commit time of commit
*
* @param message_encoding The encoding for the message in the
* commit, represented with a standard encoding name.
* E.g. "UTF-8". If NULL, no encoding header is written and
* UTF-8 is assumed.
*
* @param message Full message for this commit
*
* @param tree An instance of a `git_tree` object that will
* be used as the tree for the commit. This tree object must
* also be owned by the given `repo`.
*
* @param parent_count Number of parents for this commit
*
* @param parents Array of `parent_count` pointers to `git_commit`
* objects that will be used as the parents for this commit. This
* array may be NULL if `parent_count` is 0 (root commit). All the
* given commits must be owned by the `repo`.
*
* @return 0 or an error code
* The created commit will be written to the Object Database and
* the given reference will be updated to point to it
*/
GIT_EXTERN(int) git_commit_create(
    git_oid *id,
    git_repository *repo,
    const char *update_ref,
    const git_signature *author,
    const git_signature *committer,
    const char *message_encoding,
    const char *message,
    const git_tree *tree,
    size_t parent_count,
    const git_commit *parents[]);

·id 返回一个 OID 的指针,这个 OID 表示的就是新创建的这个 commit
·repo 仓库指针
·update_ref 需要提交的目标引用,使用当前分支的引用就用 “HEAD” 就可以了
·author 作者 git_signature 指针。git_signature 记录作者的名字、email、和一个时间
·committer 提交人也是 git_signature 指针,跟上边可以是同一个
·message_encoding 这次提交说明信息的字符集
·message 这次提交的完整说明
·tree 我们需要传入一个 git_tree 实例的指针,这个 tree 对象必须属于这个仓库。这个 ·tree 将是我们下边要解决的第一个问题
·parent_count 这个 commit 的父级的数量,如果是 merge 后的 commit 会有两个甚至以上,咱们暂时先不考虑分支的事情,所以只玩儿1个的
·parents 是一个 git_commit 指针的数组,表示上一级的提交,如果是第一次 commit 数组应该是空的,同时 parent_count 应该是0。如果不是,咱们就得先把爸爸找出来,这将是下边要解决的第二个问题

1. tree:
这个 tree 实际上就是把 index 的 entry 写到一个 tree 中,最终把这个 tree 提交。从 API 文档可以找到 git_index_write_tree 这个函数。它的作用就是写一个 tree,所以就可以写出下面的代码:

git_oid new_tree_id;
git_tree *new_tree = nullptr;

// write index to tree
error = git_index_write_tree(&new_tree_id, index);
if (error < 0)
{
    const git_error *e = giterr_last();
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
    goto SHUTDOWN;
}
else
{
    git_index_free(index);
}
git_tree_lookup(&new_tree, rep, &new_tree_id);

2. parent:
因为我们还先不考虑 merge 的情况,所以父级 commit 只有一个,这个父级实际上就是 head 引用,所以我们第一步先拿出 head。

git_reference* ref_head = nullptr; 
git_repository_head(&ref_head, rep);

这里我们要考虑一个情况:在一个全新的仓库的情况下,是没有之前的 commit 的,因此,取出来的 ref_head 会是空的。所以要做一个判断:

size_t parent_count = 0;
git_commit *parent_commit = nullptr;
const git_commit *parents[] = {nullptr};

// has parent
if (error == GIT_OK)
{
    // get parent commit
    git_commit_lookup(&parent_commit, rep, git_reference_target(ref_head));
    parents[0] = parent_commit;
    parent_count = 1;
    git_reference_free(ref_head);
}
else if (error != GIT_EUNBORNBRANCH)
{
    const git_error *e = giterr_last();
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
    goto SHUTDOWN;
}

这样我们就拿到了我们需要的 tree 和 parent commit ,下面就是签名,然后提交了:

git_signature* me = nullptr; 
git_signature_now(&me, "XiaochenFTX", "xiaochenftx@gmail.com”);
git_oid new_commit;
error = git_commit_create(&new_commit,
                                    rep,
                                    "HEAD",
                                    me,
                                    me,
                                    "UTF-8",
                                    "commit message",
                                    new_tree,
                                    parent_count,
                                    parents);

这样 commit 操作就完成了。

示例代码是 sample3
同样放在 github 的 libgit2_samples 中

libgit2使用教程(特别篇)几个基本概念说明

本来打算这篇写 commit 但是感觉涉及的一些概念不细说一下可能不太好,因此花了一下午时间整理了这些基础概念。对这些概念理解之后会比较有利于理解 git 仓库的结构,对于理解 git 系统的工作原理也会有很大的帮助。所以,这篇没有任何功能实现,纯描述概念,可能有些无聊,对于对下面这些完全没有概念同学,还得代码敲起来,自己动手探索才能理解到其中的精髓。

  • blob
  • oid
  • tree
  • commit
  • reference
  • branch
  • index

1. blob:

struct git_blob {
    git_object object;
    git_odb_object *odb_object;
};

blob 对象直接包含 git_object ,但是 git_object 这个概念是封装起来的,我们一般情况下是接触不到的。它将直接对应存放在仓库中的数据文件,对于 blob 我们就把它直接理解成我们文件夹中的文件就可以了。它是整个 git 仓库管理的基础单位,作为实际文件的代表,可以说 git 的版本管理就是花式玩儿 blob 。

2. oid:
指的是 git_object 的 id。每个独立的 git_object 都有一个 id,id 相等则可以判定是同一个对象。
它存储的是一个 SHA-1 值,20个字节大小,每个字节存放一个16进制数。如果转成字符串,则是一个40个字符长的字符串,两个字符表示一个16进制数。
相互转换的函数:
git_oid_fromstr 把 SHA-1 转成 oid
git_oid_tostr 把 oid 转成 SHA-1

3. tree 和 tree entry:
tree 就如它字面上的意思,是一个树形数据结构,tree entry 就是这个树的节点。

size_t tree_entry_count = git_tree_entrycount(tree);
std::cout << "tree entry count: " << tree_entry_count << std::endl;
for (size_t i = 0; i < tree_entry_count; ++i)
{
    const git_tree_entry *te = git_tree_entry_byindex(tree, i);
    const char *te_name = git_tree_entry_name(te);
    const git_oid *te_oid = git_tree_entry_id(te);
    const char *teid = git_oid_tostr_s(te_oid);
    git_otype otype = git_tree_entry_type(te);
    git_filemode_t filemode = git_tree_entry_filemode(te);
    std::cout << "tree entry file name: " << te_name << " \toid: " << teid << " \totype: " << otype <<
            " \tfilemode: " << filemode << std::endl;
}

可以用上边的方法简单遍历 tree 的一层,这些 entry 可能是没有子节点的“文件”,也有可能是还有子节点的“文件夹”,也就是说,tree entry 还可以作为 tree 持有自己的 tree entry。

if (otype == GIT_OBJ_TREE)
{
    git_tree *leaf_tree = nullptr;
    git_tree_lookup(&leaf_tree, rep, te_oid);
    size_t leaf_entry_count = git_tree_entrycount(leaf_tree);
    for (size_t j = 0; j < leaf_entry_count; ++j)
    {
        const git_tree_entry *leaf_te = git_tree_entry_byindex(tree, j);
        const char *leaf_te_name = git_tree_entry_name(leaf_te);
        const git_oid *leaf_te_oid = git_tree_entry_id(leaf_te);
        const char *leaf_teid = git_oid_tostr_s(leaf_te_oid);
        git_otype leaf_otype = git_tree_entry_type(leaf_te);
        git_filemode_t leaf_filemode = git_tree_entry_filemode(leaf_te);
        std::cout << "\tleaf tree entry file name: " << leaf_te_name << " \toid: " << leaf_teid << " \totype: " <<
                leaf_otype << " \tfilemode: " << leaf_filemode << std::endl;
    }
}

通过 git_filemode_t 我们可以看出,entry 就可以表示一个 blob。

/** Valid modes for index and tree entries. */
typedef enum {
    GIT_FILEMODE_UNREADABLE = 0000000,
    GIT_FILEMODE_TREE = 0040000,
    GIT_FILEMODE_BLOB = 0100644,
    GIT_FILEMODE_BLOB_EXECUTABLE = 0100755,
    GIT_FILEMODE_LINK = 0120000,
    GIT_FILEMODE_COMMIT = 0160000,
} git_filemode_t;

这里有一个要注意的地方,这个枚举的值是以8进制表示的,直接以10进制数方式打印,跟这个字面值是不一样的。

4. commit:
commit 是版本的基本单位,版本库的记录都是以 commit 为基础的,谁提交的、什么时候提交的、提交的说明信息、提交时的 tree 的状态都是由 commit 管理的。它还要知道它的父级 commit 是哪个或者哪几个,最终 commit 们会组成一个有向图。
我们要找某一个版本,就是去找这个版本的 commit ,有了这个 commit 我们就可以通过它调出那个版本当时的 tree,然后由 tree 管理的所有文件就都可以找到了。可以说 commit 是 git 版本管理的核心,其他所有的概念都是围绕着 commit 展开的。

// 通过 commit 获得 tree 
git_tree* tree = nullptr; 
git_commit_tree(&tree, commit);

5. reference:
这个引用怎么用语言来描述我一直很纠结。reference 引用的是一条 commit 链——更准确的说不应该是链,因为 commit 网上追溯有可能有多个父 commit 的情况。它实际上是一个倒过来的树,reference 是它的根节点,commit 的 parent 就是子节点。这是为了方便向上追溯版本。
实际上每多一个分支,就相当于多了一个 reference ,reference 就包含着这个分支的最新的一个 commit。
git 系统也预置了几个重要的 reference 名称,用于方便索引。其中最重要的就是 HEAD 了,在工程的 .git 文件夹就可以看到这个文件,打开看看,它只有一个指向一个 refs 文件夹的路径,实际上它指向的就是当前所在的分支。所以,通过 HEAD 就可以非常方便的找到当前的 reference。

git_reference *head_ref = nullptr;
// 当前 head 引用
git_repository_head(&head_ref, rep);

const git_oid *oid_ref = git_reference_target(head_ref);
const char *roid = git_oid_tostr_s(oid_ref);
    
git_commit *commit = nullptr;
// 这个分支中最新一个 commit
git_commit_lookup(&commit, rep, oid_ref);

6. branch:
在上边 reference 的部分我多次提到了“分支”这个词,然而了解一些 git 命令行工具的同学肯定知道,git 工具分支的命令是 branch ,而且日常沟通也都是以 branch 的概念来代表分支的。
这没有错,git 概念中的分支就是 branch ,同时 libgit2 这个库中也有 branch 这个概念,它所指的实际上就是 reference 。就是给 reference 起了个名字,这个名字是方便记忆的,而不是 reference 用的一个路径。

GIT_EXTERN(int) git_branch_lookup( git_reference **out, git_repository *repo, const char *branch_name, git_branch_t branch_type);

从这个函数声明就可以看出,通过一个分支名返回是一个 reference ,同时,在函数实现的内部,也是通过 git_reference_lookup 实现的。因此可以看出,branch 实际上就是一个有名字的 reference 。

git_reference *branch_ref = nullptr;
// 通过分支名取到 reference
git_branch_lookup(&branch_ref, rep, "master", GIT_BRANCH_LOCAL);
const git_oid *oid_branch = git_reference_target(branch_ref);
const char *boid = git_oid_tostr_s(oid_branch);

如果此时 HEAD 就是 master 分支的话,roid 和 boid 的值是相等的。

7. index 和 index entry:
index 索引的是当前工作区中未提交的内容。完整的 commit 操作就是将 index 中的内容写到一个 tree ,然后用这个新的 tree 创建一个新的 commit ,然后更新 reference 。而在这之前的 add 操作,就是将改动更新到当前 index。
index entry 就像 tree entry 也是作为文件的代表,而不一样的地方就是,单独的文件夹不再是 tree 了。可以通过它的 mode 属性,看出每一个文件是一个 blob ,submodule 是另外一个 blob。
实际上在没有改动的情况下 index entry 和 tree entry 是相同的 blob 。只有有新的 add 才会使同一个文件对应的 blob 不同。

git_index *index = nullptr;
git_repository_index(&index, rep);
const git_oid *indexoid = git_index_checksum(index);
const char *ioid = git_oid_tostr_s(indexoid);
std::cout << "index oid: " << ioid << std::endl;
size_t count = git_index_entrycount(index);
std::cout << "entry count: " << count << std::endl;
for (size_t i = 0; i < count; ++i)
{
    const git_index_entry *entry = git_index_get_byindex(index, i);
    const char *entrypath = entry->path;
    git_oid oid = entry->id;
    const char *id = git_oid_tostr_s(&oid);
    std::cout << "entry: " << entrypath << " \toid: " << id << " \tmode: " << entry->mode << std::endl;
}

初撸WordPress小记

撸这个 WordPress 站有一段时间了,也解决了一些比较蛋疼的问题,大晚上的一股鸡血上脑,就撸了一发小文,记录一下这段时间解决的几个比较有用的问题。

1. 中文标签和分类的问题
偶然发现直接点击中文标签或分类链接,wordpress 是根本找不到相关内容的。相当于访问 https://ftxtool.org/index.php/category/技术随笔/ 的时候服务器给我返回的是 404 ,然而,使用 https://ftxtool.org?category=技术随笔 传参数的方式却可以正常访问到,估计是 url 地址重定向时候的问题。
所以,网上能找到一类解决方案就是修改 rewrite.php 的代码。然而,就像某个复制粘贴的博主说的那样,根本没有找到别人说的那块代码在哪。应该是经过很多版本的更新,那部分代码早就不一样了,然而这种复制粘贴的精神依然屹立,永不过时。
另外一个解决方案,是修改 class-wp.php ,把 $_SERVER[‘PATH_INFO’] 和 $_SERVER[‘REQUEST_URI’] 转码,把 GBK 转成 UTF-8 。然而,经过测试好像并没有什么卵用。
方案三,修改别名。把别名改成一小串英文,亲测有效。具体操作方法就是:
仪表盘→分类目录/标签→选一个具体的分类或标签名字,点下边的快速编辑→别名(随便写个英文的名字),然后更新就好了。
方案四,插件。方案三虽然可以解决问题,但是每个标签都要手动改一遍还是有些蛋疼的。找来找去找到个插件,用起来还不错。插件名叫:IIS Chinese Tag Permalink 直接去安装插件里搜索这个名字就能找到。目前用起来还没啥问题,应该算比较靠谱的一个方案。然而还是有个别标签一直 404 ,只能通过改别名的办法解决,不知道有没有更好一些的插件可以解决这个问题。

2. sitemap
赵家百度的 sitemap 插件简直渣到爆,跟 google 的 XML-Sitemap 插件简直没法比。从设置页面就已经被秒成渣渣了。最蛋疼的是生成 sitemap 文件,我去点了按钮也没反应,一直是:没有生成 sitemap 文件。麻痹的没有任何提示了,只好去代码里下 log。
原来生成文件是在 wordpress 的跟目录,代码注释里竟然叫我把目录整个权限设成777。简直日了狗,你家运维都是这么干活的么?
然后,还是手动在根目录创建了个 sitemap 文件,然后加个写权限,就可以用了。
然而,还是 google 家的 sitemap 好使。自己自动生成好之后还能自动通知 google 和 bing 。

3. 友情链接
新版本添加友情链接还是蛮蛋疼的,网上能搜到的基本上都是老版本的方案,或者 link manager 插件,但是插件并不好使。真心想不明白,这帮人复制来复制去也没有经过验证,然后还被搜索引擎排的那么靠前。
新版本添加链接其实很简单,只要在侧边栏加上一个自定义菜单就可以了:
外观→自定义→小工具→侧边栏
点击【添加小工具】选择【自定义菜单】
然后选上自己添加好链接的菜单就可以了。

然而怎么添加一个菜单呢?
外观→菜单
点击【创建新菜单】
然后 填一个名字
然后 在【自定义链接】中添加
能看懂提示文字基本上不会错。

注意:这个菜单不要勾选 主菜单 。

libgit2使用教程(二) git add

主要内容:
1. git add <path>
2. git add .

示例代码:sample2

一、 open
上一篇初始化了一个 git 仓库,接下来开始使用我们本地的 git 仓库。几乎所有的操作都需要使用一个 git_repository 指针,所以第一步,我们先初始化这个指针。
如果是新创建一个仓库的话,使用上一篇的 git_repository_init 就可以了。如果是一个已有的仓库呢?
就用 git_repository_open 这个函数。这个函数很简单,两个参数,参数一是返回 git_repository 指针,参数二是仓库地址。

const char *path = "/Users/xiaochen/Documents/data";
git_repository *rep = nullptr;
int error = 0;
error = git_repository_open(&rep, path);

二、 add
add 就开始有些复杂了,我们要分情景讨论这个事。
1. add 指定文件
通过官方API我们找到了一个函数 git_index_add_bypath ,主要意思就是:通过指定 path 添加一个文件到 index [附一]
只有两个参数,参数一是 git_index 指针,参数二是指定文件的路径。
所以,我们要先拿到 git_index 的指针:

git_index* index = nullptr; 
git_repository_index(&index, rep);

拿到了 index 指针,就可以 add 了:

git_index_add_bypath(index, "file");

然后运行一下,没有报错,然后到我们的仓库调用 git status 看一下。

On branch masterInitial commit

Untracked files:
(use “git add …” to include in what will be committed)

file

nothing added to commit but untracked files present (use “git add” to track)

file 并没有被 add 进来,是什么原因呢?

其实,git_index_add_bypath 操作并没有失败,只不过它是对内存的操作,在程序结束的时候,并没有把内存中的 index 写到磁盘,如果要把 index 写到磁盘,需要调用一个函数:

/* Write the in-memory index to disk */ 
git_index_write(index);

好了,目前完整的 add 的代码就是:

git_index* index = nullptr; 

// get index 
git_repository_index(&index, rep); 

// git add file 
error = git_index_add_bypath(index, "file"); 
if (error < 0) 
{ 
    const git_error *e = giterr_last(); 
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
} 
else 
{ 
    /* Write the in-memory index to disk */ 
    git_index_write(index); 
} 

// 
git_index_free(index);

2. add 全部
使用函数 git_index_add_all 可以把没有添加到 index 的改动全部添加到 index 中。写一个简单的测试代码:

git_index_add_all(index, nullptr, GIT_INDEX_ADD_DEFAULT, nullptr, nullptr); 
git_index_write(index);

有时候对于参数不知道到底要传什么,就可以先传个 null 或者 0,运行看程序给你报什么错,再根据具体的错误找解决方案。不过显然,这段代码的运行结果是符合我们的预期的,我们对这个仓库中所有文件的修改都被 add 到 index 中了。说明参数二 paths 传空,就相当于命令:
git add .
那么,后边两个参数的作用是什么的?
倒数第二个参数是一个函数指针 git_index_matched_path_cb ,而最后一个参数则是传递给这个回调函数的参数。
所以我们给 index 添加改动的时候,是可以通过这个回调函数获得一些信息的:

int index_matched_path_cb(const char *path, const char *matched_pathspec, void *payload)
{
    std::cout << "path: " << path << "\n";
    std::cout << "matched_pathspec: " << matched_pathspec << std::endl;
    return 0;
}

这个回调的参数一是有改动的文件路径,参数二是 git_index_add_all 的第二个参数中和这个文件路径匹配的 pathspec ,第三个参数是前边提到过的自定义参数。
最重要的是它的返回值:
0:正常添加
正数:跳过添加这个文件的改动
负数:直接报错返回,这个时候 git_index_add_all 将直接返回,返回值就是我们这个回调函数的返回值。

所以这个回调函数可以让我们根据自己的情况对批量添加做一个过滤。
然后,翻回头再说一下 git_index_add_all 的第二个参数[附二]。他是一个路径规则,不满足这个规则的文件将直接被跳过。也不会触发上边那个回调函数。满足规则的文件和对应的那一项规则,将成为回调函数的参数一和参数二。
所以调用的代码是这个样子的:

char *strs[1];
git_strarray paths = {nullptr, 0};
    
strs[0] = "dir/*";
paths.strings = strs;
paths.count = 1;
    
error = git_index_add_all(index, &paths, GIT_INDEX_ADD_DEFAULT, index_matched_path_cb, nullptr);
if (error < 0)
{
    const git_error *e = giterr_last();
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
}
else
{
    /* Write the in-memory index to disk */
    git_index_write(index);
}

附一、解释一下 index
index 可以理解为一个存储区,存放被 add 进来的“改动”,add 的文件的改动被添加到 index ,commit 操作把这个 index 添加到本地仓库,可以通过 git rm —cached 从 index 中删除指定“文件”。只要不 commit 我们玩儿的就一直是同一个 index 。

附二、git_strarray
git_strarray 是一个结构体:

/** Array of strings */
typedef struct git_strarray {
    char **strings;
    size_t count;
} git_strarray;

strings 是一个字符串数组,count 是这个数组有多少个元素。
这个结构后边会经常用到,与多个路径相关的参数和返回值,都会使用这个结构。

libgit2使用教程(一)实现 git init

主要内容:
1. 使用 libgit2 的准备工作
2. 构建和运行
3. 初始化一个 git 仓库

示例代码:https://github.com/XiaochenFTX/libgit2_samples

进入正题

开始直接到 libgit2 的 readme 我们首先需要从那里获取一些有用的信息。
官网:https://libgit2.github.com/
API:http://libgit2.github.com/libgit2/
虽然官方文档写的挺水的,不过毕竟可以获得信息的途径就这么少,所以凑合着也还能用。

接下来提到了两个非常重要的函数, init 和 shutdown ,必须保证在任何操作之前调用初始化:

git_libgit2_init();

释放 init 申请的资源,使用:

git_libgit2_shutdown();

这样就可以开始第一个程序了

#include <git2.h> 

int main() 
{ 
    git_libgit2_init(); 
    
    git_libgit2_shutdown();

     return 0; 
}

ok 打完收工。

先把它跑一下看看,如果可以正常跑完,就说明所有的构建基本上都没啥问题了。

当然,这并没有什么卵用。接下来来搞个有意义的代码来结束这篇。
第一个例子就实现一下 git init

const char* path = "/Users/XiaochenFTX/Documents/data";
git_repository *rep = nullptr;
git_repository_init(&rep, path, 0);

打完收工。

当然,良好的编程习惯,在最后不要忘了释放资源。

git_repository_free(rep); 
git_libgit2_shutdown();

接口的详细说明可以去查官方的 API 我会在必要的地方对一些我觉得比较重要的东西进行说明。

git_repository_init 的第一个参数是将初始化好的 git_repository 指针返回,这个库的接口基本上都是这个风格的,以后就不再提这个事了。
这个函数会返回一个错误码,返回值为 0 表示执行成功,小于 0 表示有错误。所以可以通过判断返回值得方式来确定函数调用是否成功。
想知道具体报错信息,可以调用 giterr_last() 这个函数返回一个 git_error 结构体指针。

/**
* Structure to store extra details of the last error that occurred.
*
* This is kept on a per-thread basis if GIT_THREADS was defined when the
* library was build, otherwise one is kept globally for the library
*/
typedef struct {
    char *message;
    int klass;
} git_error;

klass 对应 git_error_t 的枚举值,用于说明是哪部分出的问题。message 是一个人类可以看懂的的说明信息。开发中遇到问题可以把这个信息打印出来,对找问题有很大帮助。

最终完成 sample1 的代码就是这个样子

#include <git2.h>
#include <iostream>

int main()
{
    git_libgit2_init();
    const char *path = "/Users/XiaochenFTX/Documents/data";
    git_repository *rep = nullptr;
    
    // git init
    int error = git_repository_init(&rep, path, 0);
    if (error < 0) {
        const git_error *e = giterr_last();
        std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
        
        goto SHUTDOWN;
    }
SHUTDOWN:
    git_repository_free(rep);
    git_libgit2_shutdown();
    
    return 0;
}

最后解释一下为什么要用 goto
很多同学在学习 C 语言的时候,如果遇到不负责任的老师,都会强调不要用 goto 这个东西,更有甚者干脆讲都不讲。而具体原因不外乎:会让代码逻辑混乱,可读性差,不好调试
当然,造成这些后果,都是在“如果用不好”的前提下的。然而,我认为这个语言特性真的不是不值一提的垃圾,把 goto 用好在一定程度上是可以让代码更美观、更简洁、更易读。
随便举个例子,比如跳出多层循环,难道还要引入一个外层变量再逐层判断吗?在这种情况下那种方式可读性更高?
还有就是在错误发生的情况下,直接跳到函数结尾进行清理。如果不使用 goto ,我见过几种奇葩方案,最突出的应该是用 do{}while(0); 吧,这样强行为了不用 goto 而产生的奇葩行为,我只能呵呵了。
所以,我推荐在必要的时候使用 goto 来使代码更清晰、简洁。当然,也不是随便瞎用,任何工具都有其最佳适用范围,不能矫枉过正。

附一、代码中引用 libgit2:
在根目录的 CMakeLists.txt 中把相关工程文件夹都用 add_subdirectory 加进来,并且指定 libgit2 的 include 为引用目录

include_directories(extras/libgit2/include)
add_subdirectory(extras/libgit2)
add_subdirectory(sample1)

在需要链接这个库的工程中使用 target_link_libraries 链接上 libgit2
就像我们的 sample1 中这样

target_link_libraries(sample1 git2)

有些环境下会报找不到 -lssh2 的错误:
ld: library not found for -lssh2
只要在根目录的 CMakeLists.txt 中加上一句:

LINK_DIRECTORIES(${LIBSSH2_LIBRARY_DIRS})

这样简单的构建系统就搭建好了,可以开始写代码了
使用其他 IDE 的话,直接用 cmake 导出对应的工程,在自己创建的工程中引入就可以了。
不太推荐直接编译好库放到工程里使用,我自己试了之后发现它用到的几个第三方库还需要自己手动构建再引用。

附二、示例代码的使用:

1. 从 github 上拉代码

git clone git@github.com:XiaochenFTX/libgit2_samples.git
cd libgit2_samples
git submodule update –init –recursive

2. 使用 cmake 导出熟悉的 IDE 工程,或者直接构建运行

mkdir build 
cd build/

Xcode:

cmake .. -G “Xcode”

Visual Studio:

cmake .. -G "Visual Studio"

直接构建:

cmake .. 
make

我使用的 IDE 是 Clion ,使用 Clion 导入工程也可以直接使用

MacOS OpenGL窗口程序的正确打开方式

当小白挖开gl渲染这个坑的时候,最先遇到的问题一定是窗口。然后就是虐死强迫症系列的剧情。
教程最多的一定是glut系列,然而,放到Xcode里一片黄,原因是苹果在未来要不支持这玩意了。再之后就是glew、glfw等一堆库,到这个时候一个没什么基础的小白玩家应该已经有流失的想法了。
这篇博客主要介绍怎么直接搞出一个干净的glview窗口程序,简简单单的开始OpenGL的新手引导。

然后也可以解决一些问题:
1. 如何创建一个OpenGL窗口
2. glut的警告问题
3. 可以使用4.0以上版本的OpenGL
4. 对于非小白玩家而言可能第3条是最重要的

废话少说,书归正传:
第一步、创建工程
1. 新建一个Cocoa Application工程
130298
2. 拖一个OpenGL View 替换掉原来的默认View
582077
这样这个窗口就可以启动运行了。

第二步、写代码
1. 新建一个Cocoa Class,选择继承自 NSOpenGLView
306506
2. 把上边建好的GLView关联上我们自己的类
742882
这样就可以在我们自己类中调用渲染代码了。

第三步、支持GL4.1
需要实现的函数:
initWithCoder
defaultPixelFormat
prepareOpenGL
drawRect

重点就是在初始化部分,需要设置一下属性才能真正的支持4.1

- (nullable instancetype)initWithCoder:(NSCoder *)coder { 

   [[self openGLContext] makeCurrentContext]; 

   self = [super initWithCoder:coder]; 

   return self; 
} 

+ (NSOpenGLPixelFormat*)defaultPixelFormat { 
   NSOpenGLPixelFormatAttribute attrs[] = 
   { 
      NSOpenGLPFADoubleBuffer, // 可选地,可以使用双缓冲 
      NSOpenGLPFAOpenGLProfile, // Must specify the 3.2 Core Profile to use OpenGL 3.2 
      NSOpenGLProfileVersion4_1Core, 
      0 
   }; 

   return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; 
}

这样就是一个可以正常撸的GL窗口程序了。非常适合强迫症玩家入门使用,编译器完全没有警告。

在这就不贴完整代码了。有一个可运行的程序,在github上。
https://github.com/XiaochenFTX/glFirst

curl在Android系统卡死的问题

问题描述:
调用 curl 获取消息头的时候,发现程序卡住不动了。代码如下:

curl_easy_setopt(curl, CURLOPT_URL, srcUrl.c_str());
curl_easy_setopt(curl, CURLOPT_HEADER, 1);
curl_easy_setopt(curl, CURLOPT_NOBODY, 1);
curl_easy_perform(curl); // 这句直接卡死,没有任何返回

经测试发现,不做任何设置,做一次发送,同样会卡死。

curl_easy_setopt(curl, CURLOPT_URL, srcUrl.c_str()); 
curl_easy_perform(curl); // 这句直接卡死,没有任何返回

解决方案:

经过反复试验,在请求的时候设置写数据回调,就可以正常执行了。

void size_t writeFunc(void *ptr, size_t size, size_t nmemb, void *userdata)
{
    return 0;
}
curl_easy_setopt(curl, CURLOPT_URL, srcUrl.c_str());
curl_easy_setopt(curl, CURLOPT_HEADER, 1);
curl_easy_setopt(curl, CURLOPT_NOBODY, 1);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, nullptr);
curl_easy_perform(curl);

具体原因不详,暂时没有精力去读它的代码。我猜卡死的问题与代码本身关系不大,暂时比较怀疑编译库的工具链。由于之前遇到过 ndk 编译出的 sscanf 等函数调用卡死,所以这货暂时列为A级怀疑对象。

C++ 如何让自己的类支持foreach

首先,我们先看看使用 foreach 的时候,都发生了什么。

新建一个类,根据经验,我们 for 一个对象应该是这样的:
for (XX::iterator iter = xx.begin(); iter != xx.end(); ++iter)
所以,迭代器, beigin(), end() 应该是会用得到的,我们做个试验:
Generator.h
class Generator 
{
public:
    typedef int *iterator;
    iterator begin();
    iterator end();
};

Generator.cpp

Generator::iterator Generator::begin()
{
    std::cout << "begin()" << std::endl;
    return nullptr;
}

Generator::iterator Generator::end()
{
    std::cout << "end()" << std::endl;
    return nullptr;
}
main.cpp
Generator g;
for (auto i: g)
{
    std::cout << i << std::endl;
}
运行结果:
begin() 
end()
说明我们想的没错,foreach 跟上边的 for 的行为是一样的。
插个体外话,(发现字打错了,不过不改了,还是挺哏儿的)
1. 之前以为 for (const auto i : g) 会调用 const 版本的 cbegin cend ,然而 并不会。
2. iterator 叫什么名字都没什么所谓,起名叫 红太阳 也ok。
接下来,处理一下迭代器:(我想确认一下丫具体被如何操 做)
class iterator
{
public:
    iterator(int*p)
    : _p(p)
    {
    }
    int * operator()()
    {
        std::cout << "operator()" << std::endl;
        return _p;
    }
    int operator*()
    {
        std::cout << "openator*" << std::endl;
        return 0;
    }
    iterator & operator++()
    {
        std::cout << "++operator" << std::endl;
        ++_p;
        return *this;
    }
    iterator operator++(int)
    {
        std::cout << "operator++" << std::endl;
        return _p++;
    }
    bool operator==(const iterator& it)
    {
        std::cout<< "operator==" <<std::endl;
        return true;
    }
    bool operator!= (const iterator& it)
    {
        std::cout<< "operator!=" <<std::endl;
        return true;
    }
private:
    int *_p;
};
输出结果:
begin() 
end() 
operator!= 
openator* 
0 
++operator 
operator!= 
openator* 
0 
++operator 
operator!= 
openator* 
0

已经足够说明问题了,自己把剩下的细节补充上就好了。

总结一下要点:
1. begin() end()
2. 迭代器
3. 迭代器重载 !=
4. 迭代器重载 前++
5. 迭代器重载 *(解引用)

iOS命令行打包的坑

最近在搞自动打包的时候,不小心踩到了了烂水果没有来得及擦干净的菊花。

总结一下经验教训:
1.技术
找不到ResourceRules.plist
类似这样的警告:
Warning: –resource-rules has been deprecated in Mac OS X >= 10.10! /tmp/QYFSJIvu7W/Payload/XX.app/ResourceRules.plist: cannot read resources
经过我缜密的调(gu)查(ge)取(bai)证(du),大致上可以猜测是烂水果更新了签名机制后并没有更新整套命令行工具。
秘密就藏在……
执行以下命令:
xcrun -sdk iphoneos -f PackageApplication

定位到 PackageApplication 然后用随便什么文本编辑器打开

搜索 ResourceRules ,定位到之后,清理掉与她有关的参数,整成这样:
my @codesign_args = ("/usr/bin/codesign", "--force", "--preserve-metadata=identifier,entitlements", "--sign", $opt{sign});

就这样。

亲测有效,目前尚未发现副作用。
参考文献:(临时找来凑数的)
2.
签名验证失败:
Program /usr/bin/codesign returned 1 :
resource envelope is obsolete 这个错误
stackoverflow 上大神给出的解决方案就是在 codesign 验证的时候,加上 –no-strict (不严格验证?这尼玛确实不报错了,然而……总之就是不报错了)
具体操作方式,也是向上边一样,修改 PackageApplication
找到 —verify 那行 加上 –no-strict 参数:
my $result = runCmd("/usr/bin/codesign", "--verify", "--no-strict", "-vvvv", $plugin );

参考资料:

RSS
Follow by Email
YouTube
YouTube
Pinterest
fb-share-icon
LinkedIn
Share
VK
Weibo
WeChat
WhatsApp
Reddit
FbMessenger
Copy link
URL has been copied successfully!