Java项目之ChatRoomWebSocket/业务/

首先,创建一个聊天室的数据库:
在这里插入图片描述

create database if not exists `chatroom_websocket` default character set `utf8`;
use `chatroom_websocket`;
create table if not exists `user`
(id int primary key auto_increment comment '用户id',
  username varchar(20) unique not null comment '用户名',
  password varchar(100) not null comment 'md5加密后的密码'
);

注意修改上一篇准备工作中写的配置文件中的数据库名:

在这里插入图片描述
业务开发分为3层:

  • dao层:java操作数据库,把信息持久化到数据库中
  • service层:中间的业务层,具体处理用户业务
  • controller:调用service,获取数据,返回给客户端/从客户端获得数据,调用业务进行处理

在这里插入图片描述
先实现用户模块的注册与登录:
无论是用户模块,还是之后拓展的文件模块,操作数据库都是四步骤,不同的地方是执行的sql语句不同,所以其他三个步骤可以封装,以供继承。因此,在dao包中创建BaseDao类:

package com.bit.chatroom.dao;
//封装基础操作:数据源,获取连接,关闭资源
//所有业务都需要继承这个dao层

import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.bit.chatroom.utils.CommUtil;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import java.util.Stack;

public class Basedao {
    private static DataSource dataSource;
    static
    {Properties properties=CommUtil.loadProperties("datasource.properties");
        try {
            dataSource=DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            System.err.println("数据源加载失败");
        }
    }

//获取数据库连接,只让子类使用
    protected Connection getConnection()
    {
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
           System.err.println("获取连接失败");
        }
    return null;
    }


    //关闭资源 Connection,Statement,(ResultSet)
    protected void closeResources(Connection connection, Statement statement)
    {if(connection!=null) {
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if(statement!=null) {
        try {
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    }


    protected void closeResources(Connection connection, Statement statement, ResultSet resultSet) throws SQLException
    {closeResources(connection,statement);
    if (resultSet!=null)
    resultSet.close();}}


接下来在dao包下新建AccountDao类:

package com.bit.chatroom.dao;
//用户模块的dao,要实现用户的注册和登录

import com.bit.chatroom.entity.user;
import org.apache.commons.codec.digest.DigestUtils;

import java.sql.*;

public class AccountDao extends Basedao {
    //用户登录--查询query(select)

    public user userLogin(String username,String passsword) throws SQLException
    {Connection connection=null;
        PreparedStatement statement=null;
        ResultSet resultSet=null;
        user user2=null;
        try{
            connection=getConnection();
            String sql="select * from user where username=? and password=?";
            statement=connection.prepareStatement(sql);
            statement.setString(1,username);
            statement.setString(2,DigestUtils.md5Hex(passsword));
        resultSet=statement.executeQuery();
        if(resultSet.next())
        {user2=getUserInfo(resultSet);}

        } catch (SQLException e) {
           System.err.println("查询用户信息出错");
        }
        finally {
            closeResources(connection,statement,resultSet);
        }
        return user2;
    }


    //需要把resultSet数据表信息转换成entity实体中的User类。
        public user getUserInfo(ResultSet resultSet) throws SQLException
        {user user1=new user();
        user1.setId(resultSet.getInt("id"));
        user1.setUsername(resultSet.getString("username"));
        user1.setPassword(resultSet.getString("password"));
        return user1;}



    //用户注册--插入insert
public boolean userRegister(user user3)
{boolean issuccess=false;//不报错的话再改为true
    String userName=user3.getUsername();
String password=user3.getPassword();
Connection connection=null;
PreparedStatement statement=null;
try {
    connection = getConnection();
    String sql = "insert into user(username,password) values (?,?)";
    statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);//主键受影响行数
    statement.setString(1, userName);
    statement.setString(2, DigestUtils.md5Hex(password));
    issuccess = (statement.executeUpdate() == 1);
}
catch (Exception ex)
{System.err.println("用户注册失败");ex.printStackTrace();}
     finally
    {closeResources(connection,statement);}


return issuccess;
}


}

每写完一层都需要进行单元测试,先对用户注册进行测试:
在这里插入图片描述

 @Test
    public void userRegister() {
        user user4=new user();
        user4.setUsername("test1");
        user4.setPassword("123");
        boolean isSuccess=accountDao.userRegister(user4);
        Assert.assertEquals(true,isSuccess);

    }
}

