首页 > 编程语言 >网络编程(一):UDP socket api => DatagramSocket & DatagramPacket

网络编程(一):UDP socket api => DatagramSocket & DatagramPacket

时间:2024-11-09 08:48:25浏览次数:7  
标签:DatagramSocket UDP DatagramPacket socket IP 端口 服务器 数据包

目录

1. TCP 和 UDP

1.1 TCP / UDP 的区别

1.1.1 有连接 vs 无连接

 1.1.2 可靠传输 vs 不可靠传输

 1.1.3 面向字节流 vs 面向数据报

1.1.4 全双工 vs 半双工

2. UDP socket api

2.1 DatagramSocket

2.1.1 构造方法

2.1.2 receive / send / close

2.2 DatagramPacket

2.2.1 构造方法

2.2.2 getAddress / getPort / getSocketAddress

3. UDP 回显 客服端-服务器

3.1 UDP 回显服务器

3.1.1 给服务器指定固定端口号

3.1.2 运行服务器

3.1.2.1 读取请求并解析

3.1.2.2 根据请求, 计算响应

3.1.2.3 返回响应

3.1.2.4 记录客户端/服务器的交互过程

3.2 UDP 回显客户端

3.2.1 给客户端随机分配空闲端口

3.2.2 运行客户端

3.2.2.1 用户输入请求

3.2.2.2 构造请求数据包

3.2.2.3 发送请求数据包

3.2.2.4 接收服务器响应

 3.2.2.5 解析响应数据


1. TCP 和 UDP

上文说到, 作为程序员, 我们写代码时, 主要是和应用层来打交道(应用程序).

将应用层的数据交给传输层, 就需要调用操作系统提供的一组 api(传输层向应用层提供的), 也就是 socket api.

而在传输层中有两个核心的协议:

  1. TCP 协议
  2. UDP 协议

这两个协议差别非常大, 编写代码时, 也是截然不同的风格, 于是 socket api 就提供了两套, 一套给 TCP 使用 ,一套给 UDP 使用.

1.1 TCP / UDP 的区别

  • TCP => 有连接, 可靠传输, 面向字节流, 全双工
  • UDP => 无连接, 不可靠传输, 面向数据报, 全双工

1.1.1 有连接 vs 无连接

这里的连接, 是一个抽象的概念, 指的是虚拟的/逻辑上的连接, 是指两个人之间有没有建立 "连接/联系" , 并非物理上网络的连接.

所以这里的 有/无连接, 就是指彼此之间, 有没有保存对方的信息(有没有保存对端的IP/端口). 

(物理上的连接(网线, wifi ...), 肯定是都有的, 因为要进行网络通信, 物理连接是必不可少的!!)

 比如, 两个人结婚, 去民政局领了结婚证后, 你们两个人才建立起这样的 虚拟的/逻辑的上的 连接, 你们两个人真正的 "连接" 了起来, 你是她的丈夫, 她是你的妻子.

  • 对于 TCP 来说(有连接), TCP 协议中就保存了对端的信息. A 和 B 通信, A 中就保存了 B 的信息, B 中就保存了 A 的信息(彼此之间, 知道是谁和自己建立的连接).
  • 而对于 UDP 来说(无连接), UDP 协议中就没有保存对端的信息. (但是我们可以在代码中手动使用变量来保存对端信息, 但这属于 UDP 的行为)

 1.1.2 可靠传输 vs 不可靠传输

网络通信时, 数据是很容易出现丢失的(丢包, 原本要传输的数据被修改, 被识别出来后就会被丢弃):

  1. 光/电信号, 可能会收到外界磁场的干扰导致丢包
  2. 网络是通过 路由器/交换机 来构建的, 当需要转发的数据量超过了 路由器/交换机 的转发上限时, 就会出现"堵车", 造成丢包(下载很大的资源时, 网络就会很卡)

所以, 传输的数据, 是不一定 100% 到达目的地的, (中间可能出现丢包现象):

  • 而 TCP 可靠传输 => 虽然不能保证 100% 发送成功, 但是会尽可能的提高传输成功的概率, 如果出现丢包, 就会尝试重新发送.(但是也会降低传输速率)
  • 而 UDP 不可靠传输 => 把数据发送后, 就不再管了(不管发送成功与否)~~ (传输速率同时也更快)

 1.1.3 面向字节流 vs 面向数据报

  • 面向字节流: 读取数据的时候, 以字节为单位, 一次读写一个字节的数据
  • 面向数据报: 读取数据的时候, 以一个数据报为单位(不是字符)

