NIO重构聊天室服务端

 BIO实现聊天室服务端---

读写的过程中可能会发生阻塞现象。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;

/**
 * 聊天室服务端
 */
public class Server {
    /**
     * java.net.ServerSocket
     * ServerSocket是运行在服务端上的。其主要有两个作用
     * 1:向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
     * 2:监听服务端口,一旦客户端连接会立即常见一个Socket,通过该Socket与客户端交互
     *
     * 如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
     */
    private ServerSocket serverSocket;
//    private PrintWriter[] allOut = {};
    private Collection<PrintWriter> allOut = new ArrayList<>();


    public Server(){
        try {
            /*
                ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
                端口建立连接。
                如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
                java.net.BindException:address already in use
                绑定异常:地址被使用了
             */
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            /*
                ServerSocket的accept方法是一个阻塞方法。
                开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
                会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
                相当于是接电话的动作。
                阻塞方法:调用后,程序就"卡住"不往下执行了。
             */
            while(true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互
                //Client:客户端  Handler:处理器
                ClientHandler clientHandler = new ClientHandler(socket);
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务是用一个线程来处理一个客户端的交互工作
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录远端计算机的地址信息

        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        public void run(){
            PrintWriter pw = null;
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于给对方发送消息
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw
                        = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                //将该输出流存入共享数组allOut中
                synchronized (Server.this) {
                    //1扩容allOut
//                    allOut = Arrays.copyOf(allOut, allOut.length + 1);
                    //2将pw放到数组最后一个格子里
//                    allOut[allOut.length - 1] = pw;

                    allOut.add(pw);
                }

                //通知所有客户端,该用户上线了!
//                sendMessage(host+"上线了,当前在线人数:"+allOut.length);
                sendMessage(host+"上线了,当前在线人数:"+allOut.size());

                String line;
                /*
                    这里的BufferedReader读取时低下连接的流是通过Socket获取的输入流,
                    当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
                    处于阻塞状态,直到对方发送过来一行字符串为止。
                    如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。

                    对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法
                    会抛出下面异常:
                    java.net.SocketException: connection reset
                    服务端无法避免这个异常。
                 */
                while ((line = br.readLine()) != null) {
                    //遍历allOut数组,将消息发送给所有客户端
                    sendMessage(host+"说:" + line);
                }
            }catch(IOException e){
//                e.printStackTrace();
            }finally {
                //处理客户端断开连接后的操作
                synchronized (Server.this) {
                    //将pw从数组allOut中删除
//                    for (int i = 0; i < allOut.length; i++) {
//                        if (allOut[i] == pw) {
//                            allOut[i] = allOut[allOut.length - 1];
//                            allOut = Arrays.copyOf(allOut, allOut.length - 1);
//                            break;
//                        }
//                    }

                    allOut.remove(pw);
                }
                //通知所有客户端,该用户下线了!
//                sendMessage(host+"下线了,当前在线人数:"+allOut.length);
                sendMessage(host+"下线了,当前在线人数:"+allOut.size());


                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 将消息群发给所有客户端
         * @param line
         */
        private void sendMessage(String line){
            synchronized (Server.this) {
                System.out.println(line);
                //遍历allOut数组,将消息发送给所有客户端
                for (PrintWriter pw : allOut) {
                    pw.println(line);
                }
            }
        }

    }


}

NIO实现服务端---

读写的过程中不会发生阻塞线程
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * NIO实现聊天室服务端
 *
 * netty框架
 */
public class NIOServer {
    public static void main(String[] args) {
        try {
            //存放所有客户端的SocketChannel,用于广播消息
            List<SocketChannel> allChannel = new ArrayList<>();


            //创建总机,ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //ServerSocketChannel模式为阻塞模式,可以将其设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);//传入false设置为非阻塞模式
            //为ServerSocketChannel绑定服务端口,客户端可以通过该端口与我们建立连接
            serverSocketChannel.bind(new InetSocketAddress(8088));
            //以上创建为固定模式,以后都可以用这样的形式创建ServerSocketChannel的非阻塞模式

            /*
                多路选择器的应用
                这个是NIO解决非阻塞的关键API,用于监听所有通道对应的事件,并做出对应的操作。
                我们的线程只要轮询处理多路选择器中待处理的通道事件即可完成所有通道的工作,避免使用大量线程
                处于阻塞来减少不必要的系统开销。
             */
            Selector selector = Selector.open();//使用其静态方法open获取一个多路选择器实例

            /**
             * 让"总机"ServerSocketChannel向多路选择器上注册一个事件,即:accept事件。
             * 原因:原来使用ServerSocket时,一旦主线程调用accept方法就会进入阻塞状态,直到一个客户端连接
             * 否则将无法继续执行其他工作。而现在的操作是让多路选择器关心该事件,避免让线程处于阻塞。
             */
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


           while(true) {
                /*
                    多路选择器的select方法
                    当注册在该选择器上的channel有事件待处理时,此方法会立即返回一个int值,表示有多少个事件待处理
                    若没有任何事件待处理时,此方法会形成阻塞。
                 */
               System.out.println("等待选择器告知是否有事件等待处理...");
               selector.select();
               System.out.println("选择器有事件待处理!!!");
               //通过选择器获取所有待处理的事件
               Set<SelectionKey> keySet = selector.selectedKeys();
               //遍历集合,将所有待处理的事件处理完毕
               for (SelectionKey key : keySet) {
                   //判断该事件是否为可以接受一个客户端连接了(对应的是向多路选择器注册的事件SelectionKey.OP_ACCEPT)
                   if (key.isAcceptable()) {
                       //处理接收客户端连接的操作
                        /*
                            通过SelectionKey的方法channel()获取该事件触发的通道

                            因为只有ServerSocketChannel向多路选择器注册了OP_ACCEPT事件,因此当该事件
                            isAcceptable()返回值为true,则说明该事件一定是由ServerSocketChannel触发的
                            所以我们通过该事件获取触发该事件的通道时,一定获取的是ServerSocketChannel
                         */
                       ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        /*
                            获取的SocketChannel与原来ServerSocket.accept返回的Socket道理一致
                            通过该SocketChannel就可以与连接的客户端进行双向交互数据了
                         */
                       SocketChannel socket = channel.accept();//接受客户端的连接
                        /*
                            非阻塞的ServerSocketChannel就算多路选择器提示有客户端请求可接受了,accept返回时
                            得到的SocketChanel有可能为null
                         */
                       if (socket == null) {
                           continue;//忽略本次事件的后续处理
                       }
                        /*
                            当我们接受了客户端连接后,原来的聊天室服务端我们通过Socket获取输入流读取客户端
                            发送过来消息的操作时如果客户端不发送内容,那么读取操作就会阻塞!
                            对此,我们将当前SocketChannel它的读取消息操作也注册到多路选择器中,这样一来只有
                            当客户端发送过来消息时,多路选择器才会通知我们进行处理。
                         */
                       //将当前SocketChannel设置为非阻塞模式
                       socket.configureBlocking(false);
                       //向多路选择器中注册读取客户端消息的事件
                       socket.register(selector, SelectionKey.OP_READ);
                       //将该SocketChannel存入共享集合,用于将消息广播
                       allChannel.add(socket);
                       System.out.println("一个客户端连接了!当前在线人数:"+allChannel.size() );


                   //该事件是否为某个SocketChannel有消息可以读取了(某个客户端发送过来了消息)
                   }else if(key.isReadable()){
                        try {
                            //通过事件获取触发了该事件的channel
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            //通过SocketChannel读取该客户端发送过来的消息
                            ByteBuffer buffer = ByteBuffer.allocate(1024);

                            //非阻塞状态下,有可能读取数据时未读取到任何字节
                            int len = socketChannel.read(buffer);
                            if (len == 0) {
                                continue;
                            } else if (len == -1) {//若本次读取的长度返回值为-1则表示客户端断开连接了
                                socketChannel.close();//关闭SocketChannel与客户端也断开
                                allChannel.remove(socketChannel);
                                continue;
                            }

                            buffer.flip();//反转后position到limit之间就是本次读取到的数据了
                            byte[] data = new byte[buffer.limit()];
                           /*
                                Buffer的get方法要求我们传入一个字节数组,作用是将当前Buffer中从下标
                                position开始的连续data数组长度的字节量装入该data数组。
                            */
                            buffer.get(data);//调用完毕后,data中保存的就是buffer中本次读取到的所有字节了
                            //将读取的消息转换为字符串(客户端发送过来的消息)
                            String line = new String(data, StandardCharsets.UTF_8);
                            //通过SocketChannel获取客户端地址信息
                            String host = socketChannel.socket().getInetAddress().getHostAddress();
                            System.out.print(host+"说:" + line);

                            //遍历所有的SocketChannel,将该消息发送给所有客户端
                            for(SocketChannel sc : allChannel){
                                buffer.flip();//position:0   limit:buffer中所有之前读取到的字节
                                sc.write(buffer);//position=limit=buffer中所有之前读取到的字节
                            }




                        }catch(IOException e){
                            //读取客户端消息这里若抛出异常,则通常是客户端强行断开连接造成的。
                            key.channel().close();//断开该SocketChannel与客户端断开连接即可
                            allChannel.remove(key.channel());
                        }
                   }
               }

           }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liamlf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值