网络编程套接字之UDP实现回显服务器及客户端

目录

前言:

基础理解

传输层协议

UDP

TCP

Socket API

DatagramSocket API

DatagramPacket API

UDP实现回显服务器

完整代码展现(有详细注释)

UDP实现回显客户端

完整代码展现(有详细注释)

小结:


前言:

    通过套接字Socket就可以实现客户端发送请求,服务起接收请求,处理完成后就可以响应给客户端。这样的一套流程就实现了数据在网络上的传输。

基础理解

    网络编程中,在硬件上使用网卡发送和接收数据。在java中使用Socket直接操作网卡,而对于操作系统来说一切皆文件,那么这个Socket对象在操作系统中是被当作文件处理的。Socket就是操作系统给应用程序提供的接口。

    Socket所提供的api和传输层密切相关,应用层首先接触的就是传输层。使用Socket所提供的api就可以实现应用层的代码并且和传输层进行交互。

    客户端发起请求 --> 服务器接收请求 --> 服务器处理请求并响应给客户端 --> 客户端接收响应

传输层协议

UDP

    特点:无连接,不可靠传输,面向数据报,全双工,大小首先(一次最多64k),有接收缓冲区无发送缓冲区。

TCP

    特点:有连接。可靠传输,面向字节流,全双工,大小不限,有接收缓冲区和发送缓冲区。

理解:

    1)无连接:不需要建立客户端和服务器之间的连接,就可以发送数据。(例如微信发消息)

    2)有连接:需要建立客户端和服务器之间的连接,才可发送数据。(例如打电话,需要接听)

    3)不可靠传输:发送方不知道数据是发过去了,还是丢包了。

    4)可靠传输:发送方知道自己的消息是否发送过去。

    注意:可靠性就是针对发送方是否清楚数据是否发送过去。

    5)面向数据报:数据传输以“数据报”为基本单位,一块一块的发数据。

    6)面向字节流:数据传输和读文件类似,“流式”的。一次发送部分数据,也可以发送全部数据。

    7)全双工:可以同时发送和接收数据,那么半双工就不支持。

Socket API

    java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报。

DatagramSocket API

DatagramSocket构造方法

 注意:

    创建一个UDP数据报套接字的Socket,绑定本机任意一个随机端口(一般用于客户端)

 注意:

    创建一个UDP数据报套接字的Socket,绑定指定端口(一般用于服务端)

DatagramSocket方法

注意:

    从网卡接收数据报。这个参数需要一个空的DatagramPacket对象,当从网卡接收到数据报就会填充好这个空的对象,以便供我们处理数据。

    如果没有接收数据报,这个方法会阻塞等待。

 注意:

    将已经构造好的数据报发送到网卡。不会阻塞等待直接发送。

 注意:

     在操作系统中Socket对象是被当作文件处理的,那么就需要释放pcb中文件描述符表中的资源。

DatagramPacket API

DatagramPacket构造方法

 注意:

    构造一个DatagramPacket以用来接收数据报,接收的数据保存在buf缓冲数组中,接收的指定长度。

 注意:

    构造一个DatagramPacket以用来接收数据报,数据填充为字节数组,从0起始位置到指定长度(offset,length),address指定目的主机IP和端口号。(一般处理完请求后,构造成数据报来发送)

DatagramPacket方法

 注意:

     从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址。

 注意:

     从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号。

 注意:

     获取数据报中的数据,就是缓冲数组。

UDP实现回显服务器

    服务器是被动的一方,需要接收客户端发起的请求。那么客户端就必须明确服务器的ip和具体进程的端口号。所以在实现服务器时就必须指定端口号,这里实现的是本机到本机的数据发送,ip就使用环回ip即可。

    由于不清楚客户端什么时候发起请求,那么服务器不能休息(随时待命)。这里使用死循环的方式,但它不会一直循环,因为receive()方法当没有接收到请求时会阻塞等待。

    我们首先需要明确服务器的工作流程。接收客户端的请求 --> 处理请求 --> 将响应发送给客户端。

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

注意:

    首先构造一个空的DatagramPacket对象,传入缓冲数组,和指定长度。当下面receive()方法从网卡接收到客户端请求时就会填充这个空对象。(数据是写入了缓冲数组)

    当receive()方法当没有接收到请求时会阻塞等待。(随时待命)

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

注意:

    由于接收的数据构造成了数据报,这样不利于我们处理数据。我们将数据报中的数据取出来构造成字符串。

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

注意:

    服务器针对请求进行需求处理,这里的process是一个方法。由于我们实现的是回显服务器,即直接返回这个字符串即可。

 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
socket.send(responsePacket);

注意:

    这里将处理完后的响应构造成数据报,然后发送给客户端程序。这里需要传入字节数组,填充的具体长度,(这里需要用字节数组的长度,不能用字符串的长度。转换之后两者长度是不一致的)和客户端的ip和端口号(getSocketAddress()方法可以获得发送方的ip和端口号)。

    当构造完成之后直接将数据报发给客户端即可。

