首页 > 其他分享 >面经梳理-计算机网络

面经梳理-计算机网络

时间:2024-06-19 22:53:48浏览次数:12  
标签:主机 报文 面经 TCP 连接 计算机网络 域名 服务器 梳理

前言

整理计算机网络的相关面试题,计算机网络在我看来挺复杂的,想要完全精通应该是不可能的,毕竟后端开发的知识点那么多,不过掌握面试的常考知识点是由必要的。建议系统学习计算机网络课本再进行知识点的整理记忆。

题目

OSI七层协议有了解么?Ip协议是哪层协议?

计算机网络体系结构

目前的计算机网络分层结构主流分为三种:OSI协议体系结构、TCP/IP协议体系结构、原理体系结构。OSI协议体系结构是国际标准化组织(OSI)制定的协议体系,但实际较为复杂,缺少商业驱动;TCP/IP是推广最广的标准,是事实国际标准;原理体系结合了OSI和TCP/IP的优势,将网络分成了5层。

原理体系分层

  • 物理层:物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流;
  • 数据链路层:数据链路层考虑同一个局域网中的点对点传输(PPP协议)和局域网内的广播传输(以太网、wifi等);
  • 网络层:网络层考虑在整个互联的网络中两个主机之间的通信,一个主机的信息需要经过若干路由器传送到目标主机。网络层最核心的功能是分组转发和路由选择。核心协议为网际协议IP,配合协议为地址解析协议(AddressResolutionProtocol,ARP)、逆地址解析协议(ReverseAddressResolutionProtocol,RARP)、网际控制报文协议(InternetControlMessageProtocol,ICMP)、网际组管理协议(InternetGroupManagementProtocol,IGMP)。
  • 运输层:如何为运行在不同主机上的应用进程提供直接的通信服务是运输层的任务,运输层协议又称为端到端协议。它属于面向通信部分的最高层,同时也是用户功能中的最低层。根据应用需求的不同,因特网的运输层为应用层提供了两种不同的运输协议,即面向连接的TCP和无连接的UDP。
  • 应用层:定义运行在不同端系统上的应用进程间为实现特定应用而互相通信的规则。

IP协议是哪层协议

网络层

参考:
《计算机网络教程》 谢钧,谢希仁 人民邮电出版社

TCP和UDP的区别

因特网的运输层为应用层提供了两种不同的运输协议,即面向连接的TCP和无连接的UDP。

UDP

UDP在传送数据之前不需要先建立连接;

远地主机的运输层在收到UDP报文后,不需要给出任何确认,不提供可靠交付;

UDP支持一对一、一对多、多对一和多对多的交互通信。

UDP是面向报文的。这就是说,UDP对应用程序交下来的报文不再划分为若干个分组来发送,也不把收到的若干个报文合并后再交付给应用程序。应用程序交给UDP一个报文,UDP就发送这个报文;而UDP收到一个报文,就把它交付给应用程序。因此,应用程序必须选择合适大小的报文。若报文太长,UDP把它交给IP层后,IP层在传送时可能要进行分片,这会降低IP层的效率。反之,若报文太短,UDP把它交给IP层后,会使IP数据报的首部相对太大,这也降低了IP层的效率;

UDP没有拥塞控制,因此网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。很多的实时应用(如IP电话、实时视频会议等)要求源主机以恒定的速率发送数据,并且允许在网络发生拥塞时丢失一些数据,但却不允许数据有太大的时延。UDP正好适合这种要求;

TCP

TCP则提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接;

提供可靠交付(通过TCP连接传送的数据无差错、不丢失、不重复,并且按序到达)的服务,因此需要提供如确认、流量控制、拥塞控制、计时器及连接管理等功能;

TCP不提供广播或多播服务。每一条TCP连接只能有两端点,即每一条TCP连接只能是点对点的(一对一)。TCP连接唯一地被通信两端的端点所确定,而两个端点分别由二元组(IP地址、端口号)唯一标识,即一条TCP连接由两个套接字(socket)地址标识。与UDP的端口队列不同的是,TCP的发送缓存和接收缓存都是分配给一个连接的,而不是一个端口。

面向字节流。TCP中的“流”(stream)指的是流入到进程或从进程流出的字节序列;

TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。在发送时,应用程序在把数据传送给TCP的缓存后,就可以做自己的事,而TCP在合适的时候把数据发送出去。在接收时,TCP把收到的数据放入缓存,上层的应用进程在合适的时候读取缓存中的数据。

参考:
《计算机网络教程》 谢钧,谢希仁 人民邮电出版社

TCP三次握手、四次挥手

三次握手

设主机B中运行TCP的服务器进程,它先发出一个被动打开(passiveopen)命令,准备接受客户进程的连接请求。然后服务器进程就处于“听”(listen)的状态,不断检测是否有客户进程要发起连接请求。如有,即做出响应。设客户进程运行在主机A中。它先向其TCP发出主动打开(activeopen)命令,表明要向某个IP地址的某个端口建立运输层连接。主机A的TCP向主机B的TCP发出连接请求报文段,其首部中的同步位SYN应置1,同时选择一个序号seq=x,这表明下一个报文段的第一个数据字节的序号是x+1。主机B的TCP收到连接请求报文段后,如同意,则发回连接请求确认。在确认报文段中应把SYN位和ACK位都置1,确认号是ack=x+1,同时也为自己选择一个序号seq=y。主机A的TCP收到B接受连接请求的确认后,还要向B给出确认,其ACK置1,确认号ack=y+1,而自己的序号seq=x+1。TCP的标准规定,SYN=1的报文段(例如,A发送的第一个报文段)不能携带数据,但要消耗掉一个序号。因此A发送的第二个报文段的序号应当是第一个报文段的序号加1(虽然在第一个报文段中并没有数据)。注意,A发送的第二个报文段中SYN是0而不是1,ACK位必须为1。该报文段是对B的同步报文段的确认,但是一个普通报文段,可携带数据。若该报文段不携带数据,则按照TCP的规定,确认报文段不消耗序号。运行客户进程的主机A的TCP通知上层应用进程,连接已经建立。当运行服务器进程的主机B的TCP收到主机A的确认后,会通知其上层应用进程,连接已经建立。

为何要三次握手

这主要是为了防止已失效的连接请求报文段突然又传送到了主机B,因而产生错误。

所谓“已失效的连接请求报文段”是这样产生的。考虑这样一种情况。主机A发出连接请求,但因连接请求报文丢失而未收到确认。主机A于是再重传一次。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。主机A共发送了两个连接请求报文段,其中的第二个到达了主机B。现

假定出现另一种情况,即主机A发出的第一个连接请求报文段并没有丢失,而是在某些网络结点滞留的时间太长,以致延误到在这次的连接释放以后才传送到主机B。本来这是一个已经失效的报文段。但主机B收到此失效的连接请求报文段后,就误认为是主机A又发出一次新的连接请求。于是就向主机A发出确认报文段,同意建立连接。主机A由于并没有要求建立连接,因此不会理睬主机B的确认,也不会向主机B发送数据。但主机B却以为运输连接就这样建立了,并一直等待主机A发来数据。主机B的许多资源就这样白白浪费了。