测试通过。
user表变成了:
在这里插入图片描述
接下来测试用户登录

@Test
    public void userLogin() throws SQLException {
user user5=accountDao.userLogin("test1","123");
System.out.println(user5);
Assert.assertNotNull(user5);
}

测试结果为: 在这里插入图片描述
把前端页面放在webapp包下:
在这里插入图片描述
部署Tomcat。
在这里插入图片描述
当Application context是“/”时, 是放在根目录root下,方便访问。
运行Tomcat,来到前端页面:
在这里插入图片描述
实现注册功能,在service包下 新建AccountService类:

package com.bit.chatroom.service;

import com.bit.chatroom.dao.AccountDao;
import com.bit.chatroom.entity.user;

import java.sql.SQLException;

public class AccountService {
    private AccountDao accountDao = new AccountDao();

    //用户注册
    public boolean userLogin(String username, String password) {
        user user6 = null;
        try {
            user6 = accountDao.userLogin(username, password);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        if (user6 == null) {
            return false;
        }
        return true;
    }

    //用户登录
    public boolean userRegister(String username,String password)
    {user user7=new user();
    user7.setUsername(username);
    user7.setPassword(password);
    return accountDao.userRegister(user7);
    }
}

在controller包中新建AccountController类:

package com.bit.chatroom.controller;

import com.bit.chatroom.dao.AccountDao;
import com.bit.chatroom.service.AccountService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/doRegister")
public class AccountController extends HttpServlet {
private AccountService accountService=new AccountService();

@Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String username=request.getParameter("username");
String password=request.getParameter("password");
response.setContentType("text/html;charset=utf8");
    PrintWriter writer=response.getWriter();
if(accountService.userRegister(username,password))
    //用户注册成功,弹框提示,返回登录界面
{
    writer.println("<script>alert(\"注册成功!\");\n" +
            "window.location.href=\"/index.html\";\n" +
            "\n" +
            "</script>");
    //参照popup.html
}



    //用户登录失败,可能是用户名已存在,或是数据库存在问题,返回原页面
    else{
    writer.println("<script>alert(\"注册失败!\");\n" +
            "window.location.href=\"/registration.html\";\n" +
            "\n" +
            "</script>");
}


}

@Override
    protected void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException
{doGet(request,response);}

}


在webapp包中新建popup.html写弹框:

<!DOCTYPE HTML>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>alert("注册成功!");
window.location.href="/index.html";

</script>
</body>
</html>

(上述可通过jsp实现,不易出错。)
可实现注册:
在这里插入图片描述
引入WebSocket的知识:

WebSocket

Http协议是如何工作的:
浏览器端和服务器端,一般都是浏览器发送请求——单边通信。
服务端是被动通信。
之前的多线程聊天室有服务端和客户端,服务端可以给客户端发送信息——全双工信息。
而WebSocket是全双工通信,服务端可以主动向浏览器发送信息。仍属于TCP-IP协议 是应用层协议。

接下来实现登录:
先在 webapp包中新建websocket.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
<body>
请输入要发送的信息:
<input type="text" id="text">
<button onclick="">发送信息 </button>
<hr>
收到服务端信息为
<div id="read_from_server"></div>
<hr>
<button onclick="">关闭WebSocket</button>


<script>
    var websocket=null;
    if('WebSocket' in window){console.log("浏览器支持WebSocket!")
        //传入后端地址
        {websocket=new WebSocket("ws://localhost:8080/websocket");}
    else {console.log("浏览器不支持WebSocket");}}

    //浏览器与服务端建立连接后回调方法,打开一个连接时......
    websocket.onopen=function () { 
        console.log("webSocket连接成功");
    }

    //浏览器收到服务器信息,收到一个服务器信息时......
    websocket.onmessage=function (event) {  
        var msg=event.data;
        flushDiv(msg);
    }

    //建立websocket失败
    websocket.onerror=function () { 
        console.log("webSocket连接失败");
    }

    //客户端socket关闭
    websocket.onclose=function () {  websocket.close();}

    //将浏览器信息发送到服务端
    function senMsg2Servei() {
       var msg=document.getElementById("text").value;
    websocket.send(msg);
    }

    //刷新当前的div
    function flushDiv() {
        document.getElementById("read_from_server").innerText=msg;
    }
    
    function cloWebSocket() {
        websocket.close();
    }
    
    //主动将当前窗口的webSocket关闭
    window.onbeforeunload=function () { cloWebSocket() }
