1. 前言
首先对应NIO来说是同步非阻塞的,这里再看一下什么是同步什么是异步,什么是阻塞什么是非阻塞
2. BIO
多人聊天室阻塞发生在哪里?
① ServserSocket.accept()
是阻塞的
② 所有输入流和输出流都是阻塞的,接收方等待发送方发送消息的时候是阻塞等待的,如果发送方一直不发送消息,那么接收方就要一直阻塞等待干不了其他事
③ 在BIO
多人聊天室的案例中,由于BIO
输出输出的阻塞,我们不得不为他们分配不同的线程
3. NIO
实现多人聊天室
对于NIO来说最核心的三个部件就是Channle
+Buffer
+selector
Channel
: Channel
是程序和磁盘中间的通道,我们可以把他理解为生活当作的铁路,只是用于连接,铁路自己本身不能完成运输,想完成运输要依赖于火车
Buffer
: Buffer
就是火车,Buffer
在Java NIO 中负责数据的存取,把数据装到缓冲区从铁路传输缓冲区到目的地,Buffer
可以在Channel
上双向移动(可读可写)
Selector
: 同步非阻塞就是Selector
来实现的,将 Channel
注册到 Selector
中,通过Selector
来管理Channel
,Selector不断检查Channel
的状态,当Channel
准备好读写了,Selector
就会感知到并进行处理
所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时
传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了"
3.1 服务端ChatServer
public class ChatServer {
private static final int DEFAULT_PORT = 8888;
private static final String QUIT = "quit";//退出关键词
private static final int BUFFER = 1024;//缓冲区大小
private ServerSocketChannel server;
private Selector selector;//selector
private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);//从用户channel读的buffer
private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);//用于向其他用户channel写的buffer
private Charset charset = Charset.forName("UTF-8");
private int port;//可以自定义服务端端口
public ChatServer(int port) {
this.port = port;
}
public ChatServer() {
this(DEFAULT_PORT);
}
public synchronized boolean readyToQuit(String msg){
if (msg.equals(QUIT)){
return true;
}
return false;
}
public void start(){//主要逻辑
try {
//打开服务端 Socket
server = ServerSocketChannel.open();//ServerSocketChannel默认是阻塞的
//转换为非阻塞
server.configureBlocking(false);
//监听端口
server.socket().bind(new InetSocketAddress(port));
selector = Selector.open();
//注册ServerSocketChannel的accept
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("启动服务器,监听端口"+port+"....");
while (true){
//select是阻塞式的,如果没有注册的通道发生监听的事件就会阻塞
selector.select();
//有事件发生了
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//处理触发事件
for (SelectionKey key : selectionKeys) {
//处理
handles(key);
}
//清空处理完的
selectionKeys.clear();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handles(SelectionKey key) throws IOException {
//触发了服务端的Accept
if(key.isAcceptable()){
//得到服务端通道
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//客户端通道
SocketChannel client = server.accept();
//转化为非阻塞
client.configureBlocking(false);
//注册Read事件
client.register(selector,SelectionKey.OP_READ);
System.out.println("客户端"+client.socket().getPort()+"已连接");
}else if(key.isReadable()){//触发了客户端的Read
SocketChannel client = (SocketChannel) key.channel();
//读
String fwdMsg = receive(client);
if(fwdMsg.isEmpty()){
//客户端异常
key.cancel();//移除
}else{
//转发数据
forwardMessage(client, fwdMsg);
//检查用户是否退出
if(readyToQuit(fwdMsg)){
//断开
key.cancel();
}
}
}
}
private void forwardMessage(SocketChannel client, String fwdMsg) throws IOException {
//找到目前在线的客户端
Set<SelectionKey> keys = selector.keys();
for (SelectionKey key : keys) {
//服务端的跳过
if(key.channel() instanceof ServerSocketChannel){
continue;
}
if(key.isValid() && !key.channel().equals(client)){
wBuffer.clear();
wBuffer.put(charset.encode(fwdMsg));
wBuffer.flip();
while (wBuffer.hasRemaining()){
((SocketChannel)key.channel()).write(wBuffer);
}
}
}
}
private String receive(SocketChannel client) throws IOException {
rBuffer.clear();
while((client.read(rBuffer))>0){//只要还能读就读
}
rBuffer.flip();
return String.valueOf(charset.decode(rBuffer));
}
public static void main(String[] args) {
}
}
3.2 客户端ChatClient
public class ChatClient {
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private final int DEFAULT_SERVER_PORT = 8888;
private final String QUIT = "quit";
private static final int BUFFER = 1024;//缓冲区大小
private SocketChannel client;
private Selector selector;//selector
private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);//从用户channel读的buffer
private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);//用于向其他用户channel写的buffer
private Charset charset = Charset.forName("UTF-8");
public boolean readyToQuit(String msg){
return QUIT.equals(msg);
}
public void start(){
try {
client = SocketChannel.open();
//非阻塞
client.configureBlocking(false);
selector = Selector.open();
//注册事件
client.register(selector, SelectionKey.OP_CONNECT);
client.connect(new InetSocketAddress(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT));
while(true){//轮询
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey key : selectionKeys) {
handles(key);
}
selectionKeys.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void handles(SelectionKey key) throws IOException {
//CONNECT事件 连接就绪
if(key.isConnectable()){
SocketChannel channel = (SocketChannel)key.channel();
if(client.isConnectionPending()){//就绪
client.finishConnect();//正式建立连接
//新线程做用户输入
new Thread(new NIOUserInputHandler(this)).start();
}
//注册,监听read
client.register(selector,SelectionKey.OP_READ);
}else if(key.isReadable()){
//READ事件 服务器转发的消息
SocketChannel channel = (SocketChannel) key.channel();
String msg = receive(client);
if(msg.isEmpty()){
selector.close();
}else{
System.out.println(msg);
}
}
}
private String receive(SocketChannel client) throws IOException {
rBuffer.clear();
while(client.read(rBuffer)>0){
}
rBuffer.flip();
return String.valueOf(charset.decode(rBuffer));
}
public void send(String msg) throws IOException {
if(msg.isEmpty()){
return;
}
wBuffer.clear();
wBuffer.put(charset.encode(msg));
wBuffer.flip();
while (wBuffer.hasRemaining()){
client.write(wBuffer);
}
}
}
3.3 客户端的写线程NIOUserInputHandler
public class NIOUserInputHandler implements Runnable{//处理用户的输入
private ChatClient chatClient;
public NIOUserInputHandler(ChatClient chatClient) {
this.chatClient = chatClient;
}
public void run() {
try {
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
while(true){
String input = consoleReader.readLine();
chatClient.send(input);
if(chatClient.readyToQuit(input)){
break;
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}