奇安信服务端一二面面经(来源牛客)

一.一面

应用层——HTTP:

​ 当输入URL后,对URL进行解析。
​ URL解析方式如下:
https://www.baidu.com/
​ https:代表访问数组的协议(http:、https:、ftp:),//后面的字符串表示服务器的名称,www.baidu.com是一个服务器,域名对应一个IP,上面的URL访问Web服务器的根目录,如果是https://www.baidu.com/example.html这样的,那就是访问根目录下的example.html文件。如果不写具体文件,那就是访问设定好的默认文件,一般是index.html
​ 对URL解析完毕后,就生成HTTP请求信息:
奇安信服务端一二面面经(来源牛客)
​ 如果是GET方法的报文,那就没有消息体。第一行为请求行,GET代表方法;/代表URL为根目录,HTTP/1.1代表版本
​ 消息头中的Accept,意如其名,代表客户端能处理的MIME类型,/就代表全部的类型下的全部子类型
​ 如果是POST方法的请求报文,前两行不变,加入一个消息体,以键值对形式。可能是JSON格式,也可能是XML格式,还可以是urlencoded,都可以。

应用层——DNS(基于UDP):

​ 虽然请求报文构建完毕,但现在不知道对方的IP地址,无法构建运输层报文,不过这有点果推因了,换种说法,应该是目前不知道要送到哪里,所以还需要DNS解析将域名转换为IP
​ DNS分为——根DNS、顶级域DNS、权威域DNS,再加一个本地DNS缓存
奇安信服务端一二面面经(来源牛客)
​ 图源为——小林coding 《图解计算机网络》 十分感谢
​ 1.客户端首先发出一个DNS请求,询问本地的DNS服务器,自己要查找的域名对应的IP地址是什么
​ 2.本地缓存中如果能找到要请求的域名,那么直接查表返回即可,如果找不到,那么本地DNS服务器就会去询问根DNS服务器
​ 3.根DNS服务器一般不直接用于域名解析,而是将请求告诉顶级域(TLD)DNS服务器,根DNS服务器根据对应的顶级域,将顶级域DNS服务器的地址回复给本地DNS服务器
​ 4.本地DNS服务器向顶级域DNS服务器进行请求,顶级域DNS服务器根据域名返回权威DNS服务器的地址,告诉给本地DNS服务器
​ 5.本地DNS服务器通过权威DNS服务器获取到域名对应的IP地址后,将IP地址存到本地的缓存中,再告诉本机客户端
​ 6.获知IP地址后,我们就可以把HTTP请求发送给服务器了
​ 这个再提一嘴,正如上面所写,一切的源头都是本地DNS服务器,那么源头是从何而来呢?当我们的机器连接到某个ISP时,ISP除了会给我们本机提供一个IP地址,还会提供一个或者多个本地DNS服务器地址,就是这样了(《计算机网络自顶向下方法》P87)

协议栈:

奇安信服务端一二面面经(来源牛客)
​ 整个协议栈如上图所示,同样图源小林coding
​ 应用程序调用socket库,来委托协议栈进行工作,TCP和UDP根据实际情况选择其一进行数据的收发,而IP则是负责网络包的收发工作
​ IP中还有ICMP和ARP协议,这个计网实验都做过。ARP协议就是根据IP地址查询相应的以太网MAC地址。而ICMP协议用于告知网络包传送过程中产生的错误以及控制信息
​ 网卡的驱动程序就负责控制实际的物理网卡,物理网卡将数字信号转换为电信号,在链路进行传输

传输层——TCP

