Java 用NIO实现一个聊天室(多人聊天、单人聊天)

多人在线,多人聊天(可能有TCP粘包bug)

服务端:

package NonBlocking;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class ChatServer {
    private final int port = 8899;
    private final String seperator = "[|]";                        //消息分隔符
    private final Charset charset = Charset.forName("UTF-8");    //字符集
    private ByteBuffer buffer = ByteBuffer.allocate(1024);        //缓存
    private Map<String, SocketChannel> onlineUsers = new HashMap<String, SocketChannel>();//将用户对应的channel对应起来
    private Selector selector;
    private ServerSocketChannel server;

    public void startServer() throws IOException {
        //NIO server初始化固定流程:5步
        selector = Selector.open();                    //1.selector open
        server = ServerSocketChannel.open();        //2.ServerSocketChannel open
        server.bind(new InetSocketAddress(port));    //3.serverChannel绑定端口
        server.configureBlocking(false);            //4.设置NIO为非阻塞模式
        server.register(selector, SelectionKey.OP_ACCEPT);//5.将channel注册在选择器上

        //NIO server处理数据固定流程:5步
        SocketChannel client;
        SelectionKey key;
        Iterator<SelectionKey> iKeys;

        while (true) {
            selector.select();                            //1.用select()方法阻塞,一直到有可用连接加入
            iKeys = selector.selectedKeys().iterator();    //2.到了这步,说明有可用连接到底,取出所有可用连接
            while (iKeys.hasNext()) {
                key = iKeys.next();                        //3.遍历
                if (key.isAcceptable()) {                    //4.对每个连接感兴趣的事做不同的处理
                    //对于客户端连接,注册到服务端
                    client = server.accept();            //获取客户端首次连接
                    client.configureBlocking(false);
                    //不用注册写,只有当写入量大,或写需要争用时,才考虑注册写事件
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("+++++客户端:" + client.getRemoteAddress() + ",建立连接+++++");
                    client.write(charset.encode("请输入自定义用户名:"));
                }
                if (key.isReadable()) {
                    client = (SocketChannel) key.channel();//通过key取得客户端channel
                    StringBuilder msg = new StringBuilder();
                    buffer.clear();        //多次使用的缓存,用前要先清空
                    try {
                        System.out.println(buffer);
                        while (client.read(buffer) > 0) {
                            buffer.flip();    //将写模式转换为读模式
                            msg.append(charset.decode(buffer));
                            buffer.clear();
                        }
                    } catch (IOException e) {
                        //如果client.read(buffer)抛出异常,说明此客户端主动断开连接,需做下面处理
                        client.close();            //关闭channel
                        key.cancel();            //将channel对应的key置为不可用
                        onlineUsers.values().remove(client);    //将问题连接从map中删除
                        System.out.println("-----用户'" + key.attachment().toString() + "'退出连接,当前用户列表:" + onlineUsers.keySet().toString() + "-----");
                        continue;                //跳出循环
                    }
                    if (msg.length() > 0) this.processMsg(msg.toString(), client, key);    //处理消息体
                }
                iKeys.remove();                    //5.处理完一次事件后,要显式的移除
            }
        }
    }

    /**
     * 处理客户端传来的消息
     *
     * @param msg 格式:user_to|body|user_from
     * @throws IOException
     * @Key 这里主要用attach()方法,给通道定义一个表示符
     */
    private void processMsg(String msg, SocketChannel client, SelectionKey key) throws IOException {
        String[] ms = msg.split(seperator);
        if (ms.length == 1) {
            String user = ms[0];    //输入的是自定义用户名
            if (onlineUsers.containsKey(user)) {
                client.write(charset.encode("当前用户已存在,请重新输入用户名:"));
            } else {
                onlineUsers.put(user, client);
                key.attach(user);    //给通道定义一个表示符
                String welCome = "\t欢迎'" + user + "'上线,当前在线人数" + this.getOnLineNum() + "人。用户列表:" + onlineUsers.keySet().toString();
                client.write(charset.encode("您的昵称通过验证 "+user));
                this.broadCast(welCome);    //给所用用户推送上线信息,包括自己
            }
        } else if (ms.length == 2) {
            String msg_body = ms[0];
            String user_from = ms[1];
            broadCast("来自'" + user_from + "'的消息:" + msg_body);
        }
    }

    //map中的有效数量已被很好的控制,可以从map中获取,也可以用下面的方法取
    private int getOnLineNum() {
        int count = 0;
        Channel channel;
        for (SelectionKey k : selector.keys()) {
            channel = k.channel();
            if (channel instanceof SocketChannel) {    //排除ServerSocketChannel
                count++;
            }
        }
        return count;
    }

    //广播上线消息
    private void broadCast(String msg) throws IOException {
        Channel channel;
        for (SelectionKey k : selector.keys()) {
            channel = k.channel();
            if (channel instanceof SocketChannel) {
                SocketChannel client = (SocketChannel) channel;
                client.write(charset.encode(msg));
            }
        }
    }

    public static void main(String[] args) {
        try {
            new ChatServer().startServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端:

package NonBlocking;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Scanner;

public class ChatClient1 {

    private final int port = 8899;
    private final String seperator = "|";
    private final Charset charset = Charset.forName("UTF-8");	//字符集
    private ByteBuffer buffer = ByteBuffer.allocate(1024);
    private SocketChannel _self;
    private Selector selector;
    private String name = "";
    private boolean flag = true;	//服务端断开,客户端的读事件不会一直发生(与服务端不一样)

    Scanner scanner = new Scanner(System.in);
    public void startClient() throws IOException{
        //客户端初始化固定流程:4步
        selector = Selector.open();								//1.打开Selector
        _self = SocketChannel.open(new InetSocketAddress(port));//2.连接服务端,这里默认本机的IP
        _self.configureBlocking(false);							//3.配置此channel非阻塞
        _self.register(selector, SelectionKey.OP_READ);			//4.将channel的读事件注册到选择器

        /*
         * 因为等待用户输入会导致主线程阻塞
         * 所以用主线程处理输入,新开一个线程处理读数据
         */
        new Thread(new ClientReadThread()).start();	//开一个异步线程处理读
        String input = "";
        while(flag){
            input = scanner.nextLine();
            if("".equals(input)){  
                System.out.println("不允许输入空串!");
                continue;
            }else if("".equals(name)){  //姓名如果没有初始化
				//啥也不干,之后发给服务端验证姓名
            }else if(!"".equals(name)) {  //如果姓名已经初始化,那么说明现在的字符串就是想说的话
                input = input + seperator + name;
            }
            try{
                _self.write(charset.encode(input));
            }catch(Exception e){
                System.out.println(e.getMessage()+"客户端主线程退出连接!!");
            }
        }
    }

    private class ClientReadThread implements Runnable{
        @Override
        public void run(){
            Iterator<SelectionKey> ikeys;
            SelectionKey key;
            SocketChannel client;
            try {
                while(flag){
                    selector.select();	//调用此方法一直阻塞,直到有channel可用
                    ikeys = selector.selectedKeys().iterator();
                    while(ikeys.hasNext()){
                        key = ikeys.next();
                        if(key.isReadable()){	//处理读事件
                            client = (SocketChannel) key.channel();
                            //这里的输出是true,从selector的key中获取的客户端channel,是同一个
//							System.out.println("client == _self:"+ (client == _self));
                            buffer.clear();
                            StringBuilder msg = new StringBuilder();
                            try{
                                while(client.read(buffer) > 0){
                                    buffer.flip();	//将写模式转换为读模式
                                    msg.append(charset.decode(buffer));
                                }
                            }catch(IOException en){
                                System.out.println(en.getMessage()+",客户端'"+key.attachment().toString()+"'读线程退出!!");
                                stopMainThread();
                            }

                            if (msg.toString().contains("您的昵称通过验证")) {
                                String[] returnStr = msg.toString().split(" ");
                                name = returnStr[1];
                                key.attach(name);
                            }
                            System.out.println(msg.toString());
                        }
                        ikeys.remove();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void stopMainThread(){
        flag = false;
    }

    public static void main(String[] args){
        try {
            new ChatClient1().startClient();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用方法:

  • 仿照着 ChatClient1,再弄一个 ChatClient2。先运行服务端,再运行客户端 1、客户端 2。
  • 客户端里,先输入自己的昵称。
  • 之后,输入想说的话。

效果是这样的:
客户端1:

请输入自定义用户名:
jojo
您的昵称通过验证 jojo
	欢迎'jojo'上线,当前在线人数1人。用户列表:[jojo]
阿姨压一压
来自'jojo'的消息:阿姨压一压
我真是High到不行啊
来自'jojo'的消息:我真是High到不行啊
	欢迎'dio'上线,当前在线人数2人。用户列表:[jojo, dio]
来自'dio'的消息:哈哈哈哈
来自'dio'的消息:我不做人啦

客户端2:

请输入自定义用户名:
jojo
当前用户已存在,请重新输入用户名:
dio
您的昵称通过验证 dio
	欢迎'dio'上线,当前在线人数2人。用户列表:[jojo, dio]
哈哈哈哈
来自'dio'的消息:哈哈哈哈
我不做人啦
来自'dio'的消息:我不做人啦

而可能的bug是tcp粘包,简单的说,是上一次发送的尾,和下一次发送的头,挨在一起了:

//服务端代码
                client.write(charset.encode("您的昵称通过验证 "+user));
                this.broadCast(welCome);    //给所用用户推送上线信息,包括自己

这两行代码由于前后发送,所以client这个channel会先后收到 昵称验证通过 和 推送上线 的消息,如果tcp在传输过程中足够快,那么客户端在一次read事件中,会把两次消息一次性读出来。而由于这个代码写的比较简陋,在tcp的传输内容上并没有建立起足够安全的内容协议(比如消息与消息用特定的分隔符、或者用前面的几个字节来标注内容的实际字节数),所以上述代码并不能两次消息分开了。

造成的问题是:客户端是通过服务端回复的 验证消息 来初始化姓名的,如果粘包情况出现,那么客户端将会把真正的姓名和上线消息合起来作为自己的姓名。
客户端1:

请输入自定义用户名:
dio
您的昵称通过验证 dio	欢迎'dio'上线,当前在线人数 1 人。用户列表:[dio]
dkajgk
来自'dio	欢迎'dio'上线,当前在线人数 1 人。用户列表:[dio]'的消息:dkajgk
哈哈哈哈
来自'dio	欢迎'dio'上线,当前在线人数 1 人。用户列表:[dio]'的消息:哈哈哈哈
   欢迎'jojo'上线,当前在线人数 2 人。用户列表:[dio, jojo]
来自'jojo'的消息:哈哈哈哈
来自'jojo'的消息:我不做人啦

客户端2:

请输入自定义用户名:
jojo
您的昵称通过验证 jojo
   欢迎'jojo'上线,当前在线人数 2 人。用户列表:[dio, jojo]
哈哈哈哈
来自'jojo'的消息:哈哈哈哈
我不做人啦
来自'jojo'的消息:我不做人啦

多人在线,多人聊天(简单解决了TCP粘包bug)

服务端主要修改了两行代码,令服务端的每个message的结尾都加上一个分割符,好让客户端即使在tcp粘包的情况下,也能分辨出两个message来:

                client.write(charset.encode("您的昵称通过验证 "+user+"|"));
                this.broadCast(welCome+"|");    //给所用用户推送上线信息,包括自己

客户端:

    private class ClientReadThread implements Runnable{
        @Override
        public void run(){
            Iterator<SelectionKey> ikeys;
            SelectionKey key;
            SocketChannel client;
            try {
                while(flag){
                    selector.select();	//调用此方法一直阻塞,直到有channel可用
                    ikeys = selector.selectedKeys().iterator();
                    while(ikeys.hasNext()){
                        key = ikeys.next();
                        if(key.isReadable()){	//处理读事件
                            client = (SocketChannel) key.channel();
                            //这里的输出是true,从selector的key中获取的客户端channel,是同一个
//							System.out.println("client == _self:"+ (client == _self));
                            buffer.clear();
                            StringBuilder msg = new StringBuilder();
                            try{
                                while(client.read(buffer) > 0){
                                    buffer.flip();	//将写模式转换为读模式
                                    msg.append(charset.decode(buffer));
                                }
                            }catch(IOException en){
                                System.out.println(en.getMessage()+",客户端'"+key.attachment().toString()+"'读线程退出!!");
                                stopMainThread();
                            }
                            //这里将读取到消息用分隔符分离
                            String[] StrArray = msg.toString().split("[|]");
                            for (String message : StrArray) {
                                if (message == "") continue;
                                if (message.contains("您的昵称通过验证")) {
                                    if (message.contains("您的昵称通过验证")) {
                                        String[] nameValid = message.split(" ");
                                        name = nameValid[1];
                                        key.attach(name);
                                    }
                                }
                                System.out.println(message);
                            }
                        }
                        ikeys.remove();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

使用方法:

  • 仿照着ChatClient1,再弄一个ChatClient2。先运行服务端,再运行客户端1、客户端2。
  • 客户端里,先输入自己的昵称。
  • 之后,输入想说的话。

不过这样改还是没有解决,如果发送的数据过多,而使得另一方的read事件发生了两次,的问题。如果发生,那么bug是 一条聊天消息被拆分成两条聊天消息。

多人在线,单人聊天

服务端:

package NonBlocking;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class ChatServer {
    private final int port = 8899;
    private final String seperator = "[|]";                        //消息分隔符
    private final Charset charset = Charset.forName("UTF-8");    //字符集
    private ByteBuffer buffer = ByteBuffer.allocate(1024);        //缓存
    private Map<String, SocketChannel> onlineUsers = new HashMap<String, SocketChannel>();//将用户对应的channel对应起来
    private Selector selector;
    private ServerSocketChannel server;

    public void startServer() throws IOException {
        //NIO server初始化固定流程:5步
        selector = Selector.open();                    //1.selector open
        server = ServerSocketChannel.open();        //2.ServerSocketChannel open
        server.bind(new InetSocketAddress(port));    //3.serverChannel绑定端口
        server.configureBlocking(false);            //4.设置NIO为非阻塞模式
        server.register(selector, SelectionKey.OP_ACCEPT);//5.将channel注册在选择器上

        //NIO server处理数据固定流程:5步
        SocketChannel client;
        SelectionKey key;
        Iterator<SelectionKey> iKeys;

        while (true) {
            selector.select();                            //1.用select()方法阻塞,一直到有可用连接加入
            iKeys = selector.selectedKeys().iterator();    //2.到了这步,说明有可用连接到底,取出所有可用连接
            while (iKeys.hasNext()) {
                key = iKeys.next();                        //3.遍历
                if (key.isAcceptable()) {                    //4.对每个连接感兴趣的事做不同的处理
                    //对于客户端连接,注册到服务端
                    client = server.accept();            //获取客户端首次连接
                    client.configureBlocking(false);
                    //不用注册写,只有当写入量大,或写需要争用时,才考虑注册写事件
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("+++++客户端:" + client.getRemoteAddress() + ",建立连接+++++");
                    client.write(charset.encode("请输入自定义用户名:"));
                }
                if (key.isReadable()) {
                    client = (SocketChannel) key.channel();//通过key取得客户端channel
                    StringBuilder msg = new StringBuilder();
                    buffer.clear();        //多次使用的缓存,用前要先清空
                    try {
                        while (client.read(buffer) > 0) {
                            buffer.flip();    //将写模式转换为读模式
                            msg.append(charset.decode(buffer));
                        }
                    } catch (IOException e) {
                        //如果client.read(buffer)抛出异常,说明此客户端主动断开连接,需做下面处理
                        client.close();            //关闭channel
                        key.cancel();            //将channel对应的key置为不可用
                        onlineUsers.values().remove(client);    //将问题连接从map中删除
                        System.out.println("-----用户'" + key.attachment().toString() + "'退出连接,当前用户列表:" + onlineUsers.keySet().toString() + "-----");
                        continue;                //跳出循环
                    }
                    if (msg.length() > 0) this.processMsg(msg.toString(), client, key);    //处理消息体
                }
                iKeys.remove();                    //5.处理完一次事件后,要显式的移除
            }
        }
    }

    /**
     * 处理客户端传来的消息
     *
     * @param msg 格式:user_to|body|user_from
     * @throws IOException
     * @Key 这里主要用attach()方法,给通道定义一个表示符
     */
    private void processMsg(String msg, SocketChannel client, SelectionKey key) throws IOException {
        String[] ms = msg.split(seperator);
        if (ms.length == 1) {
            String user = ms[0];    //输入的是自定义用户名
            if (onlineUsers.containsKey(user)) {
                client.write(charset.encode("当前用户已存在,请重新输入用户名:"));
            } else {
                onlineUsers.put(user, client);
                key.attach(user);    //给通道定义一个表示符
                //  |字符来作为消息之间的分割符
                client.write(charset.encode("您的昵称通过验证 "+user+"|"));
                String welCome = "\t欢迎'" + user + "'上线,当前在线人数" + this.getOnLineNum() + "人。用户列表:" + onlineUsers.keySet().toString();
                this.broadCast(welCome+"|");    //给所用用户推送上线信息,包括自己
            }
        } else if (ms.length == 3) {
            String user_to = ms[0];
            String msg_body = ms[1];
            String user_from = ms[2];

            SocketChannel channel_to = onlineUsers.get(user_to);
            if (channel_to == null) {
                client.write(charset.encode("用户'" + user_to + "'不存在,当前用户列表:" + onlineUsers.keySet().toString()));
            } else {
                channel_to.write(charset.encode("来自'" + user_from + "'的消息:" + msg_body));
            }
        }
    }

    //map中的有效数量已被很好的控制,可以从map中获取,也可以用下面的方法取
    private int getOnLineNum() {
        int count = 0;
        Channel channel;
        for (SelectionKey k : selector.keys()) {
            channel = k.channel();
            if (channel instanceof SocketChannel) {    //排除ServerSocketChannel
                count++;
            }
        }
        return count;
    }

    //广播上线消息
    private void broadCast(String msg) throws IOException {
        Channel channel;
        for (SelectionKey k : selector.keys()) {
            channel = k.channel();
            if (channel instanceof SocketChannel) {
                SocketChannel client = (SocketChannel) channel;
                client.write(charset.encode(msg));
            }
        }
    }

    public static void main(String[] args) {
        try {
            new ChatServer().startServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端:

package NonBlocking;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Scanner;

public class ChatClient1 {

    private final int port = 8899;
    private final String seperator = "|";
    private final Charset charset = Charset.forName("UTF-8");	//字符集
    private ByteBuffer buffer = ByteBuffer.allocate(1024);
    private SocketChannel _self;
    private Selector selector;
    private String name = "";
    private boolean flag = true;	//服务端断开,客户端的读事件不会一直发生(与服务端不一样)

    Scanner scanner = new Scanner(System.in);
    public void startClient() throws IOException{
        //客户端初始化固定流程:4步
        selector = Selector.open();								//1.打开Selector
        _self = SocketChannel.open(new InetSocketAddress(port));//2.连接服务端,这里默认本机的IP
        _self.configureBlocking(false);							//3.配置此channel非阻塞
        _self.register(selector, SelectionKey.OP_READ);			//4.将channel的读事件注册到选择器

        /*
         * 因为等待用户输入会导致主线程阻塞
         * 所以用主线程处理输入,新开一个线程处理读数据
         */
        new Thread(new ClientReadThread()).start();	//开一个异步线程处理读
        String input = "";
        while(flag){
            input = scanner.nextLine();
            String[] strArray;
            if("".equals(input)){
                System.out.println("不允许输入空串!");
                continue;
            // 如果姓名没有初始化,且长度为1.说明当前在设置姓名
            }else if("".equals(name) && input.split("[|]").length == 1){
                //啥也不干
            // 如果姓名已经初始化过了,且长度为2.说明这是正常的发送格式
            }else if(!"".equals(name) && input.split("[|]").length == 2) {
                input = input + seperator + name;
            }else{
                System.out.println("输入不合法,请重新输入:");
                continue;
            }
            try{
                _self.write(charset.encode(input));
            }catch(Exception e){
                System.out.println(e.getMessage()+"客户端主线程退出连接!!");
            }
        }
    }

    private class ClientReadThread implements Runnable{
        @Override
        public void run(){
            Iterator<SelectionKey> ikeys;
            SelectionKey key;
            SocketChannel client;
            try {
                while(flag){
                    selector.select();	//调用此方法一直阻塞,直到有channel可用
                    ikeys = selector.selectedKeys().iterator();
                    while(ikeys.hasNext()){
                        key = ikeys.next();
                        if(key.isReadable()){	//处理读事件
                            client = (SocketChannel) key.channel();
                            //这里的输出是true,从selector的key中获取的客户端channel,是同一个
//							System.out.println("client == _self:"+ (client == _self));
                            buffer.clear();
                            StringBuilder msg = new StringBuilder();
                            try{
                                while(client.read(buffer) > 0){
                                    buffer.flip();	//将写模式转换为读模式
                                    msg.append(charset.decode(buffer));
                                }
                            }catch(IOException en){
                                System.out.println(en.getMessage()+",客户端'"+key.attachment().toString()+"'读线程退出!!");
                                stopMainThread();
                            }
                            String[] StrArray = msg.toString().split("[|]");
                            for (String message : StrArray) {
                                if (message == "") continue;
                                if (message.contains("您的昵称通过验证")) {
                                    if (message.contains("您的昵称通过验证")) {
                                        String[] nameValid = message.split(" ");
                                        name = nameValid[1];
                                        key.attach(name);
                                    }
                                }
                                System.out.println(message);
                            }
                        }
                        ikeys.remove();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void stopMainThread(){
        flag = false;
    }

    public static void main(String[] args){
        try {
            new ChatClient1().startClient();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用方法:

  • 仿照着ChatClient1,再弄一个ChatClient2。先运行服务端,再运行客户端1、客户端2。
  • 客户端里,先输入自己的昵称。
  • 之后,输入对方的名字和想说的话,用|隔开。

运行效果:
客户端1:

请输入自定义用户名:
张家辉
您的昵称通过验证 张家辉
	欢迎'张家辉'上线,当前在线人数1人。用户列表:[张家辉]
张家辉|我跟自己说话
来自'张家辉'的消息:我跟自己说话
古天乐|你在吗
用户'古天乐'不存在,当前用户列表:[张家辉]
	欢迎'古天乐'上线,当前在线人数2人。用户列表:[张家辉, 古天乐]
古天乐|你终于来了,兄弟
来自'古天乐'的消息:咋了,兄弟
古天乐|来玩贪玩蓝月啊

客户端2:

请输入自定义用户名:
古天乐
您的昵称通过验证 古天乐
	欢迎'古天乐'上线,当前在线人数2人。用户列表:[张家辉, 古天乐]
来自'张家辉'的消息:你终于来了,兄弟
张家辉|咋了,兄弟
来自'张家辉'的消息:来玩贪玩蓝月啊

参考博客

java NIO及NIO聊天室,相比其中内容,修复了用户名已存在的情况,自己客户端的名字还是会初始化为这个已存在名字的bug。原文代码为单人聊天,经过修改为多人聊天(多人聊天更简单点)。

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
首先,需要了解什么是Netty。Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。 接下来,我们可以开始实现聊天室功能。一个简单的聊天室应该具备以下功能: 1. 用户连接和断开连接的处理; 2. 用户发送消息和接收消息的处理; 3. 消息广播给所有在线用户。 下面是一个简单的实现: 1. 用户连接和断开连接的处理 Netty提供了ChannelHandlerAdapter和ChannelInboundHandlerAdapter两个抽象类,我们可以继承其中一个实现自己的Handler。这里我们使用ChannelInboundHandlerAdapter。 ```java public class ChatServerHandler extends ChannelInboundHandlerAdapter { // 用户列表,用于保存所有连接的用户 private static List<Channel> channels = new ArrayList<>(); // 新用户连接时,将连接加入用户列表 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { channels.add(ctx.channel()); System.out.println(ctx.channel().remoteAddress() + " 上线了"); } // 用户断开连接时,将连接从用户列表中移除 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { channels.remove(ctx.channel()); System.out.println(ctx.channel().remoteAddress() + " 下线了"); } } ``` 2. 用户发送消息和接收消息的处理 Netty的数据传输是通过ByteBuf来实现的,因此我们需要将ByteBuf转换为字符串进行处理。 ```java public class ChatServerHandler extends ChannelInboundHandlerAdapter { // 用户列表,用于保存所有连接的用户 private static List<Channel> channels = new ArrayList<>(); // 新用户连接时,将连接加入用户列表 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { channels.add(ctx.channel()); System.out.println(ctx.channel().remoteAddress() + " 上线了"); } // 用户断开连接时,将连接从用户列表中移除 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { channels.remove(ctx.channel()); System.out.println(ctx.channel().remoteAddress() + " 下线了"); } // 接收用户发送的消息并处理 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; String received = buf.toString(CharsetUtil.UTF_8); System.out.println(ctx.channel().remoteAddress() + ": " + received); broadcast(ctx, received); } // 将消息广播给所有在线用户 private void broadcast(ChannelHandlerContext ctx, String msg) { for (Channel channel : channels) { if (channel != ctx.channel()) { channel.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8)); } } } } ``` 3. 消息广播给所有在线用户 我们可以使用broadcast方法将接收到的消息广播给所有在线用户。 ```java public class ChatServerHandler extends ChannelInboundHandlerAdapter { // 用户列表,用于保存所有连接的用户 private static List<Channel> channels = new ArrayList<>(); // 新用户连接时,将连接加入用户列表 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { channels.add(ctx.channel()); System.out.println(ctx.channel().remoteAddress() + " 上线了"); } // 用户断开连接时,将连接从用户列表中移除 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { channels.remove(ctx.channel()); System.out.println(ctx.channel().remoteAddress() + " 下线了"); } // 接收用户发送的消息并处理 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; String received = buf.toString(CharsetUtil.UTF_8); System.out.println(ctx.channel().remoteAddress() + ": " + received); broadcast(ctx, received); } // 将消息广播给所有在线用户 private void broadcast(ChannelHandlerContext ctx, String msg) { for (Channel channel : channels) { if (channel != ctx.channel()) { channel.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8)); } } } } ``` 接下来我们需要编写一个启动类,用于启动聊天室服务器。 ```java public class ChatServer { public static void main(String[] args) throws Exception { // 创建EventLoopGroup EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // 创建ServerBootstrap ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ChatServerHandler()); } }); // 启动服务器 ChannelFuture channelFuture = serverBootstrap.bind(8888).sync(); System.out.println("服务器启动成功"); // 关闭服务器 channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 现在,我们就完成了一个简单的聊天室服务器。可以通过运行ChatServer类启动服务器,然后使用telnet命令连接服务器进行聊天。 ```sh telnet localhost 8888 ``` 输入发送的消息,即可将消息广播给所有在线用户。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值