NIO专题学习(一)

一、BIO/NIO/AIO介绍

1. 背景说明

在Java的软件设计开发中,通信架构是不可避免的。我们在进行不同系统或者不同进程之间的数据交互,或者在高并发的通信场景下都需要用到网络通信相关的技术。

对于一些经验丰富的程序员来说,Java早期的网络通信架构存在一些缺陷,其中最令人恼火的是基于性能低下的同步阻塞式的I/O通信(BIO)。随着互联网开发下通信性能的高要求,Java在2002年开始支持了非阻塞式的I/O通信技术(NIO)。大多数读者在学习网络通信相关技术的时候,都只是接触到零碎的通信技术点,没有完整的技术体系架构,以至于对Java的通信场景总是没有清晰的解决方案。

本文将通过大量清晰直接的案例从最基础的BlO式通信开始介绍到NIO、AIO,读者可以清晰的了解到阻塞同步异步 的现象、概念和特征以及优缺点。本文结合了大量的案例让读者可以快速了解每种通信架构的使用

2. 技术要求

  • 不适合0基础
  • 需要掌握:Java SE基础编程,如:Java多线程、Java IO编程、Java网络基础知识,还要了解一些Java设计模式
  • 能熟练掌握OOP编程,有一定的编程思维。

3. 通信技术整体上解决的问题

  • 局域网内的通信要求
  • 多系统间的底层消息传递机制
  • 高并发下,大数据量的通信场景需要,如:netty
  • 游戏行业,无论是手游,还是大型的网络游戏,Java语言越来越被广泛应用。

二、Java的IO演进之路

1.I/O模型基本说明

I/O模型:就是用什么样的通道或者说是通信模式和架构进行数据的传输和接收,很大程度上决定了程序通信的性能,Java共支持3种网络编程的I/O模型:BlO. NIO. AlO

实际通信需求下,要根据不同的业务场景和性能需求决定选择不同的I/O模型

2. I/O模型介绍(3种)

①Java BIO

同步阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的的线程开销,以下是简单示意图:

image-20240729154517430

②Java NIO

同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理【简单示意图】

image-20240729154619992

③Java AIO

也叫NIO.2,异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用

3. BIO、NIO、AIO适用场景分析

BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并局限于应用中, jDK1.4以前的唯一选择,但程序简单易理解。

NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器弹幕系统服务器间通讯等。 编程比较复杂,jDK1.4开始支持。

AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作, 编程比较复杂,JDK7开始支持。

三、JAVA BIO深入剖析

1. Java BIO基本介绍

Java BlO就是传统的Java IO编程,其相关的类和接口在Java.io包中。

BIO(blocking I/O): 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善 (实现多个客户连接服务器)。

2. Java BIO工作机制

image-20240729155238554

  • 服务端和客户端都是socket

  • 服务端:

    • 通过ServerSocket注册端口
    • 服务端通过调用accept方法用于监听客户端的Socket请求
    • 从Socket种获取字节输入或者输出流进行数据的读写操作
  • 客户端:

    • 通过Socket对象请求与服务端的连接
    • 从Socket中得到字节输入或者字节输出流进行数据的读写操作

3. 传统的BIO编程实例回顾

网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。

传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

基于BIO模式下的通信,客户端-服务端是完全同步,完全藕合的。

