【网络】UDP回显服务器和客户端的构造,以及连接流程 ...

张春  金牌会员 | 2024-8-18 12:28:13 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 570|帖子 570|积分 1710

回显服务器(Echo Server)

最简朴的客户端服务器程序,不涉及到业务流程,只是对与 API 的用法做演示
客户端发送什么样的哀求,服务器就返回什么样的相应,没有任何业务逻辑,没有进行任何盘算或者处理
0. 构造方法



  • 网络编程必须要使用网卡,就必要用到 Socket 对象

    • 创建一个 DatagramSocket 对象,之后在基于这个对象进行操作

  1. import java.net.DatagramSocket;  
  2. import java.net.SocketException;  
  3.   
  4. public class UdpEchoServer {  
  5.     private DatagramSocket socket = null;  
  6.   
  7.     public UdpEchoServer(int port) throws SocketException {  
  8.     //SocketException 异常是 IOException 的子类
  9.         socket = new DatagramSocket(port);  
  10.     }
  11. }
复制代码


  • 对于服务器这一端来说,必要在 socket 对象创建的时候,就指定一个端标语 port,作为构造方法的参数
  • 后续服务器开始运行之后,操作系统就会把端标语和该进程关联起来
  • 端标语的作用就是来区分进程的,一台主机上大概有很多个进程很多个程序,都要去操作网络。当我们收到数据的时候,哪个进程来处理,就必要通过端标语去区分

    • 所以就必要在程序一启动的时候,就把这个程序关联哪个端口指明清楚




  • 在调用这个构造方法的过程中,JVM 就会调用系统的 Socket API,完成“端标语-进程”之间的关联动作

    • 这样的操作也叫“绑定端标语”(系统原生 API 名字就叫 bind)
    • 绑定好了端标语之后,就明白了端标语和进程之间的关联关系




  • 对于一个系统来说,同一时候,一个端标语只能被一个进程绑定;但是一个进程可以绑定多个端标语(通过创建多个 Socket 对象来完成)

    • 由于端标语是用来区分进程,收到数据之后,明白说这个数据要给谁,如果一个端标语对应到多个进程,那么就难以起到区分的效果
    • 如果有多个进程,尝试绑定一个端标语,只有一个能绑定成功,后来的都会绑定失败




  • 前面说到,这里的 socket 对象也占用一个文件形貌符表里面的资源,但在这个程序中却不必要进行文件关闭的操作

    • 由于此处代码中,socket 的生命周期是跟随整个进程的,当进程结束了,socket 才必要关闭
    • 此时,就算代码中没有 close,进程关闭,也就会开释文件形貌附表里的所有内容,也就相当于 close 了

1. 接收哀求



  • 通过 start 来启动服务器的核心流程
  • 对于服务器来说,主要的工作,就是不停地处理客户端发来的哀求,由于客户端什么时候会发来哀求是未知的,所以要时候待命
  1. public void start() {  
  2.     System.out.println("服务器启动!");  
  3.     //通过一个死循环来不停地处理请求  
  4.     while(true) {  
  5.             //1. 读取客户端的请求并解析
  6.             socket.receive();  
  7.     }
  8. }
复制代码


  • 对 7*24 小时工作的服务器来说,服务器里面有死循环是很正常的,不是说死循环就是代码 bug

  • 读取客户端的哀求并分析

    • receive 是从网卡上读取数据,但是调用 receive 的时候,网卡上不一定就有数据
    • 当调用 start 方法之后程序启动,就立刻调用了 receive,一调用 receive,就会立刻从网卡中读取数据,但这个时候客户端大概还没来,网卡中还没有数据
    • 如果网卡上收到数据了,receive 立刻返回,获取收到的数据;如果没有收到数据,receive 就会阻塞等待,直到真正收到数据为止
    • 此处 receive 也是通过“输出型参数”获取到网卡上收到的数据的




  • receive 的参数是 DatagramPacket

    • 我们就必要构造一个空的 DatagramPacket 对象,将其作为参数通报给 receive

  1. public void start() throws IOException {  
  2.     System.out.println("服务器启动!");  
  3.     //通过一个死循环来不停地处理请求  
  4.     while(true) {  
  5.         //1. 读取客户端的请求并解析  
  6.         DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
  7.         socket.receive(requestPacket);  
  8.     }
  9. }