采用三次握手的办法可以防止上述现象的发生。例如,在刚才的情况下,主机A不会向主机B的确认发出确认。主机B收不到确认,连接就建立不起来。

四次挥手

主机A的应用进程先向其TCP发出连接释放请求,并且不再发送数据。TCP通知对方要释放从A到B这个方向的连接,把发往主机B的报文段首部的FIN置1,其序号seq=u。由于FIN报文段要消耗一个序号,因此序号u等于A前面已传送过的数据的最后一个字节的序号加1。主机B的TCP收到释放连接通知后即发出确认,确认号是ack=u+1,而这个报文段自己的序号假定为v(v等于B前面已传送过的数据的最后一个字节的序号加1)。主机B的TCP这时应通知高层应用进程。这样,从A到B的连接就释放了,连接处于半关闭(half-close)状态,相当于主机A向主机B说:“我已经没有数据要发送了。但你如果还发送数据,我仍可以接收。”此后,主机B不再接收主机A发来的数据。但若主机B还有一些数据要发往主机A,则可以继续发送(这种情况很少)。主机A只要正确收到数据,仍应向主机B发送确认。若主机B不再向主机A发送数据,其应用进程就通知TCP释放连接。主机B发出的连接释放报文段必须使FIN=1,并使其序号仍为v(因为前面发送的确认报文段不消耗序号),还必须重复上次已发送过的确认号ack=u+1。主机A必须对此发出确认,把ACK置1,确认号ack=v+1,而自己的序号是seq=u+1(因为根据TCP标准,前面发送过的FIN报文段要消耗一个序号)。这样才把从B到A的反方向连接释放掉。但此时,主机A的TCP并不能马上释放整个连接,还要再等待一个超时时间才能将整个连接释放。因为主机A的确认有可能丢失,这时B会重传FIN报文段。在这段超时时间内,若A又收到B重传的FIN报文段,A需要再次进行确认。收到A的最后确认,B才能最终将整个连接释放。若等待的这段超时时间内没有收到B的FIN报文段,主机A的TCP则向其应用进程报告,整个连接已经全部释放。

为何要四次挥手

因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

参考:
《计算机网络教程》 谢钧,谢希仁 人民邮电出版社
TCP连接状态详解及TIME_WAIT过多的解决方法

为什么会有time-wait,close-wait?出现大量的time-wait是什么原因?要怎么解决呢?

为什么有close-wait

四次挥手主动关闭方已经关闭了主动发送的单向链接,但是被动关闭方可能还需要继续发送一些数据,等这些数据发送完之后才能关闭从被动关闭方向主动关闭方的单向链接,因此从主动关闭方关闭完成到被动关闭方开始关闭之前的这段时间,被动关闭方的状态都是close-wait。一般都是代码异常,问题应该比time-out更严重。

为什么会有time-wait

TIME_WAIT的出现,对应的是你的程序里的异常处理,它的出现,就是为了解决网络的丢包和网络不稳定所带来的其他问题:

  1. 防止前一个连接【五元组,这里继续以 110.122.144.166:45678, tcp, 110.88.92.104:80 为例】上延迟的数据包或者丢失重传的数据包,被后面复用的连接【前一个连接关闭后,此时你再次访问百度,新的连接可能还是由110.122.144.166:45678, tcp, 110.88.92.104:80 这个五元组来表示,也就是源端口凑巧还是45678】错误的接收(异常:数据丢了,或者传输太慢了),参见下图:

  • SEQ=3的数据包丢失,重传第一次,没有得到ACK确认
  • 如果没有TIME_WAIT,或者TIME_WAIT时间非常端,那么关闭的连接【110.122.144.166:45678, tcp, 110.88.92.104:80 的状态变为了CLOSED,源端口可被再次利用】,马上被重用【对110.88.92.104:80新建的连接,复用了之前的随机端口45678】,并连续发送SEQ=1,2 的数据包;
  • 此时,前面的连接上的SEQ=3的数据包再次重传,同时,seq的序号刚好也是3(这个很重要,不然,SEQ的序号对不上,就会RST掉),此时,前面一个连接上的数据被后面的一个连接错误的接收;
  1. 确保连接方能在时间范围内,关闭自己的连接。其实,也是因为丢包造成的,参见下图:

  • 主动关闭方关闭了连接,发送了FIN;
  • 被动关闭方回复ACK同时也执行关闭动作,发送FIN包;此时,被动关闭的一方进入LAST_ACK状态;
  • 主动关闭的一方回去了ACK,主动关闭一方进入TIME_WAIT状态;
  • 但是最后的ACK丢失,被动关闭的一方还继续停留在LAST_ACK状态;
  • 此时,如果没有TIME_WAIT的存在,或者说,停留在TIME_WAIT上的时间很短,则主动关闭的一方很快就进入了CLOSED状态,也即是说,如果此时新建一个连接,源随机端口如果被复用,在connect发送SYN包后,由于被动方仍认为这条连接【五元组】还在等待ACK,但是却收到了SYN,则被动方会回复RST;
  • 造成主动创建连接的一方,由于收到了RST,则连接无法成功;

所以,这里看到了,TIME_WAIT的存在是很重要的,如果强制忽略TIME_WAIT,还是有很高的机率,造成数据错乱,或者短暂性的连接失败。

出现大量的time-wait是什么原因?影响?要怎么解决呢?

  • 原因

出现大量的time-wait的原因是大量主动关闭了TCP连接,使当前机器出现大量time-wait状态。

  • TIME_WAIT会影响什么

端口:但是这是对于通信过程中扮演客户端角色的一端来说,因为客户端使用随机端口来访问服务器,当它主动断开的时候会出现这个状态,比如第一次系统给它分配了一个51000的随机端口访问服务器,然后客户端主动断开了,在2MSL期间,该端口就处于TIME_WAIT状态,如果它再次访问相同的服务器,那么系统会为它再次分配一个随机端口,如果51000端口还处于TIME_WAIT状态,那么这个随机端口就肯定不是51000,如果51000端口不处于TIME_WAIT状态,那么这个随机端口就有可能是51000。所以这个状态在一定期间内对于客户端角色来讲会影响并发量,大量这个TIME_WAIT就导致可用随机端口不断减少。

内存:这个量会很小,无需担心,哪怕是上万的TIME_WAIT。

文件描述符:但是处于TIME_WAIT状态的套接字其实是已经关闭了文件描述符,也就是说这个状态并不占用文件描述符这也就是意味着该状态不会对应一个打开的文件。

CPU:会消耗一定的CPU资源

  • 解决方案主要包括以下三个方案
  1. 几百上千的time-wait对于服务器而言影响过小,基本可以忽略;
  2. 客户端的随机端口使用完,可以通过调整/etc/sysctl.conf下的net.ipv4.ip_local_port_range配置,至少修改成 net.ipv4.ip_local_port_range=1024 65535,保证你的负载均衡服务器至少可以使用6万个随机端口,也即可以有6万的反向代理到后端的连接,可以支持每秒1000的并发(想一想,因为TIME_WAIT状态会持续1分钟后消失,所以一分钟最多有6万,每秒1000);如果这么多端口都使用完了,也证明你应该加服务器了
  3. 调整tcp配置。编辑内核文件/etc/sysctl.conf,加入以下内容:
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_fin_timeout = 30 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间。
# 下面三个配置最核心
net.ipv4.tcp_timestamps = 1 用于支持net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

