编程我只用CPP 发布的文章

问题背景:在刷题的过程中,要使用min函数,但是线上OJ并没有这个函数。因为一时也想不起它到底属于哪个头文件,所以为了偷懒,顺手就写下了以下宏定义:

#define min(x, y) (x) < (y) ? (x) : (y)

正常情况下这个宏定义是没有问题的,代码提交错误我也从没怀疑过它有问题。因为我认为自己对宏定义已经十分了解了,它的坑我基本都遇到过,该写的括号都写了,只是没有加do...while(0)而已,应该不会有问题。

直到我提交失败了n次后,当我抱着试一试的态度把这个宏定义替换成了内联函数后,提交就过了:

static inline int min(int x, int y) {
    return x < y ? x : y;
}

此时我的心里就只有两个字:卧槽!为什么我不早点开启调试呢?因为错误案例的数据量特别大,调试到触发问题的点太耗时了,所以一直没有调试。触发问题出现的场景是我对宏定义进行了嵌套调用:

x = min(min(1, 3), 2) + 1

使用-E选项预处理发现他们被展开成了如下形式,预期的结果应该返回2,但是这个表达式返回的结果是1,所以就出现了问题:

x = ((1) < (3) ? (1) : (3)) < (2) ? ((1) < (3) ? (1) : (3)) : (2) + 1;

《Effective C++》中明确提出了一点就是:少使用宏定义!宏定义只是简单的文本替换,它不会在编译时候检查,在复杂的表达式逻辑中很容易就会产生问题。

刷OJ的时候惊喜的发现,我竟然不会给二维数组动态分配内存。写了n年的代码了,竟然被这个难倒了!自惭形秽,难以言表。

方法一

先分配指针数组的内存,然后给数组中的每个int *指针分配内存:

int **data, i;
data = (int *)malloc(sizeof(int *) * row);

for (i = 0; i < row; i++) {
    data[i] = (int *)malloc(sizeof(int) * col);
}

数组的表现形式:

方法二

使用一维指针模拟成二维指针,分配row * col个连续的元素,访问的时候模拟成二维指针访问:

data = (int *)malloc(sizeof(int) * row * col);

for (i = 0; i < row; i++) {
    for (j = 0; j < col; j++) {
        *(data + i * col + j) = i * j;
    }
}

参考

How to dynamically allocate a 2D array in C?

openssl是目前使用最广泛的ssl库之一,除了提供全面的ssl加密库以外,还提供了一些基础的命令行工具用于测试,目前绝大多数的软件都是使用openssl库来进行ssl交互,很多系统默认都自带了openssl相关的库和工具。

在我的工作中,最常用到的就是利用它来进行漏洞检测(如SSL重协商漏洞)以及连接测试等,使用普通的浏览器构造出特定的数据包实际上是很难的,但是通过openssl命令却相当简单。

一、查看证书相关信息的命令

打印证书的完整内容:

openssl x509 -in cert.pem -noout -text

打印出证书的序列号:

openssl x509 -in cert.pem -noout -serial

查看der格式的证书内容:

openssl x509 -in cert.pem -inform der -noout -text

把PEM格式的证书转化成DER格式

openssl x509 -in cert.pem -inform PEM -out cert.der -outform DER

二、s_clinet用法

s_clinet是openssl命令中的一个客户端,可以用来进行openssl相关的连接测试,漏洞检测的时候经常会用到。

本周使用s_client做了两件事,一个是完成了ssl重协商漏洞的测试,另外一个是通过它重现了一个代码BUG。

发起一个连接请求:

openssl s_clinet -connect www.baidu.com:443 -ssl3
-ssl3表示使用SSLv3版本的协议去连接服务端,也可以换成tls1_3/tls1_2/tls1_1等。

指定发送的srever_name扩展:

openssl s_client -connect www.baidu.com:443 -server_name WWW.BAIDU.COM

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/lfu-cache

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

设计并实现最不经常使用(LFU)缓存的数据结构。它应该支持以下操作:getput

  • get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
  • put(key, value) - 如果键不存在,请设置或插入值。当缓存达到其容量时,它应该在插入新项目之前,使最不经常使用的项目无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,最近最少使用的键将被去除。

进阶:

你是否可以在 O(1) 时间复杂度内执行两项操作?

示例:

LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回 1
cache.put(3, 3);    // 去除 key 2
cache.get(2);       // 返回 -1 (未找到key 2)
cache.get(3);       // 返回 3
cache.put(4, 4);    // 去除 key 1
cache.get(1);       // 返回 -1 (未找到 key 1)
cache.get(3);       // 返回 3
cache.get(4);       // 返回 4