​ 我们都知道,HTTP是基于TCP协议的,所以运输层我们用TCP协议
​ TCP老生常谈的问题就是——三次握手,四次挥手
奇安信服务端一二面面经(来源牛客)
​ 首先必须要有源端口号和目的端口号,不然套接字也不知道要多路分解和多路复用给谁。
​ 包的序号是为了解决包乱序的问题。而确认序列,则是为了不丢包,确认对方是否成功收到了我发送的包
​ 下面的状态位(SYN、FIN这些,一共六种),这个在三次握手的图中经常看到,实际上因为TCP要双方维护链接,所以就需要发送一些包使状态进行改变。SYN是发起一个连接,ACK是回复,RST是重新连接,FIN是结束连接
​ 再说一下URG和PSH这两个状态位,因为TCP是要保证数据包完整并且按序的。所以一般都是按顺序传送,但这样不太灵活,URG即表示当前报文为紧急报文,需要优先发送,并用一个指针定位下一条报文的位置,等紧急报文发送完毕,再用指针跳回去就行了
​ 而PSH就是普通的推送,当PSH=1时,不等缓冲区填满就会把消息推送出去
​ 窗口大小就是要做流量控制和拥塞控制,滑动窗口是一种限流算法,其他的还有如漏斗算法,令牌桶算法等
奇安信服务端一二面面经(来源牛客)
​ 1.第一次客户端和服务端都处于CLOSED状态,服务端会主动监听一个端口,处于listen状态
​ 2.客户端主动发起SYN,之后处于SYN-SENT状态
​ 3.服务端收到发起连接后,返回一个SYN,并且ACK(回复)客户端的连接,之后处于SYN-RCVD状态
​ 4.客户端收到服务端发送的SYN和ACK之后,发送ACK(服务端的ACK)的ACK,之后处于ESTABLISHED状态
​ 5.服务端收到ACK(服务端)的ACK后,也处于ESTABLISHED状态,至此结束
​ 对照上图来看,其实就是第一条箭头对应2,第二条箭头对应3和4,而第三条箭头对应5
​ 三次握手的目的其实是: 保证双方都有发送和接收的能力
​ 在解释TCP如何拆解HTTP的数据前,需要先了解 MTUMSS的概念:
​ MTU:一个网络包的最大长度,以太网中一般是1500字节
​ MSS:除去IP和TCP头部,一个网络包所能容纳的TCP数据最大长度
​ 当HTTP的请求消息比较长,超过了MSS的长度,那么就需要把HTTP的数据拆成一块块的数据
​ 数据会被以MSS的长度为单位进行拆分,拆分出来的每一块都放在单独的网络包,在每个被拆分出来的数据加上TCP头信息,交给IP模块来发送数据
奇安信服务端一二面面经(来源牛客)
奇安信服务端一二面面经(来源牛客)
​ 运输层的报文组装完毕后,就进入下面的网络层

网络层——IP

​ IP模块将TCP报文封装成网络包发送出去。
奇安信服务端一二面面经(来源牛客)
​ 这个是IP的报文格式。 图片不全,最下面肯定还会有TCP报文所代表的数据。
​ 其中的源IP地址和目标IP地址,那毫无疑问就是客户端的IP地址和DNS解析域名得到的IP地址
​ IP包头的协议号,因为HTTP由TCP传输,所以协议号写06(十六进制),表示协议为TCP
​ 如果机器有多个网卡,我们就需要根据路由表规则来判断哪一个网卡作为源地址IP
奇安信服务端一二面面经(来源牛客)
​ 在Linux的发行版中输入 route -n就可以查看当前系统的路由表。英文都不解释了,四级水平
​ e.g.:加入Web服务器的目标地址是192.168.200.100
​ 1.首先与除0.0.0.0之外的目的地址进行匹配,与每条对应的子网掩码进行与运算
​ 2.首先与 172.20.0.0.的子网掩码 255.255.0.0进行与运算,得到 192.168.0.0,与目的地址不匹配,所以失败
​ 3.就这么依次执行,直到最后一条 192.168.200.0匹配成功,那么就将使用docker0网卡的ip地址作为IP包头的源地址
​ 最后说一下0.0.0.0这个目标地址,因为他的子网掩码也是0.0.0.0,所以进行与运算之后,得到的结果肯定是0.0.0.0,直接就与目标地址完成了匹配。那意思就很清晰了,如果其他地址都匹配失败,那么最后和他匹配的时候绝对会成功。而后续把包发给路由器的时候,Gateway就是路由器的地址。
​ 现在就解决了源IP地址(路由表规则)、目的IP地址(DNS解析)、协议(TCP)的问题,终于加上了IP头部

数据链路层——MAC:

​ 因为我们是以太网,所以还要靠MAC进行两点传输,需要在网络包的头部前面加上MAC头部
奇安信服务端一二面面经(来源牛客)
​ 在 TCP/IP通信中,MAC包头的协议类型只使用: 0800IP协议或者 0806ARP协议
​ 发送方的MAC地址可以查询网卡的ROM,读取出来即可。而接收方的MAC地址就需要ARP协议,这个是计网实验做到的
​ 首先需要考虑清楚,对于接收者,我们现在所掌握的信息就是—— 通过DNS解析域名所得到的IP地址
​ ARP协议就是在以太网中以 广播的形式,通过IP地址来查询MAC地址。如果同一个子网中存在某个设备拥有这个MAC地址,那么就会主动把MAC地址发送过去。这样就可以完成接收方MAC地址的填写了。并且同样的,设备中会保存一张叫ARP表的东西作为缓存,为了效率的提升。

