热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

JavaNIO实战之聊天室功能详解

这篇文章主要介绍了JavaNIO实战之聊天室功能,结合实例形式详细分析了javaNIO聊天室具体的服务端、客户端相关实现方法与操作注意事项,需要的朋友可以参考下

本文实例讲述了Java NIO实战之聊天室功能。分享给大家供大家参考,具体如下:

在工作之余花了两个星期看完了《Java NIO》,总体来说这本书把NIO写的很详细,没有过多的废话,讲的都是重点,只是翻译的中文版看的确实吃力,英文水平太低也没办法,总算也坚持看完了。《Java NIO》这本书的重点在于第四章讲解的“选择器”,要理解透还是要反复琢磨推敲;愚钝的我花了大概3天的时间才将NIO的选择器机制理解透并能较熟练的运用,于是便写了这个聊天室程序。

下面直接上代码,jdk1.5以上经过测试,可以支持多人同时在线聊天;

将以下代码复制到项目中便可运行,源码下载地址:聊天室源码。

一、服务器端

package com.chat.server;
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.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Vector;
/**
 * 聊天室:服务端
 * @author zing
 * 
 */
public class ChatServer implements Runnable {
    //选择器
    private Selector selector;
    //注册ServerSocketChannel后的选择键
    private SelectionKey serverKey;
    //标识是否运行
    private boolean isRun;
    //当前聊天室中的用户名称列表
    private Vector unames;
    //时间格式化器
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 构造函数
     * @param port 服务端监控的端口号
     */
    public ChatServer(int port) {
        isRun = true;
        unames = new Vector();
        init(port);
    }
    /**
     * 初始化选择器和服务器套接字
     * 
     * @param port 服务端监控的端口号
     */
    private void init(int port) {
        try {
            //获得选择器实例
            selector = Selector.open();
            //获得服务器套接字实例
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            //绑定端口号
            serverChannel.socket().bind(new InetSocketAddress(port));
            //设置为非阻塞
            serverChannel.configureBlocking(false);
            //将ServerSocketChannel注册到选择器,指定其行为为"等待接受连接"
            serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            printInfo("server starting...");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        try {
            //轮询选择器选择键
            while (isRun) {
                //选择一组已准备进行IO操作的通道的key,等于1时表示有这样的key
                int n = selector.select();
                if (n > 0) {
                    //从选择器上获取已选择的key的集合并进行迭代
                    Iterator iter = selector.selectedKeys().iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        //若此key的通道是等待接受新的套接字连接
                        if (key.isAcceptable()) {
                            //记住一定要remove这个key,否则之后的新连接将被阻塞无法连接服务器
                            iter.remove();
                            //获取key对应的通道
                            ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                            //接受新的连接返回和客户端对等的套接字通道
                            SocketChannel channel = serverChannel.accept();
                            if (channel == null) {
                                continue;
                            }
                            //设置为非阻塞
                            channel.configureBlocking(false);
                            //将这个套接字通道注册到选择器,指定其行为为"读"
                            channel.register(selector, SelectionKey.OP_READ);
                        }
                        //若此key的通道的行为是"读"
                        if (key.isReadable()) {
                            readMsg(key);
                        }
                        //若次key的通道的行为是"写"
                        if (key.isWritable()) {
                            writeMsg(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 从key对应的套接字通道上读数据
     * @param key 选择键
     * @throws IOException
     */
    private void readMsg(SelectionKey key) throws IOException {
        //获取此key对应的套接字通道
        SocketChannel channel = (SocketChannel) key.channel();
        //创建一个大小为1024k的缓存区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuffer sb = new StringBuffer();
        //将通道的数据读到缓存区
        int count = channel.read(buffer);
        if (count > 0) {
            //翻转缓存区(将缓存区由写进数据模式变成读出数据模式)
            buffer.flip();
            //将缓存区的数据转成String
            sb.append(new String(buffer.array(), 0, count));
        }
        String str = sb.toString();
        //若消息中有"open_",表示客户端准备进入聊天界面
        //客户端传过来的数据格式是"open_zing",表示名称为zing的用户请求打开聊天窗体
        //用户名称列表有更新,则应将用户名称数据写给每一个已连接的客户端
        if (str.indexOf("open_") != -1) {//客户端连接服务器
            String name = str.substring(5);
            printInfo(name + " online");
            unames.add(name);
            //获取选择器已选择的key并迭代
            Iterator iter = selector.selectedKeys().iterator();
            while (iter.hasNext()) {
                SelectionKey selKey = iter.next();
                //若不是服务器套接字通道的key,则将数据设置到此key中
                //并更新此key感兴趣的动作
                if (selKey != serverKey) {
                    selKey.attach(unames);
                    selKey.interestOps(selKey.interestOps() | SelectionKey.OP_WRITE);
                }
            }
        } else if (str.indexOf("exit_") != -1) {// 客户端发送退出命令
            String uname = str.substring(5);
            //删除此用户名称
            unames.remove(uname);
            //将"close"字符串附加到key
            key.attach("close");
            //更新此key感兴趣的动作
            key.interestOps(SelectionKey.OP_WRITE);
            //获取选择器上的已选择的key并迭代
            //将更新后的名称列表数据附加到每个套接字通道key上,并重设key感兴趣的操作
            Iterator iter = key.selector().selectedKeys().iterator();
            while (iter.hasNext()) {
                SelectionKey selKey = iter.next();
                if (selKey != serverKey && selKey != key) {
                    selKey.attach(unames);
                    selKey.interestOps(selKey.interestOps() | SelectionKey.OP_WRITE);
                }
            }
            printInfo(uname + " offline");
        } else {// 读取客户端聊天消息
            String uname = str.substring(0, str.indexOf("^"));
            String msg = str.substring(str.indexOf("^") + 1);
            printInfo("("+uname+")说:" + msg);
            String dateTime = sdf.format(new Date());
            String smsg = uname + " " + dateTime + "\n " + msg + "\n";
            Iterator iter = selector.selectedKeys().iterator();
            while (iter.hasNext()) {
                SelectionKey selKey = iter.next();
                if (selKey != serverKey) {
                    selKey.attach(smsg);
                    selKey.interestOps(selKey.interestOps() | SelectionKey.OP_WRITE);
                }
            }
        }
    }
    /**
     * 写数据到key对应的套接字通道
     * @param key
     * @throws IOException
     */
    private void writeMsg(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        Object obj = key.attachment();
        //这里必要要将key的附加数据设置为空,否则会有问题
        key.attach("");
        //附加值为"close",则取消此key,并关闭对应通道
        if (obj.toString().equals("close")) {
            key.cancel();
            channel.socket().close();
            channel.close();
            return;
        }else {
            //将数据写到通道
            channel.write(ByteBuffer.wrap(obj.toString().getBytes()));
        }
        //重设此key兴趣
        key.interestOps(SelectionKey.OP_READ);
    }
    private void printInfo(String str) {
        System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
    }
    public static void main(String[] args) {
        ChatServer server = new ChatServer(19999);
        new Thread(server).start();
    }
}

二、客户端

1、服务类,用于与服务端交互

package com.chat.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ClientService {
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 19999;
    private static SocketChannel sc;
    private static Object lock = new Object();
    private static ClientService service;
    public static ClientService getInstance(){
        synchronized (lock) {
            if(service == null){
                try {
                    service = new ClientService();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return service;
        }
    }
    private ClientService() throws IOException {
        sc = SocketChannel.open();
        sc.configureBlocking(false);
        sc.connect(new InetSocketAddress(HOST, PORT));
    }
    public void sendMsg(String msg) {
        try {
            while (!sc.finishConnect()) {
            }
            sc.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public String receiveMsg() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.clear();
        StringBuffer sb = new StringBuffer();
        int count = 0;
        String msg = null;
        try {
            while ((count = sc.read(buffer)) > 0) {
                sb.append(new String(buffer.array(), 0, count));
            }
            if (sb.length() > 0) {
                msg = sb.toString();
                if ("close".equals(sb.toString())) {
                    msg = null;
                    sc.close();
                    sc.socket().close();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return msg;
    }
}

2、登陆窗体,用户设置名称

package com.chat.client;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
/**
 * 设置名称窗体
 * 
 * @author zing
 * 
 */
public class SetNameFrame extends JFrame {
    private static final long serialVersiOnUID= 1L;
    private static JTextField txtName;// 文本框
    private static JButton btnOK;// ok按钮
    private static JLabel label;// 标签
    public SetNameFrame() {
        this.setLayout(null);
        Toolkit kit = Toolkit.getDefaultToolkit();
        int w = kit.getScreenSize().width;
        int h = kit.getScreenSize().height;
        this.setBounds(w / 2 - 230 / 2, h / 2 - 200 / 2, 230, 200);
        this.setTitle("设置名称");
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setResizable(false);
        txtName = new JTextField(4);
        this.add(txtName);
        txtName.setBounds(10, 10, 100, 25);
        btnOK = new JButton("OK");
        this.add(btnOK);
        btnOK.setBounds(120, 10, 80, 25);
        label = new JLabel("[w:" + w + ",h:" + h + "]");
        this.add(label);
        label.setBounds(10, 40, 200, 100);
        label.setText("在上面的文本框中输入名字
显示器宽度:" + w + "
显示器高度:" + h + ""); btnOK.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String uname = txtName.getText(); ClientService service = ClientService.getInstance(); ChatFrame chatFrame = new ChatFrame(service, uname); chatFrame.show(); setVisible(false); } }); } public static void main(String[] args) { SetNameFrame setNameFrame = new SetNameFrame(); setNameFrame.setVisible(true); } }

3、聊天室窗体

package com.chat.client;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
/**
 * 聊天室窗体
 * @author zing
 *
 */
public class ChatFrame {
    private JTextArea readCOntext= new JTextArea(18, 30);// 显示消息文本框
    private JTextArea writeCOntext= new JTextArea(6, 30);// 发送消息文本框
    private DefaultListModel modle = new DefaultListModel();// 用户列表模型
    private JList list = new JList(modle);// 用户列表
    private JButton btnSend = new JButton("发送");// 发送消息按钮
    private JButton btnClose = new JButton("关闭");// 关闭聊天窗口按钮
    private JFrame frame = new JFrame("ChatFrame");// 窗体界面
    private String uname;// 用户姓名
    private ClientService service;// 用于与服务器交互
    private boolean isRun = false;// 是否运行
    public ChatFrame(ClientService service, String uname) {
        this.isRun = true;
        this.uname = uname;
        this.service = service;
    }
    // 初始化界面控件及事件
    private void init() {
        frame.setLayout(null);
        frame.setTitle(uname + " 聊天窗口");
        frame.setSize(500, 500);
        frame.setLocation(400, 200);
        //设置可关闭
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //不能改变窗体大小
        frame.setResizable(false);
        //聊天消息显示区带滚动条
        JScrollPane readScroll = new JScrollPane(readContext);
        readScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        frame.add(readScroll);
        //消息编辑区带滚动条
        JScrollPane writeScroll = new JScrollPane(writeContext);
        writeScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        frame.add(writeScroll);
        frame.add(list);
        frame.add(btnSend);
        frame.add(btnClose);
        readScroll.setBounds(10, 10, 320, 300);
        readContext.setBounds(0, 0, 320, 300);
        readContext.setEditable(false);//设置为不可编辑
        readContext.setLineWrap(true);// 自动换行
        writeScroll.setBounds(10, 315, 320, 100);
        writeContext.setBounds(0, 0, 320, 100);
        writeContext.setLineWrap(true);// 自动换行
        list.setBounds(340, 10, 140, 445);
        btnSend.setBounds(150, 420, 80, 30);
        btnClose.setBounds(250, 420, 80, 30);
        //窗体关闭事件
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                isRun = false;
                service.sendMsg("exit_" + uname);
                System.exit(0);
            }
        });
        //发送按钮事件
        btnSend.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String msg = writeContext.getText().trim();
                if(msg.length() > 0){
                    service.sendMsg(uname + "^" + writeContext.getText());
                }
                //发送消息后,去掉编辑区文本,并获得光标焦点
                writeContext.setText(null);
                writeContext.requestFocus();
            }
        });
        //关闭按钮事件
        btnClose.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                isRun = false;
                service.sendMsg("exit_" + uname);
                System.exit(0);
            }
        });
        //右边名称列表选择事件
        list.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                // JOptionPane.showMessageDialog(null,
                // list.getSelectedValue().toString());
            }
        });
        //消息编辑区键盘按键事件
        writeContext.addKeyListener(new KeyListener() {
            @Override
            public void keyTyped(KeyEvent e) {
                // TODO Auto-generated method stub
            }
            //按下键盘按键后释放
            @Override
            public void keyReleased(KeyEvent e) {
                //按下enter键发送消息
                if(e.getKeyCode() == KeyEvent.VK_ENTER){
                    String msg = writeContext.getText().trim();
                    if(msg.length() > 0){
                        service.sendMsg(uname + "^" + writeContext.getText());
                    }
                    writeContext.setText(null);
                    writeContext.requestFocus();
                }
            }
            @Override
            public void keyPressed(KeyEvent e) {
                // TODO Auto-generated method stub
            }
        });
    }
    // 此线程类用于轮询读取服务器发送的消息
    private class MsgThread extends Thread {
        @Override
        public void run() {
            while (isRun) {
                String msg = service.receiveMsg();
                if (msg != null) {
                    //若是名称列表数据,则更新聊天窗体右边的列表
                    if (msg.indexOf("[") != -1 && msg.lastIndexOf("]") != -1) {
                        msg = msg.substring(1, msg.length() - 1);
                        String[] userNames = msg.split(",");
                        modle.removeAllElements();
                        for (int i = 0; i 

更多java相关内容感兴趣的读者可查看本站专题:《Java面向对象程序设计入门与进阶教程》、《Java数据结构与算法教程》、《Java操作DOM节点技巧总结》、《Java文件与目录操作技巧汇总》和《Java缓存操作技巧汇总》

希望本文所述对大家java程序设计有所帮助。


推荐阅读
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 深入理解OAuth认证机制
    本文介绍了OAuth认证协议的核心概念及其工作原理。OAuth是一种开放标准,旨在为第三方应用提供安全的用户资源访问授权,同时确保用户的账户信息(如用户名和密码)不会暴露给第三方。 ... [详细]
  • 本周信息安全小组主要进行了CTF竞赛相关技能的学习,包括HTML和CSS的基础知识、逆向工程的初步探索以及整数溢出漏洞的学习。此外,还掌握了Linux命令行操作及互联网工作原理的基本概念。 ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 本文介绍了如何在 DB2 环境中创建和删除数据库编目。创建编目是连接新数据库的必要步骤,涉及获取数据库连接信息、使用命令行工具进行配置,并验证连接的有效性。删除编目则用于移除不再需要的数据库连接。 ... [详细]
  • 深入解析Android自定义View面试题
    本文探讨了Android Launcher开发中自定义View的重要性,并通过一道经典的面试题,帮助开发者更好地理解自定义View的实现细节。文章不仅涵盖了基础知识,还提供了实际操作建议。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • Søren Kierkegaard famously stated that life can only be understood in retrospect but must be lived moving forward. This perspective delves into the intricate relationship between our lived experiences and our reflections on them. ... [详细]
  • 计算机网络复习:第五章 网络层控制平面
    本文探讨了网络层的控制平面,包括转发和路由选择的基本原理。转发在数据平面上实现,通过配置路由器中的转发表完成;而路由选择则在控制平面上进行,涉及路由器中路由表的配置与更新。此外,文章还介绍了ICMP协议、两种控制平面的实现方法、路由选择算法及其分类等内容。 ... [详细]
  • 本文将介绍如何使用 Go 语言编写和运行一个简单的“Hello, World!”程序。内容涵盖开发环境配置、代码结构解析及执行步骤。 ... [详细]
  • 本文探讨了Hive中内部表和外部表的区别及其在HDFS上的路径映射,详细解释了两者的创建、加载及删除操作,并提供了查看表详细信息的方法。通过对比这两种表类型,帮助读者理解如何更好地管理和保护数据。 ... [详细]
  • 本文介绍了如何使用jQuery根据元素的类型(如复选框)和标签名(如段落)来获取DOM对象。这有助于更高效地操作网页中的特定元素。 ... [详细]
  • 导航栏样式练习:项目实例解析
    本文详细介绍了如何创建一个具有动态效果的导航栏,包括HTML、CSS和JavaScript代码的实现,并附有详细的说明和效果图。 ... [详细]
  • 本文详细介绍了如何使用Maven高效管理多模块项目,涵盖项目结构设计、依赖管理和构建优化等方面。通过具体的实例和配置说明,帮助开发者更好地理解和应用Maven在复杂项目中的优势。 ... [详细]
  • 深入理解Tornado模板系统
    本文详细介绍了Tornado框架中模板系统的使用方法。Tornado自带的轻量级、高效且灵活的模板语言位于tornado.template模块,支持嵌入Python代码片段,帮助开发者快速构建动态网页。 ... [详细]
author-avatar
吴款爷
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有