Java基于UDP实现 客户端/服务器

UDP协议

  • 无连接

  • 不可靠传输(传出去了不管有没有接受到,容易数据丢失)

  • 面向数据报

  • 全双工

UDP协议端格式

UDP协议报文结构,这个图在很多计算机网络教科书上都有,而且都是这么在花,但是实际这么画不够严谨

个人理解图应该如下:

  1. 源端口:源端口号。在需要对方回信时选用。不需要使用时可用 0 填充。

  1. 目的端口:目的端口号。这在终点交付报文时必须使用。

  1. 长度:UDP 用户数据报的长度,其最小值是 8(即仅有首部部分),单位:字节。

  1. 校验和:检测 UDP 用户数据报在传输过程中是否出错。有错就丢弃。

UDP报头里包含了一些特殊的属性,携带了一些重要的信息
不同的协议功能不同,报头中带有的属性信息就不同
对于UDP来说,报头一共就是八个字节,分成四个部分(每个部分都是均衡的两字节)

UDP 报头:8个字节,4个字段.

  • 2字节 源 端口 + 发送方端口

  • 2字节 目的端口 + 接受方端口

  • 2字节 UDP报文长度 (范围:0 -> 65535) (一个UDP数据报,最长就是65535字节)

  • 2字节 校验和 (发送方把要发送的数据计算出校验和(checksum1),接收方收到数据把数据按照同样的方式再计算一次校验和(checksum2),同时接受方也收到了checksum1,接受方比对checksum1和checksum2是否相同)

类比货拉拉搬家环节

买家叫车时,根据需要决定定什么样的车

车厢用来装货的部分就叫做 "载荷"

车头用来标识这辆车的身份,是大型车还是中型车,货车还是客车

UDP会把载荷数据通过UDP Socket,通过send方法拿来的数据的基础上在前面拼接几个字节的报头(相当于字符串拼接,此处是二进制的,不是文本的)

一次网络通信涉及到

五元组(源IP,源端口,目的IP,目的端口,协议类型)(两字节)

UDP报文长度(数据范围0->65535,64KB)(两字节)

Tips:

一个UDP数据报最大只能传输64KB的数据这是大呢还是小呢?如果应用层数据超过了64KB怎么办?
1. 就需要在应用层,通过代码的方式针对应用层数据报进行手动的分包, 拆成多个包,通过多个UDP数据报进行传输(本来send一次,现在send多次),就跟搬家的时候东西太多,一车装不下,就分多车装
2. 不用UDP,(因为使用如果使用上一条UDP传输拆分方案就得写很多代码很得进行多次测试处理bug,工作量大大增加,程序员的幸福感就降低了)直接换成TCP就没有这种限制.

校验和:验证传数的数据是否正确,网络传输的本质是光信号和电信号的转换,在这过程中,可能会受到一些物理环境干扰如电场,磁场,高能射线等,导致出现"比特翻转"(1->0,0->1)状况,数据一旦发生了改变,对数据的含义来说是致命的,例如在很多程序中经常使用1表示某功能的开启,0表示关闭.本来网络数据报是想开启这个功能,结果因为比特翻转导致变成了关闭...这后果可想而知是非常严重的!但这样的现象又是客观存在的,不可避免,程序员能做的就是能及时识别出错误信息!

因此就映入了校验和来进行鉴别

大致的鉴别方式如下:针对数据内容进行一系列数学运算,得到一个比较短的结果(比如两个字节的数据),如果数据内容一定,得到的校验和结果就一定,如果数据有变化,得到的校验和也就跟着与原来不同,所以只要前后内容是相同的,按照同样的算法得到的校验和也应该是相同的

但是实际的网路传输过程是把所有的字节都参与生成校验和运算,这样任何一个字节出了问题,都能及时的被发现!

Tips:

