网络原理 - UDP

一,应用层

和应用程序直接相关

虽然在应用层中,有很多大佬研究出来了很多协议,但是在实际开发的过程中,主要的应用层还是需要"自定义协议"

实例---开发外面软件

首先,实现获取商家列表的功能

针对这样的功能,需要约定一下"应用层协议"

step1

先确定,功能中,客户端和服务器需要交互哪些数据(和功能需求直接相关)

比如需求为:根据用户的位置,获取到方圆5公里范围内所有的商家,并且按照距离升序和降序每个商家的信息,包含名称,图片,评分等等

请求:

用户所在位置(经纬度)

用户id

响应:

商家列表

商家相关的信息

step2

再确定数组组织的格式

网络通信本质上是传输"字节流"或者"字符串"(二进制)

而我们需要把结构化的数据,能够组织成"字符串" ----------- 序列化

并且还能把"字符串"解析回结构化的数据 ------------------反序列化

(结构化数据即结构体,数据中包含了多个属性)

1)直接使用纯文本的格式

请求:

用户id,经度,纬度\n

响应:

商家id,商家名称,商家图片,商家评分,商家距离; 商家id,商家名称,商家图片,商家评分,商家距离; 商家id,商家名称,商家图片,商家评分,商家距离\n

这种直接使用纯文本的格式是基于简单的分隔符的定义方式,有很多的缺点:1,不能直观看到每个属性是什么意思 2,未来要扩展/修改属性,会很麻烦

2)基于 XML 格式

XML 格式是比较经典的格式,通过成对的"标签"描述某个值的含义

请求:

<request>

<userId>111</userId>

<jingdu>12</jingdu>

<weidu>23</weidu>

</request>

XML 和 HTML 的区别为:XML 的标签是程序员自定义的,HTML的标签都是标准的,所以XML 是通用的一般的情况,而HTML 是专用的,特殊的情况

XML的缺点:非常消耗资源(网络带宽),格式上不优雅

3)JSON

JSON相当于XML来说可读性更高,网络带宽消耗更少

请求:

{

"userId": 111,

"jingdu": 12,

"weidu": 23

}

由此可以看出是键值对的格式,而且也支持嵌套,其中的值也可以是另一个JSON

注意:JSON的每个{}都是独立的,如果一次请求只是一个{},此时就不需要分隔符,如果一次请求中,需要有多个{},此时就可以使用++数组的语法 ++

响应:

{ "accountId": 1111, "name": "麻辣烫", "image": "xxxxx", "distance": "2.5km" }, { "accountId": 1111, "name": "麻辣烫", "image": "xxxxx", "distance": "2.5km" }, { "accountId": 1111, "name": "麻辣烫", "image": "xxxxx", "distance": "2.5km" }

4) protobuf

protobuf是纯二进制的数据格式(不可读,无法使用肉眼进行解析)

虽然可读性很差,但是传输效率很高

1.2 用纯文本的格式实现算术运算器

约定好协议:

1,明确传递哪些数据

请求:运算符(+-*/)

操作数(操作数1,操作数2)

响应:运算结果

2,组织传输数据的格式

自定义文本格式

请求:运算符,操作数1,操作数2\n

响应:结果\n

java 复制代码
// 表示一个请求
public class Request {
    // 支持 + - * /
    private String operator;
    private double num1;
    private double num2;

    public Request(String operator, double num1, double num2) {
        this.operator = operator;
        this.num1 = num1;
        this.num2 = num2;
    }

    // 能够把这个对象转成字符串
    public String convertToString() {
        // String.format 就类似于 printf , 把结果构造到 String 中.
        return String.format("%s,%f,%f\n", operator, num1, num2);
    }

    public static Request convertFromString(String request) {
        // 1. 获取参数
        String[] split = request.split(",");
        String operator = split[0];
        double num1 = Double.parseDouble(split[1]);
        double num2 = Double.parseDouble(split[2]);

        // 2. 创建对象
        return new Request(operator, num1, num2);
    }

