网络编程-UDP套接字
文章目录
- UDP/TCP协议简介
- 两种协议的联系与区别
- Socket是什么
- UDP的SocketAPI
- DatagramSocket
- DatagramPacket
- 使用UDP模拟通信
- 服务器端
- 客户端
- 测试
- 完整测试代码
UDP/TCP协议简介
两种协议的联系与区别
TCP和UDP其实是传输层的两个协议的内容, 差别非常大, 对于我们的Java来说, JVM对操作系统提供的关于网络的 API 进行了封装, 提供了两套的API
下面是网络连接中的一些特点
- 有/无连接: 抽象的概念, 虚拟的, 逻辑上的连接, 而不是物理的连接, 其实就是看, 在网络通信的过程中, 是否保存了对端的一些信息, 比如说IP, 端口号之类的
- 可靠传输/不可靠传输: 网络传输的过程中, 传输的信息是十分容易丢失的, 不可能100%的到达, 这里说的可靠传输还是不可靠传输是指的是, 尽可能的到达, 可靠传输, 发送消息之后, 会尽可能的提高传输的成功率, 如果出现了丢包的问题, 对面也能感知到, 但是对于不可靠传输, 发送消息之后就不管了, 只是简单的发送了数据
- 面向字节流/数据报: 指的是传输的方式, 有的协议使用字节流进行内容的传输, 容易粘包, 支持任意长度, 有的协议使用数据报进行内容的传输, 不存在粘包, 但是有长度限制
- 全双工/半双工: 一个通信的链路, 支持双向的通信, 能读, 也能写, 但是有的通信的协议只支持单向的通信, 要么读, 要么写
下面是 UDP 协议和 TCP 协议的特点的声明
UDP | TCP |
---|---|
无连接 | 有连接 |
不可靠传输 | 可靠传输 |
面向数据报 | 面向字节流 |
全双工 | 全双工 |
Socket是什么
可以理解为是一个网卡的代言人, 在计算机中来说, 文件其实是一种广义的概念, 网卡我们也抽象为一种Socket文件, 所以操作网卡的流程中, 是与文件的操作是差不多的, 对网卡的操作, 其实是对Socket这种文件类型的操作, 也会占用文件操作符表(文件操作中的一种资源), 所以也要及时关闭
- 打开 -> 读写 -> 关闭
UDP的SocketAPI
DatagramSocket
上面我们说过, 每一种套接字都有自己的一套 API, 而UDP的操作网卡的 API 就是 DatagramSocket
常见的构造方法
上图的两个构造方法是我们常用的两个方法
- 第一个是不带端口号的版本, 所以定义之后, 会给当前的程序随机分配一个端口号(一般用于客户端)
- 第二个参数是给一个指定的端口号(一般用于服务器端)
- 如果一台服务器上有多个UDP程序使用同一个端口号, 那就会出现问题, 端口号冲突, 但是如果同一台计算机上不同协议的程序使用同一个端口号不会冲突, 比如一个UDP程序使用端口号9090, 另一个TCP程序也使用9090, 这种情况就不会冲突
常用的方法
send
方法是发送构造好的DatagramPacket
对象(其实就是数据报), receive
是一种输出型函数的机制, 通常是传入一个空的DatagramPacket
对象, 然后把接收到的内容填入到这个对象内部, 如果没有客户端发送数据, 该方法就会陷入阻塞等待阶段
close
方法, 关闭该套接字
观察这个类的继承结构
该类继承了AutoCloseable
接口, 所以也支持try-with-resource
机制
DatagramPacket
该类本质上是一个数据报
常见的构造方法
我们在之前就说过, UDP是一种无连接的协议, 也就是在网卡层面是不保存对端的信息的, 那我们要如果知道数据发送给哪一台机器呢 ? 实质上就是通过DatagramPacket
来实现的, 这个数据报通常保存了对端的信息, 而传输的内容是通过字符数组来保存的, InetAddress
其实是IP地址的信息
, port
是对端的端口号, SocketAddress
可以理解为是InetAddress和port
的结合, 里面既有IP信息
还有端口号信息
常见的方法
注意:
对于一个数据报对象来说, 里面存储的地址的信息, 不仅包含接收方的地址信息
, 还保存着发送方的地址信息
, 所以想要在服务器端做出响应的时候, 对于发送的地址, 是从接收到的DatagramPacket
对象中获取到的, 因为里面也保存了客户端的地址信息
getAddress
获取的是IP地址, 既可以是发送端的, 也可以是接收端的getPort
获取的是端口号, 同上getSocketAddress
获取的是完整的地址信息, 同上getLength
获取的是接收到的数据的真实长度(以字节计)
比如下面的代码
这种情况下返回的就是发送端的地址信息
在比如服务器给客户端返回结果的时候, 使用接收到的DatagramPacket
对象的getAddress, getPort, getSocketAddress
方法, 此时得到的就是发送方(也就是客户端)的地址信息
所以, 获取到的是哪一端的地址信息要看实际的情况
使用UDP模拟通信
关于计算机通信的机制, 我们之前的版块涉及到一点, 大致流程如下
服务器端
写一个执行翻译的服务器
创建网卡还有构造方法
// 创建一个网卡对象
private DatagramSocket serverSocket = null;
// 构造方法(服务器端固定端口号)
public UdpServer(int port) throws SocketException {
serverSocket = new DatagramSocket(port);
}
start方法, 启动服务器, 不断接收用户的请求, 处理并响应
这里我们只是简单模拟一下, 真实的业务场景中, 这里的逻辑是相当相当复杂的, 所以处理时间可能会很长, 所以如果此时有别的客户端想请求的话, 那就有可能得不到及时的响应, 所以我们此时可以采用多线程的技术, 使用线程池来优化, 具体代码我们最后的完整代码会给出
// start方法开启服务器
public void start() throws IOException {
// 记录日志, UDP 服务器上线
System.out.println("UDP服务器上线");
// 使用while循环不断接收客户端的请求
while(true){
// 1. 读取请求(使用一个空数据报来接收客户端数据, 输出型函数)
DatagramPacket request = new DatagramPacket(new byte[4096], 0, 4096);
serverSocket.receive(request);
// 2. 解析请求并处理
String req = new String(request.getData(), 0, request.getLength());
String resp = process(req);
// 3. 返回响应(发送处理的结果)
DatagramPacket responce = new DatagramPacket(resp.getBytes(), 0, resp.getBytes().length,
request.getSocketAddress());
serverSocket.send(responce);
// 4. 记录日志信息
System.out.printf("[%s, %d] req:%s resp:%s\n",
request.getAddress().toString(), request.getPort(), req, resp);
}
}
处理的核心逻辑
// 对请求处理的逻辑
private static Map<String, String> chineseToEnglish = new HashMap<>();
static {
chineseToEnglish.put("小猫", "cat");
chineseToEnglish.put("小狗", "dog");
chineseToEnglish.put("小鹿", "fawn");
chineseToEnglish.put("小鸟", "bird");
}
private String process(String req){
return chineseToEnglish.getOrDefault(req, "未收录该词条");
}
客户端
关于客户端其实和服务器端差不多, 也是发送请求和接收响应的逻辑
创建网卡, 构造方法, 还有创建变量来保存对端的地址信息(构造数据报使用)
// 创建网卡
private DatagramSocket clientSocket = null;
// 创建变量保存对端信息
private InetAddress serverInet = null;
private int serverPort = 0;
// 构造方法(客户端一般是随机的端口号)
public UdpClient(String iNetAddr, int serverPort) throws UnknownHostException, SocketException {
this.serverPort = serverPort;
this.serverInet = InetAddress.getByName(iNetAddr);
clientSocket = new DatagramSocket();
}
start方法, 请求并响应
while(sc.hasNext()){
// 1. 输入并发送请求
String req = sc.next();
DatagramPacket request = new DatagramPacket(req.getBytes(), 0, req.getBytes().length, serverInet, serverPort);
clientSocket.send(request);
// 2. 等待请求响应
DatagramPacket responce = new DatagramPacket(new byte[4096], 0, 4096);
clientSocket.receive(responce);
String resp = new String(responce.getData(), 0, responce.getLength());
// 3. 输出响应结果
System.out.println(resp);
}
测试
下面是上面的代码的运行测试截图
完整测试代码
客户端
package net_demo1.net_demo03;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpClient {
// 创建网卡
private DatagramSocket clientSocket = null;
// 创建变量保存对端信息
private InetAddress serverInet = null;
private int serverPort = 0;
// 构造方法(客户端一般是随机的端口号)
public UdpClient(String iNetAddr, int serverPort) throws UnknownHostException, SocketException {
this.serverPort = serverPort;
this.serverInet = InetAddress.getByName(iNetAddr);
clientSocket = new DatagramSocket();
}
// start方法, 启动客户端
public void start() throws IOException {
// 创建一个Scanner对象接收用户输入
Scanner sc = new Scanner(System.in);
// 使用while循环来请求并接收响应
while(sc.hasNext()){
// 1. 输入并发送请求
String req = sc.next();
DatagramPacket request = new DatagramPacket(req.getBytes(), 0, req.getBytes().length, serverInet, serverPort);
clientSocket.send(request);
// 2. 等待请求响应
DatagramPacket responce = new DatagramPacket(new byte[4096], 0, 4096);
clientSocket.receive(responce);
String resp = new String(responce.getData(), 0, responce.getLength());
// 3. 输出响应结果
System.out.println(resp);
}
}
public static void main(String[] args) throws IOException {
UdpClient udpClient = new UdpClient("127.0.0.1", 9090);
udpClient.start();
}
}
服务器端
package net_demo1.net_demo03;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class UdpServer {
// 创建一个网卡对象
private DatagramSocket serverSocket = null;
// 构造方法(服务器端固定端口号)
public UdpServer(int port) throws SocketException {
serverSocket = new DatagramSocket(port);
}
// start方法开启服务器
public void start() throws IOException {
// 记录日志, UDP 服务器上线
System.out.println("UDP服务器上线");
// 创建一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 使用while循环不断接收客户端的请求
while (true) {
// 1. 读取请求(使用一个空数据报来接收客户端数据, 输出型函数)
DatagramPacket request = new DatagramPacket(new byte[4096], 0, 4096);
serverSocket.receive(request);
// 这里的改进, 由于我们处理的时间可能会很长, 如果此时有别的客户端也请求了, 那就可能造成数据丢失
// 所以我们使用线程池的技术, 通过多线程来执行任务
executorService.execute(() -> {
String req = new String(request.getData(), 0, request.getLength());
String resp = process(req);
// 3. 返回响应(发送处理的结果)
DatagramPacket responce = new DatagramPacket(resp.getBytes(), 0, resp.getBytes().length,
request.getSocketAddress());
try {
serverSocket.send(responce);
} catch (IOException e) {
e.printStackTrace();
}
// 4. 记录日志信息
System.out.printf("[%s, %d] req:%s resp:%s\n",
request.getAddress().toString(), request.getPort(), req, resp);
});
}
}
// 对请求处理的逻辑
private static Map<String, String> chineseToEnglish = new HashMap<>();
static {
chineseToEnglish.put("小猫", "cat");
chineseToEnglish.put("小狗", "dog");
chineseToEnglish.put("小鹿", "fawn");
chineseToEnglish.put("小鸟", "bird");
}
private String process(String req) {
return chineseToEnglish.getOrDefault(req, "未收录该词条");
}
public static void main(String[] args) throws IOException {
UdpServer udpServer = new UdpServer(9090);
udpServer.start();
}
}