1.项目简介
基于服务器的多用户聊天室应用软件。聊天室最大用户容量为30,每一个用户都是一个独立的线程。
2.开发环境
NetBeans IDE8.2、JDK1.8
3.项目具体功能
服务器端:
1)服务器端界面窗口
A.服务器端IP地址输入框:服务器地址输入功能
B.端口号输入框:端口号输入功能
C.启动服务器按钮:启动服务器功能
D.聊天室大厅消息显示文本域:聊天消息显示功能
E.聊天室在线用户显示文本域:在线用户显示功能
客户端:
1)登录功能
1.1 聊天室登录界面窗口
A.用户名输入框:用户名称输入功能
B.密码输入框:用户密码输入功能
C.服务器地址输入框:服务器地址输入功能
D.端口号输入框:端口号输入功能
E.进入聊天室按钮:登录进入聊天室功能
F.退出聊天室按钮:退出聊天室功能
2)聊天室功能
2.1 多用户聊天室主界面
A.聊天室大厅消息显示文本域:多用户聊天消息显示
B.聊天室在线用户显示文本域:在线用户情况显示
C.文本信息输入文本框:文本信息输入功能
D.发送功能按钮:文本信息发送功能
E.私聊功能选择框:单人聊天模式转换功能
F.清除聊天记录功能按钮:清除聊天记录功能
G.退出聊天室:退出聊天室功能
2.2 私聊界面
A.聊天信息文本域:显示聊天信息以及对方用户名
B.文本输入框:文本信息输入功能
C.发送按钮:文本信息发送功能
界面是使用NetBeans(软件下载可点)
直接新建一个Jframe项目就可以开始制作你自己的界面风格,对小白来说是一款很友好的集成开发软件。
具体的操作就是拖动你要的组件,如下图,从右边的组件面板中拖到JFrame窗体中,调节好大小,然后修改组件的变量名称,为组件添加监听事件,剩下的就是代码的编写了。
4.运行结果
服务器端界面:
客户端界面:
图 登录界面:
图 聊天室主界面:
图 私聊界面:
图 三用户聊天室效果:
5.其他功能实现运行结果
1)在线用户显示功能:
当有用户退出聊天室时,在线用户总数与在线用户列表都会同步更新,如图:
2)清除聊天记录功能:
图 清除前:
图 清除后:
3)私聊功能:
图 用户哈哈与用户康的聊天窗口:
4)退出聊天室功能:
图 服务器端与客户机端的“退出消息”提示:
5)登录错误提示:
当聊天室达到最大用户容量30时,客户机登录出错,错误提示如下:
当用户登录时,用户名或密码为空时错误提示如下:
在本次项目中定义了DatagramSocket类接收和发送数据报,实现UDP协议服务,DatagramSocket本身不维护连接状态,不能产生I/O流,Java使用DatagramPacket代表数据报文,DatagramSocket接收和发送的数据都是通过DatagramPacket报文对象完成的。如下图所示:
图 基于UDP协议的客户机与服务器通信逻辑
6.具体实现代码
登录页面:
LoginUI.java:
public class LoginUI extends javax.swing.JDialog {
private static DatagramSocket clientSocket; //客户机套接字
private static Message msg; //消息对象
private byte[] data = new byte[8096]; //8KB数组
/**
* Creates new form LoginUI
*/
public LoginUI(java.awt.Frame parent, boolean modal) {
super(parent, modal);
initComponents();
}
/*
**
函数实现部分......
*/
}
“进入聊天室”按钮的监听事件:
private void btnLoginActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
try{
String id = txtUserId.getText();
String password = String.valueOf(txtPassword.getPassword());
if(id.equals("") || password.equals("")){
JOptionPane.showMessageDialog(null, "账号或密码不能为空!",
"错误提示",JOptionPane.ERROR_MESSAGE);
return;
}
//获取服务器地址和端口
String remoteName = txtRemoteName.getText();
InetAddress remoteAddr = InetAddress.getByName(remoteName);
int remotePort = Integer.parseInt(txtRemotePort.getText());
//创建UDP套接字
DatagramSocket clientSocket = new DatagramSocket();
clientSocket.setSoTimeout(3000); //设置超时时间
//构建用户登录消息
Message msg = new Message();
msg.setUserId(id); //登录名
msg.setPassword(password); //密码
msg.setType("M_LOGIN"); //登录消息类型
msg.setToAddr(remoteAddr); //目标主机地址
msg.setToPort(remotePort); //目标主机端口
byte[] data = Translate.ObjectToByte(msg); //消息对象序列化为字节数组
//定义登录报文
DatagramPacket packet = new DatagramPacket(data, data.length, remoteAddr, remotePort);
//发送登录报文
clientSocket.send(packet);
//接收服务器回送的报文
DatagramPacket backPacket = new DatagramPacket(data, data.length);
clientSocket.receive(backPacket);
clientSocket.setSoTimeout(0); //取消超时时间
Message backMsg = (Message)Translate.ByteToObject(data);
//处理登录结果
if(backMsg.getType().equalsIgnoreCase("M_SUCCESS")){ //登录成功
this.dispose(); //关闭登录对话框
ClientUI client = new ClientUI(clientSocket,msg); //创建客户机界面
client.setTitle(msg.getUserId()); //设置标题
client.setVisible(true); //显示会话窗体
}else{ //登录失败
JOptionPane.showMessageDialog(null, "用户 ID 或密码错误!\n",
"登录失败",JOptionPane.ERROR_MESSAGE);
}
}catch(IOException ex){
JOptionPane.showMessageDialog(null, ex.getMessage(),
"登录错误",JOptionPane.ERROR_MESSAGE);
}//end try
}
主函数:
public static void main(String args[]) {
/* Create and display the dialog */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
LoginUI dialog = new LoginUI(new javax.swing.JFrame(), true);
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
System.exit(0);
}
});
dialog.setVisible(true);
}
});
}
客户端:
ClientUI.java:
public class ClientUI extends javax.swing.JFrame {
private static DatagramSocket clientSocket; //客户机套接字
private static Message msg; //消息对象
private byte[] data = new byte[8096]; //8KB数组
/**
* Creates new form ChatClient
*/
public ClientUI() {
initComponents();
}
public ClientUI(DatagramSocket socket, Message msg) {
this(); //调用无参数构造函数,初始化界面
clientSocket = socket; //初始化会话套接字
this.msg = msg; //登录消息
//创建客户机消息接收和处理线程
Thread recvThread = new ReceiveMessage(clientSocket,this);
recvThread.start(); //启动消息接收线程
}
/*
**
函数实现部分......
*/
}
“发送”按钮的监听事件:
private void btnSendActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
try{
msg.setText(txtInput.getText()); //获取输入的文本
msg.setType("M_MSG"); //普通会话消息
data = Translate.ObjectToByte(msg); //消息对象序列化
//构建发送报文
DatagramPacket packet = new DatagramPacket(
data, data.length, msg.getToAddr(), msg.getToPort());
clientSocket.send(packet); //发送
txtInput.setText(""); //清空输入框
} catch (IOException ex) {
JOptionPane.showMessageDialog(null, ex.getMessage(),
"错误提示",JOptionPane.ERROR_MESSAGE);
}
}
实现按下回车键,也能发送消息:
private void txtInputActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
btnSendActionPerformed(evt); //直接调用btnSpeak按钮的响应函数即可
}
单击窗体“关闭”按钮,关闭窗体之前发送下线消息:
/**
* @param evt 窗体事件
*/
private void formWindowClosing(java.awt.event.WindowEvent evt) {
// TODO add your handling code here:
try{
msg.setType("M_QUIT"); //消息类型
msg.setText(null);
data = Translate.ObjectToByte(msg); //消息对象序列化
//构建发送
DatagramPacket packet = new DatagramPacket(
data, data.length, msg.getToAddr(), msg.getToPort());
clientSocket.send(packet); //发送
}catch (IOException ex){}
clientSocket.close(); //关闭套接字
}
JList用户列表“双击”事件,进入私聊界面:
private void userListMouseClicked(java.awt.event.MouseEvent evt) {
// TODO add your handling code here:
if(evt.getClickCount() == 2){
//txtArea.append("确定要进行私聊吗?\n");
//this.dispose(); //关闭登录对话框
PrivateChatUI privateChat = new PrivateChatUI(); //创建客户机界面
privateChat.setTitle(msg.getUserId() + "与" + userList.getSelectedValue() + "的聊天窗口"); //设置标题
privateChat.setVisible(true); //显示会话窗体
}
}
主函数:
public static void main(String args[]) {
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new ClientUI(clientSocket, msg).setVisible(true);
}
});
}
服务器端:
ServerUI.java:
public class ServerUI extends javax.swing.JFrame {
private ServerSocketChannel listenChannel = null; //侦听通道
private Selector selector; //选择器
/**
* Creates new form ServerUI
*/
public ServerUI() {
initComponents();
}
/*
**
函数实现部分......
*/
}
启动服务器过程:
private void btnStartActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
//启动服务器
btnStart.setEnabled(false);
try{
//构建工作地址
String hostName = txtHostName.getText();
int hostPort = Integer.parseInt(txtHostPort.getText());
//构建套接字格式的地址
SocketAddress serverAddr = new InetSocketAddress(
InetAddress.getByName(hostName),hostPort);
selector = Selector.open(); //创建选择器
listenChannel = ServerSocketChannel.open(); //创建侦听通道
listenChannel.socket().bind(serverAddr); //侦听通道绑定工作地址
listenChannel.configureBlocking(false); //侦听通道工作于非阻塞模式
//侦听通道注册到选择器,设置OP_ACCEPT标志位
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
txtArea.append("服务器开始侦听客户机连接...\n");
} catch (IOException ex) {}
//服务器轮询线程
new Thread(new Runnable() {
@Override
public void run() {
try{
while(true){ //轮询各通道状态,处理连接和会话
int nKeys = selector.select(); //查询令牌集合
if(nKeys == 0) continue; //没有就绪令牌,越过下面步骤,开始新一轮查询
Set <SelectionKey> readyKeys = selector.selectedKeys();//返回就绪令牌集合
Iterator <SelectionKey> it = readyKeys.iterator(); //就绪令牌集合迭代器
while(it.hasNext()){ //遍历就绪令牌集合
SelectionKey key = it.next(); //取出下一个令牌
if(key.isAcceptable()){ //如果是连接事件
doAccept(key); //简历连接,创建新会话通道
}else if(key.isReadable()){ //如果是读数据事件
doRead(key); //接收数据
}
it.remove(); //从就绪集合中删除处理过的令牌
}//end while
}//end while
}catch(IOException ex){}//end try catch
}//end run()
}).start();
}
主函数:
public static void main(String args[]) {
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new ServerUI().setVisible(true);
}
});
}
关于项目中的一些具体细节,可以看看代码中的注释。
写了比较久,此多用户聊天室应用软件程序还有很多不完善的地方,后面会再增添一些功能,例如:文件传输,保存聊天记录,一对一聊天,服务器端关闭服务器并通知客户端退出,采用数据库检验用户账号和密码的正确性。
不足之处,欢迎各位大佬指正。