一、重点知识
服务器端处理要用多线程,因为互联网程序不能让前一个连接阻塞下一个链接
系统线程开得越多,浪费系统的资源就越,因为系统要去监测线程
NIO取数据时,limit移动到position的位置上,position移到数组最前端,当position位置与limit位置重合时,取数据过程结束
(调用flip()方法)
limit只有在切换模式时候改变位置
limit标记的位置和他之后的位置都不能用
一旦调用mark,mark就会移动到此时position的位置,mark最开始在最前面,lmark的作用可以让position再操作一遍标记后的数据
NIO现在的使用策略是与多线程结合,一个线程包裹多个连接
中文和英文再java中都是按字符算的,存储在硬盘中的时候按照编码表才有区别
无论调用put还是get position都代表可操作的位置
不论是输出模式还是输入模式都可以用mark()标记
抽象类是接口与实体类的平衡点,因为它既能有抽象方法,又能有实体方法
filechannal不支持非阻塞
NIO在避免多线程的情况下,也可以让多个连接同时进来,但是他无法平均等待时间
如果传输过程是有间隔的,NIO也可以模拟平均等待时间
处理通道部分的代码一定要加try-catch ,而且不和其他代码共用一个try-catch,单独用一个try-catch包裹起来,因为防止通道部分出现异常而导致服务器关闭
抛出异常必要性的讲解
[https://blog.csdn.net/abc_key/article/details/29295569]
二、重点问题
1、TCP使用多线程的原因
1、因为tcp本来在读取文件时要分批读取,所以有等待时间,所以要多线程来处理,在等待时间执行其他客户端读取操作
2、有可能服务端要对数据进行验证,所以有等待时间
2、canncel和remove的区别
通道调用canncel方法是能把这个通道从管理器中移除的,而remove方法存在的必要性是因为在我们遍历通道管理器的时候实际上是吧这些通道从通道管理器中取出来了,但是是需要我们还回去的,所以我们需要调用remove方法,把这些通道返回通道管理器中
3、为什么wait和notify方法定义在Object中?
因为,wait()和notify()要通过锁来调用,但是锁又可以是任何一个类的对象,所以wait()和notify()一定要定义在object内
三、课堂知识
一. NIO
-
定义
- NIO是面向缓冲区的流, 我们将数据和缓冲区通过一根管道连接起来,然后我们对缓冲区中的数据进行操作了
- NIO是双向的流, 也就是说,这个缓冲区既可以存储又可以输出
- NIO是非阻塞的, 通道建立之后,就会自动的读或取了,这就意味着一个线程可以管理多个流通道
- NIO在解析数据的时候非常麻烦, 但适用于高并发小流量的场景,如聊天服务器
- 线程越多, 浪费的资源就越多
- 多线程为什么浪费资源?
-
NIO的作用
- 避免多线程的开销
- 可以模拟出多线程的处理方式 (通道的数据时间有间隔的)
二. Buffer(缓冲区)
-
定义
- 因为NIO主要就是对缓冲区进行操作,所以,这个至关重要
-
分类
- 除了boolean外的基本数据类型,都提供了对应的缓冲区
- ByteBuffer , CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer
- 常用的就是ByteBuffer , CharBuffer
-
重要属性
- capacity : 缓冲区容量, 表示缓冲区中最大存储数据的容量, 一旦声明,不能改变
- limit : 界限, 表示缓冲区中可以操作数据的大小(limit和limit后的数据不能进行读写)
- position : 位置, 表示缓冲区中正在操作数据的位置
- mark : 标记, 可以标记position的位置 ,可以使用reset()方法,将position回到标记位置
-
常用方法
- allocate(int capacity) : 指定缓冲区的大小
- put() : 存储数据
- get() : 获取数据
- flip() : 切换成输出模式
- rewind() : 切换回输出模式的初始化位置,重复读
- clear() : 清空缓冲区, 所有标记回到最初状态, 其中的数据并没有被清空,只是处于"被遗忘"状态
- mark() : 标记position的位置
- reset() : 将position回到标记的位置
-
演示
public static void main(String[] args) throws Exception { //创建指定容量的字节缓冲区 ByteBuffer buffer = ByteBuffer.allocate(10); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); System.out.println("..........put........."); //添加 buffer.put("abc".getBytes()); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); //切换成输出模式 position位置归0 limit移动到position原来的位置 System.out.println("..........flip........."); buffer.flip(); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); //获取当前position位置的值 position位置+1 System.out.println("..........get........."); byte b = buffer.get(); System.out.println(b); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); //切换回输出模式的初始化位置,重复读 System.out.println("..........get........."); buffer.rewind(); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); //清空缓冲区,一切还原,为再次写入做准备 System.out.println("..........get........."); buffer.clear(); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); //表示postion的位置 使用reset()将position的位置回归到mark标记位置 System.out.println("..........mark()和reset()........."); buffer.put("abc".getBytes());//添加三个字节 buffer.mark(); buffer.put("df".getBytes()); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); buffer.reset(); System.out.println(buffer.position()); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); }
三. Channel(通道)
-
定义
- 用于读取、写入、映射和操作文件的通道,可以将程序和数据实体建立连接
- java的流都提供了获取通道的方法
- Channel是双向的,既可以读又可以写,而流是单向的
- Channel可以进行异步的读写
- 对Channel的读写必须通过Buffer对象
-
常用方法
- read(Buffer b) : 将数据写入到缓冲区
- write(Buffer b) : 从缓冲区输出数据
四. FileChannel(不推荐使用)
-
定义
- 用于读取、写入、映射和操作文件的通道
- 将数据读取存储到缓冲区, 也可以将缓冲区的数据写入到本地
- 这个类无法直接关联到文件,必须通过IO的流进行获取, 预留的方法,但是还没有启用
- FileChannel是阻塞的
-
演示
public static void main(String[] args) throws Exception { ByteBuffer bf = ByteBuffer.allocate(1024); //从字节数据流中获取文本FileChannel FileInputStream fis = new FileInputStream("d:\\骑在银龙的背上.mp3"); FileChannel fcr = fis.getChannel(); //从字节输出流中获取文本FileChannel FileOutputStream fos = new FileOutputStream("d:\\音乐.mp3"); FileChannel fcw = fos.getChannel(); while(fcr.read(bf)!=-1){ bf.flip(); fcw.write(bf); bf.clear(); } fcr.close(); fcw.close(); fis.close(); fos.close(); //快速复制 //fcr.transferTo(0, fcr.size(), fcw); }
五. DatagramChannel
-
定义
- 针对面向数据报套接字的可选择通道
- 操作UDP的NIO流
-
演示
- 接收端
public static void main(String[] args) throws Exception { //获取DatagramChannel DatagramChannel channel = DatagramChannel.open(); //创建socket channel.bind(new InetSocketAddress(9999)); //创建缓冲区 ByteBuffer buf = ByteBuffer.allocate(100); Scanner scanner = new Scanner(System.in); while(true){ //清空缓冲区,准备接收数据 buf.clear(); //接收网络数据 channel.receive(buf); System.out.println(new String(buf.array(),0,buf.position())); String str = scanner.nextLine(); //情况缓冲区,准备存入数据 buf.clear(); buf.put(str.getBytes()); //将缓冲区切换成输出模式 buf.flip(); //发送数据 channel.send(buf, new InetSocketAddress("127.0.0.1", 6666)); } }
- 发送端
public static void main(String[] args) throws Exception { //创建获取DatagramChannel DatagramChannel channel = DatagramChannel.open(); //创建socket channel.socket().bind(new InetSocketAddress(6666));; ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("我爱你".getBytes()); SocketAddress socket = new InetSocketAddress("127.0.0.1",9999); Scanner scanner = new Scanner(System.in); while(true){ buffer.clear(); String str = scanner.nextLine(); //将数据装入缓冲区 buffer.put(str.getBytes()); //将缓冲区切换为输出模式 buffer.flip(); channel.send(buffer, socket); //清空缓冲区,为接受数据做准备 buffer.clear(); //接收数据 channel.receive(buffer); System.out.println(new String(buffer.array(),0,buffer.position())); } }
六. SocketChannel和ServerSocketChannel
-
定义
- 对应着TCP协议
- 打开一个SocketChannel并连接到互联网上的某台服务器
- 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel
- 用法和Socket,ServerSocket完全一致
-
演示
- 客户端
public static void main(String[] args) throws Exception { //打开通道 SocketChannel channel = SocketChannel.open(); //建立连接 channel.connect(new InetSocketAddress("127.0.0.1", 9999)); //设置缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("I LOVE YOU".getBytes()); //将缓冲区设置为输出模式 buffer.flip(); channel.write(buffer); }
- 服务端
public static void main(String[] args) throws Exception { //打开通道 ServerSocketChannel channel = ServerSocketChannel.open(); //建立服务端 channel.socket().bind(new InetSocketAddress(9999)); //获取socket SocketChannel socket = channel.accept(); ByteBuffer bf = ByteBuffer.allocate(1024); //接收数据 socket.read(bf); System.out.println(new String(bf.array(),0,bf.position())); }
-
测试题
- 编程实现一个可以相互聊天的客户端和服务端
七. Selector
-
定义
- Selector是一个通道管理器
- 我们知道,NIO具有非阻塞的能力, 可以在一个线程内同时执行多个操作, 节省了线程间切换的开销
- 但是, 当启动非阻塞的时候,输入和输出方法就完全独立运行了, 这可能导致读的时候对面还没有把信息发送过来, 写的时候,对方还没有完全准备好
- 所有, 我们使用Selector类对通道进行管理,当某个操作准备好了之后, Selector会提醒我们,这时,我们就可以进行操作了
-
Selector监视的状态分类
- SelectionKey.OP_CONNECT 连接准备就绪
- SelectionKey.OP_ACCEPT 客户端已经连接
- SelectionKey.OP_READ 要读的数据已经准备好
- SelectionKey.OP_WRITE 可以进行写入了
-
常用方法
- select() 获取所有已经准备好的通道,仅仅是这次的,上一次调用这个方法获取的通道不算
- selectedKeys() 获取上一次select()方法获取到的通道
-
编码步骤
public static void main(String[] args) throws Exception { //打开通道 SocketChannel channel = SocketChannel.open(); //建立连接 channel.connect(new InetSocketAddress("127.0.0.1", 9999)); //将当前通道设置为非阻塞 channel.configureBlocking(false); //获取通道选择器 Selector selector = Selector.open(); //将通道注册进通道选择器中,这里设置通道选择器需要监视的状态是"可读取" channel.register(selector, SelectionKey.OP_READ); //往服务端发送一条数据 ByteBuffer bs = ByteBuffer.allocate(1024); bs.put("我爱你".getBytes()); bs.flip(); channel.write(bs); //控制循环,时刻检测通道选择器 while(true){ //查看通道选择监视的状态时候有通道符合要求了 //select 方法获取所有符合状态的通道 if(selector.select()>0){ //遍历符合状态的通道 for (SelectionKey key : selector.selectedKeys()) { //判断当前通道是否可读 if (key.isReadable()) { //读取内容 ByteBuffer buffer = ByteBuffer.allocate(1024); SocketChannel socket = (SocketChannel)key.channel(); int len = socket.read(buffer); System.out.println(len); System.out.println(new String(buffer.array(),0,buffer.position())); //改变通道的需要监视的状态 //key.interestOps(SelectionKey.OP_READ); } //将键从已经选择的集合中去除 //这个里获取到的通道都是上一次select()方法已经执行到的,如果不去除的话,下一次调用select()方法就无法获取到了 selector.selectedKeys().remove(key); } } } }
总结:
- 原始tcp的问题
- 如果不使用多线程, 会造成第一个连接阻塞第二个连接
- 如果使用多线程, 会增加系统的开销(会有很多的资源浪费在管理,监测线程上)
- 分析问题
- 通过分析我们发现, 尽量不要使用多线程, 但是不是使用多线程又不行, read阻塞着下一个连接
- 所以,我们设法将程序中的阻塞方法设置为非阻塞 , 就可以解决 上一个连接阻挡下一个连接的情况
- 但是, 问题是数据无法正确的读取到了, 使用通道管理器管理通道
- 方法是可以设置为非阻塞
- 通道管理器帮助我们管理通道
- NIO编程的优点
- 不需要使用多线程, 减少了线程的开销
- 可以模拟多线程的运行方式 (其实没有办法真的达到多线成的平均时间的效果)
- NIO的缺点
- 其实没有办法真的达到多线成的平均时间的效果
- 不适用于大流量的场景 , 只适用于小流量高并发的场景
- Buffer
- 缓冲区, 有两种模式: 输入和输出模式
- capacity limit postion mark
- flip() clear()
- 数据假死, 被遗忘状态
网友评论