所以 TCP 可以支持任意长度的读取, 但是存在粘包问题.

而 UDP 一次只能读取一整个 UDP 数据报, 虽然不存在粘包问题, 但是却存在长度限制.

1.1.4 全双工 vs 半双工

  • 全双工: 一个通信链路, 支持双向通信(既能读, 也能写)
  • 半双工: 一个通信链路, 只支持单向通信(要么读, 要么写, 二选一)

2. UDP socket api

socket api 针对 TCP 和 UDP 提供了两套不同的 api , 我们先来看 UDP 的 socket api.

2.1 DatagramSocket

在之前的博客提到过, "文件" 是一个广义上的概念, 文件不止指硬盘上存储的文件, 还可以代指一些硬件设备(硬件设备被抽象成文件, 操作系统通过文件对硬件设备统一进行管理).

其中, 网卡就被抽象成 socket 文件, 我们可以通过操作 socket 文件, 间接来操作网卡.

而 DatagramSocket 就可以认为是 "网卡的遥控器", 可以通过 DatagramSocket 对象来向网卡中读写数据.

2.1.1 构造方法

操作网卡时, 由于网卡被抽象成 socket 文件, 所以操作网卡的流程和操作文件的流程是类似的:

  1. 打开文件
  2. 读写文件(读写网卡)
  3. 关闭文件

其中, DatagramSocket 通过构造方法 new 出对象后, 就相当于打开文件成功.

DatagramSocket 构造方法有两种:

  1. 无参版本 => 随机分配一个未被使用的端口号
  2. 带带参版本 => 指定一个固定的端口号

(使用 socket 时, 会关联上一个端口号, 以此区分主机上不同的应用程序)

我们可以把 DatagramSocket 理解为一个包裹,

  • 里面的物品就是要传输的数据(UDP 数据包)
  • 而端口号就是收货人(我)的电话, 快递小哥可以根据电话来确定唯一的收货人是谁.(端口号能定位到这个主机中的某个进程,从而实现交互)

2.1.2 receive / send / close

  • receive --- 从 socket 文件(网卡)中读取 UDP 数据包. 

当有人向这里发送数据包后, receive 可以从网卡中将数据包读出来, 填充到 DatagramPacket 对象中(输出型参数中). 也就是说, 接受数据前(调用前), 需要先构造一个空的 DatagramPacket(非null, 只是未初始化).

  • send --- 发送 UDP 数据包. 前提是, 有目的 IP 和 目的端口
  • close --- 关闭 socket 文件

2.2 DatagramPacket

DatagramPacket 用来表示一个完成的 UDP 数据包, 数据包的载荷使用 字节数组(byte[]) 来存储.

2.2.1 构造方法

UDP 数据包的载荷数据(字节数组), 需要通过构造方法来指定:

2.2.2 getAddress / getPort / getSocketAddress

  • getAddress => 得到源 IP
  • getPort => 得到源端口
  • getSocketAddress => 得到源 IP 和 源端口, 封装在一个 InetAddress 对象中

上面说到 DatagramSocket 可以理解为一个快递包裹

而 DatagramPacket 就是包裹中的物品, 也就是要传输的数据


3. UDP 回显 客服端-服务器

这里通过模拟回显客户端服务器来加强对 DatagramSocket DatagramPacket 的理解和使用.

所谓回显, 就是指 客户端的请求是啥, 服务器的相应就是啥.(请求和相应是一样的).

3.1 UDP 回显服务器

对于服务器来说, 应该有一个固定的端口号(用户可以根据需求, 精准的找到某个服务器),

3.1.1 给服务器指定固定端口号

构造 DatagramSocket 对象, 读写网卡中的数据, 并使用带参版本的构造方法给服务器指定固定的端口号:

3.1.2 运行服务器

第二步就是把服务器运行起来, 但是对于服务器来说, 是不知道用户什么时候来访问的, 所以要 7*24 小时的运行 => while(true).

一个服务器程序, 通常都有以下三点大的流程:

  1. 读取请求并解析
  2. 根据请求, 计算响应
  3. 把响应返回给客户端
3.1.2.1 读取请求并解析

使用 socket 的 receive 方法来读取用户发来的请求(一个 DatagramPacket 数据包), 这里需要注意的是, receive 使用 "输出型参数", 将读取到的数据填充到 DatagramPacket 对象中.

