编程指北

编程指北 查看完整档案

深圳编辑华中科技大学  |  计算机 编辑腾讯  |  后台开发工程师 编辑 www.marvinle.cn 编辑
编辑

微信搜索🔍「编程指北」,这是一个有趣、有干货的程序员,期待你的到来~

个人动态

编程指北 发布了文章 · 10月16日

本科拿到BAT SSP offer,我的Linux C/C++后台服务器开发学习路线(面经,秋招)

前言

小北去年经历春秋招,拿到了不少大厂 offer,其中包括 sp、ssp 等,感觉在复习准备校招上还是有一定方法的,因为我自己是 Linux C/C++路线。

所以这一篇的主题是「Linux C/C++ 服务器/后台开发学习路线」。

这样的文章相信大家都见得不少了,写之前也非常忐忑,能不能和其它人写得不一样,
也定下了一个目标,这篇文章,不能是简单的堆砌学习资源和书单推荐,更要细化如何有效的去执行落地。

争取做到让看到的同学有一种相见恨晚的感觉哈哈哈。

所以大家可以试着看一下,帮我检查下有没有达到预想的效果哈哈,希望不要被打脸🤣

那就正式开始吧。

这篇文章会有点长有点干,可以先去冲杯咖啡,慢慢看~

正文 | 干货 |收藏

一、后端/后台/服务器开发?

经常在各大公司招聘上看到后端、后台、服务器开发等等,有些同学经常被这些名词搞混。

其实这些名词都是相近的,但是也有点区别,这里说说我的理解:

  • 首先一般公司分为前端和后端,前端就是和用户打交道的,负责用良好的视觉效果将数据呈现给用户,广义的前端包括客户端(安卓、IOS)、Web前端、小程序等。
  • 而与之对应的后端则是负责业务逻辑处理,比如下单、支付等,重在业务流程的处理。

后台一般和后端是一个意思,而服务器开发则稍微广义一点,不仅包含了后台开发,而且也包括支撑整个后台应用的基础开发,比如搜索引擎、微服务、RPC 框架、KV、存储、MQ 等。

后台/后端重在业务处理,是偏向应用层开发,而服务器开发不仅包括应用层开发,更是囊括了整个支撑后台业务的相关组件的开发。

那 Linux C/C++ 服务器/后台开发指的什么呢,其实就是基于 Linux 上 的 C++ 编程。

但是相比 Java 系更强调 Linux 系统编程、网络编程能力,有的还会涉及到服务端底层协议和网络框架开发。

传统的 Java 、Go后台开发偏向 Web 开发,也就是接收前端请求,通过微服务互相调用,完成业务逻辑处理,然后返回给前端。

实际上在腾讯这边的 C++ 后台开发,也是类似的,本身有非常成熟的基于 C++ 的微服务体系,大多数开发也只需要关注业务逻辑就好,不过还是会要求 Linux 系统编程、网络编程等能力。

二、后台开发都考察哪些?

一般来说 Linux C/C++ 后台开发方向涉及以下这些基础知识:

  • C/C++ 语言特性和实现原理
  • 计算机网络
  • 网络编程 和 Linux 系统编程
  • 操作系统原理
  • 部分 Linux 内核原理,如内存管理、文件系统、虚拟内存等
  • Linux 常见命令使用
  • 算法与数据结构
  • 数据库使用及原理
  • 常见 NoSQL组件,如 Redis、Memcached
  • 版本控制 Git

非必选加分项:

  • 分布式相关,如一致性协议比如 Raft 算法、分布式存储等
  • docker、k8s 等虚拟化和云计算相关的
  • 系统设计能力,如短链服务、评论服务、Feed流系统、抢红包、秒杀等

由于篇幅限制,这篇文章主要介绍基础知识的学习路线和方法,其它加分项以后再单独写。

三、C/C++

首先是语言的基础知识,一些关键字和实现原理等:

  • 指针、引用、数组、内存
  • 引用与指针区别
  • C 和 C++ 的一些区别,比如 new、delete 和 malloc、free 的区别
  • 虚机制:虚函数、虚函数表、纯虚函数
  • 继承、虚继承、菱形继承等
  • 多态: 动态绑定,静态多态
  • 重写、重载
  • 智能指针原理:引用计数、RAII(资源获取即初始化)思想
  • 智能指针使用:shared_ptr、weak_ptr、unique_ptr等
  • 一些关键字的作用:static、const、volatile、extern
  • 四种类型转换:static_cast, dynamic_cast, const_cast, reinterpret_cast
  • STL部分容器的实现原理,如 vector、deque、map、hashmap
  • 模板特化、偏特化,萃取 traits 技巧
  • 编译链接机制、内存布局(memory layout)、对象模型
  • C++11 部分新特性,比如右值引用、完美转发等

这里列出来的只是一些比较重要的部分,实际上可能只算 C++ 的冰山一角, 大家且学且珍惜吧,这不 C++11 还没整透彻,C++ 20 又出来了,生命不息,学习不止。

怎么学?

1. 《C++ Primer》

这本书基本包括了 C++ 11 的全部特性,最好把前面三部分:C++基础、C++标准库、类设计者的工具都看一遍,我当时花了一个多月断断续续看到了第16章模板那里。

2. Effective 系列:《Effective C++》、《More Effective C++》、《Effective STL》

第一本是重点,光看《C++ Primer》缺少实践的话,大概率还写不出合格的 C++ 代码,而《Effective C++》就是通过 55 条非常具体的做法告诉你什么样才是符合 C++ 编码规范的,可以缩短你写出合格 C++ 代码的时间,减少踩坑,强烈推荐必读,后面两本优先级稍低,可以有时间再读。

3. 《STL 源码剖析》和《深度探索 C++ 对象模型》

看完 Primer 和 Effective,你应该已经能够比较熟练的使用C++了,但是还缺少对 C++ 底层实现机制的认识。比如虚函数表、成员变量布局等,同时对于 STL 库可能也仅仅停留在使用上。

推荐的这两本可以分别完善你在 C++ 底层实现和 STL 源码、原理上的认识。

以上书籍同时建议和侯捷老师的视频配合服用,效果更佳。

直接在 B 站搜索「候捷 C++」即可,主要有以下几个系列:

  • 《C++内存管理》
  • 《STL源码分析》
  • 《C++ STL与泛型编程高级》
  • 《C++11 新特性》

我基本都看了,收获挺大的,建议看下,可以开倍速。

看完以上资料,算是 C++ 入门了,应付面试也是足够的,基本到达了正确高效地使用 C++ 这一层面。

是不是听到这有点崩溃,特么的看了这么多,才入门???

如果你想在 C++ 语言上更进一步,那么有以下的书籍推荐:

  • 《C++ 语言的设计与演化》
这本书是 C++ 之父 Bjarne Stroustrup 写的,关于 C++ 的前世今生,以及未来的演进方向,可以了解 C++ 的设计哲学。C++ 复杂的语言特性一直让人诟病,通过这本书,可以看到各种特性引入的目的,也更深入了解到了 C 和 C++ 之间关系。比如 C++ 里有个原则就是所有的实现机制都不能带来额外的运行时开销。

我也正在看这本书。

  • 《C++ 沉思录》
  • 《C++ Templates》和《C++模版元编程》
C++模板元编程属于另外一个世界了,一般公司里开发用得比较少,这个也是一个大坑,如果实在感兴趣可以去看看,感受下 C++ 的博大精深,不过这玩意我也不太会,也不推荐你去花时间在上面。
  • CppCon视频
这是 C++ 社区组织的类似开源峰会那种,每次都会讨论一些关于 C++ 的话题,没事去刷一个,还是挺有意思的。
Youtube 直接搜 CppCon 即可找到。

四、操作系统

操作系统这门课,我的感觉是易学难精,但是掌握到日常编程和面试够用还是比较容易的。

那么毕业生或者说你去准备校招面试应该达到怎样的水平:

  • OS 四大模块的理论知识: 进程与线程管理、内存管理、IO与文件系统、设备管理
  • 了解 Linux 内核部分实现原理,如内存管理、进程管理、虚拟文件系统等

其中内存、进程、IO 是重点,这几块也是和编程关系最密切的,这里推荐先挑本偏理论的书看看,了解操作系统的全貌:

  • 《现代操作系统》
  • 《操作系统—精髓与设计原理》

不必全看,两者任选一本都不错,我自己是仔细看了第二本,因为是我们教材,同时挑着看了现代操作系统部分章节。

这部分看完你应该对下面这些话题有一个清晰认知了:

  • 操作系统由哪些构成
  • 进程的状态、切换、调度
  • 进程间通信方式(共享内存、管道、消息)
  • 进程和线程的区别
  • 线程的实现方式(一对一、多对一等)
  • 互斥与同步(信号量、管程、锁)
  • 死锁检测与避免
  • 并发经典的问题:读者写者、哲学家就餐问题
  • 为什么需要虚拟内存,MMU 具体如何做地址转换的
  • 内存为什么分段、分页
  • 页面置换算法
  • 文件系统是如何组织的
  • 虚拟文件系统(VFS)是如何抽象的
  • ...

但是这还不够,看完偏理论的书,当面试官问「进程和线程的区别」时。

大概只能回答出「进程是资源分配的最小单位,线程是CPU调度的最小单位,balabala...」这样正确却普通的答案。

但是如果你了解 Linux 内核的实现,就可以实际出发,讲讲 Linux 中进程和线程是如何创建的,区别在哪里。

比如在 Linux 中进程和线程实际上都是用一个结构体 task_struct 来表示一个执行任务的实体。进程创建调用fork系统调用,而线程创建则是 pthread_create方法,但是这两个方法最终都会调用到 do_fork来做具体的创建操作 ,区别就在于传入的参数不同。

深究下去,你会发现 Linux 实现线程的方式简直太巧妙了,实际上根本没有线程,它创建的就是进程,只不过通过参数指定多个进程之间共享某些资源(如虚拟内存、页表、文件描述符等),函数调用栈、寄存器等线程私有数据则独立。

这样是不是非常符合理论书上的定义:同一进程内的多个线程共享该进程的资源,但线程并不拥有资源,只是使用他们。

这也算符合 Unix 的哲学了— KISS(Keep It Simple, Stupid)。

但是在其它提供了专门线程支持的系统中,则会在进程控制块(PCB)中增加一个包含指向该进程所有线程的指针,然后再每个线程中再去包含自己独占的资源。

这算是非常正统的实现方式了,比如 Windows 就是这样干的。

但是相比之下 Linux 就显得取巧很多,也很简洁。

对于进程、线程这块你还可以把 fork、vfork、clone 、pthread_create 这些模块关系彻底搞清楚,对你理解 Linux 下的进程实现有非常大的帮助。

说了这么多,就是想强调一下理论联系实际的重要性。

特别是操作系统,最好的实践就是看下 Linux 内核是怎么实现的,当然不是叫你直接去啃 Linux 源码,那不是一般人能掌握的。

最好的方式是看书,书的脉络给你理得很清晰。

书籍推荐:

  • 《Linux内核设计与实现》
这本书恰到好处,即讲清楚了内核实现的要点,又不会通篇源码。

这本书重点关注「第 3 章进程管理」、「第 5 章系统调用」、「第12章内存管理」、「第13章虚拟文件系统」、「第 15 章进程地址空间」

这些章节属于操作系统核心部分,其它如中断处理、块 IO、设备管理根据你自己兴趣选择看下就可以了。

基本上做到这里,操作系统就没什么大问题了。

五、计算机网络

需要掌握的网络协议和知识:

  • HTTP、TCP、IP、ICMP、UDP、DNS、ARP
  • IP地址、MAC地址、OSI七层模型(或者 TCP/IP 五层模型)
  • HTTPS安全相关的:数字签名、数字证书、TLS
  • 常见网络攻击:局域网ARP泛洪、DDoS、TCP SYN Flood、XSS等

计网知识比较繁杂,很多同学都反映网络很难学,一大堆的网络协议,依次学完后,还是不知道网络是怎么构成的。

这就是没有用对学习方法,导致只见树木,不见森林。

学习时,推荐你抓住一条主线 「一个数据包是如何发送出去的?」

带着这个问题依次去学应用层、传输层、网络层、链路层,思考这些层之间是如何串联起来的。

这就是自顶向下的思路,那自然要推荐:

  • 《计算机网络:自顶向下方法》
这本书从我们最常接触的 HTTP、FTP、SMTP 等应用层协议讲起,可以清晰看到引入各个层的作用。

比如为了区分同一个主机的不用应用,引入了传输层,并使用不用的端口号作为区别;

为了在不同子网间传输数据引入了网络层,并使用 IP 地址寻址路由;

网络层解决了不同子网间路由的问题,但是同一个局域网内确定主机却是通过 MAC 地址,所以引入了链路层来承载 IP 数据包;

同时为了将 IP 地址和 MAC 地址做转换映射又产生了 ARP 协议。

层层递进,逐层揭开网络,非常推荐!

还有一本书:

  • 《网络是怎样连接的》
非常浅显易懂的描述了「一个数据包是如何发送出去的」,也不费时间,看惯了机工社的大黑书,看这种反而有种看小人书的感觉,有基础的话,一天左右就过完了。

只有把握住了整个网络脉络主线才不至于被纷繁复杂的网络协议所搞晕,剩下的就是不断的细化,填充这些主干上的细枝末节。

那么有哪些细节可以去填充呢?

比如 ARP 工作过程、IP 地址、IP 分片、NAT(UDP 打洞)、链路层访问控制协议等等。

还有最重要的 TCP 协议,TCP 也是面试和计网中最重要的概念:

  • 三次握手、四次挥手
  • 状态转换
  • TCP 状态中 TIME_WAIT
  • 拥塞控制
  • 快速重传、慢启动等

这么多东西肯定需要背,但不要死记,最好带着问题去思考为什么要这样做。