如果内容相同,得到的校验和一定相同(肯定的),但有时候不同内容按照相同的计算法则计算出的校验和可能相同(存在极小概率).但是在工程实践中,这种情况就忽略不计了,一般认为,校验和相同原始内容也相同
例子:就像我妈叫我去买"葱,姜,蒜",并最后嘱咐一共要三样!我由于没见过蒜长啥样,买回去了"葱,姜,辣椒"也是三样.这例子中的"三样"就相当于我与我妈之间数据传输的"校验和"但是在传输过程中出现了干扰使得我最终得到的信息内容不一样,就产生了错误!

针对网络传输的数据来说,生成校验和的算法有很多种,其中比较知名的有

  1. CRC 循环冗余校验

简单粗暴,把数据的每个字节都循环的往上"累加",如果累加溢出了,截出高位不要.

此方法好计算,但是有相应的弊端,如果数据同时变动两个bit位(前一个字节少1,后一个字节多1)就会出现内容变了,但是CRC结果没有变这种情况

  1. MD5

此方法不是简单的累加,有一系列公式来进行更复杂的数学运算(数学逻辑不过多介绍)

MD5算法的特点:

  • 定长:无论原始数据多长,得到的MD5值都是固定的长度(4/8字节)

  • 冲突概率很小:原始数据哪怕只变动一个地方,最后算出来的MD5的值也会差别很大,让结果更加分散

  • 不可逆:通过原始数据计算MD5值很容易,但是通过MD5还原成原始数据(找到那个数据生成了次MD5值)就很难,理论上是不可实现的计算量极大,但也有很多解码网站他们的原理大多以"打表"的方式,把一些常用的数据计算出MD5值存储下来,用户输入MD5再在他的数据里面去一一核对(我们常见的WiFi万能🔑好像也是这原理).

MD5有以上的特点使得他的作用不仅仅适用于校验和的计算,还可以运用到Hash值的计算,和一些文件加密等提高安全操作

  1. SHA1

