前言

  • 在将NIO之前,我们必须要了解一下Java的IO部分知识。
  • BIO(Blocking IO)
  • 阻塞IO,在Java中主要就是通过ServerSocket.accept()实现的。
  • NIO(Non-Blocking IO)
  • 非阻塞IO,在Java主要是通过NIOSocketChannel + Seletor实现的。
  • AIO(Asyc IO)
  • 异步IO,目前不做学习。

BIO

简单实现服务器和客户端

package net.io;

import net.ByteUtil;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket; //NIO(NonBlocking IO)非阻塞IO
//通过一个事件监听器,吧这些客户端的连接保存起来,如果有时间发生再去处理,没时间发生不处理
public class Server {
public Server(int port) {
try {
//创建服务器端,监听端口port
ServerSocket serverSocket = new ServerSocket(port);
//对客户端进行一个监听操作,如果有连接过来,就将连接返回(socket)-----阻塞方法
while (true) {
//监听,阻塞方法
Socket socket = serverSocket.accept();
//每个服务器和客户端的通信都是针对与socket进行操作
System.out.println("客户端" + socket.getInetAddress());
InputStream inputStream = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(inputStream);
//获取客户端发送的message
Object get = ois.readObject();
System.out.println("接收到的消息为:" + get); //服务器需要给客户端进行一个回应
OutputStream outputStream = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
String message = "客户端你好,我是服务器端";
//我这里写了,不代表发送了,知识写到了输出流的缓冲区
oos.writeObject(message);
//发送并清空
oos.flush();
} } catch (Exception e) {
e.printStackTrace();
}
} public static void main(String[] args) {
new Server(7000);
}
}
package net.io;

import net.ByteUtil;

import java.io.*;
import java.net.Socket; public class Client {
public Client(int port){
try {
Socket socket = new Socket("localhost",port); //inputStream是输入流,从外面接收信息
//outpurStream是输出流, 往外面输出信息
OutputStream outputStream = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
//发送的信息
String message = "服务器你好,我是客户端";
//我这里写了,不代表发送了,知识写到了输出流的缓冲区
oos.writeObject(message);
//发送并清空
oos.flush(); //接收服务器的回应
InputStream inputStream = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object get = ois.readObject();
System.out.println("接收的信息为:" + get);
} catch (Exception e) {
e.printStackTrace();
}
} public static void main(String[] args) {
new Client(7000);
}
}

针对于BIO,为什么是阻塞IO,是因为BIO是基于Socket完成数据的读写操作,Server调用accept()方法持续监听socket连接,所以就阻塞在accept()这里(后面的操作如果没有socket连接则无法执行),那这样就代表服务器只能同时处理一个socket的操作。

所以后序我们通过为socket连接创建一个线程执行,这样就可以增大服务器处理连接的数量了。

但是新的问题也就出来了,如果客户端的连接很多,那么就会导致服务器创建很多的线程

对socket进行处理,如果服务器端不断开连接的话,那么对应的线程也不会被销毁,这样大数量的线程的维护十分消耗资源。针对于这种情况设计出了Java的NIO。

NIO

首先我们需要介绍一下NIO。如果说BIO是面向socket进行读写操作的话,那么NIO则是面向channel进行读写操作(或者说面向buffer)。

这里我们在讲解NIO之前,需要先讲解一下这个buffer。众所周知这就是一个缓冲,并且我们知道socket具有输入流inputStream和输出流outputStream(读写分开的),但是我们的channel是同时具有read和write两个方法,而且两个方法都是基于buffer进行操作(这里就可以说明channel仅能比普通输入输出流好,相当于channel是一条双向,输入输出流是两条单向),所以我们可以知道buffer的重要性。

Buffer

诸如ByteBuffer,IntBuffer等都是Buffer的派生抽象类,需要调用抽象类的静态方法allocate(X capacity)方法进行一个初始化操作,该方法就是初始化buffer的大小,或者使用wrap(X x)方法,该方法相当于直接将信息存入缓冲中。至于存入buffer的put()方法和取出缓存的get()方法在下面代码中我就详细介绍(有底层知识,具有源码阅读能力的可以根据我的注释进行阅读),最关键的还有flip()方法,它是作为一个读写切换的作用,他使的缓存可又读又写,又使得读写相互隔离(需要注意的是使用buffer尽量是依次写完然后再一次读完,最后在调用clear()方法进行复位,不然会导致buffer容量越来越小,具体解释在下面代码)。

package net.nio.buffer;

import java.nio.IntBuffer;

