Java网络编程

一. 基础知识

组网方式

把多个计算机通过网线(传输介质)连接在一起就形成初级的网络
①基于网线直连
②基于集线器组建
集线器是一种网络设备, 只能单一线路分发
③基于交换机组建
④基于交换机与路由器组建

ip地址 : 端口号

①ip地址表示了主机在网络上的地址, 类似于收发快递时的收件人与发件人地址
②端口号表示了主机中某一个进程, 使用网络的进程在启动的时候系统都会分配一个端口号
端口号主动申请, 做为服务端. 被动分配, 做为客户端
③端口号的范围是1-65535, 但是1-1024是知名(有明确的程序已经占用)端口号, 我们自定义的程序一般不使用这个范围

网络通信中五元组的概念

源IP:标识源主机, 相当于发件地址
源端口:标识源主机中此时通信发送数据的应用程序, 相当于发件人
目标IP:标识目标主机, 相当于收件地址
目标端口:标识目标主要是此次通信接收数据的应用程序, 相当于收件人
协议号:标识发送进程与接收进程中双方约定的数据格式

TCP/IP五层(四层)模型

模型

有OSI七层网络模型,但只存在于课本
image.png
①应用层: 程序员关注的层, 主要工作就在这一层, 例如数据的编码与解码
②传输层: 操作系统工作在这一层, 主要是确定程序的端口号
相当于确定发件人与收件人
③网络层: 规划出一条端对端之间的路径, 包括其中需要经过的其他网络设备
比如发一个快递, 从西安到上海, 包括途中的中转站. 相当于确定了发送方的IP, 接收方IP. 五元组到这一层就确定下来了
④数据链路层: 解决的是点到点的传输
⑤物理层: 相当于网线和其他网络设备. 对于物流来说相当于公路或铁路.

分装和分用过程

以QQ发送消息为例, 模拟一下消息在网络中的封装与分用过程
在网络传输的过程中, 每一个网络节点都会进行封装和分用, 最终才到达目标
①应用层
对于应用层协议的定义, 双方要按照相同的规则去组织与解析数据. 应用层会把消息组织好, 统一发送给操作系统的API(传输层)socket api
image.png
②传输层
在传输层中有几个非常著名的协议: TCP, UDP. 系统操作可以确认下来源端口号
image.png
③网络层
确定了源IP和目标IP
image.png
④数据链路层
帧头一般记录MAC地址, 每一个MAC地址都不相同, 每个硬件厂商都会被分配一段地址, 生产出来网络设备都在这个范围中
帧尾记录校验和, CRC校验, 把每一个BYTE做累加操作, 最终会得到一个值. 接收方也会同时的操作累加值, 如果得到的值与校验和相等, 那么就证明数据本身没有被改过. 过程中可能会出现溢出, 但不影响结果
image.png
⑤物理层
把以上的内容转换成光信号、电信号在网络设备上传输. 到此, 消息体就在网络上开始传输
⑥物理层
把光信号和电信号, 还原成数据链路层可以解析的格式
⑦数据链路层
image.png
帧头记录的MAC地址, 可以找到对应的主机. 帧尾校验和, 能校验来验数据的有效性
脱到帧头与帧层把载荷交给网络层
⑧网络层
image.png
根据IP协议头得到目标主机的IP. 之后脱掉IP协议头, 把数据交给传输层
⑨传输层
image.png
这里已经进入了操作系统中. 通过TCP中的目标端口确认应用程序(进程)
⑩应用层
image.png
应用程序按照自定义的协议格式来解析消息体, 完成通信

二. 网络编程

怎么进行网络编程

针对网络编程, 操作系统提供了用于网络编程的技术, 称为Socket套接字, 是系统提供的专门用来实现网络编程的一套API.
应用程序在应用层, 操作系统工作在传输层, Socket套接字就是传输层对应用层提供的API支持. 传输层中最知名的协议就是TCP和UDP.
JAVA对每种操作系统做了进一步的封装JDK中提供的API是我们学习的目的

Socket套接字

操作系统工作在传输层

TCP与UDP区别

流套接字TCP

使用传输层TCP协议. TCP, 即Transmission Control Protocol (传输控制协议), 传输层协议.
TCP协议的特点:
①有连接②可靠传输③面向字节流④全双工(有接收缓冲区, 也有发送缓冲区)⑤大小不限
传输数据是基于lO流, 没有边界多次发送, 多次接收

数据包套接字UDP

使用传输层UDP协议. UDP, 即User Patagram Protocol(用户数据报协议), 传输层协议.
UDP协议的特点:
①无连接②不可靠传输③面向数据报④全双式(有接收缓冲区, 也有发送缓冲区)⑤大小受限, 一次最多传输64K.
传输数据是一个整体一个整体, 不能分开发送

距离对比TCP和UDP

