Java基础简易聊天室

package edu.cheat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class ChatServer {
    private static final int PORT = 8000;//设置服务器的端口号

    private Selector selector;
    //相当于监控器,检测注册在上面的链接是否有数据传过来
    private ServerSocketChannel serverChannel;
    //这是连接客户端与服务端的管道,当客户端连接时,就会新建一个这样的管道,并将其注册在selector上,这样selector就可以监控管道里是否会有东西进来了
    private CharsetDecoder decoder;
//这个东西是可以将管道里的东西翻译出来的工具,它就相当于翻译官,将客户端传过来的字节数据转化为可以看懂的文字
    private Map<SocketChannel, String> memberChannels;
//这个东西储存着你的名字与连接(相当于“电话簿”)
    public ChatServer() throws IOException {
        selector = Selector.open();//打开注册器
        serverChannel = ServerSocketChannel.open();//打开连接通道
        serverChannel.socket().bind(new InetSocketAddress(PORT));//绑定服务器所开的端口号
        serverChannel.configureBlocking(false);//设置为非阻塞模式
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        //将改通道注册到selector注册器上。SelectionKey.OP_ACCEPT这个的意思是监听连接事件

        decoder = StandardCharsets.UTF_8.newDecoder();
//这个是规定的解码时要以什么样的格式去解码
        memberChannels = new HashMap<>();
    }

    public void run() throws IOException {
        System.out.println("聊天室服务器已启动,监听端口:" + PORT);

        while (true) {
            selector.select();
            //selector.select() 方法是 NIO 中 Selector 类的方法,它的作用是阻塞并等待直到有一个或多个通道在注册的事件上就绪。
            //
            Set<SelectionKey> keys = selector.selectedKeys();//在调用 selector.select() 方法后,通过 selector.selectedKeys() 可以获取到所有已经就绪的通道对应的 SelectionKey 对象。
//也就是说selector.select() 方法可以发现有新就绪的事件,而selectedKeys()会把所有的在selector上的所有准备就绪的事件都存储在这个集合里,selectedKeys里存的是准备就绪的连接
            for (Iterator<SelectionKey> iterator = keys.iterator(); iterator.hasNext();) {//这里是要将selectedKeys里的所有准备就绪的链接取出来
                //iterator.hasNext()是判断集合里是否存在下一个对象
                // Iterator<SelectionKey> iterator = keys.iterator()这个的作用是获得一个集合的迭代器
                SelectionKey key = iterator.next();//iterator.next()的作用是从keys集合里取出来一个对象
                iterator.remove();//这里是已经将当前的对象拿出来了,那么此时迭代器所指向的集合里的对象就可以删除了,
                // 因为在逻辑上这个被取出来的事件对象已经被处理了,而被处理过的对象就不能继续待在SelectionKeys集合里了

                if (key.isAcceptable()) {//判断当前事件是请求接收事件还是请求读取事件
                    // key.isAcceptable()这个方法用来判断此事件是否是请求接收事件
                    // key.isReadable()这个方法用来判断此事件是否是读取事件
                    handleAccept(key);
                } else if (key.isReadable()) {
                    handleRead(key);
                }
            }
        }
    }

    private void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel client = server.accept();//在这两句代码中,(ServerSocketChannel) 强转的作用是将
        // key.channel() 返回的通道对象强制转换为 ServerSocketChannel 类型的对象,
        // 以便调用 accept() 方法来接受客户端的连接。
        client.configureBlocking(false);//将这个新建的client连接也设置为非阻塞模式

        // 生成一个随机的用户名
        String username = "User" + UUID.randomUUID().toString().substring(0, 8);
        memberChannels.put(client, username);//将这个新连接赐予名字,并将其放进“电话簿”里

        client.register(selector, SelectionKey.OP_READ);
        //将新连接也放到注册器上,通过注册 SelectionKey.OP_READ,服务器就能够监听客户端的请求读取事件,
        // 这样一来,服务器就可以通过 Selector 监听并处理来自客户端的数据

        System.out.println("新客户端连接:" + client.socket().getRemoteSocketAddress());
        broadcastMessage(username + " 加入了聊天室");
    }

    private void handleRead(SelectionKey key) throws IOException {//这是处理请求读取事件的方法
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //缓冲区,相当于一个小货车,客户端从管道连接中传过来的数据就相当于货物,这个1024就规定着这个货车的大小,
        // 数字越大,货车容量越大
        StringBuilder message = new StringBuilder();//StringBuilder这是一个类,它的对象可以不断的存储字符信息

        int bytesRead = client.read(buffer);//client.read(buffer)里的client是客户端与服务端之间的管道,
        // read是读取方法(相当于工人,向buffer这个货车上装货物),而bytesRead就是指这次货物装了多少
        while (bytesRead > 0) {//这里进入循环,因为在逻辑上,第一次的buffer(货车)里肯定是会有东西的,
            // 所以bytesRead肯定会大于0
            buffer.flip();//这一行代码将 buffer 切换为读模式,以便后续的解码操作可以从缓冲区的开头开始读取数据,
            // (读取毕竟要从开头向末尾读,如果直接从末尾开始不就什么都读不到了嘛),因为这是在同一个循环里,
            // buffer的默认模式不会将其内部读到末尾的指针给重新归位的
            CharBuffer charBuffer = decoder.decode(buffer);
            //这行代码使用解码器 decoder 对 buffer 中的数据进行解码,将其转换为字符形式的数据,存储到 charBuffer 中,
            // 这就是翻译官在工作了
            message.append(charBuffer);
            //message就是StringBuilder类的对象,它存储信息。append方法就是往里写信息的
            buffer.clear();
            //清空货车
            bytesRead = client.read(buffer);
            //再读剩余未读的数据,如果没有数据(货物)了,那么此时bytesRead就等于-1.循环也会结束
        }

        String username = memberChannels.get(client);//通过“电话簿”找到相应连接的名字
        String msg = message.toString().trim(); // 去除消息首尾空格
        if (msg.equals("quit")) {// 如果消息为 "quit"
            memberChannels.remove(client); // 从 Map ““电话簿””中移除客户端信息
            key.cancel(); // 取消该 SelectionKey(就是关闭当前事件)
            client.close(); // 关闭客户端连接
            broadcastMessage(username + " 退出了聊天室");
        } else {
            broadcastMessage(username + ": " + msg);
        }
    }

    private void broadcastMessage(String message) throws IOException {
        ByteBuffer buffer = StandardCharsets.UTF_8.encode(message);
        //这里将消息用utf-8的格式编码为字节信息,那么当另一端接收到时就用CharsetDecoder decoder  这个翻译官来翻译

        for (SocketChannel client : memberChannels.keySet()) {//遍历每一个客户端
            client.write(buffer); // 向每个客户端发送消息
            buffer.rewind(); // 重置缓冲区的位置,准备下一次写入(上面也讲到过,这里是重置内部指针的位置了)
        }
    }

    public static void main(String[] args) throws IOException {//启动服务器
        ChatServer server = new ChatServer();
        server.run();
    }
}