二、题目解析

LFU算法是LRU算法的改进版本,LRU算法强调最近最少使用,逐步淘汰掉很久没有使用的缓存。而LFU在LRU的基础上引入了使用频率限制,优先使用频率来淘汰缓存。在频率同等的情况下,再通过LRU淘汰较久没有使用的。

虽然只是增加了一个维度的判断,但是在逻辑和编码上,多出来的就麻烦了不止一个档次。因为题目要求在O(1)的时间复杂度内完成两项操作。对于get操作而言,如果使用哈希表来保存所有的缓存节点,可以在O(1)的时间复杂度完成。但是对于put操作来说,想要把它在O(1)的时间复杂度内插入,就不简单了,必须要有合适的数据结构来支撑才行,因为除了保存频次以外还有记录节点的使用时间。如果像LRU一样使用链表来存,每个缓存节点都要先找到合适的位置才能插入,时间复杂度是O(n)

这里可以采取的方式是使用两个哈希表来保存所有的节点,其中一个以缓存的key值作为哈希表的key,另外一个以缓存出现的频率作为哈希表的key。假设保存所有缓存节点的哈希表为cache,保存频率的哈希表为freq,那么它的操作逻辑为:

三、代码

// 缓存节点
struct cacheNode {
    int key, val, freq; // 键
};

class LFUCache {
private:
    unordered_map<int, list<cacheNode *>> freq; // 保存所有频次的节点
    unordered_map<int, cacheNode *> cache; // 保存所有的缓存节点
    int min; // 出现最小的频次
    int capacity; // 容量
    int size; // 大小

    void incrFreq(unordered_map<int, cacheNode *>::iterator &itCache) {
        cacheNode *node;
        unordered_map<int, list<cacheNode *> >::iterator itFreq;

        node = itCache->second;

        // 找到对应频率的链表
        itFreq = freq.find(node->freq);
        if (itFreq == freq.end())
            throw logic_error("unexpect error: cannot file list in freq map");

        // 从当前链表移除
        itFreq->second.remove(node);
        if (itFreq->second.empty()) {
            freq.erase(node->freq);
            // 当前删除的节点是最小频率节点
            if (node->freq == min)
                min++;
        }

        // 增加频率
        node->freq++;
        itFreq = freq.find(node->freq);
        if (itFreq == freq.end()) {
            freq.insert(pair<int, list<cacheNode *>>(node->freq, list<cacheNode *>()));
            itFreq = freq.find(node->freq);
        }

        itFreq->second.push_front(node);
    }

    // 添加新节点
    void putNewNode(int key, int value) {
        cacheNode *node, *p;
        unordered_map<int, cacheNode *>::iterator itCache;
        unordered_map<int, list<cacheNode *> >::iterator itFreq;

        if (this->size == this->capacity) {
            // 缓存容量上限了,到使用频率最低的删除最近最少使用
            itFreq = freq.find(min);
            if(itFreq == freq.end()) // 这里必须要有节点,否则异常
                throw logic_error("unexpect error: cannot find list in min freq map");

            if (itFreq->second.empty()) // 链表的节点数量不为0
                throw logic_error("unexpect error: min freq list is empty");

            node = itFreq->second.back();
            // 弹出最近最少使用的,先清除缓存列表中的
            cache.erase(node->key);
            // 然后清除频率表中的
            itFreq->second.pop_back();
            if (itFreq->second.empty()) {
                // 如果列表中的元素删完了,完全移除key
                freq.erase(min);
            }
            this->size--;
        } else {
            node = new cacheNode;
        }
        // 给node节点赋值
        min = node->freq = 1;
        node->key = key;
        node->val = value;
        // 先插入到以频率为key的哈希表
        itFreq = freq.find(min);
        if(itFreq == freq.end()) { // 这里可能是不存在对应节点的,如果不存在,创建一个节点
            freq.insert(pair<int, list<cacheNode *>>(min, list<cacheNode *>()));
            itFreq = freq.find(min);
        }
        itFreq->second.push_front(node);
        // 然后插入到缓存哈希表
        cache.insert(pair<int, cacheNode*>(key, node));
        this->size++;
    }

    // 插入已经存在的节点
    void putExistNode(int key, int value, unordered_map<int, cacheNode *>::iterator &itCache) {
        cacheNode *node;
        unordered_map<int, list<cacheNode *> >::iterator itFreq;

        // 找到了对应的缓存,更新缓存的value,然后把频率加一
        node = itCache->second;
        node->val = value;

        incrFreq(itCache);
    }
public:
    LFUCache(int capacity) {
        this->size = 0;
        this->min = 0;
        this->capacity = capacity;
    }