具体步骤如下:

  1. 构造 DatagramPacket 对象(报头 + 载荷), new 字节数组作为 UDP 数据包的载荷(报头我们不需考虑, DatagramPacket 对象底层已经实现). 
  2. 调用 receive 方法读取请求, 填充到 DatagramPacket 对象中
  3. 将读取到的 UDP 载荷(字节数组, 二进制信息)取出来, 构造出一个 String 信息 (1. getdata 拿到字节数组 2. requestPacket.getLength 拿到数组有效信息的长度 3. 根据以上信息, new String() 构造字符串)

3.1.2.2 根据请求, 计算响应

根据请求, 计算响应. 这一步骤是服务器中最关键的步骤, 但是由于我们这里构造是回显服务器, 所以 请求内容就是响应内容.

3.1.2.3 返回响应

由于 UDP 协议本身是没有保存对端的信息的(没有保存目的IP/目的端口), 而返回响应的前提是明确 数据包要给谁发, 发到哪, 即在构造数据包时, 要指定好目的IP和目的端口.

首先, 根据响应的字符串(response)构造出 UDP 响应数据包(responsePacket).

构造相应数据包 responsePacket 的过程如下:

  1. 拿到字符串底层的字节数组(字符串底层就是通过字节数组(byte[] value)存储数据) => response.getBytes()
  2. 拿到字节数组的长度 => response.getBytes().length(); 注意:这里拿的必须是字节数组的长度, 而不是字符串的长度(字符 != 字节)
  3. 通过 DatagramPacket 的构造方法, 构造 UDP响应数据包.
  4. 响应数据包的目的IP和目的端口, 就是请求的源IP和源端口. 可以从 receive 得到的 请求数据包中得到源IP/源端口(requestPacket.getgetSocketAddress()).

注意:

请求数据包(requestPacket)作为输出型参数, 经过 receive 的接收填满数据后, 其 UDP 报头中包含了 源端口/目的端口, IP报头中包含了 源IP/目的IP. (虽然说 requestPacket 是一个 UDP 数据包, 但是 IP信息也是包含的)

构造完成 响应数据包(responsePacket)后, 调用 send 方法给客户端返回 响应.

3.1.2.4 记录客户端/服务器的交互过程

打印日志信息:

注意事项:

  • socket 文件需要调用 close 方法来关闭吗?

答: 文件是否要关闭, 需要考虑文件对象的生命周期是怎样的. 虽然 socket 是一个文件资源, 但是它是不需要 close 的, 因为此处的 socket 会自始至终伴随整个 UDP 服务器(如果关闭文件, 那么服务器就不能使用了), 所以只有当服务器关闭后(进程关闭), 文件资源才可以关闭, 而进程关闭, 就会自动释放 PCB 文件描述符表中的所有资源, 也就不需要手动 close.

  • 当 receive 没有接收到客户端的请求时, 服务器会忙等吗??

: 不会. 当 receive 没有从网卡中读到请求时, receive 是会触发阻塞等待的. 当有请求发来时, receive 才会继续往下执行.

  • 客户端给服务器发的数据, 就是字符串. 本身收到的 DatagramPacket 的二进制数据就是从 String 转来的, 我们在服务器上只是再把 二进制数据 转回 String.
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

/**
 * Created with IntelliJ IDEA.
 * Description: 服务器端
 * User: dings
 * Date: 2024-11-03
 * Time: 14:33
 */

/**
 * 回显服务器端
 */
public class UdpEchoServer {
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        // 服务器端 => 固定端口号
        // socket 对象可以看做一个包裹, 其接受/发送的数据就是包裹中的物品
        // 端口号就相当于我的手机号, 快递小哥能准确的将收货人定位到我
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 1. 接收数据
            // Packet对象相当于一个完整的 UDP 数据包
            // 这里的字节数组, 就是 UDP 载荷
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            // requestPacket 作为 "输出型参数"
            // 从 socket 中读取请求, socket 可以看做一个包裹, 其中的物品就是数据
            socket.receive(requestPacket);
            // 将字节数组(读到的二进制数据), 转化为字符串, 只是转化其中的有效信息(getLength)
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            // 2. 根据请求, 计算相应(服务器最关键的一步)
            // 这里是回显服务器, 直接返回请求即可
            String response = process(request);
            // 3. 返回相应
            // 把构造好的数据包发送出去, 前提是数据包中包含了目的IP和目的端口
            // 不能直接发送. UDP 协调没有保存对端信息(目的IP和目的端口), 需要根据请求来得到目的IP和目的端口
            // 而响应的目的IP和目的端口, 就是请求的源IP和源端口
            // (构造方法传入的是目的IP和目的端口) (getAddress/getPort/getSocketAddress => 得到的是源IP/源端口)
            DatagramPacket responsePacket = new DatagramPacket
                    (response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
            socket.send(requestPacket);
            // 4. 打印日志信息
            System.out.printf("[%s %d], request : %s, response : %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
        }
    }

