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

以下直接上代码。jdk1.5以上经过測试,能够支持多人同一时候在线聊天;

将下面代码拷贝到项目中便可执行。源代码下载地址:聊天室源代码

一、server端

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;
//当前聊天室中的username称列表
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();
//接受新的连接返回和client对等的套接字通道
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_",表示client准备进入聊天界面
//client传过来的数据格式是"open_zing",表示名称为zing的用户请求打开聊天窗口
//username称列表有更新,则应将username称数据写给每个已连接的client
if (str.indexOf("open_") != -1) {//client连接服务器
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) {// client发送退出命令
String uname = str.substring(5);
//删除此username称
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 {// 读取client聊天消息
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();
}
}

二、client

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;// okbutton
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("发送");// 发送消息button
private JButton btnClose = new JButton("关闭");// 关闭聊天窗口button private JFrame frame = new JFrame("ChatFrame");// 窗口界面 private String uname;// 用户姓名 private ClientService service;// 用于与server交互 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);
}
}); //发送button事件
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();
}
}); //关闭button事件
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 }
});
} // 此线程类用于轮询读取server发送的消息
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 NIO实战之聊天室的更多相关文章

  1. 使用 NIO 搭建一个聊天室

    使用 NIO 搭建一个聊天室 前面刚讲了使用 Socket 搭建了一个 Http Server,在最后我们使用了 NIO 对 Server 进行了优化,然后有小伙伴问到怎么使用 Socket 搭建聊天 ...

  2. Java网络编程案例---聊天室

    网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来. java.net包中JavaSE的API包含有类和接口,它们提供低层次的通信细节.你可以直接使用这些类和接口,来专注于解决 ...

  3. NIO 多人聊天室

    一前言 在家休息没事,敲敲代码,用NIO写个简易的仿真聊天室.下面直接讲聊天室设计和编码.对NIO不了解的朋友,推荐一个博客,里面写的很棒: https://javadoop.com/     里面有 ...

  4. java基于socket公共聊天室的实现

    项目:一个公共聊天室功能的实现,实现了登录聊天,保存聊天记录等功能. 一.实现代码 1.客户端 ChatClient.java import java.io.BufferedReader; impor ...

  5. Java WebSocket实现网络聊天室(群聊+私聊)

    1.简单说明 在网上看到一份比较nice的基于webSocket网页聊天项目,准备看看学习学习,如是有了这篇文章!原博主博客:http://blog.csdn.net/Amayadream/artic ...

  6. JAVA WebSocKet ( 简单的聊天室 )

    1, 前端代码 登入页 -> login.html <!DOCTYPE html> <html> <head> <meta charset=" ...

  7. JAVA实现webSocket网页聊天室

    一.什么是webSocket WebSocket 是一种网络通信协议,是持久化协议.RFC6455 定义了它的通信标准. WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全 ...

  8. IO、NIO实现简单聊天室,附带问题解析

      本篇文章主要使用IO和NIO的形式来实现一个简单的聊天室,并且说明IO方法存在的问题,而NIO又是如何解决的.   大概的框架为,先提供思路和大概框架图--代码--问题及解决方式,这样会容易看一点 ...

  9. Java TCP案例网络聊天室

    收获:1,加深了对多线程的一边一边的理解,可以将行为写成不同的类然后多线程 2,IO流的复习! 3,多线程中一边读取一边操作时容器最好(CopyOnWriteArrayList); 4,Tcp流程的熟 ...

随机推荐

  1. QVector 和vector的比较(QVector默认使用隐式共享,而且有更多的函数提供)

    QVector和vector的比较: Qvector默认使用隐式共享,可以用setSharable改变其隐式共享.使用non-const操作和函数将引起深拷贝.at()比operator[](),快, ...

  2. AlphaMobileControls介绍

    大家在开发wince程序或者windows mobile程序时还在为丑陋的界面着急吗?AlphaMobileControls可以帮你解决这些方案.当然如果你是高手可以自己去实现一些特殊的功能,自己定义 ...

  3. SQL 多个表之间联合查询

    非常少用join,这次学学,并备忘两篇文章! 转自:http://hcx-2008.javaeye.com/blog/285661 连接查询 通过连接运算符能够实现多个表查询.连接是关系数据库模型的主 ...

  4. 小胖说事24-----property&#39;s synthesized getter follows Cocoa naming convention for returning &#39;owned&#39; objec

    今天在给类的属性命名的时候,用了newValue.就给报错:property's synthesized getter follows Cocoa naming convention for retu ...

  5. 十天学习PHP之第四天

    学习目的:学会连接数据库  PHP简直就是一个函数库,丰富的函数使PHP的某些地方相当简单.建议大家down一本PHP的函数手冊,总用得到. 我这里就简单说一下连接MYSQL数据库.  1.mysql ...

  6. wamp安装后打开默认网页显示dir,图标红点

    首先网页显示dir,是因为服务这些没启动,跟图标红点是一个原因,解决了图标红点,就能解决只显示dir的问题. 先测试是不是端口占用问题,如图: 如果是,那就继续往下走. 单击图标左键(记住是左键),然 ...

  7. [置顶] 让导入的Android项目,运行起来的方法。

    Eclipse里面直接import的代码,不能运行出现如下错误: [2013-12-12 12:58:55 - Dex Loader] Unable to execute dex: java.nio. ...

  8. PHP学习之-1.7 注释

    注释 在PHP中也有注释语句:用双斜杠 "//" 来表示.其他语言中 HTML使用 "<!--  -->" ,CSS中使用 "/*     ...

  9. oracle维护表空间和数据文件

    1:重要参考 wiki 2: oracle doc 表空间参考 3:来自dba-oracle的参考 26,27,28,29 一:oracle 表空间概念 表空间是联系数据库的物理磁盘(数据文件)和逻辑 ...

  10. MSSQL - SQL Server2008附加数据库失败 错误号:5120

    附加数据库时,显示错误,错误信息为 一种解决方法为,设置mdf文件所在文件夹的权限(有些资料说只设置mdf文件的权限就好,但我试了不管用),在文件夹上右击——属性——安全,如图所示: 选择组或用户名中 ...