网络编程中BIO和NIO的区别
先上结论
BIO中,每个请求因为要阻塞直到结果返回,所以比较好的解决是每个请求都需要一个线程来处理,但是线程又是他的制约条件。
NIO中,每个请求进来都会绑定到一个channel上,然后channel注册给一个 selector(多路选择器),多路选择器可以在channel有消息的时候进行处理。
保证了只有selector在轮询查找。最开始的 selector 使用的是 select方法,jdk使用了优化后的epoll,避免了select有数量限制(1024/2048),在感兴趣的channel上看是否有该类型数据出现,如果有,则调用处理,没有则继续睡眠,等待唤醒。selector 使用reactor模式,将事件监听分离(读就绪/写就绪/链接就绪),对每个事件都可以进行独立的操作。
reactor模式其实就是:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。注册监听(每个请求来都做) + 轮询选择(单线程处理) + 事件处理。
BIO
缺点:每来一个请求就需要一个线程来处理,线程太多容易造成系统不可用.最开始的Tomcat使用的就是BIO
优化:通过线程池来管理线程,但是造成新的缺点:请求太多时不能被处理的请求就回阻塞,等待。不能被处理。正因为限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。而对Socket的输入流就行读取时,会一直阻塞。
服务端代码
package order.core.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.concurrent.ThreadPoolExecutor;
import javax.sound.sampled.Line;
public class BIOServer {
private static final int PORT = 8000;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
Socket socket = null;
ThreadPoolExecutor threadPoolExecutor = null;
try {
serverSocket = new ServerSocket(PORT);
System.out.println("serverSocket 启动了...");
while (true) {
socket = serverSocket.accept();
System.out.println("接受到socket...");
new Thread(new MyThread(socket)).start();
}
} catch (Exception e) {
// TODO: handle exception
} finally {
socket.close();
serverSocket.close();
}
}
}
class MyThread implements Runnable {
private Socket socket;
public MyThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
String expression;
String result;
// 通过BufferedReader读取一行
// 如果已经读到输入流尾部,返回null,退出循环
// 如果得到非空值,就尝试计算结果并返回
if ((expression = in.readLine()) != null) {
System.out.println("服务器收到消息:" + expression);
out.print(5678978);
}
System.out.println("=========");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 一些必要的清理工作
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
if (out != null) {
out.close();
out = null;
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
客户端代码
package order.core.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class BIOClient {
private static final String IP = "127.0.0.1";
// 默认的端口号
private static int DEFAULT_SERVER_PORT = 8000;
private static String DEFAULT_SERVER_IP = "127.0.0.1";
public static void send(String expression) {
send(DEFAULT_SERVER_PORT, expression);
}
public static void main(String[] args) {
send("12345");
}
public static void send(int port, String expression) {
System.out.println("发送消息为:" + expression);
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket(DEFAULT_SERVER_IP, port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println(expression);
System.out.println("___结果为:" + in.readLine());
} catch (Exception e) {
e.printStackTrace();
} finally {
// 一下必要的清理工作
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
if (out != null) {
out.close();
out = null;
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
NIO
优点:只需要打开Selector和 ServerSocketChannel 并开启非阻塞模式。然后只需要将ServerSocketChannel注册给selector即可。Tomcat后续默认全部使用NIO
监听的selector有四种:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
服务端监听的是OP_ACCEPT。
客户端监听的是OP_CONNECT。
发送消息类的监听OP_READ。
2.1、简介
NIO我们一般认为是New I/O(也是官方的叫法),因为它是相对于老的I/O类库新增的(其实在JDK 1.4中就已经被引入了,但这个名词还会继续用很久,即使它们在现在看来已经是“旧”的了,所以也提示我们在命名时,需要好好考虑),做了很大的改变。但民间跟多人称之为Non-block I/O,即非阻塞I/O,因为这样叫,更能体现它的特点。而下文中的NIO,不是指整个新的I/O库,而是非阻塞I/O。
NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。
新增的着两种通道都支持阻塞和非阻塞两种模式。
阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。
对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
下面会先对基础知识进行介绍。
2.2、缓冲区 Buffer
Buffer是一个对象,包含一些要写入或者读出的数据。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。
具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。
2.3、通道 Channel
我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。
底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。
Channel主要分两大类:
SelectableChannel:用户网络读写 FileChannel:用于文件操作
后面代码会涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。2.4、多路复用器 Selector
Selector是Java NIO 编程的基础。
Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
服务端代码
package order.core.common;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Server {
private static int DEFAULT_PORT = 8001;
private static ServerHandle serverHandle;
public static void start() {
start(DEFAULT_PORT);
}
public static synchronized void start(int port) {
if (serverHandle != null)
serverHandle.stop();
serverHandle = new ServerHandle(port);
new Thread(serverHandle, "Server").start();
}
public static void main(String[] args) {
start();
}
}
class ServerHandle implements Runnable {
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean started;
/**
* 构造方法
*
* @param port
* 指定要监听的端口号
*/
public ServerHandle(int port) {
try {
// 创建选择器
selector = Selector.open();
// 打开监听通道
serverChannel = ServerSocketChannel.open();
// 如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
serverChannel.configureBlocking(false);// 开启非阻塞模式
// 绑定端口 backlog设为1024
serverChannel.bind(new InetSocketAddress("localhost",port));
// 监听客户端连接请求
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 标记服务器已开启
started = true;
System.out.println("服务器已启动,端口号:" + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
started = false;
}
@Override
public void run() {
// 循环遍历selector
while (started) {
try {
// 无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(1000);
// 阻塞,只有当至少一个注册的事件发生的时候才会继续.
// selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
// selector关闭后会自动释放里面管理的资源
if (selector != null)
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// 通过ServerSocketChannel的accept创建SocketChannel实例
// 完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
SocketChannel sc = ssc.accept();
// 设置为非阻塞的
sc.configureBlocking(false);
// 注册为读
sc.register(selector, SelectionKey.OP_READ);
}
// 读消息
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
// 创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
// 读取到字节,对字节进行编解码
if (readBytes > 0) {
// 将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
// 根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
// 将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String expression = new String(bytes, "UTF-8");
System.out.println("服务器收到消息:" + expression);
// 处理数据
String result = null;
try {
result = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
} catch (Exception e) {
result = "计算错误:" + e.getMessage();
}
// 发送应答消息
doWrite(sc, result);
}
// 没有读取到字节 忽略
// else if(readBytes==0);
// 链路已经关闭,释放资源
else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
// 异步发送应答消息
private void doWrite(SocketChannel channel, String response) throws IOException {
// 将消息编码为字节数组
byte[] bytes = response.getBytes();
// 根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
// 将字节数组复制到缓冲区
writeBuffer.put(bytes);
// flip操作
writeBuffer.flip();
// 发送缓冲区的字节数组
channel.write(writeBuffer);
// ****此处不含处理“写半包”的代码
}
}
客户端代码
package order.core.common;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Client {
private static String DEFAULT_HOST = "127.0.0.1";
private static int DEFAULT_PORT = 8001;
private static ClientHandler clientHandle;
public static void start(){
start(DEFAULT_HOST,DEFAULT_PORT);
}
public static synchronized void start(String ip,int port){
if(clientHandle!=null)
clientHandle.stop();
clientHandle = new ClientHandler(ip,port);
new Thread(clientHandle,"Server").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
clientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception{
start();
sendMsg("aaaaaaaaa");
}
}
class ClientHandler implements Runnable{
private String host;
private int port;
private Selector selector;
private static SocketChannel socketChannel;
private static volatile boolean started;
public ClientHandler(String ip,int port) {
this.host = ip;
this.port = port;
try{
//创建选择器
selector = Selector.open();
//打开监听通道
socketChannel = SocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
socketChannel.configureBlocking(false);//开启非阻塞模式
socketChannel.connect(new InetSocketAddress("127.0.0.1", port));
System.out.println(socketChannel.finishConnect());
socketChannel.register(selector, SelectionKey.OP_CONNECT);
started = true;
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//循环遍历selector
while(started){
try{
//无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(1000);
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
// selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Exception e){
e.printStackTrace();
System.exit(1);
}
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
SocketChannel sc = (SocketChannel) key.channel();
if(key.isConnectable()){
if(sc.finishConnect());
else System.exit(1);
}
//读消息
if(key.isReadable()){
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:" + result);
}
//没有读取到字节 忽略
// else if(readBytes==0);
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
//异步发送消息
private void doWrite(SocketChannel channel,String request) throws IOException{
//将消息编码为字节数组
byte[] bytes = request.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
//****此处不含处理“写半包”的代码
}
public void doConnect() throws IOException{
System.out.println("==");
try {
System.out.println(socketChannel.isOpen());
System.out.println(socketChannel.isConnected());
} catch (Exception e) {
// TODO: handle exception
}
}
public void sendMsg(String msg) throws Exception{
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel, msg);
}
}