SHA1算法是Hash算法的一种。SHA1算法的最大输入长度小于2^64比特的消息,输入消息(明文)以512比特的分组为单位处理,输出160比特的消息摘要(密文

编程 基于UDP 版本的回显服务器[Java]

基于UDP来编写一个简单的客户端服务器的网络通信程序

DatagramSocket API

使用 DatagramSocket这个类表示一个Socket对象,在操作系统中(一切皆文件)把这个Socket对象当做一个文本来处理,相当于是文件描述符表上的一项.

普通文件,对应的是硬件设备的 "网盘"

Socket文件,对应的硬件设备的 "网卡"

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

DatagramSocket构造方法

  • DatagramSocket() : 创建一个UDP数据报套接字的Socket,没有指定端口,系统会自动绑定到本机任意一个随机端口(一般用于客户端

  • DatagramSocket(int port): 创建一个UDP数据报套接字的Socket,要传入一个端口号让当前的socket对象绑定到本机指定的端口(一般用于服务端

对于服务器来说端口一定要显示指定,如果不指定让系统并自动分配,不便于客户端找到这个服务器程序,或者以后可能造成端口冲突

连接本质是进程中的Socket对象与端口建立了联系

DatagramSocket方法

  • void receive(DatagramPacket p) : 从此套接字接收数据报,此处传入的相当于是一个空的对象,receive方法内部会对参数的这个空对象进行内容填充,构造出结果数据(参数是一个"输出型参数")(如果没有接收到数据报,该方法会阻塞等待)

  • void send(DatagramPacketp) : 从此套接字发送数据报包(不会阻塞等待,直接发送)

  • void close() : 关闭此数据报套接字,释放资源

拥有一个Socket对象就可以和另外台主机进行通信,但是如果要和多个不同主机通信,则需要创建多个Socket对象

DatagramPacket API

DatagramPacket 表示UDP中传输的一个报文,是UDP Socket发送和接收的数据报构造这个对象可以指定一些具体的数据进去。

DatagramPacket 构造方法

  • DatagramPacket(byte[] buf, int length) : 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中接收指定长度(第二个参数length),把buf这个缓冲区设置进去

  • DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) : 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)address指定目的主机的IP和端口号.构造缓冲区buf + 地址offset + 使用address这个类表示IP和port

DatagramPacket 方法

InetAddress

  • getAddress() : 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址

  • int getPort() : 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号

  • byte[] getData() : 获取数据报中的数据

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

InetSocketAddress API

InetSocketAddress ( SocketAddress 的子类 )构造方法:

InetSocketAddress(InetAddress addr, int port) : 创建一个Socket地址,包含IP地址和端口号

服务器 DupEchoServer 代码

一发一收(无响应)

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

创建DatagramSocket对象

网络编程本质上是操作网卡 网卡不方便直接操作,但在操作系统内核中使用了一种叫"Socket"的文件来抽象表示网卡
因此进行网络通信就得先有一个"Socket"对象
DatagramPacket为UDP传输数据的基本单位
import java.net.DatagramSocket;

//UDP回显服务器
public class DupEchoServer {
    private DatagramSocket socket = null;
}
服务器就像学校食堂,"食堂地址"就相当于"IP地址",食堂里的"用餐窗口"就像"端口号"
现在为了方便别人找到食堂的某个"用餐窗口",就得把学校食堂"地址"和"窗口号"发布出去.网络通信也是同理,要把"IP"和"端口号"明确了才能找到对应的位置才能建立通信.所以为了保证通信正常,作为一个服务器,端口号就必须是唯一的确定的,不能是随机的(如果不是固定端口,每次服务器启动,都是不同的端口)

服务器构造类方法

对于服务器来说,创建Socket对象的同时,必须要绑定一个指定具体的端口(port)
服务器是网络传输中被动的一方,如果操作系统随机分配端口,此时客户端就不知道这个端口到底指到哪的,就没法进行通信
import java.net.DatagramSocket;
import java.net.SocketException;

//UDP回显服务器
public class UdpEchoServer {
    //网络编程本质上是操作网卡
    //网卡不方便直接操作,但在操作系统内核中使用了一种叫"Socket"的文件来抽象表示网卡
    //因此进行网络通信就得先有一个"Socket"对象
    private DatagramSocket socket = null;

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

启动服务器start函数

服务器需要对很多客户端提供服务,所以得用一个循环一直站岗接受数据

public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器需要对很多客户端提供服务,所以得用一个循环一直站岗接受数据
        while(true) {
                    //只要客户端过来就可以提供服务,

    }
1.读取客户端发来的请求
receive 方法是一个 输出 型参数需要 先构造好空白的 DatagramPacket 对象做参数,交给receive来进行填充
datagramPacket 就相当于点餐纸条,把纸条写好了就交给receive处理
receive内部会针对参数(datagramPacket)对象填充数据,填充数据来自网卡
此时这个DatagramPacket(datagramPacket)是一个特殊的对象,并不方便直接进行处理,可以 把这里包含的数据拿出来构造成一个字符串
public void start() throws IOException {
    System.out.println("服务器启动!");
    //服务器需要对很多客户端提供服务,所以得用一个循环一直站岗接受数据
    while(true) {
        //只要客户端过来就可以提供服务,

        // 1.读取客户端发来的请求
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); 
        socket.receive(requestPacket);
        String request = new String(requestPacket.getData(),0,requestPacket.getLength()); //获取数据构成字符串
    }
数据会传到达网卡,经过内核层层分用,最终到达UDP传输协议,调用receive,相当于是执行到内核中相关的DUP代码,就会把这个UDP数据报里的 载荷 部分取出来,拷贝到用户提供的 byte[] 数组中
while循环每循环一次就处理一次响应,客户端什么时候发请求不知道,所以服务器就得一直快速循环一直处于工作状态,如果客户端发来了请求, receive 就能顺利读出来,如果客户端没有发送请求,此时 receive 就会 阻塞!阻塞类似Scanner的操作(读取控制台数据)
对于服务器来说这个while循环必须一致循环下去,遇到特殊情况才停止,因为服务器得随时等待客户端发送请求,要7*24小时运行
2.根据请求计算响应

由于此处是回显服务器所以请求跟响应相同

public void start() throws IOException {
    System.out.println("服务器启动!");
    //服务器需要对很多客户端提供服务,所以得用一个循环一直站岗接受数据
    while(true) {
        //只要客户端过来就可以提供服务,

        // 1.读取客户端发来的请求
        //receive 方法是一个 输出 型参数需要 先构造好空白的 DatagramPacket 对象做参数,交给receive来进行填充
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); //datagramPacket就相当于点餐纸条,把纸条写好了就交给receive处理
         socket.receive(requestPacket); //receive内部会针对参数(datagramPacket)对象填充数据,填充数据来自网卡
        //此时这个DatagramPacket(datagramPacket)是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来构造成一个字符串
        String request = new String(requestPacket.getData(),0,requestPacket.getLength()); //获取数据构成字符串

        // 2.根据请求计算响应,由于此处是回显服务器所以请求跟响应相同
        String response = process(request);
		}
		
//响应函数
private String process(String request) {
    return request;
}
3.把回应写回到客户端,
发送函数 send的参数也是DatagramPacket,需要把这个Packet对象提前构造好
此处构造的响应对象 不能用空的字节数组构造了,而是使用 响应数据构造 send 函数是指定要把数据发给哪个(指定的)客户端
DatagramPacket的参数 :
response.getBytes() : 传响应对象的这个字节数组
response.getBytes().length : 指定长度.此处这个长度使用response.length()计算字符的个数也行,response.getBytes().length是计算字节的个数,操作计量单位不同,而"DatagramPacket只认字符不认字节",所以还是用response.getBytes().length
requestPacket.getSocketAddress() : 获取到客户端的IP和端口号(这两个信息存在requestPacket中所以点得出来),相当于取餐小票纸条(上面有客户端的信息 IP和端口号)

Tips:

任何一次通信都的是源端口,目的端口两个端口,源端口和目的端口是相对而言的
对于 服务器发给客服端这个操作来说,
服务器的端口是源端口,客户端的端口是目的端口
对于 客户端发给服务器这个操作来说
服务器的端口是目的端口,客户端的端口是源端口
public void start() throws IOException {
    System.out.println("服务器启动!");
    //服务器需要对很多客户端提供服务,所以得用一个循环一直站岗接受数据
    while(true) {
        //只要客户端过来就可以提供服务,

        // 1.读取客户端发来的请求
        //receive 方法是一个 输出 型参数需要 先构造好空白的 DatagramPacket 对象做参数,交给receive来进行填充
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); //datagramPacket就相当于点餐纸条,把纸条写好了就交给receive处理
         socket.receive(requestPacket); //receive内部会针对参数(datagramPacket)对象填充数据,填充数据来自网卡
        //此时这个DatagramPacket(datagramPacket)是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来构造成一个字符串
        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;
    }
}
4. 打印当前这次请求响应的处理中间结果
requestPacket.getAddress() : 获取到Packet里的IP
requestPacket.getPort() : 获取Packet里面的端口
public void start() throws IOException {
    System.out.println("服务器启动!");
    //服务器需要对很多客户端提供服务,所以得用一个循环一直站岗接受数据
    while(true) {
        //只要客户端过来就可以提供服务,

        // 1.读取客户端发来的请求
        //receive 方法是一个 输出 型参数需要 先构造好空白的 DatagramPacket 对象做参数,交给receive来进行填充
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); //datagramPacket就相当于点餐纸条,把纸条写好了就交给receive处理
        socket.receive(requestPacket); //receive内部会针对参数(datagramPacket)对象填充数据,填充数据来自网卡
        //此时这个DatagramPacket(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()
                                                                );
    //DatagramPacket 参数
//        response.getBytes() :传响应对象的这个字节数组
//        response.getBytes().length :指定长度.此处这个长度使用response.length()计算字符的个数也行,response.getBytes().length是计算字节的个数,操作计量单位不同,而"DatagramPacket只认字符不认字节",所以还是用response.getBytes().length
//        requestPacket.getSocketAddress() : 获取到客户端的IP和端口号(这两个信息存在requestPacket中所以点得出来),相当于取餐小票纸条(上面有客户端的信息 IP和端口号)
        socket.send(responsePacket); //send函数是指定要把数据发给哪个(指定的)客户端
        //4. 打印当前这次请求响应的处理中间结果
        System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(), //requestPacket.getAddress() : 获取到Packet里的IP
                                                        requestPacket.getPort(), //requestPacket.getPort() : 获取Packet里面的端口
                                                        request,response);
    }
}