服务端代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
//服务端,目标:客户端发送消息,服务端接收消息
public class Server {public static void main(String[] args) {System.out.println("======服务端启动=======");try {//1.定义一个ServerSocket对象进行服务端的端口注册ServerSocket ss = new ServerSocket(9999);//2.监听客户端的Socket连接请求Socket socket = ss.accept();//3.从Socket管道中得到一个字节输入流对象InputStream is = socket.getInputStream();//4.把字节输入流包装成一个缓冲字符输入流BufferedReader reader = new BufferedReader(new InputStreamReader(is));String msg;if ((msg = reader.readLine()) != null) {System.out.println("服务端接收到:"+msg);}} catch (IOException e) {throw new RuntimeException(e);}}
}

客户端代码:

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
//客户端,连接上服务端,然后发送消息到服务端
public class Client {public static void main(String[] args) {//1.创建和服务端的连接try {Socket socket = new Socket("127.0.0.1",9999);//2.从socket对象中获取一个字节输出流OutputStream os = socket.getOutputStream();//3.把字节输出流包装成一个打印流PrintStream ps = new PrintStream(os);//4.输出消息ps.println("hello world!服务端,你好!");ps.flush();} catch (IOException e) {throw new RuntimeException(e);}}
}

启动服务端后启动客户端,效果如下:

======服务端启动=======
服务端接收到:hello world!服务端,你好!

小结:

在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态;同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!

4. BIO模式下多发和多收消息

上面的案例只能实现单发和单收消息,并不能实现反复的收消息和发消息。我们只需要在客户端案例中,加上反复按照行发送消息的逻辑即可!案例代码如下:

服务端代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
//服务端,目标:服务端可以反复的接收消息,客户端可以反复的发送消息
public class Server {public static void main(String[] args) {System.out.println("======服务端启动=======");try {//1.定义一个ServerSocket对象进行服务端的端口注册ServerSocket ss = new ServerSocket(9999);//2.监听客户端的Socket连接请求Socket socket = ss.accept();//3.从Socket管道中得到一个字节输入流对象InputStream is = socket.getInputStream();//4.把字节输入流包装成一个缓冲字符输入流BufferedReader reader = new BufferedReader(new InputStreamReader(is));String msg;while ((msg = reader.readLine()) != null) {System.out.println("服务端接收到:"+msg);}} catch (IOException e) {throw new RuntimeException(e);}}
}

客户端代码:

public class Client {public static void main(String[] args) {//1.创建和服务端的连接try {Socket socket = new Socket("127.0.0.1",9999);//2.从socket对象中获取一个字节输出流OutputStream os = socket.getOutputStream();//3.把字节输出流包装成一个打印流PrintStream ps = new PrintStream(os);Scanner scanner = new Scanner(System.in);String msg;while (true){//4.输出消息System.out.println("请说:");msg = scanner.nextLine();ps.println(msg);ps.flush();}} catch (IOException e) {throw new RuntimeException(e);}}
}

效果: 启动服务端->启动客户端->在客户端打字,服务端收到消息如下:

======服务端启动=======
服务端接收到:你好
服务端接收到:今天在干嘛
服务端接收到:好的

5. BIO模式下接收多个客户端

在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢 ,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程 来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:

image-20240729172908763

服务端代码:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
//目标:实现服务端可以同时接收到多个客户端的Socket通信需求
//思路:服务端每接收一个客户端的socket请求对象之后都交给一个独立的线程来处理客户端的数据交互需求
public class Server {public static void main(String[] args) {try {//1.注册端口ServerSocket ss = new ServerSocket(9999);//2.定义一个死循环,不断接收客户端的socket连接请求while(true){Socket socket = ss.accept();//3.创建一个独立的线程来处理与这个客户端的socket通信需求new ServerThreadReader(socket).start();}} catch (IOException e) {throw new RuntimeException(e);}}
}

线程处理代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
public class ServerThreadReader extends Thread {private Socket socket;public ServerThreadReader(Socket socket){this.socket = socket;}@Overridepublic void run() {try {//从socket对象中得到一个字节输入流InputStream is = socket.getInputStream();//使用缓存字符输入流包装字节输入流BufferedReader br = new BufferedReader(new InputStreamReader(is));String msg;while ((msg = br.readLine()) != null){System.out.println(msg);}} catch (IOException e) {e.printStackTrace();}}
}

客户端代码:

import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {public static void main(String[] args) {try {//1.请求与服务端的Socket对象连接Socket socket = new Socket("127.0.0.1",9999);//2. 得到一个打印流PrintStream ps = new PrintStream(socket.getOutputStream());//3. 使用循环不断的发送消息给服务端接收Scanner sc = new Scanner(System.in);while (true){System.out.println("请说:");String msg =sc.nextLine();ps.println(msg);ps.flush();}} catch (Exception e) {e.printStackTrace();}}
}

启动服务端,启动客户端1、客户端2,然后通过客户端1和客户端2分别发送消息,服务端效果如下:

客户端1
客户端2

小结

  • 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能
  • 每个线程都会占用栈空间和CPU资源
  • 并不是每个socket都进行lO操作,无意义的线程处理
  • 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出
  • 线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务

6. 伪异步I/O编程

在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现Java. lang. Runnable(线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

如下图所示:
image-20240729175115335

服务端代码:

import java.net.ServerSocket;
import java.net.Socket;
//目标:开发实现伪异步通讯架构
//思路:服务端每接收到一个客户端socket请求对象之后都交给一个独立的线程来处理客户端的数据交互需求
public class Server {public static void main(String[] args) {try {//1.注册端口ServerSocket ss = new ServerSocket(9999);//2.定义一个死循环,负责不断的接收客户端的Socket的连接请求//初始化一个线程池对象HandlerSocketServerPool pool = new HandlerSocketServerPool(3, 10);while (true) {Socket socket = ss.accept();//3.把socket对象交给一个线程池进行处理//把socket封装成一个任务对象交给线程池处理Runnable target = new ServerRunnableTarget(socket);pool.execute(target);}} catch (Exception e) {e.printStackTrace();}}
}

定义线程池代码:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class HandlerSocketServerPool {//1. 创建一个线程池的成员变量用于存储一个线程池对象private ExecutorService executorService;/*** 2.创建这个类的的对象的时候就需要初始化线程池对象* public ThreadPoolExecutor(int corePoolSize,* int maximumPoolSize,* long keepAliveTime,* TimeUnit unit,* BlockingQueue<Runnable> workQueue)*/public HandlerSocketServerPool(int maxThreadNum, int queueSize) {executorService = new ThreadPoolExecutor(3, maxThreadNum, 120,TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));}/*** 3.提供一个方法来提交任务给线程池的任务队列来暂存,等待线程池来处理*/public void execute(Runnable target) {executorService.execute(target);}
}

Socket任务类:

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
public class ServerRunnableTarget implements Runnable {private Socket socket;public ServerRunnableTarget(Socket socket) {this.socket = socket;}@Overridepublic void run() {//处理接收到的客户端socket通信需求try {//1.从socket管道中得到一个字节输入流对象InputStream is = socket.getInputStream();//2.把字节输入流包装成一个缓存字符输入流BufferedReader br = new BufferedReader(new InputStreamReader(is));String msg;while ((msg = br.readLine()) != null) {System.out.println("服务端收到:" + msg);}} catch (Exception e) {e.printStackTrace();}}
}

客户端代码:

import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {public static void main(String[] args) {try {//1.请求与服务端的Socket对象连接Socket socket = new Socket("127.0.0.1",9999);//2. 得到一个打印流PrintStream ps = new PrintStream(socket.getOutputStream());//3. 使用循环不断的发送消息给服务端接收Scanner sc = new Scanner(System.in);while (true){System.out.println("请说:");String msg =sc.nextLine();ps.println(msg);ps.flush();}} catch (Exception e) {e.printStackTrace();}}
}

效果:

可以接收到3个客户端的消息,当开启第四个客户端的时候,不报错,但是服务端不会接收到第四个客户端发送的消息。

小结

伪异步采用了线程池 实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。

如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的I/O消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。

7. 基于BIO形式下的文件上传

目标: 支持任意类型文件形式的上传

客户端代码:

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
//目标:实现客户端上传任意类型的文件数据给服务端保存起来
public class Client {public static void main(String[] args) {try(InputStream is = new FileInputStream("D:\\Desktop\\抖音的推荐机制.docx")) {//1.请求与服务端的socket连接Socket socket = new Socket("127.0.0.1", 8888);//2.把字节输出流包装成一个数据输出流DataOutputStream dos = new DataOutputStream(socket.getOutputStream());//3.先发送文件的后缀名给服务端dos.writeUTF(".docx");//4.把文件数据发送给服务端进行接收byte[] buffer = new byte[1024];int len;while ((len = is.read(buffer)) > 0) {dos.write(buffer, 0, len);}dos.flush();//通知服务端,我客户端这边的数据已经发送完毕了socket.shutdownOutput();} catch (IOException e) {throw new RuntimeException(e);}}
}

服务端代码:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {public static void main(String[] args) {System.out.println("======服务端启动=======");try {ServerSocket ss = new ServerSocket(8888);while (true) {Socket socket = ss.accept();//交给一个独立的线程来处理与这个客户端的文件通信需求new ServerReadThread(socket).start();}} catch (IOException e) {throw new RuntimeException(e);}}
}

Socket线程处理类:

import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.UUID;
public class ServerReadThread extends Thread{private Socket socket;public ServerReadThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {//1.得到一个数据输入流来读取客户端发送过来的数据DataInputStream dis = new DataInputStream(socket.getInputStream());//2.读取客户端发送过来的文件类型String suffix = dis.readUTF();System.out.println("服务端已经成功接收到文件类型:"+suffix);//3.定义一个字节输出管道,负责将客户端发送来的数据写出去OutputStream os = new FileOutputStream("D:\\Desktop\\server\\"+ UUID.randomUUID().toString() + suffix);//4.从数据输入流中读取文件数据,写出到字节输出流中去byte[] buffer = new byte[1024];int len;while ((len = dis.read(buffer)) > 0) {os.write(buffer, 0, len);}os.close();System.out.println("服务端接收文件保存成功!");} catch (IOException e) {throw new RuntimeException(e);}}
}

启动Server,后启动Client,效果如下:
image-20240730110300689

小结

  • 同步阻塞模式下(BIO),客户端怎么发,服务端就必须对应的怎么收。如客户端用的是DataOutputStream,那么服务端就该用DataInputStream,客户端dos.writeUTF(“.jpg”);服务端就该String suffix = dis.readUTF();
  • 客户端发完数据后必须通知服务端自己已经发完socket.shutdownOutput(),否则服务端会一直等待。

8. Java BIO模式下的端口转发思想

需求:需要实现一个客户端的消息可以发送给所有的客户端去接收。(群聊实现)
image-20240730110542203

服务端代码:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/*** 目标:BIO模式下的端口转发思想-服务端实现* 服务端实现需求:* 1.注册端口* 2.接收客户端的socket连接,交给一个独立的线程来处理* 3.把当前连接的客户端socket存入到一个所谓的在线socket集合中保存* 4.接收客户端的消息,然后推动给当前所有的在线socket接收*/
public class Server {public static List<Socket> allSocketOnline = new ArrayList<>();public static void main(String[] args) {try {ServerSocket ss = new ServerSocket(9999);while (true) {Socket socket = ss.accept();//把登陆的客户端socket存入到一个在线集合上allSocketOnline.add(socket);//为当前登录成功的socket分配一个独立的线程来处理与之通信new ServerReadThread(socket).start();}} catch (IOException e) {throw new RuntimeException(e);}}
}

服务端线程处理类:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class ServerReadThread extends Thread{private Socket socket;public ServerReadThread(Socket socket){this.socket = socket;}@Overridepublic void run() {try {//1.从socket中获取当前客户端的输入流BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));String msg;while ((msg = br.readLine())!=null) {//2.服务端接收到了客户端的消息之后,是需要推送给当前所有的在线socketsendMsgToAllClient(msg, socket);}} catch (Exception e) {e.printStackTrace();System.out.println("当前有人下线了");//从在线socket集合中移除本socketServer.allSocketOnline.remove(socket);}}/*** 把当前客户端发来的消息推送给全部在线的socket* @param msg*/private void sendMsgToAllClient(String msg, Socket socket) throws IOException {for (Socket socket1 : Server.allSocketOnline) {//只发送给除自己以外的客户端if (socket != socket1) {PrintStream ps = new PrintStream(socket1.getOutputStream());ps.println(msg);ps.flush();}}}
}

客户端代码:

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {public static void main(String[] args) {try {//1.请求与服务端的Socket对象连接Socket socket = new Socket("127.0.0.1", 9999);//收消息Thread clientThread = new ClientReaderThread(socket);clientThread.start();while (true) {//发消息OutputStream os = socket.getOutputStream();PrintStream ps = new PrintStream(os);//3. 使用循环不断的发送消息给服务端接收Scanner sc = new Scanner(System.in);//System.out.print("client send message:");String msg = sc.nextLine();ps.println(msg);ps.flush();}} catch (Exception e) {e.printStackTrace();}}
}

客户端线程处理类:

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
public class ClientReaderThread extends Thread {private Socket socket;public ClientReaderThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {while (true) {InputStream is = socket.getInputStream();//4.把字节输入流包装成一个缓存字符输入流BufferedReader br = new BufferedReader(new InputStreamReader(is));String msg;if ((msg = br.readLine()) != null) {System.out.println(msg);}}} catch (Exception e) {}}
}

效果

先启动服务端Server,然后启动3个客户端Client
image-20240730111455395

9. 基于BIO模式下即时通讯

基于BIO模式下的即时通信,我们需要解决客户端到客户端的通信,也就是需要实现客户端与客户端的端口消息转发逻辑。

项目案例说明

本项目案例为即时通信的软件项目,适合基础加强的大案例,具备综合性。学习本项目案例至少需要具备如下java SE技术点:

  1. java面向对象设计,语法设计
  2. 多线程技术
  3. IO流技术
  4. 网络通信相关技术
  5. 集合框架
  6. 项目开发思维
  7. java常用api使用

……

功能清单简单说明
  1. 客户端登录功能

可以启动客户端进行登录,客户端登录只需要输入用户名和服务端IP地址即可。

  1. 在线人数实时更新

客户端用户登录后,需要同步更新所有客户端的联系人信息栏。

  1. 离线人数更新

检测到有客户端下线后,需要同步更新所有客户端的联系人信息栏。

  1. 群聊

任意一个客户端的消息,可以推动给当前所有的客户端接收。

  1. 私聊

任意一个客户端消息,可以推送给当前所有客户端接收。

  1. @消息

可以选择某个员工,然后发出的消息可以@该用户,但是其他所有人都能收到消息。

  1. 消息用户和消息时间点

服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或则选择。

技术选型分析

Java GUI,BIO

服务端设计

服务端接收多个客户端逻辑

  1. 服务端需要接收多个客户端,目前我们采取的策略是 一个客户端对应一个服务端线程
  2. 服务端除了要注册端口以外,还需要为每个客户端分配 一个独立线程处理与之通信

服务端主体代码,主要进行端口注册,和接收客户端,分配线程处理该客户端请求

服务端接收登陆消息以及监测离线

在上面我们实现了服务端可以接收多个客户端连接,接下来我们要接收客户端的登陆消息。

我们需要在服务端处理客户端线程的登陆消息。需要注意的是,服务端需要接收客户端的消息可能有很多种,分别是登陆消息,群聊消息,私聊消息和@消息。这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。

  • 1代表接收的是登陆消息
  • 2代表群发| @消息
  • 3代表了私聊消息

服务端的线程中有异常校验机制,一旦发现客户端下线会在异常机制中处理,然后移除当前客户端用户,把最新的用户列表 发回给全部客户端进行在线人数更新。

服务端接收群聊消息

在上面实现了接收客户端的登陆消息,然后提取当前在线的全部的用户名称和当前登陆的用户名称,发送给全部在线用户更新自己的在线人数列表。接下来要接收客户端发来的群聊消息,然后推送给当前在线的所有客户端。

服务端接收私聊消息

解决私聊消息的推送逻辑,私聊消息需要知道推送给某个具体的客户端。我们可以接收到客户端发来的私聊用户名称,根据用户名称定位该用户的Socket管道,然后单独推送消息给该Socket管道。

代码实现

常量类:

public class Constants {//端口public static final int  PORT = 7778;//逗号分割public static final String SPLIT = ",";
}

ServerChat:

import com.linxc.bio.instant_message.util.Constants;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class ServerChat {//定义一个集合存放所有在线的socket//在线集合只需要一个,存储客户端socket的同时还需要知道这个socket的客户端名称public static Map<Socket, String> onLineSockets = new HashMap<>();public static void main(String[] args) {//注册端口ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(Constants.PORT);//循环一直等待所有可能的客户端连接while (true) {Socket socket = serverSocket.accept();//把客户端的socket管道单独配置一个线程来处理new ServerReader(socket).start();}} catch (Exception e) {e.printStackTrace();}}
}

ServerReader:

import com.linxc.bio.instant_message.util.Constants;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Set;
public class ServerReader extends Thread {private Socket socket;public ServerReader(Socket socket) {this.socket = socket;}@Overridepublic void run() {DataInputStream dis = null;try {dis = new DataInputStream(socket.getInputStream());//1.循环一直等待客户端的消息while (true) {//2.读取当前的消息类型:登录,群发,私聊,@消息int flag = dis.readInt();System.out.println("服务flag:"+flag);if (flag == 1) {//先将当前登录的客户端socket存到在线人数的socket集合中String name =dis.readUTF();System.out.println(name + "----->" + socket.getRemoteSocketAddress());ServerChat.onLineSockets.put(socket, name);}writeMsg(flag, dis);}} catch (Exception e) {System.out.println("---有人下线了---");ServerChat.onLineSockets.remove(socket);try {//重新更新在线人数并发给所有客户端writeMsg(1, dis);} catch (Exception e1) {e1.printStackTrace();}}}private void writeMsg(int flag, DataInputStream dis) throws Exception {//定义一个变量存放最终的消息形式String msg = null;if (flag == 1) {//读取所有在线人数发给所有客户端去更新自己的在线人数列表StringBuilder rs = new StringBuilder();Collection<String> onlineNames = ServerChat.onLineSockets.values();//判断是否存在在线人数if (onlineNames != null && onlineNames.size() > 0) {for (String name : onlineNames) {rs.append(name + Constants.SPLIT);}//去掉最后一个分隔符msg = rs.substring(0, rs.lastIndexOf(Constants.SPLIT));//将消息发送给所有的客户端sendMsgToAll(flag, msg);}}else if (flag == 2||flag ==3) {//读到消息群发的或者是@消息String newMsg = dis.readUTF();//消息String sendName = ServerChat.onLineSockets.get(socket);//内容StringBuilder msgFinal = new StringBuilder();//时间SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");if (flag == 2) {//群发消息和@消息msgFinal.append(sendName).append("").append(sdf.format(System.currentTimeMillis()*2)).append("\r\n");msgFinal.append(" ").append(newMsg).append("\r\n");sendMsgToAll(flag, msgFinal.toString());} else if (flag == 3) {//私聊消息msgFinal.append(sendName).append("").append(sdf.format(System.currentTimeMillis()*2)).append(" 对您私发\r\n");msgFinal.append("").append(newMsg).append("\r\n");//私发//得到发给谁String destName = dis.readUTF();sendMsgToOne(destName, msgFinal.toString());}System.out.println("发送消息:"+msgFinal);}}/*** @param destName 对谁私发* @param msg      发的消息内容* @throws Exception*/private void sendMsgToOne(String destName, String msg) throws Exception {//拿到所有在线的socket管道 给这些管道写出消息Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();for (Socket sk : allOnLineSockets) {//得到当前需要私发的Socket//只对这个名字对应的socket私发消息if (ServerChat.onLineSockets.get(sk).trim().equals(destName)) {DataOutputStream dos = newDataOutputStream(sk.getOutputStream());dos.writeInt(2);//消息类型dos.writeUTF(msg);dos.flush();}}}private void sendMsgToAll(int flag, String msg) throws Exception {//拿到所有的在线socket管道 给这些管道写出消息Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();for (Socket sk : allOnLineSockets) {DataOutputStream dos = new DataOutputStream(sk.getOutputStream());dos.writeInt(flag);//消息类型System.out.println(msg);dos.writeUTF(msg);dos.flush();}}
}
客户端设计

启动客户端界面,登录,刷新在线

客户端界面主要是GUI设计,主体页面分为登陆界面和聊天窗口,以及在线用户列表。

登陆输入服务端ip和用户名后,要请求与服务端的登陆,然后立即为当前客户端分配一个读线程处理客户端的读数据消息。因为客户端可能随时会接收到服务端那边转发过来的各种即时消息信息。客户端登陆完成,服务端收到登陆的用户名后,会立即发来最新的用户列表给客户端更新。

客户端发送消息逻辑

客户端发送群聊消息,@消息,以及私聊消息。

实现步骤: 客户端启动后,在聊天界面需要通过发送按钮推送群聊消息,@消息,以及私聊消息。

代码实现

Client:

import com.linxc.bio.instant_message.util.Constants;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataOutputStream;
import java.net.Socket;
public class ClientChat implements ActionListener {//设计界面private JFrame win = new JFrame();//消息内容框架public JTextArea smsContent = new JTextArea(23,50);//发送消息的框private JTextArea smsSend = new JTextArea(4,40);//在线人数的区域public JList<String> onlineUsers = new JList<>();//是否私聊按钮private JCheckBox isPrivateBn = new JCheckBox("私聊");//消息按钮private JButton sendBn = new JButton("发送");//登录界面private JFrame loginView;private JTextField ipEt, nameEt, idEt;private Socket socket;public static void main(String[] args) {new ClientChat().initView();}private void initView() {/** 初始化聊天窗口的界面 */win.setSize(650, 600);/** 展示登录界面 */displayLoginView();}private void displayChatView() {JPanel bottomPanel = new JPanel(new BorderLayout());//将消息框和按钮添加到窗口的底端win.add(bottomPanel, BorderLayout.SOUTH);bottomPanel.add(smsSend);JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));btns.add(sendBn);btns.add(isPrivateBn);bottomPanel.add(btns, BorderLayout.EAST);//-----------------------------------------------//给发送消息按钮绑定点击事件监听器//将展示消息区centerPanel添加到窗口的中间smsContent.setBackground(new Color(0xdd, 0xdd, 0xdd));//让展示消息区可以滚动win.add(new JScrollPane(smsContent), BorderLayout.CENTER);smsContent.setEditable(false);//-------------------------------------------------//用户列表和是否私聊放到窗口的最右边Box rightBox = new Box(BoxLayout.Y_AXIS);onlineUsers.setFixedCellWidth(120);onlineUsers.setVisibleRowCount(13);rightBox.add(new JScrollPane(onlineUsers));win.add(rightBox, BorderLayout.EAST);//-------------------------------------------------//关闭窗口退出当前程序win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);win.pack();//swing 加上这句 就可以拥有关闭窗口的功能/** 设置窗口居中,显示出来 */setWindowCenter(win, 650, 600, true);//发送按钮绑定点击事件sendBn.addActionListener(this);}private void displayLoginView() {/*** 先让用户进行登录* 服务端ip* 用户名* id*//** 显示一个qq的登录框 */loginView = new JFrame("登录");loginView.setLayout(new GridLayout(3, 1));loginView.setSize(400, 230);JPanel ip = new JPanel();JLabel label = new JLabel("IP:");ip.add(label);ipEt = new JTextField(20);ip.add(ipEt);loginView.add(ip);JPanel name = new JPanel();JLabel label1 = new JLabel("姓名:");name.add(label1);nameEt = new JTextField(20);name.add(nameEt);loginView.add(name);JPanel btnView = new JPanel();JButton login = new JButton("登录");btnView.add(login);JButton cancle = new JButton("取消");btnView.add(cancle);loginView.add(btnView);//关闭窗口退出当前程序loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);setWindowCenter(loginView, 400, 260, true);/** 给登录和取消绑定点击事件 */login.addActionListener(this);cancle.addActionListener(this);}private void setWindowCenter(JFrame frame, int width, int height, booleanflag) {/** 得到所在系统所在屏幕的宽高 */Dimension ds = frame.getToolkit().getScreenSize();/** 拿到电脑的宽 */int width1 = ds.width;/** 高 */int height1 = ds.height;System.out.println(width1 + "*" + height1);/** 设置窗口的左上角坐标 */frame.setLocation(width1 / 2 - width / 2, height1 / 2 - height / 2);frame.setVisible(flag);}@Overridepublic void actionPerformed(ActionEvent e) {//得到点击的事件源JButton btn = (JButton) e.getSource();switch (btn.getText()) {case "登录":String ip = ipEt.getText().toString();String name = nameEt.getText().toString();//校验参数是否为空//错误提示String msg = "";//12.1.2.0// \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\if (ip == null ||!ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")) {msg = "请输入合法的服务器ip地址";} else if (name == null || !name.matches("\\S{1,}")) {msg = "姓名必须1个字符以上";}if (!msg.equals("")) {//msg有内容说明参数有为空//参数一:弹出放到那个窗口里面JOptionPane.showMessageDialog(loginView, msg);} else {try {//参数都合法了//当前登录的用户,去服务器登录/** 先把当前用户的名称展示到界面*/win.setTitle(name);//去服务端登录连接一个socket管道socket = new Socket(ip, Constants.PORT);//为客户端的socket分配一个线程 专门负责收消息new ClientReader(this, socket).start();//带上用户信息过去DataOutputStream dos = newDataOutputStream(socket.getOutputStream());dos.writeInt(1);//登录消息dos.writeUTF(name.trim());dos.flush();//关闭当前窗口 弹出聊天界面loginView.dispose();//登录窗口销毁displayChatView();//展示了聊天窗口了} catch (Exception e1) {e1.printStackTrace();}}break;case "取消":/** 退出系统*/System.exit(0);break;case "发送"://得到发送消息的内容String msgSend = smsSend.getText().toString();if (!msgSend.trim().equals("")) {//发消息给服务端try {//判断是否对谁发消息String selectName = onlineUsers.getSelectedValue();int flag = 2;// 群发 @消息if (selectName != null && !selectName.equals("")) {msgSend = ("@" + selectName + "," + msgSend);//判断是否选中私发if (isPrivateBn.isSelected()) {//私发flag = 3;//私发消息}}DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(flag);//群发消息 发送给所有人dos.writeUTF(msgSend);if (flag == 3) {//告诉服务器端我对谁私发dos.writeUTF(selectName.trim());}dos.flush();} catch (Exception e1) {e1.printStackTrace();}}smsSend.setText(null);break;}}
}

ClientReader:

import com.linxc.bio.instant_message.util.Constants;
import java.io.DataInputStream;
import java.net.Socket;
import java.util.Arrays;
public class ClientReader extends Thread {private Socket socket;private ClientChat clientChat;public ClientReader(ClientChat clientChat, Socket socket) {this.clientChat = clientChat;this.socket = socket;}@Overridepublic void run() {try {DataInputStream dis = new DataInputStream(socket.getInputStream());/** 循环一直等待客户端的消息 */while (true) {/** 读取当前的消息类型: 登录,群发,私聊,@消息 */int flag = dis.readInt();if (flag == 1) {//在线人数消息回来了String nameDatas = dis.readUTF();System.out.println(nameDatas);//展示到在线人数的界面String[] names = nameDatas.split(Constants.SPLIT);System.out.println(Arrays.toString(names));clientChat.onlineUsers.setListData(names);} else if (flag == 2) {//群发,私聊 ,@消息 都是直接显示的String msg = dis.readUTF();clientChat.smsContent.append(msg);//让消息界面滚动到低端clientChat.smsContent.setCaretPosition(clientChat.smsContent.getText().length());}}} catch (Exception e) {e.printStackTrace();}}
}

我的博客:NIO专题(一)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/392806.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PXE 服务器搭建——启动界面设计实验

环境准备&#xff1a; 前期准备&#xff1a; 解决 kickstart 实验出现的 DHCP 的问题-CSDN博客 http://t.csdnimg.cn/5vZP0 当前准备&#xff1a; 两台虚拟机&#xff1a;RHEL7 OpenEuler(作为测试机器使用) ip&#xff1a;172.25.254.100 yum install syslinux.x…

【Web开发手礼】探索Web开发的秘密(十五)-Vue2(2)AJAX、前后端分离、前端工程化

主要介绍了AJAX、前后端分离所需的YApi、前端工程化所需要的环境安装&#xff01;&#xff01;&#xff01; 目录 前言 AJAX ​原生Ajax Axios Axios入门 案例 前后端分离开发 YApi ​前端工程化 环境准备 总结 前言 主要介绍了AJAX、前后端分离所需的YApi、前端工…

2024华数杯c题题解(一)

目录 原题背景 背景分析 问题一 思路 代码 问题二 思路 代码 原题背景 最近&#xff0c;“city 不 city”这一网络流行语在外国网红的推动下备受关注。随着我国过境免签政策的落实&#xff0c;越来越多外国游客来到中国&#xff0c;通过网络平台展示他们在华旅行的见闻…

出现 No mapping for DELETE/GET等

出现 No mapping for DELETE/GET等 错误一&#xff1a;请求url不对 修改前 如下图可知后端请求url为http://localhost:8080/user/addressBook 运行后控制台出现 发现后端请求url比前端请求url少了/ 改正&#xff1a; 在DeleteMapping后面加上 / DeleteMapping("/&quo…

mysql操作(进阶)

1.数据库约束 数据库自动对数据的合法性进行校验检查的一系列机制&#xff0c;目的是为了保证数据库中能够避免被插入或者修改一些非法数据。 &#xff08;1&#xff09;mysql中提供了以下的约束&#xff1a; a.NOT NULL&#xff1a;指定某列不能为null b.UNIQUE&#xff1…

搭建pxe网络安装环境实现服务器自动部署

目录 配置 kickstart自动安装脚本 搭建dhcp服务 搭建pxe网络安装环境实现服务器自动部署 测试 配置 kickstart自动安装脚本 yum install system-config-kickstart #在rhel7做&#xff0c;rhel9要收费 system-config-kickstart #启动图形制作工具 vim …

工具收集 - tinytask(相当于迷你的按键精灵)

工具收集 - tinytask&#xff08;相当于迷你的按键精灵&#xff09; 简介首页 简介 TinyTask 是一款极简主义的 PC 自动化应用程序&#xff0c;您可以用它来记录和重复操作。顾名思义&#xff0c;它小得令人难以置信&#xff08;仅 36KB&#xff01;&#xff09;&#xff0c;极…

C++第三十一弹---C++继承机制深度剖析(下)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 1.菱形继承及菱形虚拟继承 1.1 单继承 单继承&#xff1a;一个子类只有一个直接父类时称这个继承关系为单继承。 Student的直接父类是Person&#xff…

openai command not found (mac)

题意&#xff1a;mac 系统上无法识别 openai 的命令 问题背景&#xff1a; Im trying to follow the fine tuning guide for Openai here. 我正在尝试遵循 OpenAI 的微调指南 I ran: 我运行以下命令 pip install --upgrade openaiWhich install without any errors.…

干货!如何选择Ai大模型(LLMs)?

过去一年里&#xff0c;大型语言模型&#xff08;LLMs&#xff09;在人工智能界风起云涌&#xff0c;纷纷以突破性的进步拓展生成式人工智能的可能性。新模型层出不穷&#xff0c;令人目不暇接。 这些模型依靠日益增长的参数数量和庞大的数据集进行训练&#xff0c;显著提升了…

redis的key莫名失踪?

背景 在线上环境下&#xff0c;发现redis中key被莫名奇妙的删除了&#xff0c;起初怀疑是key过期但是查询了TTL并没有&#xff0c;在日志上加了删除key操作的打印 但是并没有看到产出这个key的日志记录&#xff0c;而且每次都是这个key被莫名奇妙删除掉&#xff0c;感觉很诡异。…

blender里的辉光

cycle的辉光&#xff0c; 点开支持后期效果 eevee的辉光&#xff0c;直接点bloom就行。 eevee的透明

sqli-labs-master初学者题目练习

Less-1 从源码可以看出id为注入点&#xff0c;且为单引号过滤 使用 闭合 --为注释 原本应该用--‘space’&#xff0c;但-与‘连在一起无法起到注释作用 order by为联合查询——同时查询两张表&#xff0c;但两张表列数必须相同 所有从以上两张图可以看出此表格有三列数据 爆…

防火墙工具iptables应用详解

文章目录 前言一、Netfilter内核二、Netfilter与iptables的关系三、iptables的表与链四、iptables的常用命令与参数五、 iptables使用案例 前言 iptables是Linux系统中一款强大的防火墙工具&#xff0c;它基于Netfilter内核模块&#xff0c;允许管理员定义数据包的转发、过滤和…

了解MVCC

概念 MVCC&#xff0c;全称Multi-Version Concurrency Control&#xff0c;即多版本并发控制&#xff0c;是一种并发控制的方法&#xff0c;维护一个数据的多个版本&#xff0c;使得读写操作没有冲突&#xff0c;快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现…

超级会员卡积分商城小程序多功能源码系统 带完整的安装代码包以及部署教程

源码系统概述 超级会员卡积分商城小程序多功能源码系统是一款集合了多种功能于一体的会员积分商城小程序源码系统。该系统采用先进的技术架构&#xff0c;支持多门店统一管理&#xff0c;提供丰富的会员信息和商品管理服务&#xff0c;支持多种支付方式和营销活动&#xff0c;同…

认真学习JVM中类加载过程

本文我们总结JVM中类加载器子系统关于类加载过程&#xff0c;这里默认是Oracle的Hotspot。 【1】类加载器子系统作用 类加载器子系统负责从文件系统或者网络中加载Class文件&#xff0c;class文件在文件开头有特定的文件标识。 ClassLoader只负责class文件的加载&#xff0…

一款一站式、开源、高质量的数据提取工具,支持:PDF 文档、网页和电子书提取

大家好&#xff0c;今天给大家分享的是一款一站式、开源、高质量的数据提取工具MinerU&#xff0c;主要包含以下功能: Magic-PDF PDF 文档提取 Magic-Doc 网页与电子书提取 项目介绍 Magic-PDF 简介 Magic-PDF 是一款将 PDF 转化为 markdown 格式的工具。支持转换本地文档…

ABAP+json格式数据转换时参数为空没传值

CALL METHOD /UI2/CL_JSON>SERIALIZE 我们在ABAP传输json格式数据到外围系统时&#xff0c;会用到这个类方法 /UI2/CL_JSON>SERIALIZE CALL METHOD /UI2/CL_JSON>SERIALIZEEXPORTINGDATA LO_DATACOMPRESS XPRETTY_NAME /UI2/CL_JSON>PRETTY_M…

【Linux】网络基础_3

文章目录 十、网络基础5. socket编程socket 常见APIsockaddr结构简单的UDP网络程序 未完待续 十、网络基础 5. socket编程 socket 常见API // 创建 socket 文件描述符 (TCP/UDP, 客户端 服务器) int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服…