Java 网络编程(NIO)
前提
- 熟悉IO
- 了解BIO
了解NIO
NIO(即非阻塞IO,JDK1.4), 与BIO完全不同,服务端不会阻塞当前线程去等待新的客户端连接,也不会阻塞当前线程去等待客户端发送数据。
基础NIO,服务端源码
白话:服务端一直循环去获取与客户端的连接,获取到了就存到一个集合中,然后一直遍历这个集合来处理每一个客户端连接,当客户端连接与服务端断开后,就将集合中的连接channel移除掉。
package org.net;
import javax.swing.text.html.HTMLDocument;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 基础NIO 服务端示例
*/
public class NIOSocketServer {
public static void main(String[] args) {
try {
//创建NIOSocket通道
ServerSocketChannel serverSocket = ServerSocketChannel.open();
//绑定监听的地址端口
serverSocket.socket().bind(new InetSocketAddress("127.0.0.1", 9090));;
//设置为非阻塞(如果是true,就跟BIO一个鸟样了)
serverSocket.configureBlocking(false);
//创建一个List用于放客户端连接过来的socketChannel(遍历处理)
List<SocketChannel> socketChannels = new ArrayList<>();
//开始监听客户端连接
while (true) {
//非阻塞,这里其实和BIO差不多,就是不会一直阻塞等待客户端连接,有没有客户端连接进来都会往下走
SocketChannel socketChannel = serverSocket.accept();
//有客户端连接上来了,socketChannel不为null,反之
//将客户端连接配置为非阻塞,并且将其放到一个List中存着
if (socketChannel != null) {
System.out.println("有新的客户端连接上来了");
socketChannel.configureBlocking(false);
socketChannels.add(socketChannel);
}
//遍历List,处理每个客户端连接
Iterator<SocketChannel> iterator = socketChannels.iterator();
while (iterator.hasNext()) {
//从List中取出要处理客户端连接
SocketChannel socket_channel = iterator.next();
//读写取缓存
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socket_channel.read(byteBuffer);
if (read > 0) {
//将缓存区准备成读模式
byteBuffer.flip();
//将缓存中的内容转换成字符串
String msg = new String(byteBuffer.array(), 0, read);
System.out.println("收到客户端消息:" + msg);
//将缓冲区清空
byteBuffer.clear();
//向客户端写入消息
socket_channel.write(ByteBuffer.wrap("服务端:收到消息,谢谢".getBytes()));
if (msg.equals("exit")){
//客户端退出连接
socket_channel.close();
iterator.remove();
}
} else if (read == 0) {
//将缓存区清空
byteBuffer.clear();
try {
TimeUnit.MILLISECONDS.sleep(1500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//向客户端写入消息
socket_channel.write(ByteBuffer.wrap("服务端:你处于空闲状态".getBytes()));
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
测试
客户端程序代码查看BIO文章
-
先启动服务端程序,然后直接启动两个客户端程序
可以发现,服务端一直遍历socketChannel集合,处理连接上来的每一个客户端
-
两个客户端都给服务端发送一条数据
可以发现,服务端无需开启新的线程去处理客户端连接.
发现问题
- 因为serverSocketChannel.accept(),不会阻塞,所以一直while---true死循环,cpu↑
- 每个客户端socketChannel都放入List,当客户端并发很多的时候,List↑
解决问题,多路复用(Selector)
多路复用,指多个socket连接,使用一个线程来检测多个文件描述符(Socket)的状态,(Redis单线程模型,也是使用的多路复用)
监听客户端连接,有连接才处理,监听客户端发送数据,只处理发送数据的客户端。
白话:使用Selector,就像一个监听器,可以将服务端的serverSocketChannel以及客户端连接服务端成功后的socketChannel注册到Selector里面,,只要将serverSocketChannel和SocketChannel注册到Selector中就会产生一个SelectionKey,这个SelectKey就是对应serverSocketChannel和SocketChannel注册到Selector中时所设置的事件
,只要对应的serverSocketChannel或SocketChannel触发了对应的注册进Selector时设置的事件,Selector.select()监听到事件发生就会放行,Selector.selectedKeys()就可以获取触发了的SelectionKey(以Set集合方式返回,集合中不包含没有触发的SelectionKey),通过SelectionKey.channel()就可以获取对应事件的Channel(serverSocketChannel或SocketChannel)然后做相应的处理。
/**
* 基础NIO 服务端示例
*/
public class NIOSocketServer {
public static void main(String[] args) {
try {
//创建NIOSocket通道
ServerSocketChannel serverSocket = ServerSocketChannel.open();
//绑定监听的地址端口
serverSocket.socket().bind(new InetSocketAddress("127.0.0.1", 9090));;
//设置为非阻塞(如果是true,就跟BIO一个鸟样了)
serverSocket.configureBlocking(false);
//创建一个选择器,用于监听服务端与客户端之间发生的事件
Selector selector = Selector.open();
//将服务端通道注册到选择器上,并监听客户端连接事件
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//开始监听客户端与服务端发生的事件(会阻塞,只要客户端和服务器存在事件发生才会放开)
selector.select();
//获取客户端与服务端发生的事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//只处理发生的
while (iterator.hasNext()) {
//取出事件
SelectionKey selectionKey = iterator.next();
//如果是连接事件
if (selectionKey.isAcceptable()) {
//事件中获取到这个与客户端的socketChannel,因为是ServerSocketChannel发生accept事件所以
//selectionKey.channel()获取的是因为是ServerSocketChannel发生accept事件所以
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
//将这个socketChannel放到选中器上,监听这个socketChannel读事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("有新的客户端连接");
//写数据给客户端
socketChannel.write(ByteBuffer.wrap("连接服务器成功".getBytes("UTF-8")));
//如果是读事件,也就是客户端往服务器发送数据
} else if (selectionKey.isReadable()) {
//因为是SocketChannel发生读事件,所以selectionKey.channel()获取SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
if (read > 0) {
//将byteBuffer为读
byteBuffer.flip();
String msg = new String(byteBuffer.array(), 0, read);
System.out.println("服务器收到消息:" + msg);
//清空byteBuffer
byteBuffer.clear();
//写数据给客户端
socketChannel.write(ByteBuffer.wrap("服务器收到消息:".getBytes("UTF-8")));
if (msg.equals("exit")){
//断开对应客户端连接
socketChannel.close();
}
}
}
//每次处理完一个事件就集合中清除这个事件
iterator.remove();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}