功能概述
用户上线下线,登录和注册功能,群聊和私聊功能,统计聊天室在线人数功能。
文件说明
LoginController:登陆界面的控制器
LoginLauch:整个程序的启动器
PrivateChatController:私聊控制器
PrivateChatListener:私聊监听器
RegisterController:注册控制器
UserListController:用户列表控制器
Listener:群聊监听器
软件设计
客户端
用户登录和注册
UI设计
开始采用的是swing设计GUI,后来发觉javafx的功能比较强大,然后改用javafx设计。
javafx设计
swing设计
注册按键监听器设置:当点击时加载注册界面与数据库交互。
登录按键监听器设置:检查用户名和密码是否正确,如果正确则打开聊天界面(这个界面是群聊)和用户列表,并开启一个群聊监听器Listener。
代码片段
package com.jchat.Controller;
import com.Users.User;
import com.Utils.UserDao;
import com.jchat.demo.Listener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import javax.swing.*;
public class LoginController {
@FXML
private TextField usernameField;
@FXML
private Button registerButton;
@FXML
private Button loginButton;
@FXML
private PasswordField passwordField;
@FXML
private Label usernameLabel;
@FXML
private Label pleaseLogin;
@FXML
private AnchorPane mainPane;
@FXML
private Label passwordLabel;
private PrivateChatController privateChatController;
private UserListController userListController;
private static LoginController instance;
@FXML
void Login(ActionEvent event) {
User user=new User();
user.setUserName(usernameField.getText());
user.setPassword(passwordField.getText());
UserDao dao=new UserDao();
User user1=dao.login(user);
if(user1==null){
JOptionPane.showMessageDialog(null,"该用户不存在");
passwordField.setText("");
}
else if(user.getUserName().equals(user1.getUserName())&&user.getPassword().equals(user1.getPassword())) {
JOptionPane.showMessageDialog(null, "登录成功");
try {
FXMLLoader fxmlLoader=new FXMLLoader(getClass().getResource("UserListView.fxml"));
Parent window=(AnchorPane)fxmlLoader.load();
userListController=fxmlLoader.<UserListController>getController();
FXMLLoader fxmlLoader1=new FXMLLoader(getClass().getResource("PrivateChatView.fxml"));
Parent window1=(AnchorPane)fxmlLoader1.load();
privateChatController=fxmlLoader1.<PrivateChatController>getController();
privateChatController.setUsernameLabel(user1.getUserName());
Listener listener = new Listener("localhost", 1234, user1.getUserName(),privateChatController ,userListController);
Thread x = new Thread(listener);
x.start();
Scene scene=new Scene(window);
Stage stage1=new Stage();
Stage stage2=new Stage();
stage1.setScene(scene);
Scene scene1=new Scene(window1);
stage2.setScene(scene1);
stage1.show();
stage2.show();
LoginLaunch.getPrimaryStage().setIconified(true);
LoginLaunch.getPrimaryStage().close();
} catch (Exception e){
e.printStackTrace();
}
}
}
@FXML
void Register(ActionEvent event) {
try {
//一定需要使用try-catch,不然编译器不会让你过的,Trust me!
Parent anotherRoot = FXMLLoader.load(getClass().getResource("RegisterController.fxml"));
System.out.println(getClass().getResource("RegisterController.fxml"));
Stage anotherStage = new Stage();
anotherStage.setTitle("Another Window Triggered by Clicking");
anotherStage.setScene(new Scene(anotherRoot, 304, 427));
anotherStage.show();
} catch (Exception e){
e.printStackTrace();
}
}
public LoginController() {
instance = this;
}
public static LoginController getInstance() {
return instance;
}
}
数据库设计
表设计
昵称、用户名、密码三个字段。
与数据库交互主要用到了druid数据库连接池技术
代码片段
UserDao.java
package com.Utils;
import com.Users.User;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.swing.*;
//操作数据库中的user表的对象
public class UserDao {
//创建jdbcTemplate对象,用来操作数据库
JdbcTemplate template=new JdbcTemplate(JDBCUtils.getSourse());
public User login(User loginUser){
try{
String sql="select * from users where username = ?";
User user =template.queryForObject(
sql,
new BeanPropertyRowMapper<User>(User.class),
loginUser.getUserName()
);
if(user.getPassword().equals(loginUser.getPassword()))
return user;
else {
JOptionPane.showMessageDialog(null,"密码错误,请重新输入");
}
}catch (DataAccessException e){
e.printStackTrace();
return null;
}
return null;
}
}
JDBCUtils.java
package com.Utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class JDBCUtils {
private static DataSource ds;
static {
//加载配置文件
Properties pro=new Properties();
try{
pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
//初始化连接池对象
ds= DruidDataSourceFactory.createDataSource(pro);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//获取连接池对象
public static DataSource getSourse(){
return ds;
}
//获取连接
public static Connection getConnection() throws SQLException{
return ds.getConnection();
}
}
配置文件druid.properties(必须是这个名字)
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jchat?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
username=root
password=root
maxActive=5
maxWait=3000
聊天界面
一个用户列表,一个对话框。
消息机制
登录成功后进入聊天界面。
群聊监听器Listenner会调用connect方法给服务器端发送一个消息,该消息的类型为CONNECTED,表示连接成功。服务器端收到该消息后,将该用户的用户名和输出流放入一个Map中,然后发送一个NOTIFACAITION类型的消息广播给所有成员,提示所有人有用户加入,并发送一个名为SERVER的消息,参数中包含用户列表和用户数量,广播给所有成员,让所有成员更新自己的客户端数据。
send按键监听发送事件。如果是群聊,本人的用户名和文本框中的消息构造成一个消息类型为GROUPCHAT的消息类发送给服务器,如果是私聊,则消息类型为PRIVATECHAT,还有一个目的地(发送给谁)成员变量。服务器收到该消息后判断该消息的类型,如果是私聊,则根据该消息的目的地从私聊的Map中找对应的输出流并将消息对象中的消息内容从该输出流发送出去,同时给自己发送一份该消息(显示在自己的聊天框中)。如果是群聊,则从群聊Map中的每一个输出流发送出去。
服务器
消息类
package com.Users;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
public class Message implements Serializable {
private String destination;
private String name;
private MessageType type;
private String msg;
private int count;
private ArrayList<User> list;
private ArrayList<User> users;
private Status status;
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public String getMsg(){
return msg;
}
public void setMsg(String msg){
this.msg=msg;
}
public ArrayList<User> getUserList() {
return list;
}
public void setUserList(ArrayList<User> list) {
this.list = list;
}
public ArrayList<User> getUsers() {
return users;
}
public void setUsers(ArrayList<User> users) {
this.users = users;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public MessageType getType() {
return type;
}
public void setType(MessageType type) {
this.type = type;
}
public void setUserList(HashMap<String,User> userList){
this.list=new ArrayList<>(userList.values());
}
public void setOnlineCount(int count){
this.count=count;
}
public int getOnlineCount() {
return count;
}
public String getDestination() {
return destination;
}
public void setDestination(String destination) {
this.destination = destination;
}
@Override
public String toString() {
return "Message{" +
"destination='" + destination + '\'' +
", name='" + name + '\'' +
", type=" + type +
", msg='" + msg + '\'' +
", count=" + count +
'}';
}
}
ServerSocket建立连接
Handler处理异步消息
消息类型
通过再服务器和客户端之间传递这些类型的消息进行实时的通讯。
public enum MessageType {
DISCONNECTED, //断开连接
CONNECTED, //建立连接
STATUS, //用户状态
SERVER, //服务器
NOTIFICATION, //通知
HASCONNECTED, //连接建立完成
GROUPCHAT, //群聊
PRIVATECHAT, //私聊
PRIVATECONNECT //私聊的连接建立
}
代码片段
package com.jchat.Server;
import com.Users.Message;
import com.Users.MessageType;
import com.Users.Status;
import com.Users.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class Server {
private static final int PORT=1234;
private static final HashMap<String,User> names=new HashMap<>();
private static HashMap<String,ObjectOutputStream> writers=new HashMap<>();
private static HashMap<String,ObjectOutputStream> privateChatWriters=new HashMap<>();
private static ArrayList<User> users=new ArrayList<>(); //在线用户列表
static Logger logger= LoggerFactory.getLogger(Server.class);
public static void main(String[] args) throws IOException {
logger.info("The chat server is running");
ServerSocket serverSocket=new ServerSocket(PORT);
try{
while(true){
new Handler(serverSocket.accept()).start();
}
}catch (Exception e){
e.printStackTrace();
}finally {
serverSocket.close();
}
}
private static class Handler extends Thread{
private String name;
private Socket socket;
private Logger logger=LoggerFactory.getLogger(Handler.class);
private User user;
private ObjectInputStream objectInputStream;
private OutputStream outputStream;
private ObjectOutputStream objectOutputStream;
private InputStream inputStream;
private MessageType messageType;
public Handler(Socket socket) throws IOException{
this.socket=socket;
}
public void run(){
logger.info("Attempting to connect a user...");
try{
inputStream=socket.getInputStream();
objectInputStream=new ObjectInputStream(inputStream);
outputStream=socket.getOutputStream();
objectOutputStream=new ObjectOutputStream(outputStream);
Message firstMessage=(Message) objectInputStream.readObject();
System.out.println("firstMessage: "+firstMessage);
if(firstMessage.getType()==MessageType.PRIVATECONNECT){
privateChatWriters.put(firstMessage.getName(),objectOutputStream);
messageType=MessageType.PRIVATECHAT;
}
else if(firstMessage.getType()!=MessageType.PRIVATECHAT){
checkDuplicateUsername(firstMessage);
messageType=MessageType.GROUPCHAT;
writers.put(firstMessage.getName(),objectOutputStream);
sendNotification(firstMessage);
addToList();
}
while(socket.isConnected()){
Message inputmsg=(Message)objectInputStream.readObject();
System.out.println("inputmsg:"+inputmsg);
logger.info(inputmsg.getType() + " - " + inputmsg.getName() + ": " + inputmsg.getMsg());
switch (inputmsg.getType()) {
case CONNECTED:
addToList();
break;
case STATUS:
changeStatus(inputmsg);
break;
case GROUPCHAT:
sendToGroup(inputmsg);
break;
case PRIVATECHAT:
sendToPerson(inputmsg, inputmsg.getDestination());
sendToPerson(inputmsg, inputmsg.getName());
break;
case PRIVATECONNECT:
privateConnected();
break;
}
}
} catch (SocketException socketException) {
logger.error("Socket Exception for user " + name);
} catch (DuplicateUsernameException duplicateException){
logger.error("Duplicate Username : " + name);
} catch (Exception e){
logger.error("Exception in run() method for user: " + name, e);
} finally {
closedConnections();
}
}
private void sendToGroup(Message msg) throws IOException {
for(Map.Entry<String,ObjectOutputStream> mem:writers.entrySet()){
mem.getValue().writeObject(msg);
mem.getValue().reset();
}
}
private void sendToPerson(Message msg,String destination) throws IOException {
if(privateChatWriters.get(destination)==null){
writers.get(destination).writeObject(msg);
writers.get(destination).reset();
}
else{
privateChatWriters.get(destination).writeObject(msg);
privateChatWriters.get(destination).reset();
}
}
//改变用户状态消息
private Message changeStatus(Message inputmsg) throws IOException{
logger.debug(inputmsg.getName() + " has changed status to " + inputmsg.getStatus());
Message msg = new Message();
msg.setName(user.getUserName());
msg.setType(MessageType.STATUS);
msg.setMsg("");
User userObj = names.get(name);
userObj.setStatus(inputmsg.getStatus());
sendToGroup(msg);
return msg;
}
//用户名重复消息
private synchronized void checkDuplicateUsername(Message firstMessage)throws DuplicateUsernameException{
logger.info(firstMessage.getName()+"is trying to connect");
if(!names.containsKey(firstMessage.getName())){
this.name=firstMessage.getName();
User user=new User();
user.setUserName(firstMessage.getName());
user.setStatus(Status.ONLINE);
users.add(user);
names.put(name,user);
logger.info(name+" has been added to the list");
}
else {
logger.error(firstMessage.getName()+ " is already connected");
throw new DuplicateUsernameException(firstMessage.getName()+" is already connected");
}
}
//通知消息
private Message sendNotification(Message firstMessage) throws IOException{
Message msg=new Message();
msg.setMsg(" has joined the chat");
msg.setType(MessageType.NOTIFICATION);
msg.setName(firstMessage.getName());
sendToGroup(msg);
return msg;
}
//用户离开消息
private Message removeFromList() throws IOException{
logger.debug("removeFromList() method Enter");
Message msg=new Message();
msg.setMsg(" has left the chat.");
msg.setType(MessageType.DISCONNECTED);
msg.setName("SERVER");
msg.setUserList(names);
msg.setUsers(users);
msg.setOnlineCount(names.size());
sendToGroup(msg);
logger.debug("removeFromList() method Exit");
return msg;
}
private Message privateConnected() throws IOException {
Message msg=new Message();
msg.setType(MessageType.PRIVATECHAT);
msg.setMsg("you have connected each other");
sendToPerson(msg,msg.getName());
return msg;
}
//用户加入列表消息
private Message addToList() throws IOException{
Message msg=new Message();
msg.setMsg("Welcome, You have now joined the server!Enjoy chatting!");
msg.setType(MessageType.CONNECTED);
msg.setName("SERVER");
msg.setUsers(users);
msg.setUserList(names);
msg.setOnlineCount(names.size());
sendToGroup(msg);
return msg;
}
//关闭客户端与服务器的连接
private synchronized void closedConnections(){
logger.debug("closeConnections() method Enter");
logger.info("HashMap names:"+names.size()+"writers:"+writers.size()+" userList size:"+users.size());
if(name!=null){
names.remove(name);
logger.info("User: "+name+" has been removed");
}
if (user != null){
users.remove(user);
logger.info("User object: " + user + " has been removed!");
}
if(objectOutputStream!=null){
writers.remove(objectOutputStream);
logger.info("Writer object: " + user + " has been removed!");
}
if(inputStream!=null){
try{
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(outputStream!=null){
try{
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(objectOutputStream!=null){
try{
objectOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(objectInputStream!=null){
try{
objectInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(writers.get(name)==null&&privateChatWriters.get(name)==null){
try{
removeFromList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
logger.info("HashMap names: " + names.size() + " writers:" + writers.size() + " usersList size:" + users.size());
logger.debug("closeConnections() method Exit");
}
}
}
测试
启动server
注册和登录
开启一个客户端,点击注册
可以看到后台数据库中多了一个记录,同理注册小红和小刚。
然后登录小明、小红、小刚的账号
群聊
私聊
左上角的聊天框是群聊的
1.小名打开和小红的聊天框发送小红你好,此时,小红并没有建立和小明私聊的对话框,所以在群聊框中收到一条私聊的消息,这个消息只有小明和小红能看到。
2.小红看到后打开和小明的聊天框,然后回复小明。这是消息会显示在二者的私聊对话框中(这里有一个bug就是会弹出两个私聊的聊天框,只有最上层的聊天框能显示消息,下层的聊天框虽然不能显示,但是也能发送消息。由于时间紧迫,先交作业,后期再修bug)
总结
有很多bug还没处理,比如用户名重复、重复登录等问题,
还有很多设计不好的地方,比如聊天框中没有显示对方的信息等。