Java学习笔记——网络原理1

Java学习笔记——网络编程

传输层(TCP和UDP)

  1. TCP是有连接的,而UDP是无连接的。

    ​ TCP要想通信需要先建立连接,且需保存对方信息。

    ​ UDP可以直接发送数据,不用保存对方信息(但是写程序的人需要保存,调用UDP api的时候要将对方的信息发送过去)。

  2. TCP是可靠传输,而UDP是不可靠传输。

    ​ TCP传输数据有一系列保障数据的传输,如超时重传,应答机制,流量控制,序列号机制,拥塞控制,数据校验。

    ​ UDP的不可靠传输体现在,其发送完数据后不保证数据能够到达目标地址。

    ​ 可靠传输也是有代价的——机制更复杂,传输效率更低。

  3. TCP是面向字节流的,UDP是面向数据报(Datagram)的。

    ​ 字节流和文件操作的字节流是一个意思。以字节为单位进行传输。

    ​ 数据报有严格的格式限制,是UDP传输的基本单位。

  4. TCP和UDP都是全双工的。

    ​ 一个信道允许双向通信就是全双工,如果只允许单向通信那就是半双工。(一个socket对象就可以接收和发送数据。)

UDP通信

DatagramSocket 构造方法:

方法方法说明
DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)

DatagramPacket 方法:

方法方法说明
InetAddress getAddress()从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData()获取数据报中的数据

InetSocketAddress 方法:

方法方法说明
InetSocketAddress(InetAddress addr, int port)创建一个Socket地址,包含IP地址和端口号
UDP报文
image-20240520112156951

源端口,目标端口,报文大小,校验和的大小都是2字节,所以UDP的最大报文大小为64kb。

**校验和作用:**在网络传输过程中,可能会有电平正负翻转导致数据有误。校验和通过数据校验算法(如CRC算法)来确定数据是否存在错误,确保数据的正确性。

基于校验和算法校验数据:

  1. 发送方把整理好的数据通过校验和算法计算出checksum1
  2. 发送方将数据和checksum1通过网络一同发送出去
  3. 接收方收到数据之后再根据同一个算法计算出checksum2
  4. 然后对比checksum1和checksum2,如果不相同,那说明数据有误。如果checksum1和checksum2相同,那说明数据大概率相同(因为可能会出现偏差)。
UDP服务端口
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpServer {
    private DatagramSocket socket;

    public UdpServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    private String process(String request){
        return request;
    }

    private void start() throws IOException {
        while(true){
            //1. 创建接收返回的数据包并等待请求
            DatagramPacket requirePacket = new DatagramPacket(new byte[4096],4096);//4096是接收数据包最大长度
            socket.receive(requirePacket);//接收到的数据放在datagramPacket中
            //2. 对接收到的数据进行处理
            String require = new String(requirePacket.getData(),//这里的getData()得到的数字节数组
                    0,requirePacket.getLength());//这个代码表示从字节数组的第0个位置开始算requirePacket.getLength()长度
            String response = process(require);
            //3. 将处理完的数据返回给发送端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length, requirePacket.getSocketAddress());//因为是以字节为单位,所以response要先用getBytes()转换
            socket.send(responsePacket);
            //4. 打印信息
            System.out.printf("[%s,%d]require: %s , response: %s\n",requirePacket.getAddress().toString(),
                    requirePacket.getPort(),require,response);

        }
    }

    public static void main(String[] args) throws IOException {
        UdpServer udpServer = new UdpServer(9090);
        udpServer.start();
    }
}
UDP客户端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

public class UdpClient {
    private static String ip;
    private static int port;
    private DatagramSocket socket;

    UdpClient(String ip,int port) throws SocketException {
        this.ip = ip;
        this.port = port;
        socket = new DatagramSocket();
    }

