文章目录
前言
本人是一个刚刚上路的IT新兵,菜鸟!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!
今天分享的内容是UDP数据报套接字实现的客户端与服务器,一定要理解 DatagramSocket,DatagramPacket 这两个类的作用以及方法,十分有助于你理解服务器,客户端代码。
一、理论准备
Socket套接字是什么
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
程序猿👨💻编写网络程序,主要编写的是 应用层的程序代码 ,但是真正想要发送或接收数据,都是要 通过应用层调用传输层 。
因此传输层就为应用层(为我们编写代码)提供了一组api统称为
Socket api。
简单来说,这一组api是提供给咱们 编写网络程序使用的接口 , 用来发送 / 接收网络数据使用的接口 。
Socket套接字主要针对传输层协议划分为如下三类:
1️⃣ 数据报套接字:使用传输层UDP协议 (本文重点讲解)
2️⃣ 流套接字:使用传输层TCP协议 (下篇文章重点讲解)
3️⃣原始套接字(不做介绍)
UDP协议的特点
特点 | 说明 |
---|---|
无连接 | 不刻意保存对端的相关信息 |
不可靠传输 | 不关心对端是否收到数据,不关注传输数据是否成功 |
面向数据报 | 以一个UDP数据报为基本单位 |
有接收缓冲区,无发送缓冲区 | 后续文章介绍 |
大小受限 | 一次最多传输64k |
全双工 | 一条通信路径,双向通信。(可以同时发送和接收数据) |
二、UDP数据报套接字 提供的API
DatagramSocket API
DatagramSocket 是UDP的Socket套接字, 用于发送和接收UDP数据报 。
DatagramSocket对象可以理解为一个遥控器,指挥网卡设备实施各种操作
DatagramSocket构造方法 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(intport) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口号(intport)(一般用于服务端) |
DatagramSocket方法 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacketp) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
DatagramPacket API
DatagramPacket 是UDP Socket用来发送和接收的 数据报 。
DatagramPacket对象是 数据报
DatagramPacket构造方法 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[]buf, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从offset(第二个参数offset)下标开始,数据报中的数据大小即共接收length个字节(第三个参数length)。address(第四个参数address)指定目的主机的IP和端口号 |
DatagramPacket方法 | 方法说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
三、代码实现请求响应式 客户端服务器
服务器
服务器大致就分为三个功能。
1️⃣ 读取解析客户端发来的请求
2️⃣ 根据请求计算影响
3️⃣ 把响应结果写回客户端
下面代码中一步一步实现了这三个功能,并配有详细的注释帮你快速理解
核心思路:
1️⃣核心成员属性DatagramSocket socket(网卡的遥控器)。
2️⃣构造方法要指定端口号进行绑定,便于客户端连接。
3️⃣start()方法服务器的主逻辑,负责读取请求,然后通过process(String request)方法得到响应并返回给客户端。
4️⃣start()方法读取功能 :通过构造一个新的空的数据报来接收客户端发来的数据,并从这个数据报中可以获得客户端的IP地址与端口号。
发送功能 :将响应字符串与客户端的IP地址与端口号构造成一个新的数据报进行发送。
// 服务器
public class UdpEchoServer {
// 需要先定义一个socket对象
// 网络通信,必须要使用socket对象
private DatagramSocket socket = null;
// 绑定一个端口号不一定会成功
// 如果该端口号已被别的进程占用或绑定,此时的绑定操作就会抛出异常
// 同一个主机,一个端口号,同一时刻,只能被一个进程绑定
public UdpEchoServer(int port) throws SocketException {
// 构造socket的同时指定要关联/绑定的端口号
// socket对象==遥控器,与网卡进行交互的遥控器
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);
System.out.println("客户端上线");
// 为了方便处理请求,将请求转换为字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
// 2.根据请求计算响应
String response = process(request);
// 3.把响应结果写回到客户端
// 根据response字符串构造一个数据报
// 和请求 packet 不同, 此处构造响应数据报的时候, 需要指定这个包要发给谁.
DatagramPacket responsePacker = new DatagramPacket(response.getBytes(),response.getBytes().length,
// requestPacket 是从客户端这里收来的. getSocketAddress 就会得到客户端的 ip 和 端口
requestPacket.getSocketAddress());
// 发送到客户端
socket.send(responsePacker);
// 分别打印:ip,端口号,请求内容,响应内容
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
// 这个方法是根据请求计算响应
// 有具体业务,就写相关逻辑
// 这是服务器中的重要环节
public String process(String request) {
// 测试客户端与服务器是否网络通信成功
// 并无逻辑需求
// 直接将请求作为响应返回
// 证明通信成功
return request;
}
// 运行main方法,启动服务器
public static void main(String[] args) throws IOException {
// 实例化服务器对象
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
// 启动主逻辑
udpEchoServer.start();
}
}
客户端
客户端大致就分为三个功能。
1️⃣ 读取用户输入的请求
2️⃣ 将请求转换为UDP数据报并发送至服务器
3️⃣ 尝试读取服务器的响应
4️⃣ 将响应转换为字符串并打印
下面代码中一步一步实现了这四个功能,并配有详细的注释帮你快速理解
核心思路:
核心成员属性DatagramSocket socket。
与服务器的思路大致相同,不过客户端的构造方法中要指明服务器的IP地址与端口号。
// 客户端
public class UdpEchoClient {
// 需要先定义一个socket对象
// 网络通信,必须要使用socket对象
private DatagramSocket socket = null;
private String serverIP;// 服务器IP
private int serverPort;// 服务器端口号
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
// 对于客户端来说, 不需要显示关联端口.
// 不代表没有端口, 而是系统自动分配了个空闲的端口.
this.socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
// 客户端主逻辑
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
// 通过这个客户端可以多次和服务器进行交互.
while (true) {
// 1. 先从控制台, 读取一个字符串过来
// 先打印一个提示符, 提示用户要输入内容
System.out.print("-> ");
String request = scanner.next();
// 2. 把字符串构造成 UDP packet, 并进行发送.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
// 数据报的目的IP与目的端口号
InetAddress.getByName(serverIP),serverPort);
// InetAddress.getByName这个静态方法可以将代表IP的字符串转换为IP
// 向服务器发送数据报
socket.send(requestPacket);
// 3. 客户端尝试读取服务器返回的响应
// 构造一个空的数据报
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
// 从网卡中读取数据存入 手动构造的空的数据报
// 如果网卡中没有数据则会阻塞等待
socket.receive(responsePacket);
// 4. 把响应数据转换成 String 显示出来.
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
// 运行main方法,启动客户端
public static void main(String[] args) throws IOException {
// 实例化客户端对象
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
// 启动客户端主逻辑
udpEchoClient.start();
}
}
通信结果:
疑惑解答
为什么服务器进程需要手动指定端口号而客户端进程不需要
服务器的功能是用来处理其他客户端发来的请求,因此需要为客户端提供自己的端口号,方便客户端进行访问。
虽然服务器要给客户端一个响应,但是客户端的IP地址与端口号都可以在客户端发来请求的数据报中获得,因此客户端不需要手动指定端口号
为什么用空的数据报接收网卡中的数据
一般情况下,一般是使用 参数 作为 “输入”,用返回值作为方法的 “输出”。
而用来读取数据的reveve方法却没有返回值。
因此需要传入一个引用类型的参数,通过这个方法改变这个参数,此时这个参数就是返回值,这个参数也被称为 输出型参数 。
因此这个方法就是 以参数为返回值 。
为什么客户端中的服务器IP与端口号是"127.0.0.1" 与 9090
127.0.0.1 是主机环回地址。主机环回是指地址为 127.0.0.1 的任何数据包都不应该离开计算机(主机),发送它——而不是被发送到本地网络或互联网,它只是被自己“环回”,并且发送数据包的计算机成为接收者。
端口号是9090是因为是随意指定的,当然也有一些特殊端口号被指定分配给了一些牛逼的程序。
为什么客户端与服务器都没有调用close方法
DatagramSocket对象也会产生文件描述符,如果如果文件描述符表满了会产生文件资源泄露的严重bug,那么为什么此处没有调用close方法???
原因是,无论客户端进程还是服务器进程, 在他的生命周期中,只需要用到一个DatagramSocket对象 ,而他需要调用close方法的时候也正是他进程结束的时候,此时会自动释放对象也就相当于调用了close方法,因此可以手动调用close方法,但是也可以不调用。
总结
以上就是今天要分享的内容,本文介绍了Socket套接字,以及使用UDP协议的特点以及UDP数据报套接字实现的客户端与服务器。网络编程让我愈发感觉到了编程的魅力,也让我领略到了科技的神奇。各位加油!
路漫漫不止修身,也养性。