👑 博主简介:知名开发工程师
👣 出没地点:北京
💊 2023年目标:成为一个大佬
———————————————————————————————————————————
版权声明:本文为原创文章,如需转载须注明出处,喜欢可收藏!
一. Java BIO 基本介绍
- Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.io 中。
- BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
二. Java BIO 常见案例
对 BIO 编程流程的梳理
- 服务器端启动一个
ServerSocket
,注册端口,调用accpet
方法监听客户端的Socket
连接。 - 客户端启动
Socket
对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
1. 基本案例
一个简单客户端,服务端通信案例,学过 java 网络编程基础,应该看得懂
//客户端
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("==客户端的启动==");
// (1)创建一个Socket的通信管道,请求与服务端的端口连接。
Socket socket = new Socket("127.0.0.1",8888);
// (2)从Socket通信管道中得到一个字节输出流。
OutputStream os = socket.getOutputStream();
// (3)把字节流改装成自己需要的流进行数据的发送
PrintStream ps = new PrintStream(os);
// (4)开始发送消息
ps.println("我是客户端,我想约你吃小龙虾!!!");
ps.flush();
}
}
//服务端
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("==服务器的启动==");
// (1)注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//(2)开始在这里暂停等待接收客户端的连接,得到一个端到端的Socket管道
Socket socket = serverSocket.accept();
//(3)从Socket管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
//(4)把字节输入流包装成自己需要的流进行数据的读取。
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//(5)读取数据
String line ;
while((line = br.readLine())!=null){
System.out.println("服务端收到:"+line);
}
}
}
小结
- 在以上通信中,服务端会一致等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。
- 同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态。
2. 模式下多发和多收消息
上面代码中只能实现客户端发送消息,服务端接收消息,并不能实现反复的收消息和反复的发消息,我们只需要在客户端案例中,加上反复按照行发送消息的逻辑即可!案例代码如下:
// 客户端
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("==客户端的启动==");
// (1)创建一个Socket的通信管道,请求与服务端的端口连接。
Socket socket = new Socket("127.0.0.1",8888);
// (2)从Socket通信管道中得到一个字节输出流。
OutputStream os = socket.getOutputStream();
// (3)把字节流改装成自己需要的流进行数据的发送
PrintStream ps = new PrintStream(os);
// (4)开始发送消息
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
}
}
//服务器
public class ServerDemo {
public static void main(String[] args) throws Exception {
String s = "886";
System.out.println("886".equals(s));
System.out.println("==服务器的启动==");
//(1)注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//(2)开始在这里暂停等待接收客户端的连接,得到一个端到端的Socket管道
Socket socket = serverSocket.accept();
//(3)从Socket管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
//(4)把字节输入流包装成 自己需要的流进行数据的读取。
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//(5)读取数据
String line ;
while((line = br.readLine())!=null){
System.out.println("服务端收到:"+line);
}
}
}
小结
- 本案例中确实可以实现客户端多发多收
- 但是服务端只能处理一个客户端的请求,因为服务端是单线程的。一次只能与一个客户端进行消息通信。
3. BIO模式下接收多个客户端
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:
// 客户端
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("==客户端的启动==");
// (1)创建一个Socket的通信管道,请求与服务端的端口连接。
Socket socket = new Socket("127.0.0.1",7777);
// (2)从Socket通信管道中得到一个字节输出流。
OutputStream os = socket.getOutputStream();
// (3)把字节流改装成自己需要的流进行数据的发送
PrintStream ps = new PrintStream(os);
// (4)开始发送消息
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
}
}
// 服务端
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("==服务器的启动==");
// (1)注册端口
ServerSocket serverSocket = new ServerSocket(7777);
while(true){
//(2)开始在这里暂停等待接收客户端的连接,得到一个端到端的Socket管道
Socket socket = serverSocket.accept();
new ServerReadThread(socket).start();
System.out.println(socket.getRemoteSocketAddress()+"上线了!");
}
}
}
// 线程处理类
class ServerReadThread extends Thread{
private Socket socket;
public ServerReadThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
//(3)从Socket管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
//(4)把字节输入流包装成自己需要的流进行数据的读取。
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//(5)读取数据
String line ;
while((line = br.readLine())!=null){
System.out.println("服务端收到:"+socket.getRemoteSocketAddress()+":"+line);
}
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了!");
}
}
}
小结
- 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
- 每个线程都会占用栈空间和CPU资源;
- 客户端的并发访问增加时。服务端将呈现 1:1 的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
4. 伪异步I/O编程
在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
图示:
客户端代码:
public class Client {
public static void main(String[] args) {
try {
// 1.简历一个与服务端的Socket对象:套接字
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从socket管道中获取一个输出流,写数据给服务端
OutputStream os = socket.getOutputStream() ;
// 3.把输出流包装成一个打印流
PrintWriter pw = new PrintWriter(os);
// 4.反复接收用户的输入
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null ;
while((line = br.readLine()) != null){
pw.println(line);
pw.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
线程池处理类:
public class HandlerSocketThreadPool {
// 线程池
private ExecutorService executor;
public HandlerSocketThreadPool(int maxPoolSize, int queueSize){
this.executor = new ThreadPoolExecutor(
3, // 8
maxPoolSize,
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize) );
}
public void execute(Runnable task){
this.executor.execute(task);
}
}
服务端代码:
public class Server {
public static void main(String[] args) {
try {
System.out.println("----------服务端启动成功------------");
ServerSocket ss = new ServerSocket(9999);
// 一个服务端只需要对应一个线程池
HandlerSocketThreadPool handlerSocketThreadPool =
new HandlerSocketThreadPool(3, 1000);
// 客户端可能有很多个
while(true){
Socket socket = ss.accept() ; // 阻塞式的!
System.out.println("有人上线了!!");
// 每次收到一个客户端的socket请求,都需要为这个客户端分配一个
// 独立的线程 专门负责对这个客户端的通信!!
handlerSocketThreadPool.execute(new ReaderClientRunnable(socket));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ReaderClientRunnable implements Runnable{
private Socket socket ;
public ReaderClientRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 读取一行数据
InputStream is = socket.getInputStream() ;
// 转成一个缓冲字符流
Reader fr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(fr);
// 一行一行的读取数据
String line = null ;
while((line = br.readLine())!=null){ // 阻塞式的!!
System.out.println("服务端收到了数据:"+line);
}
} catch (Exception e) {
System.out.println("有人下线了");
}
}
}
小结
- 伪异步 io 采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续 socket 的 i/o 消息都将在队列中排队。新的 Socket 请求将被拒绝,客户端会发生大量连接超时。
6. 基于BIO形式下的文件传输
支持任意类型文件形式的上传
客户端:
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("C:\\Users\\java.png");
// 1、请求与服务端的Socket链接
Socket socket = new Socket("127.0.0.1", 8888);
// 2、把字节输出流包装成一个数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
// 3、先发送上传文件的后缀给服务端
dos.writeUTF(".png");
// 4、把文件数据发送给服务端进行接收
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
dos.write(buffer, 0, len);
}
dos.flush();
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
服务端
public static void main(String[] args) {
try{
ServerSocket ss = new ServerSocket(8888);
while (true){
Socket socket = ss.accept();
// 交给一个独立的线程来处理与这个客户端的文件通信需求。
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
//线程类
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
// 1、得到一个数据输入流读取客户端发送过来的数据
DataInputStream dis = new DataInputStream(socket.getInputStream());
// 2、读取客户端发送过来的文件类型
String suffix = dis.readUTF();
System.out.println("服务端已经成功接收到了文件类型:" + suffix);
// 3、定义一个字节输出管道负责把客户端发来的文件数据写出去
OutputStream os = new FileOutputStream("C:\\Users\\"+
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 (Exception e){
e.printStackTrace();
}
}
}
三. 即时通信综合案例
基于BIO模式下的即时通信,我们需要解决客户端到客户端的通信,也就是需要实现客户端与客户端的端口消息转发逻辑。
1. 项目案例说明
功能清单简单说明:
- 客户端登陆功能:需要输入用户名和服务端ip地址
- 在线人数实时更新:客户端用户户登陆以后,需要同步更新所有客户端的联系人列表
- 离线人数更新:检测到有客户端下线后,需要同步更新所有客户端的联系人列表
- 群聊:任意一个客户端的消息,可以推送给当前所有客户端接收
- 私聊:可以选择某个员工,点击私聊按钮,然后发出的消息可以被该客户端单独接收
- @消息:可以选择某个员工,然后发出的消息可以@该用户
- 记录消息时间点:服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或者选择。
2. 项目代码结构演示
3. 项目启动步骤
- 首先需要启动服务端,点击ServerChat类直接右键启动,显示服务端启动成功
- 其次,点击客户端类ClientChat类,在弹出的方框中输入服务端的ip和当前客户端的昵称
- 私聊:需要点击选中某人,然后勾选私聊即可发送私聊消息
如需完整代码可自行下载:java socket 多人聊天室源码