复制代码


  • DatagramPacket 自身必要存储数据,但是数据的空间具体多大,必要外部来界说,自身不负责
  • 必要指定 requestPacket 所必要存储数据/持有数据的基数

    • 指定一个字节数组,和其长度
    • 巨细没什么讲求,只要能确保能够存储下你通讯的一个数据包即可




  • 收到的哀求数据是通过二进制 byte[] 的情势来体现的,而我们后续要将其进行处理,最好将它转成字符串才好处理
  1. public void start() throws IOException {  
  2.     System.out.println("服务器启动!");  
  3.     //通过一个死循环来不停地处理请求  
  4.     while(true) {  
  5.         //1. 读取客户端的请求并解析  
  6.         DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
  7.         socket.receive(requestPacket);  
  8.         
  9.             //将收到的二进制 byte[] 数据转换成字符串  
  10.         String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  11.     }
  12. }
复制代码


  • 构造 String 可以基于字节数组构造,也可以基于字符数组进行构造

    • 此处 DatagramPacket 里面持有的就是字节数组,我们就取出里面包含的字节数
    • 此处就指定了:是哪个字节数组、从哪开始构造、构造多长

2. 根据哀求盘算相应



  • 哀求(request):客户端主动给服务器发起的数据
  • 相应(response):服务器给客户端返回的数据
此处是一个回显服务器,相应就是哀求
  1. public void start() throws IOException {  
  2.     System.out.println("服务器启动!");  
  3.     //通过一个死循环来不停地处理请求  
  4.     while(true) {  
  5.         //1. 读取客户端的请求并解析  
  6.         DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
  7.         socket.receive(requestPacket);  
  8.         //将收到的二进制 byte[] 数据转换成字符串  
  9.         String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  10.   
  11.         //2. 根据请求计算响应  
  12.         String response = process(request);  
  13.     }
  14. }  
  15.   
  16. //请求是什么,响应就是什么  
  17. private String process(String request) {  
  18.     return request;  
  19. }
复制代码
3. 将相应写回客户端

此时必要主动的将数据通过网卡发送回客户端


  • 与 receive 相似, send 的参数是 DatagramPacket

    • 我们就必要构造一个 DatagramPacket 对象,将其作为参数通报给 send
    • 但此时不能使用空的数组来构造 DatagramPacket 对象
    • 必要使用刚刚的 response 数据进行构造

  1. public void start() throws IOException {  
  2.     System.out.println("服务器启动!");  
  3.     //通过一个死循环来不停地处理请求  
  4.     while(true) {  
  5.         //1. 读取客户端的请求并解析  
  6.         DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
  7.         socket.receive(requestPacket);  
  8.         //将收到的二进制 byte[] 数据转换成字符串  
  9.         String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  10.   
  11.         //2. 根据请求计算响应  
  12.         String response = process(request);  
  13.   
  14.         //3. 把响应写回到客户端  
  15.         DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,  
  16.                 requestPacket.getSocketAddress());  
  17.         socket.send(responsePacket);  
  18.     }
  19. }  
  20.   
  21. //请求是什么,响应就是什么  
  22. private String process(String request) {  
  23.     return request;  
  24. }
