在掌握了基本NIO的种种操作之后,最后通过一个较大的实例来结束NIO的学习——一个简易的网络多客户端聊天室的实现。
在开始动手之前,我们需要简单普及一个基础知识,也正是上文提到的NIO三大核心组件:channel、buffer、selector中的最后一个,selector。在之前几篇博客的几个例子之中,NIO的表现其实并没有形成对传统IO的一种颠覆。无论是文件复制也好,读取也好,甚至是大文件的修改操作,整体都没有摆脱原来传统IO整体的大模型。只不过是在原有的基础上,从面向流改为了面向通道。而selector才是NIO的精髓所在,大家先通过一张图对selector的作用作简单了解:
传统IO:每一个服务端都需要创建一条线程给客户端连接。(阻塞式)
NIO:基于通道,可以为服务端注册一个selector,通过这个selector来监听接下来即将接入的一个个客户端,再反馈给serverSocketChannel。(非阻塞式)
非阻塞形式的网络模型,基于channel和buffer。打个比方,每个socket都是一根自来水管,每个自来水管下面我们都放了个桶,也就是buffer。selector作为监视者,在这里往复巡视。一旦有一个桶满了,就会告知selector,把这个桶上交给serverSocketChannel。也就是说此时,这个serverSocketChannel并不直接去管理每一个客户端,这样一来就形成了全新的网络模型。
意义在于什么呢,一条线程我就可以对付很多客户端的连接,对于线程上的开销就可以减少非常多。如果一个客户端来我就给一条线程,那么对于大型服务器,拥有成千上万的客户端连接,势必会将这个服务器拖垮。所以这种非阻塞式的多路复用模型,才能成为主流。
了解了这些之后,下面我们就利用NIO来实践网络编程——实现一个简易的网络聊天室。编写代码的过程中有不少坑,在这里不一个一个提及,下面展示全文代码,希望大家能够细细阅读并加以理解。
服务端实现:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* 网络多客户端聊天室<br/>
* 功能1:客户端通过Java NIO连接到服务端,支持多客户端的连接。<br/>
* 功能2:客户端初次连接时,服务端提示输入昵称,之后发送消息都需要按照规定格式带着昵称发送消息。<br/>
* 功能3:客户端登录后,发送已经设置好的欢迎信息和在线人数给客户端,并且通知其他客户端该客户端上线。<br/>
* 功能4:服务器收到已登录客户端输入内容,转发至其他登录客户端。
* @author 青葉
*
*/
public class ChatRoomServer {
ServerSocketChannel serverSocketChannel;
Selector selector;
Charset charset = Charset.forName("UTF-8");
int port = 9999;
//初始化数据
public void init() {
try {
serverSocketChannel = ServerSocketChannel.open();
selector = Selector.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(port));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
watching();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
serverSocketChannel.close();
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//向其他客户端发送消息
public void broadcast(String content, String userName) throws IOException {
//获取当前所有的selectionKey。注意:selector.selectionKeys()方法返回的不是所有的key
Set<SelectionKey> keys = selector.keys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
Channel channel = selectionKey.channel();
if(channel instanceof SocketChannel) {
SocketChannel socketChannel = (SocketChannel) channel;
socketChannel.write(charset.encode(userName + "对大家说" + content));
}
}
}
public void watching() throws IOException {
System.out.println("服务器启动成功...");
while(true) {
int readyChannels = selector.select(); //等待所注册的事件发生
if(0 == readyChannels) {
continue;
}
//处理事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()) {
//客户端接入事件
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
//写入欢迎信息
channel.write(charset.encode("欢迎来到聊天室,请输入姓名"));
// selectionKey.attach(new UserInfo());
} else if(selectionKey.isReadable()) {
//获取客户端channel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
UserInfo userInfo = (UserInfo) selectionKey.attachment();
//获取channel内容
ByteBuffer buffer = ByteBuffer.allocate(128);
StringBuffer stringBuffer = new StringBuffer();
int flag = socketChannel.read(buffer);
while(flag > 0) {
buffer.flip();
stringBuffer.append(charset.decode(buffer));
buffer.clear();
flag = socketChannel.read(buffer);
}
if(null != userInfo && userInfo.init) {
broadcast(stringBuffer.toString(), userInfo.getName());
} else {
//接收用户名
UserInfo info = new UserInfo();
info.setName(stringBuffer.toString());
info.setInit(true);
selectionKey.attach(info);
//输出提示信息
socketChannel.write(charset.encode("您好," + info.getName() + ",现在您可以和聊天室的小伙伴聊天了。"));
}
}
iterator.remove();
}
}
}
public static void main(String[] args) {
//创建serverSocketChannel
try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
serverSocketChannel.configureBlocking(false); //声明非阻塞IO
//声明一个选择器
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //在客户端接入的事件上注册选择器
ChatRoomServer chatRoomServer = new ChatRoomServer();
chatRoomServer.init();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class UserInfo {
String name;
boolean init = false;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isInit() {
return init;
}
public void setInit(boolean init) {
this.init = init;
}
}
客户端实现:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class ChatRoomClient {
SocketChannel channel;
Selector selector;
int port = 9999;
String ip = "192.168.0.10";
public void init() {
try {
channel = SocketChannel.open(new InetSocketAddress(ip, port));
channel.configureBlocking(false);
selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
//启动Selector线程
new MySelectorThread(selector).start();
//获取控制台输入
while(true) {
@SuppressWarnings("resource")
Scanner scanner = new Scanner(System.in);
String content = scanner.nextLine();
channel.write(Charset.forName("UTF-8").encode(content));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
channel.close();
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChatRoomClient chatRoomClient = new ChatRoomClient();
chatRoomClient.init();
}
}
class MySelectorThread extends Thread {
Selector selector;
public MySelectorThread(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
try {
//处理事件
while(true) {
int readyChannels = selector.select(); //等待所注册的事件发生
if(0 == readyChannels) {
continue;
}
//处理事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if(selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(128);
StringBuffer stringBuffer = new StringBuffer();
while(channel.read(buffer) > 0) {
buffer.flip();
stringBuffer .append(Charset.forName("UTF-8").decode(buffer));
buffer.clear();
}
System.out.println(stringBuffer.toString());
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
其实还有一些小功能,例如上线通知、在线人数统计之类的并没有去实现,但这些都是基于已经完成的这些功能衍生出来的,十分简单,大家可以自己动手试试。
整体上来说,这个多路复用的好处在于:我们只需要一条selector所在的线程,去监听多个channel,而不需要每一个客户端都去创建一个线程来维护它。这个是NIO的精髓所在,同时也是现在大多数网络服务器的架构模式。
整个NIO的学习就告一段落,希望大家能够通过阅读我的学习笔记,体会到一定的其中的编程思想。