java实践11之网络IO BIO和NIO(上)

文章目录

  • java实践11之网络IO BIO和NIO(上)
    • 1 BIO:
      • BIO的使用demo
      • 优化版使用线程池实现异步处理
      • 为何已经异步处理了还说BIO是阻塞的?
      • BIO带来的问题
    • 2 NIO

java实践11之网络IO BIO和NIO(上)

  java 网络IO也是java基础知识体系中很重要的一部分,java目前提供的网络编程模型有3种BIO、NIO、AIO。关于他们概念上的东西,阻塞、非阻塞、同步、异步这些概念就不说了,这个很多文章中已经分享了,下面主要分享一下使用方式,和我对他们的理解。(下面没有AIO的内容,这个没实际用过,没有深入研究过)

1 BIO:

BIO:BIO是一种同步阻塞网络处理模型。当客户端有连接请求时服务器端就需要启动一个线程进行处理。
java实践11之网络IO BIO和NIO(上)_第1张图片

BIO的使用demo

下面看一下他们的基本demo。
服务端:

public class BioServer extends Thread {
	public static int port = 8080;

	private ServerSocket serverSocket;

	public BioServer(int port) {
		try {
			serverSocket = new ServerSocket(port);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void run() {
		for (;;) {
			try {
				System.out.println("等待远程连接,监控端口" + serverSocket.getLocalPort() + "...");
				Socket server = serverSocket.accept();
				DataInputStream in = new DataInputStream(server.getInputStream());
				String[] strs = in.readUTF().split(",");
				System.out.println(strs[2]+"来了,说:" + strs[0] );
				DataOutputStream out = new DataOutputStream(server.getOutputStream());
				System.out.println(strs[2]+"好个粑粑");
				out.writeUTF(strs[2]+"好个粑粑");
				server.close();
			} catch (Exception s) {
				System.out.println("Socket timed out!");
				break;
			}
		}
	}

	public static void main(String[] args) {
		int port = BioServer.port;
		Thread t = new BioServer(port);
		t.run();
	}
}

客户端

public class BioClient extends Thread {
	public static void main(String[] args) {
//		for(int i=0;i<100;i++){
			Thread t1 = new BioClient("t"+0);
			t1.start();
//		}
	}

	private String name;

	public BioClient(String name) {
		this.name = name;
	}

	@Override
	public void run() {
		String host = "127.0.0.1";
		int port = BioServer.port;
		try {
			System.out.println(name+"连接到主机:" + host + " ,端口号:" + port);
			Socket client = new Socket(host, port);
//			System.out.println("远程主机地址:" + client.getRemoteSocketAddress());
			OutputStream outToServer = client.getOutputStream();
			DataOutputStream out = new DataOutputStream(outToServer);

			out.writeUTF("你好啊兄弟,我是,"+name);
			InputStream inFromServer = client.getInputStream();
			DataInputStream in = new DataInputStream(inFromServer);
			System.out.println(name+"获取服务器响应: " + in.readUTF());
			client.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

  由于accept read write都是同步阻塞操作,这是无法改变,当多个客户端请求时,一个请求没处理完,则下一个请求一直在阻塞状态,所以一般不会这么用,会使用线程来进行异步处理,加快处理速度。

优化版使用线程池实现异步处理

  由于串行执行请求,速度较慢,所以需要加入线程来异步处理,但是线程过多会导致线程切换、创建、销毁,会对系统造成极大的负担,所以一般使用线程池来优化服务端。
优化BIO处理模型
java实践11之网络IO BIO和NIO(上)_第2张图片修改服务端代码:

public class BioServer extends Thread {
	public static int port = 8080;

	private ServerSocket serverSocket;

	public BioServer(int port) {
		try {
			serverSocket = new ServerSocket(port);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	//使用线程池来优化BIO处理模型
	ExecutorService es=Executors.newFixedThreadPool(10);
	public void run() {
		for (;;) {
			try {
				System.out.println("等待远程连接,监控端口" + serverSocket.getLocalPort() + "...");
				Socket socket = serverSocket.accept();
				//请求到了后放入线程池许处理
				es.submit(new Weiyibu(socket));
			} catch (Exception s) {
				System.out.println("Socket timed out!");
				break;
			}
		}
	}

	public static void main(String[] args) {
		int port = BioServer.port;
		Thread t = new BioServer(port);
		t.run();
	}
}

class Weiyibu implements Runnable {
	Socket socket;

	public Weiyibu(Socket socket) {
		this.socket = socket;
	}

	@Override
	public void run() {
		try {
			DataInputStream in = new DataInputStream(socket.getInputStream());
			String[] strs = in.readUTF().split(",");
			System.out.println(strs[2] + "来了,说:" + strs[0]);
			DataOutputStream out = new DataOutputStream(socket.getOutputStream());
			System.out.println(strs[2] + "好个粑粑");
			out.writeUTF(strs[2] + "好个粑粑");
			socket.close();
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}

在一般的系统中,此种方式,用的比较多,因为并发不是很大,支持异步处理请求,并且使用了线程池,避免了线程过多对系统造成的负担。

  那么使用BIO+线程池不就异步,没问题了么?不是的,针对BIO的阻塞,我的理解是虽然在BIO中使用了线程,但是他的底层仍然是阻塞的。那么他具体提现在哪呢?
  针对上面的模型,增加client3客户端和client4客户端,线程池为2。当线程池执行提交的client1和client2的请求时,如果速度很慢,那么client3和client4会一直在线程池中等待。

java实践11之网络IO BIO和NIO(上)_第3张图片
通过刚才的例子可以看到,虽然我们针对BIO增加了线程池,来异步处理,但是仍然没改变accept、write、read阻塞的本质。

为何已经异步处理了还说BIO是阻塞的?

  BIO的阻塞不是说我们的程序是阻塞的,而是系统层面做的阻塞。我们可以从程序层面做多线程做异步,但是底层的阻塞是我们无法修改的。

例如:去餐厅吃饭。餐厅为每一个包房(client)分配一个服务人员(socket),服务人员对包房人员点餐完成后,请求后厨(java server)去做菜。由于包房-服务人员-后厨是绑定的,在后厨做饭的过程中,服务人员是阻塞的,一直在等菜完毕 返回给包房后,才能进行下一个的处理。 BIO的阻塞就体现在这里,服务人员等待菜品,是阻塞的。它是系统层面的,我们正常的优化javaserver方式,相当于优化后厨处理,但是无法修改 服务员等菜的操作,即系统层阻塞操作。

BIO带来的问题

  从上面的描述中,我们可以看到虽然BIO可以使用线程池来加快处理速度,但是还有下列问题。
1、处理read、wirte、accept时是阻塞的,他是操作系统层面的,我们一般是无法修改的。
2、资源利用不合理。BIO阻塞主要体现在client、socket和java处理程序的线程是绑定的,当我们程序处理慢时,那么这一条线用户、socket、java处理线程会阻塞其他线程,没有更合理的利用资源。
3、BIO是单通道的。在实际使用中一次请求需要2个通道。由于读和写是互斥的,所以他们完全可以合并为1个双向通道。
大家是否还有疑问,只做读取,那么是否只需要1个通道就行了,为什么是2个?我理解为,读写是系统层面的定义,就算我们只读取数据,在实际请求中,比如只连接不做读写,那么也需要3次握手 ,即有读还有写,所以还是需要2个通道。

针对BIO带来的问题,下面我们来看看NIO是如何来优化处理这些问题的。

2 NIO

NIO是一种新的同步非阻塞IO模型,客户端发送的请求时,都会先放到表中,并增加监视功能,监视器会轮训表中的读写,有就绪的,则进行下一步处理。
下面是我对NIO处理流程的概念理解:
java实践11之网络IO BIO和NIO(上)_第4张图片1、首先服务启动时操作系统会有开启请求处理线程监控网卡等硬件,看是否有请求到达,同步开启监视线程,监控请求存放表中是否有请求。
2、当请求处理线程收到请求后,会为请求创建channel通道,并放入表中。
3、同时监视线程请求表,当有就绪的channel则会通知javaserver来处理对应的事件。
4、这时,javaserver可以分配给对应的handler来处理对应的事件(可以使用同步或者线程池异步来处理对应的事件)。

下面是对应的demo
Server端口

public class NioServer {
	public static int port = 8080;
	// 创建一个selector
	Selector selector;

	public NioServer() {
		try {
			// 创建server
			ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
			// 绑定一个端口
			serverSocketChannel.socket().bind(new InetSocketAddress(port));
			// 开启一个选择器 根据操作系统不同 使用不同的选择器:
			// 比如linux用的是epoll
			selector = Selector.open();
			// 设置非阻塞
			serverSocketChannel.configureBlocking(false);
			// 将ServerSocketChannel 注册到选择器中 关心Accept事件
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		} catch (Exception e) {
			// TODO: handle exception
		}
	}

	public void start() throws Exception {
		// 循环等待客户端连接
		while (true) {
			System.out.println("server:等待远程连接,监控端口" + port + "...");

			// 阻塞等待关心的channel事件发生
			selector.select();
			// 如果有事件发生select>0 获取到相关事件的集合
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while (iterator.hasNext()) {
				// 获取发生事件的key
				SelectionKey selectionKey = iterator.next();
				iterator.remove();
				// 如果是连接请求事件
				if (selectionKey.isAcceptable() && selectionKey.isValid()) {
					this.accept(selectionKey);
					continue;
					// 服务端关心的可读,意味着有数据从client传来了,根据不同的需要进行读取,然后返回
				}
				if (selectionKey.isReadable() && selectionKey.isValid()) {
					this.read(selectionKey);
					continue;
				}
				// 实际上服务端不在意这个,这个写入应该是client端关心的
				if (selectionKey.isWritable() && selectionKey.isValid()) {
					this.write(selectionKey);
					continue;
				}
				// 手动将selectionKey从集合中移除 防止重复操作
			}
		}
	}

	// 处理服务器写事件
	private void write(SelectionKey selectionKey) throws Exception {
		// 有channel可写,取出可写的channel
		SocketChannel sc = (SocketChannel) selectionKey.channel();
		System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) 
				+"开始处理客户端"+sc.getRemoteAddress()+"写请求");
		Thread.sleep(1000);
		// 设计非阻塞
		sc.configureBlocking(false);
		sc.write(ByteBuffer.wrap("wirete secsess".getBytes()));
		System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "写成功");
		// 重新将channel注册到选择器上,设计为监听

		sc.register(selector, SelectionKey.OP_READ);
	}

	// 处理读事件
	private void read(SelectionKey selectionKey) throws Exception {
		// 通过key反向获取对应的channel进行读取
		SocketChannel sc = (SocketChannel) selectionKey.channel();
		System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) 
				+"开始处理客户端"+sc.getRemoteAddress()+"读请求");
		Thread.sleep(1000);
		// try {
		// 获取该channel关联的buffer
		ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
		int len = sc.read(buffer);
		if (len < 1) {
			// 读不到东西抱异常了
			selectionKey.cancel();
			sc.close();
			return;
		}
		String msg = new String(buffer.array(), 0, buffer.position());
		buffer.clear();
		System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "收到客户端发送的数据:" + msg);

		buffer = ByteBuffer.wrap("三花淡奶没了来个海克斯科技".getBytes());
		System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "返回:三花淡奶没了来个海克斯科技");
		sc.write(buffer);
		// } catch (Exception e) {
		// System.out.println("没数据 关闭了");
		// readSocketChannel.close();
		// }
	}

	// 处理接收状态的通道
	private void accept(SelectionKey selectionKey) throws Exception {
		ServerSocketChannel serverSocketChannelAccept = (ServerSocketChannel) selectionKey.channel();
		// 给客户端生成一个SocketChannel 非阻塞
		SocketChannel sc = serverSocketChannelAccept.accept();
		System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) 
				+"开始处理客户端"+sc.getRemoteAddress()+"连接请求");
		Thread.sleep(1000);
		// 设置客户端为非阻塞的
		sc.configureBlocking(false);
		// 将与客户端连接的socketChannel也注册到selector中 同时给 Channel关联一个buffer
		sc.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
		ByteBuffer buff = ByteBuffer.wrap("connect secsess".getBytes());
		Thread.sleep(1000);
		System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "接到请求与客户端连接成功");
		sc.write(buff);
	}

	public static void main(String[] args) throws Exception {
		new NioServer().start();
	}
}