    int get(int key) {
        cacheNode *node;
        unordered_map<int, cacheNode *>::iterator itCache;
        unordered_map<int, list<cacheNode *> >::iterator itFreq;

        if (capacity == 0)
            return -1;

        itCache = cache.find(key);
        if (itCache == cache.end()) // 没有找到对应的cache,直接返回
            return -1;

        incrFreq(itCache);
        return itCache->second->val;
    }

    void put(int key, int value) {
        cacheNode *node;
        unordered_map<int, cacheNode *>::iterator itCache;
        unordered_map<int, list<cacheNode *> >::iterator itFreq;

        if (capacity == 0)
            return;

        itCache = cache.find(key);
        if (itCache == cache.end()) {
            // 插入新值
            putNewNode(key, value);
        } else {
            // 更新旧值
            putExistNode(key, value, itCache);
        }
    }
};

TIME-WAIT状态是TCP四次挥手中的状态,在我的认知中,它是客户端socket的状态。但是最近遇到了个问题是:服务端上某个处于监听状态的socket有很多连接都处于这个状态。

当然在某些特定的场景下,服务端出现大量TIME-WAIT状态的socket状态是合理的,例如爬虫服务器,它要主动发起大量的连接去爬取其他网站上的数据,它们在这个场景中都属于客户端socket,爬完数据主动关闭连接了,所以会导致出现大量TIME-WAIT状态。但是我这个并非一个客户端socket,它是执行了listen的:

图中的9090端口是我监听的端口,只列出来了前10个TIME-WAIT状态的连接,和它一样的socket有接近16w个:

这让我百思不得其解,讲道理这不应该是客户端socket才会有的状态吗?为什么都已经是监听状态的socket还会出现这种状态?为了确认我的记忆没有错误,我又回顾了一次四次挥手的过程:

image.png

  1. 客户端发起关闭socket,此时发送一个FIN数据包到服务端。
  2. 服务端收到客户端的关闭请求后,回一个ACK表示确认收到了关闭请求。
  3. 服务端在一段时间后决定也关闭socket,于是发送一个FIN到客户端。
  4. 客户端收到服务端的关闭请求后,回复一个ACK表示收到了关闭请求。

几乎所有计算机网络相关书籍上对4次挥手的描述都是这样的,TIME-WAIT应该是出现在客户端socket一侧的。。为什么处于监听状态的socket会有呢?它不应该是LAST-ACK状态吗?

这个问题困扰了我大半天,实在是捉摸不透。也是晚上睡觉前突然想起才醒悟过来,发现这个问题原因。

实际上问题原因很简单,TIME-WAIT状态并非是客户端独有的状态,而是主动发起关闭连接方都会拥有的状态。即使是服务端、处于监听状态的socket,只要它向另一端发起了关闭请求,那么它就会产生这个状态。我们的服务端程序中,因为特定业务逻辑的关系,会经常性的主动断开连接,因此就导致了出现大量TIME-WAIT状态的socket。

为了验证这个观点,使用socket编写了两个小程序,一个作为服务端监听8080端口,一个作为客户端去连接服务端。当客户端连接上来后,服务端主动close掉,然后再观察连接确实处于TIME-WAIT状态。

总结:TIME-WAIT状态是客户端socket的状态这个观点实际上是一个思维误区,因为不管是从大学中、工作中聊到的四次挥手都是客户端主动断开连接的,导致我们的思维惯性就认为TIME-WAIT是属于客户端的状态!而实际上这个观点是错误的。

一、证书和CA

HTTPS证书的颁发和验证一共包含以下几个角色:

  1. 顶级CA:最顶级的证书颁发机构,可以签发中间CA/。
  2. 中间CA:也是受信任的证书颁发机构,它由根CA签发,中间证书可以有很多级,中间CA也能再签发中间CA。
  3. 终端证书:由CA签发出来的证书。

三者的关系为:

顶级CA机构一般不直接参数证书颁发,因为顶级CA就一个公私钥,泄密后影响太大。因此一般都是通过中间CA来颁发证书,中间CA可以有多层,中间CA也可以自己再签发中间CA,他们都是等效的。实际上的正式的使用场景也是这样,中间CA会根据加密算法或其他因素再衍生出多个中间CA。以百度的证书,它的证书就是由一个中间CA颁发:

生成证书时,用户只要把自己的公私钥和身份信息(如域名信息等)提交给CA机构就行,CA机构对用户信息加密生成证书给到用户,用户把证书和私钥部署到服务端就能开启HTTPS了。

证书的表现形式

证书有多种表现形式,常用的为pemder格式,两者的区别在于前者是以ascii码表示的证书,后者是二进制形式。pem格式的证书是以下形式:

-----BEGIN CERTIFICATE-----
MIIEozCCBAygAwIBAgIJAIkKM/OEESv3MA0GCSqGSIb3DQEBBQUAMIHnMQswCQYD
...
otgUgl+vsfMW5hy8607ppPM7YWTMUV36N6mVAOGPtntf8HdlbH7MLr+PiAjBspkw
HGWHw5+FYqoBWPALLEi3d7LGHnF/qJchkjttwqakSS0u+sWQIqYD
-----END CERTIFICATE-----

两种证书在各操作系统下都是能被直接打开的(windows需要修改后缀为crt),效果也都一样,可以使用windows的证书管理或者openssl命令转换两种证书格式。

二、客户端校验CA

证书部署到服务器后,客户端请求到证书,会根据证书信息找到对应的根证书签发机构(CA),如果CA受信任,则认为证书可靠,证书中的公钥也可靠,可以建立加密连接。而如果CA不受信任,浏览器就会拦截请求,提示连接不安全,需要用户确认后才建立连接。如chrome浏览器就会弹出以下弹框信息等用户确认后才建立连接:

默认情况下,操作系统都会保存一份受信任根证书列表,这样在访问网站的时候很方便就能确认当前证书的颁发者是不是受信任CA了。windows系统可以在运行中输入certmgr.msc来查看:

ubuntu在:

 /etc/ssl/certs/ca-certificates.crt

centos在:

/etc/pki/tls/certs/ca-bundle.crt

OCSP校验

当本地信任库无法校验证书时,例如证书链不完整时,需要使用OCSP方式来校验证书的有效性,它实际上是一种在线校验证书机制,通过OCSP协议去请求服务端证书来检验CA。

三、部署HTTPS证书

nginx中配置HTTPS证书的方法:

server {
    ssl on; # 开启ssl
    ssl_certificate cert.pem; # 证书路径
    ssl_certificate_key key.pem; # 私钥路径
}

高版本的nginx配置中,取消了ssl on指令,改为了以下形式:

server {
    listen 443 ssl;
    ssl_certificate cert.pem;
    ssl_certificate_key key.pem;
}

证书中如果包含了CA,证书的格式应该为:

用户证书
中间证书
根证书

linux下的命令,大多不支持PAC形式代理,只支持http/socks代理形式。因此为了使用PAC文件做代理,必须要通过三方软件来转发这部分代理的流量。privoxy是一个支持PAC代理的程序,可以根据不同规则选择代理线路,我们可以使用它来作为代理软件转发客户端流量。privoxy官方网站:www.privoxy.org

测试环境:centos6 + centos7。

一、安装privoxy

安装privoxy的方式有以下几种:

  1. rpm包方式安装或源码包下载编译安装
  2. 使用yum命令一键安装

方式1可以在官方网站找到源码包和安装包,安装相对麻烦一些,centos系统推荐使用原生的包管理工具yum来安装,前提是要安装epel源,国内epel源可以选择阿里云的源,下载速度快(使用方式参考Epel 镜像帮助)。

使用yum安装privoxy的:

yum install privoxy

安装好后,启动进程:

# centos 6
service privoxy start
# centos 7
systemctl start privoxy

添加到开机启动:

# centos 6
chkconfig --add privoxy
chkconfig privoxy on
# centos 7
systemctl enable privoxy

二、配置privoxy以支持PAC

privoxy的配置文件目录是/etc/privoxy/,在目录下添加文件pac.action,内容:

{{alias}}
default    = +forward-override{forward .} 
pac     = +forward-override{forward 10.66.83.23:8080}

{default}
/

{pac}
.baidu.com
.qq.com

配置简单说明:

  • alias段:定义转发规则,目前是定义了两种。default表示默认规则,后面的forward .表示默认走本机。pac表示自定义的pac规则,后面的forward 10.66.83.23:8080表示通过10.66.83.23:8080的HTTP代理出去。
  • default段:匹配默认规则的地址,/表示默认的,如果没有匹配其他规则就匹配这个规则。
  • pac段:需要进行代理转发的域名,下面的地址会通过上面定义的代理地址转发出去。

以上配置的意思就是,*.baidu.com和*.qq.com域名通过10.66.83.23:8080代理,其他的默认走本机代理。其他需要代理的地址也可以加在下面。