主函数

构建服务器类的时候要传入一个自定义的服务器端口

    public static void main(String[] args) throws IOException {
        //服务器端口号的指定可以自定义(1024 <--> 65535)
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }

最终代码

package UDP;

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对象的同时,必须要绑定一个指定具体的端口(port)
    //服务器是网络传输中被动的一方,如果操作系统随机分配端口,此时客户端就不知道这个端口到底指到哪的,就没法进行通信

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    //DatagramPacket为UDP传输数据的基本单位

    public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器需要对很多客户端提供服务,所以得用一个循环一直站岗接受数据
        while(true) {
            //只要客户端过来就可以提供服务,

            // 1.读取客户端发来的请求
            //receive 方法是一个 输出 型参数需要 先构造好空白的 DatagramPacket 对象做参数,交给receive来进行填充
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); //datagramPacket就相当于点餐纸条,把纸条写好了就交给receive处理
            socket.receive(requestPacket); //receive内部会针对参数(datagramPacket)对象填充数据,填充数据来自网卡
            //此时这个DatagramPacket(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()
                                                                );
    //DatagramPacket 参数
//            response.getBytes() :传响应对象的这个字节数组
//            response.getBytes().length :指定长度.此处这个长度使用response.length()计算字符的个数也行,response.getBytes().length是计算字节的个数,操作计量单位不同,而"DatagramPacket只认字符不认字节",所以还是用response.getBytes().length
//            requestPacket.getSocketAddress() : 获取到客户端的IP和端口号(这两个信息存在requestPacket中所以点得出来),相当于取餐小票纸条(上面有客户端的信息 IP和端口号)
            socket.send(responsePacket); //send函数是指定要把数据发给哪个(指定的)客户端
            //4. 打印当前这次请求响应的处理中间结果
            System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(), //requestPacket.getAddress() : 获取到Packet里的IP
                                                            requestPacket.getPort(), //requestPacket.getPort() : 获取Packet里面的端口
                                                            request,response);
        }
    }

    //响应函数
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        //服务器端口号的指定可以自定义(1024 <--> 65535)
        UdpEchoServer server = new UdpEchoServer(9090);     //服务器在构造方法中,先指定好自己的端口,
        server.start();
    }
}

