一:原理图
二:要求
三:服务端编程
package com.atguigu.nio.qqchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.text.SimpleDateFormat;
import java.util.Iterator;
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel listenerChannel;
private static final int PORT = 6667; //服务器端口
public GroupChatServer() {
try {
// 得到选择器
selector = Selector.open();
// 打开监听通道
listenerChannel = ServerSocketChannel.open();
// 绑定端口
listenerChannel.socket().bind(new InetSocketAddress(PORT));
// 设置为非阻塞模式
listenerChannel.configureBlocking(false);
// 将选择器绑定到监听通道并监听 accept 事件
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
printInfo("服务器 ok.......");
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() {
try {
while (true) { //不停轮询
int count = selector.select();//获取就绪 channel
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 监听到 accept
if (key.isAcceptable()) {
SocketChannel sc = listenerChannel.accept();
//非阻塞模式
sc.configureBlocking(false);
//注册到选择器上并监听 read
sc.register(selector, SelectionKey.OP_READ);
//System.out.println(sc.getRemoteAddress().toString().substring(1) + "online ...");
System.out.println(sc.socket().getRemoteSocketAddress().toString().substring(1) + " 上线 ...");
//将此对应的 channel 设置为 accept,接着准备接受其他客户端请求
key.interestOps(SelectionKey.OP_ACCEPT);
}
//监听到 read
if (key.isReadable()) {
readData(key); //读取客户端发来的数据
}
//一定要把当前 key 删掉,防止重复处理
iterator.remove();
}
} else {
System.out.println("waitting ...");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void readData(SelectionKey key) {
SocketChannel channel = null;
try {
// 得到关联的通道
channel = (SocketChannel) key.channel();
//设置 buffer 缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//从通道中读取数据并存储到缓冲区中
int count = channel.read(buffer);
//如果读取到了数据
if (count > 0) {
//把缓冲区数据转换为字符串
String msg = new String(buffer.array());
printInfo(msg);
//将关联的 channel 设置为 read,继续准备接受数据
key.interestOps(SelectionKey.OP_READ);
sendInfoToOtherClients(channel, msg); //向所有客户端广播数据
}
buffer.clear();
} catch (IOException e) {
try {
//当客户端关闭 channel 时,进行异常如理
//printInfo(channel.getRemoteAddress().toString().substring(1) + "offline...");
printInfo(channel.socket().getRemoteSocketAddress().toString().substring(1) + " 离线了 ...");
key.cancel(); //取消注册
channel.close(); //关闭通道
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
public void sendInfoToOtherClients(SocketChannel except, String msg) throws IOException {
System.out.println("服务器进行消息转发 ...");
//转发数据到所有的 SocketChannel 中
for (SelectionKey key : selector.keys()) {
Channel targetchannel = key.channel();
//排除自身
if (targetchannel instanceof SocketChannel && targetchannel != except) {
SocketChannel dest = (SocketChannel) targetchannel;
//把数据存储到缓冲区中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//往通道中写数据
dest.write(buffer);
}
}
}
private void printInfo(String str) { //显示消息
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("服务器接收到消息 时间: [" + sdf.format(new java.util.Date()) + "] -> " + str);
}
public static void main(String[] args) {
GroupChatServer server = new GroupChatServer();
server.listen();
}
}
四:客户端编程
package com.atguigu.nio.qqchat;
import com.atguigu.netty.qq.ChatClient;
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.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class GroupChatClient {
private final String HOST = "127.0.0.1"; //服务器地址
private int PORT = 6667; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String userName;
public GroupChatClient() throws IOException {
//得到选择器
selector = Selector.open();
//连接远程服务器
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//注册选择器并设置为 read
socketChannel.register(selector, SelectionKey.OP_READ);
//得到客户端 IP 地址和端口信息,作为聊天用户名使用
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName + " is ok ~");
}
//向服务器端发送数据
public void sendInfo(String info) throws Exception {
//如果控制台输入 exit 就关闭通道,结束聊天
if (info.equalsIgnoreCase("exit")) {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
socketChannel.close();
socketChannel = null;
return;
}
info = userName + " 说: " + info;
try {
//往通道中写数据
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
//从服务器端接收数据
public void readInfo() {
try {
int readyChannels = selector.select();
if (readyChannels > 0) { //有可用通道
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey sk = (SelectionKey) keyIterator.next();
if (sk.isReadable()) {
//得到关联的通道
SocketChannel sc = (SocketChannel) sk.channel();
//得到一个缓冲区
ByteBuffer buff = ByteBuffer.allocate(1024);
//读取数据并存储到缓冲区
sc.read(buff);
//把缓冲区数据转换成字符串
String msg = new String(buff.array());
System.out.println(msg.trim());
}
keyIterator.remove(); //删除当前 SelectionKey,防止重复处理
}
} else {
//会检测到没有可用的channel ,可以退出
System.out.println("没有可用channel ...");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
//创建一个聊天客户端对象
GroupChatClient chatClient = new GroupChatClient();
new Thread() { //单独开一个线程不断的接收服务器端广播的数据
public void run() {
while (true) {
chatClient.readInfo();
try { //间隔 3 秒
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Scanner scanner = new Scanner(System.in);
//在控制台输入数据并发送到服务器端
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClient.sendInfo(msg.trim());
}
}
}