然后执行 /sbin/sysctl -p 让参数生效。
简单来说,就是打开系统的TIMEWAIT重用和快速回收。
其中tcp_tw_reuse主要用于客户端,即客户端主动发起请求和主动断开请求的情况,可以复用TIME_WAIT状态的连接;tcp_tw_recycle主要用于服务端,即客户端连入,服务端主动关闭的情况,可以快速退出TIME_WAIT状态(在一个RTO时间内),但是如果客户端处于NAT的网络(多个客户端,同一个IP出口的网络环境),如果配置了tcp_tw_recycle,就可能在一个RTO的时间内,只能有一个客户端和自己连接成功(不同的客户端发包的时间不一致,服务器进入TIME_WAIT状态后,会把后到的数据包丢弃掉),所以对于客户端在NAT(NAT含义参见什么是NAT?)网络的场景,服务端不适用tcp_tw_recycle;
有时一个服务器既可以作为上游的服务端,也作为下游的客户端,可能会同时配置tcp_tw_recycle和tcp_tw_reuse,需要根据实际情况(客户端、服务端、谁先断开、客户端所处网络)进行配置。

参考:
TCP连接的TIME_WAIT和CLOSE_WAIT 状态解说
正确理解tcp_fin_timeout到底起什么作用?
解读TIME_WAIT--你在网上看到的大多数帖子可能都是错误的
什么是NAT?

Tcp的报文头有了解过?Tcp是怎么保证消息的可靠传输的?网络的拥塞控制和流量控制分别是指什么?

TCP报文头

  • 序号
    序号字段的值指的是本报文段所发送的数据的第一个字节的序号。

  • 确认号
    确认号占4字节,是期望收到对方的下一个报文段的第一个数据字节的序号。

  • 数据偏移
    数据偏移占4位,它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。实际上就是TCP报文段首部的长度。

  • URG与紧急指针
    紧急URG(URGent)当URG=1时,表明紧急指针字段有效。它告诉接收方TCP此报文段中有紧急数据,应尽快交付给应用程序(相当于高优先级的数据),而不要按序从接收缓存中读取。
    当URG置1时,发送应用进程就告诉发送TCP这两个字符是紧急数据。于是发送TCP就将这两个字符插入到报文段的数据的最前面,其余的数据都是普通数据。这时要与首部中紧急指针(UrgentPointer)字段配合使用。紧急指针指出在本报文段中的紧急数据共有多少个字节。紧急数据到达接收方后,当所有紧急数据都被处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。URG在实际中很少被使用。

  • ACK
    确认ACK只有当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。

  • PSH
    推送PSH(PUSH)出于效率的考虑,TCP可能会延迟发送数据或向应用程序延迟交付数据,这样可以一次处理更多的数据。但是当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,应用程序可以通知TCP使用推送(PUSH)操作。这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去,而不需要积累到足够多的数据再发送。接收TCP收到PSH置1的报文段,就尽快地交付给接收应用进程,而不再等到接收到足够多的数据才向上交付。虽然应用程序可以选择推送操作,但现在多数TCP实现都是根据情况自动设置PUSH标志,而不是交由应用程序去处理。

  • RST
    复位RST(ReSeT)当RST=1时,表明TCP连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可称为重建位或重置位。

  • SYN
    同步SYN用来建立一个连接。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1。

  • FIN
    终止FIN(FINal)用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。

  • 窗口
    窗口占2字节。窗口值指示发送该报文段一方的接收窗口大小,在0到216–1之间。窗口字段用来控制对方发送的数据量(从确认号开始,允许对方发送的数据量),单位为字节。窗口字段反映了接收方接收缓存的可用空间大小,计算机网络经常用接收方的接收能力的大小来控制发送方的数据发送量。例如,设确认号是701,窗口字段是1000。这表明,允许对方发送数据的序号范围为701~1700。

  • 检验和
    检验和占2字节。检验和字段检验的范围包括首部和数据这两部分。用于数据校验。

可靠传输、拥塞控制和流量控制

TCP协议通过以下几个手段确保可靠传输:

  • 数据编号与确认
    TCP协议是面向字节的。TCP把应用层交下来的长报文(这可能要划分为许多较短的报文段)看成是一个个字节组成的数据流,并使每一个字节对应于一个序号。TCP使用的是累积确认,即确认是对所有按序接收到的数据的确认。但请注意,接收方返回的确认号是已按序收到的数据的最高序号加1。也就是说,确认号表示接收方期望下次收到的数据中的第一个数据字节序号。例如,已经收到了1~700号、801~1000号和1201~1500号,而701~800号及1001~1200号的数据还没有收到,那么这时发送的确认序号应填入701。
    当TCP发送一报文段时,它同时也在自己的重传队列中存放这个报文段的一个副本。若收到确认,则删除此副本。若在规定时间内没有收到确认,则重传此报文段的副本。

  • 滑动窗口、流量控制、拥塞控制
    为了提高报文段的传输效率,TCP采用滑动窗口协议。TCP发送方已发送的未被确认的字节数不能超过发送窗口的大小。

发送窗口的初始值在连接建立时由双方商定,但在通信的过程中,TCP的流量控制和拥塞控制会根据情况动态地调整发送窗口上限值(可增大或减小),从而控制发送数据的平均速率。
流量控制是基于接收方的接收缓存剩余情况限制TCP协议头的窗口字段,限制发送方的发送速率.
拥塞控制的任务是防止过多的数据注入到网络中,使网络能够承受现有的网络负载。拥塞控制基于闭环拥塞控制算法分析网络的各节点的网络负载,同流量控制一起限制TCP协议头的窗口字段,限制发送方的发送速率。

  • 超时重传

  • 快速重传
    超时触发重传存在的一个问题就是超时时间可能相对较长。由于无法精确估计实际的往返时间,超时重传时间RTO往往比实际的往返时间大很多。当一个报文段丢失时,发送方需要等待很长时间才能重传丢失的报文段,因而增加了端到端时延。幸运的是,有时一个报文段的丢失会引起发送方连续收到多个重复的确认,通过收到多个重复的确认可以快速地判断报文段可能已经丢失而不必等待重传计时器超时。快速重传就是基于该方法对超时触发重传的补充和改进。
    快速重传算法规定,发送方只要一连收到三个重复的确认,就应立即重传丢失的报文段M2(注意:重复确认的确认号正是要重传的报文段的序号),而不必继续等待为M2设置的重传计时器的超时。不难看出,快速重传并非取消重传计时器,而是尽早重传丢失的报文段。

参考:
《计算机网络教程》 谢钧,谢希仁 人民邮电出版社

HTTP

简述一下HTTP协议