复制代码


  • String 可以基于字节数组来构造,也可以随时取出里面的字节数组
  • response.getBytes().length 不能写成 response.length

    • 前者是在获取字节数组,得到字节数组的长度,单位是“字节
    • 后者是在获取字符串中字符的个数,单位是“字符

  • UDP 有一个特点——无连接

    • 所谓的连接,就是通讯两边保存对方的信息(IP+端标语)
    • 就是说 DatagramSocket 这个对象中,不持有对方(客户端)和 IP 端口的,进行 send 的时候,就必要在 send 的数据包里,把要“发给谁”这样的信息,写进去,才气够正确的把数据进行返回
    • 所以要将信息也作为参数,传入 responsePacket 中

      • 客户端刚才给服务器发了一个哀求 requestPacket,这个包记录了这个数据是从哪来,从哪来就让它回哪去,所以直接获取这个 requestPacket 的信息就可以了
      • 客户端的 IP 和端口就都包含在 requestPacket.getSocketAddress() 中
      • 后续往外发送数据包的时候,就知道该发去哪了



  • 相比之下,TCP 代码中,由于 TCP 是有连接的,则无需关心对端的 IP 和端口,只管发送数据即可
   

  • 如果字符串里都是英笔墨母/阿拉伯数字/英文标点符号的话,都是 ASCII 编码的,一个字符也就是一个字节这么长
  • 如果字符串里有中文,是 UTF8 编码的,一个中文就是 3 个字节
  • UTF8 也是能兼容 ASCII,当使用 UTF8 表现英文的时候,和 ASCII 表现英文是完全相同的
  4. 完备代码

  1. import java.io.IOException;  
  2. import java.net.DatagramPacket;  
  3. import java.net.DatagramSocket;  
  4. import java.net.SocketException;  
  5.   
  6. public class UdpEchoServer {  
  7.     private DatagramSocket socket = null;  
  8.   
  9.     public UdpEchoServer(int port) throws SocketException {  
  10.         socket = new DatagramSocket(port);  
  11.     }  
  12.    
  13.     public void start() throws IOException {  
  14.         System.out.println("服务器启动!");  
  15.         //通过一个死循环来不停地处理请求  
  16.         while(true) {  
  17.             //1. 读取客户端的请求并解析  
  18.             DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
  19.             socket.receive(requestPacket);  
  20.             //将收到的二进制 byte[] 数据转换成字符串  
  21.             String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  22.   
  23.             //2. 根据请求计算响应  
  24.             String response = process(request);  
  25.   
  26.             //3. 把响应写回到客户端  
  27.             DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,  
  28.                     requestPacket.getSocketAddress());  
  29.             socket.send(responsePacket);  
  30.   
  31.             //4. 打印日志  
  32.             System.out.printf("[%s:%d req=%s, res=%s\n",requestPacket.getAddress(),requestPacket.getPort(),request,response);  
  33.         }   
  34.     }  
  35.        
  36.        
  37.     //请求是什么,响应就是什么  
  38.     private String process(String request) {  
  39.         return request;  
  40.     }  
  41.        
  42.        
  43.     public static void main(String[] args) throws IOException {  
  44.         UdpEchoServer server = new UdpEchoServer(9090);  
  45.         server.start();  
  46.     }
  47.         public static void main(String[] args) throws IOException {  
  48.     UdpEchoServer server = new UdpEchoServer(9090);  
  49.     server.start();  
  50.         }
  51. }
复制代码


  • 将端标语设为“9090”

客户端(Echo Client)

0. 构造方法

  1. import java.net.DatagramSocket;  
  2. import java.net.SocketException;  
  3.   
  4. public class UdpEchoClient {  
  5.     DatagramSocket socket = null;  
  6.     private String serverIP;  
  7.     private int serverPort;  
  8.   
  9.     public UdpEchoClient(String serverIP, int serverPort) throws SocketException {  
  10.         socket = new DatagramSocket();  
  11.         this.serverIP = serverIP;  
  12.         this.serverPort = serverPort;  
  13.     }
  14. }
