【网络编程】UDP

请添加图片描述

✨个人主页:bit me
✨当前专栏:Java EE初阶

目 录

  • 一. UDP数据报套接字编程
  • 二. 写一个 UDP 版本的 回显服务器-客户端.(echo server)
    • 1. 服务器:
    • 2. 客户端:
    • ⛺️3. 理清楚客户端和服务器的工作流程:

一. UDP数据报套接字编程

  • DatagramSocket API

socket 类,本质上是相当于一个 “文件”,在系统中,还有一种特殊的 socket 文件,对应到网卡设备。构造一个 DatagramSocket 对象,就相当于是打开了一个内核中的 Socket 文件,打开之后,就可以传输数据了。send 发送数据;receive 接收数据;close 关闭文件。

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。

DatagramSocket 构造方法:

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

DatagramSocket 方法:

方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacketp) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字
  • DatagramPacket API

表示一个 UDP 数据报,UDP 是面向数据报的协议,传输数据,就是以 DatagramPacket 为基本单位

DatagramPacket是UDP Socket 发送和接收的数据报。

DatagramPacket 构造方法:

方法签名 方法说明
DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从 0 到指定长度(第二个参数 length)。address指定目的主机的IP和端口号

DatagramPacket 方法:

方法签名 方法说明
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData() 获取数据报中的数据

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创
建。

  • InetSocketAddress API

IP 地址 + 端口号

InetSocketAddress ( SocketAddress 的子类 )构造方法:

方法签名 方法说明
InetSocketAddress(InetAddress addr, int port) 创建一个Socket地址,包含IP地址和端口号

二. 写一个 UDP 版本的 回显服务器-客户端.(echo server)

回显:客户端发啥,服务器就返回啥。不涉及到任何的业务逻辑,而只是单纯的演示 api 的用法。

1. 服务器:

socket = new DatagramSocket(port);

绑定一个端口 => 把这个进程和一个端口号关联起来

一个操作系统上面,有很多端口号,0 - 65535 。
 
程序如果需要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符。(操作系统收到网卡数据,就可以根据网络数据报中的端口号,来确定要把这个数据交给哪个进程)

分配端口号的过程:

  1. 手动指定
new DatagramSocket(port);
  1. 系统自动分配
socket = new DatagramSocket();

一个端口,在通常情况下,是不能被同一个主机上的多个进程同时绑定的;一个进程是可以绑定多个端口的。

如果端口已经被别人占用,再尝试绑定,就会抛出异常 throws SocketException

  • 读取客户端发来的请求,尝试读取,不是说调用了就一定能读到
//1.读取客户端发来的请求
socket.receive();

如果客户端没有发来请求,receive 就会阻塞等待,直到真的有客户端的请求过来了,receive 才会返回。

注意:

receive 是通过参数来放置读取到的数据的,而不是通过返回值。看源码中:
在这里插入图片描述
输出型参数!!需要调用 receive 之前,先构造一个空的 DatagramPacket ,然后把这个空的 DatagramPacket 填到参数中,receive 返回之后自然把读到的数据给放到参数里面。出现异常是 IOException 异常,处理一下就好了。
在这里插入图片描述

  • 对请求进行解析,把 DatagramPacket 转成一个 String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());

还有一种写法:

在这里插入图片描述

但是这种写法是要被舍弃的,String 被画了删除线,可能在未来某一天就被删除了。

  • 根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
String response = process(request);

通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理

public String process(String req){
    return req;
}
  • 把响应构造成 DatagramPacket 对象(构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());

这个也是构造 DatagramPacket 的一种方式,先是拿字符串里面的字节数组,来构造 Packet 的内容,还要把请求中的客户端地址拿过来,也填到包裹里去。
 
response.getBytes().length 可以写作 response.length 吗?
不行 response.getBytes().length 表示的是字节数,response.length 表示的是字符数

requestPacket.getSocketAddress() -> (地址)IP + 端口

  • 把这个 DatagramPacket 对象返回给客户端
 socket.send(responsePacket);
System.out.printf("[%s:%d] req = %s; resp = %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);

注意是 printf !!!和 C 中的 printf 差不多

服务器总代码:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UDPEchoServer {
    //想要创建 UDP 服务器,首先要打开一个 socket 文件
    private DatagramSocket socket = null;

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

    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true){//服务器一直在运行,所以得一直运行
            //1. 读取客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);

            //2. 对请求进行解析,把 DatagramPacket 转成一个 String
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());

            //3. 根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
            String response = process(request);

            //4. 把响应构造成 DatagramPacket 对象
            //构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());

            //5. 把这个 DatagramPacket 对象返回给客户端
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req = %s; resp = %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }
    //通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,
    //但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理
    public String process(String req){
        return req;
    }

    public static void main(String[] args) throws IOException {
        //真正启动服务器,这个端口号说是随便写,但是也有范围的,0 -> 65535
        //但是一般来说 1024 以下的端口,都是系统保留
        //因此咱们自己写代码,端口号还是尽量选择 1024 以上,65535 以下
        UDPEchoServer server = new UDPEchoServer(8000);
        server.start();
    }
}

2. 客户端:

服务器,端口一般是手动指定的,如果自动分配,客户端就不知道服务器的端口是啥了,因此服务器有固定端口客户端才好访问。