超文本传送协议(HyperTextTransferProtocol,HTTP)是一个应用层协议,HTTP使用了面向连接的TCP作为运输层协议,保证了数据的可靠传输。虽然HTTP使用面向连接的TCP,但HTTP协议本身是一个无状态协议。也就是说,HTTP不要求服务器保留客户的任何状态信息。若服务器不保存任何客户状态信息,则同一个客户上一次对服务器的访问不会影响其对该服务器的下一次访问结果,因为服务器不记得曾经访问过的这个客户,也不记得曾经服务过多少次。HTTP的无状态特性简化了服务器的设计,使服务器更容易支持大量并发的HTTP请求。
HTTP/1.0协议采用的非持续连接方式,即一次请求/响应对应一个TCP连接。在非持续连接方式中,每次浏览器要请求一个文件都要与服务器建立TCP连接,当收到响应后就立即关闭连接。
HTTP/1.1协议使用持续连接,较好地解决了这个问题。所谓持续连接就是万维网服务器在发送响应后仍然保持这条连接,使同一个客户(浏览器)和该服务器可以继续在这条连接上传送后续的HTTP请求报文和响应报文。这并不局限于传送同一个页面上引用的对象,而是只要这些文档都在同一个服务器上就行。为进一步提高效率,HTTP/1.1协议的持续连接还可以使用流水线方式工作,即浏览器在收到HTTP的响应报文之前就能够连续发送多个请求报文。这样的一个接一个的请求报文到达服务器后,服务器就发回一个接一个的响应报文(这样就节省了许多个RTT时间)。流水线工作方式使TCP连接中的空闲时间减少,提高了下载文档的效率。

常见的http状态码是什么

下面三种状态行在响应报文中是经常见到的。
HTTP/1.1 202 Accepted  {接受}
HTTP/1.1 400 Bad Request  {错误的请求}
Http/1.1 404 Not Found  {找不到}

若请求的网页从http://www.ee.xyz.edu/index.html转移到了一个新的地址,则响应报文的状态行和一个首部行就是下面的形式:
HTTP/1.1 301 Moved Permanently {永久性地转移了}

http1.0,http1.1和http2.0的区别?

HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;
HTTP/1.1 串行化单线程处理,可以同时在同一个tcp链接上发送多个请求,但是只有响应是有顺序的,只有上一个请求完成后,下一个才能响应。一旦有任务处理超时等,后续任务只能被阻塞(线头阻塞);
HTTP/2 并行执行。某任务耗时严重,不会影响到任务正常执行

http请求哪些是幂等的?

幂等这个概念指的是多次同样的操作而不改变结果。幂等的概念广泛运用于各种分布式架构,由于网络延迟等原因,一个请求可能要多次重试,遇到这种情况就需要保证这个对应的请求接口是幂等的。另外还有类似银行转账的情形,就算多次请求也要保证对账户只做一次操作。

  • GET
    GET 操作是幂等的,原因是 GET 操作根本不会对服务器产生任何修改。有人可能会说我们访问 GET /last_news 可能每次拿到的结果都不一样,这里幂等的一致性指的是数据库的最终的存储结果,而不是调用方拿到的返回结果

  • PUT
    PUT 方法通常是对已经存在的资源进行修改,也是幂等的。比如我们发起多个把 A 替换成 B 的请求,最终的结果还会是 B

  • DELETE
    DELETE 方法也是幂等的,例如我们连续发起多个对 A 的删除请求,如果第一个成功的话,后面的请求都应返回资源找不到的错误

  • POST
    POST 一般是指新增资源,不是幂等的。如果连续发起三个 A 资源的增加,最终的结果会是三个 A 资源,而不是一个

Get和post的区别是什么?

  • post用于修改和写入数据,get一般用于搜索排序和筛选之类的操作(淘宝,支付宝的搜索查询都是get提交),目的是资源的获取,读取数据

  • post比get慢:最重要的一条,post在真正接收数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据
    post请求的过程:
    (1)浏览器请求tcp连接(第一次握手)
    (2)服务器答应进行tcp连接(第二次握手)
    (3)浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
    (4)服务器返回100 Continue响应
    (5)浏览器发送数据
    (6)服务器返回200 OK响应
    get请求的过程:
    (1)浏览器请求tcp连接(第一次握手)
    (2)服务器答应进行tcp连接(第二次握手)
    (3)浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
    (4)服务器返回200 OK响应
    也就是说,目测get的总耗是post的2/3左右,这个口说无凭,网上已经有网友进行过测试。

http常见请求头

