关键词:BIO NIO ServerSocket ServerSocketChannel
本文主要从BIO开始聊起,涉及到BIO的使用以及BIO存在的问题,并且根据问题提出解决方案。然后再聊聊NIO的使用。了解了BIO存在的问题以及为什么产生了NIO对理解两者有很大的帮助作用。
█ BIO
ServerSocket是一个BIO操作,BIO,blocking I/O。即阻塞IO,为什么说是阻塞的呢?在哪里阻塞呢?阻塞了什么呢?
先来看看BIO的使用:
- 单线程使用ServerSocket
①创建服务端
package bio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服务器。
*
*/
public class BIOServer {
public static void main(String[] args) throws Exception{
// ServerSocket就是一个阻塞的IO
ServerSocket serverSocket = new ServerSocket(8899);
while (true) {
System.out.println("waiting connection");
Socket socket = serverSocket.accept();
System.out.println("one client connection");
InputStream socketInputStream = socket.getInputStream();
System.out.println("waiting receive");
byte[] bytes = new byte[1024];
socketInputStream.read(bytes);
System.out.println("receive:"+new String(bytes));
}
}
}
②创建两个客户端,用来连接服务端
package bio;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
*
* 客户端1
*
*/
public class Client1 {
public static void main(String[] args) throws Exception{
// 连接上服务端
Socket socket = new Socket("localhost", 8899);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String nextLine = scanner.nextLine();
// 像服务端发送数据
outputStream.write(nextLine.getBytes());
}
}
}
package bio;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
*
* 客户端2
*
*/
public class Client2 {
public static void main(String[] args) throws Exception{
// 连接上服务端
Socket socket = new Socket("localhost", 8899);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String nextLine = scanner.nextLine();
// 像服务端发送数据
outputStream.write(nextLine.getBytes());
}
}
}
③启动服务端,观察服务端BIOServer控制台输出。(控制台打印waiting connection,并没有one client connection,此时程序阻塞在Socket socket = serverSocket.accept();这里,等待客户端连接。)
④启动一个客户端1,观察服务端BIOServer控制台输出。(控制台继续打印,但没有打印出receive:,此时程序阻塞在socketInputStream.read(bytes);等待客户端1的输出内容。)
⑤在客户端1的控制台中输入几个内容,观察服务端控制台输出。(我在客户端1的控制台输入了hello world,此时服务端接收到内容,并打印到控制台,此时。一个while循环结束,继续下一个循环,所有继续打印了waiting connection)
⑥清空服务端的控制台内容,关闭客户端1的服务然后再将其启动,启动完成之后,再启动客户端2,在服务端2的控制台输入:hello world.
⑦观察服务端的控制台,并没有打印出hello world。为什么呢?其实,当客户端1连接上来之后,当前循环的代码已经执行到了socketInputStream.read(bytes);并阻塞在这里等待客户端1发来内容。于是,客户端2的连接和输出都被阻塞了。在客户端1控制台输入内容,此时再观察服务端控制台内容:
发现问题:ServerSocket会在两个方法阻塞,一个是accept(),一个是read()。从步骤6中可以发现,如果在单线程中使用ServerSocket,一次只能与一个客户端完成连接,如果此时这个已经连接的客户端没有发送任何数据,就会一直阻塞在read方法,其他的客户端的连接也会被阻塞了。
解决思路:为了使ServerSocket能够在一个客户端连接上来之后,继续连接上其他的客户端,可以使用多线程解决问题。一个客户端连接上来之后,开辟一个线程去等待该客户端的内容,主线程还能继续处理其他客户端的连接。
- 多线程使用ServerSocket
①改造服务端代码,客户端代码不变
package bio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服务器。使用多线程。一个连接对应一个线程
*
*/
public class BIOServer2 {
public static void main(String[] args) throws Exception{
// ServerSocket就是一个阻塞的IO
ServerSocket serverSocket = new ServerSocket(8899);
while (true) {
System.out.println("waiting connection");
Socket socket = serverSocket.accept();
new Thread(()->{
try {
System.out.println("one client connection");
InputStream socketInputStream = socket.getInputStream();
System.out.println("waiting receive");
byte[] bytes = new byte[1024];
socketInputStream.read(bytes);
System.out.println("receive:"+new String(bytes));
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
②启动服务端,先启动客户端1,再启动客户端2,再客户端2的控制台输入数据,此时观察服务端控制台输出:
此时解决了使用单线程造成连接阻塞的问题。
发现问题:多线程虽然能够解决多个连接的问题,但每一个连接都会开启一个线程,这样不仅会产生太多的线程资源,即使使用线程池也会造成线程资源的浪费。一般客户端连接是有两个步骤的,一个是连接,一个是发送数据。假设有10000个连接上来了,但只有100个连接会发送数据,这样就会造成线程去处理9900个连接的浪费。
思考:多线程解决了连接阻塞的问题,但是增加了线程资源。单线程呢,没有线程资源的浪费,但又会阻塞连接。要是能够在单线程下,不产生阻塞就好了。即serverSocket.accept()和socketInputStream.read(bytes)两个方法不会阻塞。比如:
// 提供一个api,设置这里不要阻塞
Socket socket = serverSocket.accept();
// 提供一个api,设置这里不要阻塞
socketInputStream.read(bytes);
解决思路:ServerSocket是JDK提供的类,我们不能修改源代码。好在对于上面的问题,JDK的开发人员早已经发现了问题,并提供了SocketChannel,实现类不阻塞的功能。可以把SocketChannel就理解成是一个ServerSocket,在其基础上解决了阻塞的问题。(毕竟ServerSocket已经被广泛使用,不能直接修改它的源代码,那就新写一个类)
█ NIO
NIO不再使用ServerSocket、Socket,引出了ServerSocketChannel、SocketChannel。ServerSocketChannel对应ServerSocket,SocketChannel对应ServerSocket。
①编写服务端代码(注意是单线程处理请求的哦)
package bio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
*
* NIO服务器,ServerSocketChannel
*
*/
public class NIOServer {
public static void main(String[] args) throws Exception{
// 下面这两个相当于ServerSocket serverSocket = new ServerSocket(8899);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8899));
// 设置不阻塞,相当于设置serverSocket.accept()不会阻塞
// 不设置false,接收连接还是阻塞的
serverSocketChannel.configureBlocking(false);
while (true) {
// serverSocket.accept()
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("等待客户端连接:"+socketChannel);
if (socketChannel!=null) {
System.out.println("客户端连接成功,等待接收数据...");
// 设置不阻塞,相当于设置socketInputStream.read(bytes)不会阻塞
// 不设置false,读取数据还是阻塞的
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
if (read>0) {
byteBuffer.flip();
System.out.println("接收客户端数据:"+byteBuffer.toString());
}
}
}
}
}
(特别注意serverSocketChannel.configureBlocking(false); socketChannel.configureBlocking(false);)
②运行服务端,观察控制台输出
在没有客户端连接的时候,serverSocketChannel.accept()会返回null,不像serverSocket.accept()是一直阻塞等待。
③注释掉代码:System.out.println("等待客户端连接:"+socketChannel); (输出太多,影响观感),重启服务端,然后运行客户端1,观察服务端控制台输出。
服务端接收到客户端的请求,其实此时的代码并没有阻塞在等待客户端1的内容,而是一直在一次次循环中。
④在客户端1输入内容发送给服务端,观察服务端控制台输出。
发现问题:客户端1发送的内容呢?服务端并没有打印出来?这是为什么呢。其实,看看服务端代码,就能发现,在接收到客户端1的连接之后,代码继续运行,到了下一个循环里,此时serverSocketChannel.accept()==null了,相当于把客户端1的连接信息覆盖掉了,导致客户端1的连接信息丢失。
解决思路:要是能够提供一个集合能够存储每一次连接Socket,这样就不会弄丢以前的连接信息。
⑤改造代码
package bio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
/**
*
* NIO服务器,ServerSocketChannel
*
*/
public class NIOServer2 {
// 创建一个集合,用于记录每一个连接信息
private static List<SocketChannel> socketChannelList = new ArrayList<>();
public static void main(String[] args) throws Exception{
// 下面这两个相当于ServerSocket serverSocket = new ServerSocket(8899);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8899));
// 设置不阻塞,相当于设置serverSocket.accept()不会阻塞
// 不设置false,接收连接还是阻塞的
serverSocketChannel.configureBlocking(false);
while (true) {
// serverSocket.accept()
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel!=null) {
// 客户端连接成功了,加入集合中
socketChannelList.add(socketChannel);
}
// 遍历集合,看看各个客户端是否发送了数据
for (SocketChannel channel : socketChannelList) {
// 设置不阻塞,相当于设置socketInputStream.read(bytes)不会阻塞
// 不设置false,读取数据还是阻塞的
channel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = channel.read(byteBuffer);
if (read>0) {
byteBuffer.flip();
System.out.println("接收客户端数据:"+new String(byteBuffer.array()));
}
}
}
}
}
⑥依次启动服务端、客户端1、客户端2;用客户端2发送数据,客户端1发送数据,观察服务端控制台输出。
可以发现,解决了连接丢失的问题。
发现问题:ServerSocketChannel解决了ServerSocket阻塞的问题,使用单线程也能处理了多个连接的问题。可是,上面的代码就可以了吗?并没有。在上面的代码里,我使用了一个集合去记录所有的客户端连接,然后一遍遍循环集合看看客户端是否有内容。想想假设有10000个客户端连接了,集合里面就有10000个客户端数据,然而只有100个客户端会发送数据,这样在遍历集合的时候,其实只有100个是有效的数据,其余的都是无效的遍历。这样是不是也会造成资源的浪费了。
解决思路:这些问题,JDK的开发人员也帮我们解决了,于是就引出了“IO多路复用”的概念了,其中有包括“select”、“poll”、“epoll”,可以说多路复用解决的就是循环遍历造成的资源浪费问题。关于多路复用,会另起一篇来写的。
关于BIO到NIO就到这里了吧!