public class TestBuffer {
public static void main(String[] args) { /*
*IntBuffer有四个重要参数
* 1.mark 标记
* 2.position 相当于当前下标、索引
* 3.limit 代表缓冲的终点,读取不能超过该下标,当然也不能超过最大容量。(在调用flip时候会将当前下标position值赋值给limit,然后position置0)
* 4.Capacity 最大容量,在初始化IntBuffer对象时候就定义好了,不能改变(IntBuffer.allocate(int capacity) )
*
* ctrl+h 可以查看该类的子类
*/ //intBuffer初始化
IntBuffer intBuffer = IntBuffer.allocate(5); //放数据到缓冲区中
intBuffer.put(10);
intBuffer.put(11);
intBuffer.put(12);
// intBuffer.put(13);
// intBuffer.put(14); /*
*这里的读写反转的实现机制是:
* 例如我们缓冲区容量为5,调用方法put()将数据写入缓冲区中,假如我们写入三个此时position为3,此时limit = capacity
* 如果我们调用flip方法使得limit = 3 ,position = 0 ,mark我们现在先不管(下图源码已说明)
* public Buffer flip() {
* limit = position;
* position = 0;
* mark = -1;
* return this;
* }
*
* 此时我们调用get()方法时候,取得下标是position的值,即从0下标读取。直到读取到position = limit = 3时候停止(不包括3)
* 1.如果我们这个时候不调用flip()方法直接再次put()往缓冲区写入数据(即没从读状态切换到写状态),那么就会报错超过下标overflow
* 2.如果我们调用一次flip()(即进入写状态)写入一个数据后,那么此时position = 0,limit = 3,此时我们最多存放3个数据(即下标0,1,2)
* 如果我们不再次调用flip()切换状态那么就会导致,读取到错误数据,(即只存入了一个数据,但是却取出来了3个数据)
*
* 上述说明了一个问题,如果我们存取的数据越来越小,那么这个缓冲区逐渐缩小,导致并不能存取他的最大容量,可能会浪费内存,
*(因为position是不能超过limit的,然而调用flip()方法后会使的limit = position(赋值操作),那么如果数据越来越少,
* 就会导致缓冲区能使用的部分越来越小)
*
* 总结:缓冲区的大小设置应该根据实际使用进行设置(并且要及时调用clear() ),否则可能会导致缓冲区的内存浪费。
*/
intBuffer.flip();
//切换读写状态
//判断缓存区是否还有剩余
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}

Channel

Channel是NIO实现的基础,对于NIO,Channel的地位相当于BIO的socket。

Channel具有非常多方法,其中使用最多的就是两个方法write(ByteBuffer buf)和read(ByteBuffer buf)方法。

(这里需要注意的是这个read和write是buffer作为主体的,即read()方法是channel往buffer里写数据,而write()方法是指buffer向channel写数据)

package net.nio.channel;

        import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; public class TestChannel {
public static void main(String[] args) throws Exception{
String abc = "我写入文件了"; //写入的文件地址与文件名
FileOutputStream fileOutputStream = new FileOutputStream("C:\\xxx\\xxx\\xxx\\test.txt"); //从输出流中获取channel实例
FileChannel channel = fileOutputStream.getChannel(); //创建字节缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将字符串转化成字节数组并放入缓冲区中,然后缓冲区转换读写状态,由写入状态变为读取状态
byteBuffer.put(abc.getBytes());
byteBuffer.flip(); //将缓冲区数据写入到channel中(这里write代表从缓冲区写入,read代表从channel读取到缓冲区)
channel.write(byteBuffer);
//关闭通道和输出流
channel.close();
fileOutputStream.close();
}
}

简单NIO实现

上面在介绍NIO时候讲过,NIO是需要一个Selector线程去监听那些客户端有实现发生,从而在进行处理,而不是BIO的一个线程维护一个socket。

下面针对于NIO我们先不引入Selector,就用BIO的方式实现一个客户端和服务器端。(相当于作为一个练手)

Server

package net.nio.socket;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays; public class Server {
public static void main(String[] args) throws Exception {
//开启nio的服务器端,并且绑定8000端口进行监听
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(8000);
serverSocketChannel.bind(inetSocketAddress); //创建缓冲区数组
ByteBuffer byteBuffer = ByteBuffer.allocate(40); //服务器端接收来自客户端的请求,创建客户端的socket的实例
SocketChannel socketChannel = serverSocketChannel.accept();
//将客户端发送的数据读取到buffer数组中
socketChannel.read(byteBuffer);
byte[] array = byteBuffer.array();
String msg = new String(array);
System.out.println("服务器收到信息 : " + msg); //对buffer数组进行读写反转,由读状态到写状态
byteBuffer.flip(); //将数据回显到客户端去
byteBuffer.put("ok".getBytes()); //做完一套读写操作后,需要进行clear
byteBuffer.clear();
}
}

Client

package net.nio.serverclient;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel; public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);
socketChannel.configureBlocking(false); //如果客户端未连接上服务器
if (!socketChannel.connect(inetSocketAddress)) {
System.out.println("客户端连接不上服务器。。。。"); //如果客户端没有完成连接
while (!socketChannel.finishConnect()) {
System.out.println("连接中。。。。");
}
} //进入到这里说明连接成功
String message = "hello , Server!";
ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes()); //将buffer中的数据写入socketChannel
socketChannel.write(byteBuffer);
System.out.println("发送完毕");
}
}