    /**
     * 根据请求, 计算相应
     * @param request
     * @return
     */
    private String process(String request) {
        return request;
    }

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

3.2 UDP 回显客户端

3.2.1 给客户端随机分配空闲端口

对于客户端来说, 需要明确以下两点:

  1. 客户端是不能指定固定端口的(由操作系统随机分配一个空闲的端口)!!
  2. 在客户端中, 要明确所访问的服务器的IP和端口(指定目的IP/目的端口)!!

如果客户端指定的是固定端口, 那这个端口很可能已经被电脑上的其他程序给占用了(造成冲突), 运行时就会使客户端运行失败!! 而默认分配空闲端口, 就可以避免这种情况.

而为什么服务器可以指定固定端口呢??

  • 因为服务器就是一个服务网站, 它必须是固定的, 不能变来变去, 要不然用户就找不到了!!
  • 而且服务器是在程序员手中的, 即使出现端口冲突, 程序员也可以很快处理.
  • 而客户端是在用户手中的, 程序员是无法控制用户的电脑的~

举个例子:

汤老湿在陕科大食堂18号卖东北熏肉大饼(IP: 陕科大食堂, 端口: 18号)

此时, 老湿的档口就是服务器, 而来买饼的学生就是客户端.

显而易见, 老湿的档口(服务器端口号)是不能变来变去的, 他卖饼就是在18号档口卖的, 不能今天一个地明天一个地, 这样学生就找不着了~~

而来买饼的学生(客户端), 买完饼后, 肯定也不是固定在一个座位上吃饼, 因为这是食堂, 很可能昨天的坐的位置已经被其他人占了(客户端端口冲突), 所以只能坐在一个没有人的位置上吃饼(空闲端口).

3.2.2 运行客户端

3.2.2.1 用户输入请求

从控制台读取用户请求:

3.2.2.2 构造请求数据包

注意: 请求是要发送给服务器的, 所以要在数据包中指定服务器的IP和端口.

(请求数据包 = 载荷(字节数组) + 目的IP/目的端口)

因为 serverIP 是字符串, 所以要通过 InetAddress.getByName 构造一个 IP 地址(目的IP).

3.2.2.3 发送请求数据包

调用 socket 的 send 方法, 发送 UDP 请求数据包:

3.2.2.4 接收服务器响应

发送请求后, 客户端会收到服务器的响应, 仍然需要构造一个空的数据包作为输出型参数, 填充 receive 读到的响应数据.

 3.2.2.5 解析响应数据

最终, 运行客户端程序:

注意: "127.0.0.1" 是一个特殊的 IP(环回 IP), 表示当前主机(不管主机实际IP是啥, 都表示当前主机), 类似于 this.

由于此时我们写的 回显客户端-服务器, 是在同一台主机上的, 就可以使用 127.0.0.1 来访问.

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description: 客户端
 * User: dings
 * Date: 2024-11-04
 * Time: 8:03
 */

/**
 * 回显 客户端
 */
public class UdpEchoClient {
    private String serverIP;// 服务器IP
    private int serverPort;// 服务器端口号
    private DatagramSocket socket = null;

    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        // 操作系统随机指定一个空闲的端口号
        socket = new DatagramSocket();
        // 指定访问的服务器的地址
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 从控制台读取用户请求
            System.out.println("请输入你的请求: ");
            if(!scanner.hasNext()) {
                break;
            }
            String request = scanner.next();
            // 2. 构造请求数据包(载荷(字节数组) + 目的IP/目的端口)
            // 后续会将请求数据包发送给服务器端, 所以, 请求数据包要指定服务器地址(目的IP/目的端口)
            DatagramPacket requestPacket = new DatagramPacket
                    (request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIP), serverPort);
            // 3. 发送 请求数据包
            socket.send(requestPacket);
            // 4. 接收服务器返回的响应
            // responsePacket 作为输出型参数, 填充服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4090], 4090);
            socket.receive(responsePacket);
            // 5. 解析响应信息
            // 将响应中的二进制数据(字节数组)转化为字符串, 只转化其中的有效数据(getLength)
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

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

END

标签:DatagramSocket,UDP,DatagramPacket,socket,IP,端口,服务器,数据包
From: https://blog.csdn.net/2401_83595513/article/details/143467897

相关文章

