Java NIO實(shí)戰(zhàn)之聊天室功能詳解
本文實(shí)例講述了Java NIO實(shí)戰(zhàn)之聊天室功能。分享給大家供大家參考,具體如下:
在工作之余花了兩個(gè)星期看完了《Java NIO》,總體來說這本書把NIO寫的很詳細(xì),沒有過多的廢話,講的都是重點(diǎn),只是翻譯的中文版看的確實(shí)吃力,英文水平太低也沒辦法,總算也堅(jiān)持看完了?!禞ava NIO》這本書的重點(diǎn)在于第四章講解的“選擇器”,要理解透還是要反復(fù)琢磨推敲;愚鈍的我花了大概3天的時(shí)間才將NIO的選擇器機(jī)制理解透并能較熟練的運(yùn)用,于是便寫了這個(gè)聊天室程序。
下面直接上代碼,jdk1.5以上經(jīng)過測(cè)試,可以支持多人同時(shí)在線聊天;
將以下代碼復(fù)制到項(xiàng)目中便可運(yùn)行,源碼下載地址:聊天室源碼。
一、服務(wù)器端
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;
/**
* 聊天室:服務(wù)端
* @author zing
*
*/
public class ChatServer implements Runnable {
//選擇器
private Selector selector;
//注冊(cè)ServerSocketChannel后的選擇鍵
private SelectionKey serverKey;
//標(biāo)識(shí)是否運(yùn)行
private boolean isRun;
//當(dāng)前聊天室中的用戶名稱列表
private Vector<String> unames;
//時(shí)間格式化器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 構(gòu)造函數(shù)
* @param port 服務(wù)端監(jiān)控的端口號(hào)
*/
public ChatServer(int port) {
isRun = true;
unames = new Vector<String>();
init(port);
}
/**
* 初始化選擇器和服務(wù)器套接字
*
* @param port 服務(wù)端監(jiān)控的端口號(hào)
*/
private void init(int port) {
try {
//獲得選擇器實(shí)例
selector = Selector.open();
//獲得服務(wù)器套接字實(shí)例
ServerSocketChannel serverChannel = ServerSocketChannel.open();
//綁定端口號(hào)
serverChannel.socket().bind(new InetSocketAddress(port));
//設(shè)置為非阻塞
serverChannel.configureBlocking(false);
//將ServerSocketChannel注冊(cè)到選擇器,指定其行為為"等待接受連接"
serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
printInfo("server starting...");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
//輪詢選擇器選擇鍵
while (isRun) {
//選擇一組已準(zhǔn)備進(jìn)行IO操作的通道的key,等于1時(shí)表示有這樣的key
int n = selector.select();
if (n > 0) {
//從選擇器上獲取已選擇的key的集合并進(jìn)行迭代
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
//若此key的通道是等待接受新的套接字連接
if (key.isAcceptable()) {
//記住一定要remove這個(gè)key,否則之后的新連接將被阻塞無法連接服務(wù)器
iter.remove();
//獲取key對(duì)應(yīng)的通道
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
//接受新的連接返回和客戶端對(duì)等的套接字通道
SocketChannel channel = serverChannel.accept();
if (channel == null) {
continue;
}
//設(shè)置為非阻塞
channel.configureBlocking(false);
//將這個(gè)套接字通道注冊(cè)到選擇器,指定其行為為"讀"
channel.register(selector, SelectionKey.OP_READ);
}
//若此key的通道的行為是"讀"
if (key.isReadable()) {
readMsg(key);
}
//若次key的通道的行為是"寫"
if (key.isWritable()) {
writeMsg(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 從key對(duì)應(yīng)的套接字通道上讀數(shù)據(jù)
* @param key 選擇鍵
* @throws IOException
*/
private void readMsg(SelectionKey key) throws IOException {
//獲取此key對(duì)應(yīng)的套接字通道
SocketChannel channel = (SocketChannel) key.channel();
//創(chuàng)建一個(gè)大小為1024k的緩存區(qū)
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuffer sb = new StringBuffer();
//將通道的數(shù)據(jù)讀到緩存區(qū)
int count = channel.read(buffer);
if (count > 0) {
//翻轉(zhuǎn)緩存區(qū)(將緩存區(qū)由寫進(jìn)數(shù)據(jù)模式變成讀出數(shù)據(jù)模式)
buffer.flip();
//將緩存區(qū)的數(shù)據(jù)轉(zhuǎn)成String
sb.append(new String(buffer.array(), 0, count));
}
String str = sb.toString();
//若消息中有"open_",表示客戶端準(zhǔn)備進(jìn)入聊天界面
//客戶端傳過來的數(shù)據(jù)格式是"open_zing",表示名稱為zing的用戶請(qǐng)求打開聊天窗體
//用戶名稱列表有更新,則應(yīng)將用戶名稱數(shù)據(jù)寫給每一個(gè)已連接的客戶端
if (str.indexOf("open_") != -1) {//客戶端連接服務(wù)器
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();
//若不是服務(wù)器套接字通道的key,則將數(shù)據(jù)設(shè)置到此key中
//并更新此key感興趣的動(dòng)作
if (selKey != serverKey) {
selKey.attach(unames);
selKey.interestOps(selKey.interestOps() | SelectionKey.OP_WRITE);
}
}
} else if (str.indexOf("exit_") != -1) {// 客戶端發(fā)送退出命令
String uname = str.substring(5);
//刪除此用戶名稱
unames.remove(uname);
//將"close"字符串附加到key
key.attach("close");
//更新此key感興趣的動(dòng)作
key.interestOps(SelectionKey.OP_WRITE);
//獲取選擇器上的已選擇的key并迭代
//將更新后的名稱列表數(shù)據(jù)附加到每個(gè)套接字通道key上,并重設(shè)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);
}
}
}
}
/**
* 寫數(shù)據(jù)到key對(duì)應(yīng)的套接字通道
* @param key
* @throws IOException
*/
private void writeMsg(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
Object obj = key.attachment();
//這里必要要將key的附加數(shù)據(jù)設(shè)置為空,否則會(huì)有問題
key.attach("");
//附加值為"close",則取消此key,并關(guān)閉對(duì)應(yīng)通道
if (obj.toString().equals("close")) {
key.cancel();
channel.socket().close();
channel.close();
return;
}else {
//將數(shù)據(jù)寫到通道
channel.write(ByteBuffer.wrap(obj.toString().getBytes()));
}
//重設(shè)此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、服務(wù)類,用于與服務(wù)端交互
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、登陸窗體,用戶設(shè)置名稱
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;
/**
* 設(shè)置名稱窗體
*
* @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;// 標(biāo)簽
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("設(shè)置名稱");
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);// 發(fā)送消息文本框
private DefaultListModel modle = new DefaultListModel();// 用戶列表模型
private JList list = new JList(modle);// 用戶列表
private JButton btnSend = new JButton("發(fā)送");// 發(fā)送消息按鈕
private JButton btnClose = new JButton("關(guān)閉");// 關(guān)閉聊天窗口按鈕
private JFrame frame = new JFrame("ChatFrame");// 窗體界面
private String uname;// 用戶姓名
private ClientService service;// 用于與服務(wù)器交互
private boolean isRun = false;// 是否運(yùn)行
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);
//設(shè)置可關(guān)閉
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//不能改變窗體大小
frame.setResizable(false);
//聊天消息顯示區(qū)帶滾動(dòng)條
JScrollPane readScroll = new JScrollPane(readContext);
readScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
frame.add(readScroll);
//消息編輯區(qū)帶滾動(dòng)條
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);//設(shè)置為不可編輯
readContext.setLineWrap(true);// 自動(dòng)換行
writeScroll.setBounds(10, 315, 320, 100);
writeContext.setBounds(0, 0, 320, 100);
writeContext.setLineWrap(true);// 自動(dòng)換行
list.setBounds(340, 10, 140, 445);
btnSend.setBounds(150, 420, 80, 30);
btnClose.setBounds(250, 420, 80, 30);
//窗體關(guān)閉事件
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
isRun = false;
service.sendMsg("exit_" + uname);
System.exit(0);
}
});
//發(fā)送按鈕事件
btnSend.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String msg = writeContext.getText().trim();
if(msg.length() > 0){
service.sendMsg(uname + "^" + writeContext.getText());
}
//發(fā)送消息后,去掉編輯區(qū)文本,并獲得光標(biāo)焦點(diǎn)
writeContext.setText(null);
writeContext.requestFocus();
}
});
//關(guān)閉按鈕事件
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());
}
});
//消息編輯區(qū)鍵盤按鍵事件
writeContext.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
//按下鍵盤按鍵后釋放
@Override
public void keyReleased(KeyEvent e) {
//按下enter鍵發(fā)送消息
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
}
});
}
// 此線程類用于輪詢讀取服務(wù)器發(fā)送的消息
private class MsgThread extends Thread {
@Override
public void run() {
while (isRun) {
String msg = service.receiveMsg();
if (msg != null) {
//若是名稱列表數(shù)據(jù),則更新聊天窗體右邊的列表
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 {
//將聊天數(shù)據(jù)設(shè)置到聊天消息顯示區(qū)
String str = readContext.getText() + msg;
readContext.setText(str);
readContext.selectAll();//保持滾動(dòng)條在最下面
}
}
}
}
}
// 顯示界面
public void show() {
this.init();
service.sendMsg("open_" + uname);
MsgThread msgThread = new MsgThread();
msgThread.start();
this.frame.setVisible(true);
}
}
更多java相關(guān)內(nèi)容感興趣的讀者可查看本站專題:《Java面向?qū)ο蟪绦蛟O(shè)計(jì)入門與進(jìn)階教程》、《Java數(shù)據(jù)結(jié)構(gòu)與算法教程》、《Java操作DOM節(jié)點(diǎn)技巧總結(jié)》、《Java文件與目錄操作技巧匯總》和《Java緩存操作技巧匯總》
希望本文所述對(duì)大家java程序設(shè)計(jì)有所幫助。
- Java?NIO實(shí)現(xiàn)多人聊天室
- Java?NIO實(shí)現(xiàn)聊天室功能
- Java基于NIO實(shí)現(xiàn)聊天室功能
- Java實(shí)現(xiàn)NIO聊天室的示例代碼(群聊+私聊)
- java基于netty NIO的簡(jiǎn)單聊天室的實(shí)現(xiàn)
- Java NIO Selector用法詳解【含多人聊天室實(shí)例】
- 使用Java和WebSocket實(shí)現(xiàn)網(wǎng)頁聊天室實(shí)例代碼
- java聊天室的實(shí)現(xiàn)代碼
- Java基于socket實(shí)現(xiàn)簡(jiǎn)易聊天室實(shí)例
- JAVA NIO實(shí)現(xiàn)簡(jiǎn)單聊天室功能
相關(guān)文章
springboot項(xiàng)目連接不上nacos配置,報(bào)‘url‘異常問題
這篇文章主要介紹了springboot項(xiàng)目連接不上nacos配置,報(bào)‘url‘異常問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06
如何調(diào)用chatGPT實(shí)現(xiàn)代碼機(jī)器人
最近c(diǎn)hatGPT也是非常的火爆,相信大家都看到了,現(xiàn)在提供一種Java調(diào)用chatGPT的方法,我們主要通過兩個(gè)工具來實(shí)現(xiàn),一就是httpclient,二就是hutool,你覺得那種好理解你就用那種即可,今天通過本文給大家分享調(diào)用chatGPT實(shí)現(xiàn)代碼機(jī)器人,感興趣的朋友一起看看吧2022-12-12
Mybatis-plus新版本分頁失效PaginationInterceptor過時(shí)的問題
這篇文章主要介紹了Mybatis-plus新版本分頁失效,PaginationInterceptor過時(shí)問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
Springboot中靜態(tài)文件的兩種引入方式總結(jié)
這篇文章主要介紹了Springboot中靜態(tài)文件的兩種引入方式總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
Java?jar打包成exe應(yīng)用程序的詳細(xì)步驟
本文主要介紹了Java?jar打包成exe應(yīng)用程序的詳細(xì)步驟,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
Spring bean的實(shí)例化和IOC依賴注入詳解
這篇文章主要介紹了Spring bean的實(shí)例化和IOC依賴注入詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03
springboot+mybatis-plus 兩種方式打印sql語句的方法
這篇文章主要介紹了springboot+mybatis-plus 兩種方式打印sql語句的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
IO密集型任務(wù)設(shè)置線程池線程數(shù)實(shí)現(xiàn)方式
這篇文章主要介紹了IO密集型任務(wù)設(shè)置線程池線程數(shù)實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07