服务器优化

但是服务器像上述循环接收有一个缺陷,假设有很多客户端并且请求速度很快,这时候while循环就处理不过来了(高并发)

那么如何解决高并发问题呢?

多线程!更充分的调动计算机的硬件资源,或者(分布式)多加机器(有管理成本)

客户端 UdpEchoClient 代码

构造这个socket对象不需要显示绑定指定的端口,让服务器自动分配一个别的进程没使用的空闲端口

import java.net.DatagramSocket;
import java.net.SocketException;

// UDP 版本的回显客户端
public class UdpEchoClient {
    private DatagramSocket socket = null;
    public UdpEchoClient() throws SocketException {

        socket = new DatagramSocket();  //构造这个socket对象不需要显示绑定指定的端口,
                                        // 让服务器自动分配一个别的进程没使用的空闲端口
    }
}

一次通信中涉及到的IP和端口有两组

源ip, 源端口 ------ 目的ip, 目的端口

假设让客户端给服务器发个数据,

源IP就是客户端的IP地址,目的IP就是服务器的IP地址(此时客户端和服务器都是在本机主机上,因此IP都是127.0.0.1环回IP)

源端口就是客户端端口,目的端口就是服务器端口

端口号用来识别/区分一个进程,因此通常情况下不允许一个端口同时被多个进程使用(前提是一个主机上的).

