Java后端面试系列-计算机网络篇
1 OSI七层概念模型
1.1 物理层
目的:解决两台物理机的通信需求,传递的是比特流,也就是010101...二进制数据,先转
成电流的强弱进行传输,达到后转成010101...(也就是所谓的数模转换和模数转换)。网卡
就是工作在这一层。
1.2 数据链路层
在传输比特流的过程中,会产生错传、数据传输不完整等情况。针对这些情况,数据链路层
定义了如何格式化数据,控制对物理介质的访问,还提供错误检测和纠正以保证数据传输的可
靠性,其中将比特流组成了帧。交换机就是工作在这一层,对帧解码,并将帧中的数据发送到
正确的接收方。
1.3 网络层
主要工作是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方。
路由器属于网络层。此层的数据成为数据包,需要关注的协议是IP协议。
1.4 传输层
传输层解决了主机间的数据传输,同时解决了传输质量的问题,该层需要关注的是TCP协议
和UDP协议。
1.5 会话层
作用是建立和管理应用程序之间的通讯。
1.6 表示层
解决信息的语法语义及它们的关联,如加密解密、转换翻译、压缩解压缩。
1.7 应用层
规定应用发送方和接收方必须使用固定长度的消息头,且有固定规则,需要关注的是HTTP
协议。
2 TCP/IP
OSI只是一个概念模型,TCP/IP可以看成OSI的“实现”
OSI七层模型 | TCP/IP模型 | 功能 | TCP/IP协议族 |
---|---|---|---|
应用层 | 应用层 | 文件传输,电子邮件、文件服务,虚拟终端 | TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet |
表示层 | 数据格式化,代码转换,数据加密 | 没有协议 | |
会话层 | 解除或建立与别的接点的联系 | 没有协议 | |
传输层 | 传输层 | 提供端对端的接口 | TCP,UDP |
网络层 | 网络层 | 为数据包选择路由 | IP,ICMP,RIP,OSPF,BGP,IGMP |
数据链路层 | 链路层 | 传输有地址的帧以及错误检测功能 | SLIP,CSLIP,PPP,ARP,RARP,MTU |
物理层 | 以二进制数据形式在物理媒体上传输数据 | ISO2110,IEEE802,IEEE802.2 |
3 说说TCP的三次握手
3.1 传输控制协议TCP简介
- 面向连接的、可靠的、基于字节流的传输层通信协议
- 将应用层的数据流分割为报文段并发送给目标节点的TCP层
- 数据包都有序号,对方收到则发送ACK确认,未收到则重传
- 使用校验和来检验数据在传输过程中的是否有误
3.2 TCP报文头
- 源端口,目的端口:各占2个字节
- 序列号:占4个字节,因为字节流中每个字节都按顺序标号,这就是那个号
- 确认号:占4个字节,是期望收到的下一个字节的序号
- Offset:指出TCP数据距离TCP报文的起始处有多远
- Reserved:保留域,默认为0
- TCP Flags:占一个字节,详情见3.3
- Window:代表滑动窗口的大小,用来告知发送端接收端的缓存大小,以此控制发送端的发送速率,从而达到流量控制。
- 校验和:占2个字节
- 紧急指针:仅当URG为1时有效,指出报文的紧急数据的字节数
10.TCP可选项:定义其他的可选参数
3.3 TCP Flags
- URG:紧急指针标志
- ACK:确认序号标志(1表示有,0则忽略)
- PSH:push标志
- RST:重置连接标志
- SYN:同步序号,用于建立连接过程
- FIN:finish标志,用于释放连接
3.4 三次握手流程
注意:前两次握手不可以携带数据,第三次握手可带可不带,若不带,则不会消耗序号。
(1)第一次握手:建立连接时,客户端发送SYN包(syn=j)到服务端,并进入SYN_SEND状态,等待服务端确认;
(2)第二次握手:服务端接收到SYN包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RCVD状态;
(3)第三次握手:客户端接收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器今日ESTABLISHED状态,完成三次握手
3.5 为什么需要三次握手才能建立连接
为了初始化Sequence Number的初始值。
通信双方要通知给对方自己初始化的Sequence Number,这个序列号要作为以后数据通讯的序号,以保证传输层接受到的数据不会因网络上的传输问题而乱序,即TCP会用这个序号来拼接数据,因此在服务器回发它的Sequence Number,即第二次握手之后,还需要发送确认报文给服务器,告知服务器客户端已经收到你的Sequence Number了。
3.6 首次握手的隐患——SYN超时
问题起因分析
- Service收到Client的SYN,回复SYN-ACK的时候未收到ACK
- Service不断重试直至超时,Linux默认等待63秒才断开连接(重试5次,每个等待时间翻倍,即等待1s,2s,4s,8s,16s,发送五次后,会在等待32秒才判定超时,总共63s)
针对SYN Flood的防护措施 - SYN队列满后,TCP会通过源地址端口、目的地址端口、时间戳打造一个特殊的Sequence NUmber,即tcp_syncookies参数(简称SYN Cookie),发回去。
- 若为攻击者,不会有相应;若为正常连接,则会将这个SYN Cookie发回来,直接建立连接。
- 通过SYN Cookie,即便SYN队列满了,本请求不在队列中,依然能够建立连接。
3.7 建立连接后,Clinet出现故障怎么办
保活机制
- 在一段时间内,即保活时间,称为keep alive time,连接处于非活动状态,开启保活机制的一方将向对方发送保活探测报文,如果未收到相应则重发
- 直到尝试次数达到保活探测数,仍未收到相应则中断连接
4 谈谈TCP的四次挥手
注1:这一过程又客户端或服务端任意一端执行close来触发,这里假设客户端主动触发close。
注2:MSL代表最大报文生存时间
4.1 四次挥手流程
(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态
(2)第二次挥手:Server收到FIN后,发送一个ACK给Cient,确认确认序号为收到序号+1,Server进入CLOSE_WAIT状态
(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态
(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
4.2 为什么会有TIME_WAIT状态
原因:
- 确保有足够的时间让对方收到ACK包
- 避免新旧连接混淆
4.3 为什么需要四次挥手才能中断连接
因为全双工(即:客户端和服务端都可向对方发送数据),发送方和接收方都需要FIN报文和ACK报文
4.4 服务器大量出现CLOSE_WAIT状态的原因
对方关闭socket连接,我方忙于读或写,没有及时关闭连接
如何处理?
- 检查代码,特别是释放资源的代码
- 检查配置,特别是处理请求的线程配置(比如线程池)
// 查看CLOSE_WAIT的数量
$ netstat -n | awk '/^tcp/{++S[$NF]}END{for(a in S) print a,S[a]}'
5 说说UDP
5.1 UDP报文结构
5.2 UDP特点
- 面向无连接
- 不维护连接状态,支持同时向多个客户端传输相同的消息
- 数据包报头只有8个字节,额外开销小
- 吞吐量只受限于数据生成速率、传输速率以及机器性能
- 尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表
- 面向报文,不对应用程序提交的报文信息进行拆分或者合并
6 TCP和UDP的区别
- 面向连接 vs 无连接
- 可靠性
- 有序性
- 速度
- 量级
7 TCP的滑动窗口
前置概念:
- RTT:发送一个数据包到收到对应的ACK,所花费的时间
- RTO:重传时间间隔(不固定,由RTT计算而来)
TCP使用滑动窗口做流量控制与乱序重排,有两个作用:
- 保证TCP的可靠性
- 保证TCP的流控制特性
TCP头部中的Window字段用于接受方告知发送方自己缓存的剩余空间,发送方根据接受方的处理能力发送数据,不会导致接受方处理不过来。
窗口数据的计算过程
首先明确左边缓冲中的这些段的意思:
LastByteAcked之前:已发送且已确认的数据
LastByteAcked 到 LastByteSent:已发送但未确认的数据
LastByteSent 到 LastByteWritten:上层应用已写完,但还未发送的数据
在明确右边缓冲中的这些段的意思:
LastByteRead之前:上层应用已经读完,也都发送回执的数据
LastByteRead 到 LastByteExpected:已经收到,但还未发送回执的数据
LastByteRcvd:接收到的最后一个字节的位置
- 接收方还可以处理的量: AdvertisedWindow = MaxRcvBuffer - (LastByteRcvd - LastByteRead)
- 窗口内剩余可发送数据的大小:EffectiveWindow = AdvertisedWindow - (LastByteSent - LastByteAcked)
TCP滑动窗口原理
TCP会话的发送方:
其中,类别2发送但未确认这段和类别3未发送但允许发送这段组成的这段连续空间成为发送窗口。
注意:只有当左边数据(上图是指32到35数据都被确认了)被连续确认之后,滑动窗口才会向右移动,新包括进来的数据才可以发送。
TCP会话的接收方 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200512204450104.png) 其中,类别3未接收但准备接收的这段窗口成为**接收窗口**。只有当左边都接受到数据后,才会向右滑动。
8 HTTP协议
8.1 超文本传输协议HTTP的主要特点
- 支持客户/服务器模式
- 简单快速(仅需要请求方法和地址)
- 灵活(传输类型多)
- 无连接
- 无状态(对事务处理没有记忆)
HTTP1.1相较于HTTP1.0最大的特点是:增加了keep-alive
8.2 HTTP请求结构
8.3 HTTP响应结构
注意:响应头部与相应正文必有一个空行
8.4 请求/相应步骤
- 客户端连接到Web服务器
- 发送HTTP请求
- 服务器接受请求并返回HTTP响应
- 释放连接TCP连接
- 客户端浏览器解析HTML内容
问:HTTP请求方法有哪些?
共支持9种请求方法。分别是:
- OPTIONS:返回服务器对特定资源支持的HTTP方法
- HEAD:只返回头部响应报文信息
- GET:获取特定资源
- POST:提交数据进行处理,可能会导致新的资源建立、已有资源修改。
- PUT:上传数据
- DELETE:删除资源
- TREACE:回显服务器收到的请求
- CONNECT:将服务器作为代理服务器使用
追问:GET和POST有哪些区别?
- POST相对于GET更加安全
- POST不具有幂等性,GET具有幂等性
- GET可以被缓存
- GET发送的数据大小受到浏览器限制,因此小于POST
问:HTTP1.0与HTTP1.1的区别
- HTTP1.1默认支持长连接,而HTTP1.0需要设置Connection: keep-alive参数。
- HTTP1.1支持至发送头部信息(可用于探测是否有权限访问服务器,若返回100,再发送body;若返回401,则无权限,节约带宽)
- HTTP1.1有Host域
- HTTP支持请求资源部分内容。
问:HTTP2.0与HTTP1.0区别
- HTTP2.0采用二进制格式解析,HTTP1.x是文本解析
- HTTP2.0支持多路复用
- HTTP2.0实现了头部信息压缩
- HTTP2.0服务推送,会将请求相关内容一起发送出去
问:转发与重定向区别?
转发是服务器行为,重定向是客户端行为,转发是客户端连接的地址不变,而重定向时会改变
9 在浏览器地址栏键入URL,按下回车之后经历的流程
- DNS解析。服务器会依据URL逐层查询DNS服务器缓存,解析URL域名所对应的IP地址。DNS缓存由近到远依次是:浏览器缓存、系统缓存、路由器缓存、IPS服务器缓存、根域名服务器缓存、顶级域名服务器缓存。从哪个缓存找到对应的IP,就直接返回,不再继续查询
- TCP连接。即:三次握手
- 发送HTTP请求。
- 服务器处理请求并返回HTTP报文。
- 浏览器解析渲染页面。
- 连接结束。即:四次挥手
注:第5步与第6步可同时发生,哪个在前没有特别要求。
10 HTTP状态码
10.1 五种可能取值
- 1××:指示信息——表示请求已接收,继续处理
- 2××:成功——表示请求已被成功接受、理解、接受
- 3××:重定向——要完成请求必须进行更进一步的操作
- 4××:客户端错误——请求有语法错误或请求无法实现
- 5××:服务端有错误——服务器未能实现合法的请求
问:常见状态码
200 OK:正常返回信息
204:无响应内容
206:仅返回部分内容
301:永久重定向
302:临时重定向(请求方法可能会从POST变为GET)
304:自上次访问后,网页未修改
307:临时重定向(请求方法不会改变)
400 Bad Request:客户端请求有语法错误,不能被服务器所理解
401 Unauthorized:请求未经授权,这个状态码必须和WWW-Authenticate报头域一起使用
403 Forbidden:服务器收到请求,但是拒绝提供服务
404 Not Found:请求资源不存在,比如输错了URL
500 Internal Server Error:服务器发生了不可预期的错误
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常。
11 GET请求和POST请求的区别
从三个层面来解答:
- HTTP报文层面:GET将请求信息放在URL,POST放在报文体中(因此GET对数据长度具有长度限制、POST没有限制)
- 数据库层面:GET符合幂等性(即:对数据库的一次操作和多次操作获得数据是一致的)和安全性(即:没有改变数据库中的数据),POST不符合
- 其他层面:GET可以被缓存、被存储(大部分请求被CDN处理了,大大减少服务器压力),而POST不行
12 Cookie和Session
12.1 Cookie简介
- 是由服务器发送给客户端的特殊信息,以文本的形式存放在客户端
- 客户端再次请求的时候,会把Cookie回发
- 服务器接收到后,会解析Cookie生成与客户端相对应的内容
12.2 Cookie的设置以及发送过程
12.3 Session简介
- 服务器端的机制,在服务器上保存的信息(类似于散列形式)
- 解析客户端请求并操作session id,按需保存状态信息
先检查请求里是否包含一个session id,若有,则使用此id获取信息;若没有,则创建一个session,并创建一个对应的session id,并返回回去。
12.4 Session的实现方式
- 使用Cookie来实现(服务器为每个客户端分配一个JSESSIONID,客户端发起请求时,会在Cookie头中携带这个JSESSIONID)
- 使用URL回写来实现(服务器在给客户端发送的每个页面都携带一个JSESSION,客户端点击任意一个连接都会将JSESSIONID带回去)
注:Tomat同时使用这两种方式,若支持Cookie则使用第一种,若不支持,则使用第二种。
12.5 Cookie和Session的区别
- Cookie数据存放在客户的浏览器上,Session数据放在服务器
- Session相对于Cookie更安全
- 若考虑减轻服务器负担,应当使用Cookie
13 HTTPS
13.1 HTTPS简介
保护交换数据隐私、完整性以及身份认证的功能
13.2 SSL(Security Sockets Layer,安全套接层)
- 为网络通信提供安全及数据完整性的一种安全协议
- 是操作系统对外的API,SSL3.0后更名为TLS
- 采用身份验证和数据加密保证网络通信的安全和数据的完整性
13.3 加密方式
- 对称加密:加密和解密都使用同一个密钥
- 非对称加密:加密使用的密钥和解密使用的密钥是不相同的
- 哈希算法:将任意长度的信息转换成固定长度的值,算法不可逆
- 数字签名:证明某个消息或者文件是某人发出/认同的
13.4 HTTPS数据传输流程
- 浏览器将支持的加密算法信息发送给服务器
- 服务器选择一套浏览器支持的加密算法,以证书的形式回发浏览器
- 浏览器验证证书合法性,并结合证书公钥加密信息发送给服务器
- 服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器
- 浏览器解密响应信息,并对消息进行验证,之后进行加密交互数据
13.5 HTTP和HTTPS的区别
- HTTPS需要到CA申请证书,HTTP不需要
- HTTPS密文传输,HTTP明文传输
- 连接方式不同,HTTPS默认使用443端口,HTTP使用80端口
- HTTPS=HTTP+加密+认证+完整性保护,较HTTP安全
13.6 HTTPS真的很安全吗
那倒未必
- 浏览器默认填充http://,请求需要进行跳转,又被劫持的风险
- 可使用HSTS(HTTP Strict Transport Security)优化
14 Socket
14.1 Socket简介
Socket是对TCP/IP协议的抽象,是操作系统对外开放的接口
14.2 Socket通信流程
14.3 Socket相关的面试题
编写一个网络应用程序,有客户端与服务器端,客户端向服务器发送一个字符串,服务器收到
该字符串后将其打印到命令行上,然后向客户端返回该字符串的长度,最后,客户端输出服务器
端返回的该字符串的长度,分别用TCP和UDP两种方式去实现
TCP:
LengthCalculator.java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class LengthCalculator extends Thread{
// 以socket为成员变量
private Socket socket;
LengthCalculator(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 获取socket的输出流
OutputStream os = socket.getOutputStream();
// 获取socket的输入流
InputStream is = socket.getInputStream();
int ch = 0;
byte[] buff = new byte[1024];
// buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组的长度
ch = is.read(buff);
// 将接受流的byte数组转换成字符串,这里获取的内容是客户端发送过来的字符串参数
String content = new String(buff, 0, ch);
System.out.println(content);
// 往输出流里写入获得字符串的长度,回发给客户端
os.write(String.valueOf(content.length()).getBytes());
// 关闭输入输出流以及socket
is.close();
os.close();
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
TCPServer.java
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws Exception{
// 创建socket,并将socket绑定到6499端口
ServerSocket ss = new ServerSocket(6499);
// 死循环,使得socket一直等待并处理客户端发送过来的请求
while (true) {
// 监听65000端口,直到客户端返回连接信息后才返回
Socket socket = ss.accept();
// 获取客户端的请求信息后,执行相关业务逻辑
new LengthCalculator(socket).start();
}
}
}
TCPClient.java
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws Exception{
// 创建socket,并指定连接是本机的端口号为6499的服务器socket
Socket socket = new Socket("127.0.0.1", 6499);
// 获取输出流
OutputStream os = socket.getOutputStream();
// 获取输入流
InputStream is = socket.getInputStream();
// 将要传递给server的字符串参数转换给byte数组,并将数组写入到输入流中
os.write("hello world".getBytes());
int ch = 0;
byte[] buff = new byte[1024];
// buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组的长度
ch = is.read(buff);
// 将接收流的byte数组转换成字符串,这里是从服务端回发回来的字符串参数的长度
String content = new String(buff, 0, ch);
System.out.println(content);
// 关闭输入输出流以及socket
is.close();
os.close();
socket.close();
}
}
先执行Server,后执行Client,结果如下:
UDP:
UDPServer.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServer {
public static void main(String[] args) throws Exception{
// 服务器接受客户端发送的数据报
DatagramSocket socket = new DatagramSocket(6599); // 监听的端口号
byte[] buff = new byte[100]; // 存储从客户端接收到的内容
DatagramPacket packet = new DatagramPacket(buff, buff.length);
// 接受客户端发送过来的内容,并将内容封装进DatagramPacket对象中
socket.receive(packet);
byte[] data = packet.getData(); // 从DatagramPacket对象中获取真正存储的数据
// 将数据从二进制转换成字符串形式
String content = new String(data, 0, packet.getLength());
System.out.println(content);
// 将要发送给客户端的数据转换成二进制
byte[] sendedContent = String.valueOf(content.length()).getBytes();
// 服务器给客户端发送数据报
// 从DatagramPacket对象中获取到数据的来源地址与端口号
DatagramPacket packetToClient = new DatagramPacket(sendedContent,
sendedContent.length, packet.getAddress(), packet.getPort());
socket.send(packetToClient);
}
}
UDPClient.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
public static void main(String[] args) throws Exception{
// 客户端发送数据报给服务端
DatagramSocket socket = new DatagramSocket();
// 要发送给服务端的数据
byte[] buf = "Hello UDP".getBytes();
// 将IP地址封装成InetAddress对象
InetAddress address = InetAddress.getByName("127.0.0.1");
DatagramPacket packet = new DatagramPacket(buf, buf.length, address,
6599);
// 发送数据给服务端
socket.send(packet);
// 客户端接受服务端发送过来的数据报
byte[] data = new byte[100];
// 创建DatagramPacket对象用来存储服务端发送过来的数据
DatagramPacket reveivedPacket = new DatagramPacket(data, data.length);
// 将接受到的数据存储到DatagramPacket对象中
socket.receive(reveivedPacket);
// 将服务端发送过来的数据取过来并打印到控制台
String content = new String(reveivedPacket.getData(), 0,
reveivedPacket.getLength());
System.out.println(content);
}
}
先执行Server,后执行Client,结果如下: