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

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

回显服务器(Echo Server

最简单的客户端服务器程序,不涉及到业务流程,只是对与 API 的用法做演示

客户端发送什么样的请求,服务器就返回什么样的响应,没有任何业务逻辑,没有进行任何计算或者处理

0. 构造方法

  • 网络编程必须要使用网卡,就需要用到Socket对象
  • 创建一个 DatagramSocket 对象,之后在基于这个对象进行操作
import java.net.DatagramSocket;  
import java.net.SocketException;  
  
public class UdpEchoServer {  
    private DatagramSocket socket = null;  
  
    public UdpEchoServer(int port) throws SocketException {  
    //SocketException 异常是 IOException 的子类
        socket = new DatagramSocket(port);  
    }
}
  • 对于服务器这一端来说,需要在 socket 对象创建的时候,就指定一个端口号 port,作为构造方法的参数
  • 后续服务器开始运行之后,操作系统就会把端口号和该进程关联起来
  • 端口号的作用就是来区分进程的,一台主机上可能有很多个进程很多个程序,都要去操作网络。当我们收到数据的时候,哪个进程来处理,就需要通过端口号去区分
  • 所以就需要在程序一启动的时候,就把这个程序关联哪个端口指明清楚

  • 在调用这个构造方法的过程中,JVM就会调用系统的Socket API,完成“端口号-进程”之间的关联动作
  • 这样的操作也叫“绑定端口号”(系统原生 API 名字就叫 bind
  • 绑定好了端口号之后,就明确了端口号和进程之间的关联关系

  • 对于一个系统来说,同一时刻,一个端口号只能被一个进程绑定;但是一个进程可以绑定多个端口号(通过创建多个Socket对象来完成)
  • 因为端口号是用来区分进程,收到数据之后,明确说这个数据要给谁,如果一个端口号对应到多个进程,那么就难以起到区分的效果
  • 如果有多个进程,尝试绑定一个端口号,只有一个能绑定成功,后来的都会绑定失败

  • 前面说到,这里的socket对象也占用一个文件描述符表里面的资源,但在这个程序中却不需要进行文件关闭的操作
  • 因为此处代码中,socket 的生命周期是跟随整个进程的,当进程结束了,socket 才需要关闭
  • 此时,就算代码中没有 close,进程关闭,也就会释放文件描述附表里的所有内容,也就相当于 close

1. 接收请求

  • 通过 start 来启动服务器的核心流程
  • 对于服务器来说,主要的工作,就是不停地处理客户端发来的请求,因为客户端什么时候会发来请求是未知的,所以要时刻待命
public void start() {  
    System.out.println("服务器启动!");  
    //通过一个死循环来不停地处理请求  
    while(true) {  
      //1. 读取客户端的请求并解析
      socket.receive();  
    }
}
  • 7*24 小时工作的服务器来说,服务器里面有死循环是很正常的,不是说死循环就是代码 bug
  1. 读取客户端的请求并解析
  • receive 是从网卡上读取数据,但是调用 receive 的时候,网卡上不一定就有数据
  • 当调用 start 方法之后程序启动,就立刻调用了 receive,一调用 receive,就会立刻从网卡中读取数据,但这个时候客户端可能还没来,网卡中还没有数据
  • 如果网卡上收到数据了,receive 立刻返回,获取收到的数据;如果没有收到数据,receive 就会阻塞等待,直到真正收到数据为止
  • 此处 receive 也是通过“输出型参数”获取到网卡上收到的数据的

  • receive的参数是DatagramPacket
  • 我们就需要构造一个空的 DatagramPacket 对象,将其作为参数传递给 receive
public void start() throws IOException {  
    System.out.println("服务器启动!");  
    //通过一个死循环来不停地处理请求  
    while(true) {  
        //1. 读取客户端的请求并解析  
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
        socket.receive(requestPacket);  
    }
}
  • DatagramPacket 自身需要存储数据,但是数据的空间具体多大,需要外部来定义,自身不负责
  • 需要指定requestPacket所需要存储数据/持有数据的基数
  • 指定一个字节数组,和其长度
  • 大小没什么讲究,只要能确保能够存储下你通讯的一个数据包即可

  • 收到的请求数据是通过二进制 byte[] 的形式来体现的,而我们后续要将其进行处理,最好将它转成字符串才好处理
public void start() throws IOException {  
    System.out.println("服务器启动!");  
    //通过一个死循环来不停地处理请求  
    while(true) {  
        //1. 读取客户端的请求并解析  
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
        socket.receive(requestPacket);  
        
      //将收到的二进制 byte[] 数据转换成字符串  
        String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
    }
}
  • 构造String可以基于字节数组构造,也可以基于字符数组进行构造
  • 此处 DatagramPacket 里面持有的就是字节数组,我们就取出里面包含的字节数
  • 此处就指定了:是哪个字节数组、从哪开始构造、构造多长

2. 根据请求计算响应

  • 请求(request):客户端主动给服务器发起的数据
  • 响应(response):服务器给客户端返回的数据

此处是一个回显服务器,响应就是请求

public void start() throws IOException {  
    System.out.println("服务器启动!");  
    //通过一个死循环来不停地处理请求  
    while(true) {  
        //1. 读取客户端的请求并解析  
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
        socket.receive(requestPacket);  
        //将收到的二进制 byte[] 数据转换成字符串  
        String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  
        //2. 根据请求计算响应  
        String response = process(request);  
    }
}  
  
//请求是什么,响应就是什么  
private String process(String request) {  
    return request;  
}

3. 将响应写回客户端

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

  • receive相似,send的参数是DatagramPacket
  • 我们就需要构造一个 DatagramPacket 对象,将其作为参数传递给 send
  • 但此时不能使用空的数组来构造 DatagramPacket 对象
  • 需要使用刚刚的 response 数据进行构造
public void start() throws IOException {  
    System.out.println("服务器启动!");  
    //通过一个死循环来不停地处理请求  
    while(true) {  
        //1. 读取客户端的请求并解析  
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);  
        socket.receive(requestPacket);  
        //将收到的二进制 byte[] 数据转换成字符串  
        String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  
        //2. 根据请求计算响应  
        String response = process(request);  
  
        //3. 把响应写回到客户端  
        DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,  
                requestPacket.getSocketAddress());  
        socket.send(responsePacket);  
    }
}  
  
//请求是什么,响应就是什么  
private String process(String request) {  
    return request;  
}
  • 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. 完整代码

import java.io.IOException;  
import java.net.DatagramPacket;  
import java.net.DatagramSocket;  
import java.net.SocketException;  
  
public class UdpEchoServer {  
    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);  
            //将收到的二进制 byte[] 数据转换成字符串  
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  
            //2. 根据请求计算响应  
            String response = process(request);  
  
            //3. 把响应写回到客户端  
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,  
                    requestPacket.getSocketAddress());  
            socket.send(responsePacket);  
  
            //4. 打印日志  
            System.out.printf("[%s:%d req=%s, res=%s\n",requestPacket.getAddress(),requestPacket.getPort(),request,response);  
        }    
    }  
  
  
    //请求是什么,响应就是什么  
    private String process(String request) {  
        return request;  
    }  
  
  
    public static void main(String[] args) throws IOException {  
        UdpEchoServer server = new UdpEchoServer(9090);  
        server.start();  
    }
  public static void main(String[] args) throws IOException {  
    UdpEchoServer server = new UdpEchoServer(9090);  
    server.start();  
  }
}
  • 将端口号设为“9090”

客户端(Echo Client)

0. 构造方法

import java.net.DatagramSocket;  
import java.net.SocketException;  
  
public class UdpEchoClient {  
    DatagramSocket socket = null;  
    private String serverIP;  
    private int serverPort;  
  
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {  
        socket = new DatagramSocket();  
        this.serverIP = serverIP;  
        this.serverPort = serverPort;  
    }
}
  • 服务器那边,创建socket的时候一定要指定端口号;
  • 服务器必须是指定了端口号,客户端主动发起的时候,才能找到服务器
  • 客户端这边,创建socket的时候最好不要指定端口号
  • 客户端是主动的一方,不需要服务器来找它,所以不需要指定端口号
  • 不代表没有端口号,客户端这边的端口号是系统自动分配了一个端口
  • 还有一个重要的原因,如果在客户端这里指定了端口之后,由于客户端是在用户的电脑上运行的,天知道用户的电脑上都有哪些程序,都已经占用了哪些端口了。万一你的代码指定的端口和用户电脑上运行的其他程序的端口冲突,就出bug
  • 让系统自动分配一个端口,就能确保是分配一个无人使用的空闲端口

  • 创建出对象之后,需要明确好服务器在哪,才能发起请求
  • 所以在构造方法中指定两个参数:String serverIP(服务器 IP)、String serverPort(服务器端口)
  • 并将这两个内容通过成员变量记录下来,之后就可以进一步通过这两个成员指定这个 UDP 数据报具体发给谁

