2020年4月

客户端的socket不需要手动执行bind绑定地址,但这不意味着客户端socket真的不需要绑定端口,实际上是内核它帮我们做了这个操作,在执行connect时,内核发现没有绑定端口,就会自动选择一个合适的端口绑定到socket。

当然这不说明我们不能对客户端socket执行bind操作,对于客户端socket,依旧可以执行bind操作把socket绑定到一个地址。

为什么客户端的socket会自动分配端口呢?

  1. 客户端的socket很多,每产生一个连接,就会创建一个socket,我们无法准确得知有哪些端口没有被使用。
  2. 客户端的socket并不关心自己端口是多少,更多的关注是服务端的端口号,因此客户端socket可以任意指定,只要能够通信即可。

自动分配端口这一操作在内核中的体现在net/inet_connection_sock.c:inet_csk_get_port()

do {
    head = &hashinfo->bhash[inet_bhashfn(net, rover,
            hashinfo->bhash_size)];
    spin_lock(&head->lock);
    // 遍历所有的连接哈希表
    inet_bind_bucket_for_each(tb, node, &head->chain)
        // 端口已经被使用了
        if (ib_net(tb) == net && tb->port == rover) {
            // 判断是否开启了端口快速回收以及端口重用
            if (((tb->fastreuse > 0 &&
                sk->sk_reuse &&
                sk->sk_state != TCP_LISTEN) || 
                (tb->fastreuseport > 0 &&
                  sk->sk_reuseport &&
                  tb->fastuid == uid))&&
                (tb->num_owners < smallest_size || smallest_size == -1)) {
                // 处理端口重用逻辑
                smallest_size = tb->num_owners;
                smallest_rover = rover;
                if (atomic_read(&hashinfo->bsockets) > (high - low) + 1) {
                    spin_unlock(&head->lock);
                    snum = smallest_rover;
                    // 找到了一个合适的端口
                    goto have_snum;
                }
            }
            goto next;
        }
    break;
next:
    // 继续往下遍历端口号,直到找到一个可用的端口
    spin_unlock(&head->lock);
    if (++rover > high)
        rover = low;
} while (--remaining > 0);

就像上面所描述的,大多数场景,我们无法确切知道当前环境还有哪些端口可用,因此也无需绑定地址到socket。但是对于特定场景,还是需要给socket手动绑定地址。如代理程序对UDP代理时,要先在本地创建一个socket作为客户端发送数据的隧道,这个时候就要先指定好端口,然后再返回给客户端。

那么现在问题就来了,我怎么知道需要绑定哪个端口呢?上面也说了,我不知道有哪些可用的端口。同时,因为也没有执行connect,此时也没有得到一个随机的端口,这种场景应该怎么处理?可以采取的做法是:给socket绑定一个0端口,让系统随机分配一个。

代码中赋值add.sin_port = 0,就是告诉内核,我需要一个可用的随机的端口,请给我分配一个。然后内核就会分配了。

执行完后,再通过getsockname函数就可以得到系统分配的端口号了。示例代码:

int main() {
    int sock_fd;
    struct sockaddr_in addr;
    socklen_t socklen;

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    addr.sin_family = AF_INET;
    addr.sin_port = 0; // 绑定随机端口
    addr.sin_addr.s_addr = INADDR_ANY;

    socklen = sizeof(addr);
    if (bind(sock_fd, (struct sockaddr *)&addr, socklen)) {
        perror("bind error");
        return -1;
    }

    // 获取系统随机分配的端口号
    getsockname(sock_fd, (void *)&addr, &socklen);
    printf("%u\n", ntohs(addr.sin_port));
    return 0;
}

书到用时方恨少,枉我最近一直在疯狂刷题找状态,没想到关键时刻还是掉链子了。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/binary-tree-right-side-view

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

一、题目描述

给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例:

  • 输入: [1,2,3,null,5,null,4]
  • 输出: [1, 3, 4]
  • 解释:
   1            <---
 /   \
2     3         <---
 \     \
  5     4       <---

二、题解

2.1 广度优先遍历

看到题目第一个想到的就是广度优先遍历,因为右视图看到的都是每一层最右边的节点,直接通过广搜层次遍历,然后取每层最后元素即可。

2.2 深搜+递归

深搜的过程:一直读右子树的节点,直到右子树不存在,然后遍历左子树。同时,给每层数据都加上层数标记,因为遍历了右子树后还需要遍历左子树,如果当前层已经找过到了最右边的节点,就继续往下找。

三、代码

广搜代码

class Solution {
public:
    vector<int> rightSideView(TreeNode *root) {
        int i, n;
        queue<TreeNode *> q;
        TreeNode *p;
        vector<int> ans;

        if (root == nullptr) {
            return ans;
        }

        q.push(root);

        while (!q.empty()) {
            n = q.size();

            for (i = 0; i < n; i++) {
                p = q.front();
                q.pop();
                if (p->left)
                    q.push(p->left);
                if (p->right)
                    q.push(p->right);
            }

            ans.push_back(p->val);
        }

        return ans;
    }
};

深搜代码

class Solution {
private:
    void f(vector<int> &ans, int depth, TreeNode *root) {
        TreeNode *p;
        int n;

        if (root == nullptr)
            return;

        if (ans.size() <= depth) {
            ans.push_back(root->val);
        }

        if (root->right) {
            f(ans, depth + 1, root->right);
        }
        if (root->left) {
            f(ans, depth + 1, root->left);
        }
    }
public:
    vector<int> rightSideView(TreeNode* root) {
        vector<int> ans;
        f(ans, 0, root);
        return ans;
    }
};

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/edit-distance

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

一、题目描述

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

示例 1:

  • 输入:word1 = "horse", word2 = "ros"
  • 输出:3
  • 解释:

    horse -> rorse (将 'h' 替换为 'r')
    rorse -> rose (删除 'r')
    rose -> ros (删除 'e')

示例 2:

  • 输入:word1 = "intention", word2 = "execution"
  • 输出:5
  • 解释:

    intention -> inention (删除 't')
    inention -> enention (将 'i' 替换为 'e')
    enention -> exention (将 'n' 替换为 'x')
    exention -> exection (将 'n' 替换为 'c')
    exection -> execution (插入 'u')

二、题目解析

这道题目和leetcode44-通配符匹配很类似,比较两个字符串之间的状态,因此也可以使用相同的办法。

dp[i][j]表示word1的第i个字符到word2的第j字符转换需要的最小操作数,动态转移方程可以表示为:

$$\begin{equation} dp[i][j]= \begin{cases} dp[i-1][j-1] & word1[i]=word2[j]\\ min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 & word1[i] \ne word2[j]\end{cases} \end{equation}$$

说明:如果word1[i]和word2[j]相等,那么最小操作次数就等于dp[i-1][j-1]。如果不相等,那么操作次数就应该是两个字符串的前一个字符匹配结果中的最小值加一。

三、代码

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

class Solution {
public:
    int minDistance(string word1, string word2) {
        int i, j, row, col;
        vector<vector<int>> dp;

        if (word1.empty() || word2.empty())
            return word1.size() + word2.size();

        row = word1.size();
        col = word2.size();
        dp.reserve(row + 1);

        for (i = 0; i <= row; i++) {
            dp.emplace_back();
            vector<int> &v = dp[i];
            dp[i].reserve(col + 1);
            for (j = 0; j <= col; j++) {
                if (j == 0) {
                    dp[i].push_back(i);
                } else if (i == 0) {
                    dp[i].push_back(j);
                } else {
                    dp[i].push_back(0);
                }
            }
        }

        for (i = 1; i <= word1.size(); i++) {
            for (j = 1; j <= word2.size(); j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                }
            }
        }
        return dp[row][col];
    }
};

在写代码的过程中,CLion提醒我把push_back方法替换成emplace_back方法:

代码中我的想法是使用vector创建一个二维数组,并提前分配好空间,避免后序频繁扩容增加时间复杂度。

emplace_back函数的作用是减少对象拷贝和构造次数,是C++11中的新特性,主要适用于对临时对象的赋值。

在使用push_back函数往容器中增加新元素时,必须要有一个该对象的实例才行,而emplace_back可以不用,它可以直接传入对象的构造函数参数直接进行构造,减少一次拷贝和赋值操作。

例如以下学生类:

class stu_info {
private:
    string name;
public:
    stu_info(const string &name) {
        this->name = name;
        cout << "stu_info(): " << this->name << endl;
    }

    ~stu_info() {
        cout << "~stu_info(): " << this->name << endl;
    }

    stu_info(const stu_info &s) {
        this->name = s.name;
        cout << "copy constructor: " << this->name << endl;
    }
};

使用push_back插入元素的办法:

vector<stu_info> v;
v.push_back(stu_info("nginx"));

在push_back之前,必须使用stu_info实例一个临时对象传入才行,实例对象就必须要执行构造函数,然后拷贝到容器中再执行一次拷贝构造函数。

而emplace_back可以不用执行多余的拷贝构造函数了,它是直接在容器内执行对象的构造:

vector<stu_info> v;
v.emplace_back("redis");

两个函数的执行结果:

我以前确实是还不知道这个函数,要不是编译器提醒我,以后我可能也不会知道。不得不说IDE在某些方面也能帮助我们提高眼界

问题背景:在刷题的过程中,要使用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,证书的格式应该为:

用户证书
中间证书
根证书