栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java NIO实战之聊天室功能详解

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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;

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");
    
    public ChatServer(int port) {
 isRun = true;
 unames = new Vector();
 init(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();
 }
    }
    
    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);
  }
     }
 }
    }
    
    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;

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;

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);
    }
}

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

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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/136356.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号