Bootstrap

计算机网络基础

这是web开发入门知识

1.ip和端口

通过WiFi或者网线连接路由器时,会自动获得一个IP地址(通过DHCP协议自动分配),大家可以打开自己的电脑或手机,在设置中查看自己的IP地址一个IPv4地址一共有4段,每段8个bit位,一共32位,和基本类型int一致。也就是说,IPv4能够表示的地址范围,以十进制表示就是:0.0.0.0255.255.255.255,理论上如果全部拿来使用的话,大约有43亿个地址可用。但是实际上我们能够使用的地址非常有限,国际上根据不同类型的网络用途,对网段进行了划分,主要分为 A、‌B、‌C、‌D、‌E 五类,‌每类地址都有其特定的用途和特点:

  • A类地址:‌以0开头,‌网络地址空间长度为7位,‌主机地址空间长度为24位。‌A类地址的范围是从1.0.0.0到127.255.255.255。‌A类IP地址适用于有大量主机的大型网络,‌每个A类网络的主机地址数多达16,000,000个。‌
  • B类地址:‌以10开头,‌网络地址空间长度为14位,‌主机地址空间长度为16位。‌B类IP地址的范围是从128.0.0.0到191.255.255.255。‌B类地址适用于一些国际性大公司与政府机构等,‌每个B类网络的主机地址数多达65536个。‌
  • C类地址:‌以110开头,‌网络地址空间长度为21位,‌主机地址空间长度为8位。‌C类IP地址的范围是从192.0.0.0到223.255.255.255。‌C类IP地址特别适用于一些小公司与普通的研究机构,‌每个C类网络的主机地址数最多为256个。‌
  • D类地址:‌以1110开头,‌不标识网络,‌用于其他特殊的用途,‌如多目的地址。‌D类IP地址的范围是从224.0.0.0到239.255.255.255。‌
  • E类地址:‌以11110开头,‌暂时保留用于某些实验和将来使用。‌E类IP地址的范围是从240.0.0.0到255.255.255.255。‌

虽然IPv4地址能够表示的范围已经很广了,但是随着我们科技的发展,越来越多的设备要加入到互联网中,如果所有设备都分配一个独一无二的IP地址,那么肯定是不够用的,所以说在我们国家,一般很少会给个人一个可以直接使用的IP地址(我们也称为公网IP地址,也就是在互联网中的一个独一无二的IP地址)实际上我们家里的宽带上网,一般都是一个小区一栋楼或者一整个小区共用一个IP地址去与互联网上的资源交互。而路由器分配给我们的IP地址,实际上是一个局域网IP地址,局域网顾名思义就是一个局部的网络,这个网络是独立的,所有IP地址也仅仅属于这个局域网内部。

         端口是计算机网络中用来识别应用程序和服务的逻辑通道。在网络通信中,每一个计算机都  会有一个或多个端口,用于传输数据和与其他计算机进行通信。每个端口都有一个数字来标识,常见的端口号范围是0到65535,其中0到1023是系统保留端口,用于一些常见的服务,比如HTTP服务使用的端口80,FTP服务使用的端口21等,在Linux或MacOS下普通用户无权使用。通过端口,不同的应用程序可以同时在计算机上运行并与其他设备进行通信。

TCP协议:

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层协议。TCP协议在互联网中起着非常重要的作用,它保证了数据的可靠传输,通过数据确认、重传机制和拥塞控制来确保数据的完整性和可靠性。TCP协议的特点包括:

  1. 面向连接:在数据传输前需要建立连接,在数据传输结束后需要释放连接。
  2. 可靠性:通过数据确认和重传机制,确保数据能够按照正确的顺序到达目的地。
  3. 拥塞控制:通过拥塞窗口和慢启动等机制,避免网络拥塞和数据丢失。
  4. 全双工通信:允许双方同时发送和接收数据。
  5. 流量控制:根据接收方的处理能力,控制发送数据的速度,避免数据丢失。

TCP是我们传输层的常用协议之一,通信双方可以使用TCP协议建立连接,连接建立之后就可以互发数据了,它的最大特性就是可靠连接,为什么可靠,我们先从TCP连接的建立说起。要建立一个TCP连接,并不是直接告诉对方我们要开始发数据就完事的,它会经历几个阶段:

 

