大家好,我是
方圆
把这篇肝完就休息!
目录
1. NIO模型分析
- 在服务器端创建一个
Selector
,将ServerSocketChannel注册到Selector上
,被Selector监听的事件为Accept
- Client1请求与服务器建立连接,Selector接收到Accept事件,服务器端对其进行处理(handles),服务器与客户端连接成功
- 建立连接过程中,服务器通道(ServerSocketChannel)调用
accept方法
,获取到与客户端进行连接的通道(SocketChannel
),也将其注册到Selector
上,监听READ事件
,这样,客户端向服务器发送消息,就能触发该READ事件进行响应,读取该消息。
Tips: 我们处理这个建立连接并接收从客户端传过来的消息,都是在
一个线程
内完成的。在bio中,则会为单个客户端单独开辟一个线程,用于处理消息,并且客户端在不发送消息的过程中,该线程一直是阻塞的。
- 同样,两个客户连接过来也是一个线程在起作用,将客户端2的SocketChannel注册到服务器的Selector,并监听READ事件,随时响应随时处理。即一个客户端有一个SocketChannel,两个客户端就有两个SocketChannel,这个就是我们使用nio编程模型来用一个selector对象在一个线程里边监听以及处理多个通道的io的操作
各个Channel是被配置为非阻塞式
的(configureBlocking(false)),但是Selector本身调用的select()方法
,它是阻塞式
的,当监听在Selector上的事件都没有触发时,那么它就会被阻塞,直到有事件对其进行响应
2. 聊天室项目代码重点知识
2.1 服务器端
2.1.1 字段
2.1.2 主方法
扫描二维码关注公众号,回复:
11824646 查看本文章
2.1.3 处理方法
2.1.4 转发消息方法
2.1.5 接收消息方法
2.2 客户端
2.2.1 字段
2.2.2 主方法
2.2.3 处理方法
2.2.4 接收方法
2.2.5 发送方法
3. 测试结果
- 服务器端显示信息正确
4. 完整代码
4.1 服务器端
package server;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;
public class ChatServer {
private static final int DEFAULT_PORT = 8888;
private static final String QUIT = "quit";
private static final int BUFFER = 1024;
private int port;
private ServerSocketChannel serverSocketChannel;
private Selector selector;
private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);
private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);
private Charset charset = Charset.forName(String.valueOf(StandardCharsets.UTF_8));
public ChatServer(){
this(DEFAULT_PORT);
}
public ChatServer(int port) {
this.port = port;
}
public boolean readyToQuit(String msg){
return QUIT.equals(msg);
}
public void close(Closeable closeable){
if(closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void start(){
try {
//创建ServerSocketChannel通道
serverSocketChannel = ServerSocketChannel.open();
//设置非阻塞模式,默认情况也是阻塞调用的
serverSocketChannel.configureBlocking(false);
//绑定端口号
serverSocketChannel.bind(new InetSocketAddress(port));
//创建selector
selector = Selector.open();
//将accept事件注册到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功,监听端口号:" + port + "...");
//开始进入监听模式
while(true){
//阻塞式调用
selector.select();
//获取所有的监听事件,
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
//处理事件
handles(selectionKey);
}
//将已经处理完成的事件进行清空
selectionKeys.clear();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
close(selector);
}
}
/**
* 处理事件 处理accept事件 和 read事件
* @param selectionKey 与selector绑定的channel的key
*/
private void handles(SelectionKey selectionKey) throws IOException {
if(selectionKey.isAcceptable()){
//处理accept事件
//先要获取ServerSocketChannel
ServerSocketChannel server =(ServerSocketChannel) selectionKey.channel();
// 我觉得这样写也行 直接调用全局变量 SocketChannel socketChannel = serverSocketChannel.accept();
//获取对应的客户端的通道
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
//将客户端通道绑定到selector上,监听read事件
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("客户端" + socketChannel.socket().getPort() + ":已经连接");
}else if(selectionKey.isReadable()){
//处理read事件
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
String fwdMsg = receive(clientChannel);
if(fwdMsg.isEmpty()){
//接不到消息了,那么就把这个通道给他移除了
selectionKey.cancel();
//通知selector有注册的通道被移除了,更新状态
selector.wakeup();
}else {
//转发消息
forwardMessage(clientChannel,fwdMsg);
if(readyToQuit(fwdMsg)){
selectionKey.cancel();
selector.wakeup();
}
}
}
}
/**
* 转发消息
* @param clientChannel 客户端通道
* @param fwdMsg 转发的消息
*/
private void forwardMessage(SocketChannel clientChannel, String fwdMsg) throws IOException {
//keys方法区别于selectedKeys,这个方法返回的是接下来需要被处理的通道key
//而keys则返回与selector绑定的所有通道key
//跳过ServerSocketChannel和本身
for (SelectionKey selectionKey : selector.keys()) {
SelectableChannel channel = selectionKey.channel();
if(channel instanceof ServerSocketChannel)
System.out.println("客户端" + clientChannel.socket().getPort() + ":" + fwdMsg);
else if(selectionKey.isValid() && !channel.equals(clientChannel)){
wBuffer.clear();
//写入消息
wBuffer.put(charset.encode("客户端" + clientChannel.socket().getPort() + ":" + fwdMsg));
//转换为读模式
wBuffer.flip();
//有数据就一直读
while(wBuffer.hasRemaining())
((SocketChannel)channel).write(wBuffer);
}
}
}
/**
* 从客户通道上读取消息
* @param clientChannel 客户通道
* @return 消息
*/
private String receive(SocketChannel clientChannel) throws IOException {
//将当前指针置于初始位置,覆盖已有的消息(清空消息)
rBuffer.clear();
//不停的向缓存中写
while(clientChannel.read(rBuffer) > 0);
//由写模式到读模式
rBuffer.flip();
return String.valueOf(charset.decode(rBuffer));
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
chatServer.start();
}
}
4.2 客户端
package client;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;
public class ChatClient {
private static final String DEFAULT_SERVER_HOST = "127.0.0.1";
private static final int DEFAULT_SERVER_PORT = 8888;
private static final String QUIT = "quit";
private static final int BUFFER = 1024;
private String host;
private int port;
private SocketChannel clientChannel;
private Selector selector;
private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);
private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);
private Charset charset = Charset.forName(String.valueOf(StandardCharsets.UTF_8));
public ChatClient(String host, int port) {
this.host = host;
this.port = port;
}
public ChatClient() {
this(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
}
public boolean readyToQuit(String msg){
return QUIT.equals(msg);
}
public void close(Closeable closeable){
if(closeable != null){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void start(){
try {
//创建用户通道
clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);//这一步千万不能忘了
//创建selector,并且将用户通道的connect请求注册上去
selector = Selector.open();
clientChannel.register(selector, SelectionKey.OP_CONNECT);
//尝试与服务器创建连接
clientChannel.connect(new InetSocketAddress(host,port));
while (selector.isOpen()){
//一直监听selector上注册的channel请求
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
//响应请求
handles(selectionKey);
}
selectionKeys.clear();
}
} catch (IOException e) {
e.printStackTrace();
}catch (ClosedSelectorException e){
} finally {
close(selector);
}
}
private void handles(SelectionKey selectionKey) throws IOException {
//处理connect事件
if(selectionKey.isConnectable()){
//如果能够与服务器响应了
SocketChannel channel = (SocketChannel) selectionKey.channel();
if(channel.isConnectionPending()){
channel.finishConnect(); //正式建立连接
new Thread(new UserInputHandler(this)).start();
}
channel.register(selector,SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){
String msg = receive(clientChannel);
SocketChannel channel = (SocketChannel) selectionKey.channel();
if(msg.isEmpty()){
//服务端异常
close(selector);
}else {
//TODO 看看这里信息对不对
System.out.println(msg);
}
}
}
private String receive(SocketChannel clientChannel) throws IOException {
rBuffer.clear();
//一直读数据
while (clientChannel.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()){
clientChannel.write(wBuffer);
}
if(QUIT.equals(msg))
close(selector);
}
public static void main(String[] args) {
ChatClient chatClient = new ChatClient();
chatClient.start();
}
}
4.3 客户端监听用户输入进程
package client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class UserInputHandler implements Runnable{
private ChatClient chatClient;
public UserInputHandler(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public void run() {
//等待用户输入信息
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
while(true){
try {
String msg = consoleReader.readLine();
chatClient.send(msg);
if(chatClient.readyToQuit(msg)) break;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
爽到!