</script>
</body>
</head>
</html>

接下来,在service包中新建WebSocket类。
该类的注解需要与websocket.html中的一致:
在这里插入图片描述
在这里插入图片描述

package com.bit.chatroom.service;

import javax.servlet.annotation.WebServlet;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

//该注解可把当前类标记为WebSocket类
@ServerEndpoint("/websocket")
public class WebSocket {
//需要存储所有连接到后端的websocket
    //静态:共享,不必每次打开便签就在Tomcat中新建一个实例。不必每个窗口都有ArraySet
    private static CopyOnWriteArraySet<WebSocket> clients=new CopyOnWriteArraySet<>();
    
    //绑定当前websocket会话
private Session session;


//建立连接时调用
@OnOpen
    public void onOpen() {
    this.session = session;
    clients.add(this);
    System.out.println("有新的连接,当前的Session ID 为:" + session.getId() + ",当前聊天室共有" + clients.size() + "人");
}}

Yu
运行Tomcat,浏览器显示上图效果。
在刷新一下界面,控制台输出: 在这里插入图片描述
在这里插入图片描述 继续写websocket.html,加上@OnError、@OnMessage:

package com.bit.chatroom.service;

import javax.servlet.annotation.WebServlet;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

//该注解可把当前类标记为WebSocket类
@ServerEndpoint("/websocket")
public class WebSocket {

    //需要存储所有连接到后端的websocket
    //静态:共享,不必每次打开便签就在Tomcat中新建一个实例。不必每个窗口都有ArraySet
    private static CopyOnWriteArraySet<WebSocket> clients=new CopyOnWriteArraySet<>();

    //绑定当前websocket会话
private Session session;

//建立连接时调用
@OnOpen
    public void onOpen(Session session) {
    this.session = session;
    clients.add(this);
    System.out.println("有新的连接,当前的Session ID 为:" + session.getId() + ",当前聊天室共有" + clients.size() + "人");
}

@OnError
    public void onError(Throwable e)
{System.err.println("websocket连接失败!");
e.printStackTrace();}

@OnMessage
    public void onMessage(String msg) throws IOException {//默认是群聊
    System.out.println("浏览器发来的信息是:"+msg);
for(WebSocket webSocket:clients)
webSocket.sendMsg(msg);}

    public void sendMsg(String msg) throws IOException
    {this.session.getBasicRemote().sendText(msg);}

    @OnClose
    public void onClose()
    {System.out.println("有用户退出聊天室");
    clients.remove(this);
    System.out.println("当前聊天室还剩下"+clients.size()+"人");}
}

在这里插入图片描述
在controller包中新建LoginController类:

package com.bit.chatroom.controller;

import com.bit.chatroom.service.AccountService;
import com.bit.chatroom.utils.CommUtil;
import com.sun.deploy.net.HttpResponse;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/login")
public class LoginController extends HttpServlet {
    AccountService accountService=new AccountService();
    @Override
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException
    {String userName=request.getParameter("username");
    String password=request.getParameter("password");
    response.setContentType("text/html;charset=utf8");
    PrintWriter out=response.getWriter();
if(CommUtil.strIsnull(userName)||CommUtil.strIsnull(password))
{//登录失败,返回登录页面
    out.println("<script>\n" +
            "    alert(\"用户名或密码为空\")\n" +
            "        window.location.href=\"/index.html\";\n" +
            "</script>\n" +
            "\n");
}
//用户名和密码不为空
        if(accountService.userLogin(userName,password))
        {//登录成功,跳转到聊天页面
            

        }

else {
    //用户名或密码不对,登录失败,返回登录页面,再次登录
out.println("<script>\n" +
        "    alert(\"用户名或密码不正确\")\n" +
        "        window.location.href=\"/index.html\";\n" +
        "</script>\n" +
        "\n");
}
    }


    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {doGet(request,response);}
}

