一、NIO概述
1.首先介绍一下BIO(同步阻塞IO)
BIO-JDK1.0 - 同步阻塞式IO-BlockingIO
在执行ACCEPT CONNECT READ WRITE 四中操作时都会产生阻塞
Accept:客户端未连接
Connect:连接超时(connection reset);连接拒绝connection refuse
Read:服务器端读取数据,但是客户端未写入数据,产生阻塞
Write:客户端写入数据,服务器端未读取,当写入数据数量达到缓冲区极限,产生阻塞
在平常开发当中并不是问题 甚至因为这样的模型直观而简单 应用的场景非常广泛
但是在高并发的场景下 这样的阻塞式IO可能会造成如下问题:
在服务器开发中 需要在服务器端通过少量线程处理多个客户端请求 这就要求 在少量的线程应该可以灵活的切换处理不同客户端 但传统的BIO阻塞式的工作方式 一旦阻塞了线程 线程就被挂起 无法继续执行 无法实现这样的功能
2.NIO - JDK4.0 - 同步非阻塞式IO - NonBlockingIO/NewIO
操作: Accepet、 Connect、 Read、Write(非阻塞的)
可以随时让线程切换所处理的客户端 从而可以实现高并发服务器的开发
需求:用少量的线程来处理多个客户端请求
腾讯QQ聊天案例
多人连接服务器,通过一个中心选择器进行管理,当多人需要聊天发送消息时(排队处理),提供线程,一个客户占用线程没发消息,导致其他人消息发不出去(BIO)
使用NIO用少量的线程来处理多个客户端请求,让线程可以随时切换所处理的客户端
两者特点对比:
BIO:同步阻塞式IO 面向流 操作字节或字符 单向传输数据
NIO:同步非阻塞式IO 面向通道 操作缓冲区 双向传输数据
二、开源的NIO结构的服务器框架(了解)
MINA
Netty(更好)
IO方式:
阻塞/非阻塞:讨论的是线程的角度,当执行某些操作不能立即完成时,线程是否被挂起,失去cpu争夺权无法继续执行直到阻塞结束或被唤醒
同步/异步:讨论的是参与通信双方的工作机制,是否需要互相等待对方的执行
同步:
通信过程中 一方在处理通信,另一方 要等待对方执行不能去做其他无关的事
异步:
通信过程中 一方在处理通信,另一方 可以不用等待对方而可以去做其他无关的事 直到对方处理通信完成 再在适合的时候继续处理通信过程
三种IO机制的区别!!!
BIO | jdk1.0 | 同步阻塞式IO | 面向流 | 操作字节或字符 | 单向传输数据 |
NIO | jdk4.0 | 同步非阻塞式IO | 面向通道 | 操作缓冲区 | 双向传输数据 |
AIO | jdk7.0 | 异步非阻塞式IO | 大量使用回调函数 | 异步处理通信过程 |
三、粘包问题
概念:通过socket发送多段数据时 底层的tcp协议 会自动根据需要 将数据 拆分或合并 组成数据包后发送给接受者 ,接受者收到数据后 无法直接通过tcp协议本身判断数据的边界,这个问题就称之为粘包问题.
产生原因:
1.本质上是因为tcp协议是传输层的协议 本身没有对会话控制提供相应的能力
2.socket开发网络程序时相当于在自己实现会话层、表示层和应用层的功能,所以需要自己来相办法解决粘包问题
举例:aaa bbbbb cccc
TCP属于传输层,底层靠数据包传输,只负责传输数据,不负责分配数据格式,结果为ccccbbbbbaaa
导致不能明确数据的结构进行分段
解决方案:
a.只发送固定长度的数据
通信的双发约定每次发送数据的长度,每次只发送固定长度的数据,接收数据方 每次都按照固定长度获取数据
缺点:
不够灵活,只适合每次传输的数据都有固定长度的场景
b. 约定分隔符
通信双方约定一个特殊的分隔符用来表示数据的边界,接收方收到数据时,不停读取,以分隔符为标志,区分数据的边界
缺点:
如果数据本身就包含分隔符字符,则需要对数据进行预处理将数据本身包含的分隔符进行转义,相对来说比较麻烦
c. 数据分头和体,在头信息中描述数据长度或格式
通过头信息中传递数据长度或格式信息,在接收方法接收数据时,先读取头信息,在根据头信息来决定如何获取后续数据
协议:
公共协议:HTTP FTP SMTP POP3等等,需按照协议来通信,约束较大
通信问题解决:真正在开发过程中,如果需要开发底层网络通信机制,可以根据需要选择公有协议 或 自定义私有协议来解决通信规范问题
四、NIO通信案例
服务端:
服务端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.创建ServerSockentChannel对象
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.绑定指定端口
ssc.bind(new InetSocketAddress(44444));
//3.设置非阻塞模式
ssc.configureBlocking(false);
//4.接收客户端连接
SocketChannel sc = null;
while(sc == null){
sc = ssc.accept();
}
sc.configureBlocking(false);
//5.读取数据
ByteBuffer buf = ByteBuffer.allocate(5);
while(buf.hasRemaining()){
sc.read(buf);
}
//6.获取数据打印
byte[] arr = buf.array();
String str = new String(arr);
System.out.println(str);
//5.关闭通道
sc.close();
ssc.close();
}
}
客户端:
客户端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.创建客户端SocketChannel
SocketChannel sc = SocketChannel.open();
//2.配置启用非阻塞模式
sc.configureBlocking(false);
//3.连接服务器
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
if(!isConn){
while(!sc.finishConnect()){
}
}
//4.发送数据到服务器
ByteBuffer buf = ByteBuffer.wrap("abcde".getBytes());
while(buf.hasRemaining()){
sc.write(buf);
}
//5.关闭通道
sc.close();
}
}
五、少量线程处理多客户端请求案例
客户端:
客户端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.创建客户端SocketChannel
SocketChannel sc = SocketChannel.open();
//2.配置启用非阻塞模式
sc.configureBlocking(false);
//3.连接服务器
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
if(!isConn){
while(!sc.finishConnect()){
}
}
//4.发送数据到服务器
ByteBuffer buf = ByteBuffer.wrap("abcde".getBytes());
while(buf.hasRemaining()){
sc.write(buf);
}
//5.关闭通道
sc.close();
}
}
服务端:
package cn.tedu.nio.selector;
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 ServerSocketDemo01 {
public static void main(String[] args) throws Exception {
//0.创建选择器
Selector selc = Selector.open();
//1.创建代表服务器的ServerSocketChannel对象
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.设置为非阻塞模式
ssc.configureBlocking(false);
//3.设置监听的端口
ssc.bind(new InetSocketAddress(44444));
//4.将ssc注册到选择器中关注ACCEPT操作
ssc.register(selc, SelectionKey.OP_ACCEPT);
//5.通过选择器选择就绪的键
while(true){
selc.select();//尝试到注册的键集中来寻找就绪的键 如果一个就绪的键都找不到 就进入阻塞 直到找到就绪的键 返回就绪的键的个数
//6.获取就绪的键的集合
Set<SelectionKey> keys = selc.selectedKeys();
//7.遍历处理就绪的键 代表的操作
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
//--获取到就绪的键 根据键代表的操作的不同 来进行不同处理
SelectionKey key = it.next();
if(key.isAcceptable()){
//--发现了Accept操作
//--获取通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
//--完成Accept操作
SocketChannel sc = sscx.accept();
//--在sc上注册读数据的操作
sc.configureBlocking(false);
sc.register(selc, SelectionKey.OP_READ);
}else if(key.isConnectable()){
}else if(key.isWritable()){
}else if(key.isReadable()){
//--发现了Read操作
//--获取就绪的通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成读取数据的操作
ByteBuffer buf = ByteBuffer.allocate(10);
while(buf.hasRemaining()){
scx.read(buf);
}
String msg = new String(buf.array());
System.out.println("[收到来自客户端的消息]:"+msg);
}else{
throw new RuntimeException("未知的键,见了鬼了~");
}
//8.移除处理完的键
it.remove();
}
}
}
}