0.前言
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数
据报,一次接收全部的数据报。
java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:
以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请
求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:
1. DatagramSocket API
DatagramSocket
是UDP Socket,用于发送和接收UDP数据报。
- 使用这个类,表示一个socket对象,在操作系统中,就把这个socket对象也当做一个文件来处理,相当于是文件描述符表生的一项。
- 普通的文件,对应的硬件设备是硬盘,socket文件对应的设备是网卡。
- 一个socket对象只能标记一台主机,所以要和多个主机通信,则需要创建多个socket对象。
DatagramSocket
构造方法:
所以本质上来说,不是进程与端口建立了联系,而是进程中的socket对象与端口建立了联系。
DatagramSocke
t 方法:
void receive(DatagramPacket P)
此处传入的相当于是一个空对象,recieve方法内部,会对参数的这个空对象进行内容填充。从而构造出一个结果数据,就类似与c里面加了&的形参,所以这里的参数也是一个“输出型参数”。DatagramPacket P
是一个报文对象
本质上套接字也是文件,所以用完也需要关闭
2. DatagramPacket API
DatagramPacket
是UDP Socket发送和接收的数据报。
DatagramPacket
构造方法
`DatagramPacket 方法:
构造UDP发送的数据报时,需要传入 SocketAddress,该对象可以使用 InetSocketAddress 来创建。
3. InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
4.回显服务器
编写一个最简单的UDP版本的客户端服务器程序
一个最简单的UDP版本的客户端服务器程序,称之为回显服务器。
4.1服务器端代码
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPEchoClient {
//网络编程,本质上是要操作网卡
//但是网卡不方便直接操作,在操作系统内核中,使用一种特殊的叫做“socket"这样的文件来抽象表示网卡
//因此,进行网络通信,必须要有socket对象
private DatagramSocket socket = null;
//对于服务器而言,创建socket对象的同时,需要绑定一个具体的端口号
//服务器一定需要关联上一个具体的端口号,服务器是网络传输中被动的一方,如果是操作系统随机分配的端口号,那么客户端就不知道这个端口是啥,从而无法通信
public UDPEchoClient(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//只要有客户端过来就会提供服务,所以用来while循环
// 1.读取客户端发来的请求
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
//receive方法的参数是一个输出型参数,需要先构造一个空包的DatagramPacket对象交给receive去填充。
socket.receive(requestPacket);
//此时这个DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来,构造成一个字符串
//但是要注意这里getData返回的是一个byte数组,所以要调用String的带byte[]参数的构造方法
//此外虽然这个数组设置4096个元素,但是实际传输中,4096个元素长度的字节数组不一定会全部占满,所以采用getlength()方法保证实际的有效部分构造成字符串即可
String request = new String( requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应,由于此处是回显服务器,响应和请求相同
String response = process(request);
//3.把响应返回客户端,
// send的参数也是DatagramPacket ,需要把这个packet对象构造好
//此处构造的响应对象,不能用空的数组构造了,而是要用响应的结果来构造
//参数1需要的是一个byte[]数组,那么就调用String里面的getBytes方法,将字符串以字节数组的形式取出
//参数2需要是构建长度,这里要注意不能写response.length,因为这个求得是字符串长度,我们要的是字节长度,二者可能相同,可能不同,
// 如果存的是ASCII码里面的字符当然没有用影响,但是如果存的是汉字等差距就很大了,一个汉字是一个字符但是是三个字节(UTF-8)
//参数3还需要一个地址,一个响应结果的接收地址(也就是ip和端口号),这两个信息本来就在requestPacket中,所以直接调用requestPacket的getAdress方法即可。
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
}
}
//响应请求的具体操作,因为本例子中是回显服务器,所以直接返回就行了
public String process(String request){
return request;
}
}
说明一下
- 首先
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
这一句里面,为什么明明已经初始化了byte[4096]还有额外增加一个变量表示长度4096.其实是因为请求数据报里面不只有数据,还有其他的比如首部尾部等,我们这里为了方便,就将整个数据报报都作为数据部分,所以后面的长度是4.96,实际应该是小于4.96的 - while循环,每循环一次,就处理一次请求-响应,客户端啥时候发请求,服务器端是不知道的,有多少客户端发请求,服务器端也是不知道的。但是socket.recieve()本身就具有阻塞特性,在请求多的时候,会积极响应,但是没有请求的时候,那么recieve()本身也会阻塞起来,不浪费系统资源。这个就和Scanner也是这样的。
- 打印请求数据报的地址记得调佣toString方法
- 对于服务器进程而言,whiel循环啥时候结束呢?答案是服务器程序就是死循环的,因为服务器要随时等待接受请求,都是7*24小时运行的。
4.2客户端代码
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UDPEchoClient {
private DatagramSocket socket = null;
String serverIp = null;
int serverPort = 0;
//注意:这里与服务器端的区别在于客户端在构建socket的时候不需要手动指定端口号,交给操作系统随机分配;(一个socket对象对应唯一一个端口号)
//因为在本例子中,我们客户端和服务器端都在本机上,也是就是ip地址就是本地环回地址127.0.0.1
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){
//从控制台读取要发送的信息
System.out.println("请输入信息:");
String request = scanner.next();
//简单判断一下
if(request == "exit"){
System.out.println("请求结束!");
return;
}
//构造requestpack对象
//参数1是一个byte类型的数组所以request调用Stringe类里面的getBytes方法,将字符串转变为字节数组
//要构建字节数组的长度
//InetAddress.getByName(serverIp)依据名字获取ip地址,ip地址实际上就是一个32位的二进制序列
DatagramPacket requestPack = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
socket.send(requestPack);
//读取服务器的UDP响应
DatagramPacket responsePackte = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPack);
String response = new String(responsePackte.getData(), 0, responsePackte.getLength());
//打印响应
System.out.println(response);
}
}
public static void main(String[] args) throws SocketException {
UDPEchoClient client = new UDPEchoClient("127.0.0.1", 9090);
client.start();
}
}
- 一般来说客户端指定端口号可不可以呢?理论上也是可以的·但是不推荐,这是因为手动指定的端口号可能存在已经被用了造成冲突的情况。
- 那么为什么服务端指定就不用担心冲突呢?这是因为服务器端大部分的内容都是程序员自己设定的,服务器上的程序都是可控的。但是客户端程序就非常繁杂了,千奇百怪。
4.3回显服务器的执行过程
说明一下:
- DatagramSocket中的receive究竟是如何阻塞呢?
实际上这个的阻塞不是由java实现的,而是由操作系统内核实现,系统对于IO操作本身就有这样的阻塞机制。这个是看java原码是看不来的
执行结果
服务器端执行结果
客户端执行结果
注意:
- 一定要先启动服务器,再启动客户端。
- 我们如果在输入一次信息,实际是一个客户端发送了请求两次,并不是两个客户端,最直接的证据就是服务器端返回的信息ip地址和端口号是同一个。
- 那么我们如何构造多个客户端同时对服务器端进行访问呢?可以使用构建多个线程的方法,一个线程代表一个客户端,这里为了操作方便我们可以直接构建多个进程,每个进程对应一个客户端来实现即可,具体方法如下:
在UDPEchoClient进程下点击编辑配置 点击允许多个实例,即可创建多个实例,也就是每点击一次允许,就会创建一个新的进程作为新的客户端
实际效果
第一个客户端
第二个客户端
服务器端
回显服务器只是用来演示的,并不具有实际的业务能力,那么我们在上述代码的基础上进行调整,增加一定的业务功能,实现一个“查字典”的服务器,具体功能是查一个英文单词的中文意思
5.“查字典服务器”
代码实例
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
//对于DicSever而言,与EchoSever相比,大部分东西是一样的,所以我们直接继承EchoSever
public class UPDDicServer extends UDPEchoServer {
//创建一个字典
private Map<String,String> dict = new HashMap<>();
//构造函数
public UPDDicServer(int port) throws SocketException {
super(port);//调用父类构造函数
//初始化字典
dict.put("cat","猫");
dict.put("dog","狗");
dict.put("hello","你好");
dict.put("bye","再见");
}
//主要是“根据请求计算响应这个步骤是不一样的,所以我们直接重写
@Override
public String process(String request) {
//查字典的过程
return dict.getOrDefault(request, "当前单词没有查到结果");
}
public static void main(String[] args) throws IOException {
UDPEchoServer server = new UPDDicServer(9090);
server.start();
}
}
结果
不同的服务器,其业务是不同的,但是基本逻辑是一样的,一些更复杂的服务器,process里面可能要运行几万行和几十万行。
5.1Address already in use: Cannot bind异常
一个端口号是只能被一个进程所使用的,那么如果端口冲突了会是什么样的情况呢?
我们先启动UDPEchosever,然后再启动UDPDicsever,那么就出抛出这样的异常
Address already in use: Cannot bind
这个异常需要大家记住,这个异常无论是后续学习中,还是实际工作照片中都会反复出现。