建立TCP连接时主要包括以下三个步骤:

  1. 第一步:客户端向服务器发送SYN(同步)包,请求建立连接。客户端发送的包中包含一个初始序列号,用于数据传输的顺序编号。
  2. 第二步:服务器接收到客户端发送的SYN包后,向客户端发送 SYN + ACK 包作为应答。服务器发送的包中除了确认客户端的SYN包外,还会发送自己的初始化序列号。
  3. 第三步:客户端接收到服务器发送的 SYN + ACK 包后,向服务器发送 ACK 包以确认连接。这时TCP连接建立成功,双方可以开始传输数据了。

可以看到,要建立一个TCP连接需要传递三次数据包(三次握手)才能完成,为什么要设计得怎么复杂呢,我们说TCP是一个可靠连接,一定要验证连接是可靠的,是可以相互顺利发送数据的,如果缺少这三次的任意一次,会发生什么?

  • 如果只进行一次握手,我们无法确定对方是否收到了我们的数据包或是对方的环境可以建立可靠连接,这种情况下如果强行建立连接的话,肯定是无法保证稳定性的。
  • 如果只进行两次握手,也就是说收到对方返回的ACK之后就建立连接的话,看似没问题,但是对方无法确定我们是否收到的其确认信息,这对我们来说确实没问题,但是对方慌得一匹啊,对方不知道这个数据包是否成功被我们收到,万一网络有问题我们这边真的没收到数据包呢。

因此,三次握手任何一步都不能忽略,这样才能保证可靠连接。

那么三次握手机制我们了解了,我们接着来看TCP连接是如何结束的,这里就要讲到四次挥手了:

关闭TCP连接时主要包含以下步骤:

  1. 第一步:客户端(或主动关闭方)发送一个带有FIN(完成)标志的报文段给服务器,表示它已经完成数据的发送工作,不再需要保持连接。
  2. 第二步:服务器(或被动关闭方)接收到FIN报文,回复一个带有ACK(Acknowledgment)标志的报文段,表明它已经收到关闭请求,但是由于连接可能还在使用,比如有数据包还没发完之类的,所以此时不会立即关闭。
  3. 第三步:服务器处理完手头剩下的事情后,发送一个带有FIN标志的报文段给客户端,表示它也准备好关闭连接。
  4. 第四步:客户端收到服务器的FIN报文,发送一个带有ACK标志的报文段作为最终确认。此时,客户端进入 TIME_WAIT 状态,一段时间后才真正关闭连接,目的是确保服务器能够收到ACK报文。

可以看到,要关闭一个TCP连接相比建立来说,更加复杂,它一共需要发送四次数据包(四次挥手)那么为什么要设计得这么复杂呢?我们还是来一步一步分析:

  • 如果只进行一次挥手,也就是客户端直接跟服务端说我们分手,服务端此时可能根本没收到关闭的信息,客户端就直接关闭了,这显然不符合TCP连接可靠稳定的标准,所以跟握手一样,肯定要等回复。
  • 如果只进行两次挥手,也就是说当服务端回复之后,客户端直接关闭连接,虽然这样能确定对方也想关闭连接,但是此时可能还有一些数据没发送完成,如果贸然关闭会导致数据发一半就没了,同样不符合TCP连接可靠稳定的标准。
  • 如果只进行三次挥手,也就是此时对方已经发送了ACK和FIN信息了,此时客户端确实可以放心大胆关闭了,但是服务端这边依然无法确定客户端是否可以完全结束,万一客户端还有啥事没办呢(注意第一次挥手只是客户端觉得可以结束然后请求,并不是数据全都发完了,别搞混了)所以说仍有资源泄露或数据丢失的风险,不符合TCP连接可靠稳定的标准。

现在我们了解了TCP连接的连接和关闭,我们接着来看TCP连接的数据发送,由于TCP协议是传输层协议,我们前面说过在每一层都会有对应的头部信息拼接到数据包中,TCP协议同样会为数据包添加自己的头部信息

