✨个人主页:bit me
✨当前专栏:Java EE初阶
socket 类,本质上是相当于一个 “文件”,在系统中,还有一种特殊的 socket 文件,对应到网卡设备。构造一个 DatagramSocket 对象,就相当于是打开了一个内核中的 Socket 文件,打开之后,就可以传输数据了。send 发送数据;receive 接收数据;close 关闭文件。
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket 方法:
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacketp) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
表示一个 UDP 数据报,UDP 是面向数据报的协议,传输数据,就是以 DatagramPacket 为基本单位
DatagramPacket是UDP Socket 发送和接收的数据报。
DatagramPacket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从 0 到指定长度(第二个参数 length)。address指定目的主机的IP和端口号 |
DatagramPacket 方法:
方法签名 | 方法说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创
建。
IP 地址 + 端口号
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法签名 | 方法说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
回显:客户端发啥,服务器就返回啥。不涉及到任何的业务逻辑,而只是单纯的演示 api 的用法。
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 异常,处理一下就好了。
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
还有一种写法:
但是这种写法是要被舍弃的,String 被画了删除线,可能在未来某一天就被删除了。
String response = process(request);
通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理
public String process(String req){
return req;
}
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 + 端口
socket.send(responsePacket);
System.out.printf("[%s:%d] req = %s; resp = %s\n",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\n",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();
}
}
服务器,端口一般是手动指定的,如果自动分配,客户端就不知道服务器的端口是啥了,因此服务器有固定端口客户端才好访问。
客户端,端口一般是自动分配的,客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,说不好这个端口就和其他程序冲突了,导致咱们的代码无法运行。
public UDPEchoClient() throws SocketException {
//客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好
socket = new DatagramSocket();
}
System.out.println("> ");
String request = scanner.next();
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\n",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\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient client = new UDPEchoClient();
client.start();
}
}
整体效果演示:
客户端中输入一个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 上面的文件描述符表也没了,此时也就相当于关闭了。