然后在主配置文件中config添加修改:

# 添加我们自定义的PAC规则
actionsfile pac.action
# 下面这几行是系统预定义的转发规则,注释掉
# actionsfile match-all.action # Actions that are applied to all sites and maybe overruled later on.
# actionsfile default.action   # Main actions file
# actionsfile user.action      # User customizations

# 下面这几行是系统预定义的过滤规则,注释掉
# filterfile default.filter
# filterfile user.filter      # User customizations

程序默认监听端口是8118,默认只支持本地的代理,如果需要更改的话改以下配置:

listen-address  127.0.0.1:8118

没有特殊要求的话可以使用默认配置,改完后启动privoxy进程就可以了。

注意:进程启动后,如果修改了pac规则,需要重新启动进程才能生效。

三、配置系统

给本机设置代理环境变量:

export http_proxy=http://127.0.0.1:8118
export https_proxy=http://127.0.0.1:8118

然后使用wget或者curl命令测试即可!要注意的是环境变量只对当前终端生效,退出当前会话后就没有了。如果希望每次登录都生效,则需要把这两个命令放到~/.bashrc或者/etc/profile中。

四、原理说明

开启privoxy代理后,流量会全部发到privoxy,然后privoxy来决定流量走哪。配置了PAC规则的走指定线路,没有配置的走本机。

反向代理配置:

server {
    listen 80;
    server_name mirrors.maqian.work;

    location / {
        proxy_pass http://mirrors.aliyun.com;
        proxy_redirect off;
        proxy_set_header Host $proxy_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

在浏览器访问mirrors.maqian.work时:

  • $host: mirrors.maqian.work
  • $proxy_host: mirrors.aliyun.com

[leetcode]125-验证回文串

这道题本身是非常简单的,但是由于审题不仔细,忽略一个细节,导致提交了6次才成功,要检讨!

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/valid-palindrome

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

示例 1:

  • 输入: "A man, a plan, a canal: Panama"
  • 输出: true

示例 2:

  • 输入: "race a car"
  • 输出: false

二、题解

思路:使用双指针,一个从前往后遍历,一个后从往前遍历。遇到非数字和字符就跳过,如果发现两个指针的字符不一样就返回false,直到两个指针汇合,返回true。

要特别注意到的是回文串也包含了数字,我就是因为没看到还包含了数字,所以提交了6次才过。一直卡在测试案例"0P"上,注意是“零+P”,不是“欧+P”。网页上的显示0和O太像了,我看成了"OP",本地测没问题,提交就有问题,最后失败了5次才发现,看评论区才知道大家都被这个输入坑了!!

三、代码

// 判断是否是字符或数字
bool isCharOrNum(char ch) {
    return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
}

class Solution {
public:
    bool isPalindrome(string s) {
        int i = 0, j = s.size() - 1;
        while (i < j) {
            // 跳过非字符
            if (!isCharOrNum(s[j])) {
                j--;
                continue;
            }
            if (!isCharOrNum(s[i])) {
                i++;
                continue;
            }

            // 忽略大小写
            if (tolower(s[i]) != tolower(s[j]))
                return false;
            i++, j--;
        }
        return true;
    }
};

单元测试:

#include <gtest/gtest.h>

TEST(leetcode, demo) {
    string s1("A man, a plan, a canal: Panama"), s2("race a car"), s3("OP");
    Solution s;
    EXPECT_TRUE(s.isPalindrome(s1));
    EXPECT_FALSE(s.isPalindrome(s2));
    EXPECT_FALSE(s.isPalindrome(s3));
}

TEST(leetcode, OP) {
    string s1("PO"), s2("0P");
    Solution s;
    EXPECT_FALSE(s.isPalindrome(s1));
    EXPECT_FALSE(s.isPalindrome(s2));
}

int main() {
    ::testing::InitGoogleTest();
    return RUN_ALL_TESTS();
}

做代码优化,发现代码中获取系统CPU核数是通过system调用命令得到的,想想最近被system支配的恐惧,果断改掉。

linux c中获取CPU核数的函数原语有两个:

#include <sys/sysinfo.h>
get_nprocs_conf();
get_nprocs();

第二个函数是返回当前可用的CPU数量,不可用的意思是CPU HANG住了。如果安装了man page,可以直接在man page中找到用法。

#include <stdio.h>
#include <sys/sysinfo.h>

int
main(int argc, char *argv[])
{
   printf("This system has %d processors configured and "
           "%d processors available.\n",
           get_nprocs_conf(), get_nprocs());
   return 0;
}