Header 解释 示例
Accept 告诉WEB服务器自己接受什么介质类型,/ 表示任何类型,type/* 表示该类型下的所有子类型,type/sub-type Accept: text/plain, text/html, application/json
Authorization 当客户端接收到来自WEB服务器的 WWW-Authenticate 响应时,用该头部来回应自己的身份验证信息给WEB服务器。 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Host 客户端指定自己想访问的WEB服务器的域名/IP 地址和端口号 Host:rss.sina.com.cn
User-Agent 浏览器表明自己的身份(是哪种浏览器) User-Agent:Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN;rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.14
Connection: Keep-Alive 如果浏览器请求保持连接,则该头部表明希望 WEB 服务器保持连接多长时间(秒) Connection: Keep-Alive:300
Cookie 可以帮助服务器识别客户端的字段 Cookie: $Version=1; Skin=new;

session和cookie简介

session和cookie

cookie 的出现是因为 HTTP 是无状态的一种协议,换句话说,服务器记不住你,可能你每刷新一次网页,就要重新输入一次账号密码进行登录。这显然是让人无法接受的,cookie 的作用就好比服务器给你贴个标签,然后你每次向服务器再发请求时,服务器就能够 cookie 认出你。一个 cookie 可以认为是一个「变量」,形如 name=value,存储在浏览器。

但问题是,我们也知道现在的很多网站功能很复杂,而且涉及很多的数据交互,比如说电商网站的购物车功能,信息量大,而且结构也比较复杂,无法通过简单的 cookie 机制传递这么多信息,而且要知道 cookie 字段是存储在 HTTP header 中的,就算能够承载这些信息,也会消耗很多的带宽,比较消耗网络资源。

一个 session 可以理解为一种数据结构,多数情况是「映射」(键值对),存储在服务器上。session 就可以配合 cookie 解决这一问题,比如说一个 cookie 存储这样一个变量 sessionID=xxxx,仅仅把这一个 cookie 传给服务器,然后服务器通过这个 ID 找到对应的 session,这个 session 是一个数据结构,里面存储着该用户的购物车等详细信息,服务器可以通过这些信息返回该用户的定制化网页,有效解决了追踪用户的问题。

session 是一个数据结构,由网站的开发者设计,所以可以承载各种数据,只要客户端的 cookie 传来一个唯一的 session ID,服务器就可以找到对应的 session,认出这个客户。当然,由于 session 存储在服务器中,肯定会消耗服务器的资源,所以 session 一般都会有一个过期时间,服务器一般会定期检查并删除过期的 session,如果后来该用户再次访问服务器,可能就会面临重新登录等等措施,然后服务器新建一个 session,将 session ID 通过 cookie 的形式传送给客户端。

session处理过程

session 的原理不难,但是具体实现它可是很有技巧的,一般需要三个组件配合完成,它们分别是 Manager、Provider 和 Session 三个类(接口)。

1、浏览器通过 HTTP 协议向服务器请求路径 /content 的网页资源,对应路径上有一个 Handler 函数接收请求,解析 HTTP header 中的 cookie,得到其中存储的 sessionID,然后把这个 ID 发给 Manager。

2、Manager 充当一个 session 管理器的角色,主要存储一些配置信息,比如 session 的存活时间,cookie 的名字等等。而所有的 session 存在 Manager 内部的一个 Provider 中。所以 Manager 会把 sid(sessionID)传递给 Provider,让它去找这个 ID 对应的具体是哪个 session。

3、Provider 就是一个容器,最常见的应该就是一个散列表,将每个 sid 和对应的 session 一一映射起来。收到 Manager 传递的 sid 之后,它就找到 sid 对应的 session 结构,也就是 Session 结构,然后返回它。

4、Session 中存储着用户的具体信息,由 Handler 函数中的逻辑拿出这些信息,生成该用户的 HTML 网页,返回给客户端。

  • session

session 就是键值对,为啥不直接用哈希表?

第一,因为 Session 结构可能不止存储了一个哈希表,还可以存储一些辅助数据,比如 sid,访问次数,过期时间或者最后一次的访问时间,这样便于实现想 LRU、LFU 这样的算法。

第二,因为 session 可以有不同的存储方式。如果用编程语言内置的哈希表,那么 session 数据就是存储在内存中,如果数据量大,很容易造成程序崩溃,而且一旦程序结束,所有 session 数据都会丢失。所以可以有很多种 session 的存储方式,比如存入缓存数据库 Redis,或者存入 MySQL 等等。

因此,Session 结构提供一层抽象,屏蔽不同存储方式的差异,只要提供一组通用接口操纵键值对:

type Session interface {
    // 设置键值对
    Set(key, val interface{})
    // 获取 key 对应的值
    Get(key interface{}) interface{}
    // 删除键 key
    Delete(key interface{})
}
  • Provider

Provider 为啥要抽象出来?

上面那个图的 Provider 就是一个散列表,保存 sid 到 Session 的映射,但是实际中肯定会更加复杂。我们不是要时不时删除一些 session 吗,除了设置存活时间之外,还可以采用一些其他策略,比如 LRU 缓存淘汰算法,这样就需要 Provider 内部使用哈希链表这种数据结构来存储 session。

  • Manager

最后说 Manager,大部分具体工作都委托给 Session 和 Provider 承担了,Manager 主要就是一个参数集合,比如 session 的存活时间,清理过期 session 的策略,以及 session 的可用存储方式。Manager 屏蔽了操作的具体细节,我们可以通过 Manager 灵活地配置 session 机制。

参考:
《计算机网络教程》 谢钧,谢希仁 人民邮电出版社
http中长连接和websocket的长连接的区别
HTTP方法的幂等性
http请求中get和post方法的区别
HTTP请求头各参数具体含义
cookie和session到底是什么

长连接和短链接

概述

HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。

  • HTTP长连接

浏览器向服务器进行一次HTTP会话访问后,并不会直接关闭这个连接,而是会默认保持一段时间,那么下一次浏览器继续访问的时候就会再次利用到这个连接。
在HTTP/1.1版本中,默认的连接都是长连接,我们可以通过Connection: keep-alive字段进行指定。

  • HTTP短连接

浏览器向服务器每进行一次HTTP操作都要建立一个新的连接。
在HTTP/1.0版本中默认是短链接。

TCP长连接保活机制

TCP长连接是通过保活机制来实现的,通过保活机制,可以保证通讯双方的连接不被释放掉;在另一些情况下,如果客户端或者服务器发生了错误或者宕机,也可以依靠这种保活机制探测出网络通信出现了问题,进而可以释放掉这种错误连接。

首先保活机制的工作原理就是,通过在服务器端设置一个保活定时器,当定时器开始工作后就定时的向网络通信的另一端发出保活探测的TCP报文,如果接收到了ACK报文,那么就证明对方存活,可以继续保有连接;否则就证明网络存在故障。具体如下:

如果一个给定的连接在两个小时之内没有任何动作,则服务器就向客户发送一个探查报文段。客户主机必须处于以下4个状态之一。

状态1:客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常工作的。服务器在两小时以后将保活定时器复位。如果在两个小时定时器到时间之前有应用程序的通信量通过此连接,则定时器在交换数据后的未来2小时再复位。
状态2:客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务器将不能够收到对探查的响应,并在75秒后超时。服务器总共发送10个这样的探查,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
状态3:客户主机崩溃并已经重新启动。这时服务器将收到一个对其保活探查的响应,但是这个响应是一个复位,使得服务器终止这个连接。
状态4:客户主机正常运行,但是从服务器不可达。这与状态2相同,因为TCP不能够区分状态4与状态2之间的区别,它所能发现的就是没有收到探查的响应。

但是保活机制并不是RFC规定的TCP协议的内容,因此有时候在不支持保活机制的机器上,往往我们也需要先看一下内核层面是否支持,如果不支持需要在应用层自己去实现这个功能。
Linux相关的TCP保活参数:
tcp_keepalive_time,单位:秒,表示发送的探测报文之前的连接空闲时间,默认是7200s。
tcp_keepalive_intvl,单位:秒,表示两次探测报文之间的间隔时间,默认是75s
tcp_keepalive_probes,单位,秒,表示探测的次数,默认是9

TCP长连接和短链接比较

  • TCP短链接

优点:
短链接不占服务器的内存,服务器能处理的连接数量会比较多

缺点:
在有实际的资源要进行数据通信的时候才建立连接,那么在客户端发送完数据释放连接之后当服务器有向客户端发送数据时就不能做到发送消息的实时性。
频繁地建立连接、释放连接会耗费大量的CPU和网络带宽资源。

  • TCP长连接

优点:
通信双方因为在保活机制的保证下可以保证数据收发的实时性

缺点:
因为服务器需要一直保存和客户端的这条链接,因为是有状态的,那么在大量并发连接请求过来时,系统资源可能就不够了。

什么时候需要长连接

服务器需要主动发送资源给客户端时
客户端和服务器通信很频繁时
客户端宕机或者掉线时需要服务器做一些处理时

TCP长连接设计时需要考虑的问题

默认的keep-alive时间比较长,一般的业务可能不需要这么久的时间

参考:
HTTP长连接实现原理
HTTP长连接、短连接究竟是什么?

浏览器上点击链接后的处理流程?

鼠标点击“清华大学院系设置”的页面,其URL是http://www.tsinghua.edu.cn/chn/yxsz/index.htm。下面说明在用户单击鼠标后所发生的几个事件:(1)浏览器分析链接指向页面的URL;(2)浏览器向DNS请求解析www.tsinghua.edu.cn的IP地址;(3)域名系统DNS解析出清华大学服务器的IP地址为166.111.4.100;(4)浏览器与服务器建立TCP连接(在服务器端IP地址是166.111.4.100,端口是80);(5)浏览器发出取文件命令:GET/chn/yxsz/index.htm;(6)服务器www.tsinghua.edu.cn给出响应,把文件index.htm发送给浏览器;(7)释放TCP连接;(8)浏览器显示“清华大学院系设置”文件index.htm中的内容。

DNS解析流程?

域名服务器层级

域名服务器可划分为以下4种不同类型:

  • 根域名服务器
    这是最高层次的域名服务器。根域名服务器并不直接管辖某个区的域名信息,但每个根域名服务器都知道所有的顶级域名服务器的域名及其IP地址。在因特网上共有13个不同IP地址的根域名服务器。尽管我们将这13个根域名服务器中的每一个都视为单个的服务器,但每台“服务器”实际上是由许多分布在世界各地的计算机构成的服务器群集。根域名服务器通常并不直接对域名进行解析,而是返回该域名所属顶级域名的顶级域名服务器的IP地址。
  • 顶级域名服务器(即TLD服务器)
    这些域名服务器负责管理在该顶级域名服务器注册的所有二级域名。当收到DNS查询请求时就给出相应的回答(可能是最后的结果,也可能是下一级权威域名服务器的IP地址)。
  • 权威域名服务器
    负责管理某个区的域名服务器。每一个主机的域名都必须在某个权威域名服务器处注册登记。因此权威域名服务器知道其管辖的域名与IP地址的映射关系。另外,权威域名服务器还知道其下级域名服务器的地址。
  • 本地域名服务器
    本地域名服务器不属于上图的域名服务器的等级结构。当一个主机发出DNS查询报文时,这个查询报文就首先被送往该主机的本地域名服务器。本地域名服务器起着DNS代理的作用,会将该查询报文转发到域名服务器的等级结构中。每一个因特网服务提供者ISP,一个大学,甚至一个大学里的系,都可以拥有一个本地域名服务器,它有时也称为默认域名服务器。本地域名服务器离用户较近,一般不超过几个路由器的距离,有可能就在同一个局域网中。本地域名服务器的IP地址需要直接配置在需要域名解析的主机中。例如,在WindowsXP网络连接属性中设置的DNS地址就是该主机本地域名服务器的IP地址。

域名解析流程

主机向本地域名服务器的查询一般都是采用递归查询(recursivequery)。所谓递归查询就是如果本地域名服务器不知道被查询域名的IP地址时,那么本地域名服务器就以DNS客户的身份向某个根域名服务器继续发出查询请求报文(即替该主机继续查询),而不是让该主机自己进行下一步的查询。

本地域名服务器向根域名服务器查询时,是优先采用迭代查询(iterativequery)。所谓迭代查询就是由本地域名服务器进行循环查询。当根域名服务器收到查询请求报文但并不知道被查询域名的IP地址时,这个根域名服务器就把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,让本地域名服务器再向顶级域名服务器查询。顶级域名服务器在收到本地域名服务器的查询请求后,就告诉本地域名服务器下一步应当向哪一个权威域名服务器进行查询。这样查询下去,主机就知道了所要解析的域名的IP地址。

本地域名服务器和主机中均会缓存域名信息,主机只在从缓存中找不到欲解析的域名时才向本地域名服务器发送查询请求报文。本地域名服务器和本机的域名信息都会定期更新。

如果全部迭代查询,上层服务器将长时间保持连接(因为要代替本地域名服务器去查),所以负担大

参考:
《计算机网络教程》 谢钧,谢希仁 人民邮电出版社

https的流程可以描述一下么?如果没有证书可以么?

为什么要用https?

实际使用中,绝大说的网站现在都采用的是https协议,这也是未来互联网发展的趋势。下面是通过wireshark抓取的一个博客网站的登录请求过程。

可以看到访问的账号密码都是明文传输, 这样客户端发出的请求很容易被不法分子截取利用,因此,HTTP协议不适合传输一些敏感信息,比如:各种账号、密码等信息,使用http协议传输隐私信息非常不安全。

一般http中存在如下问题:
请求信息明文传输,容易被窃听截取。
数据的完整性未校验,容易被篡改
没有验证对方身份,存在冒充危险

什么是HTTPS?

为了解决上述HTTP存在的问题,就用到了HTTPS。

HTTPS 协议(HyperText Transfer Protocol over Secure Socket Layer):一般理解为HTTP+SSL/TLS,通过 SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密。

那么SSL又是什么?

SSL(Secure Socket Layer,安全套接字层):1994年为 Netscape 所研发,SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。

TLS(Transport Layer Security,传输层安全):其前身是 SSL,它最初的几个版本(SSL 1.0、SSL 2.0、SSL 3.0)由网景公司开发,1999年从 3.1 开始被 IETF 标准化并改名,发展至今已经有 TLS 1.0、TLS 1.1、TLS 1.2 三个版本。SSL3.0和TLS1.0由于存在安全漏洞,已经很少被使用到。TLS 1.3 改动会比较大,目前还在草案阶段,目前使用最广泛的是TLS 1.1、TLS 1.2。

SSL发展史(互联网加密通信)

1994年NetSpace公司设计SSL协议(Secure Sockets Layout)1.0版本,但未发布。
1995年NetSpace发布SSL/2.0版本,很快发现有严重漏洞
1996年发布SSL/3.0版本,得到大规模应用
1999年,发布了SSL升级版TLS/1.0版本,目前应用最广泛的版本
2006年和2008年,发布了TLS/1.1版本和TLS/1.2版本

浏览器在使用HTTPS传输数据的流程是什么?

  • 首先客户端通过URL访问服务器建立SSL连接。
  • 服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端。
  • 客户端的服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
  • 客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
  • 服务器利用自己的私钥解密出会话密钥。
  • 服务器利用会话密钥加密与客户端之间的通信。

HTTPS的证书认证过程

具体参考:
一篇读懂https的本质、证书验证过程以及数据加密
HTTPS数字证书与验证
主要是证书链认证一直到root证书,保障公钥的可信,然后传递对称密钥用于数据加密。

HTTPS的缺点

HTTPS协议多次握手,导致页面的加载时间延长近50%;
HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗;
申请SSL证书需要钱,功能越强大的证书费用越高。
SSL涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大。

总结HTTPS和HTTP的区别

HTTPS是HTTP协议的安全版本,HTTP协议的数据传输是明文的,是不安全的,HTTPS使用了SSL/TLS协议进行了加密处理。
http和https使用连接方式不同,默认端口也不一样,http是80,https是443。

如果没有https证书可以访问么

可以,就当相信对方,但是实际安全无法保障,因为没有权威机构认证身份。一般界面会显示“告诉你对方证书无效,是否继续连接?”,可以无视提示继续连接。

参考:
如果没有有效的证书,HTTPS连接是否加密的?
科普!什么是SSL证书?为什么要买SSL证书?
十分钟搞懂HTTP和HTTPS协议?
一篇读懂https的本质、证书验证过程以及数据加密
HTTPS数字证书与验证

select,poll,epoll的区别?epoll的ET和LT分别是什么?

select,poll,epoll

最基础的 TCP 的 Socket 编程,它是阻塞 I/O 模型,基本上只能一对一通信,那为了服务更多的客户端,我们需要改进网络 I/O 模型。

比较传统的方式是使用多进程/线程模型,每来一个客户端连接,就分配一个进程/线程,然后后续的读写都在对应的进程/线程,这种方式处理 100 个客户端没问题,但是当客户端增大到 10000 个时,10000 个进程/线程的调度、上下文切换以及它们占用的内存,都会成为瓶颈。

为了解决上面这个问题,就出现了 I/O 的多路复用,可以只在一个进程里处理多个文件的 I/O,Linux 下有三种提供 I/O 多路复用的 API,分别是:select、poll、epoll。

select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。

在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。

很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。

epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。

epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。

epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。

而且,epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。

ET和LT

epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。

使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;

使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。

如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。

一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。

参考:
这次答应我,一举拿下 I/O 多路复用

IO的模型有哪几种?Reactor和Preactor线程模型有了解?

IO的模型有哪几种

  • 阻塞I/O

当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。

注意,阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程。

  • 非阻塞I/O

非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。

注意,这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。

如果 socket 设置了 O_NONBLOCK 标志,那么就表示使用的是非阻塞 I/O 的方式访问,而不做任何设置的话,默认是阻塞 I/O。因此,无论 read 和 send 是阻塞 I/O,还是非阻塞 I/O 都是同步调用。因为在 read 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间。

  • 异步I/O

真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。当我们发起 aio_read (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。

  • 对比

举个你去饭堂吃饭的例子,你好比应用程序,饭堂好比操作系统。
阻塞 I/O 好比,你去饭堂吃饭,但是饭堂的菜还没做好,然后你就一直在那里等啊等,等了好长一段时间终于等到饭堂阿姨把菜端了出来(数据准备的过程),但是你还得继续等阿姨把菜(内核空间)打到你的饭盒里(用户空间),经历完这两个过程,你才可以离开。
非阻塞 I/O 好比,你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,过几十分钟,你又来饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒里,这个过程你是得等待的。
异步 I/O 好比,你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个过程你都不需要任何等待。
很明显,异步 I/O 比同步 I/O 性能更好,因为异步 I/O 在「内核数据准备好」和「数据从内核空间拷贝到用户空间」这两个过程都不用等待。

可惜的是,在 Linux 下的异步 I/O 是不完善的, aio 系列函数是由 POSIX 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步,并且仅仅支持基于本地文件的 aio 异步操作,网络编程中的 socket 是不支持的,这也使得基于 Linux 的高性能网络程序都是使用 Reactor 方案。

Reactor

I/O多路复用可以应对高并发请求场景,不必针对每个请求连接创建一个线程去处理。 select/poll/epoll 就是内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个事件。但是直接通过I/O 多路复用接口写网络程序开发效率太低,于是,大佬们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写,这种封装的模式就叫Reactor 模式

Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。

Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下:

Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;
处理资源池负责处理事件,如 read -> 业务逻辑 -> send;

经典的 Reactor 方案如下:

  • 单 Reactor 单进程 / 线程处理资源池;

可以看到进程里有 Reactor、Acceptor、Handler 这三个对象:Reactor 对象的作用是监听和分发事件;Acceptor 对象的作用是获取连接;Handler 对象的作用是处理业务;对象里的 select、accept、read、send 是系统调用函数,dispatch 和 「业务处理」是需要完成的操作,其中 dispatch 是分发事件操作。

Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。

单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。但是,这种方案存在 2 个缺点:第一个缺点,因为只有一个进程,无法充分利用 多核 CPU 的性能;第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;所以,单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。Redis 是由 C 语言实现的,它采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。

  • 单 Reactor 多线程 / 进程处理资源池;

Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;上面的三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤就开始不一样了:Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;

单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。「单 Reactor」的模式还有个问题,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。

单 Reactor 多进程相比单 Reactor 多线程实现起来很麻烦,实际应用中也看不到单 Reactor 多进程的模式。

  • 多 Reactor 多进程 / 线程处理资源池;

主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。

多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。

大名鼎鼎的两个开源软件 Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案。

Proactor

前面提到的 Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式。
linux中对于异步I/O支持不完善,所以Linux 的高性能网络程序都是使用 Reactor 方案。

参考:
如何深刻理解Reactor和Proactor

NIO的三大件分别是什么

Java NIO(New IO)是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用方式完全不同,NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。

传统的 IO 是面向流的(一个个字节数据的流动),传统的 IO 流是单向的

NIO面向缓冲区,缓冲区是双向的。通道Channel可以类比铁路,而缓冲区Buffer可以类比火车。

Buffer

缓冲区(Buffer):一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer

  • 核心方法

put():存入数据到缓冲区中;get():获取缓冲区中的数据

  • 核心属性

capacity: 容量,表示缓冲区中最大存储数据的容量。一旦声明不能更改。
limit: 界限,表示缓冲区中可以操作数据的大小。(limit 后的数据不能进行读写)
position: 位置,表示缓冲区中正在操作数据的位置。
mark: 标记,表示记录当前 position 的位置。可以通过 reset() 恢复到 mark 的位置。

0 <= mark <= position <= limit <= capacity

  • 直接缓冲区与非直接缓冲区

(1)非直接缓冲区

通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存之中。

String str = "abcde";
//分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("---------allocate-----------");
System.out.println(byteBuffer.capacity());   //1024
System.out.println(byteBuffer.limit());      //1024
System.out.println(byteBuffer.position());   //0

应用程序想从磁盘中读取一个数据,这时候应用程序向操作系统发起一个读请求,那么首先磁盘中的数据会被读取到内核地址空间中,然后会把内核地址空间中的数据拷贝到用户地址空间中(其实就是 JVM 内存中),最后再把这个数据读取到应用程序中来。同样,如果应用程序有数据想要写到磁盘中去,那么它会首先把这个数据写入到用户地址空间中去,然后把数据拷贝到内核地址空间,最后再把这个数据写入到磁盘中去。

(2)直接缓冲区

通过 allocateDirect() 方法分配缓冲区,将缓冲区直接建立在物理内存之中,避免无效的复制。

public static void test3() {
    // 分配直接缓冲区
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    // 判断是直接缓冲区还是非直接缓冲区
    System.out.println(byteBuffer.isDirect());
}

Channel

Channel 用于源节点与目标节点的连接, Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
常见Channel:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel

  • 获取通道

(1) Java 针对支持通道的类提供了 getChannel() 方法

本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile

网络 IO:
Socket
ServerSocket
DatagramSocket

以上几个类都可以通过调用 getChannel() 方法获取通道

(2)在 JDK1.7 中的 NIO.2 针对各个通道提供了静态方法 open()

(3)在 JDK1.7 中的 NIO.2 的 Files 工具类的 newByteChannel() 方法

  • Channel案例
public static void test1() throws Exception {
        // 利用通道完成文件的复制(非直接缓冲区)
        FileInputStream fis = new FileInputStream("a.txt");
        FileOutputStream fos = new FileOutputStream("b.txt");
        // 获取通道
        FileChannel fisChannel = fis.getChannel();
        FileChannel foschannel = fos.getChannel();

        // 通道没有办法传输数据,必须依赖缓冲区
        // 分配指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 将通道中的数据存入缓冲区中
        while (fisChannel.read(byteBuffer) != -1) {  // fisChannel 中的数据读到 byteBuffer 缓冲区中
            byteBuffer.flip();  // 切换成读数据模式
            // 将缓冲区中的数据写入通道
            foschannel.write(byteBuffer);
            byteBuffer.clear();  // 清空缓冲区
        }
        foschannel.close();
        fisChannel.close();
        fos.close();
        fis.close();
    }
public static void test2() throws Exception {
    // 使用直接缓冲区完成文件的复制(内存映射文件)
    /**
         * 使用 open 方法来获取通道
         * 需要两个参数
         * 参数1:Path 是 JDK1.7 以后给我们提供的一个类,代表文件路径
         * 参数2:Option  就是针对这个文件想要做什么样的操作
         *      --StandardOpenOption.READ :读模式
         *      --StandardOpenOption.WRITE :写模式
         *      --StandardOpenOption.CREATE :如果文件不存在就创建,存在就覆盖
         */
    FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
    FileChannel outChannel = FileChannel.open(Paths.get("c.txt"), StandardOpenOption.WRITE,
                                              StandardOpenOption.READ, StandardOpenOption.CREATE);

    /**
         * 内存映射文件
         * 这种方式缓冲区是直接建立在物理内存之上的
         * 所以我们就不需要通道了
         */
    MappedByteBuffer inMapped = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
    MappedByteBuffer outMapped = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());

    // 直接对缓冲区进行数据的读写操作
    byte[] dst = new byte[inMapped.limit()];
    inMapped.get(dst);  // 把数据读取到 dst 这个字节数组中去
    outMapped.put(dst); // 把字节数组中的数据写出去

    inChannel.close();
    outChannel.close();
}
public static void test3() throws Exception {
    /**
         * 通道之间的数据传输(直接缓冲区的方式)
         * transferFrom
         * transferTo
         */
    FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
    FileChannel outChannel = FileChannel.open(Paths.get("d.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE,
                                              StandardOpenOption.CREATE);
    inChannel.transferTo(0, inChannel.size(), outChannel);
    // 或者可以使用下面这种方式
    //outChannel.transferFrom(inChannel, 0, inChannel.size());
    inChannel.close();
    outChannel.close();
}

Selector

假设NIO没有这个Selector选择器,那么此时一个客户端连接过来就要对应的一个线程去处理,但是这个客户端可能只是连接一下,并不会立马就会有读写事件,那么此时依然会造成线程的阻塞,导致资源的浪费。有了Selector之后,客户端发送连接请求之后,服务端就把对应的SocketChannle注册到Selector上去。如果这时候该客户端Channle上有读写事件发生,再把请求交给对应的线程处理,如果没有就不做任何处理,这样没有事件的时候线程不会处于阻塞状态,也不会导致系统资源的浪费,只有真正有事件发生的Channel才会交给对应的线程去处理。
本质也是在做IO多路复用的事情,底层调用的系统调用的select函数。

参考:
Java 中 NIO 看这一篇就够了
Java NIO之三 直接缓冲区、非直接缓冲区、区别、及底层实现
NIO三大核心详解
Java NIO(NIO socket网络编程)
Java NIO 中的 Selector 详解
面试系列 深入理解NIO select&epoll

标签:主机,报文,面经,TCP,连接,计算机网络,域名,服务器,梳理
From: https://www.cnblogs.com/fattree/p/18257696

相关文章

  • 【计算机网络仿真】b站湖科大教书匠思科Packet Tracer——实验一 Packet Tracer的简单
    一、实验目的1.掌握PacketTracer的简单使用方式;2.加深对网络体系结构的理解。二、实验要求1.搭建CiscoPacketTracer仿真平台使用环境;2.观看B站湖科大教书匠仿真实验视频,完成对应实验。三、实验内容第一部分:PacketTracer的简单使用1.下载CiscoPacketTracer并......
  • 【信息学奥赛】CSP-J/S初赛03 计算机网络与编程语言分类
    第1节计算机网络基础1.1网络的定义所谓计算机网络,就是利用通信线路和设备,把分布在不同地理位置上的多台计算机连接起来。计算机网络是现代通信技术与计算机技术相结合的产物。网络中计算机与计算机之间的通信依靠协议进行。协议是计算机收、发数据的规则。TCP/IP:用于......
  • 【计算机网络】第四章.网络层 网络层超硬核复习好物(1),考前必看!!
    ......
  • 计算机网络实验(9):路由器的基本配置和单臂路由配置
    一、实验名称路由器的基本配置和单臂路由配置二、实验目的: (1)路由器的基本配置:掌握路由器几种常用配置方法;掌握采用Console线缆配置路由器的方法;掌握采用Telnet方式配置路由器的方法;熟悉路由器不同的命令行操作模式以及各种模式之间的切换;掌握路由器的基本配置命令;路......
  • 【前端面经】数组算法题解
    目录题目一:两数之和题目二:最长无重复字符子串题目三:合并两个有序数组题目四:寻找数组中的峰值题目一:两数之和描述:给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。......
  • 面经梳理-java多线程同步协作
    题目Synchronized和ReentryLock锁锁可以视作访问共享数据的许可证。锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可见性和保障有序性。Java平台中的锁包括内部锁(IntrinsicLock)和显式锁(ExplicitLock)。内部锁是通过synchronized关键字实现的;显式锁是通过java.ut......
  • 面经梳理-java多线程其他
    题目Threadlocal使用场景?原理?如何保证内存不泄露?ThreadLocal使用场景不加锁的情况下,多线程安全访问共享变量,每个线程保留共享变量的副本(线程特有对象),每个线程往这个ThreadLocal中读写是线程隔离。ThreadLocal原理Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量th......
  • 面经梳理-java多线程基础
    题目线程和进程的概念?守护线程是干什么的?常见的守护线程有哪些?线程和进程的概念进程是程序的运行实例,是程序向操作系统申请资源的基本单位,线程是进程的一条执行路径。Java的线程分为两种:用户线程和守护线程。守护线程作用是为其他线程提供服务,如果所有的用户线程死亡,后台线程......
  • 面经梳理--java
    题目Java创建一个对象的方法有几种?使用new关键字反射(1)使用Class类的newInstance方法,只能无参构造器Employeeemp2=(Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();或者(目前Class.newInstance()这种方法因为效率问题已经被标记@......
  • 计算机网络 — 为什么要有运输层(传输层)
    目录IP协议能够把源主机发送的分组,按照首部中的目的地址,交由目的主机,那为什么还需要运输层呢?IP协议能够把源主机发送的分组,按照首部中的目的地址,交由目的主机,那为什么还需要运输层呢?从IP层来说,通信的两端是两台主机,但严格的说,是主机中的一个进程和另......