    public String getOperator() {
        return operator;
    }

    public double getNum1() {
        return num1;
    }

    public double getNum2() {
        return num2;
    }
}
java 复制代码
// 表示一个响应数据
public class Response {
    private double result;

    public Response(double result) {
        this.result = result;
    }

    public String convertToString() {
        return String.format("%f\n", result);
    }

    public static Response convertFromString(String response) {
        double result = Double.parseDouble(response);
        return new Response(result);
    }

    public double getResult() {
        return result;
    }
}
java 复制代码
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 TcpCalcClient {
    private Socket socket = null;

    public TcpCalcClient(String serverIp, int serverPort) throws IOException {
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端运行中...");

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {

            Scanner scannerConsole = new Scanner(System.in);
            Scanner scannerNetwork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while (true) {
                // 1. 从控制台读取用户输入的内容
                System.out.println("请输入要进行的运算 (+ - * /): ");
                String operator = scannerConsole.next();
                System.out.println("请输入第一个数字: ");
                double num1 = scannerConsole.nextDouble();
                System.out.println("请输入第二个数字: ");
                double num2 = scannerConsole.nextDouble();

                // 2. 构造请求并发送
                Request request = new Request(operator, num1, num2);
                String requestString = request.convertToString();
                printWriter.print(requestString);
                printWriter.flush();

                // 3. 读取服务器的响应结果
                if (!scannerNetwork.hasNext()) {
                    break;
                }
                String responseString = scannerNetwork.next();
                Response response = Response.convertFromString(responseString);

                // 4. 打印响应结果
                System.out.println(response.getResult());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpCalcClient client = new TcpCalcClient("127.0.0.1", 9090);
        client.start();
    }
}
java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 进行算术运算的服务器程序
public class TcpCalcServer {
    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        System.out.println("服务器启动...");

        ExecutorService service = Executors.newCachedThreadPool();

        while (true) {
            Socket socket = serverSocket.accept();
            service.submit(() -> {
                try {
                    processConnection(socket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

    // 处理一个连接
    public void processConnection(Socket socket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!", socket.getInetAddress().toString(), socket.getPort());

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {

            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while (true) {
                // 1. 读取请求并解析
                if (!scanner.hasNext()) {
                    break;
                }
                String requestString = scanner.next();
                // 相比于前面的 EchoServer, 多了一步 "反序列化"
                Request request = Request.convertFromString(requestString);

                // 2. 根据请求计算响应
                Response response = process(request);

                // 3. 把响应写回到客户端
                String responseString = response.convertToString();
                // 构造 String 的时候, 已经带了 \n, 直接 print 写入即可.
                printWriter.print(responseString);
                printWriter.flush();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress().toString(), socket.getPort());
            socket.close();
        }
    }

    private Response process(Request request) {
        // 根据当前参数和运算符完成计算, 并且构造响应
        if (request.getOperator().equals("+")) {
            return new Response(request.getNum1() + request.getNum2());
        } else if (request.getOperator().equals("-")) {
            return new Response(request.getNum1() - request.getNum2());
        } else if (request.getOperator().equals("*")) {
            return new Response(request.getNum1() * request.getNum2());
        } else if (request.getOperator().equals("/")) {
            return new Response(request.getNum1() / request.getNum2());
        } else {
            throw new RuntimeException("不支持的运算符: " + request.getOperator());
        }
    }

    public static void main(String[] args) throws IOException {
        TcpCalcServer server = new TcpCalcServer(9090);
        server.start();
    }
}

二,传输层

传输层负责数据能够从发送端传输到接收端,不关心中间过程,只关心起点和终点

在TCP/IP协议中,⽤"源IP","源端⼝号","⽬的IP","⽬的端⼝号","协议号"这样⼀个五元组来标识⼀个 通信(可以通过netstat-n查看);

2.1 端口号

整数,用来区分同一个主机上不同的应用程序

端口号是两个字节表示的无符号整数,取值范围为0 ~ 65535,但是0 ~ 1023之间的端口号数字一般会避开,因为这些端口号是给知名的服务器预留的,

剩下的 1024-65535:操作系统动态分配的端⼝号.客⼾端程序的端⼝号,就是由操作系统从这个范围分配的.

比较重要的两个知名服务器:

1,80 ---- HTTP服务器

2,443 --- HTTPS服务器

两个问题

Q1:一个进程是否可以bind多个端口号?

A:可以。不是进程绑定端口号,而是socket绑定端口号,一个进程中完全可以创建多个socket => 同时关联到多个端口号

Q2:一个端口号是否可以被多个进程bind?

A:如果是同一种协议就不可以,如果是不同的协议就可以

2.2 UDP协议

2.2.1 UDP协议端格式

整个UDP数据报如下:

整个UDP报头为8个字节,分为四个部分,每个部分两个字节

长度:通过两个字节的数据,描述了整个UDP数据报的长度(报头固定8个字节,整个长度-8就是载荷的长度)

其中,两个字节的取值范围最大最65535,约等于64KB,但是放在现在是不够用的

  • 16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度
  • 如果校验和出错,就会直接丢弃

校验和:起到的作用,是验证传输的数据是否出错了

网络传输过程中为什么会出错?

A:像10101这样的bit流,本质上是电信号/光信号/电磁波,电信号中高电平表示1,低电平表示2,但是在网络传输的过程中,会受到外面环境的干扰,如果网线中间经过了一些变化的磁场,感应电流就可能影响到传输的数据内容(所以一些高质量的网线,外层通常都有"屏蔽层")

本质上就是把UDP数据报每个字节(除去校验和本身的这俩字节)依次代入到一个数学公式,进行一系列的计算====》得到一个整数

如果输入的这一串字节,内容是固定的,并且计算公式也是固定的,此时得到的结果也就是一定的,反之,如果发现两次计算得到的结果不同,此时输入的内容也一定是不同的

计算校验和的方案:

本质上都是hash算法

1)CRC(循环冗余方案):简单相加,计算起来比较简单快捷,但是容易出现不同输入数据,得到相同结果的情况

2)md5系列/sha1系列:套入更加复杂的公式

特点:1,定长:无论输入的内容是多少个字节,输出结果就是固定长度的(有很多个版本,16,32,64,128......)

2,分散:输入的内容哪怕只是改变一点点,都会差别很大

3,不可逆:根据原始数据,计算出md5/sha1非常快的,根据md5/sha1值,反推出原始数据,成本非常大,几乎不可能

相关推荐
xiaoye-duck21 分钟前
《Linux 网络编程》应用层自定义协议(一):序列化、sk_buff 与粘包半包处理
linux·网络
郭老师-嵌入式25 分钟前
arm linux 移植 udhcp 与 使用
linux·服务器·arm开发
专家大圣27 分钟前
给自己搭一个 PDF 工具站:Stirling-PDF 部署、格式转换与远程访问实战
网络·pdf·内网穿透
代码终究输给规则27 分钟前
MATLAB r2026a 安装包和安装教程
linux·运维·服务器
z落落30 分钟前
C# WinForm TCP 服务器+WinForm TCP 客户端
服务器·tcp/ip·c#
牧码岛32 分钟前
阿里云服务器新端口上线SOP(Windows+Nginx+HTTPS)
服务器·nginx·阿里云
rcms1527026921835 分钟前
Advanced Energy 27-368299-00 射频发生器
网络
RD_daoyi43 分钟前
谷歌改写了76%的标题:超60字符的,95%会被谷歌自己重写
大数据·服务器·开发语言·前端·搜索引擎·html
张小姐的猫1 小时前
【Linux】网络编程 —— 手动部署 & 测试内网穿透
linux·运维·服务器·网络·tcp/ip