NIO聊天室设计思路及实现
NIO聊天室设计思路及实现
各个客户端通过连接服务器端,通过向缓冲区写如数据,然后利用通道向服务器端发送消息,服务器端通过通道获取缓冲区的内的数据,通过广播的形式广播给所有的用户。客户端可以多开,也可以多次发送,以bye字符为结束标志。服务器端循环获取,循环广播。
服务器端
属性:
//ServerSocketChannel (服务器监听通道);Selector(选择器,帮助通道去监听通道事件);port(限定)
private ServerSocketChannel listenerChannel; //监听通道
private Selector selector;//选择器对象
private static final int PORT = 9999; //服务器端口
构造方法:
public ChatServer() {
try {
// 1. 得到监听通道 老大
listenerChannel = ServerSocketChannel.open();
// 2. 得到选择器 间谍
selector = Selector.open();
// 3. 绑定端口
listenerChannel.bind(new InetSocketAddress(PORT));
// 4. 设置为非阻塞模式
listenerChannel.configureBlocking(false);
// 5. 将选择器绑定到监听通道并监听accept事件
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
printInfo("Chat Server is ready.......");
} catch (IOException e) {
e.printStackTrace();
}
}
注意:为了代码的可读性将基本配置单独抽取出来
执行方法:
public void start() throws Exception {
try {
while (true) { //不停监控
//利用选择器循环监控客户端连接服务器端的情况(非阻塞的优势,在此体现,可以在不停监控的过程中做一些其他的事情);
if (selector.select(2000) == 0) {
//System.out.println("Server:服务准备就绪");
continue;
}
//如果有客户端成功连接,则利用选择器调用selectedKeys()方法,来返回已就绪通道的集合;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//利用hasNext()判断,循环通过迭代器的方式,来获取每一个通道;
SelectionKey key = iterator.next();
//判断是否是连接请求事件;
if (key.isAcceptable()) { //连接请求事件
SocketChannel sc = listenerChannel.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
//得到客户端地址和端口号
System.out.println(sc.getRemoteAddress().toString().substring(1) + "上线了...");
}
//判断是否是读取数据事件;
if (key.isReadable()) { //读取数据事件
readMsg(key);
}
//一定要把当前key删掉,防止重复处理
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
读取消息方法:
private void readMsg(SelectionKey key) throws Exception {
//根据Key值获取每个通道
SocketChannel channel = (SocketChannel) key.channel();
//获取通道内的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//判断标志
int read = channel.read(byteBuffer);
if(read>0){
//获取消息
String msg = new String(byteBuffer.array());
//打印消息
printInfo(msg);//为了控制输出,抽取了一个打印方法
//发广播
broadCast(channel,msg);//同样抽取了方法
}
}
广播方法:
public void broadCast(SocketChannel except, String msg) throws Exception {
//提示
System.out.println("服务器发送了广播...");
注意:
selector调用keys()和selectedKeys(),一个是选择器选中的键集,另一个是选择器中的键集
for (SelectionKey key : selector.keys()) {
//这里通过key获得通道,但是这个通道不一定是SocketChannel
Channel targetChannel = key.channel();
//进行判断
if (targetChannel instanceof SocketChannel && targetChannel != except) {
//赋值给SocketChannel
SocketChannel destChannel = (SocketChannel) targetChannel;
//写到缓存中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//写到通道中
destChannel.write(buffer);
}
}
}
打印方法:
private void printInfo(String str) { //往控制台打印消息
//格式化实践
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//输出拼接的字符串
System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
}
服务器端启动程序:
public static void main(String[] args) throws Exception {
new ChatServer().start();
}
客户端:
客户端首先需要连接服务器,第一次失败,第二次需要换方法进行连接,然后向服务器写数据,同时向数据库读数据,为了方便,我们需要单独开个线程,只负责读数据
属性:
private final String HOST = "127.0.0.1"; //服务器地址,确保所有客户端在一个服务器上
private int PORT = 9999; //服务器端口,确保使用同一个服务器上同一个应用服务
private SocketChannel socketChannel; //网络通道
private String userName; //聊天用户名,为了美化
构造方法:
public ChatClient() throws IOException {
//1. 得到一个网络通道
socketChannel = SocketChannel.open();
//2. 设置非阻塞方式
socketChannel.configureBlocking(false);
//3. 提供服务器端的IP地址和端口号
InetSocketAddress address = new InetSocketAddress(HOST, PORT);
//4. 连接服务器端
if (!socketChannel.connect(address)) {
while (!socketChannel.finishConnect()) { //nio作为非阻塞式的优势
。。。。。。。。。可以干一些其他事情
}
}
//5. 得到客户端IP地址和端口信息,作为聊天用户名使用
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("---------------Client(" + userName + ") is ready---------------");
}
发送数据方法:
public void sendMsg(String msg) throws Exception {
//检测到发送的字符串中有‘bye’,则关闭通道
if (msg.equalsIgnoreCase("bye")) {
//关闭通道
socketChannel.close();
return;
}
msg = userName + "说:" + msg;
//写道缓存中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//通道将缓存写入通道中
socketChannel.write(buffer);
}
接受数据方法:
public void receiveMsg() throws Exception {
//建立缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将通道内的数据写入到缓冲区
int size = socketChannel.read(buffer);
if (size > 0) {
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
客户端启动程序:
public class TestChat {
public static void main(String[] args) throws Exception {
ChatClient chatClient=new ChatClient();
//开器多线程进行读取数据
new Thread(){
public void run(){
while(true){
try {
chatClient.receiveMsg();
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}.start();
Scanner scanner=new Scanner(System.in);
//只要有输入,就一直发送
while (scanner.hasNextLine()){
String msg=scanner.nextLine();
chatClient.sendMsg(msg);
}
}
}
注意:这里一定要搞清楚几个关系
1、ServerSocketChannel, 用来在服务器端监听新的客户端 Socket 连接 ;(主要是selector在为他服务,在监听),常在服务器端使用
2、SocketChannel, 网络 IO 通道, 具体负责进行读写操作 ;常在客户端使用
public int write(ByteBuffer src), 往通道里写数据
public int read(ByteBuffer dst), 从通道里读数据
3、通道和缓冲区的关系:通道发送数据,缓冲区是存储数据,这里常用的是ByteBuffer;
数据总是从通道读取到缓冲区中,将缓冲区中的内容写到到通道中
allocate(int):创建指定的缓冲区容量
wrap(byteArray):创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用你所提供的数组作为存储空间来储存缓冲区中的数据元素;例如:char [] myArray = new char [100]; CharBuffer charbuffer = CharBuffer.wrap (myArray);

浙公网安备 33010602011771号