客户端分配端口不可取的原因

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

1. 读取输入

  • 从控制台读取到用户的输入
public void start() {  
    System.out.println("启动客户端!");  
    Scanner scanner = new Scanner(System.in);  
    
    while (true) {  
        //1. 从控制台读取到用户的输入  
        System.out.println("-> ");  
        String request = scanner.next();   
    }  
}

2. 构造一个 UDP 请求

构造 UDP 请求,并发送给服务器

public void start() throws IOException {  
    System.out.println("启动客户端!");  
    Scanner scanner = new Scanner(System.in);  
    while (true) {  
        //1. 从控制台读取到用户的输入  
        System.out.println("-> ");  
        String request = scanner.next();  
  
        //2. 构造出一个 UDP 请求,发送给服务器  
        DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.length(),   
InetAddress.getByName(this.serverIP),this.serverPort);  
        socket.send(requestPacket);
    }
}
  • 构造requestPacket对象的时候,不是拿的空对象进行构造的,要拿request里面的String 数组数组长度IP端口号进行构造
  • 此处是给服务器发送数据,发送数据的时候,UDP 数据报里就需要带有目标的 IP 和端口号。接受数据的时候,构造的 UDP 数据报就是一个空的数据报
  • 因为计算机需要的 IP 不是字符串的,而我们通过 this.serverIP 提供的是一个字符串 IP,所以我们需要把这个 IP 转换成需要的类型再进行构造
    构造对象时的注意事项:
  1. DatagramPacket 里面构造的字节数组,不能是空的数组,因为我们是要给服务器发东西,里面得有内容(从控制台读取的用户的输入),所以把刚才从控制台读取的 request 里面的字节数组取出来,然后构造到 DatagramPacket 里面
  2. 还需要指定此数据报要发给哪个服务器,需要将这个服务器的IP和端口号传进去
  • 这里传入 IP 的时候,需要将 IP 类型转换成计算机需要的格式、

3. 从服务器读取响应

public void start() throws IOException {  
    System.out.println("启动客户端!");  
    Scanner scanner = new Scanner(System.in);  
    while (true) {  
        //1. 从控制台读取到用户的输入  
        System.out.println("-> ");  
        String request = scanner.next();  
  
        //2. 构造出一个 UDP 请求,发送给服务器  
        DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.length(),  
                InetAddress.getByName(this.serverIP),this.serverPort);  
        socket.send(requestPacket);  
  
        //3. 从服务器读取到响应  
        DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);  
        socket.receive(requestPacket);   
    }  
}
  • 由于客户端给服务器发送请求之后,响应也不是立刻就会过来的,如果此时立刻去调用客户端, receive 也是可能会发生阻塞的

4. 完整代码

import java.io.IOException;  
import java.net.*;  
import java.util.Scanner;  
  
public class UdpEchoClient {  
    DatagramSocket socket = null;  
    private String serverIP;  
    private int serverPort;  
  
    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.println("-> ");  
            String request = scanner.next();  
  
            //2. 构造出一个 UDP 请求,发送给服务器  
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.length(),  
                    InetAddress.getByName(this.serverIP),this.serverPort);  
            socket.send(requestPacket);  
  
            //3. 从服务器读取到响应  
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);  
            socket.receive(requestPacket);  
            
            //4. 把响应打印到控制台上  
            String response = new String (responsePacket.getData(),0,responsePacket.getLength());  
            System.out.println(response);  
        }    
    }
    
    public static void main(String[] args) throws IOException {  
      UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);  
      client.start();  
    }
}
  • 此处传入的 IP 是一个特殊的 IP——环回 IP,这个 IP 就代表本机,如果客户端和服务器在同一个主机上,就使用这个 IP
  • 将端口号设为“9090”,和上面的服务器一样,将服务器和客户端连接起来

服务器与客户端连接

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

  1. 客户端读取到“hello”,构造出一个 requestPacket 数据报,发送给服务器
  2. 服务器收到之后,就会从 receive 返回结果,再来转成 String 类型的 request
  3. 服务器继续执行 process
  4. 服务器再构造出一个响应数据报 responsePacket
  5. 服务器最后进行返回,并打印日志
  6. 客户端这边就会从 receive 这里读到响应结果 responsePacket
  7. 最后客户端这边进行打印
//客户端
启动客户端!
-> hello
hello
//服务器
[/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


相关文章
|
24天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
16天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
20天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2577 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
18天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
3天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
2天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
163 2
|
20天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1576 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
22天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
977 14
|
4天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
221 2
|
17天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
734 9