一、Socket编程
1、简介
Socket(套接字)是一种抽象概念,它用于在不同计算机之间进行通信。
2、Java中的套接字编程
Socket类与IO类常常结合使用。通过Socket类建立网络连接后,可以使用它提供的输入输出流对象来进行数据的读取和写入。
3、Socket和TCP/UDP的关系
Socket是实现网络编程的工具,而TCP/UDP是网络传输协议。
Socket编程常用于实现基于TCP或UDP的网络通信。TCP和UDP是在传输层上的协议,而Socket是在应用层与传输层之间的一个接口。通过Socket,应用程序可以使用TCP或UDP协议与其他计算机进行通信。
在Java中,可以使用Socket类来创建TCP连接,也可以使用DatagramSocket类来创建UDP连接。通过Socket类和DatagramSocket类,Java程序员可以方便地实现TCP和UDP通信,并进行数据传输。
4、聊天室案例
Socket编程
- Socket:用于建立网络连接;在连接成功时,应用程序两端都会产生一个Socket实例
ServerSocket:用于服务端,主要完成两个工作:
1、向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立连接。
2、监听端口,一旦一个客户端建立连接,会立即返回一个Socket。通过这个Socket就可以和该客户端交互了。
服务器端:
/**
* @document: 聊天室V01:服务器端
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
//创建一个ServerSocket对象,建立通信服务
private ServerSocket serverSocket;
/**
* 初始化服务端
*/
public Server() {
try {
System.out.println("正在启动服务器...");
/*
实例化ServerSocket,在new的时候需要使用端口号
端口号是一个数字,取值范围是0~65535,使用时需避免常用端口
一般是6000以前不要使用
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务器启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器开始工作的方法
*/
public void start() {
//总机的通话设备
try {
System.out.println("等待客户端连接...");
//serverSocket.accept()这句话会造成阻塞,会等待客户端连接
//直到一个客户端发来了连接 此时accept()方法会返回一个Socket实例
//通过Socket实例就可以和别的Socket通信了
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接上了!"); //如果没有人连,这句话不会打印
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
客户端:
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
public class Client {
// 声明客户端工具
private Socket socket;
//初始化客户端
public Client() {
try {
System.out.println("正在连接服务器端...");
/*
实例化客户端工具(Socket对象)
*/
/*
参数1:要连接的服务器地址,可以是域名或Ip地址
参数2:服务器开放的端口号
*/
socket = new Socket("localhost", 8088);
System.out.println("与服务器建立了连接");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
}
}
获取网络输入流和网络输出流
使用 Socket 通讯时,可以通过 Socket 获取输入流与输出流,从而实现数据信息的交互。
- InputStream getInputStream():该方法用于返回此套接字的输入流
- OutputStream getOutputStream():该方法用于返回此套接字的输出流
服务端:
/**
* @document: 聊天室V02:服务器端
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
//创建一个ServerSocket对象,建立通信服务
private ServerSocket serverSocket;
/**
* 初始化服务端
*/
public Server() {
try {
System.out.println("正在启动服务器...");
/*
实例化ServerSocket,在new的时候需要使用端口号
端口号是一个数字,取值范围是0~65535,使用时需避免常用端口
一般是6000以前不要使用
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务器启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器开始工作的方法
*/
public void start() {
//总机的通话设备
try {
System.out.println("等待客户端连接...");
//serverSocket.accept()这句话会造成阻塞,会等待客户端连接
//直到一个客户端发来了连接 此时accept()方法会返回一个Socket实例
//通过Socket实例就可以和别的Socket通信了
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接上了!"); //如果没有人连,这句话不会打印
//低级流,负责从socket中读取对方发来的字节数据
InputStream in = socket.getInputStream();
//高级流,负责数据转换,将字节流转成字符流
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
//高级流,负责加快读取字符效率
BufferedReader br = new BufferedReader(isr);
//读取内容
String s = br.readLine();
System.out.println("客户端说:" + s);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
客户端:
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
public class Client {
// 声明客户端工具
private static Socket socket;
//初始化客户端
public Client() {
try {
System.out.println("正在连接服务器端...");
/*
实例化客户端工具(Socket对象)
*/
/*
参数1:要连接的服务器地址,可以是域名或Ip地址
参数2:服务器开放的端口号
*/
socket = new Socket("localhost", 8088);
System.out.println("与服务器建立了连接");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法,放服务端发送消息
*/
public static void start() {
try {
//低级流,将字节等信息通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流于字符流(转换流:字节流转化成字符流)
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//缓冲流
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串数据,true表示自动刷新
PrintWriter pw = new PrintWriter(bw,true);
//
pw.println("你好吗,服务器端?");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
close方法
调用 close方法关闭 Socket 以释放系统资源。关闭了套接字,也会同时关闭由此获取的输入流与输出流。
客户端循环发送信息给客户端:
服务器端:
/**
* @document: 聊天室V03:服务器端
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
//创建一个ServerSocket对象,建立通信服务
private ServerSocket serverSocket;
/**
* 初始化服务端
*/
public Server() {
try {
System.out.println("正在启动服务器...");
/*
实例化ServerSocket,在new的时候需要使用端口号
端口号是一个数字,取值范围是0~65535,使用时需避免常用端口
一般是6000以前不要使用
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务器启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器开始工作的方法
*/
public void start() {
Socket socket = null;
//总机的通话设备
try {
System.out.println("等待客户端连接...");
//serverSocket.accept()这句话会造成阻塞,会等待客户端连接
//直到一个客户端发来了连接 此时accept()方法会返回一个Socket实例
//通过Socket实例就可以和别的Socket通信了
socket = serverSocket.accept();
System.out.println("一个客户端连接上了!"); //如果没有人连,这句话不会打印
//低级流,负责从socket中读取对方发来的字节数据
InputStream in = socket.getInputStream();
//高级流,负责数据转换,将字节流转换为字符流
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
//高级流,负责加快读取字符效率
BufferedReader br = new BufferedReader(isr);
//读取内容
String s = null;
//客户端在发送的时候会发送一个中断信号,在服务器端会接收到一个null值
while ((s = br.readLine()) != null) {
//如果为null则说明客户端断开了连接
System.out.println("客户端说:" + s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
客户端:
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
public class Client {
// 声明客户端工具
private static Socket socket;
//初始化客户端
public Client() {
try {
System.out.println("正在连接服务器端...");
/*
实例化客户端工具(Socket对象)
*/
/*
参数1:要连接的服务器地址,可以是域名或Ip地址
参数2:服务器开放的端口号
*/
socket = new Socket("localhost", 8088);
System.out.println("与服务器建立了连接");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法,放服务端发送消息
*/
public static void start() {
try {
//低级流,将字节等信息通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流于字符流(转换流:字符流转化成字节流)
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//缓冲流
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串数据,true表示自动刷新
PrintWriter pw = new PrintWriter(bw, true);
//输出数据
//创建一个接收键盘的输入
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if (line.equalsIgnoreCase("bye") || line.equalsIgnoreCase("exit")) {
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/**
* 关闭所有的流,同时会给服务器发送一个中断信号
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
多线程实现聊天室:
若想使一个服务端可以支持多客户端连接,我们需要解决以下问题:
- 循环调用 accept 方法侦听客户端的连接
- 使用线程来处理单一客户端的数据交互
服务端
/**
* @document: 聊天室V04:服务器端
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
//创建一个ServerSocket对象,建立通信服务
private ServerSocket serverSocket;
/**
* 初始化服务端
*/
public Server() {
try {
System.out.println("正在启动服务器...");
/*
实例化ServerSocket,在new的时候需要使用端口号
端口号是一个数字,取值范围是0~65535,使用时需避免常用端口
一般是6000以前不要使用
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务器启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器开始工作的方法
*/
public void start() {
try {
while (true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了");
//启动线程与客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
new Thread(clientHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 创建线程任务
* 服务器端有一个客户端连接之后 服务器端单独启动一个线程与客户端进行交互
*/
private class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
String s = null;
String name = Thread.currentThread().getName();
while ((s = br.readLine()) != null) {
System.out.println(name + ": 客户端说:" + s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
public class Client {
// 声明客户端工具
private static Socket socket;
//初始化客户端
public Client() {
try {
System.out.println("正在连接服务器端...");
/*
实例化客户端工具(Socket对象)
*/
/*
参数1:要连接的服务器地址,可以是域名或Ip地址
参数2:服务器开放的端口号
*/
socket = new Socket("localhost", 8088);
System.out.println("与服务器建立了连接");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法,放服务端发送消息
*/
public static void start() {
try {
//低级流,将字节等信息通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流于字符流(转换流:字符流转化成字节流)
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//缓冲流
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串数据,true表示自动刷新
PrintWriter pw = new PrintWriter(bw, true);
//输出数据
//创建一个接收键盘的输入
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if (line.equalsIgnoreCase("bye")
|| line.equalsIgnoreCase("exit")) {
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/**
* 关闭所有的流,同时会给服务器发送一个中断信号
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
5、获取本地地址和端口号
如果服务器想给客户端发消息,需要获取客户端地址和端口号
通过 Socket 获取远端的地址以及端口号
- int getPort():用于获取远端使用的端口号
- InetAddress getInetAddress():该方法用于获取套接字绑定的远端地址
/**
* @document: 通过Socket获取本地地址
* @Author:SmallG
* @CreateTime:2023/8/15+9:13
*/
public class Demo01 {
public static void main(String[] args) {
try {
//创建一个Socket对象,可以指定访问地址
Socket socket = new Socket("127.0.0.1", 8088);
InetAddress add = socket.getLocalAddress();//得到本地的地址
//使用getCanonicalHostName()方法 获取IP地址
//得到的是完全的地址名(域名)
System.out.println(add.getCanonicalHostName());
//getHostAddress()也是获取Ip地址,返回一个文本字符串
System.out.println(add.getHostAddress());
System.out.println(socket.getLocalPort()); //通过socket获取端口号
} catch (IOException e) {
e.printStackTrace();
}
}
}
聊天室实现服务器端向客户端发送信息
服务器端:
/**
* @document: 聊天室V05:服务器端
* 服务器端向客户端发消息
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
//创建一个ServerSocket对象,建立通信服务
private ServerSocket serverSocket;
/**
* 初始化服务端
*/
public Server() {
try {
System.out.println("正在启动服务器...");
/*
实例化ServerSocket,在new的时候需要使用端口号
端口号是一个数字,取值范围是0~65535,使用时需避免常用端口
一般是6000以前不要使用
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务器启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器开始工作的方法
*/
public void start() {
try {
while (true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了");
//启动线程与客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
new Thread(clientHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 创建线程任务
* 服务器端有一个客户端连接之后 服务器端单独启动一个线程与客户端进行交互
*/
private class ClientHandler implements Runnable {
private Socket socket;
private String host; //表示客户端的IP地址信息
public ClientHandler(Socket socket) {
this.socket = socket;
//通过socket来获取远程的客户端地址信息
this.host = socket.getInetAddress().getHostAddress();
}
@Override
public void run() {
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
OutputStream os = socket.getOutputStream();
OutputStreamWriter osr = new OutputStreamWriter(os, "UTF-8");
BufferedWriter bw = new BufferedWriter(osr);
PrintWriter pw = new PrintWriter(bw, true);
String s = null;
String name = Thread.currentThread().getName();
while ((s = br.readLine()) != null) {
System.out.println(name + ": 客户端说:" + s);
//把消息发回给客户端
pw.println(host + "说" + s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端:
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
public class Client {
// 声明客户端工具
private static Socket socket;
//初始化客户端
public Client() {
try {
System.out.println("正在连接服务器端...");
/*
实例化客户端工具(Socket对象)
*/
/*
参数1:要连接的服务器地址,可以是域名或Ip地址
参数2:服务器开放的端口号
*/
socket = new Socket("localhost", 8088);
System.out.println("与服务器建立了连接");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法,放服务端发送消息
*/
public static void start() {
try {
//低级流,将字节等信息通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流于字符流(转换流:字符流转化成字节流)
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//缓冲流
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串数据,true表示自动刷新
PrintWriter pw = new PrintWriter(bw, true);
//输出数据
InputStream ins = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(ins,"utf-8");
BufferedReader br = new BufferedReader(isr);
//创建一个接收键盘的输入
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if (line.equalsIgnoreCase("bye")
|| line.equalsIgnoreCase("exit")) {
break;
}
pw.println(line);
String s = br.readLine();
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/**
* 关闭所有的流,同时会给服务器发送一个中断信号
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
服务器向所有客户端发送消息
创建集合,集合中包含所有的客户端,然后遍历集合发消息
服务器端:
/**
* @document: 聊天室V07
* 服务器端
* 服务器端向所有客户端发消息
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
//创建一个ServerSocket对象,建立通信服务
private ServerSocket serverSocket;
//创建一个存放所有输出流的集合,用于群发消息
private List<PrintWriter> allPw = new ArrayList<>();
/**
* 初始化服务端
*/
public Server() {
try {
System.out.println("正在启动服务器...");
/*
实例化ServerSocket,在new的时候需要使用端口号
端口号是一个数字,取值范围是0~65535,使用时需避免常用端口
一般是6000以前不要使用
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务器启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器开始工作的方法
*/
public void start() {
try {
while (true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了");
//启动线程与客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
new Thread(clientHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 创建线程任务
* 服务器端有一个客户端连接之后 服务器端单独启动一个线程与客户端进行交互
*/
private class ClientHandler implements Runnable {
private Socket socket;
private String host; //表示客户端的IP地址信息
public ClientHandler(Socket socket) {
this.socket = socket;
//通过socket来获取远程的客户端地址信息
this.host = socket.getInetAddress().getHostAddress();
}
@Override
public void run() {
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
OutputStream os = socket.getOutputStream();
OutputStreamWriter osr = new OutputStreamWriter(os, "UTF-8");
BufferedWriter bw = new BufferedWriter(osr);
PrintWriter pw = new PrintWriter(bw, true);
String s = null;
//把输出流存入all集合中
allPw.add(pw);
String name = Thread.currentThread().getName();
while ((s = br.readLine()) != null) {
System.out.println(name + ": 客户端说:" + s);
//把消息发回给客户端
for (PrintWriter pw1 : allPw) {
pw1.println(host + "说" + s);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端:
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
public class Client {
// 声明客户端工具
private static Socket socket;
//初始化客户端
public Client() {
try {
System.out.println("正在连接服务器端...");
/*
实例化客户端工具(Socket对象)
*/
/*
参数1:要连接的服务器地址,可以是域名或Ip地址
参数2:服务器开放的端口号
*/
socket = new Socket("localhost", 8088);
System.out.println("与服务器建立了连接");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法,放服务端发送消息
*/
public void start() {
try {
//开始的时候立即启动线程,读取服务器发来的消息线程
ClientHandler handler = new ClientHandler();
Thread t = new Thread(handler);
t.setDaemon(true);
t.start();
//低级流,将字节等信息通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流于字符流(转换流:字符流转化成字节流)
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//缓冲流
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串数据,true表示自动刷新
PrintWriter pw = new PrintWriter(bw, true);
//创建一个接收键盘的输入
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if (line.equalsIgnoreCase("bye")
|| line.equalsIgnoreCase("exit")) {
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/**
* 关闭所有的流,同时会给服务器发送一个中断信号
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
//当前线程负责接收服务器发来的消息
private class ClientHandler implements Runnable {
@Override
public void run() {
//输出数据
try {
InputStream ins = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(ins, "utf-8");
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
二、反射(Reflection)
1、概念
反射机制是程序在运行时能够获得自身的信息。
反射的作用:
- 可以动态查看一个类或对象的所有属性和方法,包括使用private修饰的属性和方法
- 可以动态加载类
- 可以动态的创建一个类的实例
- 可以动态调用一个对象的方法
2、反射优缺点
反射的优点:
- 反射允许我们在程序运行期间获得类的信息并操作一个类中的方法,因此可以提高代码的灵活性和扩展性
- 反射是Java中很多高级特性的基础,比如后面会介绍的注解、动态代理等特性
- 在很多框架中,对反射技术的使用也非常多,比如大名鼎鼎的Spring框架、各类ORM框架(关系映射)、RPC框架(服务与实现)等
反射的缺点:
- 反射的代码的可读性和可维护性都比较低
- 反射的代码执行的性能低
3、反射API
Java提供了反射相关的API,核心是java.lang.Class类,用于加载类和获取类的相关信息。
Java 反射 API 位于 java.lang.reflect 包中,主要包括:
- Constructor类:用来描述一个类的构造方法
- Field类:用来描述一个类的成员变量,属性
- Method类:用来描述一个类的方法
- Modifier类:用来描述类内各元素的修饰符
- Array:用来对数组进行操作
4、class类
(1)概述
在程序运行时,JVM首先检查要加载的类对应的Class对象是否已经创建。如果没有创建,JVM会根据类名查找.class文件,将其加载到内存中,并创建相应的Class对象。
需要注意,Class类的构造器被设计为私有的,也就是说开发者不能主动创建Class类的对象。Class类的对象仅能由JVM创建。
(2)类加载器
类加载器(Class Loader)是JVM的一个子系统,负责将class文件加载到内存中,然后在堆中创建一个代表这个类的Class对象,作为方法区中类数据的访问入口。
/**
* @document: java动态加载一个类:四种方式
* 最常用的是后两种
* @Author:SmallG
* @CreateTime:2023/8/15+11:37
*/
public class Demo01 {
public static void main(String[] args) throws Exception {
//1、通过对象的getClass()方法
Person person = new Person();
Class cla1 = person.getClass(); //Object提供的方法
//可以获取类名
System.out.println(cla1.getName()); //获取类全名(类的限定名:包名+类名) day07.reflect.Person
System.out.println(cla1.getSimpleName()); //类名的简称 Person
//2、通过类的.class
Class cla2 = Person.class;
System.out.println(cla2.getName()); //day07.reflect.Person
//3、通过Class类的静态方法forName()加载类的对象
//开发人员主动加载,作为java内存被动加载
Class cla3 = Class.forName("day07.reflect.Person");
System.out.println(cla3.getSimpleName()); //Person
//4、通过java的类的加载器ClassLoader加载类对象
//通过当前的类获取类的加载器
ClassLoader classLoader = Demo01.class.getClassLoader();
Class cla4 = classLoader.loadClass("day07.reflect.Person");
System.out.println(cla4.getName()); //day07.reflect.Person
}
}
(4)动态创建对象
通过反射在程序运行的过程中动态创建一个类的对象。getDeclaredConstructor().newInstance()方法
/**
* @document:
* @Author:SmallG
* @CreateTime:2023/8/15+14:05
*/
public class Demo02 {
public static void main(String[] args) throws Exception {
//1、动态加载类
ClassLoader loader = Demo02.class.getClassLoader(); //获取类的加载器
Class cla = loader.loadClass("day07.reflect.Person");
//2、动态创建对象
Person person1 = (Person) cla.newInstance(); //已过时
System.out.println(person1); //Person{name='null', age=null}
//从java 9 开始使用,通过调用类的默认构造方法
Person person2 = (Person) cla.getDeclaredConstructor().newInstance(); //调用已经声明的构造器
System.out.println(person2); //Person{name='null', age=null}
Constructor c = cla.getDeclaredConstructor(String.class, Integer.class);
System.out.println(c.getName());
//如果构造方法不是public声明的 都需要授权才能访问
c.setAccessible(true); //授权
//调用有参的构造方法 创建对象
Person person3 = (Person) cla.getDeclaredConstructor(String.class, Integer.class).newInstance("tom", 20);
System.out.println(person3); //Person{name='tom', age=20}
}
}
(5)动态调用方法
/**
* @document: 通过反射动态调用方法
* @Author:SmallG
* @CreateTime:2023/8/15+14:34
*/
public class Demo03 {
public static void main(String[] args) throws Exception {
//实例化对象 采用类的加载器
Class cla = Demo03.class.getClassLoader().loadClass("day07.reflect.Person");
Person person = (Person) cla.getDeclaredConstructor().newInstance();
//获取方法
Method method1 = cla.getMethod("hello"); //只能获取public修饰的方法
method1.setAccessible(true);
//调用方法
method1.invoke(person); //通过person对象调用无参的方法
//获取有参的方法 需要声明参数类型
Method method2 = cla.getDeclaredMethod("say", String.class);
//调用有参方法
method2.invoke(person, "Tom");
//获取多个参数并且有返回值的方法
Method method3 = cla.getMethod("sum", Integer.class, Integer.class);
// 调用多个参数,并且有返回值的方法
Integer sum = (Integer) method3.invoke(person, 1, 2);
System.out.println(sum);
}
}
5、注解
可以将各种信息(例如类、方法、变量等)与程序元素相关联。Java注解可以用于许多目的,例如:
- 为代码提供元数据信息,例如文档、版本号等
- 为编译器提供指示,例如抑制警告、生成代码等
- 为运行时提供指示,例如启用事务、配置参数等
/**
* @document: 创建接口
* @Author:SmallG
* @CreateTime:2023/8/15+15:20
*/
//Target是元注解,用于修饰注解的注解
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface MyAnnotation {
String value() default "name"; //属性
//声明一个注解的成员属性
String name() default "Tom"; //为注解的属性赋一个默认值
}
/**
* @document: 利用注解修饰类、方法、属性
* @Author:SmallG
* @CreateTime:2023/8/15+15:23
*/
//为注解的属性赋值,这个值默认情况下看不了,只能通过反射看,注解不参与程序运行,只对程序起到约束作用
//如果同时给注解的其他属性赋值,value不可省略
@MyAnnotation()
public class MyClass {
@MyAnnotation(value = "属性",name = "Spike")
private Integer age;
@MyAnnotation()
public void hello() {
System.out.println("Hello World");
}
}
通过反射获取类的注解
需要在注解中添加元注解:@Retention(RetentionPolicy.RUNTIME)
/**
* @document: 通过反射获取类的注解
* @Author:SmallG
* @CreateTime:2023/8/15+15:45
*/
public class Demo01 {
public static void main(String[] args) throws Exception {
//获取类对象
Class cla = Demo01.class.getClassLoader().loadClass("day07.annotation.MyClass");
//获取类上的注解
MyAnnotation an1 = (MyAnnotation) cla.getAnnotation(MyAnnotation.class);
//获取注解的值
System.out.println(an1.value());
//获取方法
Method hello = cla.getMethod("hello");
//获取方法上的注解
MyAnnotation an2 = hello.getAnnotation(MyAnnotation.class);
System.out.println(an2.value() + ":" + an2.name());
//获取属性上的注解,先获取属性
Field age = cla.getDeclaredField("age");
MyAnnotation ann3 = age.getAnnotation(MyAnnotation.class);
System.out.println(ann3.value() + ":" + ann3.name());
}
}
三、练习
1 解决聊天室案例的线程安全问题
在聊天室案例中,服务端存在着一个线程安全隐患:服务端使用一个ArrayList集合保存所有已建立连接的客户端的输出流对象。对该集合同时存在着3种操作:
- 添加:当一个客户端建立连接时,将该客户端的输出流对象存入集合
- 查询:每当一个客户端发送消息时,遍历该集合,使用输出流对象向所有客户端推送消息
- 删除:当一个客户端断开连接时,从集合中删除该客户端对应的输出流对象
如果有2个线程,A线程删除某个输出流对象的同时,B线程使用该输出流对象推送消息,则可能出现线程安全问题。
请完善聊天室案例代码,解决上述问题。
提示:应用JUC中提供的Lock API。
服务器端:
/**
* @document: 聊天室V07
* 服务器端
* 服务器端向所有客户端发消息
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
//创建一个ServerSocket对象,建立通信服务
private ServerSocket serverSocket;
//服务端使用的读写锁
private ReadWriteLock readWriteLock;
private Lock readLock;
private Lock writeLock;
//创建一个存放所有输出流的集合,用于群发消息,广播消息
private List<PrintWriter> allOut = new ArrayList<>();
/**
* 初始化服务端
*/
public Server() {
try {
//初始化读写锁
this.readWriteLock = new ReentrantReadWriteLock();
this.readLock = readWriteLock.readLock();
this.writeLock = readWriteLock.writeLock();
System.out.println("正在启动服务器...");
serverSocket = new ServerSocket(8088);
System.out.println("服务器启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器开始工作的方法
*/
public void start() {
try {
while (true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了");
//启动线程与客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
new Thread(clientHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 创建线程任务
* 服务器端有一个客户端连接之后 服务器端单独启动一个线程与客户端进行交互
*/
private class ClientHandler implements Runnable {
private Socket socket;
String host; //表示客户端的IP地址信息
PrintWriter pw = null;
public ClientHandler(Socket socket) {
this.socket = socket;
//通过socket来获取远程的客户端地址信息
this.host = socket.getInetAddress().getHostAddress();
}
/**
* 向所有客户端推送消息的方法
*/
private void sendMessage(String message){
System.out.println(this.host+":客户端说:"+message);
//获得读锁
readLock.lock();
for (PrintWriter o :allOut){
o.println(this.host+"说:"+message);
}
//释放读锁
readLock.unlock();
}
@Override
public void run() {
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
OutputStream os = socket.getOutputStream();
OutputStreamWriter osr = new OutputStreamWriter(os, "UTF-8");
BufferedWriter bw = new BufferedWriter(osr);
pw = new PrintWriter(bw, true);
//获得写锁
writeLock.lock();
allOut.add(pw);
//广播该客户端上线的操作
sendMessage(host+"上线了,当前在线人数:"+allOut.size());
writeLock.unlock();
String message = null;
while ((message = br.readLine()) != null) {
sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//获得写锁
writeLock.lock();
//处理客户端断开连接后的操作
allOut.remove(pw);
//广播该客户端下线
sendMessage(host+"下线了,当前在线人数:"+allOut.size());
//释放写锁
writeLock.unlock();
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端:
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
public class Client {
// 声明客户端工具
private static Socket socket;
//初始化客户端
public Client() {
try {
System.out.println("正在连接服务器端...");
/*
参数1:要连接的服务器地址,可以是域名或Ip地址
参数2:服务器开放的端口号
*/
socket = new Socket("localhost", 8088);
System.out.println("与服务器建立了连接");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法,放服务端发送消息
*/
public void start() {
try {
//开始的时候立即启动线程,读取服务器发来的消息线程
ClientHandler handler = new ClientHandler();
Thread t = new Thread(handler);
t.setDaemon(true);
t.start();
//低级流,将字节等信息通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流于字符流(转换流:字符流转化成字节流)
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
//缓冲流
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串数据,true表示自动刷新
PrintWriter pw = new PrintWriter(bw, true);
//创建一个接收键盘的输入
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if (line.equalsIgnoreCase("bye")
|| line.equalsIgnoreCase("exit")) {
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/**
* 关闭所有的流,同时会给服务器发送一个中断信号
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
//当前线程负责接收服务器发来的消息
private class ClientHandler implements Runnable {
@Override
public void run() {
//输出数据
try {
InputStream ins = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(ins, "utf-8");
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2 完成聊天室的私聊功能
私聊功能是指,客户端之间可以实现一对一的聊天。
服务器端程序启动后,将等待客户端连接,界面效果如下图所示:
客户端程序运行时,需要用户先输入昵称。用户输入昵称之后,提示用户可以开始聊天。
服务端需要检查客户端发来的昵称是否已被占用,如果已被占用,需要向客户端返回提示信息,如下图所示:
客户端发送的消息默认为公聊消息,向所有客户端广播,包括发出该消息的客户端,界面效果如下图所示:
客户端可以使用其他客户端可以通过输入类似“\Jerry:你好”这样的字样和昵称为“jerry”的客户端私聊,界面效果如下图所示:
如果某客户端程序停止运行,其他客户端程序可以接收到消息并显示。例如,昵称为“Jerry”的客户端停止运行,昵称为“Tom”的客户端的界面效果如图所示:
提示:
1、服务端可以使用Map集合保存所有客户端的输出流对象,键为客户端的名称,值为该客户端的输出流对象;
2、客户端启动后先输入昵称,未输入完成前不显示客户端发来的消息,如昵称重复,需更换其他昵称;
3、客户端流程图如下所示:
4、服务端流程图如下所示:
服务端:
/**
* @document: 聊天室V07
* 服务器端
* 服务器端向所有客户端发消息
* @Author:SmallG
* @CreateTime:2023/8/14+15:12
*/
public class Server {
private ServerSocket serverSocket;
// 服务端使用的读写锁
private ReadWriteLock readWriteLock;
private Lock readLock;
private Lock writeLock;
// 所有客户端输出流,key为用户的昵称,value为该用户的输出流
private Map<String,PrintWriter> allOut;
public Server(){
try {
// 初始化读写锁
this.readWriteLock = new ReentrantReadWriteLock();
this.readLock = readWriteLock.readLock();
this.writeLock = readWriteLock.writeLock();
allOut = new HashMap<String,PrintWriter>();
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程与该客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
Thread t = new Thread(clientHandler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将输出流存入共享集合,与下面两个方法互斥,保证同步安全
* @param out
*/
private void addOut(String nickName,PrintWriter out){
writeLock.lock();
allOut.put(nickName,out);
writeLock.unlock();
}
/**
* 将给定输出流从共享集合删除
* @param nickName
*/
private void removeOut(String nickName){
writeLock.lock();
allOut.remove(nickName);
writeLock.unlock();
}
/**
* 将消息转发给所有客户端
* @param message
*/
private void sendMessage(String message,boolean isSystemMessage){
readLock.lock();
for(PrintWriter o : allOut.values()){
if (isSystemMessage){
o.println("系统消息 => " +message);
}else{
o.println("公聊消息 => " + message);
}
}
readLock.unlock();
}
/**
* 将消息发送给指定昵称的客户端
* @param nickName
* @param message
* @return true-存在该用户 false-不存在该用户
*/
private boolean sendMessageToOne(String nickName,String message){
boolean flag = true;
readLock.lock();
PrintWriter out = allOut.get(nickName);
if(out!=null){
out.println(message);
}else{
flag =false;
}
readLock.unlock();
return flag;
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 定义线程任务
* 目的是让一个线程完成与特定客户端的交互工作
*/
private class ClientHandler implements Runnable{
private Socket socket;
// 该客户端的昵称
private String nickName;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
// 声明输出流引用,便于finally语句块中访问
PrintWriter pw = null;
try{
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr);
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
pw = new PrintWriter(bw,true);
//先获取该用户昵称
this.nickName = getNickName(pw, br);
addOut(nickName,pw);
Thread.sleep(100);
//广播该客户端上线的操作
sendMessage(this.nickName+"上线了,当前在线人数:" + allOut.size(), true);
String message = null;
while ((message = br.readLine()) != null) {
//首先查看是不是私聊
if(message.startsWith("\\")){
/*
* 私聊格式:\昵称:内容
*/
//找到:的位置
int index = message.indexOf(":");
if(index>=0){
//截取昵称
String name = message.substring(1,index);
//截取内容
String info = message.substring(index+1,message.length());
//拼接内容
info = nickName+"对你说:"+info;
//发送私聊信息给指定用户
boolean flag = sendMessageToOne(name, info);
if (!flag){
sendMessageToOne(this.nickName,"用户:"+name+"不存在");
}
//发送完私聊后就不在广播了。
continue;
}
}
/*
* 遍历所有输出流,将该客户端发送的信息转发给所有客户端
* 需要同步
*/
sendMessage(nickName+"说:"+message, false);
}
}catch(Exception e){
// 暂不处理
}finally {
removeOut(this.nickName);
//广播该客户端下线
sendMessage(this.nickName+"下线了,当前在线人数:" + allOut.size(), true);
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取该用户的昵称
* @return
*/
private String getNickName(PrintWriter pw, BufferedReader br) throws IOException {
try {
//读取客户端发送过来的昵称
String nickName = br.readLine();
while(true){
//若昵称为空发送失败代码
if(nickName.trim().equals("")){
pw.println("FAIL");
}
//若昵称已经存在发送失败代码
if(allOut.containsKey(nickName)){
pw.println("FAIL");
//若成功,发送成功代码,并返回昵称
}else{
pw.println("OK");
return nickName;
}
//若改昵称被占用,等待用户再次输入昵称
nickName = br.readLine();
}
} catch (Exception e) {
throw e;
}
}
}
}
客户端:
/**
* @document: 客户端
* @Author:SmallG
* @CreateTime:2023/8/14+15:46
*/
/**
* 聊天室客户端
*/
public class Client {
private Socket socket;
public Client() {
try {
System.out.println("正在连接服务端...");
socket = new Socket("localhost", 8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
//创建Scanner读取用户输入内容
Scanner scanner = new Scanner(System.in);
//首先输入昵称
inputNickName(scanner);
//启动读取服务端发送过来消息的线程
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.setDaemon(true);
t.start();
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
while(true) {
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
/**
* 输入昵称
*/
private void inputNickName(Scanner scanner)throws Exception{
//定义昵称
String nickName = null;
//创建输出流
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
socket.getOutputStream(),"UTF-8")
,true);
//创建输入流
BufferedReader br = new BufferedReader(
new InputStreamReader(
socket.getInputStream(),"UTF-8")
);
/*
* 循环以下操作
* 输入用户名,并上传至服务器,等待服务器回应,若昵称可用就结束循环,否则通知用户后
* 重新输入昵称
*/
while(true){
System.out.println("请输入昵称:");
nickName = scanner.nextLine();
if(nickName.trim().equals("")){
System.out.println("昵称不能为空");
}else{
pw.println(nickName);
String pass = br.readLine();
if(pass!=null&&!pass.equals("OK")){
System.out.println("昵称已被占用,请更换。");
}else{
System.out.println("你好!"+nickName+",开始聊天吧!");
break;
}
}
}
}
/**
* 该线程负责接收服务端发送过来的消息
*/
private class ServerHandler implements Runnable{
public void run(){
//通过socket获取输入流读取服务端发送过来的消息
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String line;
//循环读取服务端发送过来的每一行字符串
while((line = br.readLine())!=null){
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}