1.JAVA BIO深入剖析
1.1 Java BIO 基本介绍
-
Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.io
-
BIO(blocking I/O) :同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
-
每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。
-
-
连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
1.2 Java BIO 工作机制
BIO编程流程
0.服务器端启动一个 ServerSocket,注册端口,调用accpet方法监听客户端的Socket连接。
1.客户端启动 Socket和服务器进行通信,默认情况下服务端需要跟每个客户建立一个线程与之通讯。
1.3 传统的BIO编程实例回顾
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口。
客户端Socket负责发起连接操作。
连接成功后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通信,客户端 – 服务端是完全同步,完全耦合的。
服务端案例如下
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("==服务器的启动==");
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line ;
while((line = br.readLine())!=null){
System.out.println("服务端收到:"+line);
}
}
}
客户端案例如下
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("==客户端的启动==");
Socket socket = new Socket("127.0.0.1",8888);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("我是客户端,我想约你吃小龙虾!!!");
ps.flush();
}
}
小结
-
在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。 -
同时服务端是按照行获取消息,意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!
1.4 BIO模式下多发和多收消息
在1.3的案例中, 只能实现客户端发送消息,服务端接收消息,并不能实现反复的收消息和反复的发消息,我们只需要在客户端案例中,加上反复按照行发送消息的逻辑即可!案例代码如下:
服务端代码如下
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("==服务器的启动==");
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line ;
while((line = br.readLine())!=null){
System.out.println("服务端收到:"+line);
}
}
}
客户端代码如下
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("==客户端的启动==");
Socket socket = new Socket("127.0.0.1",8888);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
}
}
小结
-
本案例中确实可以实现客户端多发多收 -
但是服务端只能处理一个客户端的请求,因为服务端是单线程的。一次只能与一个客户端进行消息通信。
1.5 BIO模式下接收多个客户端
概述
在上述的案例中,一个服务端只能接收一个客户端的通信请求, 那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:
服务端案例代码
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("==服务器的启动==");
ServerSocket serverSocket = new ServerSocket(7777);
while(true){
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;
}
public void run() {
try{
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line ;
while((line = br.readLine())!=null){
System.out.println("服务端收到:"+
socket.getRemoteSocketAddress()+":"+line);
}
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了!");
}
}
}
客户端案例代码
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("==客户端的启动==");
Socket socket = new Socket("127.0.0.1",7777);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
}
}
小结
-
1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能; -
2.每个线程都会占用栈空间和CPU资源; -
3.并不是每个socket都进行IO操作,无意义的线程处理; -
4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
1.6 使用BIO模拟NIO
概述:BIO + 线程池
在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
但是如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
图示如下:
线程池处理类
public class HandlerSocketThreadPool {
private ExecutorService executor;
public HandlerSocketThreadPool(int maxPoolSize, int queueSize){
this.executor = new ThreadPoolExecutor(
3,
maxPoolSize,
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue(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("有人上线了!!");
handlerSocketThreadPool.execute(new ReaderClientRunnable(socket));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ReaderClientRunnable implements Runnable{
private Socket socket ;
public ReaderClientRunnable(Socket socket) {
this.socket = socket;
}
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("有人下线了");
}
}
}
客户端源码分析
public class Client {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream os = socket.getOutputStream() ;
PrintWriter pw = new PrintWriter(os);
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();
}
}
}
小结
-
伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。 -
*如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
比如核心线程数和最大线程数都设置1,但是客户端有2个。客户端A来连接的时候,如果它一直不下线,客户端B是一直无法被处理的,这个时候使用BIO+线程池就会陷入和1.5案例一样的困境:如果其它的客户端不下线,必须1个客户端对应1个线程,其它的客户端只能在队列中等待。除非客户端A下线释放线程池中的线程,这个时候客户端B才有被处理的可能。
-
采用 多线程+ 阻塞IO 可以达到NIO类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。 而多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。*多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态是通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
到底什么是多路IO复用,后续再讲。
原文始发于微信公众号(夏壹分享):JAVA BIO深入剖析
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/154889.html