什么是TCP/IP五层模型?它们的作用是啥?基于TCP/IP实现的应用(层协议)有哪些?
TCP/IP五层模型,从上向下分别是:
- 应用层:应用程序本身,应用层的作用是负责应用程序之间的数据通讯。不同的网络应用需要哦不同的应用层协议,比如发电子邮件需要SMTP、文件传输需要FTP协议、网络远程访问需要哦Telnet协议
- 传输层:传输层的作用是负责两台主机之间(从源地址到目的地)的数据传输。如传输控制协议(TCP)能够保证数据可靠的从源主机发送到目标主机
- 网络层:网络层的作用是负责网络上的地址管理和路由选择。在数据通讯时,可以选择很多条路径到目标地址
- 数据链路层:数据链路层的作用是负责设备之间的数据帧传送和识别的。数据在传输时现需要经过多个设备进行数据传输,而数据链路层就是i负责相邻设备间的数据传输和识别的。数据链路层可以完全消除网络层和物理层之间的不同,将数据在链路层进行有效的识别和传输
- 物理层:物理层的作用是负责将数据转换成信号,再将信号转换成数据。转换方法因通讯媒体不同而不同,所以没有特定的协议
使用TCP/IP实现的应用有哪些
网络上的大部分通讯协议都是基于TCP/IP模型实现的,例如一下这些常见的应用层协议:
- HTTP:一种用于传输超文本的协议,常用于Web程序之间的通讯
- HTTPS:基于TLS/SSL安全协议对HTTP进行加密和身份验证,用于保护Web通信的安全性
- DNS:用于将域名解析成IP地址,将域名和IP进行映射
说一下DNS的执行流程?
DNS的执行流程如下:
- 用户发起域名查询:用户在浏览器中输入网址的时候,浏览器首先尝试解析这个域名
- 本地DNS解析器查询本地缓存:本地DNS计息期首先检查本地缓存中是否存有与所查询域名相关的IP地址记录。如果有,直接返回
- 向递归DNS服务器发送查询请求:如果本地没有所查询的IP地址记录,本地的DNS解析及将向配置的递归DNS服务器发送查询请求
- 递归DNS服务器查询根域名服务器:如果递归DNS服务器也没有所需的IP地址记录,它会向根域名服务器发送查询请求。根域名服务器是域名系统的顶层服务器,扶着管理顶级域名服务器IP地址
- 根域名服务器返回顶级域名服务器的IP地址:根域名服务器收到查询请求后,会返回负责所查询域名顶级域的顶级域名服务器的IP地址,例如.com、.net等
- 递归DNS服务器查询顶级域名服务器:递归DNS服务器接收到根域名服务器返回的顶级域名服务器IP地址后,将向该顶级域名服务器发送查询请求
- 顶级域名服务器返回权威域名服务器的IP地址:顶级域名服务器接收到查询请求后,会返回负责权威域名服务器的IP地址,如example.com域的域名服务器的IP地址
- 递归DNS服务器查询权威域名服务器:递归DNS服务器继续向权威域名服务器发送查询请求
- 权威域名服务器返回所查询域名的IP地址:权威域名服务器收到查询请求后,会返回所查询域名的IP地址记录
- 递归DNS服务器将IP地址记录返回给本地DNS解析器:最后,递归DNS服务器将获取到的IP地址记录返回给本地DNS解析器
- 本地DNS解析器将IP地址记录返回给用户设备:最终,本地DNS解析器将获取到的IP地址记录返回给用户设备,用户设备可根据该IP地址访问所查询域名的服务
总结来说:它包括了从本地DNS缓存查询开始,逐级向根域名服务器、顶级域名服务器、权威域名服务器的查询过程,最终返回所查询域名的IP地址给用户设备
根域名服务器、顶级域名服务器和权威域名服务器有什么区别?
根域名服务器在整个 DNS 体系中起着导航作用,帮助其他 DNS 服务器找到正确路径;顶级域名服务器针对某一类顶级域名进行管理;而权威域名服务器则具体负责某个域名区域内的详细信息解析工作,它们的关系如图所示:
在浏览器中输入URL地址之后会执行哪些流程?
执行流程如下:
- URL解析:浏览器首先会解析用户输入的URL地址,提取出协议、主机名、端口号、路径等信息
- DNS解析:浏览器将域名解析成IP地址
- 建立TCP连接:浏览器根据URL中的协议(通常是HTTP或HTTPS)建立与服务器的TCP连接。如果是HTTPS,还需要进行SSL/TLS握手过程建立安全连接
- 发送HTTP请求:浏览器向服务器发送HTTP请求,包括请求方法(GET、POST等)、请求头、请求体等
- 服务器处理请求:服务器接收到浏览器发送的HTTP请求之后,根据请求的内容执行相应的处理,可能涉及到查询数据库、处理业务逻辑等
- 服务器返回响应:服务器处理完请求之后,会返回HTTP响应给浏览器,响应包括状态码(如200表示成功)、响应头(Content-Type、Set-Cookie等)、响应体(网页内容)等信息
- 浏览器渲染页面:浏览器接收到服务器返回的HTTP相应之后,会根据相应的内容开始渲染页面,包括解析HTML、CSS、JS等文件,并且把它们显示在浏览器窗口
- 关闭TCP连接:页面加载完成之后,浏览器会关闭与服务器的TCP连接,释放资源
GET请求和POST请求有什么区别?POST请求更安全吗?
GET请求和POST请求区别如下:
- 数据传输方式不同:GET请求数据以明文形式显示在URL中,可以被轻松地获取、收藏、修改;POST请求数据被封装在请求体中,相当于GET请求来说更难被直接获取或修改
- 请求长度限制不同:GET请求受浏览器和服务器对URL长度的先致,一般不能超过2KB;POST请求理论上没有长度限制,但是实际啥啊是哪个受服务器地限制,大多数服务器都会设置请求体大小地上限
- 回滚和刷新不同:GET请求可以直接进行回退和刷新,不会对用户和程序产生任何影响;而POST请求如果直接回滚和刷新会将数据再次提交
- 使用场景不同:GET请求适用于获取资源的新消息,比如查看网页、获取图片等查询操作;POST请求适合用于向服务器提交数据并产生副作用的操作,比如提交表单,上传文件等数据提交操作
POST请求相比GET请求更加安全,但是POST请求地数据被封装在请求体中,用一些抓包工具就可以直接获取到其内容。只要是HTTP协议的,都不是安全的
301和302有什么区别?为什么不建议使用302?
301和302都是用于请求重定向地状态码,所谓的请求重定向是指访问某个URL的时候,会自动跳转另一个URL。但是它们,一个表示请求的资源已经被永久性(301)的移动到另一个位置,而302是临时的移动到另一个位置,客户端应该通过重定向到新的位置获取资源。它们主要的区别如下:
- 行为不同:当服务器返回301状态码的时候,表示请求的资源已经永久性的移动到了新的位置;当服务器返回302的时候,表示请求资源暂时性地移动到了新的位置
- 后续操作不同:客户端在收到301响应的时候,后续应该更新书签或链接,将原来的替换成新的URL,并且以后的请求都是直接使用新的URL获取资源;客户端收到302响应的时候,后续应该继续使用原来的URL请求资源
- 搜索引擎处理不同:搜索引擎通常会将 301 重定向视为对新 URL 的引用,将之前的 URL 的搜索排名改为新的URL;搜索引擎通常不会将 302 重定向视为对新 URL 的引用,不会将之前的 URL 的搜索排名传递给新的URL
为什么不建议使用302
- SEO影响:302状态码暗示着资源的临时移动,搜索引擎不会更新其索引以反映新的搜索引擎通常不会更新其索引以反映新的URL。这意味着原始URL的排名和权重可能会受到影响,因为搜索引擎会继续将原始URL视为资源的主要位置
- 用户体验问题:302 重定向可能导致页面加载速度变慢,对用户体验产生负面影响。每次发生重定向,都会增一次请求和响应的网络开销,延迟页面的加载时间
- 安全性问题:恶意攻击者可以利用 302 重定向进行网络钓鱼攻击或重定向劫持。他们可能会伪造 302 重定向使用户被重定向到恶意站点,诱导用户泄露敏感信息或下载恶意软件
请求转发和请求重定向有什么区别?举个例子通俗易懂的说明一下
请求转发(Forword)和请求重定向(Redirect)区别如下:
- 定义不同
- 请求转发:发生在服务器程序内部,当服务器端收到一个客户端的请求之后,会先将请求转发给目标地址,再将目标地址返回结果转发给客户端
- 请求重定向:服务器端接受到客户的请求之后,会给客户端返回一个临时的响应头,这个临时响应头中记录了,客户端需要再次发送请求(重定向)地URL地址,客户端在收到了地址之后,会将请求发送到新的地址,这个就是请求重定向
- 请求方不同
- 请求转发是服务器端的行为,服务器端代替客户端发送请求,并将结果返回给客户端;而重定向是客户端的行为
- 数据共享不同
- 请求转发是服务端实现的,所以整个执行流程中,客户端(浏览器端)只需要发送一次请求,因此整个交互的过程中使用的都是一个Request请求对象和一个Response响应对象,所以整个请求过程中,请求和返回的数据是共享的;而请求重定向客户端发送两次完全不同的请求,所以两次请求数据是不同的
- 最终的URL地址不同
- 请求转发是服务器端实现的,再将结果返回给客户端,所以整个请求的过程中的URL地址是不变的;而请求重定向是服务器端告诉客户端“你去另一个地方访问”,所以浏览器会重新再发一次请求,因此客户端最终显示的URL也为最终跳转的地址,而非刚开始的请求地址,所以URL地址发生了改变
- 代码实现不同
请求转发:
@RequestMapping("/fw")
public void forward(HttpServletReguest reguest, HttpSeryletResponse response) throws ServletException,IOException{request.getRequestDispatcher("/index.html").forward(request, response);
}
请求重定向:
@RequestMapping("/rt")
public void redirect(HttpServletRequest request, HttpServletResponse response) throws
IOException{response.sendRedirect("/index.html");
}
为什么要使用HTTPS?HTTP存在什么问题?
HTTP在互联网通信中起着至关重要的作用,但是它存在一些安全性的问题,所以需要使用HTTPS来增强网络通信的安全。HTTP主要存在以下问题:
- 数据明文传输:HTTP默认情况下是以明文形式传输的,这意味着任何在网络路径上的中间节点(路由器、代理服务器)都能够捕获和查看用户信息
- 缺乏完整性的验证:由于HTTP不提高数据完整性的校验机制,恶意第三方可以轻易的篡改传输中的数据内容,接收方也无法察觉
- 身份验证缺失:使用HTTP时,不验证通讯方的真实身份,可能会遭到伪装。也就是所谓的“中间人攻击”,即攻击者冒充合法服务器,截取并篡改通信内容
HTTPS具有以下的优点:
- 加密:对客户端和服务器端之间的通信内容进行加密,防止数据被窃取和监听
- 认证:通过证书颁发机构(CA)签发的数字证书来验证服务器的身份,保证用户与正确的服务器建立连接
- 完整性:通过消息认证码(MAC)或者散列函数对数据进行完整性校验,防止数据在传输过程中被篡改
什么是中间人攻击?如何解决中间人攻击?
中间人攻击是指,正常情况下本应该是客户端和服务器进行交互的,但是在中间多出了一个中间人,盗取和篡改双方通讯的内容
所以说中间人攻击主要是有两个问题:
- 身份认证问题
- 数据篡改问题
如何解决中间人攻击
使用HTTPS就可以完美的解决中间人功能估计,HTTPS使用以下两种方式来解决:
- 解决身份认证问题:使用CA数字则行数
- 解决数据篡改问题:使用加密通讯
CA认证证书:HTTPS 解决信任问题采用的是数字证书的解决方案,也就是服务器在创建之初,会先向一个大家都认可的第三方平台申请一个可靠的数字证书,然后在客户端访问(服务器端)时,服务器端会先给客户端一个数字证书,以证明自己是一个可靠的服务器端,而非中间人
加密通讯:使用加密通讯之后,第一次通讯的密钥只有在真正的服务器端报错,所以即使中间人拦截了信息,因为是密文并且没有密钥,所以是破解不了的
说一下HTTPS执行流程?
HTTPS是一种在HTTP协议的基础上通过SSL/TLS协议提供了加密处理和身份认证的网络协议,用于确保通信内容的安全性。HTTPS执行的流程如下:
- 客户端请求连接:用户在浏览器中输入HTTPS网址并且发起连接请求。浏览器验证URL合法性,并确认时HTTPS请求
- 服务器响应并返回CA证书:
- 服务器接收到请求之后,返回其数字证书(CA证书),其中包含了服务器的身份信息以及公钥
- 浏览器验证服务器证书的有效性,包含检查证书是否过期、是否由受信任的CA签发、域名是否匹配
- 密钥协商与握手阶段:
- 如果证书有效,浏览器生成一个随机数作为会话密钥(对称密钥)的一部分
- 客户端使用服务器证书中的公钥加密整个会话密钥和其他参数(加密套件、随机数等),然后发给服务器
- 这个过程可能涉及多种握手模式,例如 RSA、DH/ECDH 密钥交换算法等
- 共享会话密钥:
- 服务器接收到加密后的信息之后,用私钥解密得到会话的密钥
- 此时,客户端和服务端都拥有了同一份会话密钥,但是该密钥在网络传输的过程中并没有明文出现
- 数据传输阶段:
- 使用协商好的会话密钥,双方开始使用TLS/SSL协议进行对称加密的数据传输
- 所有的应用层数据(比如HTTP请求和响应的消息体)都会被这个会话密钥加密,从而保证数据的机密性和完整性
- 完整性校验:在数据传输期间,还会使用哈希算法以及消息认证码(MAC)来确保数据未被篡改
- 关闭连接:当通信完成后,通过TLS/SSL的四次挥手或者其他安全机制结束会话,释放资源
什么时加密套件
加密套件是一组加密算法、密钥交换算法和摘要算法的集合,用于加密和认证网络通信的数据。它通常由以下几部分组成:
-
密钥交换算法:用于在通信双方之间安全地交换密钥的算法,例如RSA、Diffie-Hellman等
-
加密算法:用于对通信中的数据进行加密的算法,例如AES、3DES等
-
摘要算法:用于对通信中的数据进行摘要计算,以确保数据的完整性,例如SHA-256、SHA-1等
加密套件的选择对通信的安全性至关重要。通常,服务器和客户端在SSL/TLS握手过程中协商选择一种适当的加密套件来确保通信的安全性。这种协商过程可以确保通信双方都能够支持最强大的加密算法和最安全的密钥交换算法,以提供最高级别的安全性
TCP为什么要三次握手?二次或四次握手行不行?
TCP采用三次握手流程如下:
- 客户端发送连接请求(SYN):客户端向服务器发送一个SYN标志的TCP数据包,表示客户端要建立连接,并指明客户端的初始序列号
- 服务器回复连接确认和连接请求(SYN+ACK):服务器收到客户端的连接请求之后,向客户端发送一个SYN和ACK标识的数据包,表示服务器同意建立连接,并指明服务器的初始序列号
- 客户端回复连接确认(ACK):客户端收到服务器的连接确认和连接请求之后,向服务器发送一个ACK标志的TCP数据包,表示客户端接受了服务器的确认,连接建立完成
三次握手的目的有以下几点:
- 双方能够确认对方的能力:在三次握手的过程中,客户端和服务器端都能够确认对方的能力和状态,确保双方可以正常通信
- 防止过时连接请求的影响:如果服务器收到了过时连接请求,但是客户端并没有真正的发起连接,那么服务器在收到第三次握手的确认之前不会分配这个连接请求,避免资源浪费
二次握手或四次握手并不是TCP协议所采用的标准握手流程,它们都存在一些问题:
-
二次握手:在这种情况下,客户端发送连接请求后,服务器直接发送确认,这样客户端并不能确认服务器的状态,容易造成连接的不稳定性和不可靠性。
-
四次握手:在这种情况下,多了一次确认的过程,增加了通信的开销,而且不必要,因为三次握手已经足够确保连接的可靠性。
因此,TCP采用三次握手的机制是为了在可靠地建立连接的同时,保证通信的高效性和可靠性
TCP丢失的消息会一直重传吗?说一下TCP的超时重传策略是啥?
造成消息丢失和超时重传的场景有以下两种:
- 发送消息时丢失
- ACK确认消息发送丢失
无论哪种情况,TCP不会一直重传丢失的消息,因为这样如果对方真正的下线,会造成系统资源浪费。所以TCP设计了两种重传策略:
- 动态重传时间:每次传递时间翻倍,例如第一次1s,第二次2s,第四次4s
- 最大重传次数:TCP如果超过一定的重试次数,那么就会强制断开连接,不会继续重传
什么是TCP粘包问题?如何解决?
TCP粘包和半包是数据传输中比较常见的问题。所谓的粘包问题就是指在数据传输的时候,在一条消息中读取到了另一条消息的部分数据,如下图:
半包是指接收端只收到了部分的数据,而非完整的数据的情况,如下图:
大部分情况下我们都把粘包问题和半包问题看成同一个问题,所以下文就用粘包问题来替代粘包和半包问题
为什么会有粘包问题
粘包问题发生在TCP/IP协议中,因为TCP是面向连接的传输协议,它是以流的形式传输数据的,而流数据是没有明确开始和结尾的边界的,所以就会出现粘包问题
粘包问题演示
接下来我们用代码来演示一下粘包和半包问题,为了演示的直观性,我会设置两个角色:
- 服务器端用来接收消息
- 客户端用来发送一段固定的消息
服务端代码实现:
import java.io.*;
import java.net.*;public class Server {private static final int BYTE_LENGTH = 20;public static void main(String[] args) throws IOException {// 创建 Socket 服务器ServerSocket serverSocket = new ServerSocket(8888);// 获取客户端连接Socket clientSocket = serverSocket.accept();// 得到客户端发送的流对象try (InputStream inputStream = clientSocket.getInputStream()) {while (true) {// 循环获取客户端发送的信息byte[] bytes = new byte[BYTE_LENGTH];// 读取客户端发送的信息int count = inputStream.read(bytes, 0, BYTE_LENGTH);if (count > 0) {// 成功接收到有效消息并打印System.out.println("接收到客户端的信息是:" + new String(bytes, 0, count));}}}}
}
客户端代码实现:
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;public class Client {public static void main(String[] args) throws IOException {String serverAddress = "127.0.0.1";int port = 8888;try (Socket socket = new Socket(serverAddress, port);PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {String message = "hello,world";OutputStream outputStream= socket.getOutputStream();for (int i = 0; i < 10; i++) {outputStream.write(message.getBytes());}}}
}
程序执行结果:
此时我们发现出现了粘包问题,正常应该是直接输出10次hello world 才对
解决方案
粘包问题的常见解决方案有以下三种:
- 固定数据大小:发送方和接收方固定发送消息的大小,当字符长度不够的时候用空字符弥补,有了固定大小就知道每条消息的边界了
- 自定义数据协议(定义数据边界):在TCP协议的基础上封装上一层自定义数据协议,在自定义的数据协议中,包含数据头(存储数据的大小)和数据的具体内容,这样服务端的得到的数据头就可以知道数据的具体长度,也就没有粘包问题
- 以特殊字符结尾:比如以“/n”字符结尾,这样就可以直到数据的具体边界,可以避免粘包问题(推荐使用)
解决方案一:固定数据大小
收、发固定大小的数据,服务端实现代码:
import java.io.*;
import java.net.*;public class Server {private static final int BYTE_LENGTH = 1024;public static void main(String[] args) throws IOException {// 创建 Socket 服务器ServerSocket serverSocket = new ServerSocket(8888);// 获取客户端连接Socket clientSocket = serverSocket.accept();// 得到客户端发送的流对象try (InputStream inputStream = clientSocket.getInputStream()) {while (true) {// 循环获取客户端发送的信息byte[] bytes = new byte[BYTE_LENGTH];// 读取客户端发送的信息int count = inputStream.read(bytes);if (count > 0) {// 成功接收到有效消息并打印System.out.println("接收到客户端的信息是:" + new String(bytes, 0, count));}}}}
}
客户端实现代码:
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;public class Client {private static final int BYTE_LENGTH = 1024;public static void main(String[] args) throws IOException, InterruptedException {String serverAddress = "127.0.0.1";int port = 8888;String message = "hello,world";try (Socket socket = new Socket(serverAddress, port)) {OutputStream outputStream= socket.getOutputStream();byte[] bytes = new byte[BYTE_LENGTH];int idx= 0;for(byte b:message.getBytes()){bytes[idx]= b;idx++;}for (int i = 0; i < 10; i++) {outputStream.write(bytes,0,BYTE_LENGTH);}}}
}
运行结果:
后面是字符编码的问题
优缺点分析
从以上代码可以看出,虽然这种方式可以解决粘包问题,但这种固定数据大小的传输方式,当数据量比较小时会使用空字符来填充,所以会额外的增加网络传输的负担,因此不是理想的解决方案
解决方案二:自定义请求协议
这种解决方案的实现思路是将请求的数据封装成两部分:消息头(发送的数据大小)+消息体(发送的具体数据),如下图:
此解决方案的实现为以下三部分:
- 编写一个消息的封装类
- 编写客户端
- 编写服务器端
消息的封装类:
import java.nio.charset.StandardCharsets;public class CustomProtocol {private static final int HEAD_SIZE = 8; // 假设消息头固定为8个字节public static byte[] toBytes(String context) {// 协议体 byte 数组byte[] bodyByte = context.getBytes(StandardCharsets.UTF_8);int bodyByteLength = bodyByte.length;// 最终封装对象byte[] result = new byte[HEAD_SIZE + bodyByteLength];// 借助 NumberFormat 将int 转换为 byte[]NumberFormat numberFormat = NumberFormat.getNumberInstance();numberFormat.setMinimumIntegerDigits(HEAD_SIZE);numberFormat.setGroupingUsed(false);// 协议头 byte 数组byte[] headByte = numberFormat.format(bodyByteLength).getBytes();// 封装协议头System.arraycopy(headByte, 0, result, 0, HEAD_SIZE);// 封装协议体System.arraycopy(bodyByte, 0, result, HEAD_SIZE, bodyByteLength);return result;}public int getHeader(InputStream inputStream) throws IOException {int result = 0;byte[] bytes = new byte[HEAD_SIZE];inputStream.read(bytes, 0, HEAD_SIZE); // 得到消息体的字节长度result = Integer.valueOf(new String(bytes));return result;}
}
客户端代码:
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Random;public class MySocketClient {public static void main(String[] args) throws IOException {// 启动 Socket 并尝试连接服务器Socket socket = new Socket("127.0.0.1", 9093);// 发送消息合集(随机发送一条消息)final String[] messages = {"hello world"};// 创建协议封装对象SocketPacket socketPacket = new SocketPacket();try (OutputStream outputStream = socket.getOutputStream()) {// 给服务器端发送 10 次消息for (int i = 0; i < 10; i++) {// 随机发送一条消息String msg = messages[new Random().nextInt(messages.length)];// 将内容封装为:协议头+协议体byte[] bytes = socketPacket.toBytes(msg);// 发送消息outputStream.write(bytes, 0, bytes.length);outputStream.flush();}}}
}
服务器端代码:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class MySocketServer {public static void main(String[] args) throws IOException {// 创建 Socket 服务器端ServerSocket serverSocket = new ServerSocket(9093);// 使用线程池处理更多的客户端ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));while (true) {// 获取客户端连接Socket clientSocket = serverSocket.accept();// 客户端消息处理threadPool.submit(() -> {processMessage(clientSocket);});}}// 客户端消息处理private static void processMessage(Socket clientSocket) {// Socket 封装对象SocketPacket socketPacket = new SocketPacket();// 获取客户端发送的消息对象try (InputStream inputStream = clientSocket.getInputStream()) {while (true) {// 获取消息头(也就是消息体的长度)int bodyLength = socketPacket.getHeader(inputStream);// 消息体 byte 数组byte[] bodyByte = new byte[bodyLength];// 每次实际读取字节数int readCount = 0;// 消息体赋值下标int bodyIndex = 0;// 循环接收消息头中定义的长度while (bodyIndex <= (bodyLength - 1) && (readCount = inputStream.read(bodyByte, bodyIndex, bodyLength)) != -1) {bodyIndex += readCount;}bodyIndex = 0;// 成功接收到客户端的消息并打印System.out.println("接收到客户端的信息:" + new String(bodyByte));}} catch (IOException e) {e.printStackTrace();}}
}
运行结果:
优缺点分析:
此解决方案虽然可以解决粘包问题,但消息的设计和代码的实现复杂度比较高,所以也不是理想的解决方案
解决方案三:特殊字符结尾
以特殊字符结尾就可以知道流的边界了,它的具体实现是:使用Java 中自带的 BufferedReader 和Bufferedwriter ,也就是带缓冲区的输入字符流和输出字符流,通过写入的时候加上 \n 来结尾,读取的时候使用 readLine按行来读取数据,这样就知道流的边界了,从而解决了粘包的问题
服务器端代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ServerSocketV3 {public static void main(String[] args) throws IOException {// 创建 Socket 服务器端ServerSocket serverSocket = new ServerSocket(9092);// 使用线程池处理更多的客户端ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));while (true) {// 获取客户端连接Socket clientSocket = serverSocket.accept();// 消息处理threadPool.submit(() -> {processMessage(clientSocket);});}}// 消息处理private static void processMessage(Socket clientSocket) {// 获取客户端发送的消息流对象try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {while (true) {// 按行读取客户端发送的消息String msg = bufferedReader.readLine();if (msg != null) {// 成功接收到客户端的消息并打印System.out.println("接收到客户端的信息:" + msg);}}} catch (IOException ioException) {ioException.printStackTrace();}}
}
客户端代码:
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;public class ClientSocketV3 {public static void main(String[] args) throws IOException {// 启动 Socket 并尝试连接服务器Socket socket = new Socket("127.0.0.1", 9092);final String message = "hello world";// 发送消息try (BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {// 给服务器端发送 10 次消息for (int i = 0; i < 10; i++) {// 注意:结尾的\n 不能省略,它表示按行写入bufferedWriter.write(message + "\n");// 刷新缓冲区(此步骤不能省略)bufferedWriter.flush();}}}
}
运行结果:
优缺点分析:
以特殊符号作为粘包的解决方案的最大优点是实现简单,但存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性
TCP为什么要四次挥手?说一下四次挥手的流程?
四次挥手的具体流程如下:
- 用户发送FIN包:客户端发送一个FIN包,其中FIN标志位为1,表示客户端希望关闭连接
- 服务器发送ACK包:服务器收到客户端的FIN包,向客户端发送一个ACK包,其中ACK标志位为1,表示服务器已经收到了客户端的请求,并将确认号(ACK)设置为发送的序列号+1
- 服务器发送FIN包:服务器在发送完ACK包之后,也会发送一个FIN包,其中FIN标志位为1,表示服务器也希望关闭连接
- 客户端发送ACK包:客户端收到服务器的FIN包之后,向服务器发送一个ACK包,其中ACK标志位为1,表示客户端已经收到了服务器的请求,并将确认号设置为服务器发送的序列号+1
TCP进行四次挥手的作用主要是有两点:
- 确保所有数据都被传输完成:在关闭连接之前,双方都有可能还有数据需要传输,因此需要通过四次挥手来确保所有数据已经传输完成
- 确保双方都能正常关闭连接:四次挥手的过程中,客户端和服务器都需要发送 FIN 和 ACK 包,以确保双都能正确地关闭连接,避免连接一方关闭而另一方仍然处于连接状态
TCP四次挥手为什么要等两个MSL(最大生存时间)?
四次挥手时发送者最后一次等待时间是两个MSL(最大生存时间),目的就是确保最后一个ACK的可靠传输,在四次挥手的最后一步,接收方发送一个ACK给发送方,表示接受到了关闭连接的请求。发送方需要等待一段时间,确保这个ACK报文额能够可靠的传输到接受方。如果发送方在等待期间收到接收方的重传请求,可以重发ACK
TCP和UDP有什么区别?
TCP和UDP都是传输层的重要协议,但是它们存在以下几点不同:
- 连接机制不同:TCP是面向连接的协议,需要在客户端和服务器之间建立一个稳定的连接,然后再进行数据传输;而UDP是无连接协议,数据包可以直接发送给目标主机,不需要事先建立连接
- 数据传输方式不同:TCP采用可靠的数据传输方式,即在传输的过程中使用序号、确认号和重传机制等控制手段来保证数据的可靠传输;而UDP采用不可靠的数据传输方式,数据包可能会丢失或重复,不提供数据的可靠性保障
- 数据传输效率不同:由于 TCP 需要进行连接、序号确认等额外的数据包传输,因此在数据传输效率方面相对于 UDP 要低一些
- 数据大小限制不同:UDP 对数据包的大小有限制,最大只能传输 64KB 的数据,而 TCP 的数据包大小没有限制
- 应用场景不同:TCP适用于要求数据传输可靠高的场景,如网页浏览、文件下载、电子邮件等;而UDP适用实时性要求高的场景,如视频会议、在线游戏等