这里列几个问题:

  • TCP 如何实现可靠传输的(画外音:如何基于 UDP 实现可靠传输
  • TCP 连接建立为什么不是两次握手(画外音:三次握手的充分必要性说明
  • TIME_WAIT 的存在解决了什么问题,等待时间为什么是 2 MSL

整个 TCP 的核心就是围绕着 可靠传输 + 高效传输(流量控制和窗口管理)

由于 TCP 的细节实在太多,自顶向下那本书有点不太够,所以你需要去看看:

  • 《TCP/IP详解卷1:协议》
这本书不要从头看,而是挑出其中涉及到 TCP 的章节

到这里,对于整个网络以及 TCP 都应该有了一个全面而细致的认识。

但是计网中还是有一些有意思的问题,如果你没思考过,也许回答不出来。

比如:

  • 为什么有了 MAC 地址还要 IP 地址,IP 地址和 MAC 地址的区别是什么?
  • 如何理解广播域和冲突域?
  • 路由器和交换机有什么区别?
  • TCP 连接的本质是什么,真的是“链接”吗?(曾经被问过:Java socket 创建的 TCP 连接,对于主机挂了和 JVM 挂了有什么区别?

这些问题只有当你真正理解了才能回答出,仅仅记住协议的话,估计很难应对灵活的面试题。

此外,网络部分还需要准备 HTTP、HTTPS,推荐:

  • 《图解HTTP》

最后别忘了自己回答一遍那被问烂了、写烂了的问题:

  • 从 URL 输入到页面展现到底发生什么

越细越好,五百字以上吧,哈哈哈

六、网络编程

C++ 后台开发基本是离不开网络编程的,其实甚至整个后台开发也可以看做是在做网络编程。

只不过别人的框架帮我们做了协议解析、网络数据传输、解封包这些底层操作。

比如 SpringBoot 这种保姆级框架,基本上属于将一个框架能干的事都干完了,以至于我们开发业务只需要定义接收和返回包的数据格式,然后做逻辑处理就完了。

像序列化、解封包、IO 处理这种网络编程必备的脏活业务开发根本不会接触到。

但是网络编程技能还是很重要的,特别是对于 Linux C++ 开发来说。

Linux 下网络编程核心的包括系统编程和网络 IO 两个部分:

  • 进程间通信方式: 信号量、管道、共享内存、socket 等
  • 多线程编程:互斥锁、条件变量、读写锁、线程池等
  • 五大 IO 模型:同步、异步、阻塞、非阻塞、信号驱动
  • 高性能 IO 两种模式:Reactor 和 Proactor( 但是 Linux 下由于缺少异步 IO 支持,基本没有 Proactor
  • IO 复用机制:epoll、select、poll(破解 C10K 问题的利器)

推荐的书:

  • 《Unix网络编程》
  • 《Unix环境高级编程》
这两本是砖头书,虽然是网络编程和 Unix 系统编程方面的无出其右的圣经,但主要用途还是垫显示器(逃,
个人觉得这种书不是面向读者的,具体原因和如何阅读这种书在后文介绍。
  • 《Linux高性能服务器编程》
我强烈推荐,这本书前半部分基本是在重复计网基础知识,但是后面几章关于高性能服务器程序框架、高性能IO、IO复用、定时器、多线程编程、线程池和进程池还是讲得非常全面到位的,值得一看,看完基本上对于整个网络编程就有了框架。
  • 《Linux多线程服务器端编程》
这本书同样强烈推荐,这是陈硕大佬写的书,说实话第一部分:C++ 多线程系统编程都直接把我看蒙了,没有想到 C++ 里要做到线程安全这么难,第一章我看了两三遍才看懂吧。。。 这是难得的讲解 C++ 多线程编程的书。

并且在书中,陈硕大佬用了一章讲解了 Muduo 网络库设计与实现,Muduo 比较适合学完基础的网络编程后继续进阶学习如何设计和写一个网络库,是一个高质量的 Reactor 网络库,采用 one loop per thread + thread pool 实现,代码比较简洁,书和源码搭配着看作为学习网络编程方面来说是非常不错。

学完网络编程就可以写点小项目练手了,这里列举几个项目:

  • HTTP 服务器,这个似乎成了 Linux C/C++ 人手一个的项目了?

    这里推荐两个做为参考:

    https://github.com/imarvinle/...: 这是我看完高性能服务器编程后写的

    https://github.com/linyacool/... :这是牛客 linya 大佬写的

    不过 HTTP 服务器看着挺简单的,但是可以扩展写的地方还是挺多的,比如可以加入代理功能,这部分我在留学生 lab 中写过,但是没有集成到这个里面来,可以加入日志库,可以添加 CGI 支持等等。

  • 网络库

    这个也算是造轮子了, 可以就采用 one loop per thread + thread pool 这种模式,先去看懂 Muduo 源码,然后自己再写一个类似的,这个过程就算是抄,你也可以学到不少东西的,学编程不就是这样先看,再模仿、修改,然后创新吗?

  • RPC

    写一个 PRC 你需要考虑到序列化、网络传输、服务发现等,比较有名的有 grpc、brpc,这两个网上文档都比较完善,可以学习一下实现原理。

    这里还有一个简单版本的:https://github.com/guangqianp...

  • 类似QQ的网络聊天室

    简单版的就可以直接在局域网内实现群聊、单聊等。

    更进一步可以考虑一下如何不通过服务器中转消息实现 P2P 聊天,类似 QQ,这里会涉及到 UDP 打洞、NAT 转换等知识,还是很有意思的,我大二用 Java 搞过。

七、系统级编程

作为 C/C++ 程序员,编写的程序不像 Java、Python 这些是在虚拟机上,直接就是在操作系统上运行,那么就必须了解操作系统底层机制和运行原理。

就和 Java 程序员要求了解 JVM 是一个道理,你得熟悉代码运行的平台,才能在出问题的时候准确定位到。

这个也是在我们学校在大三开设的一门课程《System Programing》,从 CMU 引进的,教材也是沿用 CSAPP,这也是我觉得大学上过最值的课了。

我重新认真读 CSAPP 就是在大三上这个课期间,包括做了每个章节附带的 lab,
这是我当时做 Bomblab 的题解:

https://www.jianshu.com/p/479...

这里推荐两本书:

  • 《深入理解计算机系统》
不需要我多介绍了
  • 《程序员自我修养》
别被名字欺骗了,这不是教你养生的,而是学了会掉头发的硬核知识

两本书侧重点各不相同,CSAPP 非常巧妙的把数字电路基础、二进制与指令集体系、汇编语言、程序设计及优化、存储器体系结构、链接与装载、进程、虚拟内存这一堆来自各不同的计算机学科的核心知识点串在一起,并以程序员的视角呈现,所以这本书的英文名字叫《Computer Systems A Programmer's perspective》。

而程序员自我修养则重在链接、目标文件、装载、库与运行时,看完这本书你会了解到一个 C/C++ 程序是如何被编译成目标文件的,以及 Linux 下目标文件的格式,不同目标文件又是如何被链接成一个可执行程序,在链接时如何处理符号、重定位、地址解析等,以及静态链接、动态链接区别等等,最后可执行文件又是如何被加载进内存,如何和虚拟内存空间映射的。

你可能会觉得这个又是只能用于面试,实际派不上用场的知识?

那简直大错特错,说真的,这两本书,我是反复看了三遍以上,当然后续看都是挑着重点看的。

举个例子吧,写 C/C++ 的同学没少遇到这些编译错误吧:

undefined reference to xxx

Symbol key multiply defined (by xxx.o and yyy.o)

在我大一的时候遇到这些问题简直一脸懵逼,根本连报错都看不懂。

特别是涉及到多文件编程的时候,经常傻乎乎的在头文件中定义变量,导致变量多重定义,这些问题没有学过链接知识的其实很难理解。

在实际编程也是经常会遇到的。

又比如externstatic这些关键字是如何在编译链接时起作用,变量的申明与定义又有什么区别?

这部分可以算是真正的内功了,提升你对计算机系统的理解,也有助于解决实际编程过程中会出现的问题,当然也会在面试中出现。

八、数据库

数据库首先要学会 SQL 的使用,这里推荐《MySQL必知必会》。

数据库原理方面可以看看《数据库系统概念》,这本书挺厚的,包含了从 SQL 到数据库设计再到数据库原理、分布式数据库都有,可以挑着看,比如关系模型、数据库设计(三大范式)、数据磁盘存储和组织方式、索引、并发控制等。

当然了整个数据库最重要的还是索引和并发控制(锁、MVCC等),这部分也是面试常考的:

  • 索引存储结构:B树、B+树索引、Hash索引
  • 索引的使用:主键索引、覆盖索引、最左前缀原则、索引下推等
  • 锁:乐观锁、悲观锁、表锁,行锁,意向锁,读锁,写锁等等
  • MySQL InnoDB MVCC 实现机制
  • 存储引擎:InnoDB、MyISAM等,各自的优缺点
  • 事务:ACID理论

这部分推荐两本书:

  • 《高性能MySQL》
  • 《MySQL技术内幕》
这两本主要对索引、innodb存储引擎、锁、并发控制讲得比较清楚,建议挑对应章节看。

九、算法和数据结构

首先需要掌握常见的数据结构:

  • 线性表、数组、链表
  • 栈与队列
  • 树、二叉树、多叉树实现和遍历方式,AVL树实现以及插入删除过程、红黑树(了解定义即可)
  • 图,以及图的实现方式、遍历
  • B树、B+树
  • 散列函数和散列表

常见的算法:

  • 排序算法:冒泡、插入、快速、希尔、堆排、基数、归并等
  • 字符串匹配算法:KMP
  • 常见算法思想:递归、枚举、递推、分治、贪心、动态规划等

视频可以看看 :

  • mooc 上浙大的《数据结构》
  • 学堂在线上清华邓俊辉老师的《数据结构与算法》

这两个是我看过觉得不错的才在这推荐,第一个是初学数据结构时跟着看,第二个是大三复习时刷的。

入门版书籍可以看看:

  • 《啊哈算法》
  • 《算法图解》

稍微进阶点的:

  • 《算法第四版》
这本书强烈推荐,难度适中,但是全面。

终极版:

  • 《算法导论》
这个量力而行就好了。。。

推荐理由是:我不推荐显得没有逼格(:

有了基础的算法思想和数据结构储备,剩下的就是刷题了:

  • 《剑指offer》
建议必刷
  • leetcode
建议分类刷,先易后难,比如数组、二分、二叉树、动态规划,一个一个系列搞定,总结经验,保证 150 道简单和中等以上吧

最重要的是,保持手感,有空就刷一道。

十、网站和视频

有些同学喜欢看视频,那我也在这里统一推荐一下吧

  • B站
你的一站式学习网站,用你想学的关键字在这搜就完了
  • 中国大学 mooc
基础课程学习
  • 网易云课程、学堂在线
一些技术课程、公开课学习

Linux C/C++ 可以去看看黑马的,我试看过几集 IO,讲得还是不错的,就是有点慢,个人觉得不如看书。

刷题可以看看牛客左神的视频

计算机网络可以看看 B 站韩立刚或者 mooc 哈工大的

操作系统可以看看学堂在线上清华的《操作系统》

推荐了这么多书,从哪看起?

一、浅谈学习方法

如果你认真看过我前面介绍每一部分的学习路线时,可以发现我特别强调学习抓主线,并且将每一个基础知识的主线给大家列出来了。

这里再次强推,学习新东西的时候,重点是先对整体脉络、知识结构有一个大概的映像和了解,然后抓住这个领域的主线,顺着主干,突出重点去学习。

集中时间,速战速决,不要将时间线拉得太长,越长可能越坚持不下去,效果越差。

对于细枝末节的内容,可以留到实践的时候,用到了再去查!

如果一头扎进零碎的知识,去看手册、字典型的书,那必然是事倍功半的。

细节留给实践去补充,我们的时间要花在刀刃上,注重知识的体系性和框架的建立。

二、常见问题

说实话,其实这些书籍或多或少都被各路大神推荐过,确实经典。但是大神们却很少告诉你他们是如何去看的,该怎么去看这些书,难道一本本一页页的挨着啃吗?

这部分才是我今天最想说的部分,「该如何去看大厚书」。

比如我后台就经常有同学问:

  • 有些书看的找不到重点,看不下去了,怎么办,比如深入理解计算机系统,UNIX 网络编程,APUE,求指教
  • 我不是科班 CSAPP 可能不是全部看得懂,该怎么办?
  • 大佬,这么多书看得完吗?
  • 刚开始看这些书很痛苦怎么办?

这些书该怎么看,可能过来人,准备过秋招的都比较清楚,但是作为还在大二、大三的会比较懵逼。

就拿我当时亲身经历来说,在网上搜网络编程如何学习,很多人都推荐 UNP、APUE。

好嘛,买来看,从第一页挨着挨着看,而且书中的示例代码我大部分也照着敲了,最后看了七八章,发现始终是在学一些 socket api 和 系统 api 的用法,没摸到网络编程的框架思维。

后面我又去搜,看到有人推荐《Linux高性能服务器编程》,去豆瓣看了下了目录,似乎正是我想要的东西。

直接找来 PDF 开始看,果然这本书才真正让我理解了网络编程的整个套路和框架,学到了各种事件处理模式、计时器、信号处理、线程池这些网络编程中很重要的东西。

当然 UNP 和 APUE 也是不能丢的,这两本书我当做了字典查询,比如学到了 IO 部分,回去看 UNP 中关于五种 IO 模型的介绍。

用到 connect、listen、bind 这些函数,再回去看 UNP 第四章,不得不说,UNP 关于这些 socket API 的使用和各种异常情况的处理方式都介绍得非常详细和深入,不愧是网络编程领域的圣经。

但是初学者看却容易在细节中迷失,抓不到纲领,这也是这类书的缺点。

类似 UNP、APUE 这种书本身是面向知识体系的,而不是面向读者,它们其实更像字典,把这个领域内的所有知识,非常细致的堆叠在一起,看上去就是平铺直叙,充斥着细节,对读者极其不友好。并且书里内容实在大而全,很多根本不用学。

比如 UNP 讲了 sctp 这种协议用法、多播、unix 域协议这些实际用得很少的东西,挨着看不仅会看不下去,而且比较浪费时间。

但是它们又是经典的,确实是这些领域内在体系性和深入性上都做得非常好的书。

什么书才是面向读者的呢?

那就是抓住该领域的核心主干,提纲挈领,带领读者由浅入深,同时又有一定的细节,看完让人茅塞顿开。

比如《自顶向下》、《Linux高性能服务器编程》、《Linux多线程服务端编程》、《STL源码剖析》、《Effective C++》、《CSAPP》、《程序员自我修养》等都有各自想要论述的主线在里面,看起来也是一环扣一环,非常循序渐进。

我的看书方法就是对于面向知识体系那种堆砌细节的书,我们先浏览目录,做到对整本书有映像,再大致看一些我们关心的部分,比如 UNP 和 APUE 中 IO、文件、进程控制、信号、线程、线程控制、基本套接字编程 这些是比较重要的模块,其它边角知识,可以用到再去查。

还有一点,很多同学反映看不懂类似 CSAPP 这样的书,那我们都知道,任何一本书基本上都是有前置依赖的。

没有掌握要求的背景知识去看肯定很吃力的。

就比如我大一下只有基础的 C 知识和一丁点计算机导论知识,然后屁颠屁颠的跑去看 CSAPP(学长毕业摆地摊卖书我瞎买的),那时候只知道这本书被誉为神书,但是看到前两三章就蒙了,真的有点难,对于当时的我来说太底层了,根本不知道在说啥,看过也只是看过,就像天空飞过鸟儿,但没有痕迹。

直到后来大三再次拿起,我才意识到这本书的伟大之处就在于将计算机不同学科知识有机的串在了一起。那时候看,更多是一种补充、深入学习以及完善了,因为很多知识分别在数字逻辑、汇编语言、操作系统这些课程中学过了。

所以要明白,你看不懂不是因为你笨没天赋,而是你有前置依赖的知识没有完成,还没学会走,就想跑了。

一般来说,每本书的首页会介绍看这本书需要哪些前置知识,可以关注一下。

还有一种看书的方法,我在复习的时候采用过,那就是横向学习。

比如我复习操作系统,在《操作系统:精髓和设计原理》中看到了关于内存、虚拟内存的各种介绍,看完理论再去看《Linux内核设计与实现》12 章「虚拟内存」、15 章『进程地址空间』,最后再去看《CSAPP》第 9 章『虚拟内存』,这样看下来,基本上内存这块理解得比较透了,这些书关于这块的介绍是各有优缺点的,正好互补。

又比如在《精髓与设计原理》中介绍了进程加载和链接,其实讲得比较偏理论,看完还是觉得似懂非懂。

那我又会去《CSAPP》看第 7 章「链接」,这一章基本讲清了静态链接、目标文件、可重定位目标文件、引用解析、加载这些关于链接的核心概念,但是一个章节讲这么多,难免不够深入。

我又会去看《程序员自我修养》这本书第 4 章「静态链接」、第 6 章 「可执行文件的装载与进程」、第 7 章 「动态链接」,这本书核心主题就是链接、加载,所以这一路看下来,对于链接、加载这块基本上搞得比较透彻了,也许没几个面试官有你清楚。

同样索引你也能从《数据库系统概念》、《高性能MySQL》、《MySQL技术内幕》中挑选对应的章节,串起来看,取每本书优点,这样学习真的很高效也很深入。

这就是我在复习的时候采用的用知识点串联,跨多本书高效精准的复习方式,效果也很不错,春招十几次面试没有一次因为这些基础知识挂过。

啰嗦了一大堆,就是回答这些问题的:

“有些书看的找不到重点,看不下去了,怎么办,比如深入理解计算机系统,UNIX 网络编程,APUE,求指教”、“我不是科班 CSAPP 可能不是全部看得懂,该怎么办?”、“大佬,这么多书看得完吗”、“刚开始看这些书很痛苦怎么办”

三、要花多久时间才能学完

这个不好说,根据你的基础和学习效率不同,比如我大一、大二对于一些基础的知识学得比较认真,基础还算可以,按照这样一套走下来也就大半年。大概每天花四五个小时以上吧。

如果真的是连计网、操作系统理论这样的东西一点基础都没的话,那估计得一年以上,毕竟这些内容基本覆盖了科班的核心课程,人家上三年课,你一年解决,已经算很快了好吧。

一年真的足够从小白学起吗?

感觉是差不太多的,但是估计得每天付出五六小时以上了,并且学习方法得当。

你去牛客就会发现,存在各路大三、研一自学转码的同学,最后还能成为 offer 收割机,所以,不要怀疑一年不够,最关键的是你要找到正确的路线,然后执行下去。

文中推荐的书真的全部要看吗?

当然不是,我自己都没看完,但是我的策略已经说过了,基本上大部分书都看了重要的章节,这样看起来是很快的。

并且随着你看书越来越多,基础越来越好,你会发现每本书前面几章都是铺垫基础知识,大部分可以直接跳过,举个例子

《Linux高性能服务器编程》这本书前几章是这样的:

  • 第1章 TCPIP协议族
  • 第2章 IP协议详解
  • 第3章 TCP协议详解
  • 第四章 TCPIP通信案例:访问Internet上的Web服务器
  • 第五章 Linux网络编程基础API

你觉得这些章节在看过《自顶向下》、《TCP/IP详解》之后还有必要看吗?我反正是半天扫过去就完了。

最后,不管说再多方法,再多的路线,最终都需要自己花时间去啃、去执行。

四、语言疑惑

还有一个很多选择 C++ 方向同学都存在的疑惑,在这里我也想解释一下:

C++ 语言特性多,又难学,很多都是底层开发才会用到,C++ 就是个坑,是否应该转 Java、Go 呢?

当然不是的,的确在头条、美团、阿里这种业务部门使用 Go、Java 系更多,首先还是那个观点,校招生对于企业来说都是一张白纸,面试官考察的是你的基础知识和聪明度,来决定是否有培养潜力,语言确实不重要。

那你可能会说,明明各种面经上常常出现 ConcurrentHashMap、虚表、虚函数实现机制这样和语言强相关的问题。

在我看来啊,面试深入问一些语言实现细节,其实不是在考你语言,而是看你是否有主动钻研的意识,是不是只停留在应用的层面,同时也借语言考察一些数据结构、操作系统方面的基础知识。

所以呢,我觉得 C/C++、Java、Go 你深入学习哪一个都可以,关键还是找对相应的学习路线,一直坚持学下去,不要每天都停留在我到底是学 Java 好还是 C++ 好这样无解的问题。

另外,想对学 C++ 的同学说,可能你会发现身边同学都在搞 Java、Go之类的,找工作缺少一些一起复习准备的朋友,有些甚至劝你别学 C++。那这个时候你一定要坚定自己的选择,多在牛客或者网上找找同方向的朋友一起交流、学习。

说实话,就找工作这块来说,个人觉得区别真的是不大的,不管从薪资、面试难度来说都是差不太多,更多的还是算法和基础知识。

而且也有不少同学 Java 进腾讯需要转 C++,C++ 进阿里、美团需要转 Java 的,这都不是事儿。

那 C++ 目前应用场景有哪些呢?

一句话,对性能或者执行效率要求比较高的应用,比如游戏引擎、infra、推荐引擎、存储等,当然也能拿来写业务(没错说的就是鹅厂),也有做 C++ 客户端开发的,主要是 MFC、QT 等。

说实话,像游戏引擎、infra这类都是门槛比较高的,并且招聘的数量也有限,一般人很难进,而且目前互联网公司的业务部门大多使用的是 Java、Go这类语言。

所以 C++ 的需求量是相比 Java、Go这类少很多的,但是同时学习 C++ 也没 Java 那么多,所以相对来说竞争还没那么大,并且 C++ 学的不错,你同样可以去面阿里、美团这种 Java 技术栈的公司,大厂基本不会限制语言的。

日常求三连环节

这篇文章我预感到会写成万字长文,但是没办法,想表达的东西太多了,大家记得收藏~

还有,我把文章提到的这些书籍、资料、还有我面试时看过的一些面经、博客都汇总了,可以节省你去找资料的时间。

这是百度网盘链接:https://pan.baidu.com/s/1-944EqDz3WslYSFc5o5xjQ
提取码: b6mw

链接失效的话,请加我微信领取:haveylin

(本文首发于个人公众号「编程指北」,欢迎关注,第一时间获取干货分享~

如果对大家有帮助的话,记得点个赞,收藏下噢,

我是小北,关注我,咱们下周期见!

查看原文

赞 11 收藏 5 评论 3

编程指北 分享了头条 · 9月21日

本文以大量图解详细的介绍了对称加密和非对称加密,以及数字签名的原理,可以帮助很好的理解签名、证书这两个容易混淆的概念。

赞 1 收藏 8 评论 0

编程指北 发布了文章 · 9月20日

一文彻底搞懂加密、数字签名和数字证书!

微信搜索🔍「编程指北」,关注这个写干货的程序员,回复「资源」,即可获取后台开发学习路线和书籍

前言

这本是 2020 年一个平平无奇的周末,小北在家里刷着 B 站,看着喜欢的 up 主视频。

在一旁玩手机的女朋友突然问”你知道数字证书是来干啥的不,为啥浏览器提示证书不可信?”

你要说这个,那我可来劲了,于是乎从加密、数字签名一直讲到了数字证书。。。终于把女朋友讲睡着了,独自写下这篇文章。

正文

如果你能非常清晰的回答出以下问题,可以直接拉到最下面帮我点个赞~,把时间用去陪陪女朋友:

  1. 非对称加密中公私钥都可以加密,那么什么时候用公钥加密,什么时候用私钥“加密” ?
  2. 什么是数字签名,数字签名的作用是什么?
  3. 为什么要对数据的摘要进行签名,而不是直接计算原始数据的数字签名?
  4. 什么是数字证书,数字证书解决了什么问题?

这篇文章,主要围绕数字签名数字证书的原理以及它们的作用展开。

争取做到让不具备任何密码学基础知识的同学都能听懂,所以在这里需要先对齐一些加密相关的概念 。

1. 什么是加密

加密就是对明文数据按某种特殊算法进行处理,使其成为不可读的一段代码,通常称为“密文“, 密文通过”密钥“解密后还原出原来的明文,通过这样的途径可以达到保护数据不被非法人窃取、阅读的目的。

定义简单吧?那来看个题,考虑以下哪些属于加密方法:

  • AES
  • RSA
  • MD5
  • BASE64
  • SM4

这几种都是日常开发中常用的数据编码技术,但是只有 AES、RSA、SM4 才能算是加密方法。

为什么呢?一个区分的简单方法就是看编码后的数据是否还能还原,能还原的是加密。

MD5 实际上是对数据进行有损压缩,无论数据有多长,1KB、1Mb 还是 1G,都会生成固定 128 位的散列值,并且 MD5 理论上是不可能对编码后的数据进行还原的,即不可逆。

MD5 因为其具有不可逆性、单向恒定性(相同的数据多次计算值不变)被广泛应用于文件完整性验证、口令加密以及接下来会讲到的数字签名中。

至于 BASE64 是否算做加密方法,仁者见仁。在这里不下结论,因为 BASE64 编码不需要密钥,且编码后的字符串任何人都可以解码出原串,所以一般不认为是加密方法。BASE64 常用来做转码,把二进制字节序列转化为 ASCII 字符序列。

2. 加密算法的分类

加密算法按照加解密使用的密钥是否相同,可分为:

  • 对称加密(Symmetric Cryptography)
  • 非对称加密(Asymmetric Cryptography)

1. 对称加密

对称加密是指加密和解密时使用同一个密钥。

2. 非对称加密

非对称加密是指加密和解密使用不同的密钥,这两个密钥分别叫做「公钥」、「私钥」。

公钥是可以公开给所有人的,而私钥需要自己保密的。

公钥加密的数据只能用私钥解密:

同理,私钥“加密”的数据只能用公钥“解密”:

大家注意到没,我对 私钥“加密” 这里打了引号,为什么呢?

因为私钥不是用来加密的,准确的说法应该是 「私钥签名,公钥验签」

这个问题很多同学都存在误解,认为公私钥都可以用于加密。

实际上不是的,至于为什么,后面讲完签名我会解释的。

3. 故事开始

为了讲这个故事,小北请来了密码学中常用的学术情侣,Alice 和 Bob,以及窃听者代表 Eve。

我们从 Alice、Bob 约会的故事展开,来讲讲其中暗藏着哪些危机,又是如何一步步化解的。

3.1 第一回合

九月,一个夜黑风高的晚上,Bob 想约 Alice 出来玩,于是给 Alice 发了一封邮件:
明文通信

但我们都知道网络是不可信的,并且由于消息在网络中是明文传输的,所以黑客可以轻易的截获、篡改甚至冒充 Bob。

来,我们看看黑客 Eve 是怎么干的:

黑客窃听伪造

瞧,Eve 轻易的拿到了邮件内容 (窃听),并且修改了邮件内容 (篡改),甚至说他可以随时冒充 Bob 给 Alice 发送邮件 (伪装)

如果上图中 Eve 伪造的内容被 Alice 接收到了,那么后果可想而知。

现实世界中,我们每天都在通过网络进行聊天、转账、浏览不存在网站。

如果都是这样明文传输数据,显然毫无安全感。

3.2 第二回合

既然我们不能明文传输,那么 Bob 和 Alice 提前商量好密钥,使用对称加密对邮件内容加密不就好了~

对称加密

现在 Bob 发送的邮件都使用和 Alice 提前商量好的密钥加密后再传输。

由于没有密钥,Eve 就算截获到数据也无法获取邮件的内容,也没法篡改和冒充 Bob。

因为篡改后的数据必须使用密钥再次加密 Alice 才能正确解密。

那么只要 Bob 和 Alice 能够保证 密钥不泄露,整个通信就是安全的。

如果密钥泄露,被中间人截获,那么就等同于明文通信。

所以我们不能把安全性寄托在人上面。

并且这里也存在一个问题,如果两个人不能线下见面, 如何在网上安全的交换密钥呢?

这似乎是无解的,因为交换密钥的时候我们必须明文通信,不然对方根本看不懂。但是明文交换即意味着可能泄露。

但是别忘了我们的密码学工具箱里还有一个好东西— 「非对称加密」

Bob 和 Alice 各自生成一对公私钥,因为公钥本来就是公开的,即可以被任何人获取,所以可以通过网络明文交换公钥。

然后使用公钥加密邮件内容后发送给对方,接收者使用自己的私钥即可解密。完美~

3.3 第三回合

来看看,在非对称加密体系下,Bob 如何给 Alice 发消息的。

首先 Alice 需要先生成一对公私钥,私钥只能 Alice 自己知道,公钥是可以让任何人都知道的,因此可将公钥直接发送给 Bob,就算被截获也无所谓。

非对称加密

Bob 使用 Alice 的公钥加密邮件内容,加密后的内容只能由 Alice 的私钥解密,所以就算 Eve 截获也是徒劳。

反之,如果 Alice 想给 Bob 回信,就需要用 Bob 的公钥加密后发送。

这就解决了密钥交换问题,也保证了邮件内容不会泄露。也就是说现在可以防窃听

3.4 如何证明 Bob 是 Bob

不知道你注意到没有,这里也存在另外一个问题:

Eve 也可以使用 Alice 的公钥冒充 Bob 给 Alice 发邮件啊,因为 Alice 的公钥本来就是公开的,任何人都可以获得。

由于 Eve 也可以获得 Alice 公钥,所以没法防止 Eve 伪造篡改,并且对于 Alice 而言,她无法分辨出邮件到底是 Eve 发的还是 Bob。

所以这个问题的本质就是 「Alice 如何确认邮件来自于 Bob」

那么在生活中,我们如何做这件事呢?

那就是让 Bob 在纸上签名并且按手印,因为指纹和字迹是 Bob 独有的,其它人很难伪造。

所以我们需要在计算机中引入类似的机制:

即只有 Bob 自己能够产生的独一无二的标志,并且其它人能够验证这个标志确实是属于 Bob的。

这就是我们今天要讲的主题—「数字签名」。

还记得什么是 Bob 独有的吗?

对,就是 Bob 自己的私钥,Bob 用自己的私钥对邮件内容计算一个「签名」,将「签名」和邮件内容一起发送出去,接受者 Alice 可以使用 Bob 的公钥验证这个签名是否正确,这就叫「验签」。

如果不是 Bob 的私钥计算的签名,那么 Alice 用 Bob 公钥验签将会出错。

可以看到, Eve 试图使用自己的私钥计算签名然后发送给 Alice, 但是 Alice 使用 Bob的公钥进行验签时将会出错!

那么 Eve 可能篡改内容并冒充 Bob 的签名吗?不可能!因为内容发生改变时,对应的签名也需要重新计算,而签名的生成依赖于私钥,只要 Bob 的私钥不泄露,签名就不会被冒充。

啊啥?你说万一私钥泄露了怎么办?那就当我没说......

所以使用数字签名,我们能够鉴别消息的发送者,也就是说黑客无法伪装发送者进行发送数据,也无法篡改。

注意:

可以看出我们这里数据是明文传输的,存在窃听风险。但是我们为了阐述数字签名机制是如何运转的,故意将保证信息机密性的机制省略了。

如果想要保证数据的机密性,我们常见的做法是,通信双方通过非对称加密安全交换对称加密的密钥,后续通信过程的数据都使用对称加密保证数据机密性。

并且「签名」的作用本身也不是用来保证数据的机密性,而是用于验证数据来源的防止数据被篡改的,也就是确认发送者的身份。

一般而言,我们不会直接对数据本身直接计算数字签名,为什么呢?

因为数字签名属于非对称加密,非对称加密依赖于复杂的数学运算,包括大数乘法、大数模等等,耗时比较久。

如果数据量大的时候计算数字签名将会比较耗时,所以一般做法是先将原数据进行 Hash 运算,得到的 Hash 值就叫做「摘要」。

「摘要」就像人的指纹一样,可以代表一个人,只要内容发生了改变,计算出来的摘要也应该变化。

「摘要」最好是不可逆转的,一般使用开头提到的 MD5 作为 Hash 函数,MD5 输出的结果固定位 128 位。

为什么「摘要」最好是不可逆转的?

因为既然 Alice 可以用 Bob 公钥解开签名,那么理论上其它人,比如 Eve 也可以使用 Bob 公钥解开签名拿到数据。

所以我们最好对数据的「摘要」进行签名,这样,Eve 就算解开签名,拿到的也是「摘要」,如果摘要是不可逆转的,也就是无法从摘要反推出原文,也就达到了保密的作用。

发送者使用私钥对「摘要」计算数字签名。那么接收者如何验证呢?

接受者 Alice 收到后,取下数字签名,同时用 Bob 的公钥解密,得到「摘要1」,证明确实是 Bob 发的

( 画外音:如果使用 Bob 的公钥验证签名出错,那么签名一定不是 Bob 的私钥生成的)

再对邮件内容使用相同的散列函数计算「摘要2」,与上面得到的「摘要1」进行对比,两者一致就说明信息未被篡改。

这样两步分证明发送者身份和保证数据未被篡改。

3.5 这就够了吗?

Bob 和 Alice 现在可以依赖于对称加密进行保密通信,也可以依赖于数字签名验证消息是否是对方发送的。

但是这一切的根基是建立在 Alice 持有的公钥确实是 Bob的,反之亦然。

什么意思呢?

试想,Eve 如果将自己的公钥冒充 Bob 发送给 Alice,然后 Alice 保存了下来,那以后凡是 Bob 发送的消息,反而会验证签名失败,被当做冒充者。
那你可能会问,为什么 Eve 可以将自己的公钥发送给 Alice,而 Alice 毫不知情呢?
伪造公钥

看!我们又回到了最初的起点,只不过这次被篡改的是公钥,之前是消息本身。

因为 Bob 的公钥是直接通过网络发送给 Alice的,所以 Eve 才可以在这一步做手脚,进行篡改,将自己的公钥冒充 Bob 发送给 Alice,也就是发送公钥这一步没有做到:

  • 防篡改
  • 防冒充

防篡改怎么和防冒充怎么实现的呢?

我们前面讲了,就是靠数字签名! 但是数字签名需要接受者持有发送者公钥,才能进行验签。

而我们现在处理的是分发公钥这一步,所以.......死锁了。这像是先有鸡还是先有蛋的问题

现在的问题就是「Bob 无法证明它自己是 Bob」。

这个是不是似曾相识,以前去办事的时候经常被要求出具「我妈是我妈」这类证明。但是我们自己说“我妈就是我妈”,人家根本不会信呀,需要一个可信第三方出具证明,比如派出所。

那么「Alice 如何才能确认 Bob 发送给自己的公钥确实是 Bob 的,而没有被篡改?」

在只有 Alice 和 Bob 两人的情况下是没法验证的。

所以,我们这里也需要一个第三方帮 Bob证明 「Bob 的公钥就是 Bob 的公钥」,有点绕口令那感觉了~

3.6 数字证书

为了解决这个问题,就引入了「数字证书」,什么叫数字证书呢?

百度百科:

数字证书是指在互联网通讯中标志通讯各方身份信息的一个数字认证,人们可以在网上用它来识别对方的身份。

因此数字证书又称为数字标识。数字证书对网络用户在交流中的信息和数据等以加密或解密的形式保证了信息和数据的完整性和安全性。

看了这个描述,是不是感觉还是云里雾里,还是我用大白话来说吧~

只要你理解了前面的数字签名,就能理解这里的数字证书,因为我把数字证书叫做「公钥的数字签名」。

为什么呢?我们引入数字证书的目的是为了保证公钥不被篡改,即使被篡改了也能识别出来。

而防篡改的方法就是数字签名,但是这个签名不能我们自己做,原因说过了,因为我们的公钥还没分发出去,别人无法验证。

所以只能找可信的第三方来帮我们签名,即证书颁布机构(CA),CA 会将:证书的颁布机构、有效期、公钥、持有者(subject)等信息用 CA 的私钥进行签名。

并且将签名结果和这些信息放在一起,这就叫做「数字证书」。

这样,Bob 就可以去 CA 申请一个证书,然后将自己的证书发给 Alice,那么 Alice 如何验证这个证书确实是 Bob的呢?

当然是使用 CA 的公钥进行验签。

注意:

CA 的公钥也是需要使用证书来分发的,所以 Alice 的电脑必须安装 CA 的证书,证书里包含了 CA 的公钥。

收到 Bob 发过来的数字证书后,Alice 使用 CA 的公钥进行验证,验证通过即证明这确实是 Bob 证书,也就可以使用证书中包含的 Bob 的公钥,按照之前讨论的流程进行通信。

那么 Eve 是否可以在中途篡改 Bob 的证书呢?

答案是不行,因为证书的信息使用 CA 的私钥进行签名,只要 Eve 修改了任何一个 Bit 都会导致最后签名验证不通过。

那 Eve 可不可以修改证书信息后自己重新计算一次证书的数字签名呢?

也不行,因为证书的数字签名计算依赖于 CA 的私钥,Eve 是拿不到 CA 的私钥的。

如果拿到了,说明什么?整个世界都是不可信的。

3.7 数字证书长啥样

这是我电脑中的自带的证书:

电脑自带证书

可以看到,包含了证书持有人的公钥和证书的签名。

另外,证书颁发机构是有层级关系的,下级 CA 的证书是需要由上级 CA 签名的。

换句话说一定存在根证书颁发机构,那么他们的证书是由谁签名的呢?

答案是自签,自己给自己认证。

这是我电脑中的一个自签的根证书颁发机构:

自签的根证书机构

为什么根证书可以自签,谁来保证安全?

你把钱存在银行,你会担心吗?我们基于对国家的信任,才信任银行,这就是信任链的基础!我们思考问题应该是分层的,如果不认可一个统一的基础,一直套娃下去,那么问题就无解。

那还有个问题,如何保证根证书的可靠性?
这是操作系统和浏览器预装的,由微软、苹果等操作系统厂商来选择根证书。

3.8 证书不可信?

那么什么情况下浏览器会提示 “证书不可信” 呢?

根据我们上面的分析,下面是可能的原因:

  1. 证书不是权威 CA 颁发

有些企业为了贪图便宜使用盗版的证书,没有经过 CA 认证。也就是无法使用浏览器内置 CA 公钥进行验证。

  1. 证书过期

上面说了,证书里有一项就是有效期,一般就是一年或者两年的时间。如果证书过期,那么浏览器就会提示“证书不可信”

  1. 证书部署错误

可能是服务器证书部署出错,比如证书与域名不匹配,因为证书里有一项是持有人信息的。

好了,饶了一大圈,Bob 终于可以安全的向 Alice 发出前往红树林的邀请了~

附录

QA

现在我们来回答文章开头提出的一些问题:

  1. 非对称加密中公私钥都可以加密,那么什么时候用公钥加密,什么时候用私钥“加密” ?

    • 加密场景,那么肯定希望只有我才能解密,别人只能加密。即公钥加密,私钥解密。
    • 签名场景,既然是签名,就希望只能我才能签名,别人只能验证。即私钥签名,公钥验签
  2. 什么是数字签名,数字签名的作用是什么?

    • 数字签名就是使用私钥对数据摘要进行签名,并附带和数据一起发送。
    • 可以起到防篡改、防伪装、防否认的作用。
  3. 为什么要对数据的摘要进行签名,而不是直接计算原始数据的数字签名?

    • 数据可能比较大,签名是使用非对称加密算法,比较耗时
    • 防止第三方使用公钥解开签名后,拿到原始数据
  4. 什么是数字证书,数字证书存在解决了什么问题?

    • 数字证书就是由 CA 机构使用自己私钥,对证书申请者的公钥进行签名认证。
    • 数字证书解决了如何安全分发公钥的问题,也奠定了信任链的基础。

絮叨

大家如果觉得写得不错,可以帮帮点个关注,点赞~

你们的三连就是我创作的最大动力!

我是小北,一个每天在电脑前敲键盘的硬核男人,我们下期见!
image.png

查看原文

赞 11 收藏 8 评论 2

编程指北 发布了文章 · 9月6日

我是如何纯靠技术在大学月入上万,收获人生第一个10W

微信搜索🔍「编程指北」,关注这个有趣、有干货的程序员

前言

这个号的读者应该大多也是程序员或者互联网相关行业的,计算机这个专业薪资的确比一些传统工科高不少,甚至很多应届生的薪资比传统行业工作十来年的人还高,这还不在少数,至于这样的现象还能持续多久,我也不知道。

但是高收入必然会吸引大量的转专业、转行的同学过来,去一个高速发展的行业享受行业发展的红利这无可厚非,去年也有几个学机械、化工的高中同学找我要学计算机的经验。

毕竟一个人的命运,除了要靠自我奋斗,也要考虑历史的进程。

第三次工业革命的基础就是 信息技术 + 智能制造。搭上时代的快车,这是个人成长的最快途径。 各位在车上的更要努力抢到头等座、卧铺,毕竟站票可能会越来越挤 ,这大概就是最近互联网上流行一个词「内卷」。

说回来,为啥我单独写一篇关于赚钱的,不会显得太现实了吗? 什么是现实?北上广深高不可及的房价、上下班拥挤的地铁、十来平还特么要三四千的出租屋...... 谁不想过潇洒的生活呢,可以不为学习,就单纯的看看书,今天去三亚冲浪、明天又在哈尔滨赏雪。这不是我在贩卖焦虑,而是在深圳,实实在在的感受,经历过没钱,所以才想努力赚钱。

家庭没法选择,那就只能选择改变家庭了。

打从上大学起,我就一直在想赚钱的方法。找来找去,发现没技能、没颜值的我只能去发发传单、淘宝刷单,打打零工。。。

所以我放弃了,一方面是当时的自己抹不开面子,另一方面是觉得报酬太低而且没有什么成长性,除了让自己脸皮更厚。。。

正文

一、外包梦夭折

后来偶然听说某个学长做外包写 APP,每个月能有几千的收入,对于一个月生活费才一千多点的我来说,这吸引力比妹子什么的大多了好吧(这可能也是我单身这么多年的原因吧。。。

但当时我还是菜鸟一个,连 C 语言都还写不利索,更不懂什么是前端什么是后台,所以那段时候我就快速的学习各种做外包需要的技能,从 Java SE、Java 后台、安卓开发 我全学了个遍。

到后来自认为已经可以出山的时候,就开始到处找外包做。但是找了一圈发现,太特么难了,他们要做的简直就是翻版淘宝、翻版美团这种。又要界面美观、功能需求又复杂,就像下面这种:

image

这一套要是做下来,课余时间全得花里面去,而且我也写不出这么精美的界面。

还未开始的外包之路就这样夭折。

这类平台有很多,基本上我都注册过,比如:

  • 程序员客栈
  • 码市
  • 猪八戒
  • 开源众包
  • ...

不过亲身体会之后,我不建议你去上面做,为啥?因为上面的外包基本就是让你开发个小程序、APP、网页,这种一般少则几周,多则几个月的开发周期,太耗时间了,而且往往好多甲方经常变更需求,这就很恶心了。

像这种性价比低、学不到东西的外包我是一点也不建议去做的,特别是对于学生来说,第一任务是学习,把时间用在专业学习上,未来的回报会超出你的想象~

二、滴水穿石非一日之功

外包虽然夭折,但心中一直有一个靠技术赚钱的想法,倒不是多缺钱,因为我生活花销不高,也很少网购,所以一个月一千多点绰绰有余。

主要是想体会经济独立的感觉,自己赚钱给爸妈发个红包,买个手机,这感觉不一样。

没办法,外包之路夭折后,我就一直专心学习技术、专业知识,而且我听不少学长学姐拿到大厂的 Offer 都是年薪二三十万,听到这,我可两眼放光了!对不起,当时没见过世面,本以为月入过万就已经很多了,没想到可以这么多。

所以这时候我学习的目标也从最初的接外包,变成了我要去大厂!然后我找了几个拿到阿里百度 offer 的学长问了他们怎样才能像他们一样去大厂,从学长们那学到经验后,我就开始了自己备战的 BAT 之路。

从那时起,图书馆顶楼考研自习室来了一位天天背着电脑和一堆书的同学,每天第一个到,闭馆了走,没错,那就是我。

后来因为计算机专业的书都太厚了,我很难每天背着去图书馆,所以我找了一个空闲的位置,占为己有了。

image

周围都是考研的学长学姐。在这里,你能很明显的感受到大家那种为目标而努力的状态,这种状态是会传染的。我不知不觉的在那度过了好长一段时光。

后来,这张桌子上堆积的书越来越多,越来越高,也越来越厚。

image

中途还和同学把书放一起拍了个照,当做纪念,不过这个桌子上的书只是很少的一部分,大部分在寝室书架上放着,后面给大家看看我大学看的书合集。

image

那时候真的很纯粹,就想多学点知识。看了很多经典的书,这感觉就像张无忌被朱长龄推下山后意外得到《九阳真经》而学会九阳神功。虽然没那么夸张,但是的的确确,是这些大黑书将我零散的计算机知识连成了面,不能说学会了九阳神功,但是成长绝对是巨大的。

所以在校期间找一个学习氛围好的实验室、小团队很重要~

三、小有收获

后来凭借扎实的基础,我顺利拿到了大厂 offer,说实话,去年十月份,HR 打电话聊完薪资后,那一天我都很兴奋,我家人也很高兴。

不为别的,因为我一年的工资比我爸妈之前十来年攒的钱都多。不是说我工资有多高,而是他们收入确实挺低的。

李克强总理不是说中国还有 6 亿人月入 1000 吗?身在互联网的我们,如果家里不是农村的,可能感受不到,也无法理解。
但是中国就是这么大且魔幻,有的地方大雪纷飞,有的地方却艳阳高照,有人可以一天花掉几十万上百万,也有人为了一个月 1000 的报酬而努力工作。努力奋斗,相信世界会更好~

不过只有我心里知道,这点工资在一线城市做不了什么,对于上天的房价来说,简直杯水车薪。有人可能会说,你农村出来的,想一线的房子干什么?说实话,我确实没想过,因为没钱,也不可能拿自己辛辛苦苦敲下一行行代码换的钱去接盘,然后搭上二三十年的房贷。对我来说,实在是不值的。不过没有房子,确实会没有归属感,你会觉得你不属于这个城市,那是一种漂着的感觉,你就是来打工的。

这可能就是北上广容不下肉身 三四线放不下灵魂吧。更要命的是,互联网公司基本都在一二线,回去根本找不到合适的工作。

四、大三月入过万

不对,怎么写着写着就感觉写偏了,差点写成了鸡汤文哈哈哈,赶紧去洗手间用冷水冲冲脸。

还是说回我是如何靠技术在大三就月入过万,收获人生第一个10w+的吧,加上今年的已经十五万多了。

这事也很巧,去年四月份拿到实习 offer 后,我认识了一个朋友,他给我介绍了一个外包平台,但是这里的外包不是像上面提到的那种写 APP、小程序的。这里大多是国外学生的课程作业,他们有些不会做,所以就会找人辅导或者代写然后讲解。

为什么我会推荐这个?有三点原因:

  • 这里面的课程有些质量很高, 我们都知道国外计算机课程有很多非常经典,比如操作系统 MIT 6.828、分布式系统 6.824、编译原理 6.035、哈佛计算机导论CS 50...... 这些课除了讲得好以外,他们很多课程都会有很多实践,包括 assignment 和各种 lab。我是十分推崇这种理论结合实践的教学方法的,但是很遗憾,国内高校似乎很喜欢“理论”,就连各种 985 名校也难免有那种一套 PPT 走天下的老师。所以我们如果自己去做这些 assignment 和 lab,既把钱赚了,又学到了国外的课程,岂不美哉~这简直就是我梦寐以求的外包!
  • 性价比高, 由于都是国外的学生或者留学生,所以你懂得,一般不差钱,反正我的大多数时薪都在100左右,高的三四百也有过,不过偏少。
  • 省心, 这种 lab 和 assignment 大多是用来学习计算机原理的,不会像做 APP 这么恶心,开发十天半个月起步,而且还涉及到各种需求变更。但是这个一般来说我都能在10个小时内完成,看 lab 的难度而定,当然,难度越高,收费也就越高。对自己的挑战和成长性也越高

我在这里赚了多少钱?没仔细算过,但是十五六万是有的,贴下我去年的账单和部分转帐记录吧,我也是从去年开始记账的,小到地铁公交车这种都会记,就是为了清清楚楚的看到自己的钱花到哪去了

image

image

去年十二月份拿到 offer 在家闲得没事...... 时间很多,所以做得挺多的,也达到了顶峰接近 2w,其实你会发现我每个月花销好像都大几千上万,是不是赚得多就花得多呢?是,也不是,去年第一次赚到钱,就给我妈买礼物,给我奶奶装了空调,因为她现在一个人在农村老家住,我们那都是自建房,两三百平,但是没怎么装修,因为基本不会回去住了,所以我只给我奶奶装了,夏天和冬天可以过得舒服点。

后来自己又交了上万的学费,肉疼。。。。

又花了上万(这里也是很坑,农村装天然气,光开户就要七千多。。。)给我奶奶装上了天然气、热水器、燃气灶啥的,所以去年花了不少钱,但是我觉得很值。奶奶做饭再也不用烧柴火了,也能用上自来水、天然气,现在在农村养两只鸡鸭,一猫一狗、捣拾捣拾小菜园,日子也很惬意。到现在,每个月我也会给她打钱,所以他比村里一般的老太太过的好一点。唯一就是缺少了陪伴,没办法,难两全。

这也是我为啥这么想赚钱的原因。无他,缺钱,我需要钱给我家人更好的物质生活。

又偏了,,,继续说回来,我都做了哪些好玩的东西?学到了啥

  • 第一个是文件系统, 这个玩意挺有意思的,就是要让你自己去设计一个文件系统,比如文件、数据块、目录等等,而且你要提供创建、读写、删除、随机读、随机写等等接口,然后通过 Linux 的 VFS(虚拟文件系统) 机制挂载到 Linux 内核中,然后就可以用系统调用 open、read、write 这些来在你的文件系统上进行读写。VFS 是Linux一个机制,它规定了 IO 接口,然后你去实现这些接口,你就可以挂载在上面。通过这个东西我深入的学习和了解了文件系统,报酬也是非常值的。

    image

  • 第二个是SVC, 就是版本管理系统,类似 SVN 和 GIT的,但是相对 GIT 做了很多简化,核心的就是创建分支、分支合并、回滚、commit 这些,通过这个我又去详细了解了 GIT 的实现原理,并且自己写了一个简单版本的 GIT,受益匪浅,也赚到了几千大洋。

    image

  • 第三个是操作系统的锁实现, 这个就是让你基于硬件的原子指令 xchg、cmpxchg 这些实现一个锁,这个需要内联汇编、汇编等知识,简直是硬核、硬核、硬核!!!做完这个也解开了我对锁实现的一些疑惑

    image

  • 还有个是 Web Proxy, 这个就更有意思了,要求用 C++ 写一个代理服务器,类似 Nginx 的代理功能,当然,相比 Nginx 弱鸡很多,
    但是麻雀虽小,五张俱全,而且老外最有意思的是要求代理服务器对一些图片和 HTML 恶作剧,就是代理服务器随机的在原始服务器,返回的响应里 插入一些图片和文字,因为那天是愚人节。。。 真有你的,老外!

    image

  • 还有模拟实现 TCP 可靠传输的, 这个就更硬核了,人家老外老师就是给力,直接做了一个网站,这个网站你可以通过 TCP 连接上去,然后你需要运行几个节点,互相发消息,他们之间的消息都会经过老师的网站,所以通过网站上是可以控制丢包率的,也能控制节点的拓扑结构,要求就是让你基于这种不可靠的信道,做出可靠数据传输!这简直就是翻版 TCP 嘛,超时重传、ACK、滑动窗口啥的都给我上!就是像下面图中一样,圆圈就表示你可达的范围,不同节点形成各种网络拓扑,可以调节网络 丢包率 Loss chance。

    image

  • 也有很多写算法的,数据结构实现的,做机器学习、深度学习的,反正各个方向都有。
  • 还有视频辅导的,这个我做过几次,性价比高很多,一对多的话,我之前带过一对多 6 小时的《System Programing》这门课(就是用 CSAPP 那本书的课,非常硬核)的期末考试复习,时薪 500。也带过一对一 C++ 数据结构与算法的学生,一学期,8000块,我统计了下,大概上了20个小时。。。性价比简直爆炸。要是一直有这种活干,我是不想来上班的。。。
  • 还有好多,反正就是类似这种,在做 lab 的同时,也能学到知识

当然不是每一个 lab 都对我有成长,有些还是单纯的我花时间赚钱而已,并不会学到什么东西。前前后后做了几十个,写了几万行硬核代码。见识了国外各种课程,反正我觉得对我帮助挺大的。

看到这里你可能想让我直接把这个平台丢出来,这种平台很多,我经常用的就 2 个,但是他们都会有一定的要求:

  • 技术过硬
  • 学历不错

有一个直接要求 985、211,但是没关系,根据我做这么久的经验,学历没啥用,就是他们为了省事,缩小筛选范围的。

我在这说出来,也不是给他们打广告的,而是觉得这个确确实实对于在校的学生有一定好处,毕竟又能学知识、又能赚点零花钱,好东西的为啥不推荐呢?

不过我提出几点要求,如果满足的话,你可以在后台回复「赚钱」,联系我,我帮你推荐,学历应该不是问题

  • 技术不错, 比如你 C、C++、Java、Linux系统编程、网络编程、算法、机器学习、深度学习等等,一定要觉得自己学的很不错,有能力完成比如上面我写到的那些东西。
  • 大二以上,为什么? 因为大多数同学大一还在迷茫期,这个时候是最好的学习时间,先去积累好技术、基础,当然如果你觉得你大一技术也已经很好了,也可以来找我,不过我会提几个问题考考你哟。
  • 有闲。什么叫有闲, 比如你每天有几个小时不知道干啥,经常是刷B站、玩游戏、看剧度过的话,找点事做总比无所事事好。如果你是在准备秋招、考研、或者其它考试,就不要来了,先好好准备。等拿到 offer、考完研有空余时间了,可以来试试。

注意:上面三个条件是同时满足哈,或者有不满足的,你可以给我说明下,也行。。。不过尽量不要。

还有就是我只是推荐一个平台,至于能不能赚到钱,赚多少钱,就是看你自己的技术如何了,反正我觉得大多数都还挺好做的,有难的也有基础的作业。比如实现 BST、Java 基础编程等等。

不过现在我已经不做了,为啥?

  • 第一,因为我觉得对我来说性价比不高了,我工作的时薪应该比这个高。好好做好工作,收益会更大。
  • 第二,就是我没闲了,程序员,你懂的,虽然不至于 996,但是每天下班回家也九点多十点了,哪还有精力做。至于周末,我现在都用来学习和写文章了。刚开始工作,要学习的还有很多。学习永远是第一位的,没有输入,工作哪来的输出。
  • 第三点,很多东西对我来说,没有太大的成长了,变成了纯碎时间换金钱的了,我不想做这种。

那你可能会说,你自己都不想做了,还推荐给读者?我不是说了嘛,我不做是因为工作忙、工作性价比更高。

但是如果你还在校,又符合我说的那三个条件,那既能赚点零花钱,又能学点国外的计算机课程 lab,有什么不好呢?

当然,我只是写出我的经历,如果有需要的话,我可以搭个线,仅此而已。

五、说点题外话

我赚的这点钱不多,也算不上什么,有很多大佬通过比赛、写文章比我赚的多多了,比如帅地。我也认为这样的事情才是值的长期去做的,有成长,有复利。

所以现在除了工作以外,我还在坚持做两件事:

  • 写作, 通过文章把自己的技术积累和经历分享出来,同时收获一批读者
  • 理财,理的不是财,而是生活, 我很认可这句话。自从理财之后,我更加注重对自己的日常花销、财务规划。当然,我不是鼓励大家去炒股、买基金,而是说对于自己的财务一定要有规划,懂得合理利用一些理财工具实现财产的保值增值。不要一直做月光族,存款真的会带来很大的安全感!股市有风险,入场需谨慎。

说了这么多,最后说一句,如果你是做技术的,一定要热爱技术,努力把它学好,这是技术人赖以吃饭的根本!

最后上一张持仓图吧,免得又被人说刚编故事

image

这是拿来买基金的一部分~

最后说一句(求关注)

写这篇文章不为别的,就是觉得自己通过这个赚到了钱,学到了东西,还有点用,想推荐给我的第一批读者们~

如果你想了解平台是什么,关注 「编程指北」,后台回复「赚钱」。

就算不做也欢迎大家后台加我微信,一起交流,一起赚钱!

原创不易, 希望路过的大佬们顺手点个关注、在看, 茫茫人海相遇不易~

我是小北,一个有干货的程序员,我们下期见!

(暂时没有留言功能,大家可以在后台给我回复,每一条我都会看的~

查看原文

赞 0 收藏 0 评论 0

编程指北 分享了头条 · 9月6日

高性能、高并发、高可用是后台开发常见的问题, 本文旨在探讨和总结后台架构设计中的一些常见技术和方法,并且把他们分类为不同的编程最佳实践。

赞 0 收藏 11 评论 0

编程指北 发布了文章 · 9月5日

一文搞懂后台高性能服务器设计的常见套路, BAT 高频面试系列

微信搜索🔍「编程指北」,关注这个写干货的程序员,回复「资源」,即可获取后台开发学习路线和书籍

前言

金九银十,又是一年校招季。

经历过,才深知不易。最近,和作为校招面试官的同事聊了聊,问他们是如何去考察一个学生的,我简单归为以下几点:

  1. 聪明、反应快,这点自不必说,聪明意味着学习能力、适应力强,能够快速胜任工作。
  2. 算法不错,代码基本功好,这点其实考察的是算法能力和代码是否写得优雅。
  3. 基础过硬,技术岗面试最核心的还是考察「技术储备」,包括了语言基本功,操作系统、网络、体系结构、系统设计。
  4. 语言组织和表达能力,这点很重要,很多同学懂得某个知识点,却很难用简洁准确的语言表述出来。

想必有很多同学在刷题、刷面经,不过我想说“面经虽好,不要贪杯哦~”,面经可以刷,看看面试官都是怎么提问的,但不要寄希望于原题。
因为面试过程中的问题往往是一环扣一环的,这意味着你需要有足够的技术深度,将知识由点连接成面,而不是停留在相互孤立的知识点上。

所以还是建议系统性的看书,如果觉得时间不够,可以关注书里的重点章节。至于看哪些书?后面也会列一个我的书单和阅读建议。在【编程指北】后台回复【书单】即可获取

那么回到技术面试上,除了算法和网络、操作系统这种基础之外,还有一类系统设计和优化的问题。这类问题需要你有一个全局的技术视野,以及熟悉一些常用的系统优化方法论,也就是工程上的一些 Best Practice,而不至于自己临时拍脑袋瞎设计。

在互联网公司,经常面临一个“三高”问题:

  • 高并发
  • 高性能
  • 高可用

这篇文章将总结一下后台服务器开发中有哪些常用的解决“三高”问题的方法和思想。

希望这些知识,能够给你一丝启发和帮助,助力你收割 各大公司 Offer~

先上本文思维导图:

如何解决三高

正文

一、缓存

什么是缓存?看看维基百科怎么说:

In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.

在计算机中,缓存是存储数据的硬件或软件组件,以便可以更快地满足将来对该数据的请求。 存储在缓存中的数据可能是之前计算结果,也可能是存储在其他位置的数据副本

缓存本质来说是使用空间换时间的思想,它在计算机世界中无处不在, 比如 CPU 就自带 L1、L2、L3 Cache,这个一般应用开发可能关注较少。但是在一些实时系统、大规模计算模拟、图像处理等追求极致性能的领域,就特别注重编写缓存友好的代码。

什么是缓存友好?简单来说,就是代码在访问数据的时候,尽量使用缓存命中率高的方式。这个后面可以单独写一篇 CPU 缓存系统以及如何编写缓存友好代码的文章。

1.1 缓存为什么有效?

缓存之所以能够大幅提高系统的性能,关键在于数据的访问具有局部性,也就是二八定律:「百分之八十的数据访问是集中在 20% 的数据上」。这部分数据也被叫做热点数据。

缓存一般使用内存作为存储,内存读写速度快于磁盘,但容量有限,十分宝贵,不可能将所有数据都缓存起来。

如果应用访问数据没有热点,不遵循二八定律,即大部分数据访问并没有集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。每次访问都会回源到数据库查询,那么反而会降低数据访问效率。

1.2 缓存分类

  • 1. 本地缓存:

    使用进程内成员变量或者静态变量,适合简单的场景,不需要考虑缓存一致性、过期时间、清空策略等问题。

    可以直接使用语言标准库内的容器来做存储。例如:

本地缓存

  • 2. 分布式缓存:

    当缓存的数据量增大以后,单机不足以承载缓存服务时,就要考虑对缓存服务做水平扩展,引入缓存集群。

    将数据分片后分散存储在不同机器中,如何决定每个数据分片存放在哪台机器呢?一般是采用一致性 Hash 算法,它能够保证在缓存集群动态调整,不断增加或者减少机器后,客户端访问时依然能够根据 key 访问到数据。

    一致性 Hash 算法也是值得用一篇文章来讲的,如果暂时还不懂的话可以去搜一下。

    常用的组件有 MemcacheRedis Cluster 等,第二个是在高性能内存存储 Redis 的基础上,提供分布式存储的解决方案。

1.3 缓存使用指南

1. 适合缓存的场景:

  • 读多写少:

    比如电商里的商品详情页面,访问频率很高,但是一般写入只在店家上架商品和修改信息的时候发生。如果把热点商品的信息缓存起来,这将拦截掉很多对数据库的访问,提高系统整体的吞吐量。

    因为一般数据库的 QPS 由于有「ACID」约束、并且数据是持久化在硬盘的,所以比 Redis 这类基于内存的 NoSQL 存储低不少。常常是一个系统的瓶颈,如果我们把大部分的查询都在 Redis 缓存中命中了,那么系统整体的 QPS 也就上去了。

  • 计算耗时大,且实时性不高:
    比如王者荣耀里的全区排行榜,一般一周更新一次,并且计算的数据量也比较大,所以计算后缓存起来,请求排行榜直接从缓存中取出,就不用实时计算了。

2. 不适合缓存的场景

  • 写多读少,频繁更新。
  • 对数据一致性要求严格: 因为缓存会有更新策略,所以很难做到和数据库实时同步。
  • 数据访问完全随机: 因为这样会导致缓存的命中率极低。

1.4 缓存更新的策略

如何更新缓存其实已经有总结得非常好的「最佳实践」,我们按照套路来,大概率不会犯错。

主要分为两类 Cache-AsideCache-As-SoR。 SoR 即「System Of Record,记录系统」,表示数据源,一般就是指数据库。

1、Cache-Aside:

Cache-Aside架构图

这应该是最容易想到的模式了,获取数据时先从缓存读,如果 cache hit 则直接返回,没命中就从数据源获取,然后更新缓存。

写数据的时候则先更新数据源,然后设置缓存失效,下一次获取数据的时候必然 cache miss,然后触发回源

直接看伪代码:

Cache-Aside 代码示范

可以看到这种方式对于缓存的使用者是不透明的,需要使用者手动维护缓存。

2、Cache-As-SoR:

Cache-As-SoR架构图

从字面上来看,就是把 Cache 当作 SoR,也就是数据源,所以一切读写操作都是针对 Cache 的,由 Cache 内部自己维护和数据源的一致性。

这样对于使用者来说就和直接操作 SoR 没有区别了,完全感知不到 Cache 的存在。

CPU 内部的 L1、L2、L3 Cache 就是这种方式,作为数据的使用方应用程序,是完全感知不到在内存和我们之间还存在几层的 Cache,但是我们之前又提到编写 “缓存友好”的代码,不是透明的吗?这是不是冲突呢?

其实不然,缓存友好是指我们通过学习了解缓存内部实现、更新策略之后,通过调整数据访问顺序提高缓存的命中率。

Cache-As-SoR 又分为以下三种方式:

  • Read Through:这种方式和 Cache-Aside 非常相似,都是在查询时发生 cache miss 去更新缓存,但是区别在于 Cache-Aside 需要调用方手动更新缓存,而 Cache-As-SoR 则是由缓存内部实现自己负责,对应用层透明。
  • Write Through: 直写式,就是在将数据写入缓存的同时,缓存也去更新后面的数据源,并且必须等到数据源被更新成功后才可返回。这样保证了缓存和数据库里的数据一致性
  • Write Back:回写式,数据写入缓存即可返回,缓存内部会异步的去更新数据源,这样好处是写操作特别快,因为只需要更新缓存。并且缓存内部可以合并对相同数据项的多次更新,但是带来的问题就是数据不一致,可能发生写丢失。

二、预处理和延后处理

预先延后,这其实是一个事物的两面,不管是预先还是延后核心思想都是将本来该在实时链路上处理的事情剥离,要么提前要么延后处理。降低实时链路的路径长度, 这样能有效提高系统性能。

2.1 预处理

举个我们团队实际中遇到的问题:

前两个月支付宝联合杭州市政府发放消费劵,但是要求只有杭州市常驻居民才能领取,那么需要在抢卷请求进入后台的时候就判断一下用户是否是杭州常驻居民。

而判断用户是否是常驻居民这个是另外一个微服务接口,如果直接实时的去调用那个接口,短时的高并发很有可能把这个服务也拖挂,最终导致整个系统不可用,并且 RPC 本身也是比较耗时的,所以就考虑在这里进行优化。

那么该怎么做呢?很简单的一个思路,提前将杭州所有常驻居民的 user_id 存到缓存中, 比如可以直接存到 Redis。大概就是千万量级,这样,当请求到来的时候我们直接通过缓存可以快速判断是否来自杭州常驻居民。如果不是则直接在这里返回前端。

这里通过预先处理减少了实时链路上的 RPC 调用,既减少了系统的外部依赖,也极大的提高了系统的吞吐量。

预处理在 CPU 和操作系统中也广泛使用,比如 CPU 基于历史访存信息,将内存中的指令和数据预取到 Cache 中,这样可以大大提高Cache 命中率。 还比如在 Linux 文件系统中,预读算法会预测即将访问的 page,然后批量加载比当前读请求更多的数据缓存在 page cache 中,这样当下次读请求到来时可以直接从 cache 中返回,大大减少了访问磁盘的时间。

2.2 延后处理

还是支付宝,上栗子:

集五福活动

这是支付宝春节集五福活动开奖当晚,不过,作为非酋的我一般是不屑于参与这种活动的。

大家发现没有,这类活动中奖奖金一般会显示 「稍后到账」,为什么呢?那当然是到账这个操作不简单!

到账即转账,A 账户给 B 账户转钱,A 减钱, B 就必须要同时加上钱,也就是说不能 A 减了钱但 B 没有加上,这就会导致资金损失。资金安全是支付业务的生命线,这可不行。

这两个动作必须一起成功或是一起都不成功,不能只成功一半,这是保证数据一致性。 保证两个操作同时成功或者失败就需要用到事务

如果去实时的做到账,那么大概率数据库的 TPS(每秒处理的事务数) 会是瓶颈。通过产品提示,将到账操作延后处理,解决了数据库 TPS 瓶颈。

延后处理还有一个非常著名的例子,COW(Copy On Write,写时复制)。 Linux 创建进程的系统调用 fork,fork 产生的子进程只会创建虚拟地址空间,而不会分配真正的物理内存,子进程共享父进程的物理空间,只有当某个进程需要写入的时候,才会真正分配物理页,拷贝该物理页,通过 COW 减少了很多不必要的数据拷贝。

三、池化

后台开发过程中你一定离不开各种 「池子」: 内存池、连接池、线程池、对象池......

内存、连接、线程这些都是资源,创建线程、分配内存、数据库连接这些操作都有一个特征, 那就是创建和销毁过程都会涉及到很多系统调用或者网络 IO。 每次都在请求中去申请创建这些资源,就会增加请求处理耗时,但是如果我们用一个 容器(池) 把它们保存起来,下次需要的时候,直接拿出来使用,避免重复创建和销毁浪费的时间。

3.1 内存池

在 C/C++ 中,经常使用 malloc、new 等 API 动态申请内存。由于申请的内存块大小不一,如果频繁的申请、释放会导致大量的内存碎片,并且这些 API 底层依赖系统调用,会有额外的开销。

内存池就是在使用内存前,先向系统申请一块空间留做备用,使用者需要内池时向内存池申请,用完后还回来。

内存池的思想非常简单,实现却不简单,难点在于以下几点:

  • 如何快速分配内存
  • 降低内存碎片率
  • 维护内存池所需的额外空间尽量少

如果不考虑效率,我们完全可以将内存分为不同大小的块,然后用链表连接起来,分配的时候找到大小最合适的返回,释放的时候直接添加进链表。如:

空闲链表

当然这只是玩具级别的实现,业界有性能非常好的实现了,我们可以直接拿来学习和使用。

比如 Google 的 「tcmalloc」 和 Facebook 的 「jemalloc」。

限于篇幅我们不在这里详细讲解它们的实现原理,如果感兴趣可以搜来看看,也推荐去看看被誉为神书的 CSAPP(《深入理解计算机系统》)第 10 章,那里也讲到了动态内存分配算法。

3.2 线程池

线程是干嘛的?线程就是我们程序执行的实体。在服务器开发领域,我们经常会为每个请求分配一个线程去处理,但是线程的创建销毁、调度都会带来额外的开销,线程太多也会导致系统整体性能下降。在这种场景下,我们通常会提前创建若干个线程,通过线程池来进行管理。当请求到来时,只需从线程池选一个线程去执行处理任务即可。

线程池常常和队列一起使用来实现任务调度,主线程收到请求后将创建对应的任务,然后放到队列里,线程池中的工作线程等待队列里的任务。

线程池实现上一般有四个核心组成部分:

  • 管理器(Manager): 用于创建并管理线程池。
  • 工作线程(Worker): 执行任务的线程。
  • 任务接口(Task): 每个具体的任务必须实现任务接口,工作线程将调用该接口来完成具体的任务。
  • 任务队列(TaskQueue): 存放还未执行的任务。

线程池模型

线程池在 C、C++ 中没有具体的实现,需要应用开发者手动实现上诉几个部分。

在 Java 中 「ThreadPoolExecutor」 类就是线程池的实现。后续我也会写文章分析 C++ 如何写一个简单的线程池以及 Java 中线程池是如何实现的。

3.3 连接池

顾名思义,连接池是创建和管理连接的。

大家最熟悉的莫过于数据库连接池,这里我们简单分析下如果不用数据库连接池,一次 SQL 查询请求会经过哪些步骤:

  1. 和 MySQL server 建立 TCP 连接:

    • 三次握手
  2. MySQL 权限认证:

    • Server 向 Client 发送 密钥
    • Client 使用密钥加密用户名、密码等信息,将加密后的报文发送给 Server
    • Server 根据 Client 请求包,验证是否是合法用户,然后给 Client 发送认证结果
  3. Client 发送 SQL 语句
  4. Server 返回语句执行结果
  5. MySQL 关闭
  6. TCP 连接断开

    • 四次挥手

可以看出不使用连接池的话,为了执行一条 SQL,会花很多时间在安全认证、网络IO上。

如果使用连接池,执行一条 SQL 就省去了建立连接和断开连接所需的额外开销。

还能想起哪里用到了连接池的思想吗?我认为 HTTP 长链接也算一个变相的链接池,虽然它本质上只有一个连接,但是思想却和连接池不谋而合,都是为了复用同一个连接发送多个 HTTP 请求,避免建立和断开连接的开销。

池化实际上是预处理和延后处理的一种应用场景,通过池子将各类资源的创建提前和销毁延后。

四、同步变异步

对于处理耗时的任务,如果采用同步的方式,那么会增加任务耗时,降低系统并发度。

可以通过将同步任务变为异步进行优化。

举个例子,比如我们去 KFC 点餐,遇到排队的人很多,当点完餐后,大多情况下我们会隔几分钟就去问好了没,反复去问了好几次才拿到,在这期间我们也没法干活了,这时候我们是这样的:

同步写法

这个就叫同步轮训, 这样效率显然太低了。

服务员被问烦了,就在点完餐后给我们一个号码牌,每次准备好了就会在服务台叫号,这样我们就可以在被叫到的时候再去取餐,中途可以继续干自己的事。

这就叫异步,在很多编程语言中有异步编程的库,比如 C++ std::future、Python asyncio 等,但是异步编程往往需要回调函数(Callback function),如果回调函数的层级太深,这就是回调地狱(Callback hell)。回调地狱如何优化又是一个庞大的话题。。。。

这个例子相当于函数调用的异步化,还有的是情况是处理流程异步化,这个会在接下来消息队列中讲到。

五、消息队列

消息队列示意图

这是一个非常简化的消息队列模型,上游生产者将消息通过队列发送给下游消费者。在这之间,消息队列可以发挥很多作用,比如:

5.1 服务解耦

有些服务被其它很多服务依赖,比如一个论坛网站,当用户成功发布一条帖子有一系列的流程要做,有积分服务计算积分,推送服务向发布者的粉丝推送一条消息..... 对于这类需求,常见的实现方式是直接调用:

直接调用

这样如果需要新增一个数据分析的服务,那么又得改动发布服务,这违背了依赖倒置原则即上层服务不应该依赖下层服务,那么怎么办呢?

发布订阅模式

引入消息队列作为中间层,当帖子发布完成后,发送一个事件到消息队列里,而关心帖子发布成功这件事的下游服务就可以订阅这个事件,这样即使后续继续增加新的下游服务,只需要订阅该事件即可,完全不用改动发布服务,完成系统解耦。

5.2 异步处理

有些业务涉及到的处理流程非常多,但是很多步骤并不要求实时性。那么我们就可以通过消息队列异步处理。比如淘宝下单,一般包括了风控、锁库存、生成订单、短信/邮件通知等步骤。但是核心的就风控和锁库存, 只要风控和扣减库存成功,那么就可以返回结果通知用户成功下单了。后续的生成订单,短信通知都可以通过消息队列发送给下游服务异步处理。大大提高了系统响应速度。

这就是处理流程异步化。

5.3 流量削峰

一般像秒杀、抽奖、抢卷这种活动都伴随着短时间海量的请求, 一般超过后端的处理能力,那么我们就可以在接入层将请求放到消息队列里,后端根据自己的处理能力不断从队列里取出请求进行业务处理。

就像最近长江汛期,上游短时间大量的洪水汇聚直奔下游,但是通过三峡大坝将这些水缓存起来,然后匀速的向下游释放,起到了很好的削峰作用。

起到了平均流量的作用。

5.4 总结

消息队列的核心思想就是把同步的操作变成异步处理,异步处理会带来相应的好处,比如:

  • 服务解耦
  • 提高系统的并发度,将非核心操作异步处理,不会阻塞住主流程

但是软件开发没有银弹,所有的方案选择都是一种 trade-off。 同样,异步处理也不全是好处,也会导致一些问题:

  • 降低了数据一致性,从强一致性变为最终一致性
  • 有消息丢失的风险,比如宕机,需要有容灾机制

六、批量处理

在涉及到网络连接、IO等情况时,将操作批量进行处理能够有效提高系统的传输速率和吞吐量。

在前后端通信中,通过合并一些频繁请求的小资源可以获得更快的加载速度。

比如我们后台 RPC 框架,经常有更新数据的需求,而有的数据更新的接口往往只接受一项,这个时候我们往往会优化下更新接口,

使其能够接受批量更新的请求,这样可以将批量的数据一次性发送,大大缩短网络 RPC 调用耗时。

七、数据库

我们常把后台开发调侃为「CRUD」,数据库在整个应用开发过程中的重要性不言而喻。

而且很多时候系统的瓶颈也往往处在数据库这里,慢的原因也有很多,比如可能是没用索引、没用对索引、读写锁冲突等等。

那么如何使用数据才能又快又好呢?下面这几点需要重点关注:

7.1 索引

索引可能是我们平时在使用数据库过程中接触得最多的优化方式。索引好比图书馆里的书籍索引号,想象一下,如果我让你去一个没有书籍索引号的图书馆找《人生》这本书,你是什么样的感受?当然是怀疑人生,同理,你应该可以理解当你查询数据,却不用索引的时候数据库该有多崩溃了吧。

数据库表的索引就像图书馆里的书籍索引号一样,可以提高我们检索数据的效率。索引能提高查找效率,可是你有没有想过为什么呢?这是因为索引一般而言是一个排序列表,排序意味着可以基于二分思想进行查找,将查询时间复杂度做到 O(log(N)),快速的支持等值查询和范围查询。

二叉搜索树查询效率无疑是最高的,因为平均来说每次比较都能缩小一半的搜索范围,但是一般在数据库索引的实现上却会选择 B 树或 B+ 树而不用二叉搜索树,为什么呢?

这就涉及到数据库的存储介质了,数据库的数据和索引都是存放在磁盘,并且是 InnoDB 引擎是以页为基本单位管理磁盘的,一页一般为 16 KB。AVL 或红黑树搜索效率虽然非常高,但是同样数据项,它也会比 B、B+ 树更高,高就意味着平均来说会访问更多的节点,即磁盘IO次数!

根据 Google 工程师 Jeff Dean 的统计,访问内存数据耗时大概在 100 ns,访问磁盘则是 10,000,000 ns。

所以表面上来看我们使用 B、B+ 树没有 二叉查找树效率高,但是实际上由于 B、B+ 树降低了树高,减少了磁盘 IO 次数,反而大大提升了速度。

这也告诉我们,没有绝对的快和慢,系统分析要抓主要矛盾,先分析出决定系统瓶颈的到底是什么,然后才是针对瓶颈的优化。

其实关于索引想写的也还有很多,但还是受限于篇幅,以后再单独写。

先把我认为索引必知必会的知识列出来,大家可以查漏补缺:

  • 主键索引和普通索引,以及它们之间的区别
  • 最左前缀匹配原则
  • 索引下推
  • 覆盖索引、联合索引

7.2 读写分离

一般业务刚上线的时候,直接使用单机数据库就够了,但是随着用户量上来之后,系统就面临着大量的写操作和读操作,单机数据库处理能力有限,容易成为系统瓶颈。

由于存在读写锁冲突,并且很多大型互联网业务往往读多写少,读操作会首先成为数据库瓶颈,我们希望消除读写锁冲突从而提升数据库整体的读写能力。

那么就需要采用读写分离的数据库集群方式,一主多从,主库会同步数据到从库。写操作都到主库,读操作都去从库。

读写分离

读写分离到之后就避免了读写锁争用,这里解释一下,什么叫读写锁争用:

MySQL 中有两种锁:

  • 排它锁( X 锁): 事务 T 对数据 A 加上 X 锁时,只允许事务 T 读取和修改数据 A。
  • 共享锁( S 锁): 事务 T 对数据 A 加上 S 锁时,其他事务只能再对数据 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。

读写分离解决问题的同时也会带来新问题,比如主库和从库数据不一致

MySQL 的主从同步依赖于 binlog,binlog(二进制日志)是 MySQL Server 层维护的一种二进制日志,是独立于具体的存储引擎。它主要存储对数据库更新(insert、delete、update)的 SQL 语句,由于记录了完整的 SQL 更新信息,所以 binlog 是可以用来数据恢复和主从同步复制的。

从库从主库拉取 binlog 然后依次执行其中的 SQL 即可达到复制主库的目的,由于从库拉取 binlog 存在网络延迟等,所以主从数据存在延迟问题。

那么这里就要看业务是否允许短时间内的数据不一致,如果不能容忍,那么可以通过如果读从库没获取到数据就去主库读一次来解决。

7.3 分库分表

如果用户越来越多,写请求暴涨,对于上面的单 Master 节点肯定扛不住,那么该怎么办呢?多加几个 Master?不行,这样会带来更多的数据不一致的问题,增加系统的复杂度。那该怎么办?就只能对库表进行拆分了。

常见的拆分类型有垂直拆分和水平拆分。

考虑拼夕夕电商系统,一般有 订单表、用户表、支付表、商品表、商家表等, 最初这些表都在一个数据库里。
后来随着砍一刀带来的海量用户,拼夕夕后台扛不住了! 于是紧急从阿狸粑粑那里挖来了几个 P8、P9 大佬对系统进行重构。

  1. P9 大佬第一步先对数据库进行垂直分库,

根据业务关联性强弱,将它们分到不同的数据库, 比如订单库,商家库、支付库、用户库。

  1. 第二步是对一些大表进行垂直分表,将一个表按照字段分成多表,每个表存储其中一部分字段。 比如商品详情表可能最初包含了几十个字段,但是往往最多访问的是商品名称、价格、产地、图片、介绍等信息,所以我们将不常访问的字段单独拆成一个表。
  • 由于垂直分库已经按照业务关联切分到了最小粒度,数据量任然非常大,P9 大佬开始水平分库,比如可以把订单库分为订单1库、订单2库、订单3库...... 那么如何决定某个订单放在哪个订单库呢?可以考虑对主键通过哈希算法计算放在哪个库。
  • 分完库,单表数据量任然很大,查询起来非常慢,P9 大佬决定按日或者按月将订单分表,叫做日表、月表。

分库分表同时会带来一些问题,比如平时单库单表使用的主键自增特性将作废,因为某个分区库表生成的主键无法保证全局唯一,这就需要引入全局 UUID 服务了。

经过一番大刀阔斧的重构,拼夕夕恢复了往日的活力,大家又可以愉快的在上面互相砍一刀了。

(分库分表会引入很多问题,并没有一一介绍,这里只是为了讲解什么是分库分表)

八、具体技法

8.1 零拷贝

高性能的服务器应当避免不必要数据复制,特别是在用户空间和内核空间之间的数据复制。 比如 HTTP 静态服务器发送静态文件的时候,一般我们会这样写:

发送文件

如果了解 Linux IO 的话就知道这个过程包含了内核空间和用户空间之间的多次拷贝:

IO示意图

内核空间和用户空间之间数据拷贝需要 CPU 亲自完成,但是对于这类数据不需要在用户空间进行处理的程序来说,这样的两次拷贝显然是浪费。什么叫 「不需要在用户空间进行处理」?

比如 FTP 或者 HTTP 静态服务器,它们的作用只是将文件从磁盘发送到网络,不需要在中途对数据进行编解码之类的计算操作。

如果能够直接将数据在内核缓存之间移动,那么除了减少拷贝次数以外,还能避免内核态和用户态之间的上下文切换。

而这正是零拷贝(Zero copy)干的事,主要就是利用各种零拷贝技术,减少不必要的数据拷贝,将 CPU 从数据拷贝这样简单的任务解脱出来,让 CPU 专注于别的任务。

常用的零拷贝技术:

  1. mmap

    mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。

mmap

  1. sendfile

    sendfile 是 Linux2.1 版本提供的,数据不经过用户态,直接从页缓存拷贝到 socket 缓存,同时由于和用户态完全无关,就减少了一次上下文切换。

    在 Linux 2.4 版本,对 sendfile 进行了优化,直接通过 DMA 将磁盘文件数据读取到 socket 缓存,真正实现了 ”0” 拷贝。前面 mmap 和 2.1 版本的 sendfile 实际上只是消除了用户空间和内核空间之间拷贝,而页缓存和 socket 缓存之间的拷贝依然存在。

8.2 无锁化

在多线程环境下,为了避免 竞态条件(race condition), 我们通常会采用加锁来进行并发控制,锁的代价也是比较高的,锁会导致上线文切换,甚至被挂起直到锁被释放。

基于硬件提供的原子操作 CAS(Compare And Swap) 实现一些高性能无锁的数据结构,比如无锁队列,可以在保证并发安全的情况下,提供更高的性能。

首先需要理解什么是 CAS,CAS 有三个操作数,内存里当前值M,预期值 E,修改的新值 N,CAS 的语义就是:

如果当前值等于预期值,则将内存修改为新值,否则不做任何操作

用 C 语言来表达就是:

CAS

注意,上面 CAS 函数实际上是一条原子指令,那么是如何用的呢?

假设我需要实现这样一个功能:

对一个全局变量 global 在两个不同线程分别对它加 100 次,这里多线程访问一个全局变量存在 race condition,所以我们需要采用线程同步操作,下面我分别用锁和CAS的方法来实现这个功能。

CAS和锁示范

通过使用原子操作大大降低了锁冲突的可能性,提高了程序的性能。

除了 CAS,还有一些硬件原子指令:

  • Fetch-And-Add,对变量原子性 + 1
  • Test-And-Set,这是各种锁算法的核心,在 AT&T/GNU 汇编语法下,叫 xchg 指令,我会单独写一篇如何使用 xchg 实现各种锁。

8.3 序列化与反序列化

先看看维基百科怎么定义的序列化:

In computing, serialization (US spelling) or serialisation (UK spelling) is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, across a computer network) and reconstructed later (possibly in a different computer environment). When the resulting series of bits is reread according to the serialization format, it can be used to create a semantically identical clone of the original object. For many complex objects, such as those that make extensive use of references, this process is not straightforward. Serialization of object-oriented objects does not include any of their associated methods with which they were previously linked.

我相信你大概率没有看完上面的英文描述,其实我也不爱看英文资料,总觉得很慢,但是计算机领域一手的学习资料都是美帝那边的,所以没办法,必须逼自己去试着读一些英文的资料。

实际上也没有那么难,熟悉常用的几百个专业名词,句子都是非常简单的一些从句。没看的话,再倒回去看看?

这里我就不做翻译了,主要是水平太低,估计做到「信达雅」的信都很难。

扯远了,还是回到序列化来。

所有的编程一定是围绕数据展开的,而数据呈现形式往往是结构化的,比如结构体(Struct)、类(Class)。 但是当我们 通过网络、磁盘等传输、存储数据的时候却要求是二进制流。 比如 TCP 连接,它提供给上层应用的是面向连接的可靠字节流服务。那么如何将这些结构体和类转化为可存储和可传输的字节流呢?这就是序列化要干的事情,反之,从字节流如何恢复为结构化的数据就是反序列化。

序列化解决了对象持久化和跨网络数据交换的问题。

序列化一般按照序列化后的结果是否可读,可分为以下两类:

  • 文本类型:

    如 JSON、XML,这些类型可读性非常好,是自解释的。也常常用在前后端数据交互上,因为接口调试,可读性高非常方便。但是缺点就是信息密度低,序列化后占用空间大。

  • 二进制类型

    如 Protocol Buffer、Thrift等,这些类型采用二进制编码,数据组织得更加紧凑,信息密度高,占用空间小,但是带来的问题就是基本不可读。

还有 Java 、Go 这类语言内置了序列化方式,比如在 Java 里实现了 Serializable 接口即表示该对象可序列化。

说到这让我想起了大一写的的两个程序,一个是用刚 C 语言写的公交管理系统,当时需要将公交线路、站点信息持久化保存,当时的方案就是每个公交线路写在一行,用 "|"分割信息,比如:

5|6:00-22:00|大学城|南山站|北京站
123|6:30-23:00|南湖大道|茶山刘|世界

第一列就是线路编号、第二项是发车时间、后面就是途径的站点。是不是非常原始?实际上这也是一种序列化方式,只是效率很低,也不通用。而且存在一个问题就是如果信息中包含 “|”怎么办?当然是用转义。

第二个程序是用 Java 写的网络五子棋,当时需要通过网络传输表示棋子位置的对象,查了一圈最后发现只需要实现 Serializable 接口,自己什么都不用干,就能自己完成对象的序列化,然后通过网络传输后反序列化。当时哪懂得这就叫序列化,只觉得牛逼、神奇!

最后完成了一个可以网络五子棋,拉着隔壁室友一起玩。。。真的是成就感满满哈哈哈。

说来在编程方面,已经很久没有这样的成就感了。

总结

这篇文章主要是粗浅的介绍了一些系统设计、系统优化的套路和最佳实践。

不知道你发现没有,从缓存到消息队列、CAS......,很多看起来很牛逼的架构设计其实都来源于操作系统、体系结构。

所以我非常热衷学习一些底层的基础知识,这些看似古老的技术是经过时间洗礼留下来的好东西。现在很多的新技术、框架看似非常厉害,实则不少都是新瓶装旧酒,每几年又会被淘汰一批。

最后说一句(求关注)

这篇文章写了挺久的,从写文章、画图,调格式每一步都很花时间。如果觉得对你有帮助的话,可以点个关注或者在看鼓励下~

文章持续更新,微信搜索「 编程指北 」第一时间获取,回复【资料】有我准备的一线 BAT 大厂面试资料和简历模板。

期待你的关注~
有任何问题,欢迎留言~

查看原文

赞 14 收藏 11 评论 0

编程指北 关注了用户 · 9月5日

Java3y @java_3y

微信搜「Java3y」白嫖原创电子书和思维导图🔥

已经有8756个初学者都下载了!!🔥三歪把【大厂面试知识点】、【简历模板】、【原创文章】全部整理成电子书,共有1263页!点击下方链接领取。

https://github.com/ZhongFuChe...

关注 4051

编程指北 赞了文章 · 2019-02-26

用c++开发一个简版http服务器

初衷

在阅读了TLPI和深入理解计算机系统之后,学会了如何使用linux系统api,想在写代码的过程中来加深自己对知识的理解,更想用这些知识来去做一个更酷的东西,而不仅仅是教课书上的简单服务器。而且在实现过程中往往能学到教科书外的东西。
私以为项目为导向是学习编程的最好方法。而且没有什么比自己创造一个东西有趣。
“将一个实际的浏览器指向自己的服务器,看着他显示一个复杂的带有文本和图片的web页面,真是非常令人兴奋。"

使用方法

首先下载源码:源码地址

然后将web页面所需的html文件放在/var/www目录下

  1. $ cd /src , 进入到src目录
  2. $ make , 产生可执行文件HttpServer
  3. $ ./HttpServer \<ipv4 address> \<port number> \<process number> \<connect number per process>

    例如:./HttpServer 127.0.0.1 8080 5 1000 ,这一步是开启web-server服务。

这个服务器支持了:

  1. 目前仅仅支持HTTP/1.1的GET方法。
  2. 暂时不支持动态内容。
  3. 完整的Http报文请求行和头部解析
  4. 简单的连接池,进程池和内存池管理
  5. 简单的负载均衡。
  6. 支持HTTP/1.1长连接
  7. 实现了一个二叉堆,对定时时间进行管理(目前只有超时连接事件)。

运行环境

Unbtun 16.04.2 内核版本是4.8

如何实现一个Web服务器:

1.本服务器采用进程池,epoll和非阻塞I/O实现高效的半同步/半异步模式。如下图:

主进程只管理监听socket,连接socket都由进程池中的worker进行管理。当有新的连接到来时,主进程会通过socketpair创建的套接字和worker进程通信,通知子进程接收新连接。子进程正确接收连接之后,会把该套接字上的读写事件注册到自己的epll内核事件表中。之后该套接字上的任何I/O操作都由被选中的worker来处理,直到客户关闭连接或超时。

2.每个子进程都是一个reactor,采用epoll和非阻塞I/O实现事件循环。如下图:

  • a. epoll负责监听事件的发生,有事件到来将调用相应的事件处理单元进行处理

    • i. 对一个连接来说,主要监听的就是读就绪事件和写就绪事件。

      • 1). 通过非阻塞I/o和事件循环来将阻塞进程的方法分解。例如:每次recv新数据时,如果recv返回EAGAIN错误,都不会一直循环recv,而是将现有数据先处理,然后记录当前连接状态,然后将读事件接着放到epoll队列中监听等待下一个数据到来。因为每次都不会尽可能的将I/O上的数据读取,所以我采用了水平触发而不是边沿触发。send同理。
    • ii. 统一事件源:

      • 1). 信号:信号是一种异步事件,信号处理函数和程序的主循环是两条不同的执行路线,很显然,信号处理函数需要尽可能的执行完成,以确保信号不被屏蔽(信号是不会排队的)。一个典型的解决方案是把信号的主要处理逻辑放到事件循环里,当信号处理函数被触发时只是通过管道将信号通知给主循环接收和处理信号,只需要将和信号处理函数通信的管道的可读事件添加到epoll里。这样信号就能和其他I/O事件一样被处理。

        • a).忽略SIGPIPE信号(当读写一个对端关闭的连接时),将为SIGINT,SIGTERM,SIGCHILD(对父进程来说标识有子进程状态发生变化,一般是子进程结束)设置信号处理函数。
      • 2). 定时器事件。使用timefd,同样通过监听timefd上的可读事件来统一事件源。将其设置为边沿触发,不然timefd水平触发将一直告知该事件。

        • a). 超时将通过连接池回收连接。
  • b. 连接池和内存池的实现:

    • i. 连接池:连接池采用一个map<int,Conn>和一个set<Conn>实现。连接池在构造时,将根据传入的参数new固定数目的Conn(Conn的构造函数并不会为定时器,接收和发送缓冲申请空间),且后续数目不可变。然后连接结构的地址放入到set里。新连接到来时将从set里取出一个空闲连接,然后将其初始化,并放入map,map里保存的时套接字和对应连接地址的key-value对。连接关闭时,将回收连接,从mao中移除,然后放入到set里。
    • ii. 内存池:内存池的实现是通过连接类来完成的。连接类在第一次被初始化时即第一次被使用,将申请相应的定时器,接收和发送缓存。之后将不会将申请的内存销毁,直到进程结束。通过这样来降低申请和释放内存的次数来减少内存碎片以及节约时间。
  • c. 连接:
    每个连接都应该有一个bool Init(int connfd,size_t recv_buffer_size,size_t send_buffer_size);函数,一个Return_Code process(OptType status)函数。前一个函数会在第一次被调用时分配内存,后一个函数将根据操作类型,来决定要进行的是读还是写操作。同时根据操作结果返回相应的状态,来决定要给epoll添加什么事件。
  • d. 时间堆的实现(定时器的精度目前为s):
    采用最小堆来实现。每次都将所有定时器中的超时时间最小的定时器的超时间隔作为心博间隔。删除和更新定时器的时间复杂度都是O(logK)k是其在堆中的位置。
  • e. 负载均衡:
    当一次事件循环结束,子进程的连接数目有变化时,将通过和父进程通信的管道来通知自父进程自己的连接数。当新的连接到来时,父进程将选取连接数最少的一个进程,将新的连接发送给他。
  • f. Http报文请求行和头部解析:

    • i. 通过状态机来实现HTTP报文的解析。因为一个请求有可能不是在一个tcp包中到来,所以需要记录状态机的状态,以及上次check到的位置。在解析完HTTP报文后,还需要保存解析的结果,然后根据解析结果,来产生相应response。该部分实现参考了《Linux高性能服务器编程》中的实现。

为什么这么设计

1. 为什么采用多进程而不是单进程多线程:

a. 虽然说多线程的切换开销比多进程低。如果每一个进程都工作在一个cpu上,那么切换的开销完全可以省去,而且因为我们采用的是进程池,进程的数目在启动时是可以设置的,而且并不会在程序的执行过程中频繁的开新进程和销毁就进程,所以进程销毁和产生这块开销也避免了。
b. 同时,多进程的编码难度比多线程要低的多,而且也不用过多的考虑到线程安全问题。
c. 综上,我选择了多进程。

2. 为什么采用时间堆?

a. 首先和双链表相比,最小堆的时间复杂度是优于他的。和时间轮比,虽然添加和删除定时器的时间复杂度是O(1),但是其执行一个定时的时间复杂度是O(n),同时其精度和时间轮的槽间隔有关。而最小堆则更适合处理这种每次timer模块需要频繁找最小的key(最早超时的事件)然后处理后删除的场景。其删除一个定时器是O(lgk)(如果考虑延迟删除的话,会是O(1),但是考虑到我要复用定时器,所以执行了严格的删除),添加是O(lgn),执行则时O(1)。nigix使用的是红黑树,但是“memory locality比heap要差一些,实际速度略慢”,即使用最小堆更容易命中cache。libev使用的是更高效的4叉堆。为了简化实现,我采用了二叉堆来实现timer的功能。

3. 为什么采用连接池和内存池?

a. 和上面所说的一样,为了更好的利用资源,减少内存碎片,降低频繁的申请和销毁内存的开销。

测试

  1. 我编写了一个简单的Echo类,来测试时间堆,连接池和进程池。然后测试http_conn。最后再将各个模块结合起来进行测试。
  2. 最大的体会就是,在多模块编程的时候,一定一定一定要进行单元测试,再相应的模块没问题了之后,再联合起来进行测试。
  3. 同时,代码完成之后,写相应的类的接口和函数说明,再自己code review一遍,也是很重要的检错方法。
  4. 最诡异的bug往往都是因为最愚蠢的错误。例:我i在某个调用epoll_ctl(int epollfd,int option,int fd,struct epoll_event *evlist)函数中,将option和fd参数位置换了,导致一直epoll_ctl失败。调试了一天,最后才发现,参数位置写错了,然而其他地方的调用位置都写对了。
  5. 调试工具:使用GDB进行调试,使用valgrind进行内存泄漏的检测。
    a. 因为我是申请了很多内存都没有释放,而且放在内存池和连接池中,所以导致一个内存依旧reachable的,但是当进程结束时,其会被操作系统回收所以它不算是真正的memory 。只有当你申请了一块内存,而又丢失了指针之后,才是真正的内存leak。
  6. 当然,还没有进行压力测试,打算下一步进行压力测试。目前只测试过200个连接而已。

不足

  1. 首先就是只支持get方法,也不支持动态内容。
  2. 可以增加配置文件的读取,而不是通过启动时候设置的参数
  3. 日志系统,目前还只是简单的封装了一下printf,在调试的时候打开,不调试的时候关闭。真正上线的服务器是会需要一个高效而又不影响运行的日志系统的。
  4. 可修改性。比如做到在不重启服务器的情况下,提供给用户不用的功能,比如动态修改进程数目,动态修改并发限制等等等等
  5. 模块化设计。还是需要尽量降低模块之间的耦合度。虽然对Conn类只要求提供两个函数接口,但是其实内存池的管理是Conn做的,可否将内存池也交给连接池来管理。还有定时器的设计。目前只比较适合于连接超时事件。
  6. 需要将服务器进程该为守护进程等等等等

收获

  1. 首先肯定是增加了自己的编码能力。
  2. 加深了自己对linux系统api的了解
  3. 学到了更多关于linux服务器的知识。同时也惊叹于各种大师的智慧。我只是一个站在巨人的肩膀上重复造轮子的小人儿。
  4. 想太多是没用的,先考虑实现,再考虑性能。在写代码前想太多是没有意义的。Talk is cheap,show me the code。
  5. Code review的重要性!就算是自己review自己的代码,都能发现一些显而易见的错误。
  6. 文档文档文档!记录自己实现,整理自己的api,都有助于自己思考和编码。
  7. 学习GDB和valgrind使用,学习了makefile的编写。

参考资料:

感谢和感叹于大牛的智慧,编码的路上,还需要继续努力。
《linux多线程服务端编程》
《深入理解计算机系统》
《Linx/unix编程手册》
《linux高性能服务器编程》
《深入理解Ngix模块开发与架构解析》

查看原文

赞 3 收藏 2 评论 0

编程指北 关注了专栏 · 2019-01-04

高性能服务器开发基础系列

本专栏将介绍实现一套高性能服务器程序涉及到很多的技术细节,庖丁解牛,既有整体框架实现的介绍,又有诸如send、recv之类的网络API细节的剖析,最后开源一款即时通讯软件flamingo实战做为实例。希望对初、中级网络编程、服务器开发学习者有所帮助。

关注 5

认证与成就

  • 获得 36 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-03-26
个人主页被 529 人浏览