    private void start() throws IOException {
        while(true){
            //1. 接收输入信息
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入信息:");
            String req = scanner.nextLine();
            //2. 整理数据包
            DatagramPacket requirePacket = new DatagramPacket(req.getBytes(),req.length(),
                    InetAddress.getByName(ip),port);//这里的InetAddress.getByName(ip)表示将字符串ip转化成可发送的格式
            //3. 发送数据
            socket.send(requirePacket);
            //4. 接收响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            //5. 打印数据
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.printf("接收到的数据为:%s\n",response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpClient udpClient = new UdpClient("127.0.0.1",9090);
        udpClient.start();
    }
}

这里之所以不用关闭socket,是因为这里的socket的生命周期和程序的生命周期一致。如果是其他的程序就可能要及时关闭不用的socket。

某些特定的端口号

​ 在0-1023的端口是给某些特定应用成为"系统端口"或"知名端口",例如:HTPP:80,HTTPS:443,SSH:22等,我们选择端口的时候不要占用这些端口。最大端口号是65535

协议分层:

​ 主要目的是解耦合,方便管理。

网络传输的基本流程

在这里插入图片描述

​ 现在有一个这样的场景,我和小明建立了两个人的独立聊天软件,然后我给小明发了一条信息:“小明,在线吗?”,这个信息将在网络中如何传输呢?下面是这个信息在网络传输中的基本流程:

应用层(关注数据如何使用)

​ "小明,在线吗?"这个信息首先会在应用层包装起来然后向传输层传输。

在这里插入图片描述

传输层(关注起点和终点)

​ 传输层拿到应用层传输的信息后,会对其进行封装,加上传输层报头(TCP/UDP),这一层封装了很多重要信息,例如发送端的端口号和接收端的端口号。包装好之后会传输给网络层。

在这里插入图片描述

网络层(关注路径规划)

​ 网络层拿到传输层的信息后,也会对其进行封装,根据网络层协议(IP协议)加上报头。这里报头包含了网络层的信息,例如目标IP和源IP。接着会将其传输给数据链路层。

在这里插入图片描述

数据链路层(关注相邻节点的转发)

​ 数据链路层拿到信息后,会根据数据链路层协议(以太网)加上报头和尾,其中最重要的信息是源mac地址和目标mac地址(mac地址关心是两个相邻地址的传输),接着继续向下传输到物理层。

在这里插入图片描述

物理层(硬件设备)

​ 物理层拿到信息后会将这个信息转换成二进制序列然后通过光信号和电信号进行传输。

到达目标主机

​ 当传输到目标主机后会先将信息进行"分用",根据光电信号转换成二进制数据列,然后得到以太网数据报,对数据报进行解析,去掉报头和尾,继续传输给网络层。网络层拿到信息后去掉报头,将载荷继续向上传输。传输层拿到信息后也是去掉载荷最后传输给应用层,就这样"你好,在吗?"这条信息就顺利地传输到了小明的程序中。

路由器和交换机的作用和工作的层

TCP通信

ServerSocket构造方法:

构造方法方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket方法:

方法方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close()关闭此套接字

Socket是ServerSocket接收到客户端连接请求后得到的,用来进行和客户端的通信。

Socket构造方法:

方法方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

Socket方法:

方法方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流
TCP报文

在这里插入图片描述

​ 虽然TCP报头大小最多只能有60字节([4位数首部长度的最大值]15*4字节),但是TCP报文的大小没有限制。

​ 序列号是通信双方各自独有的,单位是字节。若A向B已经发送1000字节的报文,B向A已经发送了500字节的报文,A要继续向B发送500字节的报文,那么这个报文的序列号是1001(1001~1500的头部序列号),因为不是ACK报文,所以确认号无效。然后B确认消息,发送ACK报文,确认号为1501,序列号为500(B向A发送的报文的最后一次报文的尾部序列号,这里是1到500取500)。

在这里插入图片描述

确认应答是TCP的核心机制,支持了TCP的可靠传输!

超时重传

​ 看下图的两种情况

在这里插入图片描述

​ A面对这两种情况采用同一个处理方案,因为对于A来说它也不知道是A向B发送的数据丢了还是B向A发送的ACK报文丢了。

​ **处理方案:**A发送数据后在一段时间T内没有接收到B返回的ACK报文,那么A将会再发送一份一模一样的数据,然后等待的时间T也会变成,如果还是没有返回ACK报文,那么A将再重复几次发送数据,T也会变长。经过几次后,A就会任务与B的TCP连接有问题,那么就会强制关闭与B的TCP连接。这里的时间T就是超时时间,这个机制是超时重传。

​ **重复接收到同一报文的情况:**TCP里有一个接收缓冲区。当A或B重复接收到一个报文的时候,它会发现在TCP接收缓冲区中有同样的数据,就丢弃后面的到的报文而保留最先到达的。

​ **报文先后顺序:**想象一种情况,就是A向B发送1001-1500的报文,紧接着又发送1501-2000的报文。但是中途网络传输过程中第一个报文出现阻塞,而第二个报文先到达。那么B向A的报文的ACK报文会不会出现顺序颠倒?(就是说原本第一份ACK报文是对应对一个报文的,但是由于第二报文先到达,是否会造成ACK报文应答颠倒)——答案:不会!B会根据序列号来确定确认号,B会根据报文顺序发送ACK报文。

​ **报文丢弃:**由于A到B的线路中出现严重的网络拥塞,以至于当A发送信息给B,B迟迟没有收到,经过超时重传机制后,A就会重置与B的连接,如果还是没有响应就强制关闭了A与B的TCP连接。当A到B的网络恢复正常后,B会收到曾今A发送过来信息,这时候B接收到了A曾今发送的消息,B会根据序列号来判断这是本次TCP连接的消息还是上一次TCP连接的消息,如果是上一次的,他会直接丢弃该数据。

三次握手

在这里插入图片描述

三次握手的作用:

  • 确认当前网络是否通畅。
  • 要让发送方和接收方都能确认自己的发送能力和接受能力均正常
  • 让通信双方在握手过程中,针对一些重要参数进行协商。

两次握手行不行?——不行!因为如果是两次握手的话接收方B就无法知道自己发送给A的报文是否被收到,无法保证自己能正常发送数据。

四次握手行不行?——行,但是没必要!因为三次握手就可以解决问题,四次握手就在浪费资源。

四次挥手

在这里插入图片描述

​ **TIME_WAIT状态的作用:**TIME_WAIT状态是为了在发送ACK报文给接收端的时候报文丢失的时候接收接收端重传的FIN报文,留下后手,如果有没有TIME_WAIT状态,接收端重传的FIN报文就再也没人能处理。TIME_WAIT等待2MSL(MSL是两个节点通信的最大消耗时间,是一个可配置的参数)。

中间的ACK和FIN能不能合并成一次?——不一定!得看时机,一般来说被发起关闭方在发送完ACK报文和在发送FIN报文之间会干很多事情,中间就可能会存在很大的时间空隙,这个时候就不能合并(捎带应答)成一次。如果ACK报文和FIN报文几乎在同一时刻发出,那么就可以合并成一次(因为FIN报文的发送一般是要执行到close()才会发送)。前面的三次握手的ACK和第二个SYN都是内核触发的,同一个时机,所以可以合并。

滑动窗口

在这里插入图片描述
在这里插入图片描述

​ 正常情况下,主机间的通信并不是一个报文一个报文地发送,那样通信效率太低了。而是像图中那样,一次性发送多送多个报文。

但是如果在这种情况下,要是中间有报文丢失该怎么办?这时候就会触发快速重传机制

快速重传

情况1:ACK报文丢失

在这里插入图片描述

​ 这种情况下,接收ACK报文的一端会根据就收到的最大的ACK报文序列号来判断。如图,如果A发送给B 1~6000的报文,如果中间的任何ACK报文丢失了,只要最后一个ACK报文(下一个是6001)没有丢失,那么A就会默认报文全部发送成功,而不会进行重传操作。如果是后边的几个ACK报文丢失,也会按照最大的ACK报文来重新发送数据包。例如:如果是4001-5000和5001-6000对应的ACK报文都丢失了那么就会重传4001-5000和5001-6000数据包

情况2:数据包丢失

在这里插入图片描述

​ 按图中解释,A发送的1001~2000的数据包丢了,那么B就会一直向A索要这个数据包(发送的ACK数据的确认号一直是1001),之后A就会重新发送丢失的数据包。成功补发到B之后,B就会将ACK确认号改成下一个要接收的数据包序列号(图中是7001)

流量控制

​ TCP有接收缓冲区和发送缓冲区,发送端发送数据的速度也需要根据接收端的接收缓冲区的窗口大小来决定。如果接收端的缓冲区过小,那么发送方就会降低发送数据的速率。接收端在返回ACK报文的时候会在TCP报文的16位窗口大小字段告诉接收端当前接收缓冲区的窗口大小,这样的措施是为了降低因为网络阻塞而造成的丢包。如果窗口比16位窗口大小大怎么办?——可以利用报头选项中的窗口拓展因子来告诉发送端可以提高发送速率。

拥塞控制

在这里插入图片描述

​ 如上图,A向B发送数据是要经过一系列的路由器和交换机的,但是这些路由器和交换机并不只是给A和B直接的通信服务,而是给大量的主机间进行通信,这个时候难免会出现网络拥塞,如果A发生数据给B,可能B的接收缓冲区的窗口大小比较大,能够以比较高的速度接受数据,但是中间的路由器和交换机正在给大量用户进行服务,这种情况下就很容易造成丢包,所以就不能单以B的接收缓冲器来确定A发送数据的速度。

解决方案:

在这里插入图片描述

​ 慢启动(慢开始),一开始传输信息的效率低,然后以指数级的速度增长,当增长到ssthresh的时候就改成线性增长。当速度会造成网络拥塞的时候就将速度降到慢启动的值,然后继续以指数级增长,到达新的ssthresh值的时候就又返回慢开始,以此往复。每次都会有新的ssthresh值。(现在还有另一种就是遇到网络拥塞的时候不再降发送速率降低到慢开始的值,效率太慢,而是直接降到新的ssthresh值,然后以线性增长,以此往复。)

流量控制注重的是目标节点的情况,而拥塞控制注重的是中间节点的情况。

延时应答

​ 控制传输速率的一个非常重要的因素——接收端的接收缓冲区窗口大小。

​ 发送端如何知道接收端的接收缓冲区大小?——通过TCP报文中的16位窗口大小来通知发送端,发送端会根据窗口大小调整发送速率。有时候窗口太小就可能影响发送端的发送速率,那么接收方可以选择处理完一部分接收缓冲区的数据后再给发送端ACK报文,这样发送端就知道接收端的接收缓冲区变大了,就可以提高发送速率。

捎带应答

​ 当A发送给B数据的时候,B也想给A发送数据,这个时候B可以将ACK报文和要发送的数据一同发送给A,将ACK报文和数据报文合成一份报文,这样就可以更高效地传输数据,这就是捎带应答。例如三次握手中间的那一次,SYN报文和ACK报文合成了一份报文。

面向字节流与粘包问题

​ 因为TCP报文是以流的形式传输的,所以如果不做特殊处理是难以将数据包分隔开,就容易出现多个数据包粘在一起,数据包错误。

解决:自定义分隔符协议或者使用json,protobuf,xml等现成协议。

异常情况处理
  1. 进程崩溃:进程没了,异常终止了。文件描述符表也就是释放了,相当于调用了socket.close()方法。此时就会触发FIN报文发送,对方接收到报文后也会返回FIN和ACK报文,之后发送ACK报文就会断开连接了。TCP连接可以独立于进程之外,进程虽然崩溃了,但是TCP连接不一定也崩溃了。

  2. 主机关机(正常关机):在主机正常关机的时候,会强制关闭进程,这个时候进程会发送FIN包给对端。但是这个时候不只是进程关闭了,而是整个系统都关闭了,如果在系统关闭之前接收到了对端返回FIN和ACK报文,系统还是可以返回ACK报文的。但是如果系统关闭了,ACK和FIN报文迟到了,那么就不会返回ACK报文给对端,对端在进行几次超时重传后就会断开与这个端口的TCP连接。

  3. 主机掉电:主机掉电是一瞬间的事,没有时间给进程反应发送FIN包的时间。此时如果对端发送信息给对方,对端如果长时间不返回ACK包,对端就会任务本端连接挂了,就会断开与本端的连接。要是对端没有发信息给本端——TCP有心跳包机制,双方会周期性地给对方发心跳包,如果对方收到心跳包后迟迟没有应答,那么就会认为对方连接挂了,也会关闭与对方的TCP连接。

  4. 网线断开:如果A在给B发送数据,一旦网线断开

    ​ A就会触发超时重传->连接重置->单方面释放连接

    ​ B就会触发心跳包机制->发现对方没有响应->单方面释放连接

服务端
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 TcpServer {
    private ServerSocket serverSocket = null;

    public TcpServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    private String process(String require){
        return require;
    }

    private void processConnection(Socket socket) throws IOException {
        try(InputStream inputStream = socket.getInputStream();//拿到socket的输入输出流
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true){
                //利用Scanner来接收输入
                if(!scanner.hasNext()){//如果没有下一行,那说明客户端断开连接了
                    System.out.printf("[%s:%d]客户端下线!\n",socket.getInetAddress(),socket.getPort());
                    break;
                }
                //1. 接收输入端请求
                String require = scanner.nextLine();//我们规定以换行符为报文截止符
                //2. 处理请求
                String response = process(require);
                //3. 利用PrintWriter向outputStream流写入数据
                printWriter.println(response);
                //4. 冲刷缓冲区,不然数据发送不完整。
                printWriter.flush();
                System.out.printf("处理新的请求,require:%s,response:%s\n",require,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            socket.close();
        }
    }

    public void start() throws IOException {
        //创建线程池
        ExecutorService service = Executors.newCachedThreadPool();
        while(true){
            //1. 主线程利用serverSocket接受新的连接
            Socket socket = serverSocket.accept();
            System.out.printf("[%s:%d]新的客户端连接服务器!\n",socket.getInetAddress(),socket.getPort());
            //2. 利用线程池接受新的连接
            service.submit(()->{
                try {
                    processConnection(socket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

    public static void main(String[] args) throws IOException {
        TcpServer tcpServer = new TcpServer(9090);
        tcpServer.start();
    }
}
客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpClient {
    private Socket socket;

    public TcpClient(String ip,int port) throws IOException {
        socket = new Socket(ip,port);
    }
    public void start(){
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            Scanner scannerNetWork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true){
                //1. 通过键盘输入请求
                System.out.printf("请输入请求:");
                String require = scanner.nextLine();
                //2. 通过printWriter向服务器发送请求
                printWriter.println(require);
                printWriter.flush();
                //3. 利用ScannerNetWork接收服务器处理结果
                String response = scannerNetWork.nextLine();
                //4. 打印在界面上
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        TcpClient tcpClient = new TcpClient("127.0.0.1",9090);
        tcpClient.start();
    }
}

网络层

IP协议

在这里插入图片描述

  • 版本号:表示是IPv4还是IPv6。
  • 4位首部长度:和TCP一样,表示报文首部(不包括数据)的最大大小为15*4字节=60字节。
  • 8位服务类型:包括3位优先权字段(已经弃用),4位TOS字段(包括四种形态:最小延时、最大吞吐量、最高可靠性、最小成本,每一位代表一个形态,只能有其中一位置为1,各个形态互相冲突),和1位保留字段(必须置为0)。
  • 64位总长度(字节数):表示IP协议包最大只能为64字节,但是IP协议支持拆包组包功能。
  • 16位标识:如果一个大的IP协议需要拆成多个小包,这些小包的16位标识都相同。
  • 3位标志:有一位表示是否允许拆包,还有一位表示是否是最后一个包。
  • 13位片偏移:描述当前每个小的数据包相对位置(其实就相当于小数据包的序号,描述小数据包的先后顺序)。
  • 8位生存时间(TTL):描述这个数据包还能再网络上存活多久,TTL的单位是转发次数。(可以设置为32,64,128……每转发一次减1),如果TTL已经耗尽,但是还没有到对端,就丢弃这个数据包。这个机制能够让无目的的数据包或者出问题的数据包销毁,减少网络拥塞。
  • 8位协议:描述的是UDP数据包还是TCP数据包还是KCP数据包,描述传输层用的是什么协议。
  • 16位首部校验和:这个校验和只校验首部,而不对数据进行校验!(数据都是TCP或者UDP等传输层数据包,这些数据包自身都有校验,所以就不用重复校验)。
IP的两大分类:
局域网IP(内网IP):

​ 形如10.*或者 172.16.*-172.31.*或者192.168.*就是局域网IP

​ 在同一局域网内部,IP不能重复,在不同的局域网下,IP可以重复。

广域网(外网IP):

​ 除了局域网IP的IP都是广域网IP,广域网IP始终都不能重复,务必唯一!

NAT机制:

IP只有32位只能用42亿9千万多个主机,但是现在用的机器早已超过42亿9千万该怎么解决?——NAT机制

​ 1. 局域网内一台主机发送信息的情况:

在这里插入图片描述

​ 2. 局域网内多态主机发送给同一台外网主机的情况
在这里插入图片描述

​ 如果A和B打开的进程的端口号相同,那么路由器会修改他们的端口号,如果相同则不会对端口号进行修改。

其实除了NAT机制还有动态分配IP地址,但是这两个方案还是治标不治本的,最终的解决方案还是利用IPv6

网段划分

​ IP地址分为两个部分,网络号和主机号

网络号标识网段,保证相互连接的两个网段具有不同的标识;

主机号标识主机,同一网段内,主机之间具有相同的网络号,但是必须有不同的主机号;

主机号为0或者255的主机号都不能分给任何一个设备,0是网络号,255(例如192.168.100.255)是广播地址!——注意!UDP支持广播TCP不支持

“上古时期”的网段划分——就是很少用了

在这里插入图片描述

数据包寻址

​ A给B发生一条信息,信息是怎么进行传递的呢?——信息首先会从A主机发送到最近的路由器,然后路由器再根据路由表再进行跳转到下一个路由器节点,直到到达B主机为止。

​ 每个路由器内部都有一个路由表,根据目标IP查路由表来确定跳转到哪个节点。如果查到了就直接按照路由表给定的方向转发。如果没有查到,路由表里有一个默认的表项(下一个地址),按照默认的地址转发。

​ (交换机里面存有转发表,一般是以哈希表形式展现)

​ 数据包的寻址可以模拟成问路,如果我要从学校大门(其实IP)去图书馆(目标IP),我可以问附近的人图书馆怎么走,附近的人(路由器)不知道图书馆怎么走,但是知道图书馆在北边(查路由表),他就说你往前面北边那个教学楼走,图书馆在那个方向。然后我到达了北边教学楼,又问附近的人(路由器)图书馆怎么走,他知道图书馆是往东边走(查路由表),他就说你往东边的宿舍楼走,图书馆在那个方向。于是我就到了宿舍楼下,我又问附近的人图书馆怎么走,此时这个人知道图书馆怎么走,他说直走五十米后右拐一百米就到了。这就是问路成功了!

数据链路层

以太网数据帧

​ 以太网数据帧=帧头+载荷(IP数据报)+帧尾

在这里插入图片描述

  • 类型:这里值得是承载的数据包是什么类型数据帧。

下面两种传输的不是业务数据,而是辅助转发的协议。——当一个设备需要向网络上的另一个设备发送数据时,它需要知道目标设备的MAC地址。如果设备只知道目标的IP地址,那么它就会发送一个ARP请求报文。(当不知道目标MAC地址的时候就会发送ARP报文)

例如,如果你的电脑(假设IP地址为192.168.1.2)想要向同一局域网内的另一台电脑(假设IP地址为192.168.1.3)发送数据,但不知道它的MAC地址,你的电脑就会发送一个ARP请求报文,请求网络上的设备告诉它192.168.1.3对应的MAC地址。

以太网数据帧揭示了IP为啥要分包的真正原因——一个以太网数据帧最多能存1500字节大小(MTU)的数据。这里的目标地址和原地址指的是mac地址!

IP协议立足于全局,王城通信过程的路径规划,以太网则是关注于局部相邻两个设备的通信过程

假如有个通信路径:A->T1->T2->B(在不考虑NAT机制的情况下)

当A将数据传输给T1时,数据报中,源IP:A,目标IP:B,源mac:A,目标mac:T1

T1传输给T2时:数据报中,源IP:A,目标IP:B,源mac:T1,目标mac:T2

T2传输给B时:数据报中,源IP:A,目标IP:B,源mac:T2,目标mac:B

可见目标IP和源IP不会变,而源mac地址会随着路由器变化

Java学习笔记——网络原理2

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
文件上传是Web开发中常见的功能之一,Java中也提供了多种方式来实现文件上传。其中,一种常用的方式是通过Apache的commons-fileupload组件来实现文件上传。 以下是实现文件上传的步骤: 1.在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.3</version> </dependency> ``` 2.在前端页面中添加文件上传表单: ```html <form method="post" enctype="multipart/form-data" action="upload"> <input type="file" name="file"> <input type="submit" value="Upload"> </form> ``` 3.在后台Java代码中处理上传文件: ```java // 创建一个DiskFileItemFactory对象,用于解析上传的文件 DiskFileItemFactory factory = new DiskFileItemFactory(); // 设置缓冲区大小,如果上传的文件大于缓冲区大小,则先将文件保存到临时文件中,再进行处理 factory.setSizeThreshold(1024 * 1024); // 创建一个ServletFileUpload对象,用于解析上传的文件 ServletFileUpload upload = new ServletFileUpload(factory); // 设置上传文件的大小限制,这里设置为10MB upload.setFileSizeMax(10 * 1024 * 1024); // 解析上传的文件,得到一个FileItem的List集合 List<FileItem> items = upload.parseRequest(request); // 遍历FileItem的List集合,处理上传的文件 for (FileItem item : items) { // 判断当前FileItem是否为上传的文件 if (!item.isFormField()) { // 获取上传文件的文件名 String fileName = item.getName(); // 创建一个File对象,用于保存上传的文件 File file = new File("D:/uploads/" + fileName); // 将上传的文件保存到指定的目录中 item.write(file); } } ``` 以上代码中,首先创建了一个DiskFileItemFactory对象,用于解析上传的文件。然后设置了缓冲区大小和上传文件的大小限制。接着创建一个ServletFileUpload对象,用于解析上传的文件。最后遍历FileItem的List集合,判断当前FileItem是否为上传的文件,如果是,则获取文件名,创建一个File对象,将上传的文件保存到指定的目录中。 4.文件上传完成后,可以给用户一个提示信息,例如: ```java response.getWriter().write("File uploaded successfully!"); ``` 以上就是使用Apache的commons-fileupload组件实现文件上传的步骤。需要注意的是,文件上传可能会带来安全隐患,因此在处理上传的文件时,需要进行严格的校验和过滤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值