在socket 通讯层面,BIO就是阻塞io,也就是说,在socket等待连接事件或者读写事件的发生的过程中,当前线程会一直处于阻塞状态,不能做其他事情;
1、bio
public static void main(String[] args) throws IOException {
//bio阻塞io,主线程只能处理一个连接,当前连接没有处理完,是不能接收新链接的,这对于并发来讲,是非常不友好的
ServerSocket serverSocket = new ServerSocket(9000);
while(true) {
//此处会阻塞,等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端已连接");
byte[] bytes = new byte[128];
//此处会阻塞,等待客户端发送数据
int len = socket.getInputStream().read(bytes);
System.out.println("客户端发送数据:" + new String(bytes, 0, len));
}
}
2、bio-plus
对于上种情形,可以简单地做出改进,也就是利用多线程把建立连接和处理读写事件分开,这样,二者之间就不会互相影响
public static void main(String[] args) throws IOException {
//加强版bio,利用异步线程去做收发数据,主线程只负责建立连接,这样,即时已连接客户端不发送数据,主线程也不会一直阻塞,可以继续循环接收新的连接;
// 但是,这样在并发高的情况下,对内存是巨大的消耗;于是,nio就应运而生;
ServerSocket serverSocket = new ServerSocket(9000);
while(true) {
Socket socket = serverSocket.accept();
System.out.println("客户端已连接");
new Thread(() -> {
byte[] bytes = new byte[128];
int len = 0;
try {
len = socket.getInputStream().read(bytes);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客户端发送数据:" + new String(bytes, 0, len));
}).start();
}
}
bio-plus版虽然将建立连接和处理读写事件分开了,但是在高并发的情况下仍然“不堪一击”,并且,这种方式虽然看似是解决了bio的阻塞问题,但是属于治标不治本,它的解决方式只不过是利用空间(开辟新线程)去换取时间而已,实质上还是阻塞IO;
3.Nio-Select
于是,nio非阻塞式IO出现了,如上所示,将通道对象serverSocketChannel设置为非阻塞即可,并且引入了selector选择器(可以认为就是socketList),将建立好的连接对象放到socketList中,每次循环就会将所有的socket连接遍历一遍,处理读写事件。这样,单个线程也可以处理多个连接和事件。
public class NioSelect {
private static List<SocketChannel> socketList=new ArrayList();
public static void main(String[] args) throws IOException {
//nio演示,接收客户端连接和接收数据都不会阻塞;
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
//设置serverSocketChannel为非阻塞
serverSocketChannel.configureBlocking(false);
System.out.println("服务启动");
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel!=null){
System.out.println("客户端已连接");
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
//将客户端连接放到集合中
socketList.add(socketChannel);
}
//遍历所有的socket连接,获取数据;但是这样会有空轮训的问题,就是没有发送数据的连接也会被遍历,不合理
while(socketList.size()>0){
for(int i=0;i<socketList.size();i++){
ByteBuffer allocate = ByteBuffer.allocate(128);
int read = socketList.get(i).read(allocate);
if(read>0){
System.out.println("客户端发送消息:"+new String(allocate.array()));
}else{
socketList.remove(i);
}
}
}
}
}
}
但是,问题仍然存在。比如此时socketList维护了100个连接,只有一个连接向服务端发送了数据,但是,按照代码却需要将着100个连接全部遍历一遍,这显然是不合理的,并且,socketList中可以存放的连接数量也是有限的,无法解决c10k问题。(所谓c10k问题,指的是:服务器如何支持10k个并发连接)
4.Nio-Epoll
public class NioEpoll {
private static List<SocketChannel> socketList=new ArrayList();
public static void main(String[] args) throws IOException {
//nio演示,接收客户端连接和接收数据都不会阻塞;
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
//设置serverSocketChannel为非阻塞
serverSocketChannel.configureBlocking(false);
//获取selector选择器
Selector selector = Selector.open();
//将serverSocketChannel注册到选择器,并且声明需要选择器监听的是accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动");
while(true) {
//选择器开始执行监听,无监听事件发生会一直阻塞,有监听事件发生会跳出阻塞,继续往下执行
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//判断发生的是什么事件
//是接收连接事件,就获取新的客户连接,及事件的注册
if(selectionKey.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = channel.accept();
System.out.println("客户端连接成功");
socketChannel.configureBlocking(false);
//监听的是数据读事件
socketChannel.register(selector,SelectionKey.OP_READ);
//如果是发生数据读事件,则接收数据,并打印
}else if(selectionKey.isReadable()){
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer allocate = ByteBuffer.allocate(128);
int read = channel.read(allocate);
if(read>0){
System.out.println("客户端发送数据:"+new String(allocate.array(),0,read,"GBK"));
}else{
System.out.println("客户端已断开连接");
channel.close();
}
}
//将处理过的事件从列表中移除,防止重复处理
iterator.remove();
}
}
}
}
这个版本的nio看起来就又进步了一节,这里用epoll选择器代替了上面的select选择器,epoll选择器内部可以分成两部分,一部分是注册到其上的对象列表,另一个部分是有事件发生的列表,于是,可以想到的就是,相比于select遍历所有,epoll只需要遍历有事件发生的对象即可。
其实,在select和epoll之间还有一个poll选择器,只不过poll与select相比,仅仅是存放的连接对象数量变多了,其他的并没有什么区别。