但是一个进程可以绑定多个端口,进程只要创多个Socket对象就可以分别关联不同的端口,[Socket和端口号是一对一的,进程和Socket是一对多的],对于服务器来说端口必须是确定好的,而客户端端口可以是系统分配的

客户端不指定端口,为了防止客户端其他程序也占用了这个端口,导致程序不能正常工作

构造UdpEchoClient对象

// UDP 版本的回显客户端
public class UdpEchoClient {
    private DatagramSocket socket = null;
    // 一次通信需要有两个 ip 和 端口
    // 客户端的IP 127.0.0.1 是以知的,port端口是系统自动分配的
    String severIP = null;
    int severPort = 0;
    // 服务器的IP和端口也需要告诉客户端才能建立通信
    //所以构造方法里就需要传入 服务器的IP和端口
    public UdpEchoClient(String severIP, int severPort) throws SocketException {
        socket = new DatagramSocket();  // 构造这个socket对象不需要显示绑定指定的端口,
        // 让服务器自动分配一个别的进程没使用的空闲端口
        this.severIP = severIP;
        this.severPort = severPort;
    }
}

start方法

由于不是只接受一条数据,所以要嵌套一个循环

public void start() throws IOException {
    System.out.println("客户端启动!");
    while(true) {
    // 1.从控制台读取要发送的数据
    // 2.构造成UDP请求并发送
    // 3.读取UDP响应并解析
    // 4.把解析好的结果显示出来
    }
}
  1. 从控制台读取要发送的数据
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;
        }
    }
}
  1. 构造成UDP请求并发送
  • 构造packet的时候需要把 severIP 和 port 都传入进来

  • 但是此时IP需要填入一个32位的整形

  • 而之前的IP地址又是一串字符串,需要用InetAddress.getByName()来进行转换

我们看到的IP地址 如 127.0.0.1 是"点分十进制"每个部分范围是0-255 占一个字节,想要给计算机解析看得懂就得转换为32位的整数

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请求并发送
        DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                                                        request.getBytes().length,
                                                        InetAddress.getByName(severIP), // 需要把IP字符串格式转化为整形格式
                                                        severPort);
        socket.send(requestPacket); // 发送
    }
}
  1. 读取UDP响应并解析
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的时候需要把 severIP 和 port 都传入进来
        // 但是此时IP需要填入一个32位的整形
        // 而之前的IP地址又是一串字符串,需要用InetAddress.getByName()来进行转换
        DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                                                        request.getBytes().length,
                                                        InetAddress.getByName(severIP), // 需要把IP字符串格式转化为整形格式
                                                        severPort);
        socket.send(requestPacket);

        // 3.读取UDP响应并解析
        //构造对象
        DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
        socket.receive(responsePacket); // 填充
        //把结果转换为字符串
//            String response = new String(responsePacket.getData(), 0, requestPacket.getLength(),"utf8");
        String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
    }
}
  1. 把解析好的结果显示出来
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的时候需要把 severIP 和 port 都传入进来
        // 但是此时IP需要填入一个32位的整形
        // 而之前的IP地址又是一串字符串,需要用InetAddress.getByName()来进行转换
        DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                                                        request.getBytes().length,
                                                        InetAddress.getByName(severIP), // 需要把IP字符串格式转化为整形格式
                                                        severPort);
        socket.send(requestPacket);

        // 3.读取UDP响应并解析
        //构造对象
        DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
        socket.receive(responsePacket); // 填充
        //把结果转换为字符串
