BIO和NIO
- BIO:同步阻塞式IO,服务器使用一个线程处理一个请求,当客服端有请求到达时就启动一个线程进行处理,直到请求结束才会释放这个线程,如果这个请求什么也不做就会一直占用该线程,当有很多这样的请求就会消耗大量的资源。
- NIO:同步非阻塞式IO,多个请求注册到多路复用器(Selector)上,共用一个线程进行处理,多路复用器会轮询每个请求,当有事件发生时才会通知主线程进行处理。
Socket与SocketChannel
Socket
- Socket与ServerSocket是JDK提供的两个用于实现TCP程序的类
- ServerSocket表示服务器端,socket表示的是客户端。通信时,服务器端要创建ServerSocket对象,对本机的指定端口进行监听;客户端创建Socket向指定IP地址的端口号发起连接请求,连接后就可以进行通信。
代码示例
public static void main(String[] args) throws IOException, InterruptedException {
final ServerSocket serverSocket=new ServerSocket(8080);
//服务端线程
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
Socket socket=serverSocket.accept();
System.out.println("连接成功");
System.out.println("开始接收数据...");
InputStream inputStream=socket.getInputStream();
inputStream.read();
System.out.println("数据接收完毕...");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
System.out.println("thread:"+thread.getState());
thread.start();
//客户端线程
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
try {
Socket socket=new Socket(InetAddress.getLocalHost(),8080);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
System.out.println("thread:"+thread.getState());
thread1.start();
System.out.println("thread:"+thread.getState());
}
输出
thread:NEW
thread:RUNNABLE
thread:RUNNABLE
连接成功
开始接收数据…
分别建立了两个线程用于模拟客户端与服务端,线程创建完成处于NEW状态,调用start()方法以后变成Runnable状态。从输出可以看出服务端线程启动以后,serverSocket.accept方法后面的语句并没有执行,而是在客户端线程启动以后才开始输出;而在执行了inputStream.read方法后,后面的语句也没有执行,这是因为客户端并没有向服务端输入。
显然这是因为Socket是阻塞式的,使用这些方法都会等待对方做出响应以后程序才会继续执行。
I/O阻塞时线程的状态
在这里还有一个问题,为什么线程已经阻塞了,但是使用getState方法查看线程的状态时却仍然是Runnable呢?
这就要区分JVM层面线程的状态和操作系统层面线程的状态了,这两着有些差异,而上面提到的阻塞指的是操作系统层面的线程阻塞。
JVM线程的状态
JVM中线程的状态有六种,分别是:
NEW(初始状态)
RUNNABLE(运行状态)
BLOCKED(阻塞状态)
WATTING(等待状态)
TIMED_WAITING(限时等待状态)
TERMINATED(终止状态)
线程的状态转换关系
其中BLOCKED、WATTING和TIMED_WAITING都会使线程进入休眠状态,将这三种归为一类,线程的生命周期可以简化为
操作系统的线程状态
操作系统的状态可分为
初始状态
可运行状态
运行状态
休眠状态
终止状态
前面说到的阻塞指的操作系统层面上的阻塞,也就是说线程在操作系统上处于休眠状态,可为什么JVM中仍然是Runnable状态呢,因为使用JVM获取到的线程状态实际上指的是线程在JVM中的状态,线程可能在操作系统上被阻塞了,但是在JVM的角度来看这个线程仍然在运行。
JVM线程状态的改变通常只于自身显式引入的机制有关,比如显式调用wait()、sleep()等方法。而操作系统层面的线程阻塞并不会改变线程在JVM中的状态。
SocketChannel
- Socket与ServerSocket是两个通信的端点,Socket的方法都是阻塞的,属于BIO通信;SocketChannel与ServSocketChannel分别是客户端与服务端的通道,SocketChannel提供configureBlocking方法,来描述通道的阻塞状态。我们可以将SocketChannel设置为非阻塞状态。
- 两者关系:虽然每个SocketChannel通道都有一个关联的Socket对象,但并非所有socket都有一个关联的SocketChannel。如果我们使用传统的方式来new Socket,那么其不会有关联的SocketChannel。
- 阻塞方法与非阻塞的方法对比
- 输入操作
- 进程A调用阻塞socket.read方法时,若该socket的接收缓冲区没有数据可读,则该进程A被阻塞,操作系统将进程A睡眠,直到有数据到达;
- 进程A调用非阻塞SocketChannel.read方法时,若该SocketChannel的接收缓冲区没有数据可读,则进程A收到一个EWOULDBLOCK错误提示,表示无可读数据,read方法立即返回。
- 输出操作
- 进程A调用阻塞socket.write方法时,若该socket的发送缓冲区没有多余空间,则进程A被阻塞,操作系统将进程A睡眠,直到有空间为止;
- 进程A调用非阻塞SocketChannel.write方法时,若该SocketChannel的发送缓冲区没有多余空间,则进程A收到一个EWOULDBLOCK错误提示,表示无多余空间,write方法立即返回。
- 连接操作
- 对于阻塞型的socket而言,调用socket.connect方法创建连接时,会有一个三次握手的过程,每次需要等到三次握手完成之后(ESTABLISHED 状态),connect方法才会返回,这意味着其调用进程需要至少阻塞一个RTT时间。
- 对于非阻塞的SocketChannel而言,调用connect方法创建连接时,当三次握手可以立即建立时(一般发生在客户端和服务端在一个主机上时),connect方法会立即返回;而对于握手需要阻塞RTT时间的,非阻塞的SocketChannel.connect方法也能照常发起连接,同时会立即返回一个EINPROGRESS(在处理中的错误)。
代码示例
public static void main(String[] args) throws IOException, InterruptedException {
final ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
System.out.println("服务启动...");
System.out.println("开始监听端口号8080...");
final Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
SocketChannel socketChannel=serverSocketChannel.accept();
System.out.println("连接成功...");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
Thread.currentThread().sleep(2000);
System.out.println("主线程休眠2s等待服务端准备就绪...");
System.out.println("客户端启动...");
//客户端
final SocketChannel socketChannel=SocketChannel.open();
socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(),8080));
System.out.println("客户端发起连接...");
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
String data="Hello, here is a piece of information, please pay attention to check!";
try {
socketChannel.write(ByteBuffer.wrap(data.getBytes()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
thread1.start();
}
输出
服务启动…
开始监听端口号8080…
连接成功…
主线程休眠2s等待服务端准备就绪…
客户端启动…
客户端发起连接…
主线程中启动两个线程进行分别模拟服务端和客户端,将ServerSocketChannel设置为非阻塞状态,通过程序可以看到,执行serverSocketChannel.accept方法非没有使程序发生阻塞,而是直接输出了“连接成功…”。
多路复用器Selector
Selector选择器的概述和作用
SocketChannel经常与多路复用器Selector放在一起使用。
概述: Selector被称为:选择器,也被称为:多路复用器,可以把多个Channel注册到一个Selector选择器上, 那么就可以实现利用一个线程来处理这多个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了, 减少系统负担, 提高效率。因为线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。
作用: 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率 。
代码示例
public class SocketChannelTest {
//定义任务的处理类,对SocketChannel做出处理
private static class ServerHandler implements Runnable{
SocketChannel socketChannel;
ServerHandler(SocketChannel socketChannel){
System.out.println("开始处理READ事件...");
this.socketChannel=socketChannel;
}
@Override
public void run() {
try {
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
int len=socketChannel.read(byteBuffer);
System.out.println("读到字节数:"+len);
byteBuffer.flip();
while(byteBuffer.hasRemaining()) {
System.out.print((char)byteBuffer.get());
}
byteBuffer.clear();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws IOException, InterruptedException {
//创建一个线程池用于处理SocketChannel的任务
final ExecutorService executorService= Executors.newFixedThreadPool(5);
//服务端:将serverSocketChannel的ACCEPT事件注册到selector中
//在监听到ACCEPT事件后再将SocketChannel的READ事件注册到selector中
//在监听到READ事件后启动一个任务放到线程池中进行处理
final ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
final Selector selector=Selector.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动...");
System.out.println("开始监听端口号8080...");
final Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
while(true){
selector.select();
Set<SelectionKey> selectionKeys=selector.selectedKeys();
for(SelectionKey selectionKey:selectionKeys) {
selectionKeys.remove(selectionKey);
if (selectionKey.isAcceptable()){
SocketChannel socketChannel=serverSocketChannel.accept();
System.out.println("连接成功...");
String clientInfo=socketChannel.socket().getInetAddress().getHostAddress();
int port=socketChannel.socket().getPort();
System.out.println("客户端信息为"+clientInfo+":"+port);
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("开始监听READ事件...");
}else if(selectionKey.isReadable()){
System.out.println("监听到READ事件...");
executorService.submit(new ServerHandler((SocketChannel) selectionKey.channel()));
selectionKey.cancel();
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
System.out.println("主线程休眠2s等待服务端准备就绪...");
Thread.currentThread().sleep(2000);
System.out.println("客户端启动...");
//客户端
final SocketChannel socketChannel=SocketChannel.open();
socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(),8080));
System.out.println("客户端发起连接...");
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
String data="Hello, here is a piece of information, please pay attention to check!";
try {
socketChannel.write(ByteBuffer.wrap(data.getBytes()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
thread1.start();
}
}
输出:
服务启动…
开始监听端口号8080…
主线程休眠2s等待服务端准备就绪…
客户端启动…
客户端发起连接…
连接成功…
客户端信息为172.19.32.1:59251
开始监听READ事件…
监听到READ事件…
开始处理READ事件…
读到字节数:69
Hello, here is a piece of information, please pay attention to check!
同样使用两个线程来模拟客户端与服务端,服务端将serverSocketChannel的ACCEPT事件注册到selector中,在监听到ACCEPT事件后再将SocketChannel的READ事件注册到selector中,在监听到READ事件后启动一个自定义的任务ServerHandler放到线程池中进行处理。
Selector选择器使用方法
- Selector选择器的获取
Selector selector = Selector.open();
- 注册Channel到Selector
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
register()方法的第二个参数:是一个int值,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,而且可以使用SelectionKey的四个常量表示:
连接就绪–常量:SelectionKey.OP_CONNECT
接收就绪–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项)
读就绪–常量:SelectionKey.OP_READ
写就绪–常量:SelectionKey.OP_WRITE
注意:对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。