完整代码展现(有详细注释)

public class UdpEchoSever {
    //Socket对象直接操作的是网卡,在操作系统中任务Socket对象是文件(一切皆文件)
    //通过Socket对象接收和发送数据
    private DatagramSocket socket = null;
    public UdpEchoSever(int port) throws SocketException {
        //服务器是被动的一方,客户端必须找到服务器的端口,才能找到指定程序,因此服务器必须指定端口号
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            //构造空的Packet对象,传入缓冲数组
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //receive从网卡接收数据,解析后填充这个空对象(输出形参数)(可以认为写入了缓冲数组)
            //客户端如果没有发请求receive就会阻塞,直到客户端发送请求(保证这里不会一直循环)
            socket.receive(requestPacket);

            //根据接收的数据(由于接收的数据不方便处理),因此构造成字符串
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //服务器响应处理
            String response = process(request);
            //构造发送的数据报,字节数组,字节数组长度,IP和端口(根据响应的字符串)
            //这个DatagramPacket只认字节数组,因此就需要获取字节数组的长度而不是字符的个数
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            //发送数据到Ip和端口指定的客户端程序
            socket.send(responsePacket);
            //打印下,请求响应的中间结果
            System.out.printf("源IP:%s 源端口:%d 请求数据:%s 响应数据:%s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }
    //回显服务器,处理直接返回数据(响应)
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        //端口号的指定在 1024 -- 65535 里指定
        UdpEchoSever sever = new UdpEchoSever(8280);
        sever.start();
    }
}

UDP实现回显客户端

    客户端发送数据需要明确服务器的ip和具体进程的端口号。客户端的端口号我们不需要手动指定,因为客户端程序是存在于客户主机上,我们如果手动指定就很可能与其他进程端口号冲突,这样就直接抛异常了(Address already in use)。直接让操作系统随机分配一个空闲的端口号。

    那么为什么服务端我们可以指定端口号,这样就不怕与其他进程冲突了么?因为服务器在我们自己手里,我们明确里面的各种端口号,简单来说就是可控的。 

    我们首先明确客户端的工作流程。用户输入数据 --> 发送到服务器 --> 接收服务器的响应。这里也使用死循环,和上面一样receive()方法会阻塞,不会一直循环。

 Scanner scanner = new Scanner(System.in);
 System.out.println("输入你要发送的数据:");
 String request = scanner.next();
 if (request.equals("exit")) {
        System.out.println("bye bye");
        break;
 }

注意:

    提示用户输入数据,这里做了简单的判断。

 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(severIp), severPort);
 socket.send(requestPacket);

注意:

    根据用户输入的数据构造成数据报。需要字节数组,具体填充的长度(同样的需要字节数组长度而不是字符串长度),ip(由于这里需要一个32位的ip,而上面的是字符串,因此需要转换)和服务器端口号。然后直接发送即可。

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

注意:

    接收服务器的响应。首先构造一个空的DatagramPacket对象,传入缓冲数组和指定长度。receive()方法从网卡接收到数据报然后构造好这个空的对象。

    receive()当没有接收到响应前同样的也会阻塞等待。

 String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
 System.out.println(response);

注意:

    由于是数据报不利于用户观察数据,因此转转换为字符串。获取到数据报中的缓冲数组,从0位置到指定长度来构造这个字符串,最终显示给用户。

完整代码展现(有详细注释)

public class UdpEchoClient {
    private DatagramSocket socket = null;
    //客户端需要知道服务器的IP,和端口,这里先存一下
    private String severIp = null;
    private int severPort = 0;
    public UdpEchoClient(String severIp, int severPort) throws SocketException {
        //客户端不需要指定端口号,客户端程序在用户手里,指定端口号就可能和其他进程重复。因此让操作系统分配一个空闲的端口
        //服务器为什么指定端口不怕重复呢?因为服务器在程序员手里我们清楚端口号的使用(可控的),而客户端是(不可控的)
        socket = new DatagramSocket();
        this.severIp = severIp;
        this.severPort = severPort;
    }
    //客户端启动
    public void start() throws IOException {
        //用户输入数据
        while (true) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入你要发送的数据:");
            String request = scanner.next();
            if (request.equals("exit")) {
                System.out.println("bye bye");
                break;
            }
            //发送数据报(构造DatagramPacket对象)
            //此处的IP需要一个32位的整数,而上面的是字符串,需要转换
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(severIp), severPort);
            socket.send(requestPacket);

            //接收数据报(填充这个空对象)(阻塞到服务器发送过来数据)
            //receive的阻塞操作系统实现的,JAVA只是封装了一下
            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 static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 8280);
        udpEchoClient.start();
    }
}

小结:

    这里大多是api的使用,我们要理解其中的原理,便能得心应手。

猜你喜欢

转载自blog.csdn.net/weixin_62353436/article/details/128814302