NIO实现(基于Selector)

首先我们需要知道Selector是什么?

Selector是一个选择器,既然是一个选择器,那么肯定是先有选项再有选择,理解这个后就知道channel肯定有的就是rigister()方法(因为需要将自己注册到Selector中)。

既然选项有了,那么如何选择呢?

Selector是针对已注册的channel中对有事件(例如:服务器:接受,读写,客户端:读写,服务器是在服务器开始就将自己注册,客户端是连接成功后由服务器将其注册)发生的channel进行处理。

Selector注册的不是简单的channel,而是将channel和其监听事件封装成一个SelectionKey保存在Selector底层的Set集合中。

Selector的keys()和selectedKeys()两个方法需要注意:

keys()方法是返回已注册的所有selectionKey。

selectedKeys()方法是返回有事件发生的selectionKey。

上面就是Selector的简单工作流程,下面我将附上代码,因为有较详细的注释,所以除了重要知识点我不再多介绍。

Server

package net.nio.serverclient;

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 NIOServer {
public static void main(String[] args) throws Exception {
//开启ServerSocketChannel的监听
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //绑定端口8000
serverSocketChannel.bind(new InetSocketAddress(8000)); //创建Selector对象
Selector selector = Selector.open(); //设置监听为非阻塞
serverSocketChannel.configureBlocking(false); //将ServerSocketChannel注册到Selector中(注册事件为ACCEPT)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //Selector监听ACCEPT时间
while (true) {
//未有事件发生(下面是等待),返回值为int,代表事件发生个数
if (selector.select(1000) == 0) {
System.out.println("服务器等待了1S,无事件发生。。。。");
continue;
} //有客户端请求过来,就获取到相关的selectionKeys集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//获取到事件
SelectionKey selectionKey = iterator.next();
//移出读取过的事件
iterator.remove(); //根据对应事件对应处理
if (selectionKey.isAcceptable()) {
//有新的客户端连接服务器
SocketChannel socketChannel = serverSocketChannel.accept();
//给客户端设置非阻塞
socketChannel.configureBlocking(false);
//设置该SocketChannel为读事件,并为它绑定一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {
//通过Key反向获取到事件的channel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//获取到事件绑定的buffer
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
socketChannel.read(byteBuffer);
//重置缓冲
byteBuffer.clear();
String message = new String(byteBuffer.array());
System.out.println("接收到客户端信息为: "+ message);
}
}
}
}
}

Client

package net.nio.serverclient;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner; public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);
socketChannel.configureBlocking(false); //如果客户端未连接上服务器
if (!socketChannel.connect(inetSocketAddress)) {
System.out.println("客户端连接不上服务器。。。。"); //如果客户端没有完成连接
while (!socketChannel.finishConnect()) {
System.out.println("连接中。。。。");
}
} //进入到这里说明连接成功
while(true) {
Scanner scanner = new Scanner(System.in);
String message = scanner.nextLine();
ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes()); //将buffer中的数据写入socketChannel
socketChannel.write(byteBuffer);
System.out.println("发送完毕");
} }
}

我们这里可以发现,只需要主函数中进行一个死循环,死循环中对selector注册的channel进行监听(select()方法),有事件发生则根据channel注册的监听事件对应进行处理。

这里需要注意的是需要将ServerSocketChannel和SocketChannel编程非阻塞(调用configureBlocking(false)),不然是无法注册到Selector中。

还有一件事需要注意:我们每次是通过iterator(迭代器)遍历发生时间的Set ,为了避免重复处理时间,我们在获取发生时间的selctionKey以后,就将其remove()。

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

