基于websocket的聊天室系统

Rookkieee 2022-01-18 16:57:31

1.项目简介

该系统实现了一个简单的聊天室,包含用户注册和登录、消息群发和私发、上线通知等功能;通过websocket实现消息群发;通过mysql存储用户信息,持久层使用mybatis框架;前端使用html、css、JavaScript以及FreeMarker模板引擎实现;最终部署在tomcat服务器上。

2.主要技术介绍

1)websocket

HTML5 WebSocket设计出来的目的就是取代轮询和长连接,使客户端浏览器具备像C/S框架下桌面系统的即时通讯能力,实现了浏览器和服务器全双工通信,建立在TCP之上,虽然WebSocket和HTTP一样通过TCP来传输数据,但WebSocket可以主动的向对方发送或接收数据,就像Socket一样;并且WebSocket需要类似TCP的客户端和服务端通过握手连接,连接成功后才能互相通信。

java websocket实现(主要函数实现):

//有连接时的触发函数
@OnOpen
public void onOpen(Session session) throws IOException{ } 

//连接关闭时的调用方法。
@OnClose
public void onClose(){ }

//收到消息时调用的函数,其中Session是每个websocket特有的数据成员
@OnMessage
public void onMessage(String message, Session session) { }

//发生意外错误时调用的函数
@OnError
public void onError(Session session, Throwable error) { }

 2)FreeMarker

FreeMarker是一个使用纯Java编写的基于模板生成文本输出的通用工具,通常它的文件都是.ftl结尾。FreeMarker最初被用在MVC模式的Web开发框架中生成HTML页面,但是它的功能可不仅仅可以用到Web开发生成模板文件,它也可以用于非Web应用环境中,如:生成Java代码、xml、html等。它是一款轻量级的模板文件,不像JSP那样是Servlet要嵌入到应用程序中。该系统中使用它构建了聊天室,来实现与后端的信息交互。

3)mybatis

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。使用它可以很方便地进行数据库的增删查改。

3.主要代码实现

1)该系统使用maven管理,首先新建一个maven项目,然后导入相关依赖包。

    <!-- websocket-->
    <dependency>
      <groupId>javax.websocket</groupId>
      <artifactId>javax.websocket-api</artifactId>
      <version>1.1</version>
      <scope>provided</scope>
    </dependency>

    <!-- freemarker模版引擎 -->
    <dependency>
      <groupId>org.freemarker</groupId>
      <artifactId>freemarker</artifactId>
      <version>2.3.28</version>
    </dependency>

    <!-- mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.3</version>

    <!-- mysql -->
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.26</version>
    </dependency>

2) 实现用户的登录注册功能。

在pojo包中声明user实体类:(省略了get和set方法)

public class User {
    private int id;
    private String username;
    private String password;

    public User(String usernamem, String password){
        this.username = usernamem;
        this.password = password;
    }
}

在resources包下新建mybatis-config.xml,定义数据库的连接参数;在mapper包中新建UserMapper接口,声明数据库的查询和插入接口方法;在resources包下新建UserMapper.xml,实现接口对应方法的sql语句。(注:UserMapper.xml应与相应接口同名,并且需要在相同的包路径下)。