物理层——网卡

​ 通过网卡驱动程序,我们可以控制网卡将网络包 从数字信号转换为电信号,才能在网线上进行传输
​ 网卡驱动将网络包复制到网卡的缓存区内,在开头加上报头和起始帧分界符。在末尾加入FCS

交换机:

​ 交换机工作在MAC层,也叫二层网络设备。设计的宗旨是将网络包原封不动的发送到目的地。
​ 交换机接收到网络包的电信号后,将电信号转换为数字信号,然后通过FCS校验,没问题就放到缓冲区,有问题那就解决问题
​ 由于交换机的端口没有MAC地址,所以他不会校验接收到的包对应的接收者MAC地址是否为自己的,所以只要是个网络包他就会接受
​ 交换机自身有一张MAC地址表,这张表中记录着经过交换机的报文所涉及到的MAC地址以及IP地址的映射关系。为了提高效率,我们还是先查询交换机本身的MAC地址表中是否存在当前包的接收方MAC地址
奇安信服务端一二面面经(来源牛客)
​ 如果表中有所要的MAC地址,那么就知道MAC地址对应的设备连接在交换机的几号端口,就可以通过交换电路发送了
​ 如果没有MAC地址,那就说明对应的设备发送或接受的包还没有经过此交换机,或者是MAC地址表的算法将他删除了。
​ 那这时交换机就会把网络报发送到全部端口(当然除了源端口),冗余的这些包不会影响网络的速度,微乎其微。
​ 如果接收方的MAC地址是一个广播地址,那么交换机会把包发送到除源端口外的所有端口。
​ 广播地址例如:MAC地址中的 FF:FF:FF:FF:FF:FF,IP地址中的 255.255.255.255

路由器:

​ 路由器是基于IP设计的,所以是三层网络设备,每个端口都有IP地址和MAC地址
​ 因为路由器的端口具有MAC地址,所以可以成为以太网的发送方和接收方。查询转发的过程和交换机大致相同
​ 不同的是,如果报文中的MAC地址不是发给自己的,那么路由器就会丢弃这个包
​ 完成包的接受操作后,首先去掉最外层的MAC头部,根据IP头部开始进行转发操作
奇安信服务端一二面面经(来源牛客)
​ 假设当前地址为 10.10.1.101的客户端要向 192.168.1.100的服务器发送一个包,目前包到达了路由器
​ 还是同样的操作,与每个IP的子网掩码进行与运算,寻找匹配的条目
​ 上图中最后与第二条匹配,那么第二条就是转发目标了
​ 同样的,如果没有找到匹配的,那最后就和0.0.0.0匹配,GateWay就是要转发到的目的地址了,还未抵达终点,需要路由器继续转发
​ 如果GateWay是空的,那么就说明IP头部的IP地址就是要转发到的目的地址,抵达了终点
​ 但是因为我们在以太网中,所以两点传输还是要靠MAC,而现在我们就要查询路由器的ARP表,找到对应转发地址的MAC地址,为其头部添加上MAC头部
​ 网络包完成之后,接收方的MAC地址就是下一个路由器的地址,就这样一直转发下去,源IP和目的IP始终不变,变的只是MAC地址,最后我们就到达了服务器。

服务端解包:

奇安信服务端一二面面经(来源牛客)
之后的整个流程就是相反即可,不过要注意的是,最后需要客户端向服务器发送四次挥手断开双方的连接。

​ 本题中还可以涉及一些Linux指令:

1.

我们可以输入 netstat -napt来查看本机的TCP连接状态
ForeignAddress代表目的地址和端口
2.

​ 输入 route -n可以查看本机的路由表
3.

​ 输入 arp -a可以查看arp缓存的内容

2.Http1.0和2.0的区别?

HTTP1.0:

  • 浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接

  • HTTP1.1使用长连接,有效减少三次握手的开销

