一、API介绍
UDP协议面向数据报进行传输,所以在代码中基本是以数据报(DatagramPacket)作为操作对象,进行输入和输出的。
JAVA提供了两个常用的类去操作UDP套接字:
- 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
- 类方法:
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
- 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf),从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
- 类方法:
方法签名 | 方法说明 |
---|---|
getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据,以字节数组的形式 |
int getLength() | 返回字节数组的长度 |
getSocketAddress() | 获取原数据报持有放的IP和端口 |
注意:
1、只要是获取IP地址或者端口号的方法,数据原本产生自哪里,返回的IP或端口号就来自哪里。
2、只要是套接字,就会消耗文件资源描述符,而文件资源描述符是有限的,如果出现大量请求,在不关闭已经运行完了的程序的文件资源描述符,就可能出现文件资源泄露。所以在使用套接字的时候,程序执行完,一定要记得关闭文件资源!
二、创建简单的回显服务器
服务器端
public class Server {
DatagramSocket serverSocket=null;
public Server(int port) throws SocketException {
//这一步JVM使用操作系统提供的关于socket的API完成端口和进程之间的绑定
serverSocket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.printf("服务器启动!\n");
try {
while(true){
//1、获取从客户端发来的请求,注意第二个参数不能大于第一个参数!
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096 );
//如果没有获取到客户端发来的请求,会进入阻塞状态,知道获取到数据
serverSocket.receive(requestPacket);
//2、数据报转化成字符串
String request=new String(requestPacket.getData(),
0,
requestPacket.getLength());
//3、根据请求request,生成响应,然后打包成数据报
//由于是回显服务器,所以直接返回相同的字符串,在实际情况中,这一步可能会很复杂
String response=process(request);
//把响应封装成数据报
DatagramPacket responsePacket=new DatagramPacket(
response.getBytes(),
response.getBytes().length,
requestPacket.getSocketAddress()
);//由于UDP是无连接的通信方式,所以要指定好请求客户端的IP和端口号
//4、把封装好的数据报发送给客户端
serverSocket.send(responsePacket);
//5、打印工作日志[IP地址:端口号]
System.out.printf("[%s:%d] 请求=%s 响应=%s\n",
requestPacket.getAddress(),
requestPacket.getPort(),
request,
response);
}
} finally {
//记得只要是套接字,一定要分析是否要关闭文件资源描述符,避免文件资源泄露。
if(serverSocket!=null&&!serverSocket.isClosed()){
close();
}
}
}
private void close(){
serverSocket.close();
System.out.println("服务器套接字已关闭。");
}
private String process(String request){return request;}
public static void main(String[] args) throws IOException {
//程序员自己指定一个端口
Server server=new Server(9090);
server.start();
}
}
客户端
public class Client {
//用于发送和接收数据
private DatagramSocket clientSocket=null;
//服务器的IP地址
private String serverIP=null;
//服务器的端口号
private int serverPort=0;
public Client(String ip,int port) throws SocketException {
this.serverIP=ip;
this.serverPort=port;
//不设置参数,让本地计算机自己分配一个端口号,如果让用户设置端口号可能出现端口占用冲突
clientSocket=new DatagramSocket();
}
public void start() throws IOException {
System.out.printf("客户端启动!");
while (true){
//1、通过控制台,获取用户请求
Scanner scanner=new Scanner(System.in);
System.out.printf("-> ");//单纯做个标记,好看
if(!scanner.hasNext()){
System.out.println("客户端停止输入。");
//养成良好的编程习惯,用完就关闭
clientSocket.close();
break;
}
String request=scanner.next();
//2、把请求打包成数据报,然后发送给服务器处理,容量大小自己设置,只要数据能够存放的下就行
DatagramPacket requestPacket=new DatagramPacket(
request.getBytes(),
request.getBytes().length,
//注意,因为UDP是无连接通信,所以要程序员自己添加服务器的IP和端口号,这样才能成功连接
InetAddress.getByName(this.serverIP),
this.serverPort);
//发送请求
clientSocket.send(requestPacket);
//3、接收服务器发来的响应
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
clientSocket.receive(responsePacket);//等待阻塞直到服务器响应
//4、把获取的数据报转化成字符串,打印到控制台
String response=new String(
responsePacket.getData(),
0,
responsePacket.getLength());
System.out.printf("响应:"+response);
System.out.println();
}
}
public static void main(String[] args) throws IOException {
//这里填写的是服务器的IP和端口号
Client client=new Client("127.0.0.1",9090);
client.start();
}
}
执行结果:
服务器:
服务器启动!
[/127.0.0.1:64316] 请求=11 响应=11
[/127.0.0.1:64316] 请求=22 响应=22
[/127.0.0.1:64316] 请求=33 响应=33
客户端:
客户端启动!-> 11
响应:11
-> 22
响应:22
-> 33
响应:33
->
三、程序优化
在上面这个回显服务器中,虽然成功运行了,但是只有一个客户端,并且服务器——客户端都是在本地主机上运行的。
在实际场景中,可能会有多个客户端同时发送请求给到服务器,那么效率就会变低,因为上述实现的服务器是串行执行的,这时我们该如何提高效率呢?
1. 引入多线程
多线程可以极大的避免同一时刻,多个客户端发来请求的情况,因为多个线程之间并发执行的,共同消费这些请求,从而缓解服务器压力。
2. 使用线程池
我们实现的这个服务器属于长连接
1。
倘若请求量较多,并且每个请求处理速度比较快,还可以进一步的使用线程池来缓解服务器压力,避免频繁的创建、销毁线程。
3. IO多路复用
IO多路复用是为了解决线程由于需要等待客户端IO操作而造成的CPU资源浪费,因为对应线程在等待过程中无法处理其他的任务。它的核心作用就是不让线程停下来,让它去完成别的任务,充分利用CPU资源。
最常见的就是Linux特用的epoll
IO多路复用机制,它通过一些回调机制以及独特的数据结构,降低资源开销,并且本身的性能也非常高。
4 .部署公网IP
我们自己写的这个服务器,设置的是回环IP
2 。服务器和客户端也是在自己的电脑上运行的。倘若把客户端的代码给到其他设备执行,发送的请求在我们的电脑上是不会得到响应的。换句话说,这个服务器只能在自己的设备上自嗨,不具备真正的联网能力!
**想要具备联网能力,那么需要把回环IP“126.0.0.1”换成公网IP。**最好的方式就是把服务器代码部署到云服务器上,云服务器可以通过阿里云、腾讯云等厂商购买,这里就不作过多演示了。