在工作之余花了两个星期看完了《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. How to calculate the undo_retention time

    UNDO_RETENTION The undo_retention is a initialization parameter of the undo tablespace. The initiali ...

  2. 一步一步重写 CodeIgniter 框架 (1) —— url 如何映射到具体的方法

    CodeIgniter 框架最显著的特征就是 MVC 模式,它的做法就是提取 url 中的'分段', 映射到某个类的某个方法,从而由该方法来输出最终显示的页面内容.那么我们第一课中就是实现一个这样的原 ...

  3. rackup工具

    gem包rack提供了rackup工具来启动webapplication 下面是一个入门范例,使用 bundler 管理库的一个sinatra应用   在begin文件夹下有三个文件 begin.ru ...

  4. int 转换成 CString(VC2008里有这个问题)

    int s = 123; CString str; str.Format("%d",s); 这样就可以了,但是有的会提示这个错误 如果出现这个错误,就改成下面这个就OK了:  st ...

  5. WCF技术剖析之十三:序列化过程中的已知类型(Known Type)

    原文:WCF技术剖析之十三:序列化过程中的已知类型(Known Type) [爱心链接:拯救一个25岁身患急性白血病的女孩[内有苏州电视台经济频道<天天山海经>为此录制的节目视频(苏州话) ...

  6. android studio 怎么运行java

    方法/步骤 1.新建一个project,或者如果已经有project的话,那就直接新建一个module.注意选择Java library,然后下一步 2.输入module的一些信息.点击finish ...

  7. 基于visual Studio2013解决面试题之1201链表去重

     题目

  8. VC 为静态控件添加事件

    操作系统:Windows 7软件环境:Visual C++ 2008 SP1本次目的:为静态控件添加事件,如:STATIC.Picture Control等等 有时候我们找到一个图片,为对话框背景添加 ...

  9. Web端server推送技术原理分析及dwr框架简单的使用

    1 背景 "server推送技术"(ServerPushing)是近期Web技术中最热门的一个流行术语.它是继"Ajax"之后又一个倍受追捧的Web技术.&qu ...

  10. qt学习笔记(七)之数据库简介(所有支持数据库类型的列表)

    笔者最近用Qt写公司的考勤机.本来要求是要基于frameBuffer下用自己开发的easyGUI来进行上层应用开发,但是考虑到easyGUI提供的接口不是很多,就考虑用Qt来开发,顺带练练手. 废话不 ...