文件操作—IO
文件在计算机中可以代表很多东西
在操作系统中, 文件主要是指硬盘文件
硬盘主要分为机械硬盘和固态硬盘。机械硬盘通过磁头在旋转的磁盘上读取数据,适合顺序读取。而固态硬盘则使用闪存芯片来存储数据,没有机械部件,因此读取速度更快,且更耐用。尽管固态硬盘的读取速度相比机械硬盘有很大提升,但与内存相比,其速度仍然较慢。
内存和硬盘的区别:
- 内存读取速度快, 硬盘读取速度慢
- 内存会因为断电操作而失去信息, 硬盘则会在较长时间内存储数据
文件系统是操作系统的一个重要组成部分,它以多叉树的形式组织文件,方便用户通过路径来描述和定位文件。路径分为绝对路径和相对路径。
绝对路径:通常以盘符(如C:或D:)后接反斜杠(\)及文件夹路径为开头,对文件进行精确的位置描述。
相对路径:通常是以当前目录作为基准目录,使用…(上一级目录)或.(本机目录,可以省略不写)来描述目标文件的位置。
文件大体上可以分为多种类型,其中最常见的是二进制文件和文本文件。文本文件主要由字符构成,用于存储人类可读的文本信息;而二进制文件则包含二进制编码的数据,可以是程序、图像、音频等各种类型的数据。
Reader reader = new FileReader("D:/test.txt");
while (true) {
int c = reader.read();
if(c==-1) {
break;
}
System.out.print((char)(c));
}
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%22%E7%82%B9%E5%87%BB%E5%B9%B6%E6%8B%96%E6%8B%BD%E4%BB%A5%E7%A7%BB%E5%8A%A8%22&pos_id=img-anK6Upbb-1715605174086
Reader是一个抽象类, 直接使用Reader创建实例需要实现内部抽象方法, 所以使用其子类来创建实例, Reader内部的read()方法的返回值是int类型, 因为当文件读取完之后需要返回-1来通知程序, 因为char的对应字符表的值都是非负的
try {
while (true) {
char[] cbuf = new char[3];
int n = reader.read(cbuf);
if(n==-1) {
break;
}
for (int i = 0; i < n; i++) {
System.out.print(cbuf[i]);
}
}
} finally {
reader.close();
}
传入char类型的数组, 将传入的值存放到数组中, 实际上,read(char[] cbuf)
方法会根据当前可用的字符数来填充数组,并返回实际读取的字符数。如果文件内容少于数组长度,它只会读取并返回实际的内容长度;如果文件内容大于数组长度,则需要多次调用read()
方法来读取剩余内容, 使用try-finally可以避免当程序读取过程中因为报出异常而中止使得未执行close()方法
每个进程都有文件描述符表, 使用顺序表, 当获取文件时会存放进去一个, 但是文件描述符表的数组长度有限, 所以一直开启而不关闭就会出错
Java中的char
类型固定为两个字节(UTF-16编码),而中文字符在UTF-8编码中可能需要多个字节。但是,当使用FileReader
或指定UTF-8编码的InputStreamReader
读取文件时,Java会自动进行编码转换,将UTF-8编码的字节序列转换为UTF-16编码的char
序列。
try(Reader reader = new FileReader("D:/test.txt")) {
while (true) {
char[] cbuf = new char[3];
int n = reader.read(cbuf);
if(n==-1) {
break;
}
for (int i = 0; i < n; i++) {
System.out.print(cbuf[i]);
}
}
}
也可以使用try来对创建的对象进行包裹, 在程序退出无论正常还是异常退出都会对其进行关闭, 但是传入的类需要实现Closeable接口
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
// 这就相当于把字节流转成字符流了.
PrintWriter writer = new PrintWriter(outputStream);
writer.println("hello");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
PrintWriter
使用了内部缓存来存储数据,以减少直接写入输出流(在这里是FileOutputStream
)的次数,从而提高性能。这是因为从内存到硬盘的I/O操作通常比内存中的操作要慢得多。通过将多个小的写入操作合并成一个大的写入操作,PrintWriter
减少了这种性能开销。
当调用flush()
方法时,PrintWriter
会将其内部缓存中的数据写入到输出流中。如果不调用flush()
,那么缓存中的数据可能不会被写入到输出流,直到PrintWriter
对象被垃圾回收或显式关闭时才会自动调用flush()
。因此,在需要确保数据立即写入输出流的情况下,显式调用flush()
是很重要的。
网络初识
网络通信基础
IP地址
在互联网上进行数据传输,就像发送快递一样,需要知道收件人的地址。在网络中,这个地址就是IP地址。
IP地址使用32位二进制数表示,通常由4个字节组成。为了方便阅读和记忆,我们通常将IP地址转换为4个0~255之间的十进制数字表示,这4个数字之间用点(.)分隔。这种表示方法称为点分十进制表示法。
虽然知道了IP地址,但网络需要精确到哪一个进程需要接收这部分信息,这就需要使用端口号来确定。
端口号
为了确定主机上哪个进程负责接收外界信息,我们需要使用端口号进行进一步的标识。每个端口号只能绑定一个进程,以确保端口号的唯一性。然而,一个进程可以绑定多个端口号。端口号的范围通常是065535,其中0通常不被使用,而11023是保留给一些众所周知的服务的,如FTP、HTTP等。这些系统进程会绑定到这些特定的端口上。
协议
网络本质上是通过光或电信号进行数据传输的,而协议则是网络的一个重要组成部分。协议是网络中数据发送者和接收者之间共同遵守的规则和约定,用于确保数据的正确传输。
协议五元组是用于标识网络中的数据流的,包括以下几个要素:
- 目的IP:数据包的目标地址,即接收方的IP地址。
- 目的端口:数据包的目标端口,即接收进程绑定的端口号。
- 源IP:数据包的源地址,即发送方的IP地址。
- 源端口:数据包的源端口,即发送进程绑定的端口号。
- 协议类型:数据包所使用的网络协议类型,如TCP、UDP等。
协议分层
由于网络通信的复杂性,有时需要将协议拆分成多个小协议来分别处理不同的任务。随着小协议的增多,为了方便管理和维护,需要对这些协议进行分层管理。
分层管理的好处主要体现在以下几个方面:
- 不同层级之间彼此独立,封装性较好,一层的变化对其他层的影响较小,提高了系统的稳定性和可维护性。
- 每一层的协议可以灵活切换和替换,便于技术的更新和升级。
- 上层协议通过调用下层协议提供的服务来实现其功能,下层协议为上层协议提供必要的支持和保障。
通过分层管理,可以更加高效地组织和管理网络协议,促进网络通信的顺利进行。
TCP/IP五层网络模型
TCP/IP五层网络模型从底到顶分别为物理层、数据链路层、网络层、传输层和应用层。物理层是连接网络通信的基础设备,它负责传输原始的比特流,就好比公路、铁路这些基础设施。数据链路层在物理层之上,它负责将网络层交下来的IP数据报组装成帧,在两个相邻节点间的链路上实现无差错的帧传输,并进行流量控制。网络层负责为分组交换网上的不同主机提供通信服务,它确定使用哪一条路径将数据包从源主机发送到目的主机。传输层为应用进程之间提供端到端的逻辑通信,它确定起点和终点,确保数据的可靠传输。应用层则是对于应用程序上的数据使用,它负责处理特定的应用程序协议请求及响应。
在大多数现代计算机系统中,操作系统与网络接口卡(NIC)及其驱动程序协同工作,以支持网络协议栈的实现。操作系统主要提供对网络协议栈的支持,特别是在网络层、传输层和应用层。网络接口卡(NIC)及其驱动程序则负责实现物理层和数据链路层的功能。
集线器(Hub)是工作在物理层的设备,它简单地将一个端口的信号复制到其他所有端口,允许所有连接的设备在同一时刻进行通信,但所有的设备都共享同一个带宽。
封装和分用
当你在QQ中发送一则消息给你的同学时,QQ会生成消息内容,并通过套接字接口将数据传递给网络协议栈进行封装。首先,在传输层,数据会被封装成传输层的数据报,包括报头(包含源端口和目的端口、传输层协议信息等)和载荷(即QQ生成的消息内容)。然后,数据报被传递到网络层,在网络层,数据报会被进一步封装,加上IP报头(包含源IP地址、目的IP地址、生存时间、协议类型等信息),形成IP数据报。接下来,IP数据报被传递到数据链路层,在数据链路层,会在IP数据报的前后分别加上源MAC地址和目的MAC地址,形成以太网帧。最后,以太网帧被传递到物理层,通过光/电信号进行传输。从上层到下层,数据经过层层封装,确保能够在网络中正确传输。
接收方在接收到传输的光/电信号后,会进行一一分用。首先,物理层将光/电信号转换成比特流,然后传递给数据链路层。在数据链路层,接收方会检查MAC地址,确保帧是发送给自己的,然后去掉MAC头部和尾部,将载荷(即IP数据报)传递给网络层。在网络层,接收方会根据IP报头中的信息,去掉IP报头,将载荷(即传输层数据报)传递给传输层。在传输层,接收方根据端口号将数据报传递给对应的应用程序(如QQ)。最后,QQ程序对载荷进行分析,提取出消息内容并进行展示。这个过程就是数据的分用过程,确保数据能够正确地从网络层传输到目标应用程序。
网络编程
通过网络,两个主机之间可以进行通信,基于这种通信功能,我们可以实现各种应用需求。在进行网络编程时,操作系统会提供API(应用程序接口)。这些API是应用程序与网络通信之间的桥梁,使得应用程序能够与网络建立联系并进行数据交换。在传输层,主要的协议有TCP(传输控制协议)和UDP(用户数据报协议)。为了支持这两种协议,操作系统分别提供了不同的API接口
TCP和UDP的区别与相同点:
- 二者都支持全双工通信,这意味着在一个信道上可以同时进行数据的发送和接收。这种特性使得它们能够满足多种应用场景的需求。
- TCP是有连接的协议,它在通信过程中需要建立连接,确保双方都已准备好进行数据传输。这种连接机制使得TCP能够提供可靠的数据传输服务。相比之下,UDP是无连接的协议,它不需要建立连接就可以直接发送数据报。这种无连接特性使得UDP在实时性要求较高或可以容忍一定数据丢失的场景中更为适用。
- TCP是可靠传输协议,它通过序列号、确认应答、超时重传等机制来保证数据的可靠到达。当数据在传输过程中因网络问题而丢失或损坏时,TCP会尝试重新传输这些数据,以确保数据的完整性和顺序性。而UDP则不提供这样的可靠性保证,它发送的数据报可能会因为网络问题而丢失,且UDP不会进行重传。
- 在传输方式上,TCP是基于字节流的传输协议,它将数据分割成小的TCP数据段进行传输,并在接收端按序重新组装成完整的字节流。而UDP则是基于数据报的传输协议,它将数据封装在UDP数据报中直接发送,每个数据报都有固定的大小限制,并且UDP不保证数据报的顺序或可靠性。
UDP回声服务器的实现
import java.io.IOException; // 导入I/O异常类,用于处理输入输出错误
import java.net.DatagramPacket; // 导入DatagramPacket类,用于UDP通信中封装数据
import java.net.DatagramSocket; // 导入DatagramSocket类,用于UDP通信中创建套接字
import java.net.SocketException; // 导入Socket异常类,用于处理套接字相关的错误
// 定义一个名为UdpEchoServer的公共类,实现UDP回声服务器
public class UdpEchoServer {
private DatagramSocket socket; // 私有成员变量,用于接收和发送UDP数据包的套接字
// 构造函数,初始化UdpEchoServer对象,并绑定到指定端口
public UdpEchoServer(int port) throws SocketException {
// 初始化DatagramSocket对象,并绑定到指定端口,准备接收客户端的数据
socket = new DatagramSocket(port);
}
// 定义一个start方法,用于启动服务器并监听客户端的请求
public void start() throws IOException {
// 无限循环,持续监听客户端的请求
while (true) {
// 创建一个长度为4096字节的数组,用于存储接收到的数据
byte[] buffer = new byte[4096];
// 创建一个DatagramPacket对象,用于接收客户端发送的数据
DatagramPacket requestPacket = new DatagramPacket(buffer, buffer.length);
// 调用socket的receive方法,接收客户端发送的数据包
socket.receive(requestPacket);
// 将接收到的字节数组转换为字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 调用process方法处理请求,并获取响应
String response = process(request);
// 创建一个新的DatagramPacket对象,用于封装要发送给客户端的响应数据
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.length(),
requestPacket.getAddress(), requestPacket.getPort());
// 调用socket的send方法,发送响应给客户端
socket.send(responsePacket);
// 打印接收到的请求和发送的响应信息
System.out.printf("[%s:%d] req=%s, resp=%s\n",
requestPacket.getAddress().getHostAddress(), requestPacket.getPort(), request, response);
}
}
// 定义一个私有方法process,用于处理请求并返回响应
private String process(String request) {
// 这里只是简单地将请求内容作为响应返回,实际中可以根据需求进行更复杂的处理
return request;
}
// 主方法,程序的入口点
public static void main(String[] args) {
try {
// 创建一个UdpEchoServer对象,并监听1314端口
UdpEchoServer server = new UdpEchoServer(1314);
// 调用start方法启动服务器
server.start();
} catch (IOException e) {
// 捕获并打印可能出现的I/O异常
e.printStackTrace();
}
}
}
Socket是操作系统提供的一个通信机制,它允许不同进程之间进行数据交换。在操作系统层面,Socket可以被视为一种特殊的文件描述符,它提供了对底层网络通信的抽象。在Java中,我们使用Socket类来创建和管理网络通信的端点,使得开发者可以像操作文件一样来进行网络通信。
UDP简单回声客户端的实现
import java.io.IOException; // 导入I/O异常类,用于处理输入输出错误
import java.net.*; // 导入网络编程相关的所有类
import java.util.Scanner; // 导入Scanner类,用于读取用户输入
// 定义一个名为UdpEchoClient的公共类
public class UdpEchoClient {
private DatagramSocket socket; // 私有成员变量,用于发送和接收UDP数据包的套接字
private String serverIP; // 私有成员变量,存储服务器IP地址
private int serverPort; // 私有成员变量,存储服务器端口号
// 构造函数,初始化UdpEchoClient对象
UdpEchoClient(String ip, int port) throws SocketException {
socket = new DatagramSocket(); // 创建一个新的DatagramSocket对象,用于网络通信
serverIP = ip; // 设置服务器IP地址
serverPort = port; // 设置服务器端口号
}
// 定义一个start方法,用于启动客户端并接收用户输入、发送请求、接收响应
public void start() throws IOException {
System.out.println("客户端启动"); // 打印客户端启动信息
Scanner scanner = new Scanner(System.in); // 创建一个Scanner对象,用于从控制台读取用户输入
while (true) { // 无限循环,持续等待用户输入
System.out.println("输入请求: "); // 提示用户输入请求
System.out.print("->"); // 打印箭头,提示用户输入位置
String request = scanner.next(); // 读取用户输入的请求
// 创建一个DatagramPacket对象,用于封装发送给服务器的请求数据
// 把请求发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
// 使用socket发送请求数据包
// 等待服务器的回应
socket.send(requestPacket);
// 创建一个DatagramPacket对象,用于接收服务器的响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
// 使用socket接收服务器的响应数据包
socket.receive(responsePacket);
// 将响应数据包的内容转换为字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 打印服务器返回的响应
System.out.println(response);
}
}
// 主方法,程序的入口点
public static void main(String[] args) throws IOException {
// 创建一个UdpEchoClient对象,设置服务器IP为本地地址(127.0.0.1),端口号为1314
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 1314);
// 调用start方法启动客户端
client.start();
}
}
TCP回声服务器的实现
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket socket = null;
// 构造函数,初始化服务器套接字,监听指定端口
TcpEchoServer(int port) throws IOException {
socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
// 创建线程池,用于处理客户端连接
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
// 不断接受客户端的连接请求
Socket clientSocket = socket.accept();
// 将客户端连接请求提交给线程池处理
service.submit(new Runnable() {
@Override
public void run() {
try {
// 处理客户端连接
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// 处理客户端连接的方法
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
// 获取客户端的输入流和输出流
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
while (true) {
// 检查客户端是否还有输入
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
// 获取客户端的输入
String receive = scanner.next();
// 处理客户端的输入
String request = process(receive);
// 将处理结果写入输出流
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
System.out.printf("[%s:%d] receive=%s request=%s\n", clientSocket.getInetAddress(),
clientSocket.getPort(),
receive, request);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭客户端连接
clientSocket.close();
}
}
// 处理客户端输入的方法,这里只是简单地返回输入的字符串
private String process(String s) {
return s;
}
public static void main(String[] args) throws IOException {
// 创建服务器对象,监听1314端口
TcpEchoServer server = new TcpEchoServer(1314);
// 启动服务器
server.start();
}
}
TCP简单回声客户端的实现
import java.io.*; // 导入Java的输入输出流相关的库
import java.net.Socket; // 导入Java的Socket类,用于建立网络连接
import java.util.Scanner; // 导入Java的Scanner类,用于从控制台读取输入
public class TcpEchoClient { // 声明一个名为TcpEchoClient的公共类
Socket socket = null; // 定义一个Socket对象,初始化为null
String ip = ""; // 定义一个字符串变量,用于存储服务器的IP地址
int port = 0; // 定义一个整型变量,用于存储服务器的端口号
// 构造函数,用于创建TcpEchoClient对象时初始化Socket连接
TcpEchoClient(String serverIp, int port) throws IOException {
socket = new Socket(serverIp, port); // 使用给定的IP地址和端口号创建一个新的Socket连接
}
// 定义一个名为start的方法,用于启动客户端并处理与服务器的交互
public void start() {
Scanner scanner = new Scanner(System.in); // 创建一个Scanner对象,用于从控制台读取输入
System.out.printf("客户端启动\n"); // 打印"客户端启动"到控制台
try ( // 使用try-with-resources语句自动关闭资源
InputStream inputStream = socket.getInputStream(); // 获取Socket的输入流
OutputStream outputStream = socket.getOutputStream() // 获取Socket的输出流
) {
while (true) { // 无限循环,保持客户端运行
PrintWriter printWriter = new PrintWriter(outputStream); // 创建一个PrintWriter对象,用于向服务器发送数据
Scanner scannerNetWork = new Scanner(inputStream); // 创建一个Scanner对象,用于从Socket的输入流读取数据
System.out.printf("-> "); // 打印"-> "到控制台,提示用户输入
String send = scanner.next(); // 从控制台读取用户输入的数据
printWriter.println(send); // 将用户输入的数据发送到服务器
printWriter.flush(); // 刷新输出流,确保数据被发送
String response = scannerNetWork.next(); // 从Socket的输入流读取服务器的响应
System.out.println(response); // 打印服务器的响应到控制台
}
} catch (IOException e) { // 捕获IO异常
e.printStackTrace(); // 打印异常的堆栈信息
}
}
// 主方法,程序的入口点
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 1314); // 创建一个TcpEchoClient对象,连接到本地的1314端口
client.start(); // 启动客户端
}
}
网络原理
网络通信依赖于协议,而管理这些协议的最佳方式是采用分层结构。TCP/IP分层协议模型主要分为物理层、数据链路层、网络层、传输层和应用层。
- 物理层:定义了数据传输的电气、机械和时序接口特性,包括电缆规格、接口卡、数据编码方式等,以确保数据在设备之间的正确传输。
- 数据链路层:在物理层服务的基础上,负责将网络层交下来的IP数据报组装成帧,在两个相邻节点间的链路上透明地传输帧,并进行差错控制和流量控制。
- 网络层:主要负责路径规划,决定数据从源主机到目标主机之间所经过的路由路径。
- 传输层:为应用进程之间提供端到端的逻辑通信,对报文进行分割与重组,并以适当的顺序传输给目标系统的对应层次。
- 应用层:为特定应用程序提供网络服务。它关心的是应用程序如何使用网络服务,而不是数据如何在网络中传输。
应用层
应用层中的自定义协议方式:
- XML:一种古老的标记语言,使用标签来管理数据。HTML就是基于XML发展而来的,但HTML对标签的使用有严格的限制。XML的优势在于可读性较好,但占用的网络带宽较大,效率不高。
- JSON:使用大括号包裹数据,内部通过键值对的形式表示,键值对之间用逗号分隔,键和值之间用冒号分隔。JSON的可读性较好,同样占用的网络带宽较大,但因其简洁明了的结构,目前已成为流行的数据交换格式。
- Protobuffer(Protocol Buffers):由Google开发的一种数据序列化协议(类似于XML、JSON、YAML等)。它使用二进制格式对数据进行序列化和反序列化,因此具有高效的传输效率和数据压缩率。同时,它支持多种编程语言,具有良好的跨平台性和跨语言性。然而,由于其使用二进制格式,可读性不如XML和JSON,因此在开发时可能需要借助工具进行序列化和反序列化的处理。
DNS应用层协议
DNS(域名系统)是应用层协议的一部分,用于将便于记忆的域名转换为IP地址,后者描述了设备在网络上的位置。由于IP地址难以记忆,我们通常使用域名来访问网站。为了管理这些映射关系,DNS系统利用一组分布式服务器来存储域名与IP地址的对应关系。
鉴于单一服务器面对大量并发访问时可能会资源不足甚至崩溃,DNS架构采用了多级缓存和服务器镜像技术来分散负载。首先,许多运营商和大型网络服务提供商部署了镜像服务器来存储根服务器和其他关键DNS服务器的数据。当用户尝试访问某个域名时,他们会被引导至最近的镜像服务器,这样可以减少对核心DNS服务器的访问压力。
此外,DNS查询结果通常会在用户的本地计算机或网络中进行缓存,这意味着一旦域名被解析,相同的DNS请求在缓存有效期内可以直接从本地获取答案,无需重复向DNS服务器发起请求。这种缓存机制进一步减少了对DNS服务器的访问频率,从而提高了系统的整体效率和可靠性。
Http协议
HTTP协议(超文本传输协议)是基于请求与响应模型的无状态协议,其中请求和响应的结构存在明显区别。
请求结构:
- 首行 :由三部分组成,用空格分隔。第一部分是请求方法(如GET、POST等),第二部分是请求的资源URL,第三部分是HTTP版本(如HTTP/1.1)。
- 请求头 :由多个键值对构成,每对键值用冒号加空格分隔。每个键值对占用一行。请求头包含客户端环境信息、请求参数等。
- 空行 :一个单独的空行,表示请求头部的结束。
- 正文(Body) :请求的内容区域,可以包含用户提交的数据。不是所有请求都有正文,例如GET请求通常没有正文。
响应结构:
- 首行 :由三部分组成,用空格分隔。第一部分是HTTP版本号,第二部分是状态码(如200、404等),第三部分是状态消息(如OK、Not Found)。
- 响应头 :结构与请求头类似,由键值对组成,包含服务器的类型、内容类型、编码方式等信息。
- 空行 :标志着响应头部的结束。
- 正文 :响应的内容,可以包括HTML、图片、文本等多种格式的数据,根据请求的不同,响应正文的内容和格式也会有所不同。
URL的基本格式
编辑
URL(统一资源定位符)是用于描述网络资源位置的标准方式。它的基本格式包括以下几个部分:
- 协议方案名 :指明使用的网络协议,如HTTP、HTTPS、FTP等。
- 登录信息 (可选):用于身份认证,通常包括用户名和密码,但现代网页很少在URL中直接使用,通常通过网页表单或API进行身份认证。
- 服务器网址 :资源所在的服务器地址,通常是域名或直接的IP地址。
- 服务器端口号 (可选):指定服务器上的端口,对于采用标准端口的协议(如HTTP的80端口或HTTPS的443端口)通常省略。
- 带层次的文件路径 :描述资源在服务器上的位置,类似文件系统的路径,但可能指向硬盘上的文件、内存中的资源或其他形式的数据。
- 查询字符串 (可选):以问号(?)开头,后接一个或多个键值对,每对键值用等号(=)连接,不同键值对之间用和号(&)分隔。用于传递额外参数或进行数据检索。
- 片段标识符 (可选):以井号(#)开始,用于直接定位到网页的特定部分,常见于技术文档或长文章中。
URL中的特殊字符,如空格、问号等,需要使用百分号编码(URL编码)转换,以避免解析错误。这种编码方式将特殊字符转化为%后跟两位十六进制数的形式。
GET 和 POST 的区别:
- 用途 :
- GET :用于从服务器请求数据。通常用于检索信息,而不会影响资源的状态。
- POST :用于向服务器提交数据,通常会导致服务器状态的变化或产生副作用,例如用户登录、数据提交和文件上传等。
- 安全性 :
- GET :由于请求的数据会附加在 URL 上,敏感数据可能会被浏览器缓存或保存在浏览器历史记录中,这可能导致安全问题。不推荐使用 GET 方法发送敏感信息。
- POST :数据包含在请求的正文(body)中,不会在 URL 中显示,适用于传输敏感或大量数据。
- 幂等性 :
- GET :是幂等的,意味着多次执行相同的 GET 请求,理论上应该得到相同的结果,并且不会改变服务器的状态。
- POST :通常不是幂等的,多次提交相同的 POST 请求可能会每次都在服务器上产生副作用。
- 缓存 :
- GET :请求的响应通常可以被缓存,除非特定的缓存指令禁止。
- POST :由于 POST 请求可能改变服务器状态,响应通常不被缓存
Header
- Host(主机) :主机头通常与URL匹配,但由于代理或负载均衡器的存在,可能会有所不同。它指定了请求的目标主机,通常在虚拟主机中使用。
- Content-Length和Content-Type :这两个属性确实只在HTTP请求中存在消息主体时才会出现。Content-Length指定消息主体的长度,以减少分段传输。Content-Type指定正在发送的资源的媒体类型,影响服务器对请求的处理方式。
- User-Agent(用户代理) :尽管其重要性已经降低,但用户代理仍然有助于确定浏览器的功能,并处理特定的业务逻辑。最初用于适应不同浏览器的能力,但由于标准化的改进和浏览器功能的统一性,其重要性已大大降低。
- Referer(引用页) :记录了前一个地址,通常在首次进入浏览器首页时不存在。它提供了关于请求来源的信息,有助于进行分析和跟踪。
- Cookie(Cookie) :由于安全问题,网站无法直接访问用户的文件系统。因此,临时数据如登录会话被存储在浏览器中,以Cookie的形式存在。这些通常由服务器或网页自动生成,以键值对的形式组织,并根据域名存储在浏览器的文件夹中,在后续请求中随请求一起发送到服务器。
https的加密
HTTP协议传递的数据不进行加密,一旦被截获,信息便可以直接被读取。相对地,HTTPS增加了数据传输的安全性,通过使用对称加密和非对称加密。非对称加密中,公钥用于加密数据,私钥用于解密;私钥保存在服务器上,而公钥则是公开的。通常在建立HTTPS连接时,使用非对称加密传送对称密钥,因为非对称加密虽然安全但处理速度慢,不适合大量数据传输。使用对称密钥加密数据传输,提高效率。
在实际应用中,可能遇到中间人攻击。攻击者在用户与服务器之间截获并篡改信息,用户可能接收到攻击者提供的伪造公钥。为防止此类攻击,HTTPS协议采用数字证书验证公钥的合法性。数字证书由可信的证书颁发机构(CA)签发,用户通过浏览器内置的CA公钥验证证书的真实性,确保所使用的公钥属实
HTTP响应状态码
- 2XX类 :表示请求成功。这类状态码表明请求已被服务器正确接收、理解和处理。例如,200 OK 是成功响应的标准代码。
- 3XX类 :表示需要进行重定向。这些状态码告知客户端需要进一步操作以完成请求。例如,301 Moved Permanently 指资源已永久改变位置。
- 4XX类 :表示客户端错误。这类状态码指出请求含有错误或无法被执行。404 Not Found 表示服务器找不到请求的资源,403 Forbidden 表示没有权限访问请求的资源。
- 5XX类 :表示服务器错误。这类状态码表明服务器在处理请求时内部发生错误。例如,500 Internal Server Error 表示服务器遇到错误,无法完成请求。
UDP协议
学习UDP协议主要需要了解其报文结构。UDP报文由报头和载荷两部分组成。载荷是应用层的数据,而报头则包含了源端口、目的端口、报文长度和校验和等信息。其中,源端口和目的端口各占2个字节,用于标识发送方和接收方的应用程序;报文长度字段占2个字节,表示整个UDP数据报(包括报头和载荷)的总长度,其值在8(UDP报头最小长度)到65535字节之间;校验和字段占2个字节,用于检验UDP数据报在传输过程中是否出现错误。
由于网络通信过程中可能会受到各种干扰导致信息传递错误,因此UDP协议使用了校验和来检验数据报的完整性。UDP的校验和是基于二进制反码求和的算法,对报头和载荷中的每个16位字进行求和,然后将结果取反码作为校验和。接收方在收到数据报后,会重新计算校验和并与发送方提供的校验和进行比较,以验证数据的正确性。
为了提高校验和的可靠性,现在常使用MD5等更复杂的算法来计算校验和。MD5算法具有以下特点:
- 定长:无论输入数据的大小如何,MD5算法都会生成一个固定长度的哈希值(128位)。
- 离散性:MD5算法具有很好的离散性,即使输入数据只有微小的差异,计算出的哈希值也会有很大的不同。这种特性使得MD5算法非常适合用于哈希表、数据去重等场景。
- 不可逆性:在现有计算技术下,通过MD5哈希值很难反推出原始数据。这种特性使得MD5算法在密码存储、文件完整性校验等领域得到广泛应用。
TCP协议
确认应答
TCP协议的核心功能是提供可靠的数据传输服务。在TCP协议中,发送方在向接收方传输数据后,接收方会返回一个确认信息(ACK)给发送方,以告知数据是否已成功接收。这种确认机制确保了数据的可靠传输。
在TCP中,数据被分割成一个个的报文段(segment),每个报文段都有唯一的序列号。当接收方收到数据后,它会返回一个包含下一个期望接收的字节序列号的确认信息(ACK)。这样,即使在网络中数据包的传输路径不同、传输速率不同,接收方也能根据序列号正确地重新组装数据。
为了避免数据包乱序到达的问题(即后发的数据包先到达接收方),TCP使用序列号来确保数据包按照正确的顺序被接收方重新组装。
超时重传
在网络环境中,当负载过大或线路拥塞时,数据包可能会在传输过程中滞留而无法准时到达接收端,或者在途中被丢弃。为了保证信息的可靠传输,网络协议(如TCP)会采用超时重传机制。由于丢包的概率通常不是很高,因此重传的数据包通常能够成功送达。
超时的时间长度是根据实际情况动态调整的。在发生第一次超时后,下一次的超时时间限制会增加,这是为了应对可能存在的更多不可预测的网络状况。然而,超时时间会有一个最大的时间限制,一旦超过这个限制,数据包将被视为无法送达,并且发送方将放弃该数据包。
导致超时重传的原因可能是数据包在发送时丢失,也可能是接收方返回的应答在传输过程中丢失。由于发送方无法直接区分这两种情况,因此无论是哪种情况,它都会尝试对数据包进行重传。这可能会导致数据包被传输两次,但TCP协议具有去重和排序机制,能够识别并处理重复的数据包,确保数据的完整性和顺序性。TCP的缓冲区会存储数据包及其编号,当收到相同编号的数据包时,会进行去重处理,并按照正确的顺序将数据包传递给接收端。
连接管理
连接管理主要负责管理TCP连接的建立和断开。在建立连接之前,通信双方需要确认彼此的网络通畅以及传输能力是否正常,这个过程类似于开黑前测试麦克风,以确保双方的通信能够正常进行。
三次握手:建立连接
TCP连接的建立通常是由客户端发起。首先,客户端向服务器发送一个SYN(Synchronize Sequence Numbers)信号,这个信号并不包含应用层的数据,只是一个通知,告知服务器客户端想要建立连接。服务器收到SYN信号后,会返回一个SYN+ACK(Synchronize Acknowledgment)信号给客户端,表示服务器接收到了客户端的请求,并且服务器本身也准备发送SYN信号以测试自己的传输能力。最后,客户端在收到服务器的SYN+ACK信号后,会发送一个ACK(Acknowledgment)信号给服务器,表示已经收到并确认了服务器的SYN信号。
注意:在三次握手中,第二个步骤中服务器返回的SYN和ACK信号并不是被合并成一次发送的,而是作为同一个TCP报文段的不同部分被发送的。SYN和ACK是TCP报文段中的标志位,用于标识报文段的类型。
三次握手的作用 :
- 确认双方接收和传输能力是否正常运行。
- 初始化序列号,以便后续通信中能够正确识别并组装数据包。
- 确保网络通畅,为数据传输做好准备。
四次挥手:断开连接
TCP连接的断开可以由客户端或服务器中的任何一方发起。当一方想要断开连接时,会向对方发送一个FIN(Finish)信号,告知对方即将关闭连接。接收方在收到FIN信号后,会先发送一个ACK信号给发起方,表示已经收到并确认了结束连接的请求。然后,接收方在完成自己的数据传输后,也会向对方发送一个FIN信号,告知对方自己也准备关闭连接。发起方在收到对方的FIN信号后,会再次发送一个ACK信号给对方,表示已经收到并确认了对方的关闭连接请求。至此,TCP连接被完全关闭。
注意:在四次挥手中,为了确保接收方能够收到ACK信号,发起方会进入一个TIME_WAIT状态,等待一段时间(通常为2MSL,MSL是报文段在网络中的最大生存时间)以确保接收方能够收到ACK信号。如果在这段时间内没有收到对方的重传请求,发起方才会真正关闭连接。这是为了防止由于网络拥塞等原因导致的ACK信号丢失。
滑动窗口
虽然TCP使用滑动窗口机制来提高网络信息流通的效率,但在某些场景下,与UDP相比,由于TCP需要确保数据的可靠传输(包括顺序和完整性),其效率可能不及UDP。UDP不建立连接,不保证数据包的顺序和可靠性,因此在某些对实时性要求高且对数据可靠性要求不高的应用中,UDP可能具有更高的效率。
TCP通过滑动窗口和累积确认(Cumulative Acknowledgment)的方式,允许发送方一次性发送多个数据包而无需等待每个数据包的单独确认。接收方只需确认最近连续接收到的数据包,发送方据此继续发送后续数据包。这种方式减少了网络中的往返时间(RTT),提高了传输效率。
关于丢包问题:
-
丢发送的数据包:
当发送方检测到某个数据包丢失(通过超时或重复确认)时,会触发重传机制。TCP使用序列号来标识每个数据包,确保即使数据包乱序到达,也能按正确的顺序组装。接收方在缓冲区中按照序列号对数据包进行排序,直到收到完整的连续数据包序列。
-
丢失返回的ack应答:
TCP使用超时和重复确认来检测丢失的ACK。如果发送方在发送数据包后没有在规定时间内收到ACK,或者收到重复的ACK(表示某个数据包之后的数据包都已被接收,但该数据包丢失),则会触发快速重传机制,立即重传丢失的数据包。这种方式比简单的超时重传更为高效。
在通信不频繁或数据量较小的情况下,TCP的普通确认应答和超时重传机制已经足够。但在通信频繁或数据量较大的情况下,滑动窗口和快速重传等机制能够显著提高传输效率。需要注意的是,窗口大小的选择需要考虑到网络的带宽、延迟以及接收方的处理能力,以避免因窗口过大而导致接收方处理不过来,反而降低效率
流量控制
在TCP中,每个socket都有数据缓冲区用于存储从发送方接收的数据包。为了降低丢包的风险,发送方会根据接收方的当前可用窗口大小(即接收方尚未确认的字节数)来限制其发送速率。这可以类比于一个水池,水池的容量对应于接收方的缓冲区大小,而流入水池的水流则对应于发送方的发送速率。如果流入水池的水流太快,水池就可能溢出,即发生丢包。
随着数据从接收方的缓存中被读取并处理,接收方会发送确认报文(ACK)给发送方,告知其已成功接收的字节序列号。随着ACK的发送,接收方的可用窗口大小(即尚未确认的字节数)会动态变化。发送方会结合接收方的当前可用窗口大小来调节其发送速率,确保不会发送超过接收方能够处理的数据量。
当接收方的可用窗口大小为0时,发送方会暂停发送数据,并周期性地发送一个不包含应用层数据的窗口探测包(零窗口探测)来检测接收方的窗口是否已重新打开。一旦接收方确认窗口已重新打开(通过发送一个带有非零窗口大小的ACK),发送方就可以继续发送数据。
TCP使用16位的窗口大小字段来表示接收方的可用窗口大小。然而,在某些情况下,这个大小可能不足以满足高速数据传输的需求。为了支持更大的窗口大小,TCP提供了窗口扩展(Window Scale)选项。在建立连接时,发送方和接收方可以协商一个缩放因子,将窗口大小字段的实际值乘以这个缩放因子,从而得到一个更大的有效窗口大小。
拥塞控制
拥塞控制是TCP协议中的重要机制,它主要关注在传输过程中中间节点(如路由器、交换机等)的负载情况,以避免网络拥塞。TCP采用了四种算法来实现拥塞控制:慢开始、拥塞避免、快重传和快恢复。
- 慢开始:在建立连接或长时间未发送数据时,TCP会采用慢开始算法,初始发送一个很小的数据量,然后逐渐增大。这样可以探测网络的可用带宽。
- 拥塞避免:当发送方探测到网络拥塞的迹象(如超时或重复ACK)时,会降低发送速率,以避免进一步的拥塞。
- 快重传:当接收方收到一个失序的数据包时,会立即发送重复ACK给发送方,通知其某个数据包丢失。发送方在收到一定数量的重复ACK后,会立即重传丢失的数据包,而不需要等待超时。
- 快恢复:与快重传配合使用,当发送方收到重复ACK时,会降低发送速率到慢开始阶段的一半,并重新进入拥塞避免阶段。
在拥塞控制过程中,TCP会根据网络的实际情况动态地调整发送速率,以避免网络拥塞。
流量控制和拥塞控制的关系
流量控制和拥塞控制都会对TCP的窗口大小进行限制。流量控制是基于接收方的处理能力来限制发送方的发送速率,而拥塞控制则是基于网络的拥塞情况来限制发送方的发送速率。在实际应用中,TCP会取两者中的较小值作为实际的窗口大小。
延时应答
在TCP中,接收方在接收到数据包后,通常不会立即返回ACK应答给发送方,而是会等待一段时间(通常称为延迟时间),以便查看是否有更多的数据包到达。如果在这段时间内又有数据包到达,接收方会将这些数据包的ACK应答合并成一个应答发送给发送方。这样可以减少网络中的ACK报文数量,提高网络带宽的利用率。同时,由于接收方有更多的时间来读取数据缓存区,使得剩余缓存区变大,从而可能使得窗口变大,提高了数据的传输效率。
捎带应答
捎带应答是TCP协议中的一种优化策略,当接收方准备向发送方发送数据时,如果它尚未发送对之前接收到的数据段的确认(ACK),那么接收方会将这个ACK与要发送的数据一起封装在一个TCP报文段中发送出去。这样可以将两次传输合并成一次,减少了封装和分用的开销,从而提高了传输效率。这种优化策略并不直接依赖于TCP的延迟确认特性,而是TCP协议栈的一种实现优化
面向字节流
像TCP这种使用字节流进行传递消息的方式就存在粘包和拆包的现象。为了避免这些问题,我们需要使用一种机制来明确消息的边界。这可以通过在消息中添加定长、特殊标记或长度前缀等方式来实现。而自定义协议如XML、JSON、Protocol Buffers等,虽然它们定义了数据的结构和编码方式,但在使用它们进行网络通信时,我们仍然需要一种机制来识别消息的边界
异常的处理情况
-
进程崩溃:
当进程崩溃时,操作系统会尝试替进程关闭套接字。这通常会导致TCP正常发送FIN给对端,等待对端发送FIN和ACK,然后再发送ACK来关闭连接。但具体行为可能取决于操作系统和编程语言的实现。
-
电脑关机:
如果你正常关闭电脑,操作系统会负责关闭所有进程和套接字,并尝试向对端发送FIN包。但是,如果系统突然崩溃或电源被强行关闭,可能不会发送FIN包,对端将开始超时重传,并最终可能进入连接重置状态。
-
电脑断电:
电脑断电是瞬间的,进程和操作系统都无法执行任何清理操作,包括发送TCP的FIN包。对端将注意到连接不再活跃,并开始超时重传。最终,对端可能会进入连接重置状态并删除连接信息。
如果应用层实现了心跳包机制,它将用于检测连接的活跃性。在多次发送心跳包没有响应后,应用层将尝试断开连接。 -
网线断开:
网线断开后,TCP连接将失去通信能力。双方都会注意到连接不再活跃,并开始超时重传。但是,由于物理层的问题,TCP的FIN包可能永远不会发送或接收。类似地,应用层的心跳包也将无法发送或接收。在这种情况下,双方最终都可能进入连接重置状态并删除连接信息。
心跳机制在分布式系统中常用于维护和监测节点间的连接状态,确保所有组件能够有效地通信。一般而言,分布式应用通常会设计自己的应用层心跳机制,而不是依赖于底层传输协议如TCP的机制,因为TCP协议标准中并未包含内置的心跳功能。应用层的心跳机制允许开发者自定义心跳频率和超时机制,从而更适应各种不同场景的具体需求。
TCP提供可靠传输,保证数据的顺序性、可靠性和数据完整性,非常适合于需要高可靠性的网络通信场景。TCP通过使用确认和重传机制来保证数据包的正确传达,因此广泛用于文件传输、网页浏览等场合。
相比之下,UDP是一种无连接的协议,它不保证数据包的顺序或可靠传达,但提供了较低的延迟和较少的协议开销。这使得UDP特别适合于对实时性要求很高的应用,例如视频流、VoIP(语音通信)以及局域网内的高效率数据传输。在机房等受控环境中,数据的安全性和完整性可以通过其他方式保障,因此使用UDP可以提高通信效率。
网络层
网络层主要有两个基本功能:一是通过IP地址来标识网络上的设备位置,确保数据包能被正确路由到目的地;二是管理数据包在发送源和目的地之间的传输路径,包括数据包的生成、传输以及接收过程。
IP协议详解
- 4位版本号 :当前主流的IP协议版本为IPv4和IPv6。IPv4地址长度为32位,而IPv6地址长度则扩展至128位,以应对互联网快速增长的地址需求。
- 4位首部长度 :这一字段规定了IP首部的最小和最大长度。首部的基本长度是20字节,通过首部长度字段可以扩展到60字节,以包含更多的选项。
- 8位服务类型 :旧的IPv4头部中的服务类型字段(ToS)已部分被DSCP(区分服务代码点)和ECN(显式拥塞通知)所替代。ToS中的4位被用于DSCP以实现服务质量(QoS)管理。
- 16位总长度 :这一字段表示IP数据包的总长度,包括首部和数据载荷。总长度最大可达65535字节。
- 16位标识、3位标志位与13位片偏移 :这些字段共同管理IP分片。标识字段帮助重新组装分片后的数据包,而标志位中的MF(更多分片)和DF(禁止分片)控制分片行为。
- 16位首部校验和 :这是用于检测IP首部在传输过程中是否发生错误的校验和,类似于TCP和UDP的校验机制。
- 32位源IP地址与32位目的IP地址 :这两个字段标识了数据包的发送者和接收者的IP地址。
NAT机制与内外网交互
NAT(网络地址转换)是一种广泛使用的技术,它允许多个设备共享一个公共IP地址进行互联网访问,同时保持内网IP地址的私有性。在NAT环境中,内部设备的私有IP地址会被转换为公共IP地址,同时,端口号也可能被重新映射,以区分来自同一内网的不同请求。这使得内网设备可以主动访问外网,但外网设备无法直接访问内网设备,从而增强了网络的安全性。
通过这种机制,NAT不仅解决了IPv4地址短缺的问题,也为网络设备提供了一层额外的安全保护
网段划分
IP地址由网络号和主机号两部分组成。在同一局域网(LAN)中,所有设备的网络号必须相同,而主机号必须唯一,以确保各个设备的地址不重复。
不同局域网可以连接到同一路由器。这些局域网可以配置相同或不同的网络号,这取决于网络的具体设计需求和子网掩码的配置。
子网掩码用于区分IP地址中的网络号和主机号。子网掩码中,连续的‘1’位代表网络号,而‘0’位代表主机号。子网掩码中的‘1’位始终位于‘0’位之前,这样才能正确地区分出网络地址和主机地址。
路由选择
在网络通信中,IP数据报的传输并非像地图上的已知路径那样预先规划,而是根据实际网络环境动态进行探索。每个路由器都维护着一张路由表,其中记录了相邻路由器及其对应的网络。当接收到数据报时,路由器会根据目标IP地址查询路由表,如果找到匹配项,则将数据报转发到相应的网络接口;如果没有匹配项,则按照默认路由进行转发。
数据链路层
以太网协议是一种常见的数据链路层协议,同时涵盖了物理层的功能。以太网数据帧由帧头、数据部分和帧尾组成。帧头包含了目标地址和源地址,它们是MAC地址(即网络地址)的表示形式。以太网协议主要关注相邻节点之间的数据传输和转发。与之相反,网络层则更关注数据的起始点和终点,以实现跨网络的数据传输。