头部信息各个部分介绍如下,大概了解一下就行:

  1. 源端口(Source Port):16位,用于标识发送端的应用程序或进程。
  2. 目标端口(Destination Port):16位,用于标识接收端的应用程序或进程。
  3. 序列号(Sequence Number):32位,标识从发送端向接收端发送的字节流中的第一个字节的位置。
  4. 确认序列号(Acknowledgment Number):32位,仅在ACK标志为1时有效,指示期望接收的下一个字节的位置,表示已经成功接收到的数据的最后一个字节。
  5. 数据偏移(Data Offset):4位,表示TCP头部的长度,单位为4字节。该值最小为5,最大可达15,表明TCP头部长度在20到60字节之间。
  6. 保留(Reserved):3位,保留为将来使用,应设置为0。
  7. 标志位(Flags,或Control Bits):9位,控制标志包含以下若干子标志:
    • URG:紧急指针(urgent pointer)有效。
    • ACK:确认序列号有效。
    • PSH:接收方应该尽快将数据推送到应用层。
    • RST:重置连接。
    • SYN:同步序列号,用于建立连接。
    • FIN:发送方已完成发送数据,用于终止连接。
  8. 窗口大小(Window Size):16位,用于流量控制,指示接收方当前能够接收的数据量(以字节为单位)。
  9. 校验和(Checksum):16位,检验头部和数据的完整性。
  10. 紧急指针(Urgent Pointer):16位,仅在URG标志有效时使用,指示紧急数据的结束位置。
  11. 选项(Options):可变长度(最多40字节),用于携带诸如最大报文段长度(MSS)、时间戳等选项。
  12. 填充(Padding):为了确保TCP头部长度为4字节的整数倍,填充的数据。

那么数据是如何相互发送的呢?这就简单了,每次发送数据只需要给一个响应即可:

当三次握手完成后,此时就可以进行数据发送,我们的应用程序发送数据时,实际上首先会将数据发送到TCP发送缓冲区中,然后,TCP协议栈会负责将这些数据按照协议信息分片、打包,并逐步发送出去。这样可以极大地优化数据的传输效率,可以想像一下一堆很小的数据一个一个发和缓存好了一次性发有什么区别。同样的,接受数据时也可以像这样先缓存一下再一起让应用程序读取。

只不过,虽然这种缓冲机制能够一定程度上优化数据的传输,但是有时候也会造成一些麻烦,最常见的就是数据粘包和拆包问题:

  1. 发送方可能会为了优化网络效率,将一些小数据包合并成一个更大的数据包发送出去,导致接收方收到的直接就是一个大数据包,但是这可能是两条完全独立的消息。
  2. 接收方可能会将两个或多个TCP段中的数据合并在一起进行读取,同上。

常用的解决粘包问题的方法包括:

  1. 定长消息:每个消息都以固定长度发送,接收方只要按照固定长度读取数据即可,但这种方法不适用于可变长度数据。
  2. 分隔符:在每个消息之间添加特殊的分隔符,接收方可以根据分隔符判断消息的边界。例如,使用\n 或者其他特殊字符。
  3. 消息头部添加长度信息:在每个消息前增加一个字段表示消息长度,接收方先读取长度字段,然后按照长度字段读取完整消息。
  4. 应用层协议设计:设计上层协议时,考虑粘包与拆包,通过协议规范解决粘包问题。

UDP协议(较于TCP简单):

UDP(User Datagram Protocol,用户数据报协议)是一种无连接的、轻量级的传输层协议。UDP相比于TCP,不提供可靠的数据传输、数据流控制和错误恢复,但与TCP相比,UDP的头部较小,只有8字节,由于其简单高效的特点,适用于一些对实时性要求较高、数据传输容忍一定丢失的应用场景,比如:

  • 实时音视频传输:如VoIP(Voice over IP)、直播流媒体等。
  • DNS查询:UDP被用于域名解析查询,在保持连接性不那么重要的情况下,可以更快速地进行域名解析。
  • 简单数据传输:在一些对可靠传输要求不高的应用场景下,UDP可以简化实现,并减少网络开销。

 

报文格式如下:

  • 源端口:占用2个字节,表示UDP报文的源端口号。
  • 目的端口:占用2个字节,表示UDP报文的目的端口号。
  • 长度:占用2个字节,表示UDP报文的总长度,包括首部和数据部分。
  • 校验和:占用2个字节,提供对UDP报文的差错检测。

UDP报文格式简单高效,适合短消息传递等对实时性要求较高的通信场景。由于UDP本身不提供可靠性保证,因此在使用UDP时,应用程序需要自行实现数据完整性校验、丢包重传等机制来保证数据传输的可靠性。

HTTP协议:

这里的内容较多这里推荐:

柏码 - 让每一行代码都闪耀智慧的光芒!

在java中实现用户端和服务端传数据(基于tcp)

