云计算百科
云计算领域专业知识百科平台

【网络篇】从零写UDP客户端/服务器:回显程序源码解析

在这里插入图片描述

大家好呀 我是浪前 今天讲解的是网络篇的第四章:从零写UDP客户端/服务器:回显程序源码解析

从零写UDP客户端/服务器:回显程序源码解析

  • UDP 协议特性​
    • 核心类介绍​
  • UDP的socket应该如何使用:
  • 1: DatagramSocket
  • 2: DatagramPacket
    • 回显服务器
    • 进行网络编程的第一步
    • 服务器的代码(回显服务器)
      • 创建一个DatagramSocket的对象:
      • 定义服务器启动方法:
      • 手动创建内存空间:
      • 将读取到的数据转成字符串:
      • 封装分用:
    • 客户端的代码:
    • 通信过程:

UDP 协议特性​

UDP(User Datagram Protocol)作为传输层协议,有着与 TCP 截然不同的特性,在网络通信中扮演着独特的角色,适用于对实时性要求高、能容忍少量数据丢失的场景。​

无连接通信:UDP 在数据传输前,发送方和接收方无需像 TCP 那样进行三次握手建立连接,可直接发送数据。无需复杂的连接建立过程,极大降低了传输延迟。

不可靠传输:它不保证数据一定能到达接收方,也不确保数据的顺序和完整性。若在网络传输中,UDP 数据包丢失或乱序,协议本身不会重传或纠正。

面向数据报:UDP 以独立的数据报为单位传输数据,每个数据报都包含完整的目标地址等信息,可独立传输,服务器收到后直接响应,简单高效。​

全双工通信:同一 UDP Socket 可同时进行数据的发送和接收。在语音通话应用中,双方能同时说话并实时听到对方声音,就是因为 UDP 的全双工特性,保证了语音数据的双向实时传输。​

核心类介绍​

在 Java 的 UDP 网络编程里,DatagramSocket和DatagramPacket是两个关键类,分别负责 socket 通信和数据报的封装传输,相互配合实现 UDP 通信功能。

UDP的socket应该如何使用:

UDP的API主要是提供了两个类:

  • DatagramSocket
  • DatagramPacket
  • 1: DatagramSocket

    socket: 本质上是操作系统中的一个概念,本质上是一个特殊的文件 这里的socket属于就是把“网卡”这个设备,给抽象成了文件,而进行网络通信最核心的硬件设备就是网卡

    往socket文件中写数据,就相当于是通过网卡发送数据 往socket文件中读数据,就相当于是通过网卡接收数据

    上面就是把文件操作和网络通信给统一了 在Java中就是使用这个 DatagramSocket 类就是来表示系统内部的socket文件 这个 DatagramSocket 类负责文件读写,也就是借助网卡发送和接收数据

    2: DatagramPacket

    使用这个类就是来表示一个UDP数据报: UDP是面向数据报的 每次进行传输,都要以UDP数据报为基本单位 每次传输都只能够传输一个完整的数据报,不可以传输半个数据报,也不可以传输一个半数据报

    写一个简单的UDP的客户端/服务器通信的程序: 这个程序没有什么业务逻辑,请求什么,就响应什么 只是单纯滴调用Socket API

    让客户端给服务器发送一个请求,请求就是一个从控制台输入的字符串,服务器收到字符串之后,也就会把这个字符串原封不动地返回给客户端,然后客户端再显示出来, 这个程序就是请求什么,就响应什么 这个程序是最简单的网络通信程序,叫做回显服务器

    回显服务器

    服务器的主要功能:

    负责接收客户端的请求,然后根据实际的业务场景来返回不同的响应

    有一种服务器是回显服务器:

    回显服务器的作用就是客户端发啥请求,回显服务器就立马返回啥请求,没有业务逻辑的

    比如: 客户端发送想吃蛋炒饭的请求 服务器就接收到蛋炒饭的请求之后,就返回蛋炒饭的响应

    这个回显服务器是网络编程中最简单的程序,相当与网络编程中的“Hello World”

    回显服务器的作用

  • 学会掌握Socket API的基本使用
  • 学会典型的客户端服务器的工作流程
  • 进行网络编程的第一步

    服务器的代码(回显服务器)

    我们先来写一个服务器代码:

    创建一个DatagramSocket的对象:

    注意: 这个对象是在创建在内存中的,直接对内存进行操作就可以影响到网卡

    程序启动的同时要关联/绑定一个端口号,这个端口号是专门用来区分主机的

    一个主机只能够有一个端口号,同时一个主机中的端口号只能和一个进程进行绑定, 一个端口号和进程A进行了绑定之后,如果进程B也要和这个端口号进行绑定,那么进程B会绑定失败

    但是一个进程是可以同时和多个端口号进行绑定的 为什么? 因为每一个端口号对应了一个DatagramSocket对象,如果一个进程中有多个DatagramSocket对象的话,就可以和多个端口号进行绑定 如下图所示: 在这里插入图片描述

    同时我们在创建DatagramSocket对象的时候必须要手动指定一个端口号 在运行一个服务器程序的时候,也要手动指定端口号

    DatagramSocket对象的代码创建如下:

    package netWork;

    import java.net.DatagramSocket;
    import java.net.SocketException;

    public class Server {
    private DatagramSocket socket = null;

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

    }

    上述的代码抛出的异常为SocketException: 这个异常是在创建DatagramSocket对象时,当主机的端口号已经被其他进程绑定的时候会抛出的异常

    定义服务器启动方法:

    接下来定义一个start方法来作为服务器启动的方法: start方法的执行逻辑如下所示:

    在start方法中会有一个while循环: 由于服务器每天从客户端那里接收到的请求有很多, 所以服务器会每时每刻都在不停地运行,每循环一次,就是服务器在接收到请求,返回响应的过程

    在每次while循环中,会经历下面三个步骤 :

  • 服务器读取客户端发来的请求,解析请求
  • 服务器根据请求来计算响应(回显服务器没有这一步)
  • 服务器向客户端返回响应 代码如下:
  • public void start(){
    System.out.println("服务器启动");
    while(true){
    //每次循环,都是一次服务器在接收请求,返回响应的过程
    //1.读取请求,解析请求
    socket.receive();
    //2.根据请求计算响应(回显服务器不需要这一步)
    //3. 返回响应

    }
    }

    第一步:读取请求,进行解析,

    socket.receive()

    上面代码中的这个receive方法中需要填写一个DatagramPacket类型的参数, 这个参数是一个输出型参数,这个参数在文件IO中也涉及到了,实际上在DatagramPacket内部就会包含一个字节数组,如下所示

    DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);

    这个字符数组会保存收到的消息正文,这个消息正文就是应用层数据包,也就是UDP数据报载荷部分,这个载荷空间的大小可以灵活设置

    将这个参数传入socket.receive()中后,会抛出一个异常:

    IOException : //网络编程,读写socket本质就是IO

    public void start() throws IOException {
    System.out.println("服务器启动");
    while(true){
    //每次循环,都是一次服务器在接收请求,返回响应的过程
    //1.读取请求,解析请求
    DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
    socket.receive(requestPacket);
    //2.根据请求计算响应(回显服务器不需要这一步)
    //3. 返回响应
    }
    }

    手动创建内存空间:

    当收到数据的时候,需要搞一个内存空间来保存这个数据

    所以上面的requestPacket对象是用来承载从网卡那里读到的数据

    但是由于在DatagramPacket的内部是不可以自行分配内存空间的,所以需要手动把内存空间创建好

    这里创建了一个字节数组,这个字节数组就是真正的用来承载数据的内存空间 然后再交给DatagramPacket处理:

    之后receive就会从这个requestPacket对象中读取数据,然后把读取到的数据填充到socket对象中去

    此处receive就可以从网卡中读取一个UDP数据报

    这个UDP数据报就是被放进了requestPacket对象中

    其中UDP数据报的载荷部分被放进了requestPacket内置的字节数组中,同时,UDP的报头部分和收到的数据源IP,源IP端口都会被保存在 requestPacket的其他属性中

    所以我们requestPacket还可以知道数据是从哪里来的(源IP源端口)

    如果执行到receive的时候,还没有客户端发来请求,那么此时receive就没有可以读取的数据,此时receive就会发生阻塞,一直阻塞到客户端发来请求为止

    将读取到的数据转成字符串:

    此时的receive会读取到一个字节数组, 当receive读取完毕之后,数据是以二进制的形式存储到DatagramPacket中

    要想能够把这里的数据给显示出来,就需要把这个二进制数据转化为字符串 所以,此时的读的字节数组必须要先转成(字符串)String之后,才方便后续的逻辑处理:

    String request = new String(requestPacket.getData(),0,requestPacket.getLength());

    基于字节数组构造String,字节数组里面保存的内容不一定就是二进制数据,也可能是文本数据 而字符串(String) 不仅可以保存二进制数据,还可以保存文本数据,所以需要将这个字节数组转成字符串 :

    在这里插入图片描述

    注意: 在getLength()中获取的字节数组的有效数据的长度不一定就是4096 这个4096是这个字节数组的最大长度, 而getLength()获取到的结果是收到的数据的真实长度,即发送方这一次实际发送了多少个数据 比如: 如果这一次收到的数据长度是10,那么这个getLength()获取到的就是10。 所以我们这里构造字符串是使用有效数据长度来进行构造,不能使用字节数组的最大长度来构造

    以上就是把一个请求转化为字符串了

    在这里插入图片描述

    封装分用:

    网路通信过程中涉及到"封装和分用":

    只有当应用层调用传输层提供的API的时候,才会把这个数据给读取到; 数据来到服务器时,会经由物理层,一层层分用到应用层:

    在传输层中:会给每一个socket对象都分配一个缓冲区(这个缓冲区在操作系统内核里) 每次网卡收到一个数据都是经由层层分用,解析好之后,最终放到缓冲区里 在应用层的应用程序调用receive就是从这个缓冲区里面拿走一个数据 这个本质上就是生产者-消费者模型,而此处给socket对象分配的缓冲区就是阻塞队列

    所以从客户端传过来的数据 不是存在socket文件中,而是存在socket对象中的一个内存缓冲区的阻塞队列中

    第二步:根据请求来构造响应:

    String response = process(request);

    这个代码要根据请求构造响应,通过这个process方法来构造响应

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

    由于此处是回显服务器,所以只需要单纯滴返回这个请求就可以了

    第三步: 把响应返回给客户端:

    1: 构造一个响应对象DatagramPacket作为响应对象

    同时由于UDP是无连接的,所以UDP不会保存要发给谁

    所以就需要在每次发送的时候,重新指定,数据要发送到哪里去

    所以在这个响应对象(数据报)中需要指定数据内容,也要指定数据具体要发送给谁。

    //构造一个DatagramPacket作为响应对象
    DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
    response.getBytes().length);

    在这里插入图片描述

    刚刚的socket对象还没有构造完毕,在构造时还需要指定一个socketAddress进去:

    DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
    response.getBytes().length,requestPacket.getAddress(),requestPacket.getPort());

    这个requestPacket.getAddress()和requestPacket.getPort() 方法会获取到一个IP和一个端口号

    这个IP和端口号是和服务器通信的对端的客户端的IP和 端口号 这个IP和端口号是从这个requestPacket这个客户端数据包中获取的

    同时在代码中的response.getBytes().length获取到的是字节

    在进行网络传输的时候,一定是使用字节来进行传输的

    而response.length() 获取到的是字符,如果全是英文,那么字节和字符的个数一样,但是如果有中文,那么此时字节和字符的个数就不一样了。

    为什么要获取到这个客户端的IP和端口号?

    这里是把客户端(请求)中的源IP和源端口,作为响应的目的IP和目的端口 此时就可以做到把消息返回给客户端的效果了

    此时我们的响应对象就构造好了,只需要将这个responsePacket作为参数使用send方法传递出去即可:

    socket.send(responsePacket);

    上述代码中,可以看到UDP 是无连接通信,UDP socket自身不保存对端(客户端)的IP和端口号 :

    这个IP和端口号是在数据包中有一个,同时在代码中也没有"建立连接"和"接受连接"的操作

    这个是直接读取请求,若没有请求,则阻塞等待,若有请求,则对请求进行解析,然后根据请求构造响应,最后返回响应

    所谓的UDP不可靠传输目前代码中没有体现:

    而UDP的面向数据报有体现: 上述代码中的send和receive的参数接收都是以DatagramPacket为单位进行发送和接收的

    UDP的全双工在代码中也有体现: 一个socket既可以发送又可以接收,就叫做全双工

    最后在代码中进行一个打印日志的操作:

    //打印日志:
    System.out.printf("[%s:%d] req: %s,resp: %s\\n",requestPacket.getAddress().toString(),
    requestPacket.getPort(), request, response);

    在这里插入图片描述

    之后撰写一个main方法即可:

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

    在这里插入图片描述

    上述服务器的代码编写完毕,

    下面是回显服务器的完整代码:

    package netWork;

    import javax.xml.crypto.Data;
    import java.io.IOException;
    import java.net.*;
    import java.sql.SQLOutput;

    public class UdpServer {
    //创建一个DatagramSocket对象,是后续进行网卡的基础
    private DatagramSocket socket = null;

    public UdpServer(int port) throws SocketException{
    //下面是手动指定端口
    socket = new DatagramSocket(port);

    //下面这么写就是系统自动分配端口
    //socket = new DatagramSocket();
    }

    //程序的主方法,通过这个方法来启动服务器
    public void start() throws IOException {
    System.out.println("服务器启动");
    //一个服务器要不停滴运行,所以需要一个while循环来进行操作,一个服务器程序是要长时间运行的
    //为了保证客户端随时来,随时可以响应,
    while(true){
    //1.第一步,读取请求并解析
    DatagramPacket requestPacket = new DatagramPacket(new byte[4090],4090);
    socket.receive(requestPacket);
    //将请求转化为字符串
    String request = new String(requestPacket.getData(),0,requestPacket.getLength());

    //2.根据请求计算响应
    //这个步骤是服务器最核心的一个步骤,
    String response = process(request);

    //3. 把响应返回给客户端
    //使用一个响应对象 DatagramPacket 往响应对象中构造刚才的数据,再通过send返回
    DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
    requestPacket.getSocketAddress());

    //使用send方法传入参数进行发送
    socket.send(responsePacket);

    System.out.printf("[%s:%d] req = %s,resp = %s\\n",requestPacket.getAddress().toString(),
    requestPacket.getPort(),request,response);

    }
    }

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

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

    为什么上述的代码中没有出现close()? socket也是一个文件,不进行关闭的话,会造成文件资源泄露

    什么是文件资源泄露? 你一直申请,但是一直都没有进行close,没有进行释放,结果到最后,你想用的时候,发现用不了了,就是文件资源泄露。

    为什么这里不写close()方法,也不会出现文件资源泄露呢? 因为socket是文件描述符表中的一个表项.

    每次打开一个文件,就会占用一个位置,文件描述符是在PCB(进程)上的,是跟随进程的 这个socket在整个程序过程中一直都在使用,不可以提前释放,不可以提前关闭

    当socket不使用的时候,此时整个程序也要结束了 当进程结束时,文件描述符表也会跟随着进程的结束被销毁,就可能发生泄露问题了。

    总结:

    不会泄露的原因是因为socket会随着进程销毁的过程中,被系统自动回收了

    什么时候会出现泄露?

    代码中频繁地打开文件,但是不关闭,在一个进程的运行过程中,不断积累打开的文件,逐渐消耗掉了文件描述符表里的内容,最后内容会被消耗光,就出现了泄露

    但是如果进程的生命周期很短,打开一下就关闭了,也就不会出现泄露了 所以文件资源泄露的问题在服务器上经常出现,因为服务器的进程生命周期很长,要一直运行 泄露的问题在客户端上很少出现,因为客户端的进程的生命周期很短,客户端打开之后用一下就直接关闭了

    客户端的代码:

    接下来我们去 编写客户端的代码:

    注意:服务器需要手动指定端口号 但是客户端不需要手动指定端口号,不手动指定也有端口号, 因为系统会自动给客户端分配一个空闲的端口号

    为什么服务器必须要自己手动指定一个端口号? 因为服务器要保证端口号是固定不变的

    因为只有在服务器代码中手动指定一个端口号

    才能保证端口始终是固定的,如果不手动指定,服务器依赖系统自动分配端口号,就会导致服务器每次开机重启后,系统自动分配的端口号就发生了改变 。

    如果服务器的端口号发生了改变,那么客户端就可能会找不到这个服务器在哪里了,所以服务器的端口号必须要在代码中手动指定

    那么为什么客户端中的端口号不需要手动指定, 可以通过系统自动分配呢?

    客户端的端口号让系统随机分配,系统会去分配给客户端一个空间中可用的端口号 如果是手动指定端口号,那么就无法确定这个端口号是不是可控的,有没有被别的进程占用

    为什么服务器的端口号就不怕被别的进程占用呢?

    因为服务器这个机器是在程序员手中的,程序员对于服务器上有哪些端口号是可用的一清二楚

    但是客户端是在用户手中的,有千千万万个用户,上面的环境也千差万别,程序员无法得知端口号是否被占用,如果强行手动指定客户端的端口号就会导致端口绑定失败.

    所以程序员手中的服务器的端口号是可以手动指定的,但是在用户手中的客户端的端口号是不能手动指定的,只能靠系统自动分配一个空闲的端口号

    在构造方法中,由于UDP自身不会保存对端的信息,所以就需要在应用程序中,把对端的情况给记录下来,在构造方法中主要记录的就是对端的IP和端口,也就是目的IP和目的端口:

    public class Client {
    //首先要创建socket对象,但是此处不需要手动指定端口号
    DatagramSocket socket = null;

    //构造方法:要传输服务器IP(目的IP)和服务器端口(目的端口)
    public Client(String serverIp, int serverPort) throws SocketException {
    socket = new DatagramSocket();
    }
    }

    在这里插入图片描述

    接下来创建start方法来启动客户端: 在这个start方法中,依然是使用一个循环来不停滴发送请求: 在循环中一共要做四件事情:

  • 从控制台中读取请求数据
  • 构造请求并发送
  • 读取服务器的响应
  • 把响应显示到控制台上
  • public void start(){
    System.out.println("客户端启动");
    Scanner scanner = new Scanner(System.in);
    while(true){
    System.out.println("-> ");
    //1. 从控制台中读取请求数据
    //2. 构造请求并发送
    //3. 读取服务器的响应
    //4. 把响应显示到控制台上
    }
    }

    第一步: 从控制台中读取数据,作为请求:

    String request = scanner.next();

    这里从控制台读取请求,使用scanner读取字符串,最好使用next来读取,而不是使用nextLine

    因为如果使用nextLine读取,可能会读取不到空格

    nextLine遇到空格就自动作为分隔符了,所以需要手动输入换行符

    使用enter来控制,由于enter键不仅仅会产生\\n,还会产生其他的字符,就会导致当前的这个读取到的内容会出问题

    而使用next其实是以“空白符”作为分隔符,包括但不限于换行,回车,空格,制表符,垂直制表符

    总结:

    如果从控制台读取内容,就使用next() 如果是从文件读取内容,那么使用next()和nextLine()都可以

    第二步: 把请求的内容构造成一个DatagramPacket对象,在对象中保存数据,长度, 目的IP和目的端口:

    DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
    request.getBytes().length, InetAddress.getByName(serverIp),serverPort);

    然后将这个对象发给服务器:

    socket.send(requestPacket);

    OK,此时客户端已经向服务器发送了请求,那么接下来就只需要去读取服务器返回的响应即可:

    第三步: 尝试读取服务器返回的响应: 此时我们也是需要先构造一个空的DatagramPacket对象来接收响应的数据:

    DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);

    接下来使用这个responsePacket对象对响应进行接收即可:

    socket.receive(responsePacket);

    第四步: 把响应转换成字符串,并显示出来:

    //4. 把响应转化成字符串,并显示到控制台上
    String response = new String(responsePacket.getData(),0,responsePacket.getLength());
    System.out.println(response);

    上述的start方法就结束了,最后再补上一个main方法即可:

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

    综上所述:客户端的完整代码如下所示:

    package netWork;

    import java.io.IOException;
    import java.net.*;
    import java.util.Scanner;

    public class Client {
    //首先要创建socket对象,但是此处不需要手动指定端口号
    DatagramSocket socket = null;

    private String serverIp;
    private int serverPort;

    //构造方法:要传输服务器IP(目的IP)和服务器端口(目的端口)
    public Client(String serverIp, int serverPort) throws SocketException {
    this.serverIp = serverIp;
    this.serverPort = serverPort;
    //下面就是自动分配一个端口号
    socket = new DatagramSocket();
    }

    public void start() throws IOException {
    System.out.println("客户端启动");
    Scanner scanner = new Scanner(System.in);
    while(true){
    System.out.println("-> ");
    //1. 从控制台中读取请求数据
    String request = scanner.next();

    //2. 构造请求并发送
    DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
    request.getBytes().length, InetAddress.getByName(serverIp),serverPort);

    socket.send(requestPacket);

    //3. 读取服务器的响应
    DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);

    socket.receive(responsePacket);

    //4. 把响应转化成字符串,并显示到控制台上
    String response = new String(responsePacket.getData(),0,responsePacket.getLength());
    System.out.println(response);

    }
    }

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

    }

    通信过程:

    此时客户端和服务器就可以相互配合,完成通信过程:

    步骤如下:

  • 先启动服务器
  • 再启动客户端
  • 在客户端中编写hello
  • 然后就可以在服务器中看见
  • 下面是 客户端的界面展示: 在这里插入图片描述

    下面是服务器的界面展示: 在这里插入图片描述

    执行过程:

  • 第一步:服务器启动,进入while循环,执行到receive这里时发生阻塞(此时客户端还没有发送请求)
  • 第二步:客户端开始启动: 也会进入while循环,执行scanner.next,并且在这里阻塞,直到用户在控制台输入,当用户输入字符串之后,next就会返回,从而构造请求数据并发送出来
  • 第三步:客户端发送出数据之后,在服务器那边,就会从receive中返回数据,进一步的解析请求为字符串,执行process操作,执行send操作。 此时的客户端也会继续往下执行,执行到receive,等待服务器的响应
  • 客户端收到服务器返回的数据之后,就会从receive中返回,执行这里的打印操作,也就把响应给显示出来了
  • 服务器完成一次循环之后,就又会执行到receive,重新进入阻塞
  • 客户端完成一次循环之后,就又会执行到scanner.next,重新进入阻塞
  • 我们重点要理解网络程序的交互逻辑

    刚刚的两个程序都是在一个主机上的,没有实现跨主机通信的效果

    能否让同学使用客户端程序来访问老师的服务器代码呢?

    如果我的服务器就在我的电脑上,此时,你是不可以直接访问的,除非老师和同学的电脑都在同一个局域网下,即同一个路由器下,才可以

    但是还有一种方式“ 云服务器” 有了这个,就可以访问老师的电脑了,因为老师的电脑没有公网IP,但是云服务器有公网IP

    jar包是java打包的一种基本方式

    把刚才的UDP服务器部署到云服务器上,进一步的,就可以让大家来访问了

    之后可以调整一下客户端的代码,让客户端访问云服务器上的服务程序,就只需要把IP地址换成云服务器的IP即可

    在这里插入图片描述

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【网络篇】从零写UDP客户端/服务器:回显程序源码解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!