基于JAVA的聊天程序

    之前JAVA老师布置的大作业,自选标题,然后我选的是实现一个聊天软件,使用JAVAFX来做界面(跟SWING差不多,但是可以用CSS来美化界面,而且拖入式布局比较方便),实现的功能有登入注册,找回密码,更改头像,发送接收消息,查看好友资料,修改好友备注,好友是否在线的提示,消息的提示,气泡的大小自动改变,标为已读未读,清除聊天记录,删除好友,添加好友,搜索好友,好友备注,个人资料的修改查看,设置,聊天助手的提示,右键菜单等功能。运行结果如下:

(项目已上传至github,客户端:https://github.com/sundial-dreams/WeChatClient,服务端:https://github.com/sundial-dreams/WeChatServer,数据库的话也就三个表,可以看着下图建,然后将客户端代码的数据库连接部分修改一下,指向你的数据库)

登入:

 

注册:

忘记密码:

主界面:

好友资料:

 

添加好友:

个人资料:

修改个人资料:

头像:

好了,现在讲讲我的构建思路,由于界面比较多,使用我采用MVC的架构模式,包括控制模块(Controller),数据模块(Model),界面模块(View)

然后控制模块将数据和界面整合,对于数据模块,包括数据库的连接,消息的保存,好友列表的保存,登入信息的保存,数据库部分,我的个人资料包括九个属性,分别是account(账号),name(姓名),password(密码),age(年龄),sex(性别),head(头像),address(地址),label(个性标签),phone(电话号),background(主题),数据库表(使用mysql数据库)如下:

好友的话,用I_account(我的账号),Y_account(你的账号),remark(备注)来表示,数据库表如下:

然后还用了个登入表,来表示用户已登入,不可重复登入,退出时在清除掉改用户,数据库表如下:

然后就可以专门写个类来连接,操控数据库了,

package Model;

import java.sql.*;

/**
 * 
 * 数据库控制类
 * 化简数据库的操作
 */
public class DatabaseModel {

    private String url = "jdbc:mysql://localhost:3306/wechat?useUnicode=true&characterEncoding=utf-8";
    private final static String driver = "com.mysql.jdbc.Driver";
    private String userName = "root";
    private String password = "";
    private Connection connection;
    private Statement statement;//静态查询
    private PreparedStatement preparedStatement;//动态查询
    public DatabaseModel() {

    }
    /*
    链接数据库
     */
    public void connect(){
        try {
            Class.forName(driver).newInstance();
            connection = DriverManager.getConnection(url, userName, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     *
     *
     * 该方法用来执行Sql语句并返回结果集 适合需要返回结果集的查询语句 例如   execResult("select*from user where id = ? and name = ?","1","jack");
     * 用问号占位 然后传入个String数组代表要问号的值 该方法返回个结果集 即 ResultSet
     *
     * @param Sql
     * @param data
     * @return
     * @throws SQLException
     */
    public  ResultSet execResult(String Sql, String... data) throws SQLException {
        preparedStatement = connection.prepareStatement(Sql);
        for (int i = 1; i <= data.length; i++) {
            preparedStatement.setString(i, data[i - 1]);
        }
        return preparedStatement.executeQuery();
    }

    /**
     * 
     *
     * 执行Sql语句 不返回任何东西 例如exec("update user set password = ? where account = ?","password","name");
     * exec("delete from user where name = ? and account = ?","name","account");
     * exec("insert into user values(?,?,?,?,?,?,?,?,?)",1,2,3,4,5,6,7,8,9);
     * @param Sql
     * @param data
     * @throws SQLException
     */
    public void exec(String Sql, String...data) throws SQLException {

        preparedStatement = connection.prepareStatement(Sql);
        for (int i = 1; i <= data.length; i++) {
            preparedStatement.setString(i, data[i - 1]);
        }
        preparedStatement.executeUpdate();
    }

    /**
     * 执行静态SQL语句  例如exec("delete from user");
     * @param Sql
     */
    public void exec(String Sql) {
        try
        {
            preparedStatement = connection.prepareStatement(Sql);
            preparedStatement.executeUpdate();
        }catch (Exception e){
        }
    }
    /**
     * 该方法插入个数据  例如insert(表名,要插入的数据(String数组的形式))
     *
     * @param tableName
     * @param data
     * @throws SQLException
     */
    public void insert(String tableName, String... data) throws SQLException {

        String pre = "";
        for (int i = 0; i < data.length; i++) {

            if (i != data.length - 1)
                pre += "?,";
            else
                pre += "?";

        }

        String Sql = "INSERT INTO " + tableName + " VALUES(" + pre + ")";
        preparedStatement = connection.prepareStatement(Sql);
        for (int i = 1; i <= data.length; i++) {

            preparedStatement.setString(i, data[i - 1]);

        }
        preparedStatement.executeUpdate();

    }

    /**
     * 该方法删除表数据 例如delete(表名,删除时的条件(例如"id = ? AND name = ?"),传入问号代表的值)
     *
     * @param tableName
     * @param condition
     * @param data
     * @throws SQLException
     */
    public void delete(String tableName, String condition, String... data) throws SQLException {


        String Sql = "DELETE FROM " + tableName + " WHERE " + condition;


        preparedStatement = connection.prepareStatement(Sql);
        for (int i = 1; i <= data.length; i++) {

            preparedStatement.setString(i, data[i - 1]);


        }
        preparedStatement.executeUpdate();


    }

    /**
     * 跟上面那些一样
     *
     * @param tableName
     * @param target
     * @param condition
     * @param data
     * @throws SQLException
     */
    public void update(String tableName, String target, String condition, String data[]) throws SQLException {
        String Sql = "UPDATE " + tableName + " SET " + target + " WHERE " + condition;
        preparedStatement = connection.prepareStatement(Sql);

        for (int i = 1; i <= data.length; i++) {

            preparedStatement.setString(i, data[i - 1]);

        }
        preparedStatement.executeUpdate();

    }

    /**
     * @param Sql
     * @return
     * @throws SQLException
     */
    public ResultSet select(String Sql) throws SQLException {

        statement = connection.createStatement();
        return statement.executeQuery(Sql);


    }

    /**
     * @param Sql
     * @param data
     * @return
     * @throws SQLException
     */
    public ResultSet select(String Sql, String... data) throws SQLException {


        preparedStatement = connection.prepareStatement(Sql);
        for (int i = 1; i <= data.length; i++) {

            preparedStatement.setString(i, data[i - 1]);
        }
        return preparedStatement.executeQuery();

    }
    
 
    /**
     * 得到静态查询对象
     * @return
     */
    public Statement getStatement() {
        return statement;
    }

    /**
     * 得到动态查询对象
     * @return
     */
    public PreparedStatement getPreparedStatement() {
        return preparedStatement;
    }

    /**
     * 得到数据库链接对象
     * @return
     */
    public Connection getConnection() {
        return connection;
    }

    /**
     * 数据库重连
     * @param Url
     * @param UserName
     * @param Password
     * @throws ClassNotFoundException
     * @throws SQLException
     */
    public void reConnection(String Url, String UserName, String Password) throws ClassNotFoundException, SQLException {

        Class.forName(driver);
        connection = DriverManager.getConnection(Url, UserName, Password);

    }


}

然后就是要保存登入人的个人资料了,我的个人资料部分,属性比较多,可以直接写9个私有属性,也可用个map映射来保存资料,修改的时候,只需要覆盖原来的键的内容即可,私有数据直接是个Map:

private Map<String,String> usermap;//对应的属性和值

关于消息的保存如下:


public staticVector<Vector<String>>msg=newVector<>();//保存消息,意思是和第几个好友的聊天消息是什么
public staticMap<String, Vector<String>>MsgMap=newHashMap<>();//保存消息,列表中的某个好友,及和该好友的聊天消息
public staticVector<String>accountList=newVector<>();//保存好友账号
public static Map<String,Integer> msgTip = new HashMap<>();//保存消息提示,某个好友,和他的消息提示

对于气泡的大小变化,需要两个助手函数,来根据输入的文字来获得最适高度和宽度:

如下:

//获取最适高度和宽度

public class Tool {
public static double getWidth(String Msg){//获得宽度

int len = Msg.length();
double width=20;
for(int i=0;i<len;i++)
{
if(isChinese(Msg.charAt(i))){
width+=17;//一个中文字符占17个大小

}
else
{
width+=9;//其他占9个大小
}
}
//29中 17=15px/64英 8=7px
if(width<=480)//气泡最宽设置为480
{
return width;
}
else
{
return 480;
}
}
public static double getHight(String Msg){//获得高度
int len = Msg.length();
double width = 20;
double height = 40;
for(int i=0;i<len;i++){

if(isChinese(Msg.charAt(i))){
width+=17;
}
else
{
width+=9;
}
if(width>=480)
{
height+=17.4;
width=20;
}

}
return height;

}
private static final boolean isChinese(char c) {//判断是否为中文字符,及中文标点,中文字符比英文的要大
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|| ub == Character.UnicodeBlock.GENERAL_PUNCTUATION
|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) {
return true;
}
return false;
}
}

关于界面模块,我使用的是javafx做界面,好处在于可以用CSS美化,而且它的拖入式布局比较方便,然后我的每个界面都是去除了操作系统的装饰,所以就自定义了最小化,退出,窗口拖拽等,然后每一个窗口都具有这些操作,所以可以定义个抽象类window,来定义这些方法:

public abstract class window extends Stage {
    Parent root;
    private double xOffset;
    private double yOffset;

//设置图标方法
  
    public void setIcon(){
        getIcons().add(new Image(getClass().getResourceAsStream("/View/Fxml/CSS/Image/icon.png")));

    }


/**  * 窗口移动方法  */

public void move() {
 root.setOnMousePressed(event -> { 
            xOffset = getX() - event.getScreenX(); 
            yOffset = getY() - event.getScreenY();
            getRoot().setCursor(Cursor.CLOSED_HAND);
   }); 
root.setOnMouseDragged(event -> { 
            setX(event.getScreenX() + xOffset); 
            setY(event.getScreenY() + yOffset); 
   }); 
root.setOnMouseReleased(event -> { 
            root.setCursor(Cursor.DEFAULT); 
   });
  } 
/** * 抽象方法 窗口退出操作 */ 
abstract public void quit();

/** * 最小化 */ 
abstract public void minimiser(); 

/** * 获取root * * @return */
public Parent getRoot() {
 return root; 
  } 
 
/** * 选择界面元素 * * @param id * @return */
public Object $(String id) { 
return (Object) root.lookup("#" + id); 
  }
}

之后再利用window类来派生出不同的界面类,如登入界面,主界面,修改资料界面等等,这样就可以获得不同的界面了,对于界面都可以使用javafx的secen build快速做出来,添加一些CSS样式,但做出来的是Fxml文件(类似于HTML),就得利用

Parentroot= FXMLLoader.load(getClass().getResource("Fxml/Dialog.fxml"));

的方式加载个文档对象,而root代表的就是整个界面文档,可以通过一些方法来获取界面文档中的特定元素,如(Button)root.lookup("#dialog")这样获取的就是文档中id叫dialog的按钮,所以可以使用这种方式来为界面中的不同元素设置事件,或获取内容等,可以对其进行封装,封装成一个方法,如:

public Object $(String id) {
return (Object) root.lookup("#" + id);
}

,对于每一个输入框都得用正则表达式匹配看看输入是否符合规范,比如账号,规定的账号只能是中文或数字或英文,并且在1-15位,所以对于的表达式为

"^[0-9,a-z,A-Z,\\u4e00-\\u9fa5]{1,15}$",

对于聊天的内容可以用个ListView来保存,聊天的内容也相当于是一个列表,然后根据不同的消息,添加不同的Pane,对于气泡,三角形是用一张图片做上去的,而内容框就是TextArea,设置为不可用,然后通过CSS改变颜色,和三角形一样的颜色

然后就是控制模块了,控制模块要做的事就是把数据和界面整合在一起,把每个界面类,和数据操作类都做为它的私有属性,然后用界面来展示数据,完成界面的交互操作,比如登入框的按钮点击,登入框隐藏,主界面显示等,每一个功能写一个方法,比如登入功能,就写一个public void dialog()//方法

然后就是接收消息的部分,得开个线程来监听别人发来的消息,利用socket来监听服务器发来的消息,例如Socket socket = new Socket("127.0.0.1",2347)//监听本机的2347端口,然后在这个端口上有个服务端,专门往这个端口发消息,消息可以用JSON格式来传,为了简单就直接传个String,然后把消息分为几种情况:

1.#### 姓名 #### 断开连接的消息,并把该用户的消息广播给所以在线用户,如果在线用户中有这个人,就把他的状态设置为离线

2.###@ user1 user2 添加的消息,user1把user2添加为好友.user2就要将user1添加到他的聊天列表中去

3.##@@ user1 user2 删除好友的消息,user1把user2删除,user2接收到这条消息,就要将user1在其好友列表中删除

4.user1 user2 msg 一般消息,user1 给user2发送消息,并把这个消息发给user2

5.#@@@ user1 #### user1上线的消息,广播给全体在线客户

6.@@@@ user1 #### user1下线的消息,广播给全体在线客户

通过这些类型的消息,客户端解析服务端过来的消息,根据不同的消息干不同的事。

对于服务端,使用Map<String,Socket>的方式保存客户端的Socket,键是账号,账号是唯一的,然后也得开个线程来处理消息,每个线程处理一个客户端Socket的方式,然后对应的发消息,user1 user2 Msg的消息就可以发送了,比如:

public void sendMsg(String from,String to,String Msg) throws IOException {
    for(Map.Entry<String ,ChatSocket> entry:map.entrySet()){
               ChatSocket socket = entry.getValue();
               if(entry.getKey().equals(to))//找到要发给谁
                    socket.out(from+" "+to+" "+Msg);//然后把消息发出去即可。

    }
}

就可以了。

如果上面的没看太懂,可以参考我的工程,客户端:JavaFx WeChat聊天软件客户端

服务端:Java WeChat聊天软件服务端

(项目已上传到GitHub,如果觉得不错,欢迎点赞,客户端:https://github.com/sundial-dreams/WeChatClient,服务端:https://github.com/sundial-dreams/WeChatServer,数据库的话也就三个表,可以看着下图建,然后将客户端代码的数据库连接部分修改一下,指向你的数据)

本人现在上大二,学java没多久,如过有什么说得不对的地方,请多多包含。

猜你喜欢

转载自blog.csdn.net/daydream13580130043/article/details/79171256
今日推荐