网络IO
阻塞模型
在之前网络通信都是阻塞模型
-
客户端向服务端发出请求后,客户端会一直处于等待状态,直到服务器端返回结果或网络出现问题 -
服务器端也是如此,在处理某个客户端A发来的请求时,另一个客户端B发来的请求会等待,直到服务器端的处理线程线程上一个请求的处理
在服务端使用ServerSocket来建立套接字,accept方法会进行阻塞等待客户端的连接
try(
// 创建一个ServerSocket对象
ServerSocket serverSocket = new ServerSocket(9090);
// accept方法,返回Socket对象,这里会进行阻塞,应用程序向操作系统请求接收已准备好的客户端连接的数据信息
Socket s = serverSocket.accept();
// 获取输入流,这里读取数据也会阻塞
InputStream is = s.getInputStream();
// 输出流,给客户端返回消息
OutputStream os = s.getOutputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader reader = new BufferedReader(isr);
){
String str;
while ((str = reader.readLine()) != null){
System.out.print(str);
}
os.write("我已收到消息".getBytes());
} catch (IOException e){
e.printStackTrace();
}
serverSocket.accept()阻塞
服务器端发起一个accept动作,询问操作系统是否有新的Socket套接字信息从端口发送过来,如果没有则serverSocket.accept()
会一直等待
阻塞模型的问题:
-
同一时间,服务器只能接收一个客户端的请求信息,第二个客户端需要等待服务器接收完第一个请求数据后才会被接收 -
服务器一次只能处理一个客户端请求,处理完成并返回后才能进行第二次请求的处理
多线程阻塞模型
由于阻塞模型的弊端,高并发时会导致请求太慢,所以提出了使用多线程来解决上述阻塞问题
-
服务器收到客户端A的请求后,开启线程去进行数据处理。主线程可以继续接收客户端B的请求
但是这样在进行serverSocket.accept();
操作时还是单线程运行,只有业务处理才会使用多线程,对于接收数据的并发能力并没有提升
同步非阻塞模型
这里先说一下同步和非同步的概念
同步和非同步是操作系统级别的,主要描述操作系统在收到程序请求网络IO操作后,如果网络IO资源没有准备好,该如何响应程序
-
同步IO不响应程序,直到网络IO资源准备好 -
非同步IO返回一个标记,当网络IO资源准备好后,用事件机制通知给程序
再说一下阻塞和非阻塞的概念
阻塞和非阻塞是程序级别的,主要描述程序请求操作系统IO操作后,如果网络IO资源没有准备好,程序如何处理
-
阻塞IO会进行等待 -
非阻塞IO会继续执行,且使用线程一直轮询,直到IO资源准备好
{
boolean flag = true;
try {
ServerSocket serverSocket = new ServerSocket(6666);
// 使用超时时间来设置为非阻塞状态,超过该时间会抛出SocketTimeoutException
serverSocket.setSoTimeout(100);
while (true){
Socket socket = null;
try{
// 设置了超时时间后accept就不会阻塞了
socket = serverSocket.accept();
} catch (SocketTimeoutException e){
synchronized (obj){ // 100ms内没有接收到任何数据,可以在这里做一些别的操作
System.out.println("没接收到数据,先歇一歇吧");
try {
obj.wait(10);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
continue;
}
// 开线程处理数据
new Thread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
serverSocket.setSoTimeout
可以使accept方法不一直阻塞,而是到了超时时间后抛出SocketTimeoutException异常,此时就可以用主线程做别的事情了,虽然实际还是使用的accept阻塞模型,但是有所改善
多路复用模型
多路复用模型(也就是NIO)不在使用操作系统级别的同步IO,目前主要实现有select、poll、epoll、kqueue
{
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 绑定8080端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 注册监听的事件
// ServerSocketChannel只能注册OP_ACCEPT
// SocketChannel可注册OP_READ、OP_WRITE、OP_CONNECT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
// 询问selector中准备好的事件
selector.select();
// 获取到上述询问拿到的事件类型
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
// 接收到服务端的请求
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
// 处理过了就要移除掉,否则再次select()还会拿到该事件
iterator.remove();
} else if(selectionKey.isReadable()){
SocketChannel sc = (SocketChannel) selectionKey.channel();
byteBuffer.clear();
int n = sc.read(byteBuffer);
if(n > 0){
byteBuffer.flip();
Charset charset = StandardCharsets.UTF_8;
String message = String.valueOf(charset.decode(byteBuffer).array());
System.out.println(message);
}
sc.register(selector,SelectionKey.OP_WRITE);
iterator.remove();
} else if(selectionKey.isWritable()){
SocketChannel sc = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("已接收到消息".getBytes());
buffer.flip();
sc.write(buffer);
iterator.remove();
}
}
}
}
多路复用显然绕过了accept方法的阻塞问题,使得操作系统可以在一个端口上能够同时接收多个客户端的IO事件