BIO&NIO
java存在的Socket,以下是它API的类:
BIO
ServerSocket
Socket
NIO
ServerSocketChannel
SocketChannel
BIO
案例
模拟一段BIO服务器端进行Socket编程:
public class BIOServer {
public static void start(int port) throws IOException {
//1、建立socket连接
ServerSocket serverSocket = new ServerSocket();
//2、绑定和监听
serverSocket.bind(new InetSocketAddress(port),2);
//支持连接的端口号和连接数
//3、起线程跑连接,或者用线程池。
while (true){
final Socket clientSocket = serverSocket.accept();
System.out.println("accept");//标识接收到了请求
new Thread(()->{
try {
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true);
String line = in.readLine();//阻塞
while(line !=null){
out.println(line);
out.flush();
line=in.readLine();
}
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
public static void main(String[] args) throws IOException {
start(2020);
}
}
启动cmd然后和我们的程序建立连接:
telnet localhost 2020然后执行cmd命令发现成功:

阻塞分析
红色部分代表了阻塞,下面解释一下阻塞的原因:
accept:阻塞是因为网络传输的问题等
read&parse:需要解决字节边界问题,需要截取每个请求需要的字节。当收不到请求字节了,就要切换线程。当发送的请求断断续续的,每次接收一点就要切换一次后,阻塞就会非常严重。
write:接收方有缓存大小,类似于窗口大小,当接收到足够的数据包才会写出去。例如,需要数据包为1-5,但只接受到了2-5,那我们会一直堵塞,直到得到数据包1。

tcp也有自己的一套流量控制和拥塞控制算法,所以tcp天生拥有背压能力,能够很天然的适应流量弹性的变化。
缺点
上下文切换:在read&parse过程中,我们需要去切割字节,获取到对应的请求,但如果此时网络不畅,那么我们接收到的封包就会断断续续的,过了一段时间后就会切换下个线程。
eg:如果A线程需要的数据是0-100,我的线程是A,B,C。刚开始A线程接收到50个数据,接下去因为网络原因无法接受到了,所以切换到了B或者C线程。这时候如果又来了10个A所需的数据又要切换到A接收,这样非常浪费时间。

总结:可以发现,当我们的连接数多了,我们传统的blocking的服务器模型,效率就很低了。为了解决BIO的上下文切换存在的问题,我们引出了NIO
NIO
核心目标:减少线程数,减少上下文切换问题。
解决办法:将accept、read&parse、write放入更为底层的部分,放入到一个线程中,由一个线程去统筹其他的线程来执行这些操作。比如说,当网络断断续续后,这个线程可以通知正在read过程中被阻塞的线程别阻塞了,切换到其他的Socket线程进行读。
下面是NIO的模型,采用操作系统事件的机制,我们将统筹所有连接的线程称之为Selector:
模型解释
负责多个Socket连接,当Socket的状态发生变化了,都会通知Selector。Selector会对所有的连接进行轮询(定时任务),做对应事件的事情,所以不会涉及到任何的浪费。

Selector API
channel.register(selector) :注册监听
while(true)+ select():轮询事件
selectedKeys():获得selectionKey对象,表示channel的注册信息
SelectionKey.attach()对selectionkey:关联任何对象
isReadable()/ isAcceptable()/ isWritable():判断事件类型
事件类型:OP_ACCEPT/OP_READ/OP_WRITE/OP_CONNECT
整体步骤
把想要被Selector监听的ServerSocket注册到channel上
无限轮询,然后去查看Socket的状态
一旦轮询到需要的对象,使用selectedKeys去获取对象
根据事件类型(ACCEPT、READ、WRITE、CONNECT),根据这四种状态去进行相应的操作。
案例
举例说明下步骤,一共七步走,
第一步:创建一个信道
第二步:设置是否阻塞并设置端口号,这里要用NIO肯定是非阻塞的
第三步:同BIN过程,绑定套接字地址,这里可以绑定多个,只要在后面加上.bin即可
第四步:创建selector并绑定事件
第五步:进行轮询,查看是否有注册过channel的状态得到了满足
第六步:从selector中得到集合,但也有可能Socket状态都没改变,集合为空
第七步:进入事件处理三步走
从信道中获取连接
同BIO过程,对其进行accept
设置连接非阻塞,并且转换连接的状态
public class NIOServer {
public static void start(int port) throws IOException {
//1、创建一个信道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2、设置是否阻塞并设置端口号,这里要用NIO肯定是非阻塞的
serverSocketChannel.configureBlocking(false);
InetSocketAddress address = new InetSocketAddress(port);
//3、同BIN过程,绑定套接字地址,这里可以绑定多个,只要在后面加上.bin即可
serverSocketChannel.bind(address);
//4、创建selector并绑定事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){ //这里如果只请求一次不会出错
//5、进行轮询,查看是否有注册过channel的状态得到了满足
//但是这块底层会有一些bug,因为非阻塞,所以while会空转
selector.select();
//6、从selector中得到集合,但也有可能Socket状态都没改变,集合为空
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> it = readyKeys.iterator();
//7、进入事件处理三步走
while (it.hasNext()){ //进入事件处理三步走
SelectionKey key = it.next();
if (key.isAcceptable()){
//(1)、从信道中获取连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//(2)、同BIO过程,对其进行accept
SocketChannel socket = server.accept();
System.out.println("Accept !");
//(3)、设置连接非阻塞,并且转换连接的状态
socket.configureBlocking(false);
socket.register(selector,SelectionKey.OP_READ);//将其从accept转换成read
System.out.println("经历了一次状态转换过程");
}
if (key.isReadable()) {
//(1)、从信道中获取连接
SocketChannel socket = (SocketChannel) key.channel();
//(2)创建字节流,接受传入的流
final ByteBuffer buffer =ByteBuffer.allocate(64);
final int bytesRead =socket.read(buffer);//读取流
if (bytesRead>0){
buffer.flip();//翻转缓冲区,理解成刷新缓存
int ret =socket.write(buffer);
if (ret<=0){
socket.register(selector,SelectionKey.OP_WRITE);
}
buffer.clear();
} else if (bytesRead<0){
key.cancel();
socket.close();
System.out.println("Client close");
}
}
it.remove();
}
}
}
public static void main(String[] args) throws IOException {
start(2020);
}
}
测试一下,运行程序,打开cmd连接端口号:
telnet localhost 2020查看结果:

这里会存在一个问题,因为NIO非阻塞,所以当没接收到Socket连接的时候会存在空转问题,空转就是while(true)就会一直执行,直到吧内存占满。
缺点
空转问题(会报错):因为NIO是非阻塞的,所以当没有请求后,也不会阻塞。此时while(true)就会一直执行,直到吧内存占满。
代码不好复用,如果我需要实现某一功能并没有BIO那么容易,也不好抽取出来成单独模块。
虽然NIO有缺点,但NIO的非阻塞和单线程处理Socket对效率的提升而言非常大,所以我们还是得用。往后我们就引入了Reactor模式,即响应式编程。