代码如下:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
       try(Socket socket = new Socket("localhost",8080);
        Scanner scanner = new Scanner(System.in)) {
           System.out.println("已经连接成功");
           OutputStream out = socket.getOutputStream();
           InputStream in = socket.getInputStream();
            while(true) {
                String line = scanner.nextLine();
                out.write(line.getBytes());
                int len;
                byte[] bytes = new byte[1024];
                len = in.read(bytes);
                System.out.println(new String(bytes,0,len));
            }
       }catch(IOException e){
           e.printStackTrace();
       }
    }
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        try(ServerSocket server =  new ServerSocket(8080)){;
       Socket socket= server.accept();
       System.out.println("接受来自用户端的连接"+socket.getInetAddress()+":"+socket.getPort());
       InputStream in = socket.getInputStream();
       OutputStream out = socket.getOutputStream();
       int len=0;
       byte[] buffer=new byte[1024];
       while((len=in.read(buffer))> 0){
           System.out.println("接受客户端数据:"+new String(buffer,0,len));
           out.write(("已收到长度:"+len+"字节数据:").getBytes());
       }

    }catch(IOException e){
            e.printStackTrace();
        }
    }
}

这里使用的是io的知识。

 

实现文件的传输:

代码如下:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
       try(Socket socket = new Socket("localhost",8080);
        Scanner scanner = new Scanner(System.in)) {
           System.out.println("已经连接成功,请输入传输地址:");
           OutputStream out = socket.getOutputStream();
           String path = scanner.nextLine();
           FileInputStream in = new FileInputStream(path);
           in.transferTo(out);
           in.close();
       }catch(IOException e){
           e.printStackTrace();
       }
    }
}
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        try(ServerSocket server =  new ServerSocket(8080)){;
       Socket socket= server.accept();
       System.out.println("接受来自用户端的连接"+socket.getInetAddress()+":"+socket.getPort());
       InputStream in = socket.getInputStream();
            FileOutputStream out= new FileOutputStream("test");
            long len ,total=0;
            byte[] buffer=new byte[1024];
            while((len=in.read(buffer))>0){
                total+=len;
                System.out.println("正在进行文件传输,当前已接受:"+total+"字节数据");
                out.write(buffer,0,(int) len);
                out.close();
            }

    }catch(IOException e){
            e.printStackTrace();
        }
    }
}

使用UDP通信

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
       try(DatagramSocket socket = new DatagramSocket();
       Scanner scanner = new Scanner(System.in);
       ){while(true) {
           String str = scanner.nextLine();
           byte[] data = str.getBytes();
           InetAddress address = InetAddress.getByName("localhost");
           socket.setSoTimeout(5000);
           DatagramPacket packet = new DatagramPacket(data, data.length, address, 8080);
           socket.send(packet);
       }
       }catch (IOException e){
           e.printStackTrace();
       };
    }
}
import java.io.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        try(DatagramSocket socket = new DatagramSocket(8080)) {
            while(true) {
                DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
                socket.receive(packet);
                System.out.println("接受来自:"+packet.getAddress()+":"+packet.getPort()+"的数据包:"+
                        new String(packet.getData(),0,packet.getLength()));
            }
        }catch (Exception e) {
            e.printStackTrace();
        };
    }
}

Http请求浏览器:

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) {
        try(ServerSocket server = new ServerSocket(8080)){
            Socket socket = server.accept();
            InputStream in = socket.getInputStream();
            while (!socket.isClosed()) {
                int i = in.read();
                if(i == -1) break;
                System.out.print((char) i);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

 

这里没页面添加页面:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) {
        try (ServerSocket server = new ServerSocket(8080)) {
            Socket socket = server.accept();
            OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream());
            String html = """
                    <!DOCTYPE html>
                    <html lang="en">
                    <head>
                        <title>测试网站</title>
                    </head>
                    <body>
                        <h1>欢迎访问我们的测试网站</h1>
                        <p>这个网站包含很多你喜欢的内容,但是没办法展示出来,因为我们还没学会</p>
                    </body>
                    """;
            writer.write("HTTP/1.1 200 OK\r\n");   //根据HTTP协议规范,返回对应的响应格式
            writer.write("Content-Type: text/html;charset=utf-8\r\n");  //务必加一下内容类型和编码,否则会乱码
            writer.write("\r\n");
            writer.write(html);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 打开localhost:8080

 

;