zl程序教程

您现在的位置是:首页 >  云平台

当前栏目

网络编程套接字之UDP

网络编程 UDP 接字
2023-09-27 14:25:52 时间

在这里插入图片描述

一、网络编程

我们网络编程的核心: Socket API,操作系统为我们应用程序提供的API,我们的Socket是和传输层密切相关的。

我们传输层为我们提供了两个最核心的协议UDP/TCP,所以我们的Socket API也为我们提供了TCP/UDP。
简单认识一下TCP/UDP:
TCP 有连接 可靠传输 面向字节流 全双工
UDP 无连接 不可靠传输 面向数据报 全双工

TCP:
特点:

  1. 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
  2. 传输前,采用”三次握手"方式建立连接,所以是可靠的
  3. 在连接中可进行大数据量的传输
  4. 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率低

应用场景:对信息安全要求较高的场景,例如:文件下载、金融等数据通信
TCP:
特点:

  1. UDP是一种无连接,不可靠传输协议
  2. 将源IP、目的IP和端口封装成数据包,不需要建立连接
  3. 每个数据包大小限制在64kb内
  4. 发送不管对方是否准备好,接收方收到也不确认,所以是不可靠的
  5. 可以广播发送,发送数据结束时无需释放资源,开销小,速度快
    应用场景: 语音通话,视频会话等

二、UDP数据报套接字编程

DatagramSocket

DatagramSocket 这个类表示一个Socket对象,我们操作系统中,把socket对象是当作一个文件来处理的。
一个Socket对象就可以与另一台主机进行通信了,如果要和不同的主机通信,就需要创建多个Socket对象

方法作用
DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到任意一个随机端口号(一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket,绑定到本机指定端口(一般用于服务器)
方法作用
void receive(DatagramPacket p)从此套接字接收数据,如果没有接收到数据报,进行阻塞等待)
void send(DatagramPacket p)从此套接字发送数据包(不会阻塞等待,直接发送)
void close()关闭此数据报套接字

我们的receive方法参数传入的是一个空的对象,receive方法内部会对这个对象进行填充,从而构造出结果数据,我们称这样的参数为输出型参数

DatagramPacket

DatagramPacket是UDP socket进行发送和接收的数据报。

方法作用
DatagramPacket(byte[] buf,int length)构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组里,接受指定长度
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address)构造一个DatagramPacket用来发送数据报,发送的数据为字节数据,从0到指定长度,address用来指定目的主机的IP和端口号

DatagramPacket的一些方法:

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

实现客户端服务器程序

我们在这里编写一个最简单的客户端服务器程序:回显服务器(echo server).
我们的服务器做的工作:收到请求,根据请求计算响应,返回响应,最重要的环节就是计算响应这一部分,我们的echo server省略了这一部分,接收到什么就返回什么。

EchoServer

我们网络编程,本质上是要操作网卡,但是网卡不方便我们直接操作,于是我们操作系统内核中,使用“socket"这样的文件来抽象表示网卡,所以我们要想进行网络通信,首先得有一个socket对象

public class EchoServer {
    private DatagramSocket socket = null;
}

我们的服务器,在真正创建对象的时候,需要绑定一个具体的端口号,为啥是具体的呢?因为我们的服务器在进行网络通信中,是属于比较被动的一方,如果我们使用的是系统随机进行分配的端口号,那么我们的客户端就不知道服务器端口号是多少,也就无法进行通信了。

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

我们的UDP传输的基本单位是DatagramPacket.
在这里插入图片描述
此时我们服务器接收到的DatagramPacket是一个特殊的对象,并不方便我们直接进行处理,我们可以把这里包含的数据拿出来,构造成一个字符串。

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

在这里插入图片描述
我们在创建DatagramPacket给的最大长度是4096,但我们实际可能只用了一小部分,因此我们在构造字符串时,通过getLength()获取数据报实际的长度。
我们获取到了客户端的请求之后然后我们对请求进行处理,我们这里实现的是接收什么,回应什么。
在这里插入图片描述

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

然后我们将这个响应发送给客户端,首先我们需要构造出DatagramPacket对象。

DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length);

因为我们的DatagramPacket不认字符只认字节,所以我们将response转换为字节数组。
在这里插入图片描述
答案是不可以,因为response.length()获取的是字符的个数,response.getBytes().length获取的是字节的个数。
我们这里的DatagramPacket的构造还是有一点点问题,我们这里的数据是构建好了,那给谁发呢?

DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPcket.getSocketAddress());

我们在参数中,应该传入客户端的地址信息。
在这里插入图片描述
然后进行发送

socket.send(responsePacket);

在这里插入图片描述
数据到达网卡,经过内核层层分用,最终到达了UDP传输层协议,调用receive相当于是执行内核中udp相关的代码,将UDP数据报的载荷取出来,拷贝到用户提供的byte[] 数组中。

public class EchoServer {
    private DatagramSocket socket = null;
    public EchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            DatagramPacket requestPcket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPcket);
            String request = new String(requestPcket.getData(),0, requestPcket.getLength());
            String response = process(request);
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPcket.getSocketAddress());
            socket.send(responsePacket);
            //打印本次请求响应的结果
            System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPcket.getAddress().toString(),
                    requestPcket.getPort(),request,response);
        }
    }
    public String process(String request) {
        return request;
    }
}

客户端

我们在构造客户端Socket对象时,不需要显式的去绑定一个端口,而是由系统分配一个空闲端口。

服务器的端口:需要固定指定,为了方便客户端找到服务器程序。
客户端的端口:由系统自动分配的,如果我们手动指定,可能会与客户端其他程序的端口冲突
为什么服务器不怕冲突?
因为服务器上面的程序可控,而客户端是运行在我们用户电脑上,环境复杂,更不可控。

首先我们需要创建一个Socket对象,并且获取服务器的ip和端口号

private DatagramSocket socket = null;
    private String serverIp = null;
    private int serverPort = 0;
    
    public EchoClient(String serverIp,int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

我们一次网络通信涉及到五元组:
源IP,源端口,目的IP,目的端口,协议类型。

然后从控制台接收我们需要发送端数据。

System.out.print("> ");
            String request = scan.next();
            if(request.equals("exit")) {
                System.out.println("客户端退出");
                break;
            }

我们将request字符串构造成DatagramPacket进行发送。

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

我们在构造DatagramPacket的时候,需要将ip和端口号都传入,此处需要传入的IP是32位的整数形式,但我们这里的ip是字符串,所以需要使用InetAddress.getByName进行转换,然后进行发送。

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.println(response);
public class EchoClient {
    private DatagramSocket socket = null;
    private String serverIp = null;
    private int serverPort = 0;

    public EchoClient(String serverIp,int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scan = new Scanner(System.in);
        while (true) {
            System.out.print("> ");
            String request = scan.next();
            if(request.equals("exit")) {
                System.out.println("客户端退出");
                break;
            }
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            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.println(response);
        }
    }
}

在这里插入图片描述
在这里插入图片描述
我们分别启动客户端和服务器
在这里插入图片描述
在这里插入图片描述
我们此处显示的客户端IP是环回IP,端口号是系统随机分配的。
我们客户端服务器程序,一个服务器是给许多客户端提供服务的,但是我们IDEA默认只能启动一个客户端,我们需要手动设置一下。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
现在我们就可以创建多客户端与服务器进行通信了。
端口冲突
一个端口只能被一个进程使用,如果有多个使用就不行。
在这里插入图片描述