①有无连接
TCP相当于打电话, 接听方必须接通电话,是双方才可以通信
UDP相当于发短信, 对方有没有开机都不重要
②可靠传输
相当于: 打电话时必须要经过拨号, 接听后才可以通话, 发短信发出去了就不管了.如果数据包在传输的过程中丢了, TCP有重传机制, UDP丢了就没了
③面向字节流&面向数据报
面向字节流: 打电话说一个字对方就能听到一个字
面向数据报: 发短信一条信息必须发出去了对方才可以接收到才可以阅读
④双全工(有接收缓冲区也有发送缓冲区)
可以打电话也可以接电话, 可以发短信也可以接短信
⑤大小不限
打电话打完了挂了就可以了, 时间不受限制
发短信, 短信的大小有限制, 例如超过150个字会自动分成两个短信发

Java中使用UDP

类和方法:DatagramSocket

用于创建一个UDP数据报套接字

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

普通方法
方法说明
void receive(DatagramPacket p)从套接字接收数据报(如果没接收到数据报, 该方法会阻塞等待
void secd(Datagram p)从此套接字发送数据报包(不会阻塞等待, 直接发送)
void close()关闭此数据报套接字

类和方法:DatagramPacket

用于用于UDP Socket发送和接收的数据报(个人理解成存放数据的包)

构造方法
方法说明
DatagramPacket(byte[] buf, int length)用于接收: 构造一个DatagramPacket用来保存接收数据报, 接收的数据保存在字节数组buf里, 接收指定长度length
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)用于发送: 构造一个DatagramPacket来发送数据报, 发送的数据在字节数组buf里, 发送从0到length的长度
InetAddress getAddress()从接收的数据报里, 获取发送端主机的IP地址
或者从发送的数据报中, 获取接收端的主机IP地址
int getPort()从接收的数据报里, 获取发送端主机的端口号
或者从发送的数据报中, 获取接收端的主机端口号
byte[] getData()获取数据报里的数据

实现一个简单UDP回显服务器与客户端

功能就是发送什么数据, 接收什么数据

服务器端

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;

//回显服务器
public class UDPEchoServer {
    // 定义一个用于服务器端的DatagramSocket
    private DatagramSocket server;

    /**
     * 构造方法,完成服务器的初始化
     * @param port 端口号
     */
    public UDPEchoServer (int port) throws Exception {
        if (port > 65535 || port < 1024) {
            throw new Exception("端口号必须在1024 ~ 65535之间");
        }
        // 初始化服务器端的UDP服务
        this.server = new DatagramSocket(port);
    }
    //对外提供服务
    public void start () throws IOException {
        System.out.println("服务器已启动....");
        // 循环接收用户的请求
        while (true) {
            // 1. 创建一个用于接收请求数据的DatagramPacket
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
            // 2. 接收请求, 把真实的内容填充到requestPacket
            server.receive(requestPacket);
            // 3. 从requestPacket获取数据
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
            // 4. 根据请求获取响应
            String response = processor (request);
            // 5. 把响应封装到DatagramPacket
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
                    response.getBytes().length, requestPacket.getSocketAddress());
            // 6. 发送数据
            server.send(responsePacket);
            // 7. 打印日志
            System.out.printf("[%s:%d] request: %s, response: %s.\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }
    public String processor(String request) {
        return request;
    }
    public static void main(String[] args) throws Exception {
        // 初始化服务器
        UDPEchoServer server = new UDPEchoServer(9999);
        // 启动服务
        server.start();
    }
}

客户端

1.第50行可以写网络上其他的计算机地址,例如同局域网两台电脑之间可以分别运行服务端和客户端
2.可以修改idea设置从而同时运行两个客户端,而不会"终止再运行"

import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class UDPEchoClient {
    // 定义一个用于客户端的DatagramSocket
    private DatagramSocket client;
    // 定义服务器的IP地址
    private String serverIp;
    // 定义服务器的端口号
    private int port;
    private SocketAddress address;
    /**
     * 构造方法,指定服务器的Ip地址和端口号
     *
     * @param serverIp 服务器IP
     * @param port 端口号
     */
    public UDPEchoClient (String serverIp, int port) throws SocketException {
        this.client = new DatagramSocket();
        this.serverIp = serverIp;
        this.port = port;
        this.address = new InetSocketAddress(serverIp, port);
    }
    public void start () throws IOException {
        System.out.println("客户端已启动.");
        // 循环接收用户的输入
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("->");
            // 接收用户输入
            String request = scanner.next();
            // 1. 把请求内容包装成DatagramPacket
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(StandardCharsets.UTF_8),
                    request.getBytes().length, address);
            // 2. 发送数据
            client.send(requestPacket);
            // 3. 接收响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
            // 4. 在receive方法中填充响应数据
            client.receive(responsePacket);
            // 5. 解析响应数据
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8");
            // 6. 打印日志
            System.out.printf("request: %s, response: %s.\n", request, response);
        }
    }
    public static void main(String[] args) throws IOException {
        UDPEchoClient client = new UDPEchoClient("127.0.0.1", 9999);
        // 启动服务
        client.start();
    }
}

