前言1:选择c++的原因及每个时间节点准备(P1)
C++与Java的区别
1
如何选择?!
选C++还需要
C++就业方向
应届生实习找工作流程
招聘网站
应届生要求
offer选择:
量化开发
职位推荐:(社会需求层面)
1.量化:金融
2.车企:不要选零部件厂商
网络,存储开发->产生精英
前言2.C/C++学习路线(P2)
问题:
C++与其他上层语言的分工
学习阶段
1.基础部分
2.网络:传入步骤及标准接口
通过项目进行串联
3.基础组件:(重要!)
4.中间件:(5类)
RPC 代表 远程过程调用(Remote Procedure Call)。它是一种允许程序在一台计算机上调用另一台计算机上的服务或方法的机制,仿佛调用的是本地方法一样,从而简化分布式系统中的通信。
RPC 的常见协议:
- HTTP-based RPC:如 RESTful APIs,基于 HTTP 协议进行远程调用。
- gRPC:一种高性能的 RPC 框架,基于 HTTP/2 和 Protocol Buffers 进行高效的序列化和通信。
- XML-RPC:使用 XML 作为消息格式,通过 HTTP 传输数据。
- JSON-RPC:使用 JSON 格式的消息,通常通过 HTTP 或 WebSocket 传输。
5.框架:快速产品化
6.部署、运维
7.性能分析
gtest
8.多机器问题
前言3:C++岗位介绍(P3)
后端:backend(后台,后端)
提高技术服务
音视频方向:(QT+音视频)
网络(推荐职位)
存储(推荐职位)
安全:
cdn(传统开发法向)云厂家
游戏后端(难)
游戏客户端:
嵌入式:面试容易
嵌入式bsp(闭源)
车企
Linux学习的四个层级
linux开发例子:抖音,b站,微信
一.设计模式:(P4)
问题:
1.设计模式:解决问题的固定套路
先满足设计原则,再慢慢迭代。
1)前提:既有稳定点,又有变化点(整洁的房间,好动的猫)
其他情况:
如何保持房间整洁->猫关在笼子里
解决的问题:修改少量代码,适应需求的变化。
2)设计思路
2)设计模式需要先理解的前提
1.模板模式
解决的问题:
有统一的接口,有固定的流程(算法骨架不改变),但是流程需要改变。
典型应用:skynet(游戏框架)
实例问题
开闭原则:修改关闭,拓展开放
用继承进行拓展
父类中为子类留的接口(protected)
模板模式的代码结构:
基类中:
父类中有一个固定接口,固定算法的接口(固定算法骨架)
让子类复写的函数,写成虚函数接口(在protected写)
派生类中
子代虚函数复写进行更新(protected)
李氏替换(在继承中)
在继承复写时,继承隐藏职责
2.责任链模式
定义:解决问题分步骤解决,一步一步,如果中途解决错误,则跳出解决链表
案例:
变化点:
稳定点:
代码:
基类代码
派生类代码
Ihandler(基类)封装稳定点:
组合基类指针(在自己的类中定义自己的指针)在IHandler类中(链表)
处理事情方式,构建链表结构
总结:
责任链模式的开闭原则:
ngnix
3.组合模式
定义:解决整体与部分的关系
将叶子节点和非叶节点(容器节点:包含叶子节点)统一:
容器节点:shu
二、红黑树、B+树,B-树、散列表(P15,P16,P17,P18)
1.epoll中红黑树的使用
问题:epoll如何从若干的fd查找到已发生事件的fd
1.1哈希表
刚开始建立一个表头,后续fd不断向后加,如果有冲突,就在对应值的后面加链表。时间复杂度log(1)
初始状态如果只有一个fd,也要分配一块连续的内存(存储空间),需要大空间闲置。
1.2 B树,B+树
刚开始也需要一块内存,为后续分叉准备。时间复杂度log(n+l) [l为层数]
适合磁盘,磁盘块大,层数低
1.3红黑树(二叉树)
需要的内存,与fd的增长呈线性关系,不会一下子分配很大内存的现象
1.4数组
存储元素有限
epoll_create源码(为系统调用)
传入的size只有大于零和小于零,小于零返回错误值,最开始代表最大就绪队列的长度。
epoll_create任务:
创建根节点,传入epfd,创建eventpool
如果想查找linux系统内核系统调用,看0.11版本,容易看懂
ep_item
里面存储着fd对应的事件
2.sk_buff(封装在协议栈里面)中红黑树的使用
网络通信流程
dpdk
DPDK(Data Plane Development Kit)是一个用于快速数据包处理的开源软件项目,主要运行在Linux用户空间。通过DPDK提供的网络接口控制器(NIC)轮询模式驱动,操作系统内核可以将TCP数据包处理任务卸载给用户空间的进程,从而提高计算效率和数据包处理速度。DPDK的性能提升主要体现在以下几个方面:
-
数据平面优化:通过减少内核态和用户态之间的上下文切换,DPDK能提高处理数据包的效率,尤其适用于高性能网络环境。
-
低延迟和高吞吐量:DPDK的框架允许网络设备直接与用户空间进程通信,减少数据传输的延迟,并且支持大规模并发数据包处理,从而提高吞吐量。
-
多核支持:DPDK能够充分利用多核处理器的资源,允许多个核并行处理数据包,进一步增强系统的处理能力。
-
自定义网络功能:开发人员可以根据应用需求编写和集成自定义的网络功能,并通过DPDK提供的API直接访问硬件资源。
DPDK被广泛应用于网络设备制造商、通信服务提供商以及需要高性能网络处理的企业环境中
红黑树的作用:
当有大量的客户端连接进来时,根据原ip端口对协议进行划分,把同一个客户端的tcp,udp,ssh,rcp等放到一个节点中,以便后续连续处理。
设置tcp的send和recv的buff
wmem、rmem:最小值,默认值,最大值
图中system_tcp_wmem[1],取默认值1024
源码:双向链表+时间戳+红黑树
流程
->sk_buff传入数据
-> tcp_output/input 加入/去掉tcp头,ip头,以太网头
->缓存区r/wmen为裸数据
->sock tcp.c处理sock
->fd找到与自己映射的数据
-》》》》》》与红黑树的关系?
sk_buff要接受大量不同客户端的数据,他要区分哪些信息属于同一个客户端,因此需要红黑树来进行分类
Q:sk_buff结构本身存不存储数据?哪一个域用来存储数据
在Linux网络栈中,sk_buff
结构(通常简称为 skb
)用于描述网络数据包。它不仅仅是一个数据包的描述符,还包含了和数据包相关的各种元数据,如网络协议信息、时间戳等。然而,sk_buff
本身并不直接存储数据,而是通过指针指向实际存储数据的内存区域。
在 sk_buff
结构中,存储实际数据的域是 data
和相关的指针域。以下是关键的几个域:
data
:指向数据缓冲区的开始位置,即实际数据的首地址。head
:指向整个数据缓冲区的首地址,通常包括协议头和数据。tail
:指向数据缓冲区中的末尾,用于管理数据的边界。end
:指向数据缓冲区的结束位置,用于控制可用空间。
这些指针域让 sk_buff
能够灵活地管理内核中实际存储的数据,并支持各种协议头部和数据的操作。
3.内存块中红黑树的使用(k,v)存储
用kv存储
fork()调用一次返回两次的原因
因为子进程与父进程代码段和数据段一模一样,在执行命令时,父进程执行完,子进程又执行一次,所以返回两次
内存管理的结构体
里面有红黑树的根节点,每次准备一块虚拟内存区
4.红黑树如何用?
1.不用自己写,复用别人代码
2.了解红黑树原理:内存增长,加锁,时间复杂度
3.若干定时器,用红黑树,fd与业务信息的映射
4.message消息队列,多个客户端,如何从消息队列中拿到属于自己的完整信息。
5.收集ip地址访问网关的次数,ip与count的绑定(ip不确定的情况下),如果确定大量用哈希表
三、网络(P37)
网络软件开发需要的两个技能:
网络编程:1对N
网络原理:1对1
TCP,UDP(8个字节头,没有协议格式),http
ps:epoll是文件系统,管理fd的一个组件
1.网络与进程线程
进程虚拟分布图(每一个进程都有)
1.fastcgi(一个请求对应一个进程,leetcode实现方式)
2.nginx 多个进程共同监听一个端口(处理http)
场景:利用多核特性
Nginx 通过共享监听套接字让多个工作进程可以共同监听同一个端口。
fork()创建多个进程,里面所有的东西都一样,->nginx的实现原理
3.one master - multi worker IM->衍生:N master->M worker
同一个进程内的线程fd是通用的
master listen,多线程
worker fd recv/send
4.多个线程监听同一个端口
每一个线程一个epoll还是共用一个epoll?
一个线程服务不同fd,不同客户端
多线程网络模型事项
1.避免多个线程(不同进程中)监听同一个fd
当传输信息被拆包的时候,分别在不同线程中,处理很麻烦
2.一请求一线程(select,apache源码)问题:
C10K(并发一万)
场景:局域网内几个几十个客户端
3.epoll I/O多路复用:C1000K,
内核问题,(涉及)用户态协议栈
2.网络编程与内核协议栈
2.1连接的建立
系统api(strace查看系统调用)
网络编程与内核协议栈:
三次握手之后,tcb控制块调用accept函数,
accept返回对应的客户端fd(找到刚才建立三次握手的tcb)
accept()
返回的 FD 只对应服务端的 TCB,用于标识和管理客户端的连接。
为tcb分配对应的fd(文件系统分配)
将tcb与clientfd进行连接
2.2数据的传输
接收/发送缓冲区
2.3连接的断开
close内核空间
socket 1.把文件系统的fd回收,断了fd与tcb的联系
2.tcb中放入->fin包
四次挥手协议栈空间
主动方 被动方
大量close_wait产生原因:
被动方没有及时调用close,或者没有调用close
UDP:没有链接建立和断开
http是基于tcp的
3.网络与网卡
网络适配器:adapter(纯软件)
内核net_device 为每个网卡分配,net_device不一定需要网络适配器,虚拟化
4.网络与虚拟化
四、网络面试题(P38P40P43)
1.TCP首部
TCP三次握手:服务端被动实现
syn和ack都为一个标志位
调用三次握手的函数:(三个函数)
客户端TCP调用connect函数(UDP调用connect为探测)
客户端调用connect函数,服务端为listen状态,连接建立完成,调用accept函数从全连接队列里面取出
半连接状态(syn队列):建立第一次握手,没有完全建立连接的状态
全连接状态(accept队列):三次握手全部建立(蒸熟的馒头)
accept函数(把蒸熟的馒头从锅里端出来)
就绪连接状态:在全连接完成之后,会触发就绪事件,提醒应用层(listenfd触发),此时用到了select,poll,epoll(listen加入到epoll里面,会触发到事件,accept去处理)
-->就绪-调用产生事件和对应的回调 event—actor
TCP数据传输(难点在于对方如何一定能收到)
保证:先发现收到,后发后收到
粘合和分包:
如何保证可靠:
序列号:最开始随机产生。
发送的信息和收到对方发送的确认信息
三个状态:已发送已确认,已发送未确认,已发送未确认
->滑动窗口:
->拥塞控制:如果确认的消息超过了等待时间,减缓发送
窗口大小:windowsSize(最大)
需要多少个定时器:
1.超时重传
2.延迟确认
3.探测定时器:告诉对方仓库已满,对方什么时候再发。探测对方的包有没有空间收我
UDP首部
极度不负责的协议
实时性要求高,下载的时候
TCP断开:双方调用close
1.函数:close
如果出现大量的close wait
原因:
大量断开
清空耗时较长:clean up调用另外的线程去执行来解决此问题
2.状态迁移
3.状态
TCP状态迁移图
1.大量close_wait原因(被动方,recv=0,收到fin方)
收到recv返回0,就调用close()
原因:清理处理不及时
1.异步清理 2.清理放到close后面
2.UDP存在的意义
头部8字节
TCP是流:先发的先到,后发的后到
UDP是数据报:不知道哪个先到
TCP延迟确认机制:(减少网络压力,多个ack一起回复)
放一个技能,服务器等待一段时间发送ack,这是客户端才显示技能
游戏领域用UDP,实时性强
迅雷下载:不带拥塞控制
UDP并发
并发:同时承载用户数量。前提:网络连接存在
你的项目并发量怎么样?
1.内部测试
2.项目真实数据
系统调用recvfrom,accept每秒钟一万次相当高了
解决方案:
解决100w并发量(udp共用一个fd)
假设recvfrom()每秒钟极限为1万次,需要准备一百个recvfrom(),分散在线程里面
如何在不读取数据的情况下,前三个分一组,后三个分一组
创建150~200个fd,根据uerID模以20,然后根据这个端口进行通信
IO多路复用:io检测
linux工具:select,poll,epoll
select(maxfd,rfds,wfds,efds,timeout)
select(最大fd的数量,可读集合,可写的集合,出错的集合,多长时间轮询一次)
for (i=0;i<maxfd;i++)
poll(pfds,length,timeout)
epoll三个函数
int epfd = epoll_create(size);//创建一个总集合
epoll_ctl(epfd,opera,fd,event);//向总集里面加入一个一个节点
epoll_wait(epfd,events,length,timeout);//每次带出就绪的节点的集合
现实情况:10w个io可读的不到1% ——->不需要每一次都把十万个io拷贝进内核
select/epoll的性能区别:10倍差距
传统网络的不足:
引入新的技术:旁路技术
NIC网络
dpdk
五、用libevent(事件通知库)解决网络思路(P39)
(封装reactor模型)
网络编程步骤:
任何网络模型1-3是相同的,不同的是4-8
1.封装了那些事件:驱动服务端逻辑的事件
a.网络事件
解决的问题
有不同的方式
reactor如何解决问题:io->事件(同步io)
read为例
a.检测io是否就绪,检测接收缓冲区是否有数据
io多路复用select/poll/epoll解决a步骤
b.有数据的话执行IO操作(从内核态数据拷贝到->用户态)
回调函数:提前写一个回调函数,让有读事件发生时,去调用回调函数
异步IO:AcceptEx
b.延时事件
c.信号事件
libevent封装层次
a.事件对象:(对应一个链接)与连接一一对应
bufferevevnt:不需要自己去设计网络缓冲区
evcoonnlistener封装了accept,通过回调拿到客户端的addr,size,fd
b.reactor对象:(对应一个线程)
作用:管理事件对象
c.操作事件:
1.注册时间/注销时间->IO多路复用之中
2.响应事件(回调函数)
响应时间
d.事件循环
解决了哪些痛点?
接受连接(accpet流程):
1.将socket绑定到event_base里面(reactor对象),
2.backlog(listen中)全连接队列中的长度,同时允许多少个服务器连接
3.cb回调在event_loop中触发
主动建立连接与redis
connect指定 远端地址和端口
创建一个具体的事件对象
处理IO:
自己处理IO:
event不带缓冲区,需要自己建立缓冲区,他只知道事件发生了
1.创建socket
2.绑定地址
3.监听
裸事件对象,需要自己调用accept
如果时间发生,调用socket_read_cb
len返回0,处理断开
自己调用read
EOF文件结束符,收到close时,在接收缓冲区加入EOF
read读到EOF时,返回0
用libevent处理IO
没有accept,send,receive
在memcached中如何应用?
基于多reactor模型
初始化线程
创建管道
每个线程都有一个队列
主线程接受一个新连接后
第一步:主线程通过robin(轮询)将新来的连接,加入到子线程的连接队列中,
第二步:通过管道通知子线程去处理她连接队列中的连接
主线程
子线程
六、reactor网络模型(P41,P42)
1.cs网络编程流程
阻塞io
用strace查看阻塞
非阻塞IO模型
阻塞和非阻塞是由fd决定的
设置非阻塞
io多路复用只解决数据是否就绪
reactor解决:事件回调
为什么不用IO多路复用+阻塞?
1.IO多路复用会被其他信号/其他系统调用打断
a.三个系统调用未检测到数据就绪就开始返回-1
此问题可以用条件判断解决
b.多线程问题,每个线程io有一个io多路复用,这些多个io多路复用监听同一个listenfd
四个IO多路复用的线程都被唤醒,但只能有一个线程处理,其他线程阻塞,会报错
c.select会误报读事件,此时read会阻塞
reactor 把io的处理转化为对事件的处理
回调函数处理IO
代码:
1.把fd设置为非阻塞fcntl
2.初始化IO多路复用
3。注册
并且告诉event对哪个fd的读事件感兴趣
注册读事件,
epoll_wait阻塞等待多路连接的就绪事件,检测多个连接
nevent返回同时有多少个事件触发
reactor基于事件
epfd管理io多路复用的句柄
事件对象:对应fd和socket,读写缓冲区,回调函数
reactor封装
事件的封装
init_sever();//返回一个已经监听的listenfd
epoll_wait()最后一位,小于零一直阻塞,等于零不阻塞,大于零为阻塞时长
accept
服务器最初就一个fd(listenfd)
小疑问:epoll服务器最初就一个fd(listenfd),那是如何做到监听多个fd呢?
假设你有 5 个不同的 listenfd
,每个代表一个不同的监听套接字。你需要将每个 listenfd
都通过 epoll_ctl
注册到 epoll
中,监听 EPOLLIN
事件(表示有新的客户端连接)
c
struct epoll_event ev;
int listenfd1 = socket(AF_INET, SOCK_STREAM, 0); // 第1个监听套接字
int listenfd2 = socket(AF_INET, SOCK_STREAM, 0); // 第2个监听套接字
//...类似地创建第3, 4, 5个 listenfd
ev.events = EPOLLIN; // 监听可读事件,即有新连接到来
ev.data.fd = listenfd1;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd1, &ev); // 注册第1个 listenfd
ev.data.fd = listenfd2;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd2, &ev); // 注册第2个 listenfd
// 类似地,注册第3, 4, 5个 listenfd
一旦所有的 listenfd
都被注册到 epoll
,你就可以通过 epoll_wait
来同时监听这些 listenfd
的事件。当有客户端连接请求时,epoll_wait
会返回对应的 listenfd
。
struct epoll_event events[MAX_EVENTS]; // 用于存储发生事件的文件描述符
while (1) {
// 等待事件发生
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listenfd1) {
// listenfd1 有新客户端连接
int connfd = accept(listenfd1, NULL, NULL);
// 处理 connfd 的逻辑...
} else if (events[i].data.fd == listenfd2) {
// listenfd2 有新客户端连接
int connfd = accept(listenfd2, NULL, NULL);
// 处理 connfd 的逻辑...
}
// 类似地处理其他 listenfd 的事件
}
}
建立一个结构体struct item,将buff和clientfd进行绑定,防止一次读不完。
补充:写buffer
根据fd找到对应item
读事件
写事件
因为epoll对于没读写完的缓冲区不做处理,所以需要自己建立bufferr与socket做映射,来保存数据
如何处理buffer---->回调的由来
1.不能在里面对数据直接进行解析
2.也不能push,那样会依赖其他模块
---->定义一个接口,回调函数(fd与item--对应)
----->重新定义accept事件、read事件和write事件
如果合并成一个回调,是否更好
--->reactor里面需要的成员
1.内部创建,内部销毁
2.外部传进来
哪一种更好?(第二种更让人接受)
调用
如果fd作索引,但fd的值大于1024怎么办?
在建立一个结构体,默认一块1024,不够再追加1024,一直下去
设置回调,提供给别人实现的接口
千万不能出现一个fd被多个线程使用
放在一起无法处理一个客户端的不连续发送的数据,无法得知消息的先后
七、epoll-tcp并发组件(P44)
后续更新。。。
八、6种epoll做法
后续更新。。。