client端:

public class NioClient {
	public static void main(String[] args) throws Exception {
		NioClient client = new NioClient();
		client.client();
	}

	Selector selector;
	SocketChannel socketChannel;

	public NioClient() {
		try {
			selector = Selector.open();
			socketChannel = SocketChannel.open();
			// 设置异步连接
			socketChannel.configureBlocking(false);
			socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE);
			socketChannel.connect(new InetSocketAddress("127.0.0.1", NioServer.port));
			System.out.println("client:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "请求连接服务端");
		} catch (Exception e) {
			// TODO: handle exception
		}
	}

	public void client() throws Exception {

		// 判断连接
		while (true) {
			selector.select();
			Iterator<SelectionKey> setKeys = selector.selectedKeys().iterator();
			while (setKeys.hasNext()) {
				SelectionKey setKey = setKeys.next();
				setKeys.remove();
				// 如果与服务端连上了 回先走这里,因为上边是异步
				if (setKey.isConnectable()) {
					connect(setKey);
					// continue;
				}
				if (setKey.isReadable()) {
					read(setKey);
					// continue;
				}
				if (setKey.isValid() && setKey.isWritable()) {
					write(setKey);
					// continue;
				}
				if (isExit) {
					System.out.println("客户端完毕 退出");
					return;
				}
			}
		}
	}