翻译服务器

修改回显服务器的响应方法,实现翻译操作

import java.util.HashMap;
import java.util.Map;

public class UDPDictServer extends UDPEchoServer{
    private Map<String,String> map = new HashMap<>();
    public UDPDictServer(int port) throws Exception {
        super(port);
        //初始化字典内容
        map.put("dog","小狗");
        map.put("cat","小猫");
        map.put("pig","小猪");
        map.put("tiger","老虎");
        map.put("veryGood","nb");
    }
    @Override
    public String processor(String request) {
        return map.getOrDefault(request,"无法翻译");
    }
    public static void main(String[] args) throws Exception {
        UDPDictServer server = new UDPDictServer(9999);
        server.start();
    }
}

Java中使用TCP

类和方法: ServerSocket

ServerSocket是创建TCP服务端Socket的API

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

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

类和方法: Socket

Socket是客户端Socket, 或服务端中接收到客户端建立连接(accept方法)的请求后, 返回的服务端Socket. 不管是客户端还是服务端Socket, 都是双方建立连接以后, 保存的对端信息, 及用来与对方收发数据的.

构造方法
方法说明
Socket(String host, int port)创建一个客户端流套接字Socket, 并与对应ip的主机上的对应端口建立连接(通过ip和端口可以确定网络上的主机和进程

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

补充

①格式化输出
MessageFormat.format("{0},{1},...,{n}",a,b,...,x)
{?}代表占位符, 编号从0开始, 后面的参数代表要替换的占位符, 记得顺序要对
②telnet工具
在"启用或关闭Windows功能"里, 可以快速连接到指定端口, 语法:
telnet ip地址 端口号
例如telnet 192.168.1.86 9999:连接到IP地址为192.168.1.86计算机的9999号端口

实现一个简单TCP回显服务器

建立连接后, 当有新的客户端时, 无法继续连接, 所以要使用多线程来处理多客户端

服务器端

该服务器可以同时连接多个用户, 实现聊天功能

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ProtocolException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.MessageFormat;
import java.util.Scanner;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

//基于TCP的服务器程序
public class TCPEchoServer {
    // 声明一个用于服务端的Socket对象
    private ServerSocket server;

    //通过指定端口号实例化服务
    public TCPEchoServer(int port) throws IOException {
        if (port < 1025 || port > 65535) {
            throw new RuntimeException("端口号要在 1025 ~ 65535之间.");
        }
        // 实例化ServerSocket并指定端口号
        this.server = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动成功...");
        // 创建一个线程池
        ThreadPoolExecutor poolExecutor = new 
        ThreadPoolExecutor(3, 10, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        // 循环接收客户端的连接
        while (true) {
            Socket clientSocket = server.accept();
            // 每接收到一个新连接请求,就创建一个新的子线程
            //            Thread thread = new Thread(() -> {
            //                // 处理Socket中的数据
            //                try {
            //                    processConnections(clientSocket);
            //                } catch (IOException e) {
            //                    e.printStackTrace();
            //                }
            //            });
            //            // 启动线程
            //            thread.start();

            // 提交任务到线程池中
            poolExecutor.submit(() -> {
                try {
                    processConnections(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });

        }
    }

    // 处理数据
    private void processConnections(Socket clientSocket) throws IOException {
        // 打印日志
        String clientInfo = MessageFormat.format("[{0}:{1}] 客户端已上线",
                clientSocket.getInetAddress(),
                clientSocket.getPort());
        System.out.println(clientInfo);
        // 处理数据之前要获取一下输入输出流
        //写try里面,结束会自动关闭流
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 循环处理用户的请求
            while (true) {
                // 通过Scanner读取用户请求中的数据
                Scanner requestScanner = new Scanner(inputStream);
                if (!requestScanner.hasNextLine()) {
                    // 日志
                    clientInfo = MessageFormat.format("[{0}:{1}] 客户端已下线.",
                            clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    System.out.println(clientInfo);
                    break;
                }
                // 获取真实的用户请求数据
                String request = requestScanner.nextLine();
                // 根据请求计算响应
                String response = process(request);
                // 把响应写回客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 写入输出流
                printWriter.println(response);
                // 强制刷新缓冲区
                printWriter.flush();
                // 打印日志
                clientInfo=MessageFormat.format("[{0}:{1}], request: {2}, response: {3}",
                        clientSocket.getInetAddress(),
                        clientSocket.getPort(),
                        request,
                        response);
                System.out.println(clientInfo);

            }


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }

    }
	//可以两个人一个在服务端,一个在客户端互相聊天
    private String process(String request) {
        System.out.println("收到新消息:"  + request);
        Scanner scanner = new Scanner(System.in);
        String response = scanner.nextLine();
        return response;
    }

    public static void main(String[] args) throws IOException {
        TCPEchoServer server = new TCPEchoServer(9999);
        server.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;

/**
 * @Author 比特就业课
 * @Date 2023-05-23
 */
public class TCPEchoClient {
    // 定义一个用于客户端的Socket对象
    private Socket clientSocket;

    /**
     * 初始化客户端的Socket
     *
     * @param serverIp 服务器IP地址
     * @param serverPort 服务器的端口号
     * @throws IOException
     */
    public TCPEchoClient (String serverIp, int serverPort) throws IOException {
        this.clientSocket = new Socket(serverIp, serverPort);
    }

    public void start () throws IOException {
        System.out.println("客户端已启动...");
        // 获取Socket中的输入输出流
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 循环处理用户的输入
            while (true) {
                System.out.println("->");
                // 接收用户的输入内容
                Scanner requestScanner = new Scanner(System.in);
                String request = requestScanner.nextLine();
                // 发送用户的请求
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                // 强制刷新缓冲区
                printWriter.flush();
                // 接收服务器的响应
                Scanner responseScanner = new Scanner(inputStream);
                // 获取响应数据
                String response = responseScanner.nextLine();
                // 打印响应内容
                System.out.println("接收到服务器的响应:" + response);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }

    public static void main(String[] args) throws IOException {
        TCPEchoClient client = new TCPEchoClient("127.0.0.1", 9999);
        client.start();
    }
    
}	

三. 网络原理

介绍TCP/IP协议中每一层的核心内容

应用层

例如上面实现的TCP服务端和客户端, 发送双方确定的应用层协议就是以换行符作为每条消息的结尾. 所以此时就涉及到发送时以换行符进行编码, 接收时以换行符进行解码.
在开发应用程序时, 一个重要的工作就是进行协议(组织数据的格式)的确定. 多个程序相互通信, 就需要实现对方的编码格式, 于是就有人就对常用的应用场景做了一些特殊的协议, 并确定下来, 形成了标准常见的应用层协议, 例如http,ftp…

协议的设计

如果有一个外卖的信息, 他的组织方式可以有:
经度,纬度,商品类别,电话: 用逗号分割每一个属性, 或者
商家信息,商品图片,商品信息,距离\n: 接收响应后先按\n把每一条消息解析出来, 然后按照逗号解析每一个属性.

常见的标准协议

(1)XML协议

主要是一种组织数据的格式.

<!--定义一个对象-->
<person>
  <!-- 对象中的属性-->
	<name>张三</name>
  <age>18</age>
  <school>西安XX大学</school>
  <sn>10003</sn>
  <!--表示一个集合-->
  <all_courses>
  	<course>JAVA SE</course>
    <course>数据结构</course>
    <course>MYsQL</course>
    <course>JAVA EE</course>
  </all_courses>
  <!-- 嵌套对象-->
  <class>一班</class>
<person>

在XML文件中, 每一个标签都是成对出现的, 闭合标签带一个/
如果一个标签中含有子标签, 那么这个标签就可以表示一个对象
如果一个标签中包含多个相同的子标签, 那么这个标签就表示集合

缺点:

①结构复杂 ②不美观 ③冗余字符太多, 在网络传输中比较耗费带宽

(2)JSON协议

组织数据的一种格式, 目前JSON格式的使用非常广泛, 大部分代替了XML

{
  "name ":"张三",
  "age":18,
  "school":"西安XXX大学",
  "sn":10003,
  "all_courses" :[
    "JAVA SE",
    "MYSQL",
    "数据结构",
    "JAVA EE",
  ],
	"class":"一班"
}

用{}表示一个对象
用[]表示一个集合
属性用"key":"value"表示(如果是整形就不用加引号
多个属性用逗号隔开, 最后一个属性不用加逗号

优点:

①可读性好 ②美观 ③扩展性强

缺点:

①引入了额外的字符, 占用带宽较大

其他协议

①Google : protobuf
②IBM : MQTT协议: 消息队列遥测传输, 针对物联网应用设计的协议. 是以字节的方式组织, 其中固定的长度表示着不同的含义, 省略了多余的字符,节省了报文长度, 在网络传输过程中大幅减少了带宽, 但是编码与解析过程比较复杂.

传输层

传输层协议

核心的协议有两个:
①UDP : 无连接, 不可靠传输, 面向数据报, 全双工, 大小受限
②TCP : 有连接, 可靠传输, 面向字节流, 全双工, 大小不限

格式

UDP格式
源端口号目的端口号UDP长度校验和数据长度
16位16位16位16位UDP写的长度
UDP是传输层协议, 是由操作系统实现, 操作系统管理进程, 进程开放端口号<br />16位最大可以表示65535, `16位UDP长度`意味着`65535byte`约等于`64KB`. `16位UDP校验和`通过对数据(byte数组)中的每一个byte累加, 得到的值, 用来检验数据传输是否有错误<br />在解析UDP报文件时, 先截16位表示源端口, 再截16位表示目的端口号.......

TCP格式

tcp.png
注意, 实际上是一个长条, 因为排版问题叠放一起
4位首部长度: 指的是TCP报文段头部的长度, 以32位(4字节)为单位计算. 该字段占4个bit, 可以表示0~15, 因此最大值为15*4=60字节. TCP报文段头部的长度是可变的, 它包含了一些必需的信息, 例如源端口号,目的端口号,序列号,确认号,窗口大小等等. 因此, TCP头的长度取决于这些信息的数量和类型. TCP报文段的最小长度为20字节(没有选项字段), 如果有选项字段, 则会增加头部的长度. TCP的首部长度字段是用来标识TCP数据报头的长度, 以便接收方能够知道从哪里开始解析后面的数据. 在TCP报文段中, 首部长度字段和其他控制标志共同组成了TCP报文段的第一个32位字. 通过前4位, 接收方就能够读取到TCP报文段的首部长度, 从而正确地解析出后续的TCP数据.
保留(6位): 目前没用, 是协议设计之初留的冗余区域
6个标志位:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
选项: 自定义的信息, 例如公司信息

TCP的安全和效率机制

确认应答(可靠机制)

image.png
一般来说, 一问一答. 但是有可能由于网络的原因, 导致消息顺序乱序, 导致歧义.
TCP为了解决这个问题, 对每个字节编了号. 对于图中, 32位序号表明已经收到了1000字节数据, 32位确认序号表明下次的消息是从1001开始. 当有很多消息的时候, 根据32位序号32位确认序号就能正确匹配哪个消息是响应哪个请求的了. 而且发送方可以根据32位确认序号得知对方收到了自己发的消息
同时对于发送消息, 标记为SYN, 响应消息标记为ACK.

超时重传(可靠机制)

消息在网络传输过程中, 会经过很多操作系统和很多硬件设备, 每个设备都有负载能力, 若超出了范围, 数据包可能就会被阻塞或丢弃.
发送方丢包
image.png
当发送SYN请求后, 等待了一会发现还没有收到ACK, 就会在特定的时间间隔后重新发送.
接收方丢包(响应超时):
image.png
主机B接收到了数据, 并发出ACK响应. 但对于主机A来说, 不知道自己发没发成功, 都会超时重传. 而主机B会有重复接收的问题. 所以主机B在自己的缓冲区中会过滤掉重复数据. 并直接给出ACK应答.
③对于超时时间,
如果设置太长, 会影响整体重传效率. 如果时间太短, 有可能造成频繁发送重复的包.
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍. 如果重发一次之后, 仍然得不到应答, 等待2500ms后再进行重传. 如果仍然得不到应答, 等待4500ms进行重传. 依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.

连接管理(可靠机制)

主机之间作为发送方, 与接收方在网络通信, 必须要确认双方收发数据的能力, 其中涉及到建立连
接与断开连接的协商过程.

三次握手

image.png
发送方发送SYN, 接收方响应ACK, 此时就可证明发送方具备发送能力, 接收方具备响应能力.
接收方发送SYN, 接收方响应ACK, 此时就可证明接收方具备发送能力, 发送方具备响应能力.
通过这两次SYN和ACK过程, 可保证双方网络没问题. 在此基础上可进行正常的数据发送与接收
可以优化一下, 做到真正三次握手
image.png
把SYN和ACK合并成一次通信完成, 从而提高效率.
问题: 三次挥手的过程? 三次挥手的过程能够简化成两次吗? 四次如何实现?
两次是不可以的, 因为没有完整的验证双方的收发能力. 四次是可以的.
三次握手还有一个重要功能就是协商序列号从哪开始.
image.png
发送方发送SYN请求, 那么SYN标志位记为1, 同时随机生成32位序号.
接收方接收到了请求, 然后发送ACK响应, 此时会把32位确认序号加1, ACK标志位记为1. 由于合并了操作, 所以这条消息既是ACK也是SYN, 所以把SYN标志位记为1, 并随机生成32位序号, 然后发送消息.
发送方收到了ACK证明自己通讯没问题, 然后又根据SYN发送ACK帮助接收方确认通讯, 于是32位确认序号加1, ACK标志位记为1, 发送消息给接收方. 接收方收到了ACK代表接收方通讯没问题, 此时就完成了三次握手.

端口状态

服务端状态转化:
[CLOSED -> LISTEN]: 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列 中, 并向客户端发送SYN确认报文.
[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状 态, 可以进行读写数据了. (established adj.已确立的)
[ESTABLISHED -> CLOSE_WAIT]当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据). 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入 LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.
客户端状态转化:
[CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
[SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进 入FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.

四次挥手(断开的过程)

image.png
第二个FIN丢包了如何处理?
如果丢包会触发超时重传.image.png
客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1; 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段; 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK; 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.
(A:我要关了FIN B:好的ACK A:(嗯) B:我也关了FIN A:嗯ACK B:(我已关闭) A:(等了一会,关闭))

滑动窗口(效率机制)

数据能通过一发一收的过程, 是可以保证正常通信的, 但是效率不高. 那么我们一次发送多条数据, 就可以大大的提高性能.
image.png
这个案例中一次性连续发了四个(16位窗口大小决定)SYN请求.
窗口大小指的是, 无需等待确认应答,而可以继续发送数据的最大值. 上图的窗口大小就是4000个字节(四个段). 发送前四个段的时候, 不需要等待任何ACK, 直接发送; 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉; 窗口越大, 则网络的吞吐率就越高
image.png
滑动窗口本身是一种数据结构, 维护窗口的大小, 以及已经发送和正在发送的数据块.

丢包问题

①数据包已经抵达, ACK被丢了.
image.png
1001的应答包丢了没关系, 当2001的包送到了之后, 可以说明1001已经送到了, 不然也不会收到2001的响应
②数据包就直接丢了.
image.png
在接收数据的过程中, 如果发现32位序号缺失了一部分, 那么就会一直发ACK来向发送方索要缺失部分的数据. 此时收到其他窗口的数据就会被缓存起来, 等要到了缺失的数据, 再把数据组织完整, 再把缓存的数据拼在后面.

效率问题

①效率的高低取决于窗口的大小
②窗口越大效率越高
③窗口越小效率越低
④假设窗口无穷大, 此时发送方就完全不需要等待ACK, 此时效率就和UDP一样
滑动窗口倒低取多大合适?->流量控制

流量控制(可靠机制)

主要是确定滑动窗口的大小, 通过发送方与接收方动态协商来确认
image.png
每个程序在启动时都会去申请系统资源, 发送与接收缓冲区(内存中的一片区域, 用来存放BYTE数据流)就是申请来的资源
image.png
在ACK中, 把缓冲区的剩余大小, 填充到16位窗口大小协议字段. 通过接收方反制发送方对于窗口大小的限制, 发送方不能为了提高效率而无限制的扩大窗口大小.
已使用空间与剩余空间的大小是动态变化的, 每次接收方从缓冲区中读取数据之后, 剩余空间就会变大
如果接收方的处理能力比较低, 可能会出现缓冲区装满的情况
image.png
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项段中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位, 就可以设置很大的窗口.

拥塞控制(可靠机制)

虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题. 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的. TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据.
image.png
此处引入一个概念程为拥塞窗口. 发送开始的时候, 定义拥塞窗口大小为1. 每次收到一个ACK应答, 拥塞窗口加1; 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小(16位窗口大小)做比较, 取较小的值作为实际发送的窗口; 像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快. 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍. 此处引入一个叫做慢启动的阈值, 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
image.png
①发送方第一次发送数据, 窗口大小是1
②接下来的每次发送, 窗口大小以指数形式扩大
③当达到初始阈值时, 就变成了线性的形式增长, 每次+1
④当窗口到达某个阈值时, 出现了大量的丢包现象, 也就是频繁出现超时重传, 就说明网络出现了堵塞
⑤阻塞时, 窗口大小直接回到最小值1, 新拥塞窗口阈值会被调整为 当前拥塞窗口值/2
⑥重复①~⑤步

延迟应答(效率机制)

接收方并不是马上就给发送方返回应答. 接收方在不停的处理数据, 未使用部分在不断的加大, 通过延迟应答可以把最新的未使用大小返回给发送方, 从而提高窗口大小. 提升网络收发效率.
对于收到的请求, 不一定每次都应答, 可以接收两次请求, 应答一次, 例如第2,4,6,8…次应答. 但如果发送了奇数次, 那么最后一次就永远不应答了. 于是还要有时间限制, 系统有一个默认值, 超过最大延迟时间就应答一次
具体的数量和超时时间, 根据操作系统都有不同的差异, 一般间隔数量为2, 超时时间为200ms

捎带应答(效率机制)

由于延迟应答机制的存在, 可能存在把SYN报文和ACK报文同时发送的情况, 那么系统就会把两个报文合二为一. (例如三次握手)

面向字节流

由于TCP是面向字节流, 接收方的缓冲区不同消息的内容都是紧挨在一起的, 分不清哪句话是哪个消息的.
image.png
类似于这种不能有效区分消息边界的现象叫做"粘包问题", 解决方法有两种:
①在消息的末尾加上特殊的分隔符来标识消息结束
在使用的时候按特殊字符去截取缓冲区的内容
例如JSON用大括号来包裹消息, 那么就可以理解为他是使用大括号做为特殊字符来表示消息结尾的
HTTP应用层的协议, 既使用分隔符也使用了表示消息长度的字段解决粘包问题
②使用一个专门用来描述消息体长度的字段, 来标识消息体的具体长度

42“长度为42字节的消息”12“长度12字节的消息”

当读取消息之前, 先把4byte的表示消息体长度的字段内容读出来, 值=42
继续在缓冲区里读42个字节, 这42个字节就可以表示消息的内容
再读4byte表示下一个消息的长度… 反复执行即可

TCP异常情况

①程序崩溃
操作系统是会感知到的, 可以做相应的处理. 操作系统会回收进程的资源, 其中释放包括文件描述符表, 就想当于调用了对应socket的close. 之后触发FIN操作, 进而开始进入四次挥手, 和普通的四次挥手没有区别.
②正常关机
通过开始菜单或执行关机命令, 系统会强制结所有进程, 回收资源. 与程序崩溃执行的流程类似
③主动掉电
大多数发生的情况, 操作系统不会做出任何反应
接收方掉电: 发送方并不知道接收方挂了, 继续发送数据. 发送数据后收不到ACK应答, 触发超时重传. 多次重传都没有收到ACK应答, 会尝试进行连接重置(RST标识位). 连接重置也失败, 只能放弃连接.
发送方掉电: 一般出现在长连接中, 服务器与客户端会维护一个心跳包(客户端每隔1秒给服务器发送一个数据包, 证明自己存活, 这个包是告诉对方我还在线, 没有真实数据). 如果服务器一直收不到这个心跳包, 比如过了10秒之后还没有收到, 就判定为客户端挂了, 自行断开连接. 客户端网络恢复之后, 再次进行重连即可.
④网线断开
与主机掉电的情况相同, 只不过是主机都是正常工作的.
怎么对UDP做安全和效率的控制?
在UDP的基础, 实现TCP的10种安全效率机制. 不需要具体实现, 因为没必要

网络层

网络层的重点协议就是IP协议

IP协议格式

image.png
4位版本号: 指定IP协议的版本, 对于IPv4来说就是4
4位头部长度: 指的就是条消息处理后面具体数据的头部内容的长度. IP头部的长度是多少个32bit, 也就是length*4 的字节数. 4bit表示最大的数字是15, 因此IP头部最大长度是60字节.
8位服务类型: 包括3位优先字段(已经弃用), 4位TOS字段, 1位保留字段(必须为0)
其中4位TOS字段表示最小延时, 最大吞吐量, 最高可靠性, 最小成本, 这四者只能选一个, 选择需要执行的"标准". 对于ssh/telnet这样的应用程序, 最小延时比较重要, 对于ftp这样的程序, 最大吞吐量比较重要
16位总长度: ip数据报总体占多少字节
16位标识: 唯一的标识主机发送的报文. 如果IP报文在数据链路层被分片了, 那么每一个片里面的这个id都是相同的.
3位标志: 第一位保留(保留的意思是现在不用, 但是还没想好说不定以后要用到). 第二位置为1, 表示禁止分片, 这时候如果报文长度超过MTU, IP模块就会丢弃报文. 第三位表示"更多分片", 如果分片了的话, 最后一个分片置为0, 其他是1. 类似于一个结束标记.
8位生存时间: 数据报到达目的地的最大报文跳数. 一般是64, 每次经过一个路由, TTL -= 1, 一直减到0还没到达, 那么就丢弃了. 这个字段主要是用来防止出现路由循环.
8位协议: 表明传输层用的哪个协议, 比如UDP, TCP, …

关于IP协议

IPv4: 总长32位(4个字节), 最多可以表示42亿个地址
IPv6: 总长128位(16字节), 约等于42亿42亿42亿*42亿
注意: IPv4与IPv6不兼容, 我国没有IPv4的分配权,所以大力推行IPv6

动态分配

设置上网的时候才获取一个IP(一个IP只能同时表示一台主机), 下线时候就会被收回
(IP协议本身并没有动态分配机制, 它只是一种用于在因特网上传输数据包的协议. 然而, DHCP(动态主机配置协议)是一种用于动态分配IP地址以及其他网络参数的协议, 通常与IP协议一起使用. DHCP服务器可以自动为连接到网络的设备分配可用IP地址, 并帮助管理网络中的IP地址资源. 因此, 虽然IP协议本身不具备动态分配机制, 但DHCP协议提供了这种支持. )

NET机制

一个子网中的所有机器, 共用一个公网IP地址, 子网里的机器分配内网ip.
因为公网IP是不能重复的, 不同的子网的IP可以重复

分配置方式

把IP地址分为两大类:
①外网ip(公网IP)
②内网IP(局域网IP): 约定 10., 172.16.~172.31., 192.168.

路由选择

发出一个请求之后, 如何到达目标主机. 路由的选择过程本身也是一个动态的过程.
在路由选择的过程中, 每一次询问并且向目标前进, “一跳”, 完整的通信过程就是通过一跳一跳完成的
image.png
每个设备都有目的IP和源IP, 每次消息前进过程, 源IP会被替换成当前设备的IP, 并记录原本的源IP. 当消息最终到达目的IP, 并返回时, 会一步一步的返回到真正的源IP地址.
访问网站时, 输入的网址都是被DNS解析出来真正的IP地址才去访问. DNS服务器就保存了所有网站域名和IP的对应关系.

数据链路层

以太网

“以太网” 不是一种具体的网络, 而是一种技术标准. 既包含了数据链路层的内容, 也包含了一些物理层的内容. 例如: 规定了网络拓扑结构, 访问控制方式, 传输速率等; 例如以太网中的网线必须使用双绞线; 传输速率有10M, 100M, 1000M等; 以太网是当前应用最广泛的局域网技术; 和以太网并列的还有令牌环网, 无线LAN等;

以太网帧格式

image.png
源地址目的地址是指网卡的硬件地址(也叫MAC地址), 长度是48位, 是在网卡出厂时固化的
MAC地址一般指的是网卡(网络设置)的地址, 每个生产网卡的厂商都会分配一个MAC地址的范围, 网卡在出场的时候, MAC地址就已经固化到了硬件中.
帧协议类型字段有三种值, 分别对应IP、ARP、RARP;
帧末尾是CRC校验码, 即校验和

MTU最大传输单元

认识MTU

①MTU相当于发快递时对包裹尺寸的限制. 这个限制是不同的数据链路对应的物理层, 产生的限制.
②以太网帧中的数据长度规定最小46字节, 最大1500字节, ARP数据包的长度不够46字节, 要在后面补填充位
③最大值1500称为以太网的最大传输单元(MTU), 不同的网络类型有不同的MTU
④如果一个数据包从以太网路由到拨号链路上, 数据包长度大于拨号链路的MTU了, 则需要对数据包进行分片(fragmentation)
⑤不同的数据链路层标准的MTU是不同的
⑥一旦这些小包中任意一个小包丢失, 接收端的重组就会失败. 但是IP层不会负责重新传输数据

MTU对IP协议的影响

由于数据链路层MTU的限制, 对于较大的IP数据包要进行分包:
①将较大的IP包分成多个小包, 并给每个小包打上标签;
②每个小包IP协议头的 16位标识(id) 都是相同的;
③每个小包的IP协议头的3位标志字段中, 第2位置为0, 表示允许分片, 第3位来表示结束标记 (当前是否是最后一个小包, 是的话置为1, 否则置为0);
④到达对端时再将这些小包, 会按顺序重组, 拼装到一起返回给传输层;
⑤一旦这些小包中任意一个小包丢失, 接收端的重组就会失败. 但是IP层不会负责重新传输数据. 意思就是在使用IP协议进行数据传输时, 当数据被分割为多个小数据包来传输时, 如果其中的一个数据包在传输过程中丢失了, 它将不可避免地影响接收方对数据的重组和还原. 而IP层仅仅是负责将数据包从源地址传输到目标地址, 对于数据包是否完整、有序等问题不进行保证. 因此, 一旦发生数据包丢失, IP层不会负责重新传输已经丢失的数据包, 需要上层协议自行处理错误.

MTU对UDP协议的影响
一旦UDP携带的数据超过1472(1500-20(IP首部) - 8(UDP首部)), 那么就会在网络层分成多个IP数据报.  <br />这多个IP数据报有任意一个丢失, 都会引起接收端网络层重组失败. 那么这就意味着, 如果UDP数据报在网络层被分片, 整个数据被丢失的概率就大大增加了.   

MTU对于TCP协议的影响

TCP的一个数据报也不能无限大, 还是受制于MTU.
TCP的单个数据报的最大消息长度, 称为MSS(Max Segment Size);
TCP在建立连接的过程中, 通信双方会进行MSS协商.
最理想的情况下, MSS的值正好是在IP不会被分片处理的最大长度(这个长度仍然是受制于数据链路层的MTU).
双方在发送SYN的时候会在TCP头部写入自己能支持的MSS值.
然后双方得知对方的MSS值之后, 选择较小的作为最终MSS.
MSS的值就是在TCP首部的40字节变长选项中(kind=2);

浏览器输入URL后会发生的事

在浏览器中输入一个URL, 到最终的显示页面, 会发生哪些事?
(1)进行DNS域名解析
①网络上是以IP地址作为主机的标识, 但IP地址不好记, 于是用一个域名来表示IP地址
②DNS服务器的功能就是把域名转换为IP地址
③DNS服务器在全球有13个根服务器, 其中一个叫总根, 12个辅根
(2)进行数据封装
①浏览器根据用户的请求, 构造出HTTP请求(应用层协议)
②交给传输层TCP
③TCP三次握手, 与目的主机建立连接
④把数据交给网络层, 使用IP协议进行封装
⑤网络层再把数据交给数据链路层
⑥数据链路层再把数据封装后交给物理层进行传输
(3)传输过程
①中间会经历交换机和路由器等网络设备
②每个网络设备会进行分用, 之后再封装(替换源IP)进行相邻节点的传输
(4)到达目标服务器
①进行层层分用
②到达HTTP这一层后服务器就可以解析出用户请求的资源
③服务器做出响应(服务端程序要处理的业务逻辑)
(5)服务器把响应数据重新封装, 交给下一层
(6)响应数据通过中间转发, 回到客户端
(7)客户端解析数据得到响应数据
(8)浏览器渲染并呈现内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值