不会吧!做了这么久开发还有不会NIO的,看看BAT大佬是怎么用的吧的更多相关文章

  1. 为什么做java的web开发我们会使用struts2,springMVC和spring这样的框架?

    今年我一直在思考web开发里的前后端分离的问题,到了现在也颇有点心得了,随着这个问题的深入,再加以现在公司很多web项目的控制层的技术框架由struts2迁移到springMVC,我突然有了一个新的疑 ...

  2. 如何利用vue和php做前后端分离开发?

    新手上路,前端工程师,刚毕业参加工作两个月,上面让我用vue搭建环境和php工程师一起开发,做前后端分离,然而我只用过简单的vue做一些小组件的经验,完全不知道怎样和php工程师配合,ps: php那 ...

  3. 做了这么多年java开发,关于 Long 和 BigDecimal 的相等比较,你可不一定能准确回答下面 26 个问题

    Java 里面的 == 和equals的坑是在是太多了,即使做了多年java开发的程序员也不一定就能准确说出 a == b 或 a.equals(b) 这样简单的问题的答案. 请看下面这26道关于Lo ...

  4. 转: 为什么做java的web开发我们会使用struts2,springMVC和spring这样的框架?

    from: https://github.com/RubyLouvre/agate/issues/8 今年我一直在思考web开发里的前后端分离的问题,到了现在也颇有点心得了,随着这个问题的深入,再加以 ...

  5. 在做java 的web开发,为什么要使用框架

    现在做项目都会使用框架,现在很常见的框架就是SSH(Struts+SpringMVC+spring+hibernate),SSM(Struts/springMVC+Spring+Hibernate), ...

  6. C语言主要做哪些方面的开发---一个来自“IT技术学习”微信群的问题及答复

    近期,在"IT技术学习"微信群中,有同学问了这样一个问题:C语言主要做哪些方面的开发?在这篇文章中,我想结合自身的经验,对这个问题进行下解答. C语言是计算机及其相关专业(如通信. ...

  7. [转]Android通过NDK调用JNI,使用opencv做本地c++代码开发配置方法

    原文地址:http://blog.csdn.net/watkinsong/article/details/9849973 有一种方式不需要自己配置所有的Sun JDK, Android SDK以及ND ...

  8. 做php网站后台开发,在Linux系统上进行更好吗?

    1. PHP是开源软件,它在bsd/linux/win下都有很好的正式版及孪生版.并非开发php就必须要在linux下进行.主机服务商们习惯性的把asp与php分为两个主机系列几进行销售.由于asp只 ...

  9. 做了这么久的 DBA,你真的认识 MySQL 数据安全体系?【转】

    给大家分享下有关MySQL在数据安全的话题,怎么通过一些配置来保证数据安全以及保证数据的存储落地是安全的. 我是在2014年加入陌陌,2015年加入去哪儿网,做MySQL的运维,包括自动化的开发. 接 ...

随机推荐

  1. Helium文档1-WebUI自动化-环境准备与入门

    前言 Helium 是一款 Web 端自动化开源框架,全称是:Selenium-Python-Helium,从名字上就可以看出,Helium 似乎和 Selenium 息息相关,基于Selenium的 ...

  2. Ansible之YAML语言

    playbook写yml语句,若干模块发给Ansible,变成一个一个play,多个片段组合起来变成大片. 最终还是要读取主机清单,来确定作用在哪些机器上. YAML语言 YAML是一个可读性高的用来 ...

  3. Spring Boot 学习摘要--关于日志框架

    date: 2020-01-05 16:20:00 updated: 2020-01-08 15:50:00 Spring Boot 学习摘要--关于日志框架 学习教程来自:B站 尚硅谷 1. 关于日 ...

  4. zookeeper Cli的常用命令

    zookeeper Cli的常用命令 服务管理 启动ZK服务: zkServer.sh start 查看ZK状态: zkServer.sh status 停止ZK服务: zkServer.sh sto ...

  5. iOS 14 egret 游戏卡顿问题分析和部分解决办法

    现象 总体而言,iOS 14 渲染性能变差,可以从以下三个测试看出. 测试1:简单demo,使用egret引擎显示3000个图(都是同一个100*100 png 纹理),逐帧做旋转.(博客园视频播放可 ...

  6. 上午小测1 B.序列 哈希表+数学

    题目描述 \(EZ\) 每周一都要举行升旗仪式,国旗班会站成一整列整齐地向前行进. 郭神摄像师想要选取其中一段照下来.他想让这一段中每个人的身高成等比数列,展示出最萌身高差.但他发现这个太难办到了.于 ...

  7. vue-cli @4安装

    10月16日,官方发布消息称Vue-cli 4.0正式版发布,安装和vue-cli3.0的是一模一样的,与3.0的脚手架,除了目录发生变化一些,其他的都一样,由于近期才推出,企业中还在使用3.0,但是 ...

  8. TCP/IP 基础知识

    我把自己以往的文章汇总成为了 Github ,欢迎各位大佬 star https://github.com/crisxuan/bestJavaer 已提交此篇文章 要说我们接触计算机网络最多的协议,那 ...

  9. python实现密码破解

    排列组合(破解密码) 关注公众号"轻松学编程"了解更多. 1.排列 itertools.permutations(iterable,n) 参数一:要排列的序列, 参数二:要选取的个 ...

  10. 万亿级KV存储架构与实践

    一.KV 存储发展历程 我们第一代的分布式 KV 存储如下图左侧的架构所示,相信很多公司都经历过这个阶段.在客户端内做一致性哈希,在后端部署很多的 Memcached 实例,这样就实现了最基本的 KV ...