注意该类与Index.xml的一致。在index.xml中,登录表单提交的action是"/login",类的注释@WebServlet(urlPatterns)应为"/login"。
在这里插入图片描述
类与index.xml中input的属性名要对应:
String userName=request.getParameter("username");在这里插入图片描述
在CommUtils中写方法判断输入的用户名和密码字符串是否为空:

  public static boolean strIsnull(String str)
    {return str==null||str.equals("");
    //顺序不可颠倒,否则str变成空指针了。
    }

在webapp中写入模板引擎FreeMarker(鼻祖是JSP):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*">
    <title>时话时说</title>


    <link rel="stylesheet" href="assets/css/bootstrap.css"/>
    <link rel="stylesheet" href="assets/css/font-awesome.min.css"/>
    <link rel="stylesheet" href="assets/css/build.css"/>
    <link rel="stylesheet" type="text/css" href="assets/css/qq.css"/>

</head>
<body>

<div class="qqBox">
    <div class="context">
        <div class="conLeft">
            <ul id="userList">

            </ul>
        </div>
        <div class="conRight">
            <div class="Righthead">
                <div class="headName">${username}</div>
            </div>
            <div class="RightCont">
                <ul class="newsList" id="message">

                </ul>
            </div>
            <div class="RightMiddle">
                <div class="file">
                    <form id="form_photo" method="post" enctype="multipart/form-data"
                          style="width:auto;">
                        <input type="file" name="filename" id="filename" onchange="fileSelected();"
                               style="display:none;">
                    </form>
                </div>
            </div>
            <div class="RightFoot">
                    <textarea id="dope"
                              style="width: 100%;height: 100%; border: none;outline: none;padding:20px 0 0 3%;" name=""
                              rows="" cols=""></textarea>
                <button id="fasong" class="sendBtn" onclick="send()" style="border-radius: 5px">发送</button>
            </div>
        </div>
    </div>
</div>


<script src="assets/js/jquery_min.js"></script>
<script src="https://cdn.bootcss.com/jquery.form/4.2.2/jquery.form.min.js"></script>
<script type="text/javascript">
    var webSocket = null;

    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        webSocket = new WebSocket('ws://127.0.0.1:8080/websocket?username=' + '${username}');
    } else {
        alert("当前浏览器不支持WebSocket");
    }

    //连接发生错误的回调方法
    webSocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误!");
    };

    webSocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功!")
    };

    webSocket.onmessage = function (event) {

        $("#userList").html("");
        eval("var msg=" + event.data + ";");

        if (undefined != msg.content)
            setMessageInnerHTML(msg.content);

        if (undefined != msg.names) {
            $.each(msg.names, function (key, value) {
                var htmlstr = '<li>'
                        + '<div class="checkbox checkbox-success checkbox-inline">'
                        + '<input type="checkbox" class="styled" id="' + key + '" value="' + key + '" checked>'
                        + '<label for="' + key + '"></label>'
                        + '</div>'
                        + '<div class="liLeft"><img src="assets/img/robot2.jpg"/></div>'
                        + '<div class="liRight">'
                        + '<span class="intername">' + value + '</span>'
                        + '</div>'
                        + '</li>'

                $("#userList").append(htmlstr);
            })
        }
    };

    webSocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭");
    };

    window.onbeforeunload = function () {
        closeWebSocket();
    };

    function closeWebSocket() {
        webSocket.close();
    }

    function send() {
        var time = new Date().toLocaleString();
        var message = $("#dope").val();
        $("#dope").val("");

            //发送消息
            var htmlstr = '<li><div class="answerHead"><img src="assets/img/2.png"></div><div class="answers">'
                    + '[本人]' + '   ' + time + '<br/>' + message + '</div></li>';
            webSocketSend(htmlstr,message,"");
    };

    function webSocketSend(htmlstr,message,re){
        $("#message").append(htmlstr);
        var ss = $("#userList :checked");
        var to = "";
        $.each(ss, function (key, value) {
            to += value.getAttribute("value") + "-";
        });
        console.info(to);

        if (ss.size() == 0) {
            var obj = {
                msg: message,
                type: 1
            }
        } else {
            var obj = {
                to: to,
                msg: message,
                type: 2
            }
        }
        var msg = JSON.stringify(obj);
        webSocket.send(msg);

        if(re){
            $("#jsonImg").attr("src", string.data);
            // loadDiv(re);
        }

    }

    //回车键发送消息
    $(document).keypress(function (e) {

        if ((e.keyCode || e.which) == 13) {
            $("#fasong").click();
        }

    });

    //局部刷新div
    function loadDiv(sJ) {
        $("#delayImgPer").html('<img src="'+sJ+'" class="delayImg" >');
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
            var msg = '<li>'
                    + '<div class="nesHead">'
                    + '<img src="assets/img/robot.jpg">'
                    + ' </div>'
                    + '<div class="news">'
                    + innerHTML
                    + '</div>'
                    + '</li>';
        $("#message").append(msg);

    }
