• 6.网络编程套接字(上)


    大家好,我是晓星航。今天为大家带来的是 网络编程套接字 相关的讲解!😀

    1.网络编程基础

    1.1为什么需要网络编程?——丰富的网络资源

    用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。

    与本地打开视频文件类似,只是视频文件这个资源的来源是网络。

    相比本地资源来说,网络提供了更为丰富的网络资源:

    所谓的网络资源,其实就是在网络中可以获取的各种数据资源。

    而所有的网络资源,都是通过网络编程来进行数据传输的。

    1.2什么是网络编程

    网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。

    当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据, 也属于网络编程。

    特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编 程。

    但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:

    • 进程A:编程来获取网络资源
    • 进程B:编程来提供网络资源

    1.3网络编程中的基本概念

    1.3.1发送端和接收端

    在一次网络数据传输时:

    发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。

    接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。

    收发端:发送端和接收端两端,也简称为收发端。

    注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

    1.3.2请求和响应

    一般来说,获取一个网络资源,涉及到两次网络数据传输:

    • 第一次:请求数据的发送
    • 第二次:响应数据的发送

    好比在快餐店点一份炒饭:

    先要发起请求:点一份炒饭,再有快餐店提供的对应响应:提供一份炒饭

    1.3.3客户端和服务端

    服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。

    客户端获取服务的一方进程,称为客户端。

    对于服务来说,一般是提供:

    • 客户端获取服务资源

    • 客户端保存资源在服务端

    好比在银行办事:

    • 银行提供存款服务:用户(客户端)保存资源(现金)在银行(服务端)
    • 银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管的现金)

    1.3.4常见的客户端服务端模型

    最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:

    1. 客户端先发送请求到服务端
    2. 服务端根据请求数据,执行相应的业务处理
    3. 服务端返回响应:发送业务处理结果
    4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

    2.Socket套接字

    至于为什么英译为插座,我们却将他用为网络编程的API我们不得而知…

    可以认为 Socket api 是和传输层密切相关的

    传输层里,提供了两个最核心的协议 UDP TCP

    2.1概念

    Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基 于Socket套接字的网络程序开发就是网络编程。

    2.2分类

    Socket套接字主要针对传输层协议划分为如下三类:

    流套接字:使用传输层TCP协议

    TCP,即Transmission Control Protocol(传输控制协议),传输层协议。

    以下为TCP的特点(细节后续再学习):

    • 有连接
    • 可靠传输
    • 面向字节流
    • 有接收缓冲区,也有发送缓冲区
    • 大小不限

    对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情 况下,是无边界的数据,可以多次发送,也可以分开多次接收。

    数据报套接字:使用传输层UDP协议

    UDP,即User Datagram Protocol(用户数据报协议),传输层协议。

    以下为UDP的特点(细节后续再学习):

    无连接

    不可靠传输

    面向数据报

    有接收缓冲区,无发送缓冲区

    大小受限:一次最多传输64k

    对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一 次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。

    原始套接字

    原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。

    2.3Java数据报套接字通信模型

    对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数 据报,一次接收全部的数据报。

    java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:

    以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请 求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:

    2.4Java流套接字通信模型

    2.5Socket编程注意事项

    1. 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场 景,一般都是不同主机。
    2. 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
    3. Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议, 也需要考虑,这块我们在后续来说明如何设计应用层协议。
    4. 关于端口被占用的问题

    如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端 口被占用。对于java进程来说,端口被占用的常见报错信息如下:

    此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用。以下为通过端口号查进程的方式:

    • 在cmd输入 netstat -ano | findstr 端口号 ,则可以显示对应进程的pid。如以下命令显 示了8888进程的pid

    图片

    • 在任务管理器中,通过pid查找进程

    图片

    解决端口被占用的问题

    • 如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B
    • 如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。

    3.UDP数据报套接字编程

    无连接(发微信发qq信息)

    不可靠传输(没有已读功能)

    面向数据报 (数据传输则以一个个的"数据报"为基本单位。 一个数据报可能是若干个字节,带有一定的格式的)

    全双工(一个通信通道,可以双向传输。 既可以发送又可以接收)- 一根网线,里面其实有 8 根线

    共 8字节

    2字节 源端口 发送方的端口

    2字节 目的端口 接收方的端口

    2字节 UDP 报文长度

    2字节 校验和

    发送方,把要接受的数据计算出校验和(checksum1)

    接收方,收到的数据就把数据按照同样的方式再计算一次校验和(checksum2)

    同时接收方也收到了 checksum1.接收方对比 checksum1 和 checksum2 是否相同。

    检验和:作用是验证传输的数据是否是正确的。(网络传输,本质上就是光信号/电信号,他们可能受到一些物理环境的影响[电场/磁场/高能射线])

    3.1DatagramSocket API

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

    用他来表示一个 socket 对象。

    DatagramSocket 构造方法:

    port - 是一个端口号(简单的整数)

    第二个构造方法中传入 port 就是让当前的 socket 对象和这个指定的端口,关联起来。

    而第一个构造方法中没有指定端口号,此时系统则会自动分配一个空闲的端口。

    DatagramSocket 方法:

    第一个方法 receive 传入的相当于是一个空对象。receive 方法内部,会对参数的这个空对象进行内容填充,从而构造出结果数据了。参数也是一个"输出型参数"。

    第三个方法 close 是用来释放资源的,用完之后要记得关闭。

    3.2DatagramPacket API

    UDP 来说,传输数据的基本单位,DatagramPacket。

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

    用它来表示 UDP 中传输的一个报文。

    DatagramPacket 构造方法:

    第一个构造方法中的 buf 参数相当于是把 buf 这个缓冲区给设置进去了,length用来接收指定长度。

    第二个构造方法,相当于 构造缓冲区buf + 地址address,构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。

    DatagramPacket 方法:

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

    3.3InetSocketAddress API

    InetSocketAddressSocketAddress 的子类 )构造方法:

    3.4示例一:一发一收(无响应)

    以下为一个客户端一次数据发送,和服务端多次数据接收(一次发送一次接收,可以接收多次),即只有客户端请求,但没有服务端响应的示例:

    3.4.1UDP服务端

    服务器的工作流程:

    1. 读取请求并解析
    2. 根据请求计算响应
    3. 构造响应并写回客户端
    package network;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.SocketException;
    
    //UDP 版本的回服务器
    public class UdpEchoServer {
        //网络编程,本质上是要操作网卡
        //但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的叫做"socket"这样的文件来抽象表示网卡
        //因此进行网络通信,是必须要先有一个 socket 对象。
        private DatagramSocket socket = null;
        //对于服务器来说,创建 socket 对象的同时,要让他绑定上一个具体的端口号。
        //服务器一定要关联上一个具体的端口的!!!
        //服务器是网络传输中,被动的一方,如果是操作系统随机分配的端口,此时客户端就不知道端口是啥,也就无法进行通讯了。
        public UdpEchoServer(int port) throws SocketException {
            socket = new DatagramSocket(port);
        }
    
        public void start() throws IOException {
            System.out.println("服务器启动!");
            //服务器不是只给一个客户端提供服务,需要服务很多客户端!
            while (true) {
                //只要有客户端过来,就可以提供服务。
                //1.读取客户端发来的请求是啥
                //receive 方法的参数是一个输出型参数,需要先构造好一个空白的 DatagramPacket 对象,交给 receive 来进行填充。
                DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
                socket.receive(requestPacket);
                //此时这个 DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来,构造成一个字符串。
                String request = new String(requestPacket.getData(),0,requestPacket.getLength());
                //2.根据请求来计算响应,由于此处是回显服务器,响应和请求相同
                String response = process(request);
                //3.把响应写回客户端,send 的参数也是 DatagramPacket,需要把这个 Packet 对象构造好。
                //  此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应数据来构造
                DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
                socket.send(responsePacket);
                //4.打印一下,当前这次请求响应的处理中间结果
                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 {
            //端口号的指定可以随便指定,在1024 -> 65535 这个范围里随便挑个数字就行
            UdpEchoServer server = new UdpEchoServer(9090);
            server.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    运行后,服务端就启动了,控制台输出如下:

    可以看出,此时代码是阻塞等待在 socket.receive(packet) 代码行,直到接收到一个UDP数据报。

    在客户端输入字符串 “hello world” 之后:

    这里的数字 是我们客户端的 IP 和 端口号,此处 IP 是环回 IP ,这里的端口是系统随机分配的。

    对于客户端服务器程序来说一个服务器要给很多客户提供服务的,咱们也就可以需要构造出多个客户端来进行测试。

    但是我们idea是不支持多个客户端同时启动的,这时我们可以这样设置:

    多个客户端运行时客户端的截图:

    多个客户端运行时服务器的截图:

    在客户端输入 exit 后:

    可以发现虽然我们客户端退出了,但是我们服务端是会继续保持在线的,因为我们要确保每一个用户在客户端登陆的时候我们都能为其提供服务。

    代码解析:

    new DatagramPacket 相当于刚开始给他一个空白的纸条,客户写好之后,再把纸条给老板,老板根据纸条写的需求来做食物。

    receive 内部会针对 参数 对象填充数据 ,填充的数据来自于网卡。

    第一个参数为 传响应对象的这个字节数组

    第二个参数为指定长度,此处这个长度,使用 response.length() 是否可以? 是不是必须写作 response.getByte().length ?

    答:不可以,response.length() 是计算字符个数,而 response.getByte().length 是在算字节个数

    那么为什么我们这里一定要返回字节的个数呢,字符不行吗?

    答:不行,因为我们这里的 DatagramPacket 不认识字符,只认识字节!!!

    最后的 requestPacket.getSocketAddress() 这个参数是用来 获取到客户端 ip 和 端口号。(这两个信息本身就在 requestpacket 中)

    对应API解释参数:

    会快速进行循环,每循环一次,就处理一次请求。

    如果客户端发来了请求,receive就能顺利读出来

    如果客户端没有发请求,此时receive就会阻塞!!!(和Scanner操作类似,取决于用户啥时候输入数据(请求) )

    如果有很多客户端,并且请求发的很快,那么此时我们的这个 while 循环会不会因为客户端请求太多,处理不过来了呢?

    答:是会的,我们此时使用多线程,更充分的调动计算机的硬件资源,多加机器(分布式),但是相应的消耗的硬件资源也会增多。

    第一个参数:获取到 packet 里面的 ip

    第二个参数:获取到里面的端口

    注意:

    1. 服务器不可以是随机端口,需要是固定端口,这样才方便客户端找到我。

    3.4.2UDP客户端

    package org.example.udp.demo1;
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetSocketAddress;
    import java.net.SocketAddress;
    public class UdpClient {
        // 服务端socket地址,包含域名或IP,及端口号
        private static final SocketAddress ADDRESS = new
    InetSocketAddress("localhost", 8888);
        public static void main(String[] args) throws IOException {
            // 4.创建客户端DatagramSocket,开启随机端口就行,可以发送及接收UDP数据报
            DatagramSocket socket = new DatagramSocket();
            // 5-1.准备要发送的数据
            byte[] bytes = "hello world!".getBytes();
            // 5-2.组装要发送的UDP数据报,包含数据,及发送的服务端信息(服务器IP+端口号)
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length, 
    ADDRESS);
            // 6.发送UDP数据报
            socket.send(packet);
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    客户端启动后会发送一个"hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:

    ---------------------------------------------------
    等待接收UDP数据报...
    客户端IP127.0.0.1
    客户端端口号:57910
    客户端发送的原生数据为:[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 
    0, 0, 0, ...此处省略很多0]
    客户端发送的文本数据为:hello world!                                                 
                                                                                    
                     
                     
                     
                     
                     
                     
                     
                     
                     
                     
                     
       
    ---------------------------------------------------
    等待接收UDP数据报...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    从以上可以看出,发送的UDP数据报(假设发送的数据字节数组长度为M),在接收到以后(假设接收 的数据字节数组长度为N):

    1. 如果N>M,则接收的byte[]字节数组中会有很多初始化byte[]的初始值0,转换为字符串就是空白 字符;
    2. 如果N

    要解决以上问题,就需要发送端和接收端双方约定好一致的协议,如规定好结束的标识或整个数据的长 度。

    自己版本的UDP客户端:

    package network;
    
    import java.io.IOException;
    import java.net.*;
    import java.util.Scanner;
    
    public class UdpEchoClient {
        private DatagramSocket socket = null;
        private String serverIp = null;
        private int serverPort = 0;
        //一次通讯,需要有两个ip,两个端口
        //客户端的 ip 是 127.0.0.1 已知
        //客户端的 port 是系统自动分配的
        //服务器 ip 和 端口 也需要告诉客户端,才能顺利把消息发给服务器
        public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
            socket = new DatagramSocket();
            this.serverIp = serverIp;
            this.serverPort = serverPort;
        }
    
        public void start() throws IOException {
            System.out.println("客户端启动!");
            Scanner scanner = new Scanner(System.in);
            while (true) {
                //1.从控制台读取要发送的数据
                System.out.print("> ");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("goodbye");
                    break;
                }
                //2.构造成 UDP 请求,并发送
                //构造这个 Packet 的时候,需要把 serverIp 和 port 都传入过来, 但是此处 IP 地址需要填写的是一个32位的整数形式
                //上述的 IP 地址是一个字符串,需要使用 InetAddress.getByName 来进行一个转换
                DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                        InetAddress.getByName(serverIp),serverPort);
                socket.send(requestPacket);
                //3.读取服务器的 UDP 响应,并解析
                DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
                socket.receive(responsePacket);
                String response = new String(responsePacket.getData(),0,responsePacket.getLength());
                //4.把解析好的结果显示出来
                System.out.println(response);
            }
        }
    
        public static void main(String[] args) throws IOException {
            //端口号的指定可以随便指定,在1024 -> 65535 这个范围里随便挑个数字就行
            UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
            client.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    启动之后:

    输入字符串"helloworld" 之后:

    在客户端输入了 exit 后:

    可以看到我们输入了 exit 之后,我们客户端就退出了程序。

    多个客户端一起运行时:

    多个客户端运行时服务器的截图:

    代码解析:

    对于服务器发给客户端这个操作来说。服务器的端口就是源端口,客户端的端口就是目的端口。

    对于客户端发给服务器的操作来说,服务器的端口就是目的端口,客户端的端口就是源端口。

    构造这个 Socket 对象,不需要显示绑定一个端口。(让操作系统自动分配一个端口)

    和我们这里的 服务端 对比就可以发现我们服务端是需要手动绑定一个端口的而客户端来说,端口可以是系统分配的。

    3.5示例二:请求响应

    示例一只是客户端请求和服务端接收,并没有包含服务端的返回响应。以下是对应请求和响应的改造:

    构造一个展示服务端本地某个目录(BASE_PATH)的下一级子文件列表的服务

    (1)客户端先接收键盘输入,表示要展示的相对路径(相对BASE_PATH的路径)

    (2)发送请求:将该相对路径作为数据报发送到服务端

    (3)服务端接收并处理请求:根据该请求数据,作为本地目录的路径,列出下一级子文件及子文件夹

    (4)服务端返回响应:遍历子文件和子文件夹,每个文件名一行,作为响应的数据报,返回给客户端

    (5)客户端接收响应:简单的打印输出所有的响应内容,即文件列表。

    为了解决空字符或长度不足数据丢失的问题,客户端服务端约定好统一的协议:这里简单的设计为 ASCII结束字符 \3 表示报文结束。

    以下为整个客户端服务端的交互执行流程:

    3.5.1UDP服务端

    package org.example.udp.demo2;
    import java.io.File;
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.nio.charset.StandardCharsets;
    public class UdpServer {
        //服务器socket要绑定固定的端口
        private static final int PORT = 8888;
        //本地文件目录要展示的根路径
        private static final String BASE_PATH = "E:/TMP";
        public static void main(String[] args) throws IOException {
            // 1.创建服务端DatagramSocket,指定端口,可以发送及接收UDP数据报
            DatagramSocket socket = new DatagramSocket(PORT);
            //不停的接收客户端udp数据报
            while (true){
                // 2.创建数据报,用于接收客户端发送的数据
                byte[] requestData = new byte[1024];//1m=1024kb, 1kb=1024byte, UDP最64k(包含UDP首部8byte)
                DatagramPacket requestPacket = new DatagramPacket(requestData, 
    requestData.length);
                System.out.println("------------------------------------------------
    ---");
                System.out.println("等待接收UDP数据报...");
                // 3.等待接收客户端发送的UDP数据报,该方法在接收到数据报之前会一直阻塞,接收到数
    据报以后,DatagramPacket对象,包含数据(bytes)和客户端ip、端口号
                socket.receive(requestPacket);
    System.out.printf("客户端IP:%s%n", 
    requestPacket.getAddress().getHostAddress());
                System.out.printf("客户端端口号:%s%n", requestPacket.getPort());
                // 7.接收到的数据作为请求,根据请求数据执行业务,并返回响应
                for (int i = 0; i < requestData.length; i++) {
                    byte b = requestData[i];
                    if(b == '\3') {
                        // 7-1.读取请求的数据:读取到约定好的结束符(\3),取结束符之前的内容
                        String request = new String(requestData, 0, i);
                        // 7-2.根据请求处理业务:本地目录根路径+请求路径,作为要展示的目录,列
    出下一级子文件
                        //请求的文件列表目录
                        System.out.printf("客户端请求的文件列表路径为:%s%n", BASE_PATH +
    request);
                        File dir = new File(BASE_PATH + request);
                        //获取下一级子文件,子文件夹
                        File[] children = dir.listFiles();
                        // 7-3.构造要返回的响应内容:每个文件及目录名称为一行
                        StringBuilder response = new StringBuilder();
                        if(children != null){
                            for (File child : children) {
                                response.append(child.getName()+"\n");
                           }
                       }
                        //响应也要约定结束符
                        response.append("\3");
                        byte[] responseData =
    response.toString().getBytes(StandardCharsets.UTF_8);
                        // 7-4.构造返回响应的数据报DatagramPacket,注意接收的客户端数据报包IP和端口号,要设置到响应的数据报中
                        DatagramPacket responsePacket = new
    DatagramPacket(responseData, responseData.length, 
    requestPacket.getSocketAddress());
                        // 7-5.发送返回响应的数据报
                        socket.send(responsePacket);
                        break;
                   }
               }
           }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    以上服务端运行结果和示例一是一样的:

    ---------------------------------------------------
    等待接收UDP数据报...
    
    • 1
    • 2

    3.5.2UDP客户端

    package org.example.udp.demo2;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetSocketAddress;
    import java.net.SocketAddress;
    import java.nio.charset.StandardCharsets;
    import java.util.Scanner;
    public class UdpClient {
        // 服务端socket地址,包含域名或IP,及端口号
        private static final SocketAddress ADDRESS = new
    InetSocketAddress("localhost", 8888);
        public static void main(String[] args) throws IOException {
            // 4.创建客户端DatagramSocket,开启随机端口就行,可以发送及接收UDP数据报
            DatagramSocket socket = new DatagramSocket();
            // 5-1.准备要发送的数据:这里调整为键盘输入作为发送的内容
            Scanner scanner = new Scanner(System.in);
            while(true){
                System.out.println("------------------------------------------------
    ---");
                System.out.println("请输入要展示的目录:");
                // 5-2.每输入新行(回车),就作为UDP发送的数据报,为了接收端获取有效的内容(去除
    空字符串),约定\3为结束
                String request = scanner.nextLine() + "\3";
                byte[] requestData = request.getBytes(StandardCharsets.UTF_8);
                // 5-3.组装要发送的UDP数据报,包含数据,及发送的服务端信息(服务器IP+端口号)
                DatagramPacket requestPacket = new DatagramPacket(requestData, 
    requestData.length, ADDRESS);
                // 6.发送UDP数据报
                socket.send(requestPacket);
                // 8.接收服务端响应的数据报,并根据响应内容决定下个步骤(我们这里简单的打印即可)
                // 8-1.创建数据报,用于接收服务端返回(发送)的响应
                byte[] responseData = new byte[1024];
                DatagramPacket responsePacket = new DatagramPacket(responseData, 
    responseData.length);
                // 8-2.接收响应数据报
                socket.receive(responsePacket);
                System.out.println("该目录下的文件列表为:");
                // byte[]下次解析的起始位置
                int next = 0;
                for (int i = 0; i < responseData.length; i++) {
                    byte b = responseData[i];
                    if(b == '\3')//结束符退出
                        break;
                    if(b == '\n'){//换行符时进行解析
                        //起始位置到换行符前一个索引位置为要解析的内容
                        String fileName = new String(responseData, next, i-next);
                        System.out.println(fileName);
                        //下次解析从换行符后一个索引开始
                        next = i+1;
                   }
               }
           }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    客户端启动后会等待输入要展示的路径:

    ---------------------------------------------------
    请输入要展示的目录:
    
    • 1
    • 2

    在输入想查看的目录路径后,会接收并打印服务端响应的文件列表数据:

    ---------------------------------------------------
    请输入要展示的目录:
    /
    该目录下的文件列表为:
    1
    2
    60441b1b8a74be3695ccc0d970693815
    8f25103aa249707ee4ab17635142cd0e
    ---------------------------------------------------
    请输入要展示的目录:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此时服务端也会打印接收到的客户端请求数据:

    ---------------------------------------------------
    等待接收UDP数据报...
    客户端IP127.0.0.1
    客户端端口号:57297
    客户端请求的文件列表路径为:E:/TMP/
    ---------------------------------------------------
    等待接收UDP数据报...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.6示例三:一发一收(有响应) - 根据信息返回对应的响应

    3.6.1UDP服务端

    package network;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.SocketException;
    
    //UDP 版本的回服务器
    public class UdpEchoServer {
        //网络编程,本质上是要操作网卡
        //但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的叫做"socket"这样的文件来抽象表示网卡
        //因此进行网络通信,是必须要先有一个 socket 对象。
        private DatagramSocket socket = null;
        //对于服务器来说,创建 socket 对象的同时,要让他绑定上一个具体的端口号。
        //服务器一定要关联上一个具体的端口的!!!
        //服务器是网络传输中,被动的一方,如果是操作系统随机分配的端口,此时客户端就不知道端口是啥,也就无法进行通讯了。
        public UdpEchoServer(int port) throws SocketException {
            socket = new DatagramSocket(port);
        }
    
        public void start() throws IOException {
            System.out.println("服务器启动!");
            //服务器不是只给一个客户端提供服务,需要服务很多客户端!
            while (true) {
                //只要有客户端过来,就可以提供服务。
                //1.读取客户端发来的请求是啥
                //receive 方法的参数是一个输出型参数,需要先构造好一个空白的 DatagramPacket 对象,交给 receive 来进行填充。
                DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
                socket.receive(requestPacket);
                //此时这个 DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来,构造成一个字符串。
                String request = new String(requestPacket.getData(),0,requestPacket.getLength());
                //2.根据请求来计算响应,由于此处是回显服务器,响应和请求相同
                String response = process(request);
                //3.把响应写回客户端,send 的参数也是 DatagramPacket,需要把这个 Packet 对象构造好。
                //  此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应数据来构造
                DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                        requestPacket.getSocketAddress());
                socket.send(responsePacket);
                //4.打印一下,当前这次请求响应的处理中间结果
                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 {
            //端口号的指定可以随便指定,在1024 -> 65535 这个范围里随便挑个数字就行
            UdpEchoServer server = new UdpEchoServer(9090);
            server.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    package network;
    
    import java.io.IOException;
    import java.net.SocketException;
    import java.util.HashMap;
    import java.util.Map;
    
    //对于我们的 DictServer 来说,和 EchoServer 相比,大部分的东西都是一样的
    //主要是 "根据请求计算响应" 这个步骤不太一样。
    public class UdpDictServer extends UdpEchoServer{
        private Map<String, String> dict = new HashMap<>();
        public UdpDictServer(int port) throws SocketException {
            super(port);
    
            //给这个 dict 设置点内容
            dict.put("cat","小猫");
            dict.put("dog","小狗");
            dict.put("duck","小鸭");
            dict.put("pig","小猪");
            //当然这里可以无限多的设置键值对......
    
        }
        @Override
        public String process (String request) {
            //查词典的过程。
            return dict.getOrDefault(request,"当前单词没有查到结果!");
        }
    
        public static void main(String[] args) throws IOException {
            UdpDictServer server = new UdpDictServer(9090);
            server.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    由于我们之前的示例一已经谢过了一遍,因此我们这里直接继承 UdpEchoServer 然后重写一下peocess方法和构造方法即可!!!

    服务器运行结果图:

    3.6.2UDP客户端

    package network;
    
    import java.io.IOException;
    import java.net.*;
    import java.util.Scanner;
    
    public class UdpEchoClient {
        private DatagramSocket socket = null;
        private String serverIp = null;
        private int serverPort = 0;
        //一次通讯,需要有两个ip,两个端口
        //客户端的 ip 是 127.0.0.1 已知
        //客户端的 port 是系统自动分配的
        //服务器 ip 和 端口 也需要告诉客户端,才能顺利把消息发给服务器
        public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
            socket = new DatagramSocket();
            this.serverIp = serverIp;
            this.serverPort = serverPort;
        }
    
        public void start() throws IOException {
            System.out.println("客户端启动!");
            Scanner scanner = new Scanner(System.in);
            while (true) {
                //1.从控制台读取要发送的数据
                System.out.print("> ");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("goodbye");
                    break;
                }
                //2.构造成 UDP 请求,并发送
                //构造这个 Packet 的时候,需要把 serverIp 和 port 都传入过来, 但是此处 IP 地址需要填写的是一个32位的整数形式
                //上述的 IP 地址是一个字符串,需要使用 InetAddress.getByName 来进行一个转换
                DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                        InetAddress.getByName(serverIp),serverPort);
                socket.send(requestPacket);
                //3.读取服务器的 UDP 响应,并解析
                DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
                socket.receive(responsePacket);
                String response = new String(responsePacket.getData(),0,responsePacket.getLength());
                //4.把解析好的结果显示出来
                System.out.println(response);
            }
        }
    
        public static void main(String[] args) throws IOException {
            //端口号的指定可以随便指定,在1024 -> 65535 这个范围里随便挑个数字就行
            UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
            client.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    这里我们的客户端和示例一的客户端代码是一模一样的,所以直接复制过来了。

    由上图可以看到,我们客户端输入对应的英文,则会返回他们对应的翻译出来。

    3.7客户端和服务端区别

    1.定义不同

    客户端与服务器相对应,为客户提供本地服务的程序,而服务器是指网络中能对其他机器提供某些服务的计算机系统。

    2.程序编写不同

    客户端程序通常不需要我们编写,可以使用浏览器等现成的程序。而服务器端则需要编写相应的服务端程序。

    3.组成不同

    客户端通常只需要一个浏览器等软件,而服务器端则由中央处理器、内存、芯片组、I/O总线、I/O设备、电源、机箱和相关软件等组成。

    4.储存方式不同

    客户端通常不需要储存数据,而服务器端需要储存大量的数据。部署服务器端闪存的方式包括SAS和SATA、PCIe闪存卡、NVMe闪存和双列直插式内存插槽等多种方式。

    5.服务对象不同

    客户端是使用服务的对象,而服务器端则是为客户端提供服务的对象。

    3.8客户端和服务端相互配合图解

    服务器:

    客户端:

    3.9服务器端口冲突异常

    故名思意就是 一个端口被两个服务器同时抢占,那么此时必有一个服务器会阻塞等待抛出异常。

    此时我们有 UdpDictServerUdpEchoServer 两个服务其同时抢占9090端口导致 我们的 UdpEchoServer 端口冲突抛出异常。

    这里我们圈出的异常就是告诉我们此时出现了 服务器端口冲突。

    Address 表示的含义为 “绑定IP + 端口”

    感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘

  • 相关阅读:
    如何用python给女神写一封照片情书?亲测表白率100%~
    模板化的封装,降低业务代码开发
    Java如何实现大文件分片上传,断点续传和秒传
    Spring基础1——概念、IOC和DI
    在Blazor中使用Chart.js快速创建图表
    Java面向对象编程
    【操作系统】存储器管理:对换
    好好学习第三天:RNN与股票预测
    【matlab网络通信】tcpserver参数详解
    初始化antdv项目,按需引入
  • 原文地址:https://blog.csdn.net/xinhang10/article/details/132655739