摘要
看了很多关于IO模型的文章,对于传统阻塞以及传统为什么又能处理多客户端消息一直没有很直观的理解。所以决定通过最原始的方式,增加打印语句查看程序运行情况。参考了别人的文章,将代码进行了修改。
BIO服务端单线程处理
服务端代码:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
public class ServerSingle {
public static void main(String[] args) {
byte[] buffer = new byte[1024];
try {
ServerSocket serverSocket = new ServerSocket(7001);
System.out.println(LocalDateTime.now() + " " + "服务器已启动并监听7001端口");
while (true) {
System.out.println(LocalDateTime.now() + " " + "服务器正在等待连接...");
Socket socket = serverSocket.accept();
System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "服务器已接收到连接请求..." + socket.getInetAddress() + " " + socket.getPort());
System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "服务器正在等待数据...");
socket.getInputStream().read(buffer);
System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "服务器已经接收到数据");
String content = new String(buffer);
System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "接收到的数据:" + content);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端代码:
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
public class Consumer {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 7001);
String message = null;
Scanner sc = new Scanner(System.in);
message = sc.next();
socket.getOutputStream().write(message.getBytes());
socket.close();
sc.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
启动服务端
2020-03-16T12:01:20.766 服务器已启动并监听7001端口
2020-03-16T12:01:20.766 服务器正在等待连接…
通过CMD客户端启动客户端。SocketIO_01\target\classes>java mybio1.Consumer
2020-03-16T12:01:20.766 服务器已启动并监听7001端口
2020-03-16T12:01:20.766 服务器正在等待连接…
2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器已接收到连接请求…/127.0.0.1 55104
2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器正在等待数据…
客户端输入数据:12312312323214
2020-03-16T12:01:20.766 服务器已启动并监听7001端口
2020-03-16T12:01:20.766 服务器正在等待连接…
2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器已接收到连接请求…/127.0.0.1 55104
2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器正在等待数据…
2020-03-16T12:02:35.160 客户端端口 55104请求: 服务器已经接收到数据
2020-03-16T12:02:35.160 客户端端口 55104请求: 接收到的数据:12312312323214
2020-03-16T12:02:35.160 服务器正在等待连接…
-
结论
从上文的运行结果中我们可以看到,服务器端在启动后,首先需要等待客户端的连接请求(第一次阻塞),如果没有客户端连接,服务端将一直阻塞等待,然后当客户端连接后,服务器会等待客户端发送数据(第二次阻塞),如果客户端没有发送数据,那么服务端将会一直阻塞等待客户端发送数据。服务端从启动到收到客户端数据的这个过程,将会有两次阻塞的过程。这就是BIO的非常重要的一个特点,BIO会产生两次阻塞,第一次在等待连接时阻塞,第二次在等待数据时阻塞。 -
在单线程条件下BIO的弱点
在上文中,我们实现了一个简易的服务器,这个简易的服务器是以单线程运行的,其实我们不难看出,当我们的服务器接收到一个连接后,并且没有接收到客户端发送的数据时,是会阻塞在read()方法中的,那么此时如果再来一个客户端的请求,服务端是无法进行响应的。换言之,在不考虑多线程的情况下,BIO是无法处理多个客户端请求的。 -
BIO如何处理并发
在刚才的服务器实现中,我们实现的是单线程版的BIO服务器,不难看出,单线程版的BIO并不能处理多个客户端的请求,那么如何能使BIO处理多个客户端请求呢。其实不难想到,我们只需要在每一个连接请求到来时,创建一个线程去执行这个连接请求,就可以在BIO中处理多个客户端请求了,这也就是为什么BIO的其中一条概念是服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。
BIO服务端增加子线程处理
为了让服务端能够并发处理多个客户端消息,在服务端增加子进程。
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
public class ServerMulti {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(7001);
System.out.println(LocalDateTime.now() + " " + "服务器已启动并监听7001端口");
while (true) {
System.out.println(LocalDateTime.now() + " " + "服务器正在等待连接...");
final Socket socket = serverSocket.accept();
new Thread(new Runnable() {
public void run() {
byte[] buffer = new byte[1024];
System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "服务器已接收到连接请求..." + socket.getInetAddress());
System.out.println(LocalDateTime.now() + " " + " 客户端端口 " + socket.getPort() + "请求: " + "服务器正在等待数据...");
try {
socket.getInputStream().read(buffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(LocalDateTime.now() + " " + " 客户端端口 " + socket.getPort() + "请求: " + "服务器已经接收到数据");
String content = new String(buffer);
System.out.println(LocalDateTime.now() + " " + " 客户端端口 " + socket.getPort() + "请求: " + "接收到的数据:" + content);
}
}).start();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
在IDEA中启动服务端。通过WindowsCMD命令窗口查看。
jps -l
jstack -l 进程ID | find “Con”
BIO子线程模式下运行结果
C:\Users\admin>jps -l
21456 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
10264 sun.tools.jps.Jps
13656 org.jetbrains.jps.cmdline.Launcher
20136
20968 org.jetbrains.idea.maven.server.RemoteMavenServer
3176 bhz.mybio1.Consumer
16844
22252 bhz.mybio1.Consumer
9532 bhz.mybio1.ServerMulti
启动三个客户端。。。。。
C:\Users\admin>jps -l
16240 org.jetbrains.idea.maven.server.RemoteMavenServer
6864 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
15524 mybio1.Consumer
17812 sun.tools.jps.Jps
18516 org.jetbrains.jps.cmdline.Launcher
19000
15836 mybio1.Consumer
15996 mybio1.Consumer
5100 mybio1.ServerMulti
CompilerThread 编译线程,启动就有,先不管。
客户端连接上之后就新增了线程,可以比较一下。
运行结果:
C:\Users\admin>jstack -l 5100
2020-03-16 11:57:01
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode):
"Thread-5" #17 prio=5 os_prio=0 tid=0x000000001e823800 nid=0x4470 runnable [0x00000000013ae000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.net.SocketInputStream.read(SocketInputStream.java:127)
at mybio1.ServerMulti$1.run(ServerMulti.java:23)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Thread-4" #16 prio=5 os_prio=0 tid=0x000000001e79d800 nid=0x7b8 runnable [0x00000000012ae000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.net.SocketInputStream.read(SocketInputStream.java:127)
at mybio1.ServerMulti$1.run(ServerMulti.java:23)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Thread-3" #15 prio=5 os_prio=0 tid=0x000000001e79b800 nid=0x2ec runnable [0x00000000011ae000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.net.SocketInputStream.read(SocketInputStream.java:127)
at mybio1.ServerMulti$1.run(ServerMulti.java:23)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001e818800 nid=0x2fe0 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001e776800 nid=0x47dc waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001e774000 nid=0x2ec8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001e773000 nid=0x1654 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001e76d800 nid=0x2fbc waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001e761000 nid=0x2af4 runnable [0x000000001fd4e000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076b84b910> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000076b84b910> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
Locked ownable synchronizers:
- None
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001e6b3000 nid=0x2a88 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001e6b2000 nid=0x3920 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001e693800 nid=0x3eb8 in Object.wait() [0x000000001f9ef000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x000000076b588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
Locked ownable synchronizers:
- None
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x00000000033db000 nid=0x466c in Object.wait() [0x000000001f8ee000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x000000076b586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
Locked ownable synchronizers:
- None
"main" #1 prio=5 os_prio=0 tid=0x00000000032e3800 nid=0x2a40 runnable [0x00000000031de000]
java.lang.Thread.State: RUNNABLE
at java.net.DualStackPlainSocketImpl.accept0(Native Method)
at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)
- locked <0x000000076b71a248> (a java.net.SocksSocketImpl)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at mybio1.ServerMulti.main(ServerMulti.java:16)
Locked ownable synchronizers:
- None
"VM Thread" os_prio=2 tid=0x000000001e672800 nid=0x1d20 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000032fa000 nid=0x3ef0 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000032fb800 nid=0x4994 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000032fd000 nid=0x2744 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000032ff800 nid=0x4148 runnable
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000003301800 nid=0x4254 runnable
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000003303000 nid=0x32ac runnable
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000003306000 nid=0x1510 runnable
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000003307000 nid=0x1b6c runnable
"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x0000000003308800 nid=0x32d4 runnable
"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x0000000003309800 nid=0x4134 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x000000001e846000 nid=0x28ac waiting on condition
JNI global references: 314
三个窗口再输入数据。查看控制台打印信息。
2020-03-16T11:55:51.450 服务器正在等待连接…
2020-03-16T11:55:51.450 客户端端口 54691请求: 服务器已接收到连接请求…/127.0.0.1
2020-03-16T11:55:51.450 客户端端口 54691请求: 服务器正在等待数据…
2020-03-16T11:55:56.259 服务器正在等待连接…
2020-03-16T11:55:56.260 客户端端口 54708请求: 服务器已接收到连接请求…/127.0.0.1
2020-03-16T11:55:56.260 客户端端口 54708请求: 服务器正在等待数据…
2020-03-16T11:55:59.098 服务器正在等待连接…
2020-03-16T11:55:59.098 客户端端口 54710请求: 服务器已接收到连接请求…/127.0.0.1
2020-03-16T11:55:59.098 客户端端口 54710请求: 服务器正在等待数据…
2020-03-16T11:58:19.168 客户端端口 54710请求: 服务器已经接收到数据
2020-03-16T11:58:19.168 客户端端口 54710请求: 接收到的数据:33333333
2020-03-16T11:58:25.413 客户端端口 54691请求: 服务器已经接收到数据
2020-03-16T11:58:25.413 客户端端口 54691请求: 接收到的数据:1111111
2020-03-16T11:58:34.100 客户端端口 54708请求: 服务器已经接收到数据
2020-03-16T11:58:34.100 客户端端口 54708请求: 接收到的数据:222222
多线程BIO服务器的弊端
多线程BIO服务器虽然解决了单线程BIO无法处理并发的弱点,但是也带来一个问题:如果有大量的请求连接到我们的服务器上,但是却不发送消息,那么我们的服务器也会为这些不发送消息的请求创建一个单独的线程,那么如果连接数少还好,连接数一多就会对服务端造成极大的压力。所以如果这种不活跃的线程比较多,我们应该采取单线程的一个解决方案,但是单线程又无法处理并发,这就陷入了一种很矛盾的状态,于是就有了NIO。
Java中BIO和NIO的概念
通常一些文章都是在开头放上概念,但是我这次选择将概念放在结尾,因为通过上面的实操,相信大家对Java中BIO和NIO都有了自己的一些理解,这时候再来看概念应该会更好理解一些了。
概念整理于:
https://blog.csdn.net/guanghuichenshao/article/details/79375967
先来个例子理解一下概念,以银行取款为例:
同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)。 异步 :
委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)。
阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)。 非阻塞 :
柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。
Java对BIO、NIO的支持:
Java BIO (blocking I/O):
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
Java NIO (non-blocking I/O):
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
BIO、NIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
关键观点
关于使用select/epoll和直接在应用层做轮询的区别
我们在之前实现了一个使用Java做多个客户端连接轮询的逻辑,但是在真正的NIO源码中其实并不是这么实现的,NIO使用了操作系统底层的轮询系统调用 select/epoll(windows:select,linux:epoll),那么为什么不直接实现而要去调用系统来做轮询呢?
假设有A、B、C、D、E五个连接同时连接服务器,那么根据我们上文中的设计,程序将会遍历这五个连接,轮询每个连接,获取各自数据准备情况,那么和我们自己写的程序有什么区别呢?
首先,我们写的Java程序其本质在轮询每个Socket的时候也需要去调用系统函数,那么轮询一次调用一次,会造成不必要的上下文切换开销。
而Select会将五个请求从用户态空间全量复制一份到内核态空间,在内核态空间来判断每个请求是否准备好数据,完全避免频繁的上下文切换。所以效率是比我们直接在应用层写轮询要高的。
如果select没有查询到到有数据的请求,那么将会一直阻塞(是的,select是一个阻塞函数)。如果有一个或者多个请求已经准备好数据了,那么select将会先将有数据的文件描述符置位,然后select返回。返回后通过遍历查看哪个请求有数据。
select的缺点:
底层存储依赖bitmap,处理的请求是有上限的,为1024。
文件描述符是会置位的,所以如果当被置位的文件描述符需要重新使用时,是需要重新赋空值的。
fd(文件描述符)从用户态拷贝到内核态仍然有一笔开销。
select返回后还要再次遍历,来获知是哪一个请求有数据。
poll函数底层逻辑
poll的工作原理和select很像,先来看一段poll内部使用的一个结构体。
struct pollfd{
int fd;
short events;
short revents;
}
poll同样会将所有的请求拷贝到内核态,和select一样,poll同样是一个阻塞函数,当一个或多个请求有数据的时候,也同样会进行置位,但是它置位的是结构体pollfd中的events或者revents置位,而不是对fd本身进行置位,所以在下一次使用的时候不需要再进行重新赋空值的操作。poll内部存储不依赖bitmap,而是使用pollfd数组的这样一个数据结构,数组的大小肯定是大于1024的。解决了select 1、2两点的缺点。
epoll
epoll是最新的一种多路IO复用的函数。这里只说说它的特点。
epoll和上述两个函数最大的不同是,它的fd是共享在用户态和内核态之间的,所以可以不必进行从用户态到内核态的一个拷贝,这样可以节约系统资源;另外,在select和poll中,如果某个请求的数据已经准备好,它们会将所有的请求都返回,供程序去遍历查看哪个请求存在数据,但是epoll只会返回存在数据的请求,这是因为epoll在发现某个请求存在数据时,首先会进行一个重排操作,将所有有数据的fd放到最前面的位置,然后返回(返回值为存在数据请求的个数N),那么我们的上层程序就可以不必将所有请求都轮询,而是直接遍历epoll返回的前N个请求,这些请求都是有数据的请求。
文章参考
https://blog.csdn.net/qq_42046105/article/details/10292737
这两篇都写了,具体哪个是原创不太清楚,这里就都贴出来。