首页 > 代码库 > Java NIO实战之聊天室
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<String> unames; //时间格式化器 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * 构造函数 * @param port 服务端监控的端口号 */ public ChatServer(int port) { isRun = true; unames = new Vector<String>(); 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<SelectionKey> 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<SelectionKey> 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<SelectionKey> 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<SelectionKey> 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("<html>在上面的文本框中输入名字<br/>显示器宽度:" + w + "<br/>显示器高度:" + h + "</html>"); 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 < userNames.length; i++) { modle.addElement(userNames[i].trim()); } } else { //将聊天数据设置到聊天消息显示区 String str = readContext.getText() + msg; readContext.setText(str); readContext.selectAll();//保持滚动条在最下面 } } } } } // 显示界面 public void show() { this.init(); service.sendMsg("open_" + uname); MsgThread msgThread = new MsgThread(); msgThread.start(); this.frame.setVisible(true); } }
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。