文章目录
IO
同步与异步(一个任务依赖另外一个任务)
- 同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
- 异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞
- 阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。
- 非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。
BIO(Bloking input-output)
服务端多线程的话,每来一个客户端请求,就会创建一个线程。因为如果总共是一个线程的话,如果有一个服务端等待客户端输入数据等情况,会造成服务端的阻塞,比如read方法等待客户端输入。
在BIO的时候,Server中的通道是单向的,而不是双向的,socket.getOutputStream(),socket.getIntputStream()
主要讲的是网络上的输入输出
你给server发消息,客户端就叫做输出,服务端给客户端发消息对客户端来说就叫做输入。
传统的BIO的几个地方有阻塞
- accept
- read
- write
BIO服务端单线程时候的阻塞情况(不支持并发)
下面的服务端代码会在read的时候进行阻塞。
客户端
package SocketTest;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
public class IOClient
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",8888);
Scanner scanner = new Scanner(System.in);
String next = scanner.next(); //客户端不发消息
socket.getOutputStream().write(next.getBytes());
socket.getOutputStream().flush();
System.out.println("waiting for msg back");
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
socket.close();
}
}
服务端
package SocketTest;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class IOServer
{
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("127.0.0.1",8888));
while (true)
{
Socket socket = serverSocket.accept();
handle(socket);
}
}
static void handle(Socket socket)
{
try
{
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
socket.getOutputStream().write("hello client".getBytes());
socket.getOutputStream().flush();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
服务端多线程解决阻塞
多线程会造成服务器端很浪费。
客户端
package SocketTest;
import java.io.IOException;
import java.net.Socket;
public class IOClient
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",8888);
socket.getOutputStream().write("Hello Server".getBytes());
socket.getOutputStream().flush();
System.out.println("waiting for msg back");
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
socket.close();
}
}
服务端
ServerSocket对象只会监听有没有人连,Socket才是真正进行传输的对象。
package SocketTest;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class IOServer
{
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("127.0.0.1",8888));
while (true)
{
Socket socket = serverSocket.accept(); //阻塞
new Thread(()->{
handle(socket);
}).start();
}
}
static void handle(Socket socket)
{
try
{
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
socket.getOutputStream().write("hello client".getBytes());
socket.getOutputStream().flush();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
存在的问题
client连到server的话,如果server没收到连接请求,accept()可能会阻塞。
输入流的read和输出流的write方法也是会阻塞的。
NIO
NIO设计初衷就是使用单线程来处理并发。
上面的BIO的单线程的代码中,服务端的read方法会阻塞。read方法不解阻塞(因为客户端先write-》服务端accept-》服务端read-》服务端write-》客户端read),那么在单线程中accept就不会再被调用,那我们就想让read解阻塞。
在单线程中,如果read不阻塞就会出现其他问题
- 1、比如serverSocket.accept()获得到的socket丢掉了,因为如果新接收请求会得到新的socket,所以下面的例子中会想到使用List进行存放。
- 2、accept()阻塞,需要设置成非阻塞。
下面是伪代码(用List存放Socket,while (true)接收socket请求和遍历所有的Socket中接收数据。)
package SocketTest;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
public class SolveTest
{
public static void main(String[] args) throws IOException
{
List<Socket> socketList = null;
ServerSocket serverSocket = new ServerSocket(8080);
while (true)
{
serverSocket.zl(false); //设置accept方法非阻塞,serverSocketChannel里面有一个方法可以设置成非阻塞,serverSocketChannel.configureBlocking(false);
Socket clientSocket = serverSocket.accept();
for(Socket socket:socketList) //循环看有没有人发消息过来
{
byte[] bytes = new byte[1024];
int read = clientSocket.getInputStream().read(bytes);
if(read > 0)
{
System.out.println();
}
}
}
}
}
下面是非伪代码
configureBlocking(false);设置非阻塞,这样accept、read就应该是非阻塞了。
package SocketTest;
import sun.tools.jstat.Jstat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
public class SolveTest
{
static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) {
try
{
ServerSocketChannel serverSocket = ServerSocketChannel.open(); //底层会调用一个native方法即poll0方法,poll0会调用select方法
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8080);
serverSocket.bind(socketAddress);
serverSocket.configureBlocking(false); //设置非阻塞
while (true)
{
for (SocketChannel socketChannel : channelList)
{
int read = socketChannel.read(byteBuffer);
if (read > 0) {
System.out.println("read------111------:" + read);
byteBuffer.flip();
byte[] bs = new byte[read];
byteBuffer.get(bs);
String content = new String(bs);
System.out.println(content);
byteBuffer.flip();
}
}
else
{
channelList.remove(socketChannel);
}
SocketChannel accept = serverSocket.accept();
if (accept != null)
{
System.out.println("conn success");
accept.configureBlocking(false);
channelList.add(accept);
System.out.println(channelList.size()+"---list---size");
}
}
}
catch (IOException e)
{
}
}
}
存在的问题
如果上面的List里面有10000个SocketChannel,只有1000个是活跃的,那么就浪费了9000次空轮询。
解决思路:
- 假设让list循环交给内核执行,那么速度肯定快了。
- 使用Selector
epoll性能好于select
linux平台下调用epoll
windows平台下调用select
Selector
为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:
- 1、connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
- 2、accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
- 3、read:读事件,对应值为SelectionKey.OP_READ(1)
- 4、write:写事件,对应值为SelectionKey.OP_WRITE(4)
这个很好理解,每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。
所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理。
使用Selector的服务端代码
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
Selector selector = Selector.open(); //通过调用Selector.open()方法创建一个Selector对象
serverChannel.register(selector, SelectionKey.OP_ACCEPT); //注册Channel到Selector
while(true)
{
int n = selector.select(); //阻塞到至少有一个通道在你注册的时间上就绪了,可以加时间参数,设置最长阻塞时间
if (n == 0) continue;
Iterator ite = this.selector.selectedKeys().iterator();
while(ite.hasNext()){
SelectionKey key = (SelectionKey)ite.next();
if (key.isAcceptable()) //判断是否可接收,是返回true
{
SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
clntChan.configureBlocking(false);
//将选择器注册到连接到的客户端信道,
//并指定该信道key值的属性为OP_READ,
//同时为该信道指定关联的附件
clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
}
if (key.isReadable()){
handleRead(key);
}
if (key.isWritable() && key.isValid()){
handleWrite(key);
}
if (key.isConnectable()){
System.out.println("isConnectable = true");
}
ite.remove();
}
}