</script>

</body>
</html>

后端必须加载模板引擎,需要在Tomcat中配置加载ftl页面的路径——用监听器WebListerner,类似于类中的静态代码块,项目启动时监听器也执行,可以在监听器中放全局配置,各个Servlet都可使用。例如:
在chatroom包下新建一个包config,其下新建一个类FreeMarker:

获取文件/文件夹绝对路径的方式:
在这里插入图片描述
在LoginController中写当用户名和密码正确时,登录操作:

//用户名和密码不为空
        if(accountService.userLogin(userName,password))
        {//登录成功,跳转到聊天页面
//需要把用户名传到前端
//加载chat.ftl,在本类中写方法getTemplate
            Template template=getTemplate(request,"/chat,ftl");
            Map<String,String> map=new HashMap<>();
            map.put("username",userName);
            try {
                template.process(map,out);
            } catch (TemplateException e) {
                e.printStackTrace();
            }

        }

getTemplate方法:

private Template getTemplate(HttpServletRequest request,String fileName)
{Configuration cfg=(Configuration) request.getServletContext().getAttribute(FreeMarkerListener.TEMPLATE_KEY);
    try {
        return  cfg.getTemplate(fileName);
    } catch (IOException e) {
        e.printStackTrace();
    }
return  null;
}

进行测试,用户名和密码正确后,在浏览器中显示:
在这里插入图片描述
开发一般是前后端分类;
[前端发给后端的API]
JSON字符串中有参数:
群聊{“msg”:“777”,“type”:1}
私聊{“to”:“0-”,“msg”:“3333”,“type”:2}
"msg"是消息内容,"type"是群聊/私聊类型,私聊中的"to"是发送对象,"0-"是SessionId。如果是"0-1-2-"是发给0、1、2对象。

[后端发给前端的API]

public class Message2Client
{private String content;

//用户列表,所有用户在服务端保存
private Map<String,String> names; 
//<SessionID,用户名>

}

public class MessageFromClient
{private String msg;
private String type;
private String to;
}
接下来实现群聊功能,在WebSocket中写:

在entity包中新建类MessageFromClient和类Message2Client:

package com.bit.chatroom.entity;

//前端发来的
// 群聊{"msg":"777","type":1}
//私聊{"to":"0-","msg":"3333","type":2}
//需要把字符串还原成对象,再通过get、set方法操作

import lombok.Data;

@Data
public class MessageFromClient {
//聊天信息
    private String msg;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    //聊天类别,1表示群聊,2表示私聊
    private String type;

    //私聊的对象SessionID
private String to;
}
package com.bit.chatroom.entity;

import java.util.Map;

public class Message2Client {
//聊天内容和用户列表
private String content;

//服务端登录的所有列表
private Map<String,String> names;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Map<String, String> getNames() {
        return names;
    }

    public void setNames(Map<String, String> names) {
        this.names = names;
    }
}


再写用户退出的显示:

@OnClose
    public void onClose() throws IOException {//将客户端移除
            clients.remove(this);

            names.remove(session.getId());

            System.out.println("有用户下线了,用户名为" + userName);

            //这时需要给所有在线用户一个下线通知
            Message2Client message2Client = new Message2Client();
            message2Client.setContent(userName + "下线啦");
            message2Client.setName(names);

            //发送信息
            String jsonStr = CommUtil.objectToJson(message2Client);
            for (WebSocket webSocket : clients) {
                webSocket.sendMsg(jsonStr);
            }
        }

最后实现消息的发送:

在Message2Client类中写setContent的方法重载:

    public void setContent(String userName,String msg)
    {this.content=userName+"说:"+msg;}
    
发布了47 篇原创文章 · 获赞 1 · 访问量 1291

猜你喜欢

转载自blog.csdn.net/weixin_41750142/article/details/99284158