零、基础知识
OSI七层理论体系结构
-
物理层:解决两台主机的通信问题——A往B发送比特流(0101),B能接收到这些比特流。定义了物理设备的标准如网线的类型,光纤的接口类型以及传输介质的传输速率等。
-
数据链路层:由于物理层上的传输的比特流可能会出现错传、误传等,所以数据链路层定义了如何格式化数据即将比特流封装成帧,提供了错误检测。
-
网络层:随着节点的增加,点对点通信是需要经过多个节点的,如何找到目标节点,如何找到最优路径变成为了首要需求。所以出现了网络层,主要目的是将网络地址翻译成对应的物理地址,分组传输、路由选择,本层的传输单位是数据报(分组),本层需要注意的TCP/IP协议中的TCP协议。
-
传输层:随着网络需要的进一步扩大,通信过程中需要传输大量的数据,网络可能会发生中断,为了保证传输大量文件时的准确性,需要对发送的数据进行切分,切分成一个个的segment进行发送,考虑如何在接受方拼接切分的segment组成完整的数据,以及发现丢失segment时该如何处理,需要注意的协议TCP、UDP。
-
会话层:不同机器上的用户之间建立以及管理会话。用于保证应用程序自动收发包和寻址。
-
表示层:信息的语义语法,加密解密,转换翻译,压缩解压缩。
-
应用层:规定双方必须使用固定长度的消息头,且消息头必须记录消息长度等信息。需要注意的是TCP/IP协议中的HTTP协议。
TCP/IP四层模型
是OSI的一种实现,包括应用层、运输层、网际层和网络接口层。
一、TCP的三次握手
传输控制协议TCP:
- 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 将应用层的数据流分割成报文段并发送给目标节点的TCP层。
- 数据包都有序号,对方收到则发送ACK确认,未收到则重传。
- 使用校验和来检验数据在传输过程中是否有误。
序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。
确认号字段——占 4 字节,是期望收到对方的下一个报文段的数据的第一个字节的序号。
确认 ACK —— 只有当 ACK = 1 时确认号字段才有效。
同步 SYN —— 同步 SYN = 1 表示这是一个连接请求或连接接受报文。
终止 FIN (FINish) —— 用来释放一个连接。FIN = 1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。
TCP的三次握手回答:
-
第一次握手:建立连接时,客户端发送请求SYN包,SYN=1,seq=x,客户端进入SYN_SENT状态,等待服务器的确认。
-
第二次握手:服务器收到SYN报文段,需要对这个SYN报文段进行确认,设置ack=x+1,同时自己还要发送SYN请求信息给客户端,所以SYN=1,seq=y。服务器将上述信息放到SYN+ACK报文段中一并发给客户端,此时服务器进入SYN_RECV状态。
-
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(其中ack=y+1),此包发送完毕后,客户端和服务器进入ESTABLISHED状态,完成三次握手。
二、TCP的四次挥手
TCP的四次挥手回答:TCP采用四次挥手来释放连接。
- 第一次挥手:Client发送一个FIN包,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态;
- 第二次挥手:Server收到FIN包后,发送一个ACK包给Client,其中确认序号ack为收到序号+1,Server进入CLOSE_WAIT状态;
- 第三次挥手:Server发送一个FIN包用来关闭Server到Client的数据传送,Server进入LAST_ACK状态;
- 第四次挥手:Client收到FIN包后,Client进入TIME_WAIT状态,接着发送一个ACK包给Server,确认序号ack为收到序号+1,Server进入CLOSED状态,完成四次挥手。
为什么有TIME_WAIT状态?
- 确保有足够的时间让对方收到ACK包
- 避免新旧连接混淆
为什么需要四次挥手才能断开连接?
因为全双工,发送方和接收方都需要FIN报文和ACK报文。
三、TCP和UDP的区别
UDP报文示意图:
UDP的特点:
- 面向非连接的
- 不维护连接状态,支持同时向多个客户端传输相同的消息
- 数据包报头只有8个字节(源端口、目的端口、长度、校验和),额外开销较小
- 吞吐量只受限于数据生成速率、传输速率以及机器性能
- 尽最大努力交付,不保证可靠交付,不需要维持复杂的连接状态表
- 面向报文,不对应用程序提交的报文进行拆分或者合并
TCP和UDP的区别回答:
- TCP是面向连接的,UDP是无连接的
- TCP比UDP是更可靠的(握手和确认重传机制)
- TCP是有序的,UDP是无序的。
- TCP速度相比UDP慢(需要建立连接)
- TCP相比UDP的开销更大(TCP首部20个字节,UDP首部8个字节)
四、HTTP与HTTPS
HTTP是在TCP/IP四层模型中的协议,超文本传输协议HTTP主要特点如下:
- 支持客户端/服务器模式
- 简单快速
- 灵活
- 无连接
- 无状态
面:在浏览器地址栏中输入URL,按下回车之后经历的流程
回答:
- DNS解析:首先浏览器会根据URL逐层查询DNS服务器缓存,解析URL中域名所对应的IP地址,DNS缓存从近到远依次是:浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,当找到IP后,直接返回无需查询下一层缓存。
- TCP连接:根据IP和端口(默认80)和服务器建立TCP连接,通过TCP的三次握手。
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析响应报文并渲染页面
- 连接结束,浏览器释放TCP连接,通过TCP的四次挥手。
面:说说常见的HTTP状态码。
答:HTTP响应状态码有5种可能的取值:
- 1xx:指示信息--表示请求已接收,继续处理
- 2xx:成功--表示请求已被成功接收、理解、接受
- 3xx:重定向--要完成请求必须进行更进一步的操作
- 4xx:客户端错误--请求有语法错误或请求无法实现
- 5xx:服务器端错误--服务器未能实现合法的请求。
面:GET请求和POST请求的区别
- Http报文层面:GET请求将请求信息放在URL中,POST将请求信息放在报文体中
- 数据库层面:GET符合幂等性(对数据库的一次操作或多次操作获得的结果是一致的)和安全性(对数据库的操作没有改变数据库的数据),POST不符合。
- 其他层面:GET请求可以被缓存、被存储,而POST不行
Cookie和Session的区别
Cookie简介:
- 是由服务器发给客户端的特殊信息,以文本的形式存放在客户端
- 客户端再次请求时,会把Cookie回发
- 服务器接收到后,会解析Cookie生成与客户端相对应的内容
Cookie的设置以及发送过程如下:
Session简介:
- 服务器端的机制,在服务器上保存的信息
- 解析客户端请求并操作session id,按需保存状态信息
Session的实现方式:
-
使用Cookie实现
-
使用URL回写来实现(返回给浏览器的所有链接中都携带JSESSIONID参数)
面:Cookie和Session的区别
- Cookie数据存放在客户的浏览器上,Session数据放在服务器上
- Session相对于Cookie更安全
- 若考虑减轻服务器的负担,应当使用Cookie
HTTPS
HTTPS相对于HTTP就是多了一层SSL(Security Sockets Layer,安全套接层)。SSL定义如下:
- 为网络通信提供安全及数据完整性的一种安全协议
- 是操作系统对外的API,SSL3.0后更名为TLS
- 采用身份验证和数据加密保证网络通信的安全和数据的完整性
加密的方式:
- 对称加密:加密和解密都使用同一密钥
- 非对称加密:加密使用的密钥和解密使用的密钥是不同的
- 哈希算法:将任意长度的信息转为固定长度的值算法不可逆
- 数字签名:证明某个消息或者文件是某人发出/认同的
HTTPS数据传输流程:
- 浏览器将支持的加密算法信息发送给服务器
- 服务器选择一套浏览器支持的加密算法,以证书的形式回发浏览器
- 浏览器验证证书合法性,并结合证书公钥加密信息发送给服务器
- 服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器
- 浏览器解密响应消息,并对消息进行验证,之后进行加密交互数据
面:HTTP和HTTPS的区别
- HTTPS需要到CA申请证书,HTTP不需要
- HTTPS密文传输,HTTP明文传输
- 连接方式不同,HTTPS默认使用443端口,HTTP使用80端口
- HTTPS=HTTP+加密+认证+完整性保护,较HTTP安全
五、Socket
我们知道如果两个进程如果要进行通信的话,那么首先应该能够唯一地标识这2个进程,在本地进程中我们可以使用PID唯一标识进程,那么在网络中呢?在网络中,各个主机的PID可能会重复,因此光靠PID就不能唯一标识进程了。前面我们知道TCP/IP协议中的IP能够唯一标识主机,而TCP/IP中的端口号又能唯一标识相应主机的唯一进程,因此可以通过IP+协议+端口号来唯一标识网络中的一个进程。Socket就是通过这种方式来进行网络中进程的通信的。
Socket是对TCP/IP协议的抽象,是操作系统对外开放的接口。
Socket通信流程图如下:
Socket相关的面试题:编写一个网络应用程序,有客户端和服务端,客户端向服务器发送一个字符串,服务器收到该字符串后将其打印到命令行上,然后向客户端返回给字符串的长度,最后客户端输出服务端返回的该字符串的长度,分别用TCP和UDP两种方式实现。
- TCP方式
package com.yunche.socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName: TCPServer
* @Description: TCP服务端
* @author: yunche
* @date: 2019/03/11
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//创建socket,并将socket绑定到65000端口
ServerSocket serverSocket = new ServerSocket(65000);
ExecutorService executor = Executors.newCachedThreadPool();
while (true) {
//监听65000端口,直到客户端返回连接信息才返回
Socket socket = serverSocket.accept();
//获取客户端的的请求信息,执行相关的业务逻辑
executor.execute(()->{
try {
service(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
private static void service(Socket socket) throws IOException {
//获取socket的输入流
InputStream is = socket.getInputStream();
//获取socket的输出流
OutputStream os = socket.getOutputStream();
byte[] bytes = new byte[1024];
//读取的字节数
int ch = is.read(bytes);
String content = new String(bytes, 0, ch);
//输出收到的字符串
System.out.println(content);
//往输出流中写服务端收到的客户端的字符串的长度
os.write(String.valueOf(content.length()).getBytes());
//关闭流以及socket
is.close();
os.close();
socket.close();
}
}
package com.yunche.socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* @ClassName: TCPClient
* @Description: TCP客户端
* @author: yunche
* @date: 2019/03/11
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
//创建socket,并指定连接的是本机的端口为65000的服务器socket
Socket socket = new Socket("localhost", 65000);
//获取输出流
OutputStream os = socket.getOutputStream();
//获取输入流
InputStream is = socket.getInputStream();
//将要传递给server的字符串参数转换成byte数组,并将数组写入到输出流中
os.write("hello world".getBytes());
//用来读取输入的内容,即从服务器返回的字符串长度
byte[] bytes = new byte[1024];
int ch = is.read(bytes);
String len = new String(bytes, 0, ch);
System.out.println(len);
//关闭相应的流以及socket
is.close();
os.close();
socket.close();
}
}
- UDP方式
package com.yunche.socket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName: UDPServer
* @Description:
* @author: yunche
* @date: 2019/03/11
*/
public class UDPServer {
public static void main(String[] args) throws IOException {
//创建一个底层是UDP的socket
DatagramSocket socket = new DatagramSocket(65001);
//存储从客户端接受到的内容
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
//接受客户端发送过来的内容,并将内容封装进DatagramPacket对象中
socket.receive(packet);
//从DatagramPacket对象中获取到真正存存储的数据
byte[] data = packet.getData();
//将数据从二进制转换成字符串
String content = new String(data, 0, packet.getLength());
System.out.println(content);
//将要发送给客户端的数据转为二进制
byte[] len = String.valueOf(content.length()).getBytes();
//服务端给客户端发送数据报
//从DatagramPacket对象中获取到数据的来源地址与端口号
DatagramPacket packetToClient = new DatagramPacket(len, len.length, packet.getAddress(),packet.getPort());
socket.send(packetToClient);
}
}
package com.yunche.socket;
import java.io.IOException;
import java.net.*;
/**
* @ClassName: UDPClient
* @Description: UDP客户端
* @author: yunche
* @date: 2019/03/11
*/
public class UDPClient {
public static void main(String[] args) throws IOException {
//客户端发送数据报给服务端
DatagramSocket socket = new DatagramSocket();
byte[] bytes = "Hello world".getBytes();
//将IP地址封装成InetAddress对象
InetAddress address = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address,65001);
//发送数据给服务端
socket.send(packet);
//客户端接受从服务端返回的数据报
byte[] data = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(data, data.length);
socket.receive(receivePacket);
String len = new String(data, 0, receivePacket.getLength());
System.out.println(len);
}
}