大家好,我是小林。
这次跟大家分享一位同学面腾讯后端开发的面经,一步一步深挖计算机基础的内容,问的问题很多,光面试时常长达 1 个小时多,再加上写算法 20 分钟,面试的强度还是挺大的。
虽然面试强度是比较大了一点,但是整体面下来,同学反馈还是很有收获的,也算是对自己没掌握的内容进行查漏补缺的过程。
为了使得多种设备能通过网络相互通信,和为了解决各种不同设备在网络互联中的兼容性问题,国际标准化组织制定了开放式系统互联通信参考模型(Open System Interconnection Reference Model),也就是 OSI 网络模型,该模型主要有 7 层,分别是应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层。
img应用层:应用层最接近终端用户。大多数应用程序都位于这一层。我们从后端服务器请求数据,无需了解数据传输的具体细节。这一层的协议包括 HTTP、SMTP、FTP、DNS 等。
表示层:这一层处理数据编码、加密和压缩,为应用层准备数据。例如,HTTPS 利用 TLS(Transport Layer Security)实现客户端与服务器之间的安全通信。
会话层:该层用于打开和关闭两个设备之间的通信。如果数据量较大,会话层就会设置检查点,避免从头开始重新发送。
传输层:该层处理两个设备之间的端到端的通信。它在发送方将数据分解成段,然后在接收方重新组装。这一层有流量控制,以防止拥塞。这一层的主要协议是 TCP 和 UDP。
网络层:这一层实现不同网络之间的数据传输。它进一步将网段或数据报分解成更小的数据包,并使用 IP 地址找到通往最终目的地的最佳路由。
数据链路层:这一层允许在同一网络的设备之间传输数据。数据包被分解成帧,这些帧被限制在局域网内。
物理层:这一层通过电缆和交换机发送比特流,因此与设备之间的物理连接密切相关。与 OSI 模型相比,TCP/IP 模型只有 4 层。在讨论网络协议的层次时,必须明确上下文。
TCP适用:网页、电子邮件、远程登录连接、文件传输
UDP适用:语音通话,多播通信,DNS解析
面向连接、同步序列号、校验和、流量控制和拥塞控制。
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
半连接队列与全连接队列不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。
有可能会导致TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
避免 SYN 攻击方式,可以有以下四种方法:
方式一:调大 netdev_max_backlog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
net.core.netdev_max_backlog = 10000
方式二:增大 TCP 半连接队列
增大 TCP 半连接队列,要同时增大下面这三个参数:
方式三:开启 net.ipv4.tcp_syncookies
开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
tcp_syncookies 应对 SYN 攻击具体过程:
cookie
值;accpet()
接口,从「 Accept 队列」取出的连接。可以看到,当开启了 tcp_syncookies 了,即使受到 SYN 攻击而导致 SYN 队列满时,也能保证正常的连接成功建立。
net.ipv4.tcp_syncookies 参数主要有以下三个值:
那么在应对 SYN 攻击时,只需要设置为 1 即可。
$ echo 1 > /proc/sys/net/ipv4/tcp_syncookies
方式四:减少 SYN+ACK 重传次数
当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。
那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。
SYN-ACK 报文的最大重传次数由 tcp_synack_retries
内核参数决定(默认值是 5 次),比如将 tcp_synack_retries 减少到 2 次:
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?
服务端通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端 IP 和端口是可变的,其理论值计算公式如下:
img
对 IPv4,客户端的 IP 数最多为 2
的 32
次方,客户端的端口数最多为 2
的 16
次方,也就是服务端单机最大 TCP 连接数,约为 2
的 48
次方。
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:
文件描述符限制
,每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
cat /proc/sys/fs/file-max
查看;cat /etc/security/limits.conf
查看;cat /proc/sys/fs/nr_open
查看;内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。
也需要的,客户端发送第一个 syn 报文的时候,也需要在数据链路层组装 mac 地址的。
至此,我们完成了 DNS 的解析过程。现在总结一下,整个过程我画成了一个图。
最直接的办法就是抓包,排查的思路大概有:
先确定是服务端的问题,还是客户端的问题。先确认浏览器是否可以访问其他网站,如果不可以,说明客户端网络自身的问题,然后检查客户端网络配置(连接wifi正不正常,有没有插网线);如果可以正常其他网页,说明客户端网络是可以正常上网的。
如果客户端网络没问题,就抓包确认 DNS 是否解析出了 IP 地址,如果没有解析出来,说明域名写错了,如果解析出了 IP 地址,抓包确认有没有和服务端建立三次握手,如果能成功建立三次握手,并且发出了 HTTP 请求,但是就是没有显示页面,可以查看服务端返回的响应码:
如果客户端网络是正常的,但是访问速度很慢,导致很久才显示出来。这时候要看客户端的网口流量是否太大的了,导致tcp发生丢包之类的问题。
总之就是一层一层有没有插网线,网络配置是否正确、DNS有没有解析出 IP地址、TCP有没有三次握手、HTTP返回的响应码是什么。
推荐阅读:网站显示不出来,怎么排查?
http 是 80,https 默认是 443。
第一种方式:telnet:telnet命令用于建立与远程主机的Telnet连接,并可以使用telnet命令测试特定端口的可访问性。
telnet IP地址 端口号
用于测试指定IP地址上的指定端口是否可访问。如果能够建立连接,则表示端口通畅;如果连接失败或超时,则表示端口不可访问。第二种方式:nc:nc命令(也称为netcat)是一个网络工具,可以用于创建各种类型的网络连接,包括测试端口的可访问性。
nc -zv IP地址 端口号
用于测试指定IP地址上的指定端口是否可访问。如果能够成功连接,则表示端口通畅;如果连接失败或拒绝,则表示端口不可访问。
netstat -tunlp
用于显示所有TCP和UDP端口的监听状态。
lsof -i :端口号
用于显示指定端口的相关信息。
ICMP 协议。
ICMP 回送消息1xx
类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。2xx
类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。3xx
类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。4xx
类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。5xx
类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。「403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
常见的情况包括:
重定向状态码如下,301 和 302 都会在响应头里使用字段 Location
,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。
重定向是指将一个URL请求转发到另一个URL的过程。重定向的作用包括:
更改URL:通过重定向,可以更改URL,使其更易于记忆、更友好或更有意义。例如,将长而复杂的URL重定向到简洁的、易于理解的URL。
网站迁移:当网站进行重构、更换域名或更改URL结构时,通过重定向旧的URL到新的URL,可以让用户和搜索引擎正确地访问和索引新的内容。
推荐阅读:面试官:你背一下负载均衡算法?
拥塞控制是用于在网络拥塞时减少网络流量和避免网络崩溃。
TCP的拥塞控制机制主要包括以下几个方面:
慢启动:当建立连接或恢复丢失的数据包时,TCP会以指数增加的方式逐渐增加发送窗口的大小,从而逐渐增加发送的数据量。
拥塞避免:在慢启动阶段,当检测到网络拥塞时,TCP会切换到拥塞避免模式。拥塞避免使用线性增加的方式逐渐增加发送窗口的大小,以降低网络拥塞的风险。
快速重传:当发送方连续接收到同一个确认号的重复确认时,它会认为该数据包已经丢失,并立即重新发送丢失的数据包,而不等待超时重传。
快速恢复:在快速重传后,发送方会将拥塞窗口减半,并使用拥塞避免模式逐渐增加发送窗口的大小,以便恢复正常的发送速率。
通过这些机制,TCP能够根据网络的拥塞情况动态调整发送窗口的大小和发送速率,以避免网络拥塞并保证数据的可靠传输。拥塞控制是TCP协议的重要特性,它在保持网络稳定和高效运行方面起着重要作用。
GET:用于请求获取指定资源,通常用于获取数据。
POST:用于向服务器提交数据,通常用于提交表单数据或进行资源的创建。
PUT:用于向服务器更新指定资源,通常用于更新已存在的资源。
DELETE:用于请求服务器删除指定资源。
HEAD:类似于GET请求,但只返回资源的头部信息,用于获取资源的元数据而不获取实际内容。
根据 RFC 规范,GET 的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。
比如,你打开我的文章,浏览器就会发送 GET 请求给服务器,服务器就会返回文章的所有文字及资源。
GET 请求根据 RFC 规范,POST 的语义是根据请求负荷(报文body)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。
比如,你在我文章底部,敲入了留言后点击「提交」(暗示你们留言),浏览器就会执行一次 POST 请求,把你的留言文字放进了报文 body 里,然后拼接好 POST 请求头,通过 TCP 协议发送给服务器。
POST 请求如果从 RFC 规范定义的语义来看:
但是实际过程中,开发者不一定会按照 RFC 规范定义的语义来实现 GET 和 POST 方法。比如:
HTTP 协议采用的是「请求-应答」的模式,也就是客户端发起了请求,服务端才会返回响应,一来一回这样子。
请求-应答由于 HTTP 是基于 TCP 传输协议实现的,客户端与服务端要进行 HTTP 通信前,需要先建立 TCP 连接,然后客户端发送 HTTP 请求,服务端收到后就返回响应,至此「请求-应答」的模式就完成了,随后就会释放 TCP 连接。
一个 HTTP 请求如果每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 HTTP 短连接,如下图:
HTTP 短连接这样实在太累人了,一次连接只能请求一次资源。
能不能在第一个 HTTP 请求完后,先不断开 TCP 连接,让后续的 HTTP 请求继续使用此连接?
当然可以,HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接。
HTTP 长连接HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
举个例子:进程=火车,线程=车厢
多线程不一定越多越好,过多的线程可能会导致一些问题。
01 先来先服务调度算法
最简单的一个调度算法,就是非抢占式的先来先服务(*First Come First Serve, FCFS*)算法了。
FCFS 调度算法
顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
这似乎很公平,但是当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。
FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。
02 最短作业优先调度算法
最短作业优先(*Shortest Job First, SJF*)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
SJF 调度算法
这显然对长作业不利,很容易造成一种极端现象。
比如,一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。
03 高响应比优先调度算法
前面的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和长作业。
那么,高响应比优先 (*Highest Response Ratio Next, HRRN*)调度算法主要是权衡了短作业和长作业。
每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,「响应比优先级」的计算公式:
从上面的公式,可以发现:
04 时间片轮转调度算法
最古老、最简单、最公平且使用最广的算法就是时间片轮转(*Round Robin, RR*)调度算法。
RR 调度算法
每个进程被分配一个时间段,称为时间片(*Quantum*),即允许该进程在该时间段中运行。
另外,时间片的长度就是一个很关键的点:
一般来说,时间片设为 20ms~50ms
通常是一个比较合理的折中值。
05 最高优先级调度算法
前面的「时间片轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。
但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(*Highest Priority First,HPF*)调度算法。
进程的优先级可以分为,静态优先级和动态优先级:
该算法也有两种处理优先级高的方法,非抢占式和抢占式:
但是依然有缺点,可能会导致低优先级的进程永远不会运行。
06 多级反馈队列调度算法
多级反馈队列(*Multilevel Feedback Queue*)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。
顾名思义:
多级反馈队列
来看看,它是如何工作的:
可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也变更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。
MySQL 是关系型数据库,适用于需要保持数据一致性、进行复杂的数据分析和关联查询的场景。
Redis是一种基于内存的数据存储系统,它主要用于缓存和高速数据访问。Redis适合处理高速读写和缓存需求,适用于需要快速响应和高并发的场景,例如实时计数器、会话缓存、消息队列、发布/订阅系统等。
一般会用Redis 作为MySQL的缓存,主要是因为 Redis 具备「高性能」和「高并发」两种特性。
1、Redis 具备高性能
假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。
img如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有 Redis 和 MySQL 双写一致性的问题,后面我们会提到。
2、Redis 具备高并发
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
InnoDB:支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。
MyISAM:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低,也可以使用。如果数据表主要用来插入和查询记录,则MyISAM引擎能提供较高的处理效率
MEMORY:所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果
事务用于解决数据库操作中的一致性和持久性问题。
一致性问题指的是在多个并发操作中,如果其中一个操作失败或发生错误,可能导致数据处于不一致的状态,不符合预期的要求。
持久性问题指的是确保已提交的数据在数据库系统
事务的四大特性主要是:
InnoDB 存储引擎的索引结构是 b+树。
选择 b+树作为数据结构的原因是:
O(logdN)
,其中 d 表示节点允许的最大子节点个数为 d 个。在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN)
,这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。MySQL 3 种text类型的最大长度如下:
索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:
所以,索引不是万能钥匙,它也是根据场景来使用的。
WHERE
条件,GROUP BY
,ORDER BY
里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。
#include
#include
#include
class LRUCache {
private:
int capacity;
std::unordered_map<int, std::list<std::pair<int, int>>::iterator> cache;
std::list<std::pair<int, int>> lruList;
public:
LRUCache(int capacity) {
this->capacity = capacity;
}
int get(int key) {
if (cache.find(key) == cache.end()) {
return -1; // 如果key不存在,返回-1
}
// 将访问的节点移动到链表头部
std::pair<int, int> keyValue = *cache[key];
lruList.erase(cache[key]);
lruList.push_front(keyValue);
cache[key] = lruList.begin();
return keyValue.second;
}
void put(int key, int value) {
if (cache.find(key) == cache.end()) {
// 如果容量已满,移除最久未使用的节点
if (lruList.size() == capacity) {
std::pair<int, int> lastPair = lruList.back();
int lastKey = lastPair.first;
cache.erase(lastKey);
lruList.pop_back();
}
// 将新节点添加到链表头部
lruList.push_front(std::make_pair(key, value));
cache[key] = lruList.begin();
} else {
// 如果key已存在,更新节点的值,并将节点移动到链表头部
lruList.erase(cache[key]);
lruList.push_front(std::make_pair(key, value));
cache[key] = lruList.begin();
}
}
};
int main() {
LRUCache cache(2);
cache.put(1, 1);
cache.put(2, 2);
std::cout << cache.get(1) << std::endl; // 输出1
cache.put(3, 3);
std::cout << cache.get(2) << std::endl; // 输出-1
cache.put(4, 4);
std::cout << cache.get(1) << std::endl; // 输出-1
std::cout << cache.get(3) << std::endl; // 输出3
std::cout << cache.get(4) << std::endl; // 输出4
return 0;
}
全部0条评论
快来发表一下你的评论吧 !