网络编程中BIO和NIO的区别

网络编程中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);
    }
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值