【网络编程】UDP
🏯一. UDP数据报套接字编程
-
DatagramSocket API
socket 类,本质上是相当于一个 “文件”,在系统中,还有一种特殊的 socket 文件,对应到网卡设备。构造一个 DatagramSocket 对象,就相当于是打开了一个内核中的 Socket 文件,打开之后,就可以传输数据了。send 发送数据;receive 接收数据;close 关闭文件。
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
DatagramSocket 方法:
-
DatagramPacket API
表示一个 UDP 数据报,UDP 是面向数据报的协议,传输数据,就是以 DatagramPacket 为基本单位
DatagramPacket是UDP Socket 发送和接收的数据报。
DatagramPacket 构造方法:
DatagramPacket 方法:
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创 建。
-
InetSocketAddress API
IP 地址 + 端口号
InetSocketAddress ( SocketAddress 的子类 )构造方法:
🏰二. 写一个 UDP 版本的 回显服务器-客户端.(echo server)
回显:客户端发啥,服务器就返回啥。不涉及到任何的业务逻辑,而只是单纯的演示 api 的用法。
🏭1. 服务器:
socket = new DatagramSocket(port);
绑定一个端口 => 把这个进程和一个端口号关联起来
一个操作系统上面,有很多端口号,0 - 65535 。 程序如果需要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符。(操作系统收到网卡数据,就可以根据网络数据报中的端口号,来确定要把这个数据交给哪个进程)
分配端口号的过程:
- 手动指定
new DatagramSocket(port);
- 系统自动分配
socket = new DatagramSocket();
一个端口,在通常情况下,是不能被同一个主机上的多个进程同时绑定的;一个进程是可以绑定多个端口的。
如果端口已经被别人占用,再尝试绑定,就会抛出异常 throws SocketException
-
读取客户端发来的请求,尝试读取,不是说调用了就一定能读到
//1.读取客户端发来的请求 socket.receive();
如果客户端没有发来请求,receive 就会阻塞等待,直到真的有客户端的请求过来了,receive 才会返回。
注意:
receive 是通过参数来放置读取到的数据的,而不是通过返回值。看源码中: 输出型参数!!需要调用 receive 之前,先构造一个空的 DatagramPacket ,然后把这个空的 DatagramPacket 填到参数中,receive 返回之后自然把读到的数据给放到参数里面。出现异常是 IOException 异常,处理一下就好了。
-
对请求进行解析,把 DatagramPacket 转成一个 String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
还有一种写法:
但是这种写法是要被舍弃的,String 被画了删除线,可能在未来某一天就被删除了。
-
根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
String response = process(request);
通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理
public String process(String req){ return req; }
-
把响应构造成 DatagramPacket 对象(构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
这个也是构造 DatagramPacket 的一种方式,先是拿字符串里面的字节数组,来构造 Packet 的内容,还要把请求中的客户端地址拿过来,也填到包裹里去。 response.getBytes().length 可以写作 response.length 吗? 不行 response.getBytes().length 表示的是字节数,response.length 表示的是字符数
requestPacket.getSocketAddress() -> (地址)IP + 端口
-
把这个 DatagramPacket 对象返回给客户端
socket.send(responsePacket); System.out.printf("[%s:%d] req = %s; resp = %s ",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
注意是 printf !!!和 C 中的 printf 差不多
服务器总代码:
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; public class UDPEchoServer { //想要创建 UDP 服务器,首先要打开一个 socket 文件 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); //2. 对请求进行解析,把 DatagramPacket 转成一个 String String request = new String(requestPacket.getData(),0,requestPacket.getLength()); //3. 根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情 String response = process(request); //4. 把响应构造成 DatagramPacket 对象 //构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress()); //5. 把这个 DatagramPacket 对象返回给客户端 socket.send(responsePacket); System.out.printf("[%s:%d] req = %s; resp = %s ",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response); } } //通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑, //但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理 public String process(String req){ return req; } public static void main(String[] args) throws IOException { //真正启动服务器,这个端口号说是随便写,但是也有范围的,0 -> 65535 //但是一般来说 1024 以下的端口,都是系统保留 //因此咱们自己写代码,端口号还是尽量选择 1024 以上,65535 以下 UDPEchoServer server = new UDPEchoServer(8000); server.start(); } }
🗼2. 客户端:
服务器,端口一般是手动指定的,如果自动分配,客户端就不知道服务器的端口是啥了,因此服务器有固定端口客户端才好访问。
客户端,端口一般是自动分配的,客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,说不好这个端口就和其他程序冲突了,导致咱们的代码无法运行。
public UDPEchoClient() throws SocketException { //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好 socket = new DatagramSocket(); }
-
让客户端从控制台获取一个请求数据
System.out.println("> "); String request = scanner.next();
-
把这个字符串请求发送给服务器,构造 DatagramSocket,构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里(另外一种 DatagramPacket 的构造方法)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);
InetAddress.getByName(“127.0.0.1”) -> 通过这个字符串来构造的 InetAddress,此处的 127.0.0.1 回环 IP 就表示当前主机。 8000 -> 服务器端口号
这个包裹,就是要从客户端发送给服务器,就需要知道,发送的内容,以及发送的目的地是哪里(收件人地址 + 端口)
目前已经见过三个版本的 DatagramPacket 的构造:
只填写缓冲区,用来接收数据的,一个是空的 Packet 填写缓冲区,并且填写把包发给谁,InetAddress 对象来表示的 填写缓冲区,并且填写把包发给谁,InetAddress + port 来表示的
-
把数据报发送给服务器
socket.send(requestPacket);
-
从服务期读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096); socket.receive(responsePacket);
-
把响应数据获取出来,转成字符串
String response = new String(responsePacket.getData(),0,responsePacket.getLength()); System.out.printf("req: %s;resp: %s ",request,response);
客户端总代码:
import java.io.IOException; import java.net.*; import java.util.Scanner; public class UDPEchoClient { private DatagramSocket socket = null; public UDPEchoClient() throws SocketException { //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好 socket = new DatagramSocket(); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); while (true){ //1. 让客户端从控制台获取一个请求数据 System.out.println("> "); String request = scanner.next(); //2. 把这个字符串请求发送给服务器,构造 DatagramSocket //构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000); //3. 把数据报发送给服务器 socket.send(requestPacket); //4. 从服务期读取响应数据 DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096); socket.receive(responsePacket); //5. 把响应数据获取出来,转成字符串 String response = new String(responsePacket.getData(),0,responsePacket.getLength()); System.out.printf("req: %s;resp: %s ",request,response); } } public static void main(String[] args) throws IOException { UDPEchoClient client = new UDPEchoClient(); client.start(); } }
⛺️3. 理清楚客户端和服务器的工作流程:
- 客户端根据用户输入,构造请求
- 客户端把请求发送给服务器
- 服务器读取请求并解析
- 服务器根据请求计算响应(服务器核心逻辑)
- 服务器构造响应数据,并且返回给客户端
- 客户端读取服务器返回的响应
- 客户端解析响应,并显示给用户
整体效果演示:
-
先启动服务器,再启动客户端
客户端中输入一个hello:
在服务器中:
继续在客户端中输入一个你好:
服务器中显示:
一个服务器是可以同时给多个客户端提供服务的
如在 IDEA 中,你想打开多个客户端,你发现你再运行一次客户端,就会把之前的客户端给关闭了,此时我们需要设置一下,就可以启动多个客户端。
此时我们就可以打开多个客户端了
一个服务器灵魂所在,就是大体框架是一样的,比如这个回显服务器,我们要改成带有业务逻辑的服务器,只需要把 process 改掉即可,如我们简单实现一个词典
import java.io.IOException; import java.net.SocketException; import java.util.HashMap; import java.util.Map; //字典服务器 / 翻译服务器 //希望实现一个英译汉的效果 //请求的是一个英文单词,响应是对应的中文翻译 public class UDPDicServer extends UDPEchoServer{ private Map<String, String> dic = new HashMap<>(); public UDPDicServer(int port) throws SocketException { super(port); //这里的数据可以无限的构造下去 //即使是有道词典这种,也是类似的方法实现(打表) dic.put("cat","小猫"); dic.put("dog","小狗"); dic.put("fuck","卧槽"); } //和 UDPEchoServer 相比,只是 process 不同,就重写这个方法即可 public String process(String req){ return dic.getOrDefault(req,"这个词俺也不会!"); } public static void main(String[] args) throws IOException { UDPDicServer server = new UDPDicServer(8000); server.start(); } }
一个服务器要完成的工作,都是通过 “根据请求计算响应” 来体现的
不管是啥样的服务器,读取请求并解析,构造响应并返回,这两个步骤,大同小异,唯有 “根据请求计算响应” 是千变万化,是非常复杂的,可能一次处理请求就要几w,几十w的代码来完成。
拓展: 在 DatagramSocket 中有 send,receive 和 close。只有 send 和 receive 写进去了,close 却没有写进去,原因是在上述代码中 socket对象,生命周期都是应该伴随着整个进程的(while(true) 循环),因此进程结束之前,提前用 close 关闭 socket 对象,不合适,当进程已经结束,对应 PCB 没了,PCB 上面的文件描述符表也没了,此时也就相当于关闭了。