//            String response = new String(responsePacket.getData(), 0, requestPacket.getLength(),"utf8");
        String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
        // 4.把解析好的结果显示出来
        System.out.println(response);
    }
}

注:String函数可以规定字符集,所以有两种写法(可以加个参数"utf8"指定中文情况下不乱码)

启动

客户端的IP和端口得和服务器的IP端口相对应 127.0.0.1是本机,端口号是之前自己定义的9090

public static void main(String[] args) throws IOException {
    UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
    client.start();
}

客户端与服务器代码区别

对于服务器来说是先去receive读请求,再send,再返回响应

而客户端是先构造请求,再send发送,再读取响应,再receive

最终代码

package UDP;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

// UDP 版本的回显客户端
public class UdpEchoClient {
    private DatagramSocket socket = null;
    // 一次通信需要有两个 ip 和 端口
    // 客户端的IP 127.0.0.1 是以知的,port端口是系统自动分配的
    // 服务器的IP和端口也需要告诉客户端才能建立通信
    String severIP = null;
    int severPort = 0;
    //所以构造方法里就需要传入 服务器的IP和端口
    public UdpEchoClient(String severIP, int severPort) throws SocketException {
        socket = new DatagramSocket();  // 构造这个socket对象不需要显示绑定指定的端口,
                                        // 让服务器自动分配一个别的进程没使用的空闲端口

        this.severIP = severIP;
        this.severPort = severPort;
    }

    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的时候需要把 severIP 和 port 都传入进来
            // 但是此时IP需要填入一个32位的整形
            // 而之前的IP地址又是一串字符串,需要用InetAddress.getByName()来进行转换
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                                                            request.getBytes().length,
                                                            InetAddress.getByName(severIP), // 需要把IP字符串格式转化为整形格式
                                                            severPort);
            socket.send(requestPacket); // 发送

            // 3.读取UDP响应并解析
            //构造对象
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket); //填充
            //把结果转换为字符串
//            String response = new String(responsePacket.getData(), 0, requestPacket.getLength(),"utf8");
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 4.把解析好的结果显示出来
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

服务器与客户端运行顺序

服务器是被动接收的一方,客户端是主动发送数据一方

  1. 服务器先启动执行,服务器运行到receive阻塞

  1. 客户端scanner.next读取用户输入的内容

  1. 用户端socket.send发送请求

  1. 客户端socket.receive等待阻塞响应

  1. 服务器process根据请求计算响应

  1. 服务器执行socket.send返回响应

  1. 客户端socket.receive从阻塞中返回,读到了响应

Tips:

如何阻塞:DatagramSocket这个类的receive函数能阻塞不是Java实现的而是操作系统原生提提供的API(recv)及时阻塞的函数,系统对于IO操作本身就有这样的阻塞等待机制,哪个线程如果进行IO完成之前,就会自动把对应的线程放到阻塞队列中,暂时不参与调度

测试

客服端的端口号和IP,

此处IP是环回IP,这里的端口是系统随机分配的

对于客户端服务器程序来说一个服务器要给很多个客户端提供服务的,多客户端,启动多个进程

咱们也就可以需要要构造多个客户端来进行测试

IDEA默认只能启动一个客户端,需要稍微调试一下让它能启动多个客户端

如果想上上述程序连上其他电脑是上的服务器程序,这背后的操作就挺复杂的,虽然没发连一般的电脑服务器(没有外网IP只能在局域网内部访问),但是可以连“云服务器”(有外网IP任何一个连上网络的设备都能访问)这样的“特殊电脑”

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

补集王子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值