网络编程
简述
网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。
java.net
包中 J2SE 的 API 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。
java.net
包中提供了两种常见的网络协议的支持:
- TCP:TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP 层是位于 IP 层之上,应用层之下的中间层。TCP 保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。
- UDP:UDP位于 OSI 模型的传输层,一个无连接的协议。提供了应用程序之间要发送数据的数据报。由于UDP缺乏可靠性且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包。
实现网络编程的三要素
- **IP地址:**设备在网络中的地址,是唯一的标识。
- **端口号:**应用程序在设备中唯一的标识。
- **网络通信协议:**数据在网络中传输的规则,常见的协议有UDP协议和TCP协议。
IP地址
IP地址是设备在网络中的地址,是唯一的标识。
IP地址分为公网地址和私有地址(局域网使用)
192.168.0.0 - 192.168.255.255
就是局域网地址,专为组织机构内部使用。
常用命令(运行在cmd)
ipconfig
:查看本机IP地址
ping IP地址/域名
:检查是否与目标地址/网站连通
特殊IP地址
127.0.0.1
或 localhost
:本地回环地址,在本机中查询
端口号
端口号标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围是0~65535。
端口类型
- 周知端口: 0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
- 注册端口: 1024~49151,分配给用户进程或某些应用程序。(如:Tomcat占用8080,MySQL占用3306)
- 动态端口: 49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样
网络协议
网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则
各层及部分协议
传输层的2个常见协议
TCP | UDP |
---|---|
面向连接的可靠传输协议。传输前会采用“三次握手”方式建立连接,传输完毕采用“四次挥手”断开连接。 | 无连接、不可靠传输协议。发送方不管接收方是否准备好,接收方收到数据也不确认。 |
在连接中可进行大数据量的传输。 | 每个由数据源IP、目的地IP和目的地端口封装成数据报的大小限制在64KB内。 |
通信效率相对较低。连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接。 | 发送数据结束时无需释放资源,开销小,速度快。 |
不可以广播发送 | 可以广播发送 |
InetAddress
InetAddress
类用于代表Internet协议(IP)地址,内部封装了关于操作IP地址的API。
API
常用方法 | 说明 |
---|---|
static InetAddress getLocalHost () | 获取本机的IP地址对象 |
static InetAddress getByName (String host) | 获取指定主机的IP地址对象,参数可以是IP或域名 |
String getHostName () | 获取该IP地址的主机名 |
String getHostAddress () | 以字符串形式返回IP地址 |
boolean isReachable (int timeout) | 在指定毫秒内尝试连通该IP地址,返回值表示是否连通 |
TCP协议
简述
TCP(传输控制协议)是一种面向连接的可靠传输协议。传输前会采用“三次握手”方式建立连接,传输完毕采用“四次挥手”断开连接。
客户端向服务端发送请求,需要借助字节输出流;服务端接收客户端的数据,需要借助字节输入流。反之亦然。
只要是使用
java.net.Socket
类实现的网络通信,底层就是TCP协议。
Socket
套接字(Socket)是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议把数据传输到网络。
Socket、TCP和部分IP的功能都是由操作系统提供的,Java只是提供了对操作系统调用的简单的封装。
Socket
是由IP地址和端口号组成。
为什么需要Socket进行网络通信?
因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。
Socket一方关闭或者出现异常,另一方也会关闭或者出现异常
使用Socket进行网络编程的本质
使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。
因此,当Socket连接成功地在服务器端和客户端之间建立后:
- 对服务器端来说,它的Socket是指定的IP地址和指定的端口号;
- 对客户端来说,它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。
API
构造方法 | 说明 |
---|---|
Socket (String host, int port) | 指定服务端ip和端口创建客户端对象 |
常用方法 | 说明 |
---|---|
OutputStream getOutputStream () | 获取字节输出流对象发送数据 |
InputStream getInputStream () | 获取字节输入流对象接收数据 |
ServerSocket
Java标准库提供了ServerSocket
来实现对指定IP和指定端口的监听,即服务端。
API
构造方法 | 说明 |
---|---|
ServerSocket (int port) | 创建指定端口的服务端对象 |
常用方法 | 说明 |
---|---|
Socket accept () | 等待接收客户端的Socket通信连接。 |
OutputStream getOutputStream () | 获取字节输出流对象发送数据 |
InputStream getInputStream () | 获取字节输入流对象接收数据 |
accept
方法连接成功后会返回Socket
对象与客户端进行端到端通信。
TCP通信演示
- 客户端使用
Socket(InetAddress, port)
连接服务器; - 服务器端用
ServerSocket
监听指定端口; - 服务器端用
accept()
接收连接并返回Socket
; - 双方通过
Socket
打开InputStream
/OutputStream
读写数据; - 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
flush()
用于强制输出缓冲区到网络。
客户端(发送端)
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
try (OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream()) {
//把字节流交给 operation 方法处理收发数据
operation(os, is);
} catch (IOException e) {
e.printStackTrace();
}
socket.close();
System.out.println("已断开会话");
}
private static void operation(OutputStream os, InputStream is) throws IOException {
//将普通字节流包装成高级流,提高效率
PrintStream ps = new PrintStream(os);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
Scanner sc = new Scanner(System.in);
while (true) {
//发送数据
System.out.print(">>> ");
String line = sc.nextLine();
ps.println(line);
ps.flush();
//接收数据
String s = br.readLine();
System.out.println("<<< " + s);
if ("exit".equals(s)) {
break;
}
}
}
}
服务端(接收端)
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8888);
//主线程负责接收客户端Socket通信请求
while (true) {
//等待接收客户端的Socket通信连接。
Socket socket = ss.accept();
System.out.println(socket.getRemoteSocketAddress() + " 已连接会话");
//子线程负责处理客户端Socket通信连接请求,从而同时接收多个客户端
new ServerThread(socket).start();
}
}
}
//线程类
class ServerThread extends Thread {
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (OutputStream os = this.socket.getOutputStream();
InputStream is = this.socket.getInputStream();) {
operation(socket, os, is);
} catch (IOException e) {
//当客户端Socket管道关闭时,服务端这边会抛出异常。可以利用这一点跟踪客户端断开时间。
System.out.println(socket.getRemoteSocketAddress() + " 已断开会话");
try {
this.socket.close();
} catch (IOException ee) {
}
}
}
private static void operation(Socket socket, OutputStream os, InputStream is) throws IOException {
//将普通字节流包装成高级流,提高效率
PrintStream ps = new PrintStream(os);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while (true) {
//接收数据
String s = br.readLine();
System.out.println("来自 " + socket.getRemoteSocketAddress() + " 的消息:" + s);
//发送确认数据
if ("exit".equals(s)) {
ps.println("exit");
ps.flush();
break;
}
ps.println("已收到:" + s);
ps.flush();
}
}
}
运行测试
客户端终止后:
为什么写入网络数据时,要调用
flush()
方法?如果不调用
flush()
,我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()
强制把缓冲区数据发送出去。
使用线程池优化
上述代码逻辑创建的服务端是不好的:每有一个客户端请求,服务端就要重新创建一个线程,而创建新线程的开销很大,严重影响了系统的性能,过多的线程可能会导致系统崩溃。对此,我们可以使用线程池限制线程的数量。
只适合客户端通信时常较短的场景
更改服务端代码
public class Server {
//定义一个线程池:3个核心线程,5个最大线程,闲置时间2s,队列长度3,默认线程工厂,默认拒绝策略
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8888);
while (true) {
Socket socket = ss.accept();
System.out.println(socket.getRemoteSocketAddress() + " 已连接会话");
//创建处理客户端Socket通信连接请求的Runnable任务,交给线程池管理
pool.execute(new ServerRunnable(socket));
}
}
}
//Runnable任务类
class ServerRunnable implements Runnable {
private Socket socket;
public ServerRunnable(Socket socket) {
this.socket = socket;
}
// ...
//与线程类 run 方法一致
}
即时通信
上述代码只能将数据由客户端发送到服务端,这不符合业务场景。即时通信可以将数据由客户端发送到客户端。
即时通讯(Instant messaging,简称IM)是一个终端服务,允许两人或多人使用网路即时的传递文字讯息、档案、语音与视频交流。
即时通信需要使用端口转发的设计思想。
步骤
- 客户端用单独的线程来接收数据
- 服务端用一个集合来维护所有在线的客户端
- 服务端需要将收到的数据推送给客户端
客户端
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
//启动一个线程:用于接收消息
new ClientThread(socket).start();
try (OutputStream os = socket.getOutputStream()) {
writeMsg(os);
} catch (IOException e) {
e.printStackTrace();
}
socket.close();
System.out.println("已断开会话");
}
//发送数据
private static void writeMsg(OutputStream os) throws IOException {
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print(">>> ");
String line = sc.nextLine();
ps.println(line);
ps.flush();
}
}
}
//用于“收消息”的线程
class ClientThread extends Thread {
private Socket socket;
public ClientThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new InputStreamReader(this.socket.getInputStream()))) {
readerMsg(br);
} catch (IOException e) {
e.printStackTrace();
}
}
private void readerMsg(BufferedReader br) throws IOException {
while (true) {
String s = br.readLine();
System.out.println("<<< " + s);
}
}
}
服务端
public class Server {
//线程池:3个核心线程,5个最大线程,闲置时间2s,队列长度3,默认线程工厂,默认拒绝策略
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//集合:用于维护所有在线的客户端
public static ArrayList<Socket> allClient = new ArrayList<>();
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8888);
while (true) {
Socket socket = ss.accept();
//将在线的客户端添加到集合
allClient.add(socket);
System.out.println(socket.getRemoteSocketAddress() + " 已连接会话");
//创建处理客户端Socket通信连接请求的Runnable任务,交给线程池管理
pool.execute(new ServerRunnable(socket));
}
}
}
//Runnable任务类
class ServerRunnable implements Runnable {
private Socket socket;
public ServerRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (InputStream is = this.socket.getInputStream()) {
operation(socket, is);
} catch (IOException e) {
//将离线的客户端移除集合
Server.allClient.remove(socket);
System.out.println(socket.getRemoteSocketAddress() + " 已断开会话");
try {
this.socket.close();
} catch (IOException ee) {
ee.printStackTrace();
}
}
}
private static void operation(Socket socket, InputStream is) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while (true) {
//接收数据
String s = br.readLine();
System.out.println("来自 " + socket.getRemoteSocketAddress() + " 的消息:" + s);
//转发数据(群发)
for (Socket sock : Server.allClient) {
//不发给自己
if (sock.equals(socket)) {
continue;
}
PrintStream ps = new PrintStream(sock.getOutputStream());
ps.println(s);
ps.flush();
}
}
}
}
B/S
BS(Browser/Server,浏览器/服务器模式),web应用,可以实现跨平台,客户端零维护,但是个性化能力低,响应速度较慢。
与之对应的CS(Client/Server,客户端/服务器模式),桌面应用,响应速度快,安全性强,个性化能力强,响应数据较快。
优点
- 分布性强,客户端零维护。只要有网络、浏览器可以随时随地进行操作。
- 业务扩展简单方便,通过增加网页就可以实现增加功能。
- 维护方便,通过修改网页即可实现所有用户的更新。
- 开发简单,共享性强。
缺点
- 个性化弱,个性化定制差。因为基于浏览器
- 跨浏览器差。
- B/S的交互方式是请求→响应,需要动态刷新页面,响应数据考虑到网络问题。后台数据压力大。
- 安全性和速度上需要进行特定优化
- 功能不及传统模式
服务器必须给浏览器响应的HTTP协议格式的数据,否则浏览器不识别
服务器
// BS架构的服务器
public class BSServer {
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(9999);
System.out.println("服务器已启动...");
while (true) {
//将浏览器Socket请求封装成Runnable任务,交给线程池处理
Socket socket = ss.accept();
pool.execute(new BSRunnable(socket));
}
}
}
class BSRunnable implements Runnable {
private Socket socket;
public BSRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//到这里浏览器已经与服务器建立了Socket通信
try {
//将信息显示到浏览器上
PrintStream ps = new PrintStream(socket.getOutputStream());
//打印HTTP协议格式
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println();
//展示的信息
ps.println("HelloWorld");
ps.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
启动服务器
在浏览器输入 127.0.0.1:9999
UDP协议
简述
UDP(用户数据报协议)是一种无连接、不可靠传输协议。发送方不管接收方是否准备好,接收方收到数据也不确认。
和TCP编程相比,UDP编程就简单得多,因为UDP没有创建连接,数据包也是一次收发一个,所以没有流的概念。
在Java中使用UDP编程,仍然需要使用Socket
,因为应用程序在使用UDP时必须指定网络接口(IP)和端口号。注意:UDP端口和TCP端口虽然都使用0~65535,但他们是两套独立的端口,即一个应用程序用TCP占用了端口1234,不影响另一个应用程序用UDP占用端口1234。
举例
在火锅店中,厨师负责在后台做火锅料,服务员负责上火锅料,顾客点菜。厨师做好火锅后,不会把自己的锅交给顾客,而是用顾客那里的锅来接收火锅料。
在上述中:
厨师(发送端)做好菜后不会告诉顾客“菜做好了”(无连接)。
服务员上菜的过程中,可能会出现意外把菜给撒了(不可靠传输)。
顾客(接收端)收到菜后,也不会告诉厨师“我收到菜了“(无连接)。
火锅就是数据。
厨师和顾客的锅就是数据报对象(用于存放数据)。
DatagramPacket
DatagramPacket
类 代表数据包
API
DatagramPacket构造方法 | 说明 |
---|---|
DatagramPacket (byte[] buf, int length,InetAddress address, int port) | 创建发送端数据报对象。buf:要发送的内容。length:要发送内容的字节长度。address:接收端的IP地址对象。port:接收端的端口号 |
DatagramPacket (byte[] buf,int length) | 创建发送端/接收端数据报对象。buf:用来存储接收的内容。length:能够接收内容的长度 |
DatagramPacket常用方法 | 说明 |
---|---|
InetAddress getAddress () | 返回发送此数据报或接收数据报的IP地址 |
int getOffset () | 获取要发送的数据长度或接收的数据在缓冲区的起始位置 |
int getLength () | 获取要发送的数据长度或接收的数据的长度 |
byte[] getData () | 获取接收或发送数据的数据缓冲区 |
void setData (byte[] buf) | 设置此数据包的数据缓冲区。数据包的长度设置为缓冲区长度。 |
DatagramSocket
DatagramSocket
类 代表发送端或接收端
API
DatagramSocket构造方法 | 说明 |
---|---|
DatagramSocket () | 创建发送端对象,随机分配端口号 |
DatagramSocket (int port) | 创建接收端对象,监听指定端口号 |
DatagramSocket常用方法 | 说明 |
---|---|
void send (DatagramPacket dp) | 发送数据报 |
void receive (DatagramPacket p) | 接收数据报 |
void disconnect () | 断开此UDP数据报套接字与远程主机的连接。在未连接的套接字上调用此方法不会执行任何操作。 |
void setSoTimeout (int timeout) | 设置接收数据包时的最大等待时间 |
void connect (InetAddress address, int port) | 将套接字连接到远程地址,以便此套接字从该地址发送或接收数据包。 |
其他补充
方法
disconnect()
不是真正地断开连接,它只是清除了客户端DatagramSocket
实例记录的远程服务器地址和端口号,这样,DatagramSocket
实例就可以连接另一个服务器端。
使用方法
setSoTimeout(int timeout)
给客户端设定等待时间。如果不设置,在没有收到UDP包时,客户端会无限等待下去。服务器端可以无限等待,因为它本来就被设计成长时间运行。
UDP是无连接的协议,为什么会有
connect()
方法?
connect()
方法不是真正意义上的连接,它是为了在客户端的DatagramSocket
实例中保存服务器端的IP和端口号, ,不能往其他地址和端口发送。这不是UDP的限制,而是Java内置了安全检查。
UDP通信演示
- 客户端使用
DatagramSocket.connect()
指定远程地址和端口; - 服务器端用
DatagramSocket(port)
监听端口; - 双方通过
receive()
和send()
读写数据; DatagramSocket
没有IO流接口,数据被直接写入byte[]
缓冲区。
客户端(发送端)
public class Client {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
//设置客户端接收服务端数据包时的最大等待时间
ds.setSoTimeout(1000);
//确保客户端实例只能往指定的地址和端口发送UDP包
ds.connect(InetAddress.getByName("localhost"), 8888);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print(":");
String line = sc.nextLine();
if ("exit".equals(line)) {
System.out.println("已断开会话");
ds.disconnect();
break;
}
//发送数据
byte[] data = line.getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
//接收数据
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
String s = new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println("来自 " + packet.getSocketAddress() + " 的消息:" + s);
}
}
}
服务端(接收端)
public class Server {
public static void main(String[] args) throws IOException {
//监听指定端口
DatagramSocket ds = new DatagramSocket(8888);
//无限循环等待接收数据
while (true) {
byte[] buffer = new byte[1024]; // 数据缓冲区
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
//接收一个UDP数据包
ds.receive(packet);
//将数据包按UTF-8编码转换为String。
//收取到的数据存储在buffer中,由packet.getData()获取接收或发送数据的数据缓冲区;packet.getOffset(), packet.getLength()指定起始位置和长度
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
System.out.println("来自 " + packet.getSocketAddress() + " 的消息:" + s);
//向发送者发送确认包(ACK)
byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
packet.setData(data);
ds.send(packet);
}
}
}
运行演示
那么如何让一个类并行?
按照如下设置:
应用即可
广播
在同一网络可达范围内,发送方向本网络内所有设备进行通信就是广播。
广播地址: 255.255.255.255
一般步骤
- 发送端发送的数据报的目的地写的是广播地址、且指定端口。
- 发送端所在网段的其他主机的程序只要匹配端口就可以收到消息。
代码演示
发送端(客户端)
...
// 以广播地址创建数据报对象
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("255.255.255.255"), 8888);
...
// 除创建数据报对象代码更改,其余代码与“多发多收”代码无异
只要是与发送端在同一网段的主机,端口号指定为 8888 均可接收广播消息。(我只有一台主机,无法演示)
组播
在同一网络可达范围内,发送方与向特定的一部分接收方进行通信就是组播。
组播地址: 224.0.0.0 ~ 239.255.255.255
,其中 224.0.0.0
是保留地址不能使用。
一般步骤
- 将发送端的数据报指定为组播发送(例如IP:224.0.0.1,端口:8888)。
- 接收端加入组播组中。
DatagramSocket
的子类MulticastSocket
中有API可以让接收端加入组播组:使用
MulticastSocket
构造创建接收端对象,调用joinGroup
方法就可以加入组播组。
MulticastSocket方法 | 说明 |
---|---|
void joinGroup (InteAddress groupAddr) | 加入指定组播组(JDK14已过时) |
void joinGroup (SocketAddress mcastaddr, NetworkInterface netIf) | 指定组播组,指定接收组播数据报的本地接口 |
代码演示
发送端(客户端)
...
// 以组播地址创建数据报对象
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("224.0.0.1"), 8888);
...
// 除创建数据报对象代码更改,其余代码与“多发多收”代码无异
接收端(服务端)
public class Server {
public static void main(String[] args) throws IOException {
//1.创建MulticastSocket接收端对象
MulticastSocket ms = new MulticastSocket(8888);
//2.加入组播组
// ms.joinGroup(InetAddress.getByName("224.0.0.1")); //已过时
ms.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.0.1"), 8888), NetworkInterface.getByInetAddress(InetAddress.getLocalHost()));
//3.接收数据
while (true) {
byte[] bytes = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
ms.receive(packet);
int length = packet.getLength();
String s = new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println("来自 " + packet.getSocketAddress() + " 的消息:" + s);
byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
packet.setData(data);
ds.send(packet);
}
}
}