复制代码


  • 服务器那边,创建 socket 的时候一定要指定端标语;

    • 服务器必须是指定了端标语,客户端主动发起的时候,才气找到服务器

  • 客户端这边,创建 socket 的时候最好不要指定端标语

    • 客户端是主动的一方,不必要服务器来找它,所以不必要指定端标语
    • 不代表没有端标语,客户端这边的端标语是系统自动分配了一个端口

  • 尚有一个重要的缘故原由,如果在客户端这里指定了端口之后,由于客户端是在用户的电脑上运行的,天知道用户的电脑上都有哪些程序,都已经占用了哪些端口了。万一你的代码指定的端口和用户电脑上运行的其他程序的端口冲突,就出 bug 了

    • 让系统自动分配一个端口,就能确保是分配一个无人使用的空闲端口




  • 创建出对象之后,必要明白好服务器在哪,才气发起哀求

    • 所以在构造方法中指定两个参数:String serverIP(服务器 IP)、String serverPort(服务器端口)
    • 并将这两个内容通过成员变量记录下来,之后就可以进一步通过这两个成员指定这个 UDP 数据报具体发给谁

   客户端分配端口不可取的缘故原由
  

  • 比如你去下馆子,进到店里面之后,老板让你找个地方坐
  • 你找个地方坐,必然是找个“空闲的地方”
  • 并且你这次坐的地方大概率和以前来坐的地方是差别的(大概前次坐的地方有人了)
    你给服务器分配了端口之后,就相当于说是:你每次去吃饭,都被固定坐谁人位置,不管有人没人
  1. 读取输入



  • 从控制台读取到用户的输入
  1. public void start() {  
  2.     System.out.println("启动客户端!");  
  3.     Scanner scanner = new Scanner(System.in);  
  4.    
  5.     while (true) {  
  6.         //1. 从控制台读取到用户的输入  
  7.         System.out.println("-> ");  
  8.         String request = scanner.next();   
  9.     }  
  10. }
复制代码
2. 构造一个 UDP 哀求

构造 UDP 哀求,并发送给服务器
  1. public void start() throws IOException {  
  2.     System.out.println("启动客户端!");  
  3.     Scanner scanner = new Scanner(System.in);  
  4.     while (true) {  
  5.         //1. 从控制台读取到用户的输入  
  6.         System.out.println("-> ");  
  7.         String request = scanner.next();  
  8.   
  9.         //2. 构造出一个 UDP 请求,发送给服务器  
  10.         DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.length(),   
  11. InetAddress.getByName(this.serverIP),this.serverPort);  
  12.         socket.send(requestPacket);
  13.     }
  14. }
复制代码


  • 构造 requestPacket 对象的时候,不是拿的空对象进行构造的,要拿 request 里面的 String 数组数组长度IP端标语进行构造

    • 此处是给服务器发送数据,发送数据的时候,UDP 数据报里就必要带有目的的 IP 和端标语。接受数据的时候,构造的 UDP 数据报就是一个空的数据报

  • 由于盘算机必要的 IP 不是字符串的,而我们通过 this.serverIP 提供的是一个字符串 IP,所以我们必要把这个 IP 转换成必要的类型再进行构造
    构造对象时的注意事项:

  • DatagramPacket 里面构造的字节数组,不能是空的数组,由于我们是要给服务器发东西,里面得有内容(从控制台读取的用户的输入),所以把刚才从控制台读取的 request 里面的字节数组取出来,然后构造到 DatagramPacket 里面
  • 还必要指定此数据报要发给哪个服务器,必要将这个服务器的 IP 和端标语传进去

    • 这里传入 IP 的时候,必要将 IP 类型转换成盘算机必要的格式、

3. 从服务器读取相应

  1. public void start() throws IOException {  
  2.     System.out.println("启动客户端!");  
  3.     Scanner scanner = new Scanner(System.in);  
  4.     while (true) {  
  5.         //1. 从控制台读取到用户的输入  
  6.         System.out.println("-> ");  
  7.         String request = scanner.next();  
  8.   
  9.         //2. 构造出一个 UDP 请求,发送给服务器  
  10.         DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.length(),  
  11.                 InetAddress.getByName(this.serverIP),this.serverPort);  
  12.         socket.send(requestPacket);  
  13.   
  14.         //3. 从服务器读取到响应  
  15.         DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);  
  16.         socket.receive(requestPacket);   
  17.     }  
  18. }
