概述
对ServerSocketChannel、SocketChannel、SelectionKey有一定的理解和了解对应API。
NIO非阻塞网络编程相关关系梳理:
以下概念:
ServerSocketChannel:可以类比BIO网络编程的ServerSocket,属于服务端的Socket,用于监听接收客户端连接。
SocketChannel:类比Socket,用于连接两台计算机的套接字,只是支持非阻塞。
Selector:多路复用器。
SelectionKey:与Channel进行绑定,Selector监听事件发生会返回SelectionKey,然后根据相应的SelectionKey获得对应的Channel进行操作。
流程:
- 服务端创建ServerSocketChanel,用于监听客户端连接,此时ServerSocketChannel也要注册到Selector中。
- Selector进行监听各种事件发生。
- 当有客户端连接进来时,发产生ServerSocketChannel的ACCEPT事件,然后可以获取连接进来的SelectionKey。
- 根据SelectionKey获取对应SocketChannel,然后注册到Selector,可以注册成读事件或者写事件,如果读写完成,则触发该事件。
- 完成业务处理。
群聊系统
要求:
- 编写一个NIO群聊系统,实现服务器端与客户端之间的简单数据通讯。
- 实现多人群聊。
- 服务器端可以检测用户上线、下线、并实现消息转发功能。
- 客户端通过channel可以无阻塞发送消息给其他所有在线用户,同时接受其他用户发送的消息。
目的:通过一个小demo熟悉相关NIO非阻塞网络编程API和机制的应用。
//服务端
public class GroupChatServer {
private int port;
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public GroupChatServer(int port){
this.port = port;
}
public void start() throws IOException {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(port));
//设置非阻塞
serverSocketChannel.configureBlocking(false);
selector = Selector.open();
//把serverSocketChannel自身注册到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("启动成功");
try {
while (true){
selector.select();
//获得发生了事件的SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//遍历处理事件
SelectionKey next = iterator.next();
if (next.isAcceptable()){
//处理发生了OP_ACCEPT事件的SelectionKey
handleAccept(next);
}else if (next.isReadable()){
//处理发生了OP_READ事件的SelectionKey
handleRead(next);
}
else if (next.isWritable()){
System.out.println("转发给一个用户完成");
}
//迭代完要删除,不然下次触发事件获取的SelectionKey会有他在里面
iterator.remove();
}
}
}catch ( Exception e){
e.printStackTrace();
}
}
private void handleRead(SelectionKey next) throws IOException {
//获取触发事件的SocketChannel
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取事件,这里不会阻塞,因为触发了读取完成事件才进入这里,数据必定是准备好的
try {
//不循环读完了 ,直接就假设信息小于1024,一个buffer一次能读完。
int read = channel.read(byteBuffer);
if (read>0){
//转发到除了自身以外的其他客户端,以实现群聊功能
forwardOthersClient(next, byteBuffer.array());
}
}catch (Exception e){
System.out.println("用户:" + next.attachment() + "离线了。");
next.cancel();
try {
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
private void forwardOthersClient(SelectionKey next,byte[] msg) throws IOException {
//获取所有注册到selector的SelectionKey
Set<SelectionKey> keys = selector.keys();
//遍历
for (SelectionKey selectionKey:keys){
if (selectionKey == next || selectionKey.channel() == serverSocketChannel){
//排除当前客户端和ServerSocketchannel不转发,
continue;
}
SocketChannel channel = (SocketChannel) selectionKey.channel();
channel.configureBlocking(false);
try {
channel.write(ByteBuffer.wrap(msg));
}catch (Exception e){
System.out.println("用户:" + next.attachment() + "离线了。");
next.cancel();
try {
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
private void handleAccept(SelectionKey next) throws IOException {
//因为ACCEPT事件只能是serverSocketChannel,所以这里就直接用ServerSocketchannel了
//监听客户端进来,因为已经发生了ACCEPT事件,必定有了客户端进来,所以accept方法不会阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
//将该channel注册到selector,事件为读事件,因为连接后,要读取客户端传来的数据,如果读取完成,就会触发该事件,而不用阻塞
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("客户端 : " + socketChannel.getRemoteAddress() + "连接服务器");
}
//启动服务端
public static void main(String[] args) throws IOException {
GroupChatServer groupChatServer = new GroupChatServer(5555);
groupChatServer.start();
}
}
客户端:
public class GroupChatClient {
private Selector selector;
private String host = "127.0.0.1";
private int port;
private SocketChannel socketChannel;
private String clientName;
public GroupChatClient(int port,String clientName){
this.port = port;
this.clientName = clientName;
}
public void start() throws IOException {
socketChannel = SocketChannel.open(new InetSocketAddress(host,port));
socketChannel.configureBlocking(false);
//其实这里用不用selector都可以,因为就只有一个SocketChannel
selector = selector.open();
socketChannel.register(selector, SelectionKey.OP_READ,clientName);
try {
while (true){
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey next = iterator.next();
if (next.isReadable()){
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = channel.read(byteBuffer);
if (read>0)
System.out.println(new String(byteBuffer.array()));
}
if (next.isWritable()){
System.out.println("消息发送完成");
}
//迭代完要删除,不然下次触发事件获取的SelectionKey会有他在里面
iterator.remove();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
public void sendMsg(String msg){
if (!StringUtils.isEmpty(msg)) {
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
GroupChatClient groupChatClient = new GroupChatClient(5555,"salar");
try {
//发送消息要新开线程,并且在start方法前面使用,然后睡眠一定时间确保start方法执行完成,也可以用countdownlatch来确保。
//要新开线程发消息的原因是到时主线程会在start方法的while死循环里面不出来,执行不到后面
new Thread(()->{
while (true){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
groupChatClient.sendMsg("Hi, I am salar");
}
}).start();
groupChatClient.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
先启动服务端再启动客户端,启动三个客户端,每个发送的消息都不一样,以做区别。
结果:
服务端:
客户端:能够收到其他客户端发送的消息。