  • 第一天打卡,udp协议
    今天学了udp协议基础,udp协议是一种无连接的网络协议,提供一种简单的方式来输送数据。发送:要用到的方法封装在InetAddress类中,其中DatagramSocket对象ds相当于快递员身份,不传递参数值的话会随机生成端口,进行输送快递(数据),快递的身份由DatagrampPacket对象充当,把东西打包。其中的......
  • 知识分享:Air780E软件之UDP应用示例
    一、UDP概述UDP(用户数据报协议,UserDatagramProtocol)是一种无连接的、不可靠的传输层协议,主要用于实现网络中的快速通讯。以下是UDP通讯的主要特点:1.1无连接通讯:UDP在发送数据之前不需要建立连接,这大大减少了通讯的延迟。发送方只需将数据包封装成UDP报文,并附上目的地址......
  • 面试:TCP、UDP如何解决丢包问题
    文章目录一、TCP丢包原因、解决办法1.1TCP为什么会丢包1.2TCP传输协议如何解决丢包问题1.3其他丢包情况(拓展)1.4补充1.4.1TCP端口号1.4.2多个TCP请求的逻辑1.4.3处理大量TCP连接请求的方法1.4.4总结二、UDP丢包2.1UDP协议2.1.1UDP简介2.1.2......
  • 详解UDP协议
    UDP是一种无连接的、简单的传输层协议,UDP协议的设计目的是提供一种简单、轻量级的通信机制,适用于那些对实时性和传输效率有较高要求,但对数据完整性和可靠性要求相对较低的应用。UDP协议报头UDP协议的报头部分由四部分组成:源端口号,目的端口号,UDP长度,校验和。源端口号:识别发......
  • TCP和UDP
    TCP(传输控制协议)连接导向:在数据传输之前,TCP需要建立连接(如三次握手),确保双方可以通信。可靠性:TCP提供数据传输的可靠性,确保数据包按顺序到达,且没有丢失。丢失的数据包会被重传。流量控制和拥塞控制:TCP具有流量控制机制,防止发送方过快发送数据,导致接收方处理不过来。同时,它也会根......
  • 【重磅新品】芯驿电子 ALINX 推出全新 IP 核产品线,覆盖 TCP/UDP/NVMe AXI IP 核
    在创新加速的浪潮中,为更好地响应客户群需求,芯驿电子ALINX推出全新IP核产品线,致力于为高性能数据传输和复杂计算需求提供高带宽、低延迟的解决方案。发布的第一批IP核包括10GBe/40GBeUDP协议栈IP核、10GbETCP/IP协议栈IP核和NVMeAXIIP核。 ALINX发布的10Gb......
  • 【linux网络编程】| socket套接字 | 实现UDP协议聊天室
        前言:本节内容将带友友们实现一个UDP协议的聊天室。主要原理是客户端发送数据给服务端。服务端将数据再转发给所有链接服务端的客户端。所以,我们主要就是要实现客户端以及服务端的逻辑代码。那么,接下来开始我们的学习吧。    ps:本节内容建议了解so......
  • java 将Log4j2 的日志内容输出到udp上
    在Maven项目中pom.xml中添加Log4j2的依赖 <dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.14.1</version></dependency><depen......
  • TCP与UDP的区别和应用场景
    TCP和UDP的区别包括:1.连接方式不同;2.传输可靠性不同;3.数据顺序性不同;4.速度和延迟不同;5.头部大小不同;6.应用场景不同。TCP是一种面向连接、可靠的传输协议,主要用于需要数据完整性和顺序的应用,如Web浏览和电子邮件。而UDP是一种无连接、速度更快但可能丢失数据的协议,常用于流媒体......
  • 简答剖析 UDP:从基础代码到高级封装与应用
    C++学习之路一、C++中构造函数与析构函数简单解析二、Makefile编写简单教程三、UDP协议学习四、简答剖析UDP:从基础代码到高级封装与应用简答剖析UDP:从基础代码到高级封装与应用C++学习之路前言一、UDP基础:涉及的API和结构体(一)`sockaddr_in`结构体(二)`sock......