客户端,端口一般是自动分配的,客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,说不好这个端口就和其他程序冲突了,导致咱们的代码无法运行。

public UDPEchoClient() throws SocketException {
    //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好
    socket = new DatagramSocket();
}
  • 让客户端从控制台获取一个请求数据
System.out.println("> ");
String request = scanner.next();
  • 把这个字符串请求发送给服务器,构造 DatagramSocket,构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里(另外一种 DatagramPacket 的构造方法)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);

InetAddress.getByName(“127.0.0.1”) -> 通过这个字符串来构造的 InetAddress,此处的 127.0.0.1 回环 IP 就表示当前主机。
 
8000 -> 服务器端口号

这个包裹,就是要从客户端发送给服务器,就需要知道,发送的内容,以及发送的目的地是哪里(收件人地址 + 端口)

目前已经见过三个版本的 DatagramPacket 的构造:

  • 只填写缓冲区,用来接收数据的,一个是空的 Packet
  • 填写缓冲区,并且填写把包发给谁,InetAddress 对象来表示的
  • 填写缓冲区,并且填写把包发给谁,InetAddress + port 来表示的
  • 把数据报发送给服务器
socket.send(requestPacket);
  • 从服务期读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
  • 把响应数据获取出来,转成字符串
String response = new String(responsePacket.getData(),0,responsePacket.getLength());

System.out.printf("req: %s;resp: %s\n",request,response);

客户端总代码:

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

public class UDPEchoClient {
    private DatagramSocket socket = null;

    public UDPEchoClient() throws SocketException {
        //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好
        socket = new DatagramSocket();
    }

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

            //2. 把这个字符串请求发送给服务器,构造 DatagramSocket
            //构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);

            //3. 把数据报发送给服务器
            socket.send(requestPacket);

            //4. 从服务期读取响应数据
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);

            //5. 把响应数据获取出来,转成字符串
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());

            System.out.printf("req: %s;resp: %s\n",request,response);
        }
    }

    public static void main(String[] args) throws IOException {
        UDPEchoClient client = new UDPEchoClient();
        client.start();
    }
}

⛺️3. 理清楚客户端和服务器的工作流程:

  1. 客户端根据用户输入,构造请求

【网络编程】UDP_第1张图片

  1. 客户端把请求发送给服务器

在这里插入图片描述

  1. 服务器读取请求并解析

【网络编程】UDP_第2张图片

  1. 服务器根据请求计算响应(服务器核心逻辑)

在这里插入图片描述

  1. 服务器构造响应数据,并且返回给客户端

【网络编程】UDP_第3张图片

  1. 客户端读取服务器返回的响应

在这里插入图片描述

  1. 客户端解析响应,并显示给用户

【网络编程】UDP_第4张图片

整体效果演示:

  • 先启动服务器,再启动客户端

客户端中输入一个hello:

【网络编程】UDP_第5张图片

在服务器中:

【网络编程】UDP_第6张图片

继续在客户端中输入一个你好:

【网络编程】UDP_第7张图片

服务器中显示:

【网络编程】UDP_第8张图片

一个服务器是可以同时给多个客户端提供服务的

如在 IDEA 中,你想打开多个客户端,你发现你再运行一次客户端,就会把之前的客户端给关闭了,此时我们需要设置一下,就可以启动多个客户端。

【网络编程】UDP_第9张图片

【网络编程】UDP_第10张图片

此时我们就可以打开多个客户端了

【网络编程】UDP_第11张图片

一个服务器灵魂所在,就是大体框架是一样的,比如这个回显服务器,我们要改成带有业务逻辑的服务器,只需要把 process 改掉即可,如我们简单实现一个词典

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

//字典服务器 / 翻译服务器
    //希望实现一个英译汉的效果
    //请求的是一个英文单词,响应是对应的中文翻译
public class UDPDicServer extends UDPEchoServer{
    private Map<String, String> dic = new HashMap<>();

    public UDPDicServer(int port) throws SocketException {
        super(port);

        //这里的数据可以无限的构造下去
        //即使是有道词典这种,也是类似的方法实现(打表)
        dic.put("cat","小猫");
        dic.put("dog","小狗");
        dic.put("fuck","卧槽");
    }

    //和 UDPEchoServer 相比,只是 process 不同,就重写这个方法即可
    public String process(String req){
        return dic.getOrDefault(req,"这个词俺也不会!");
    }

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

【网络编程】UDP_第12张图片

一个服务器要完成的工作,都是通过 “根据请求计算响应” 来体现的

不管是啥样的服务器,读取请求并解析,构造响应并返回,这两个步骤,大同小异,唯有 “根据请求计算响应” 是千变万化,是非常复杂的,可能一次处理请求就要几w,几十w的代码来完成。

拓展:
 
在 DatagramSocket 中有 send,receive 和 close。只有 send 和 receive 写进去了,close 却没有写进去,原因是在上述代码中 socket对象,生命周期都是应该伴随着整个进程的(while(true) 循环),因此进程结束之前,提前用 close 关闭 socket 对象,不合适,当进程已经结束,对应 PCB 没了,PCB 上面的文件描述符表也没了,此时也就相当于关闭了。

你可能感兴趣的:(Java,EE初阶,udp,网络,网络协议)