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

网友投稿 234 2022-12-22


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 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.jscrollhttp://Pane;

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){

servhttp://ice.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程序设计有所帮助。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java递归遍历文件目录代码实例
下一篇:SpringMVC 参数绑定意义及实现过程解析
相关文章

 发表评论

暂时没有评论,来抢沙发吧~