​ 因为HTTP1.0时代,每次请求资源都会建立一个连接,这就导致页面里的每个静态资源每次都要建立连接,就造成了效率的低下,所以HTTP1.1就针对这个问题推出了长连接,使得b和s之间的连接可以长时间存在,静态资源都可以反复利用这条连接

  • 虽然允许复用TCP连接(长连接),但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着(注意和HTTP2.0中的多路复用区分开来)
  • HTTP1.1允许只发送header信息不携带body,此时如果服务器认为客户端拥有权限,就会向客户端发送100,客户端接收100后再向服务器发送 body信息
  • HTTP1.0没有host域HTTTP1.1才开始支持

​ 关于Host这个字段,我们都知道一个IP可以对应多个域名,比如我有一个IP:192.168.1.71,然后我云服务器的DNS解析有三个不同域名都指向了这个IP,那么我们访问任何一个域名最终解析到的IP地址都是它。但实际上,我们更加希望三个不同的域名访问后是三个不同的站点,host字段就是起这个作用的,拿tomcat配置文件中的host标签来举例子:


​ 这其实就是tomcat配置多项目的一种方式,这种方式不新在某个端口上运行一个新的服务端,在已经有的一个服务端上直接开多个站点,一目了然了,三个host分别对应三个不同站点,这个就是host字段的作用

  • 新增了一些请求头和响应头
  • 新增了一些请求方法(DELETE、PUT、OPTIONS)

HTTP2.0:

​ HTTP2.0主要是性能的提升,添加了如下的特性:

  • 采用二进制格式而非文本格式 帧是 HTTP2通信中最小单位信息 HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x的文本格式,解析起来更高效 将请求和响应数据分割为更小的帧,并且它们采用二进制编码 HTTP2中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流 每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装,这也是多路复用同时发送数据的实现条件
  • 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行 HTTP/2 复用 TCP连接,在一个连接里,客户端和服务端都可以 同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了”队头堵塞”,如下图第四步所示(对js和css的请求同时发送到了服务端)
  • 使用报头压缩,降低开销 HTTP/2在客户端和服务器端使用”首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送 首部表在 HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新 例如:下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销
  • 服务器推送 HTTP2引入服务器推送,允许服务端推送资源给客户端 服务器会顺便把一些客户端需要的资源一起推送到客户端,如在响应一个页面请求中,就可以随同页面的其它资源 免得客户端再次创建连接发送请求到服务器端获取 这种方式非常合适加载静态资源 个人感觉有点推断的意思,有点像程序的局部性原理一样。总之你如果要请求一个html资源,那么大概率页面上的CSS和JS你也要请求过去,服务器现在可以直接一口气全部发送给你,不用你再另外发起一次或多次请求了

​ 个人觉得这个题,我会分 关系型数据库非关系型数据库来回答

​ 关系型数据库:

MySQL
SQLlite
PostgreSQL

​ 非关系型数据库:

Redis
MongoDB

​ Redis的缓存淘汰策略:

​ Redis的缓存淘汰策略,我觉得要分淘汰数据的范围来回答,具体内容如下:

1.不进行数据淘汰

​ 不进行数据淘汰只有一种策略,叫 noeviction

2.进行数据淘汰

​ 在进行数据淘汰中,又可以分出两种淘汰策略

在设置了TTL(Time To Live)的数据中进行淘汰:

规则 规则内容 volatile-lru 当内存不足时,在设置了TTL的键中依照lru算法移除key volatile-random 当内存不足时,在设置了TTL的键中随机移除某个key volatile-ttl 当内存不足时,在设置了TTL的键中,移除过期时间最早的key volatile-lfu 当内存不足时,按照lfu算法移除key

在未设置TTL的数据中进行淘汰:

规则 规则内容 allkeys-lru 在所有键中按照lfu算法移除key allkeys-random 随机移除key allkeys-lru 按照lru算法移除key

​ 首先说一下lfu和lru的区别,然后再说lru的实现方式

  • LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。
  • LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

​ 无论其他人如何,反正我第一次接触相关概念的时候,是有些迷糊不清的。其实就抓住一个关键点即可:

​ LRU强调在 一段时间内数据使用次数最少的,而LFU则强调是 在全部时间内,使用数量最少的那一个

​ 相比于LFU,LRU的实现方式还是比较简单的,但是在结尾我也贴出来LFU的实现方式。

​ LRU实现方式:

1.我们可以用一个数组存放数据,并且给每一个数据项设定一个时间戳,当新数据插入时,将所有时间戳自增,并将新加入数据的时间戳置零,每次访问数组中的数据项时,将该数据项的时间戳置为0,当数组空间满了,就把时间戳最大的数据淘汰即可
2.我们可以用一个链表实现,每次插入数据时将新数据插入到链表的头部;当缓存命中时,我们就把数据移到头部,当链表满时,将尾部元素删去即可
3.我们可以使用一个双向链表和一个hashmap。当插入新的数据时,如果缓存命中,那么就把该数据移动到链表头部,如果不存在,那就新建一个节点扔到头部。若空间已满,就把链表最后一个节点删除。当访问数据时,如果数据存在,那就把节点移动到头部,不存在就返回-1即可

​ 第一种方法,我们需要维护时间戳,且插入、删除、访问的时间复杂度均为O(n),而第二种方法,涉及到需要定位链表元素的操作时,也会有O(n)的时间复杂度。所以我们用第三种办法实现LRU算法。

import java.util.HashMap;

//leetcode submit region begin(Prohibit modification and deletion)
class LRUCache {

    private HashMap map;

    private DoubleList cache;

    private int capacity;

    public LRUCache(int capacity) {
        this.capacity=capacity;
        map=new HashMap<>();
        cache=new DoubleList();
    }

    public int get(int key) {
        if (!map.containsKey(key)){
            return -1;
        }
        makeRecently(key);
        return map.get(key).val;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)){
            deleteKey(key);
            addRecently(key,value);
            return;
        }
        if (capacity==cache.size()){
            removeLeastRecently();
        }
        addRecently(key,value);
    }

    private void makeRecently(int key){
        Node node = map.get(key);
        cache.remove(node);
        cache.addLast(node);
    }

    private void addRecently(int key,int val){
        Node x=new Node(key,val);
        cache.addLast(x);
        map.put(key,x);
    }

    private void deleteKey(int key){
        Node node = map.get(key);
        if (node==null){
            return;
        }
        cache.remove(node);
        map.remove(key);
    }

    private void removeLeastRecently(){
        Node node = cache.removeFirst();
        int key = node.key;
        map.remove(key);
    }
}

class Node{
    public int key,val;
    public Node next,prev;
    public Node(int k,int v){
        this.key=k;
        this.val=v;
    }
}

class DoubleList{
    private Node head,tail;
    private int size;

    public DoubleList(){
        head=new Node(0,0);
        tail=new Node(0,0);
        head.next=tail;
        tail.prev=head;
        size=0;
    }

    public void addLast(Node x){
        x.prev=tail.prev;
        x.next=tail;
        tail.prev.next=x;
        tail.prev=x;
        size++;
    }

    public void remove(Node x){
        x.prev.next=x.next;
        x.next.prev=x.prev;
        size--;
    }

    public Node removeFirst(){
        if (head.next==tail){
            return null;
        }
        Node first=head.next;
        remove(first);
        return first;
    }

    public int size(){
        return size;
    }

}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */
//leetcode submit region end(Prohibit modification and deletion)

​ 这里解释一下为什么用哈希表。题目中要求我们get和put操作都要在O(1)的时间复杂度完成,链表的插入删除可以做到O(1),但查找做不到,所以我们需要一种数据结构完成在O(1)时间内查找任意元素,那就是哈希表了

​ 其次再解释一下为什么需要双向链表而不是单向链表以及为何哈希表中存放了key而链表中还要存放key,不能节省空间吗?首先是第一点,因为我们需要删除一个节点时,代码也涉及到了对该节点前一个节点的操作,所以我们需要双向链表

​ 第二点就是为什么不能节省空间,因为在 removeLeastRecently方法中,我们还需要通过获取要删除的节点中的key属性,来移除哈希表中的key,所以必须存放key在链表中

​ 至于使用各语言自带的哈希表结合链表的数据结构,这些方法就不写了。没有太大含金量,例如Java中的 LinkedHashMap

​ LRU是中等类型的题目,还是比较好解决了。LFU的话,难度就高很多了(至少我个人觉得高比较多)

​ 说实话,这位作者的这篇面经中,所有的题目我都有思路。这道题目的话,可能太开放了,一时不知道从何答起,而且目前的水平对于这种(架构方面的题?)还是理解太少,我贴一篇我收获颇丰的文章吧。

