1. C10K问题
全称 Client 10000 问题,即
在同时连接到服务器的客户端数量超过 10000 个的环境中,即便硬件性能足够, 依然无法正常提供服务
为什么?上古时期的技术没想到,仅仅有16位无法支持打开多个进程。现在发展到了64位,采用虚拟内存提高了资源利用,目前已经采用epoll等技术解决了
2. 标准协议
2.1 POSIX
可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX),是IEEE为要在各种UNIX操作系统上运行的软件,而定义API的一系列互相关联的标准的总称,其正式称呼为IEEE 1003,而国际标准名称为ISO/IEC 9945。它基本上是Portable Operating System Interface(可移植操作系统接口)的缩写,而X则表明其对Unix API的传承。
2.2 OSI模型
第7层 应用层
应用层(Application Layer)提供为应用软件而设的接口,以设置与另一应用软件之间的通信。例如: HTTP、HTTPS、FTP、TELNET、SSH、SMTP、POP3、HTML等。
第6层 表达层
表达层(Presentation Layer)把数据转换为能与接收者的系统格式兼容并适合传输的格式。
第5层 会话层
会话层(Session Layer)负责在数据传输中设置和维护电脑网络中两台电脑之间的通信连接。
第4层 传输层
传输层(Transport Layer)把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。例如:传输控制协议(TCP)等。
第3层 网络层
网络层(Network Layer)决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成报文。网络表头包含了网络数据。例如:互联网协议(IP)等。
第2层 数据链接层
数据链路层(Data Link Layer)负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成信息框(Data Frame)。数据链表头(DLH)是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。例如以太网、无线局域网(Wi-Fi)和通用分组无线服务(GPRS)等。
分为两个子层:逻辑链路控制(logical link control,LLC)子层和介质访问控制(Media access control,MAC)子层。
第1层 物理层
物理层(Physical Layer)在局部局域网上传送数据帧(Data Frame),它负责管理电脑通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机接口卡等。
2.3 TCP
重要状态解释
FIN_WAIT_1:
FIN_WAIT_1状态是客户端想主动关闭连接,向服务器发送了FIN报文,当服务器回应ACK报文后,则进入到FIN_WAIT_2状态。
在实际的正常情况下,无论对方何种情况下,都应该马 上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的
TIME_WAIT:
客户端收到服务器的FIN报文,然后发出ACK报文,等2MSL后即可回到CLOSED可用状态了。
如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
2.3.1 问题
为什么是三次握手不是两次
为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤
如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认
为什么是四次握手不是三次
TCP只是一个跑腿的、为application服务的,需要和application 互动,把对端的数据、命令反馈给application,并随时接收本地application 的数据、以及关闭连接命令。所以关闭连接一定要听从application 的意图,所以不能简单FIN + ACK 合二为一。
3. 线程和进程
3.1 线程模型
一对一
一个用户线程对应一个内核线程
优点:
实现简单。
缺点:
对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换。
内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。
多对一
多个用户线程对应到同一个内核线程上,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。
优点:
用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换。使线程的创建、调度、同步等非常快。
缺点:
由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行。
内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等
3.2 用户级线程切换
3.3 进程
3.3.1 进程通信
- 匿名管道(pipe)
半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
#include
int pipe (int fd[2]); //返回:成功返回0,出错返回-1
- 父进程创建管道,得到两个⽂件描述符指向管道的两端
- 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道
- 进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
消息队列通信
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。共享内存
高效的操作同一块内存,需要用信号量进行同步操作套接字通信
4. 锁
4.1 基本互斥锁mutex
std::mutex mtx
mtx.lock()
调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
mtx.unlock()
解锁,释放对互斥量的所有权。
mtx.try_lock()
尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况
(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
4.2 区域锁std::lock_guard 介绍
与 Mutex RAII 相关,方便线程对互斥量上锁
std::mutex mtx;
void function() {
// 构造时自动加锁
std::lock_guard (kMutex);
//do something
// 离开局部作用域,析构函数自动完成解锁功能
}
lock_guard对象并不负责管理mutex对象的生命周期,lock_guard对象只是简化了mutex对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁
4.3 区域锁std::unique_lock介绍
unique_lock 和 lock_guard 一样,对 std::mutex 类型的互斥量的上锁和解锁进行管理,一样也不管理 std::mutex 类型的互斥量的声明周期。但是它的使用更加的灵活。但是效率上差一点,内存占用多一点。
#include
#include
#include
#include
using namespace std;
std::mutex mtx;
int cnt=0;
void add()
{
for(;;){
unique_lock lock(mtx);
cnt++;
std::cout<<"added "<0){ //保证变量大于零才可以用
unique_lock lock(mtx);
cnt--;
std::cout<<"deleted "<
两个函数分别对cnt变量进行修改,del函数进行操作时要满足cnt>0。如果del函数的cnt此时<=0则什么也不干等add函数加完的。
由于循环体for的两次循环间隔非常短,会导致add函数一直持有锁,所以在for最后等待1s让del函数有时间获得锁
5. 条件变量
与互斥量不同,条件变量的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的线程。条件变量总是与互斥量组合使用。
互斥量为共享数据的访问提供互斥支持,而条件变量可以就共享数据的状态的变化向相关线程发出通知。
上一个例子中生产者消费者都默认延迟了1s来解决cpu性能问题。但是i对于消费者来说应该在生产者完成生产之后就立即消费,所以1s对于消费者来说并不合适,他应该是一个浮动的值,这就得用条件变量了
#include
#include
#include
#include
#include
using namespace std;
std::mutex mtx;
std::condition_variable cond;
int cnt=0;
void add()
{
for(;;){
unique_lock lock(mtx);
cnt++;
std::cout<<"added "<0){ //保证变量大于零才可以用
unique_lock lock(mtx);
cnt--;
std::cout<<"deleted "<
流程:
- 消费者运行时如果cnt=0就阻塞在cond wait这里。(此时不需要锁,生产者也在跑)
- 生产者运行拿到锁后cnt+=1,并通知消费者
- 消费者接到通知,此时cnt不是0,拿到锁后消费后再开锁
- 之后二者继续竞争这个锁,前提是cnt不是0,如果cnt=0只有生产者可以跑,因为消费者此时阻塞在wait这里
6. STL大根堆小根堆
堆也叫优先队列,堆是一种特殊的完全二叉树数据结,分为两种,最大堆(大根堆),最小堆(小根堆)。
最大堆:根节点大于左右两个子节点的完全二叉树
最小堆:根节点小于左右两个子节点的完全二叉树
它的功能强大在哪里呢?
四个字:自动排序
既然是队列那么先要包含头文件#include
, 他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队
//对于基础类型 默认是大根堆(从大到小排列)
priority_queue q;
//升序队列(强调:“>”不要两个拼在一起!)
priority_queue ,greater > q;
//降序队列
priority_queue ,less > q;
那么如何定义优先级呢?
struct node
{
int x,y;
bool operator < (const node & a) const
{
return x q;
当然这个并没有sort灵活,呵呵
6.1 基于小根堆的定时器
优点:
添加删除事件复杂度为O(lgN)
获取最小 key 值(小根堆)的复杂度为 O(1)
比起传统的按照固定时间间隔执行tick函数的链表/时间轮定时器,小根堆定时器可以动态地将所有定时器时间的最小的一个定时器的超时值作为心博时间。这样,一旦心博函数tick()被调用,则超时时间最小的定时器一定到期,执行相关处理。然后将下一个最小值定时器作为下一次的超时时间。这样,减少了调用tick()的次数。
最小堆计时器是比较常用的一种计时器,在libevent中可以看到它的使用。这种数据结构每次返回的是最小值时间间隔定时器。当添加计时器时,就不断调整计时器在堆中的位置保证堆顶是一个最小计时。当计时器从堆中删除时就不断的向下找最小的计时器放在堆顶