// UserMapper.xml
<mapper namespace="com.chatroom.mapper.UserMapper">
    <select id="selectUser" resultType="com.chatroom.pojo.User">
        select * from user where username = #{username} and password = #{password};
    </select>

    <insert id="addUser">
        insert into user (id, username, password) values (null, #{username}, #{password});
    </insert>
</mapper>


// UserMapper 接口
public interface UserMapper {
    User selectUser(@Param("username") String username, @Param("password") String password);

    void addUser(User user);
}

在service包下新建UserService类实现用户查询和插入:

public class UserService {
    SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();

    public User selectUser(String username, String pasword){
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User user = mapper.selectUser(username, pasword);
        sqlSession.close();

        return user;
    }

    public boolean addUser(User user){
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        mapper.addUser(user);
        sqlSession.close();

        return true;
    }
}

最后在web包下新建LoginServlet和RegisterServlet来实现登录和注册:

登录时将用户呢称传递到前端模板

@WebServlet(urlPatterns = "/login")
public class LoginServlet extends HttpServlet {
    private UserService userService = new UserService();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 要和表单中的参数一致
        String userName = req.getParameter("username");
        String password = req.getParameter("password");
        resp.setContentType("text/html;charset=utf8");
        PrintWriter out = resp.getWriter();
        if(CommUtils.strIsNull(userName) || CommUtils.strIsNull(password)) {
            // 登陆失败后返回登录页面
            out.println("<script>\n" +
                    "    alert(\"用户名或密码为空!\");\n" +
                    "    window.location.href = \"/index.html\";\n" +
                    "</script>");
        }
        // 到数据库中对比 username 和 password是否一致
        if(userService.selectUser(userName,password) != null) {
            // 登陆成功,跳转到聊天页面
            Template template = getTemplate(req,"/chat.ftl");
            Map<String,String> map = new HashMap<>();
            map.put("username", userName);
            try {
                //通过 map集合传入到前端
                template.process(map, out);
            } catch (TemplateException e) {
                e.printStackTrace();
            }
        }else {
            // 登陆失败,跳转到登录页面
            out.println("<script>\n" +
                    "    alert(\"用户名或密码不正确!\");\n" +
                    "    window.location.href = \"/index.html\";\n" +
                    "</script>");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);

    }

    // 加载 chat.ftl
    private Template getTemplate(HttpServletRequest req,String fileName) {
        // 获取设置的配置
        Configuration cfg = (Configuration)
                req.getServletContext().getAttribute(FreeMarkerListener.TEMPLATE_KEY);

        try {
            return cfg.getTemplate(fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
@WebServlet(urlPatterns = {"/doRegister"})
public class RegisterServlet extends HttpServlet {
    private UserService userService = new UserService();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 获取用户输入的 username,password(在表单中提交的值)
        String userName = req.getParameter("username");
        String password = req.getParameter("password");
        resp.setContentType("text/html;charset=utf8");
        //获取页面输出流
        PrintWriter writer = resp.getWriter();

        User user = new User(userName, password);
        user.setId(100);
        if(userService.addUser(user)) {
            // 用户注册成功
            //提示返回登录页面
            writer.println("<script>\n" +
                    "    alert(\"注册成功\");\n" +
                    "    window.location.href =\"/index.html\";\n" +
                    "</script>");

        }else {
            // 提示注册失败,保留原页面(还停留在注册页面)
            writer.println("<script>\n" +
                    "    alert(\"注册失败!!!\");\n" +
                    "    window.location.href =\"/registration.html\";\n" +
                    "</script>");
        }
    }

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

3)实现消息发送功能:

在pojo包中定义MessageFromClient以及MessageToClient消息实体类:

public class MessageFromClient {
    // 聊天信息
    private String msg;
    // 聊天类别: 1 表示群聊   2 表示私聊
    private String type;
    //  私聊对象的 SessionID, 根据SessionID到服务端找对应的 WebSocket
    private String to;
}

public class MessageToClient {
    // 聊天内容
    private String content;

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

在service中实现websocket类:(主要为实现上述websocket四个方法)

@ServerEndpoint("/websocket")  // 把当前类标记为 Websocket类
public class WebSocket {
    // 存储所有连接到后端的 websocket
    private static CopyOnWriteArraySet<WebSocket> clients = new CopyOnWriteArraySet<>();
    // 缓存所用的用户列表
    private static Map<String,String> namespaces = new ConcurrentHashMap<>();
    // 绑定当前websocket会话
    private Session session;
    // 当前客户端的用户名
    private String userName;

    @OnOpen
    // 浏览器与 websocket建立连接
    public void onOpen(Session session) {
        // 绑定当前 session
        this.session = session;
        // username =' + '${username}'字符串拆分
        String userName = session.getQueryString().split("=")[1];
        this.userName = userName;
        // 将客户端聊天实体保存到 clients
        clients.add(this);
        //将当前用户以及SessionID保存到用户列表
        namespaces.put(session.getId(),userName);
        
        //发送给所有用户上线通知(QQ好友上线通知)
        MessageToClient messageToClient = new MessageToClient();
        messageToClient.setContent(userName + "上线了!");
        // 更新当前所有用户列表
        messageToClient.setNames(namespaces);
        // 给当前所有用户发送登录信息
        String JSONStr = CommUtils.object2Json(messageToClient);
        for(WebSocket webSocket : clients) {
            webSocket.sendMsg(JSONStr);
        }
    }

    @OnError
    // 浏览器与后端连接失败
    public void onError(Throwable e) {
        System.err.println("WebSocket连接失败!");
        e.printStackTrace();
    }

    @OnMessage
    // 服务器收到了来自浏览器的信息
    /**
     * 群聊:{"msg":"777","type":1}
     * 私聊:{"to":"0-","msg":"33333","type":2}
     */
    public void onMessage(String msg) {
        // 将 msg反序列化为 MessageFromClient
        MessageFromClient messageFromClient = (MessageFromClient) CommUtils.json2Object
                (msg,MessageFromClient.class);

        //根据type的类型来做响应的区分
        if (messageFromClient.getType().equals("1")) {
            // 群聊信息
            String content = messageFromClient.getMsg();
            MessageToClient messageToClient = new MessageToClient();
            messageToClient.setContent(content);
            messageToClient.setNames(namespaces);
            // 广播发送
            for (WebSocket webSocket : clients) {
                webSocket.sendMsg(CommUtils.object2Json(messageToClient));
            }
        }else if (messageFromClient.getType().equals("2")) {
            // 私聊信息
            // {"to":"0-1-2-","msg":"33333","type":2}
            // 私聊内容
            String content = messageFromClient.getMsg();
            int toL = messageFromClient.getTo().length();
            String tos[] = messageFromClient.getTo()
                    .substring(0,toL-1).split("-");
            List<String> lists = Arrays.asList(tos);
            // 给指定的SessionID发送信息
            for (WebSocket webSocket : clients) {
                if (lists.contains(webSocket.session.getId()) &&
                        this.session.getId() != webSocket.session.getId()) {
                    // 发送私聊信息
                    MessageToClient messageToClient = new MessageToClient();
                    messageToClient.setContent(userName,content);
                    messageToClient.setNames(namespaces);
                    webSocket.sendMsg(CommUtils.object2Json(messageToClient));
                }
            }
        }
    }

    @OnClose
    // 关闭连接
    public void onClose() {
        // 将客户端聊天实体移除
        clients.remove(this);
        // 将当前用户以及SessionID保存到用户列表
        namespaces.remove(session.getId());
        System.out.println("有连接下线了"+
                ",用户名为"+userName);
        // 发送给所有在线用户一个下线通知
        MessageToClient messageToClient = new MessageToClient();
        messageToClient.setContent(userName+"下线了!");
        messageToClient.setNames(namespaces);
        // 发送信息
        String jsonStr = CommUtils.object2Json(messageToClient);
        for (WebSocket webSocket : clients) {
            webSocket.sendMsg(jsonStr);
        }
    }

    // 发送信息
    public void sendMsg(String msg) {
        try {
            this.session.getBasicRemote().sendText(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

前段代码chat.ftl:通过嵌入html代码将消息展示到界面上

<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/administrator-male.png">'
                    + ' </div>'
                    + '<div class="news">'
                    + innerHTML
                    + '</div>'
                    + '</li>';
        $("#message").append(msg);
    }
</script>

4.系统演示

 

1)登录注册界面

 

2)聊天室界面(群聊)

 

 

 3)私聊(通过用户前面的复选框控制发送的对象)

 

该系统实现了基本的聊天功能,但还有很多不完整的地方,例如没有实现用户登出、不能查询历史聊天记录、不能更改用户信息等。

 

作者:NP497

 

...全文
179 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