这是服务端

package edu.cheat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class ChatClient {
    private static final String SERVER_HOST = "127.0.0.1";  //服务器地址(127.0.0.1就是本地地址)
    private static final int SERVER_PORT = 8000;  // 端口

    private SocketChannel socketChannel;//创建一个通道,一会连接客户端要用
    private CharsetDecoder decoder;//翻译官😂

    public ChatClient() throws IOException {
        socketChannel = SocketChannel.open(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
        //用固定的方法绑定端口    SERVER_HOST SERVER_PORT是已经定义过的
        decoder = StandardCharsets.UTF_8.newDecoder();//指定解码格式(指定翻译官的语言😂)
    }

    public void run() {
        try {
            Thread messageReader = new Thread(new MessageReader());//新建一条线程
            messageReader.start();//开启此线程

            Scanner scanner = new Scanner(System.in);//新建扫描器
            while (true) {//进入循环,会一直等待用户的输入
                String message = scanner.nextLine();//接收键盘输入
                ByteBuffer buffer = StandardCharsets.UTF_8.encode(message);//将输入的信息用utf-8编码为字节信息(方便传输)
                socketChannel.write(buffer);//往通道里写入(这里相当于向服务器发了一个请求读写事件)
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private class MessageReader implements Runnable {
        //这个类实现了Runnable接口(这个接口是专门设置或者说是调控线程的),
        // 那么这个类就可以去参与进对某一线程的创建里了
        @Override
        public void run() {//Runnable接口里唯一的一个方法,当线程启动时会先执行此方法
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            try {
                while (true) {//进入循环
                    socketChannel.read(buffer);
                    //读取通道中的信息,因为没有设置非阻塞队列,所以阻塞队列会一直等待信息的输入,
                    // 也就是客户端会一直监听着服务器是否有发送消息过来
                    buffer.flip();
                    CharBuffer charBuffer = decoder.decode(buffer);
                    System.out.println(charBuffer.toString());
                    buffer.clear();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        ChatClient client = new ChatClient();//新建客户端对象
        client.run();//启动run方法(启动客户端)
    }
}


这是客户端

此外:

非阻塞模式(Non-blocking mode)是一种I/O模式,与传统的阻塞式I/O(Blocking I/O)相对应。

在阻塞式I/O中,当我们从通道中读取数据时,如果没有数据可读,那么程序将会一直阻塞,直到有数据可读为止。同样地,当我们向通道中写入数据时,如果通道已满,那么程序也会一直阻塞,直到有空余的空间为止。

而在非阻塞模式下,程序不会一直等待阻塞的I/O操作完成,而是立即返回并继续执行其他任务。当I/O操作完成时,程序会通过回调或轮询等方式获取结果。

使用非阻塞模式可以有效提高系统的并发性和响应速度,因为它允许程序同时处理多个I/O操作,而不必等待每个操作完成后再进行下一个操作。

在Java NIO中,可以将通道(Channel)设置为非阻塞模式,通过调用configureBlocking(false)方法来实现。在非阻塞模式下,我们可以使用Selector等机制来监听通道上的事件,并根据事件类型进行相应的操作。

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值