复盘面经(3):26届年后字节一面 @EpiphanyCyy
这是我复盘别人的面经,不是我自己的,引用块内都是我自己写的回答,引用块后面的都是gpt生成的。
作者:EpiphanyCyy
链接:https://www.nowcoder.com/feed/main/detail/5f6c0485ff144f8483c08bb8709bae76?sourceSSR=users
来源:牛客网
计算机网络:
1、url输入发生了什么?客户端和服务端之间如何通信?
浏览器会解析这个url,解析出来http协议、域名、请求路径以及参数等信息。解析完之后,就得到了域名,但是只知道域名没办法发送请求到服务器,还需要获取到该域名的IP地址。所以就需要进行DNS域名解析。它先发起一个请求到本地DNS服务器,寻问这个域名的IP地址,如果他不知道就返回根域名服务器的地址给本地DNS服务器,本地DNS服务器知道根域名服务器之后就回去访问根域名服务器,如果他也不知道就会返回顶级域名服务器的地址,本地域名服务器根据这个地址去访问顶级域名服务器,如果它也不知道就会返回权威DNS服务器,权威DNS服务器就是当前这个域名的来源,他肯定知道IP地址,就返回对应的IP地址给本地DNS服务器,本地DNS服务器知道后在返回给浏览器,此时浏览器就知道了这个域名的IP地址。知道IP地址之后,就可以进行TCP连接,建立TCP连接是保证可靠传输的,建立连接之前会经过三次握手。建立连接成功后,浏览器会发起一个HTTP请求,这个HTTP请求包含了请求头和请求体,请求头中有HTTP协议信息,请求路径、请求方式等信息,请求体中有请求数据信息。请求报文到传输层的时候,会进行分片然后加入TCP首部进行传输。到了网络层之后,又会加上IP首部,首部中包含原IP地址和目标IP地址。有了目标IP地址就可以找到目标服务器。然后又到了数据链路层,数据链路层仅仅知道目标IP地址还是不够的,它需要这个IP地址的MAC地址才行,因为IP地址只是一个虚拟网络地址,不是设备的真实地址,MAC地址就是设备的实际物理地址,有了这个地址就可以在链路中知道方向进行传输,加上首部在进行传输。到了物理层,就会将这个报文在实际物理链路上传输。到达目标服务器之后,会一层一层的删掉首部,最后获取到原始的HTTP请求报名。服务器收到这个HTTP请求报文后,会处理这个请求,这其中包括执行一些业务逻辑等。处理完成后生成一个HTTP响应报文发送给浏览器,浏览器解析这个响应报文,如果这个响应体是HTML代码,就会渲染页面。如果浏览器和服务器之间没有请求了,就会关闭TCP连接,其中要经过四次挥手。
2、tcp三次握手、四次挥手?
TCP三次握手:第一次握手:客户端首先发起一个SYN表示的请求报文,并且带上自己的序列号x。第二次握手:服务器收到这个请求之后,会向客户端发起一个带有SYN+ACK标识的报文,表示自己已经接收到请求了,并且也知道客户端的序列号了,ack=x+1,这个报文也会带上服务器的序列号y。第三次握手:客户端收到之后,发起一个ACK标识的报文,ack=y+1,表示知道服务器的序列号了。服务器收到后三次握手就完成了。
四次挥手:第一次挥手:客户端先发起一个带有FIN标志的报文,表示要终止连接,并带上序列号seq=x。第二次挥手:服务器收到这个报文后,也会发起一个ACK标志的报文,ack=x+1,表示接收到这个请求了。在第二次挥手之后,服务器会继续执行剩下的请求,当所有请求完成之后。进行第三次挥手:服务器发送一个带有FIN标志的报文,带上序列号seq=y,表示自己所有请求发送完了,可以终止连接了。第四次挥手,客户端收到这个报文后,会发送一个ACK标志的报文,ack=y+1,表示自己接受到这个报文了,告诉服务器可以断开连接了。客户端发送完这个报文后还会等待2*msl(报文寿命)时间之后才会关闭,因为他担心发送的ACK确认报文服务器没有接受到,服务器会一直等待,超时重传,这样客户端就会在发送一次ACK报文,2msl正好是两次发送报文的最长时间,所以就可以避免报文丢失服务器一直等待的问题。
3、滑动窗口体现的上层应用有哪些?
TCP的可靠传输机制:有了滑动窗口,就可以控制当前发送数据的速率,并且可以保证数据按序接收,不重复接受。并且如果数据丢失了,那么就会让客户端重新发送,这样就可以保证数据不丢失。流量控制和拥塞控制都用到了滑动窗口。
传输层:TCP 协议
- 流量控制原理:TCP 是面向连接的、可靠的传输层协议,滑动窗口机制在其中用于流量控制。接收方会维护一个接收窗口(RWND),并将其大小告知发送方。发送方依据这个窗口大小来决定可以连续发送多少字节的数据。例如,接收方的接收窗口大小为 5000 字节,那么发送方最多可以连续发送 5000 字节的数据而无需等待确认。随着接收方接收并处理数据,接收窗口会动态调整大小,并通过 ACK 报文将新的窗口大小告知发送方,发送方相应地调整发送数据的量,以此避免发送方发送数据过快致使接收方缓冲区溢出。应用场景:在网页浏览、文件下载等应用中,TCP 的滑动窗口流量控制机制能够确保数据在发送方和接收方之间平稳传输。比如,当你从服务器下载一个大文件时,服务器会根据你的客户端接收窗口的大小来调整发送数据的速率,保证文件能够顺利下载。
- 拥塞控制原理:除了流量控制,滑动窗口在 TCP 的拥塞控制中也起着重要作用。发送方还会维护一个拥塞窗口(CWND),它反映了网络的拥塞状况。发送方实际可以发送的数据量取决于接收窗口和拥塞窗口中的较小值。当网络出现拥塞时,拥塞窗口会减小,从而限制发送方的发送速率;当网络状况改善时,拥塞窗口会逐渐增大。应用场景:在网络拥塞的情况下,如多个用户同时访问一个热门网站,TCP 的拥塞控制机制通过滑动窗口动态调整发送速率,避免网络进一步拥塞,保障网络的稳定性和可靠性。
4、我们平时说的翻“墙”,“墙”是什么?梯子的原理是怎么做到的?如果要阻止“翻墙”的行为,应该怎么做?
这个我还真没具体了解过,虽然我也干过,我只知道连接VPN。我们访问外国网站,这个请求肯定会经过中国的路由器,中国的路由器会识别出来这些网站,然后拒绝转发该请求包。
梯子的原理是我去访问没有被限制的外国网站,然后这些网站去帮我发起获取那些被限制的请求,然后他们获取到数据之后返回给我。阻止的话我知道很多卖翻墙服务的网站经常被封,将这些IP给禁了。
1. “墙”的含义
在网络语境中,所谓的“墙”一般指中国的网络防火墙,正式名称为“国家网络防火墙” 。它是为了维护国家网络安全、信息安全,抵御外部网络攻击、恶意渗透,防止不良信息流入国内而设立的安全防护系统。它会对进出中国互联网的信息进行监测和管理,阻止访问包含不良内容(如宣扬恐怖主义、色情、暴力等)、危害国家安全以及未经授权的境外网站等。
2. “梯子”(翻墙工具)原理
“梯子”通常指通过非法手段绕过网络防火墙限制,实现访问被限制境外网站的工具,其常见原理如下:
- VPN(虚拟专用网络)违规搭建:正规的VPN服务需相关部门许可,而用于“翻墙”的VPN是私自搭建的。它通过在境外服务器和本地设备之间建立一条加密通道,本地设备的数据先发送到境外服务器,伪装成该服务器发起的请求,从而绕过国内网络监管访问境外网站。
- 代理服务器:“翻墙”者使用位于境外的代理服务器,将自己的网络请求发送到代理服务器,代理服务器再代为访问目标境外网站,并把响应结果返回给“翻墙”者,以此绕过限制。但未经电信主管部门批准,不得自行建立或租用专线(含虚拟专用网络VPN)等其他信道开展跨境活动。
3. 阻止“翻墙”行为的措施
- 技术监管:深度包检测:通过对网络数据包的深度检测,识别出与非法“翻墙”工具相关的网络流量特征,如特定的协议、端口使用等,从而对这类流量进行拦截。IP封锁:识别并封锁已知用于“翻墙”的代理服务器、VPN服务器的IP地址,阻止用户连接到这些非法通道。
- 法律约束:完善法律法规:明确“翻墙”行为属于违法行为,对私自建立、租用专线或者使用未经授权的“翻墙”工具等行为制定严格的处罚标准。加强执法:相关部门加大对“翻墙”行为及相关非法服务提供商的打击力度,依法追究违法者的法律责任,包括罚款、拘留等处罚措施。
- 宣传教育:网络安全教育:通过学校、社区等多种渠道,开展网络安全知识普及活动,让公众了解“翻墙”行为的违法性和可能带来的安全风险,如个人信息泄露、遭受网络诈骗、引入恶意软件等。正面引导:引导公众合法、文明地使用网络,通过正规渠道获取信息,提高公众的网络安全意识和法律意识,自觉抵制“翻墙”行为。
未经许可“翻墙”违反了中国的法律法规,同时也会带来诸多安全风险,如个人信息泄露、遭受恶意攻击等。每个人都应当遵守国家法律,合法使用网络。
操作系统
5、进程通信的几种方式?
管道、信号量、共享内存,消息队列
这里可以说一下每个方式的实现原理:
管道:内存有一个区域专门用来做管道,这个管道一方只能去写数据,一方去读数据,而且是读写互斥的。
信号量:信号量主要是用来做同步的,信号量有一个值来判断当前锁的持有者,还有一个阻塞队列用来存储所有没有获取锁的进程。
共享内存:多个线程共享的一块区域,每个区域都可以被读或者写,使用效率很高。
消息队列:每个线程都有一个消息队列,一个进程可以将数据都发送到另一个进程的消息队列中,获取数据就可以直接在消息队列中读取,也没有读写互斥了。
6、什么是“死锁”?如何处理“死锁”问题?
死锁就是有多个进程因为互相争夺资源而导致每个进程无法继续执行的情况,就是死锁。
死锁发生的四个条件:
- 互斥条件:争夺的资源只能被一个进程占用。
- 持有并等待:当前进程已经获得某些资源了,仍然继续申请资源,并且不释放当前资源。
- 不可剥夺:当前进程已经获得的资源不能被其他进程强行剥夺。
- 循环等待:多个进程执行互相占有对方所需要的资源,形成了一个环。比如进程A想获取进程B的资源,进程B想获取进程A的资源。
死锁预防:
- 破坏互斥条件:将互斥资源变成共享的,比如spolling技术,这个技术做到了一个伪共享,并不是说资源真的被共享了。比如说打印机资源,他有一个输入进程和输出进程,输入井和输出井。每个进程都可以将自己的打印任务交给输入进程,输入进程将任务放到输入井中等待打印机打印,打印完成之后,放到输出井中,进程可以通过输出进程去获取结果。
- 破坏持有并等待条件:当一个进程申请资源的时候,就放弃自己所有已经拥有的资源。这个方案不太好,因为很多资源还会继续使用,如果全部释放了还要重新获取,影响进程执行。进程执行前就申请全部所需要的资源。这个也有问题,一次申请全部资源,首先就要等待很长时间,全部获取到之后进程执行过程中,很多资源只使用了很短的时间,但进程一直不释放这些资源。就会导致资源空间但不释放,影响其他进程执行。
- 破坏不可剥夺条件:当进程想获取某个资源但被其他线程占有时,直接强行剥夺。这个方法也不太好,加入别的进程还在使用该资源呢,强行剥夺会影响进程的执行。
- 破坏循环等待条件:将所有资源编一个唯一的序号,所有进程申请资源时,只能按照序号递增的顺序申请,这样就不会出现一个进程拥有A资源,一个进程拥有B资源,然后互相申请对方的资源。也就破坏了循环等待条件。
死锁避免:银行家算法,每个进程申请资源时,都会去计算申请资源后系统会不会出现不安全情况。如果出现就不允许分配资源给该进程。
7、什么是“孤儿进程”?什么是“僵尸进程”?
孤儿进程:听名字应该是这个进程只有自己,不跟其他任何进程之间相关联,也不与其他进程之间出现竞争关系。
僵尸进程:听名字应该是一个进程申请资源没有获取到进入了阻塞状态,然后一直没有被唤醒,所以就成了僵尸进程。
在操作系统中,“孤儿进程”和“僵尸进程”是与进程生命周期相关的两个概念:
孤儿进程
- 定义:如果一个父进程在它的子进程结束之前就终止了,那么这些子进程就会成为孤儿进程。孤儿进程会被init进程(在Linux系统中,进程ID为1)收养,init进程成为它们新的父进程。这是因为在操作系统中,每个进程都必须有一个父进程,当原父进程提前退出时,系统需要为这些子进程重新指定一个父进程来管理它们。
- 存在意义与影响:通常情况下,孤儿进程本身并不会对系统造成危害。init进程会负责回收孤儿进程的资源,确保它们正常结束。例如,某些守护进程(daemon)在启动后会创建子进程来执行特定任务,父进程执行完初始化工作后就退出,留下的子进程成为孤儿进程继续独立运行,完成如日志记录、数据采集等后台任务。
僵尸进程
- 定义:当一个子进程先于父进程结束运行,且父进程没有调用wait()或waitpid()系统调用来获取子进程的退出状态时,子进程虽然已经终止运行,但其在进程表中的进程控制块(PCB)仍会保留,这样的进程就被称为僵尸进程。僵尸进程处于一种特殊的状态,它已经释放了大部分资源,但PCB还存在,目的是等待父进程获取其退出状态信息。
- 存在意义与影响:僵尸进程存在的初衷是为了让父进程能够了解子进程的执行结果,比如子进程的退出状态、资源使用情况等。然而,如果大量僵尸进程堆积,会消耗系统资源,因为每个僵尸进程都占用一定的进程表空间。而且,由于进程表的大小是有限的,过多的僵尸进程可能导致系统无法创建新的进程,影响系统的正常运行。例如,在一个循环中频繁创建子进程且父进程不等待子进程结束的程序中,可能会产生大量僵尸进程,最终耗尽系统资源。
8、会不会Linux?包括一些基本命令。
ls:查看当前目录下的所有文件
mkdir:创建一个文件或者目录
vim:编辑文件
top:查看当前系统中执行的进程,按照CPU占有率排序。
rm:删除文件
ping:查询某个网址是否正常访问
ifconfig:查看当前网络配置
cd:切换目录
find:查询文件
MySQL
9、解释下不可重复读、幻读,以及它们的区别;
脏读:事务执行的过程中,读取到了其他还未提交的事务修改的数据。
不可重复读:事务执行过程中,前后读取相同的数据结果却不一样,因为在第一次读取完之后,其他事务修改了这条数据并提交了事务。所以读取结果不一致。
幻读:事务执行过程中,读取相同查询条件下的记录,发现前后数据总数不一致,好像出现了幻觉。
这里可以扩展事务隔离级别、MVCC、事务的四大特性、MYSQL的几个log日志。
10、IP地址如何在数据库里存储?
我第一个想的是字符串存储,字符串存储需要使用十几个字节,ip地址是由32位二进制数组成,如果用整数存的话只有4个字节。
正确答案还有一个二进制存储。
Redis
11、redis的持久化(rdb、aof)什么情况下都开启
这个我学过,但记不清了,害,学着忘着。
Redis的读写操作都是在内存中的,所以Redis性能才会高,但当Redis重启的时候,内存中的数据就会丢失,为了保证redis重启后数据不会丢失,redis实现了数据持久化的机制,这个机制会将redis中的数据存储在磁盘中,当redis重启后能从磁盘中恢复数据。redis有两种实现机制:
- AOF:每次执行一条写命令,就将这条命令以追加的形式加入到一个文件中
- RDB:将某一时刻的内存数据,以二进制形式存入磁盘
AOF日志是如何实现的?
每次执行一条写命令,就将这条命令以追加的形式加入到一个文件中,然后Redis重启的时候,会重新读取这个文件,然后逐一执行每条命令来恢复数据。
Redis提供了三种写回磁盘的策略:
- Always:总是,每次执行一条写命令,就将该命令写到磁盘中
- EverySec:每次执行完一条写命令后,将这条命令写入一个缓冲区,然后每间隔1s将缓冲区内的数据写回磁盘。
- No:每次执行完一条写命令后,将这条命令写入一个缓冲区,缓冲区里的数据什么时候写回磁盘由操作系统决定。
RDB 快照是如何实现的呢?
AOF日志文件中存取的都是一条条命令,当恢复数据时,需要读取每一条命令并且执行,如果命令很多,那么执行时间就会很长。为了解决这个问题,提出了RDB快照。
RDB快照,顾名思义,就是将某一时刻的所有的数据保存下来,以后直接获取数据到内存中,不需要在执行写命令了。
Redis提供了两种实现RDB快照的方式,区别在于是否在主线程中执行:
- save:主线程中生成RDB快照,主线程还要执行读写操作命令,所以生成RDB快照会阻塞主线程的执行。
- bgsave:后台线程去生成RDB快照,不会影响主线程的执行。
AOF和RDB的优缺点
- AOF:优点:安全性高,如果redis某一时刻宕机了,那么只有最后一次的写命令丢失,前面的命令都已经保存到磁盘中了,而且提供了三种写回策略,根据需要调整性能和安全性的平衡。缺点:AOF文件存的都是命令,就导致文件很大,占用更多的磁盘空间,而且每次写操作执行都写回磁盘,肯定影响redis的性能。而且恢复数据的时候,文件太大执行恢复的时间也会很长。
- RDB:优点:生成的RDB快照只保存了某一时刻redis的二进制数据,所以文件肯定比AOF小很多,恢复的时候也可以直接将数据读取到内存中,不需要在执行命令了。而且可以让后台进程去执行RDB快照,不会影响redis的正常操作的执行。缺点:RDB快照的间隔时间需要控制,如果两次快照之间保存的数据的过程中,redis宕机了,那么这一段的数据都丢失了,那么在恢复的时候,这期间的写操作也就丢失了,这就导致恢复前和恢复后的数据不一致的情况。比AOF丢失要严重。
JVM
12、JVM为什么要设置不同的区来处理?(开放题)
这里是说的垃圾回收中的新生代和老年代是吧。
为啥要分区呢,因为在大多数java程序的执行过程中,很多对象仅仅使用一次或几次就不在使用了,而有些对象会一直使用,那么就可以将新对象都放到一个区域,这个区域中的大多数对象很快就会被垃圾回收掉,而少部分对象经过几次垃圾回收都没有被回收掉说明是经常使用的对象,可以将这些对象放到另外一个区域。另外一个区域都用来存放这些使用次数多的对象。这个区域不会经常执行垃圾回收,只有当整个内存都满了才会将整个区域内的对象进行垃圾回收。这样做有很多好处,如果只有一个区域,那么很多不会被垃圾回收的对象也会被影响,比如标记整理或者标记复制算法都会影响到对象的存放位置,这样需要消耗更多的时间来进行垃圾回收。而且这样也会导致垃圾挥手的次数也会更加频繁。
算法
13、LC:另一棵树的子树
求两个数的前序遍历,然后看子树的前序遍历是否是大树的子串。
/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), * right(right) {} * }; */ class Solution { public: void dfs(TreeNode* root,vector<int>&v) { if (root == NULL) { v.push_back(-1000000000); return; } v.push_back(root->val); dfs(root->left,v); dfs(root->right,v); } bool isSubtree(TreeNode* root, TreeNode* subRoot) { vector<int> l; vector<int> r; dfs(root,l); dfs(subRoot,r); for (int i = l.size() - r.size(); i >= 0; i--) { int ok = 1; for (int j = 0; j < r.size(); j++) { if (l[i + j] != r[j]) { ok = 0; } } if (ok == 1) { return true; } } return false; } };