完整版代码
java -聊天室的代码: 用于存放聊天室的项目的代码和思路导图https://gitee.com/to-uphold-justice-for-others/java---code-for-chat-rooms.git
先引入线程的正统解释
线程(Thread)是程序执行流的最小单元。线程是操作系统分配CPU时间片的基本单位,每个线程都拥有独立的程序计数器、栈、本地方法栈等,共享进程的资源,如代码段、数据段、堆等。Java通过线程实现并发编程,提高了程序的执行效率。
线程的常见有两种方式
继承Thread类:通过继承Thread类并重写其run()方法,可以创建线程。然后,通过调用线程的start()方法来启动线程。
public class MyThread extends Thread { @Override public void run() { // 线程执行的代码 System.out.println("MyThread is running."); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 }
}
实现Runnable接口:通过实现Runnable接口的run()方法,也可以创建线程。这种方式更加灵活,因为Java不支持多继承,但可以实现多个接口。通常,我们将线程的任务逻辑放在Runnable实现类中,然后将其传递给Thread对象来启动线程。
public class MyRunnable implements Runnable { @Override public void run() { // 线程执行的代码 System.out.println("MyRunnable is running."); } public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程 }
}
如果线程要多个线程调用一个类对象的时候常用Runnable接口,单一线程调用类对象个多方法可以使用继承比较方便
这边因为是服务端去获取多个客户端的消息并进行读取操作,线程在服务端创建,即一个客户端连接到服务器时候,服务端会接受客户端的插口,并新建一个线程去处理客户端的写入服务端的数据.
这边是服务端的封装私有的内部类一个线程的类来专门处理来自客户端的数据
私有的内部类的好处,在服务的执行业务,外部类不能直接访问,保证业务安全性,因为是内部类不用向外部类中建立一个引用变量实例来访问其对应的方法和属性
引用变量
引用变量的定义通常包括变量类型(即对象类型)和变量名
// 定义一个引用变量,其类型为String
String myString; // 为引用变量分配一个String对象
myString = new String("Hello, World!");
//这里的mystring就是个引用变量
服务端
之前在在单线程获取用户的昵称和ip地址这边需要封装成私有属性,保护用户的数据私密性
客户端需要ip是可以服务端接受的插口获取的,所以在有参构造中需要将ip赋值
全局变量相比局部变量 全局变量可以被其内部类和内部方法访问到,也容易因其遭到数据修改
局部变量可以保证在自己的类或者自己的方法中使用.
常常相比的是如果一个类中有成员变量,其非静态方法可以访问其成员变量,静态方法只能访问静态变量,静态变量是由static修饰的,常称为类的全局变量,如果其中还有内部类,类中的成员变量常称为局部实例变量,如果类中方法有变量常称为局部变量
private class ClientHandler implements Runnable{private Socket socket;private String ip;//记录当前客户端的IP地址private String nickname;//记录当前客户端的昵称public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机的IP地址ip = socket.getInetAddress().getHostAddress();}public void run(){PrintWriter pw = null;try {/*Socket的方法:InputStream getInputStream()通过socket获取一个字节输入流,读取该流就可以读取到远端计算机发送过来的数据*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);BufferedReader br = new BufferedReader(isr);nickname = br.readLine();} catch (IOException e) {//可以添加处理客户端异常断开的操作} finally {try {socket.close();} catch (IOException e) {throw new RuntimeException(e);}}}
}
同时线程也需要启动,这边使用的Runnable接口实现的所以需要新建一个线程实例化对象,
实现实例化对象.start启动线程
这要写在class Server类中
//实例化线程任务ClientHandler handler = new ClientHandler(socket);//实例化线程Thread t = new Thread(handler);//启动线程t.start();
问题 服务端连接了多个客户端,虽然每个客户端可以给服务端发送消息,但是日常的情况,常需要客户端与客户端聊天
解决 因为客户端都是将信息传递给客户端,我们可以在服务端整合客户端的输给服务端的数据,这边的话使用集合的思想,集合是可以不固定的其长度,其内部是有初始的长度,会根据添加的数据进行扩容.这边推荐ArrayList ,是因为服务端是整合客户端数据发送,而ArrayList 相比与LinkedList查询数据快.
public class Server {
private Collection<PrintWrite> allOut = new ArrayList<>();
}
这边需要定义在服务端一个输出流,而在客户端是输入流
服务端
这边服务端封装类一个内部类处理客户端的数据,
public class Server {private class ClientHandler implements Runnable{
public void run(){ PrintWriter pw = null;try {OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);BufferedWriter bw = new BufferedWriter(osw);pw = new PrintWriter(bw, true);allOut.add(pw); String message;while ((message = br.readLine()) != null) {for(PrintWriter o : allOut){o.println(nickname + "[" + ip + "]说:" + message);}}}
}
客户端
同样的这边需要封装一个私有的内部类利用线程来处理服务端发送的数据
private class ServerHandler implements Runnable{@Overridepublic void run() {try {//通过socket获取输入流,用于读取服务端发送过来的消息InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);BufferedReader br = new BufferedReader(isr);String line;while((line = br.readLine())!=null){System.out.println(line);}} catch (IOException e) {}}}
同时线程也需要启动,这边使用的Runnable接口实现的所以需要新建一个线程实例化对象,
这边需要注意的是客户端输入昵称在客户端接受服务端数据之前
ServerHandler handler = new ServerHandler();Thread t = new Thread(handler);t.setDaemon(true);t.start();
线程实例化对象.setDaemon(true);是将普通线程变成守护线程
问题这边客户端接受服务端数据为什么使用守护线程
守护线程定义
守护线程(Daemon Thread)是Java中的一种特殊线程类型,它的存在主要是为了服务其他的线程,特别是用户自定义的线程。当所有的非守护线程(即用户线程)执行完成或退出时,即使守护线程仍在运行,Java虚拟机(JVM)也会直接退出。因此,守护线程通常用于执行一些辅助工作或后台任务,例如垃圾回收、自动保存数据、心跳检测、日志记录、清理临时文件以及监控系统状态等。
守护线程的特点包括:
- 随主线程结束而结束:当主线程(即非守护线程)结束时,守护线程会随之被终止,不管它是否执行完毕。
- 不执行finally块:如果守护线程中执行的代码块中有finally块,当守护线程被终止时,finally块不会被执行。
- 不能持有程序运行的关键资源:由于守护线程在所有用户线程结束时可能被中断,如果持有关键资源,可能会导致数据不一致或资源泄漏。
- 不能用于执行必须完成的任务:由于守护线程可能随时被中断,它不适合执行必须完成的任务,例如文件写入等。
在Java中,可以通过Thread类的setDaemon(true)方法将一个线程设置为守护线程,而setDaemon(false)方法则用于取消守护线程的设置。
总之,守护线程在Java中扮演着重要的角色,用于在后台执行一些必要的辅助任务,以确保程序的正常运行和资源的有效管理。
原因
不阻塞主线程:客户端的主线程通常负责用户界面的更新、用户交互的处理等重要任务。如果接收服务端数据的操作是同步的并且需要花费较长时间,那么它可能会阻塞主线程,导致用户界面无响应或响应缓慢。通过使用守护线程来执行这项任务,主线程可以继续处理其他重要工作,从而保持用户界面的流畅性。
后台处理:接收服务端数据通常是一个后台任务,它不需要与用户进行实时交互。守护线程非常适合执行这类任务,因为它们可以在后台默默运行,而不需要用户或主线程的特别关注。
问题 客户端想要获取在线人数,以便后续的私聊(这个之后会讲) ,同时也想知道客户端下线之后人数,
解决 这边我们发现我们需要重复获取集合中的信息,而集合的长度会根据在线人数变,我们这边可以将集合信息写一个方法以减少其代码量
public void sendMessage(String message){System.out.println(message);//先在服务端控制台上输出一下//遍历要和增删互斥,迭代器要求遍历过程中不可以通过集合方法增删元素// for (PrintWriter o : allOut) {//发送给所有客户端for(PrintWriter o : allOut){o.println(message);}}
之前的可以简写了
集合对象.size()可以获取集合长度,正好可以统计在线人数,
这个需要在集合添加数据之后调用,业务逻辑
sendMessage(nickname+"上线了,当前在线人数:"+allOut.size());
因为之前只考虑了将服务端整合客户端的数据集体发送,没考虑客户端下线的情况
这边如果客户端下线(包括异常断开),关闭其接口以及将其移除(昵称)
finally是保证程序一定会执行,常用与关闭文件流
finally {//处理客户端断开链接后的操作//将该客户端的输出流从共享集合allOut中删除// allOut.remove(pw);allOut.remove(nickname);sendMessage(nickname+"下线了,当前在线人数:"+allOut.size());//将socket关闭,释放底层资源try {socket.close();} catch (IOException e) {throw new RuntimeException(e);}
问题,线程真的安全吗,这边需要对服务端的中存储数据的集合进行增减,服务端的集合需要同时面对多个客户端输入的数据,不会引起资源抢占吗
解决这个涉及到多线程并发的问题,这个需要运用到线程锁的概念,这边暂时还在总结,我也是一个新手🤣🤣🤣