​ 这里也贴一下原作者的回答情况: &#x6211;&#x7B54;&#x4E86;&#x53EF;&#x7528;&#x6027;&#x3001;&#x70ED;&#x70B9;&#x6570;&#x636E;&#x5904;&#x7406;&#xFF08;&#x7F51;&#x7EA2;&#x7C89;&#x4E1D;&#x5F88;&#x591A;&#xFF0C;&#x4E00;&#x822C;&#x4EBA;&#x5FAE;&#x535A;&#x6CA1;&#x5565;&#x4EBA;&#x770B;&#xFF09;&#x3001;&#x65F6;&#x95F4;&#x6709;&#x5E8F;&#x6027;&#x7B49;&#x7B49;&#xFF0C;&#x9762;&#x8BD5;&#x5B98;&#x63D0;&#x793A;&#x4E86;&#x8BFB;&#x6269;&#x6563;&#x3001;&#x5199;&#x6269;&#x6563;&#x76F8;&#x5173;&#x5185;&#x5BB9;&#xFF0C;&#x5F00;&#x653E;&#x6027;&#x5F88;&#x5F3A;&#xFF0C;&#x5EFA;&#x8BAE;&#x88AB;&#x95EE;&#x5230;&#x7C7B;&#x4F3C;&#x95EE;&#x9898;&#x627E;&#x81EA;&#x5DF1;&#x64C5;&#x957F;&#x7684;&#x8BF4;&#xFF09;

二.二面

​ 使用场景:Web服务器(Tomcat、Apache)
​ 微服务(Spring Cloud)
​ Web后端
​ 数据处理
​ 最不满意的一点:
​ 运行速度的话,搞Web,我感觉不用纠结。每次各种语言来回比对,一个是远离现实场景,一个是太过于理论层面,至少这个我不会说
​ 我可能会说不能和底层打交道,因为程序员大概都有一个写操作系统或者什么底层玩意的梦想,但是因为Java是跨平台的,要搞一个虚拟机,所以很难和底层打交道了

  • HTTP使用明文传输,安全性较差;而HTTPS(SSL+HTTP)使用加密传输,安全性较好
  • HTTPS协议需要向CA申请证书,一般来说需要花费一定费用
  • HTTP因为不需要加密的资源消耗,所以响应速度快于HTTPS;HTTP使用TCP三次握手建立连接,C/S之间需要交换三个包,而HTTPS除了TCP的三个包,还需要加上SSL握手的九个包,所以一共会有十二个包
  • HTTP和HTTPS的默认端口也不同,HTTP为80,HTTPS为443
  • HTTPS更加消耗服务器资源

  • TCP是面向连接的,而UDP则不是

  • TCP主打可靠传输,而UDP则不保证可靠传输
  • TCP面向字节流,而UDP面向数据报文
  • TCP只支持点对点通信,而UDP支持点对点,一对多,多对多
  • TCP报文首部有20字节,而UDP有8字节
  • TCP有拥塞控制,而UDP则没有
  • TCP协议下双方具有发送缓冲区和接收缓冲区,而UDP则只有接收缓冲区

​ Filddler、wireshark

​ 不能说熟悉,只能说用过,因为不知道多熟悉算熟悉。命令用过的很多了,我觉得没准能一直说下去,如果我都想得起来……

  • ps:process status,用于显示当前进程状态。一般以 ps -aux |grep xxx或者 ps -ef |grep xxx使用 ps -u root表示显示以root用户身份运行的进程
  • grep:用于查找文件中符合内容的字符串 grep test *file查找当前目录下后缀有file的文件内容中包含test的文件,并打印出包含test的内容; grep -r update /etc/acpi在/etc/acpi目录以及其所有子目录中寻找文件内容中含有update的内容并打印出来; grep -n '2022-10-24 00:01:11' *.log,因日志文件太大,不适合使用cat查看,所以用grep查找并且打印出行数
  • ifconfig:显示当前的网络设备状态

​ 在Linux内核下,输入 cat /proc/sys/net/ipv4/ip_local_port_range,可以查看本地随机端口的范围。一般来说都会是65535.所以有人就说一台主机最多能创建65535个TCP连接,那实际上肯定不是

​ 实际上在计网里我们都学过,TCP由一个五元组唯一确定,即 (src-ip src-port 传输层协议 dest-ip dest-port),而65535对应的就是里面的两个port,那结果很清晰了,TCP连接的最大数就是这五个变量所能组合出的数量

​ 端口号是一个16bit整数,能表示的数的范围也就是0-2^16-1,即0-65535。也就是64k个端口。首先客户端可以有64k个不同的端口像服务器发送连接,其次服务端的任何一个端口都可以一对多,最多情况下可以一个端口连接65535台主机,只要内存够大。所以极限情况下,TCP连接数可能为64k*64k=4G

