目录
一. UDP API
二. UDP回显客户端-服务器
1. 服务器
2. 客户端
3. 客户端-服务器的工作流程
UDP数据报套接字编程是一种基于无连接协议的网络通信方式
一. UDP API
在网络通信中最重要的硬件设备就是网卡,我们可以将网卡这样的硬件设备抽象为Socket文件
通过网卡发送数据,可以抽象为写Socket文件
通过网卡接收数据,可以抽象为读Socket文件
在这里我们可以理解为对Socket文件进行读写,也就是借助网卡发送和接受数据
在UDP编程中,主要使用两个核心类:DatagramSocket和DatagramPacket
DatagramSocket
DatagramSocket是UDP Socket,负责数据报的发送和接收。
DatagramSocket 构造方法 :
方法 | 说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定本机随意分配的一个端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定本机指定的端口(一般用于服务端) |
DatagramSocket 方法:
方法 | 说明 |
---|---|
void receive(DatagramPacket p) | 接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacketp) | 发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
DatagramPacket
DatagramPacket是UDP Socket,用于封装 UDP 数据报的数据内容和地址信息
DatagramPacket构造方法:
方法 | 说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket用来接收数据报 第一个参数存放接收的数据, 第二个参数表示接收数据的指定长度 |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket用来发送数据报 第一个参数存放要发送的数据 第二个参数表示起始偏移量 第三个参数表示发送数据的指定长度 第四个参数表示目的主机的IP和端口号 |
DatagramPacket方法:
方法签名 | 方法说明 |
---|---|
InetAddress getAddress() | 获取主机IP地址 |
int getPort() | 获取主机端口号 |
byte[] getData() | 获取数据报中的数据 |
二. UDP回显客户端-服务器
回显服务器
回显服务器:不进行任何的业务逻辑,只是将收到的数据显示出来
- 在网络编程中,我们只需要会使用传输层提供的API,明白网络传输的基本流程即可
- 传输层会对每一个Socket对象分配独立的缓冲区(操作系统内核中)
- 网卡收到的数据会层层分用并解析,放入缓冲区中
- 网络编程本质上是在对缓冲区中的数据进行操作
1. 服务器
分配端口号
无论是发送方还是接受方,都必须分配一个端口号
socket = new DatagramSocket(port);
端口号类似于网络区分进程的身份标识符,在网络通信中,根据端口号确定将数据交给那个进程
注意:
- 一个端口只能被一个进程绑定,一个进程可以同时绑定多个端口
- 如果一个端口被多个进程使用,则会抛出异常
衡量服务器效率
服务器往往是24小时全天无休运行,一直在循环处理请求并返回响应的过程
一个服务器在单位时间内处理请求和返回响应越多,说明服务器的效率越高
服务器如何处理请求和返回响应?
1)读取请求并解析
(1)构造一个数据报容器
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
- 由于UDP使用数据报传输数据,构造一个数据报容器,将读取的数据放入容器中
(2)读取请求
socket.receive(requestPacket);
- 使用receive方法从内核的缓冲区中读取数据报,放入requestPacket容器中
- UDP数据报中的载荷部分放到了requestPacket的字节数组中,报头中的属性信息也会被保存在requestPacket中
- 如果缓冲区中没有数据,则会陷入阻塞等待中
(3)将字节数组转换为字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
- getLength()方法:获取字节数组中的有效数据长度(如果全长可能会有很大的空白)
- 字节数组中可能保存文本数据,也可能保存二进制数据,这里统一使用String来保存
2)根据请求得出响应
回显服务器:不会处理数据,输入什么就会返回什么
String response = process(request);
使用process方法来实现回显功能
public String process(String request) {return request;}
如果想要实现特定的功能,直接在process中实现即可
3)将响应返回客户端
(1)构造数据报
由于数据格式为UDP数据报,所以将String类型的数据转换成UDP数据报
DatagramPacket respondPacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
- 数据报中包含:字节数组,字节数,客户端IP地址和端口号
- 客户端的IP地址和端口号从接收的请求数据报中获取
- 将请求中的源ip,源端口,作为响应的目的ip和目的端口
注意:response.getBytes().length 和 response.length 区别
- response.getBytes().length 表示字节数
- response.length 表示字符数
(2)发送数据报
socket.send(respondPacket);
通过使用send()方法将UDP数据包发送出去(send方法不会阻塞等待)
服务器总代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;//udp服务端
public class UdpEchServer {private DatagramSocket socket = null;//在创建的时候规定端口号public UdpEchServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("启动服务器");while(true){//一直在处理请求和发回响应
// 1.读取请求并解析//Udp使用数据报,所以使用DatagramPacketDatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//从内核的缓存区中取出数据放入requestPacket中socket.receive(requestPacket);//将requestPacket中的数据(字节数组)转换成String类型String request = new String(requestPacket.getData(),0,requestPacket.getLength());
// 2.根据请求得出响应3String response = process(request);
// 3,将响应返回客户端//继续使用数据报的方式返回//数据报中包含,响应的内容,长度,请求方的地址DatagramPacket respondPacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());socket.send(respondPacket);//打印日志System.out.printf("[客户端IP= %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 {UdpEchServer udpEchServer = new UdpEchServer(9090);udpEchServer.start();}
}
- 一般服务器需要分配指定的端口(保证端口固定),而客户端则不需要
- 如果服务器的端口号由系统随机分配,有可能会出现,服务器重启一次,所有的客户端找不到服务器的位置
2. 客户端
指定IP和端口号
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.serverPort = serverPort;this.serverIp = serverIp;socket = new DatagramSocket();}
- 客户端需要主动向服务器发送消息,发送消息的前提是知道服务器的位置(IP和端口号)
客户端如何发送请求和接收响应?
1)获取请求数据
(1)从控制台输入数据
Scanner scanner = new Scanner(System.in);while(true){System.out.print("请输入:");
// 1.从控制台中获取发送的请求数据if(!scanner.hasNext()){break;}//将数据转为字符串格式String request = scanner.next();}
- 使用Scanner输入数据,如果没有输入数据,则会进入阻塞等待
2)发送请求数据报
(1)构造请求数据报
DatagramPacket requestPack = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);
- 数据报中包含:字节数组,字节数,服务器IP地址和端口号
- 客户端将数据报发送给服务器,就必须要知道内容和接收的地址
- InetAddress.getByName(serverIp):将IP 地址字符串解析为InetAddress对象。
(2)发送请求数据报
socket.send(requestPack);
调用send()方法,将构造的requestPack数据报(请求数据报)发送出去
3)接收服务器响应
(1)创建一个数据报容器
//创建一个空的字节数组来接受响应DatagramPacket respondPack = new DatagramPacket(new byte[1024],1024);
- 构建出响应数据报容器,将接收的响应数据报,放入容器中
(2)接收响应数据报
socket.receive(respondPack);
- 通过调用receive()方法,将从内核缓冲区取出的数据报放入respondPack中
- 当缓冲区中没有数据的时候,会进入阻塞等待
4)显示响应信息
String respond = new String(respondPack.getData(),0,respondPack.getLength());System.out.println("服务端的回应:"+respond);
- 将数据报中的字节数组转换为字符串类型,并打印在控制台
客户端总代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp ;private int serverPort;public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.serverPort = serverPort;this.serverIp = serverIp;socket = new DatagramSocket();}public void start() throws IOException {System.out.println("客户端启动!");Scanner scanner = new Scanner(System.in);while(true){System.out.print("请输入:");
// 1.从控制台中获取发送的请求数据if(!scanner.hasNext()){break;}//将数据转为字符串格式String request = scanner.next();
// 2.将请求数据构造成数据报,并发送//其中数据报中包括,内容,长度,服务端的ip,服务端的端口号DatagramPacket requestPack = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);//发送请求socket.send(requestPack);
// 3.服务器的响应//创建一个空的字节数组来接受响应DatagramPacket respondPack = new DatagramPacket(new byte[1024],1024);//从缓存区接受数据报保存在respondPack中socket.receive(respondPack);
// 4.将数据报中的内容显示到控制台String respond = new String(respondPack.getData(),0,respondPack.getLength());System.out.println("服务端的回应:"+respond);}}public static void main(String[] args) throws IOException {UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);udpEchoClient.start();}
}
代码中出现的三种构造UDP数据报(DatagramPacket)
(1)因为接收数据报,构造的数据报容器
DatagramPacket respondPack = new DatagramPacket(new byte[1024],1024);
(2) 客户端发送请求数据报
DatagramPacket requestPack = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);
(3)服务器发送响应数据报
DatagramPacket respondPacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
- 从接收的请求数据报中获取IP和端口号
3. 客户端-服务器的工作流程
整个过程中,服务器先启动
- 服务器启动后,进入while循环,执行receive,由于缓冲区没有数据,故进入阻塞
- 客户端启动后,进入while循环,由于没有输入信息,故在hasNext()这里被阻塞
- 客户端输入信息后,hasNext()阻塞被取消,将输入的数据构造成数据报,并使用send发送,发送成功后,调用receive()接收响应数据报,由于服务器没有返回响应数据报,故进入阻塞
- 服务器的缓冲区收到数据后,从阻塞中唤醒,调用receive()接收客户端的请求数据报,将读到数据报构造成String类型,通过process()方法得到响应字符串,将字符串转换为数据报并使用send发送给客户端
- 客户端从receive中返回,将得到的响应数据报转换成String类型,并打印在控制台上
上述的功能一直循环,直到进程销毁
在整个代码中,并没有使用close方法,对于socket对象来说,生命周期应该伴随整个进程,因此进程结束之前,不能提前用 close 关闭 socket 对象,当进程已经结束后,所有的资源自然也就释放了
点赞的宝子今晚自动触发「躺赢锦鲤」buff!