	private void write(SelectionKey setKey) throws Exception {
		Thread.sleep(1000);
		SocketChannel client = (SocketChannel) setKey.channel();
		System.out.println("client:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "向客户端请求三花淡奶");
		ByteBuffer buff = ByteBuffer.wrap("三花淡奶".getBytes());
		client.write(buff);
		client.register(selector, SelectionKey.OP_READ);
	}

	boolean isExit = false;

	private void read(SelectionKey setKey) throws Exception {
		Thread.sleep(1000);
		SocketChannel client = (SocketChannel) setKey.channel();
		ByteBuffer bf = ByteBuffer.allocateDirect(1024);// 创建缓冲区大小
		client.read(bf);
		byte[] bytes = new byte[bf.position()];
		for (int i = 0; i < bytes.length; i++) {
			bytes[i] = bf.get(i);
		}
		String msg = new String(bytes);
		System.out.println("client:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "接收服务端返回的数据" + msg);

		if (msg.equals("三花淡奶没了来个海克斯科技")) {
			socketChannel.close();
			setKey.cancel();
			isExit = true;
			// selector.close();
			return;
		}
		client.register(selector, SelectionKey.OP_WRITE);

	}

	private void connect(SelectionKey setKey) throws Exception {
		// 获取客户端通道
		SocketChannel client = (SocketChannel) setKey.channel();
		if (client.isConnectionPending()) {
			client.finishConnect();
			client.register(selector, SelectionKey.OP_READ);
		}
		// System.out.println(new String(buffer.array()));
	}
}

在上面的例子中,server端,为什么select还是阻塞的,为什么说NIO是非阻塞的,它的非阻塞提现在哪?

篇幅有限,请看下章java实践12之网络IO BIO和NIO(下)

你可能感兴趣的:(java,多线程,java,网络,nio)