​ 但上述的假设是建立在客户端ip和服务端ip都确定的情况下。而IPv4用32位来描述ip地址,所以一个服务端的端口可以有大约2^32*64k个不同的TCP连接。所以一般我们都说,可以有无数个TCP连接

​ 用户态是指操作系统当前无法调用特权指令,内核态即是除了普通的指令外,还可以调用特权指令

​ 为了保证操作系统的安全性,我们需要对其进行权限保护。而保护的方式就是区分用户态和内核态。如果每个程序都可以随意修改存放操作系统程序的内存空间,那么操作系统想必无时无刻都会崩溃

​ 协程这个概念很多年前就有了。包括现在很多语言也是有协程这个玩意,比如说JS啊,python啊,Java社区里在搞的loom啊,C++20啊等等等,但是笔者是面的go,那就肯定是go里的协程了

​ 进程和线程就不多说了,这个老生常谈的东西。主要说说协程,之前很多篇博客都提到了这个,但是一直没有很深入写过

​ 协程是比线程还更加轻量级的存在。实际上这种由程序员自己管理的轻量级线程叫做 &#x7528;&#x6237;&#x7A7A;&#x95F4;&#x7EBF;&#x7A0B;,对内核并不可见(由软件管理,也就减少了上下文切换带来的开销)。一个进程可以拥有多个线程,一个线程可以拥有多个协程。

​ 我们都知道,在客户端向服务端每一次申请资源时,就会在服务器进程里开一个线程。所以系统的吞吐能力取决于每个线程的操作耗时,如果遇到非常耗时的I/O操作,那就会一直阻塞,极大程度降低了效率。并且如果你频繁的切换线程或者进程,那上下文切换带来的开销也是巨大的。Node.js和Vert.x用的是单线程加异步回调(Java实现异步回调 – jrliu – 博客园 (cnblogs.com)](https://www.cnblogs.com/liujiarui/p/13395424.html))来解决这个问题。协程的意义就在于,当出现如长时间的I/O操作或者长时间等待网络或数据库连接时,让出当前的协程调度,执行下一个任务

​ 线程的默认Stack大小为1M,而协程则仅为1K。

​ 线程为同步机制,而协程为异步机制

​ MySQL是关系型数据库,主要持久性的存放数据,读写速度相对较慢;而Redis是非关系型数据库,效率很高,但数据存储时间有限

​ MySQL涉及I/O操作,而Redis因为将数据存放在内存中,所以不涉及I/O操作

​ 当数据需要持久化时,我们使用MySQL,当数据不需要持久化且会被频繁请求访问时,我们使用Redis

  • 首先Redis的操作都是基于内存的,那肯定是要快一些 如果让我回答的话,我只会说,Redis快是因为架构真的快,换句话说,就是因为它使用了基于内存的key-value,以下几点是存在争议的,也就是说有些人觉得只是因为面试造火箭才有这么些个答案
  • 采用单线程,避免了不必要的上下文切换操作,也不必考虑锁,进而也就不用担心出现了死锁
  • 使用了多路I/O复用,非阻塞IO 实际上在Redis6.0之后,就引入了多线程。所以网上很多资料说,作者使用单线程是刻意的,就是为了快。那这个说法就有点站不住脚了,如果真的是为了快的话,那主打快的Redis也就没必要大费周章引入多线程了。实际上在Redis4.0后,就并不是单线程了,首先我们能用主线程进行所谓的操作,而背后其实也还有线程在进行着例如清理脏数据、释放无用链接、大key的删除等等 官方一直不引入多线程的原因是,他们认为Redis的瓶颈不在于CPU,而在于你机器的内存和网络。事实也确实如此,那么我用了单线程,还可以把可维护性提高。 那么Redis又为何在6.0引入多线程呢?因为之前的Redis,对于小数据包可以做到8W到10W的QPS,大部分公司肯定是够用。但保不齐总有些大公司,动不动就上亿的交易量,所以就需要更大的QPS才可以。 总的来说其实Redis引入多线程就两个原因:
  • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核
  • 多线程任务可以分摊 Redis 同步 IO 读写负荷

​ 这个是真的学到了,之前从来没了解过。

参考内容

Original: https://www.cnblogs.com/appletree24/p/16707282.html
Author: Appletree24
Title: 奇安信服务端一二面面经(来源牛客)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/685728/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球