复制代码


  • 由于客户端给服务器发送哀求之后,相应也不是立刻就会过来的,如果此时立刻去调用客户端, receive 也是大概会发生阻塞的
4. 完备代码

  1. import java.io.IOException;  
  2. import java.net.*;  
  3. import java.util.Scanner;  
  4.   
  5. public class UdpEchoClient {  
  6.     DatagramSocket socket = null;  
  7.     private String serverIP;  
  8.     private int serverPort;  
  9.   
  10.     public UdpEchoClient(String serverIP, int serverPort) throws SocketException {  
  11.         socket = new DatagramSocket();  
  12.         this.serverIP = serverIP;  
  13.         this.serverPort = serverPort;  
  14.     }  
  15.     public void start() throws IOException {  
  16.         System.out.println("启动客户端!");  
  17.         Scanner scanner = new Scanner(System.in);  
  18.         while (true) {  
  19.             //1. 从控制台读取到用户的输入  
  20.             System.out.println("-> ");  
  21.             String request = scanner.next();  
  22.   
  23.             //2. 构造出一个 UDP 请求,发送给服务器  
  24.             DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.length(),  
  25.                     InetAddress.getByName(this.serverIP),this.serverPort);  
  26.             socket.send(requestPacket);  
  27.   
  28.             //3. 从服务器读取到响应  
  29.             DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);  
  30.             socket.receive(requestPacket);  
  31.             
  32.             //4. 把响应打印到控制台上  
  33.             String response = new String (responsePacket.getData(),0,responsePacket.getLength());  
  34.             System.out.println(response);  
  35.         }   
  36.     }
  37.    
  38.     public static void main(String[] args) throws IOException {  
  39.             UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);  
  40.             client.start();  
  41.     }
  42. }
复制代码


  • 此处传入的 IP 是一个特别的 IP——环回 IP,这个 IP 就代表本机,如果客户端和服务器在同一个主机上,就使用这个 IP
  • 将端标语设为“9090”,和上面的服务器一样,将服务器和客户端连接起来
服务器与客户端连接

将服务器和客户端运行起来之后,在客户端输入“hello”的哀求之后:

  • 客户端读取到“hello”,构造出一个 requestPacket 数据报,发送给服务器
  • 服务器收到之后,就会从 receive 返回结果,再来转成 String 类型的 request
  • 服务器继续执行 process
  • 服务器再构造出一个相应数据报 responsePacket
  • 服务器末了进行返回,并打印日志
  • 客户端这边就会从 receive 这里读到相应结果 responsePacket
  • 末了客户端这边进行打印
  1. //客户端
  2. 启动客户端!
  3. -> hello
  4. hello
  5. //服务器
  6. [/127.0.0.1:65075 req=hello, res=hello
复制代码


  • 客户端:输入 hello 之后,打印出 hello
  • 服务器:输出 [/127.0.0. 1:65075 req=hello, res=hello

    • 此处的信息就是客户端给服务器发起哀求,服务器处理的过程,关键日志
    • 127.0.0.1 是客户端 IP
    • 65075 是客户端的端标语,客户端没有指定端标语,这是系统自动分配的空闲的端标语
    • 哀求和相应都是 hello,由于是回显服务器,所以哀求和相应是一样的

完备流程


   此处的通讯,是本机上的客户端和服务器通讯,如果使用两个主机,能够跨主机通讯吗?如果我把客户端代码发给你,你能通过你的客户端访问到我的这个服务器吗?
  

  • 能,也不能
  • 如果我就把服务器代码运行在我自己的电脑上,此时你是无法访问到我这个服务器的,除非你抱着你的电脑来我这,和我连上一样的 WiFi 才气访问(IPv 4 的锅
  • 如果把我写的服务器代码写到“云服务器”上,此时就是可以的。

    • 云服务器拥有公网 IP,而我自己的电脑没有公网 IP


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张春

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表