基于 WebSocket 的聊天室项目(下)

1.创建一个聊天室的数据库

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

创建数据库,如果不存在`websocket_chatroom`默认字符集`utf8`; 
使用`websocket_chatroom`; 
如果用户不存在,则创建表
(id int主键auto_increment注释'用户id',
  用户名varchar(20)唯一不为空注释'用户名',
  密码varchar(100)不为空注释'md5加密后的密码' 
) ; ```


#### 2.三层架构
- 道【数据访问对象,数据访问层】:JAVA操作数据库(增删改查),把信息持久化到数据库中- 服务【服务层】:中间的业务层,具体处理用户业务,调用道- 控制器【控制层】:负责具体的业务模块流程的控制。调用服务,获取数据,返回给客户端/从客户端获得数据。


 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200305105427839.png)
 #### 3.用户注册与登录
        无论是用户模块,还是其他,操作数据库都是之前说的5步走,不同的是执行的SQL语句不同,而加载驱动/数据源,获取连接,释放资源三个步骤可以封装,其实也就是之前的JDBCUtilWizDruid,在dao包中创建BaseDao类封装这些步骤,注意要把方法权限转换仅允许让子类调用:
```java
package chatRoom.Dao;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.alibaba.druid.pool.DruidPooledConnection;

import chatRoom.utils.*;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class BaseDao {
    private static DataSource dateSource;
    static {

        Properties properties =CommUtil.loadProperties("database.properties");

        try {
            dateSource = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            //  e.printStackTrace();

            System.err.println("获取数据源失败");
        }
    }
   protected static DruidPooledConnection getConnection()
    {
        try {
            return (DruidPooledConnection) dateSource.getConnection();
        } catch (SQLException e) {
            // e.printStackTrace();
            System.err.println("连接数据库失败");
        }

        return null;
    }

protected static void closeResource(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 static void closeResource(Connection connection, Statement statement, ResultSet resultSet)
    {closeResource(connection,statement);
        if(resultSet!=null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

接下来,而用户注册 ——insert 与登录—— query都是数据库相关的操作,也是封装在 Dao 层,类名为AccountDao :

package chatRoom.Dao;
import org.apache.commons.codec.digest.DigestUtils;
import java.sql.*;
public class AccountDao extends BaseDao
    {
        //用户注册:insert
//传入 user 对象,作为元组插入到数据库中

        public boolean userRegister(entity.user user1) throws SQLException {
            //注册成功与否的标志,如果没有报错再改为 true
            boolean issuccess=false;
         String username=user1.getUsername();
         String password=user1.getPassword();
         Connection connection=null;
         PreparedStatement statement=null;
 connection=getConnection();
         String sql="insert into user(username,password) values (?,?)";

         //还可获取自增ID
statement=connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
            try {
                statement.setString(1,username);
                statement.setString(2,DigestUtils.md5Hex(password));
                issuccess=(statement.executeUpdate()==1);
            } catch (Exception e) {
               //e.printStackTrace();
                System.err.println("用户注册失败`");
            }
            finally {
                closeResource(connection,statement);
            }
return issuccess;
        }



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


//用户登录:根据输入的用户名和密码进行查询 select
public entity.user userLogin(String username,String password)
{Connection connection=null;
    PreparedStatement statement=null;
    ResultSet resultSet=null;
    entity.user user1=null;
connection=getConnection();
String sql="select * from user where username=? and password=?";
    try {
        statement=connection.prepareStatement(sql);
        statement.setString(1, username);
        statement.setString(2,DigestUtils.md5Hex(password));
    resultSet=statement.executeQuery();
    if(resultSet.next())
    {
        user1=getUserInfo(resultSet);
    }
    } catch (SQLException e) {
        //e.printStackTrace();
        System.err.println("查询用户信息出错");
    }
finally {
        closeResource(connection,statement,resultSet);
}

return user1;
    }
}

对注册和登录操作进行单元测试:

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

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

测试均通过。

3.放置前端页面,部署Tomcat

把前端页面放在webapp包下,img 资源见github 链接:
在这里插入图片描述
部署Tomcat, 当Application context是“/”时, 是放在根目录root下,方便访问:
在这里插入图片描述

(

  • Web服务器:可以向发出请求的浏览器提供文档的程序。Web服务器的作用就是用户 通过浏览器 向Web服务器 发送http请求,Web服务器解析
    http请求 将请求路径的文件 返回给浏览器,浏览器 再将文件进行渲染,因此web服务器的作用就是返回服务端的静态文件。
  • Servlet:用Java编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容,看到这可能又迷糊了,说半天还是不知道Servlet是什么,其实Servlet就是一种用来处理网络请求的一套规范。
  • Tomcat:Servlet容器【管理生命周期】,同时它也包括了Web服务器的功能, 因此 Tomcat是Web服务器的扩展,也可以理解为Tomcat就是Web服务器。
    Tomcat的整个运行流程:
    (1)用户通过浏览器向服务器发送请求
    (2)Tomcat 接收请求后解析请求具体访问哪个应用
    (3)Tomcat创建一个HttpServletRequest对象,将用户发送的请求封装到这个对象里
    (4)Tomcat创建一个HttpServletResponse对象
    (5)Servlet容器调用 HttpServlet 对象的 service 方法,把 HttpRequest 对象与HttpResponse 对象作为参数传给 HttpServlet 对象
    (6)HttpServlet 调用 HttpRequest 对象的有关方法,获取Http请求信息
    (7)HttpServlet 调用 HttpResponse 对象的有关方法,生成响应数据
    (8)Servlet 容器把 HttpServlet 的响应数据结果传给浏览器,浏览器再根据返回的 response 进行相应的渲染

         
    运行Tomcat,显示前端页面:
    在这里插入图片描述
4.服务程序

     接下来就可以在Service 包中提供用户注册与登录的服务了。新建 Account(账户)Service 类:

    package chatRoom.Service;
import chatRoom.Dao.AccountDao;
import entity.user;
import java.sql.SQLException;

public class AccountService {
    private AccountDao accountDao;
    
    //用户注册
    public boolean userLogin(String username,String password)
    {
        entity.user user1=null;
        user1=accountDao.userLogin(username,password);
        
        
        if(user1==null) return false;
        return true;
    }
    
    //用户登录
    public boolean userResgister(String username,String password) throws SQLException {
        entity.user user1=new user();
        user1.setUsername(username);
        user1.setPassword(password);
        return accountDao.userRegister(user1);
    }
}
5.控制程序
5.1注册

在这里插入图片描述
     在 registration.html中,没有点击”登录“按钮,也就是还在注册页面时,用户输入的用户名和密码将通过 标签,创建HTML 表单。

 <form method="post" action="/doRegister" 
 onsubmit="return submitTest()">
  • method 属性规定如何发送表单数据,即通过 post 方法。
  • action 属性规定当提交表单时向何处发送表单数据“/doRegister”。
  • onsubmit 属性是提交表单时触发的操作,submitTest()是这样的:(在 common_form_test.js中),主要是对输入格式做一些验证,比如是否为空。
//若输入框为空,阻止表单的提交
function submitTest() {
    // 全局变量a和b,分别获取用户框和密码框的value值
    var a = document.getElementsByTagName("input")[0].value;
    var b = document.getElementsByTagName("input")[1].value;
    if (!a && !b) { //用户框value值和密码框value值都为空
        document.getElementById("remind_1").innerHTML = "请输入用户名!";
        document.getElementById("change_margin_1").style.marginBottom = 1 + "px";
        document.getElementById("remind_2").innerHTML = "请输入密码!";
        document.getElementById("change_margin_2").style.marginBottom = 1 + "px";
        document.getElementById("change_margin_3").style.marginTop = 2 + "px";
        return false; //只有返回true表单才会提交
    } else if (!a) { //用户框value值为空
        document.getElementById("remind_1").innerHTML = "请输入用户名!";
        document.getElementById("change_margin_1").style.marginBottom = 1 + "px";
        return false;
    } else if (!b) { //密码框value值为空
        document.getElementById("remind_2").innerHTML = "请输入密码!";
        document.getElementById("change_margin_2").style.marginBottom = 1 + "px";
        document.getElementById("change_margin_3").style.marginTop = 2 + "px";
        return false;
    }
    }

     在 controller 包中新建 AccountController 类,控制注册与登录操作。它应该继承 HttpServlet 类,使用 @WebServlet 注解,并通过(urlPatterns = “/doRegister”) 实现这个Servlet 的与 URL 的匹配模式,使用户输入的用户名和密码可以发送到 AccountController 类:

    @WebServlet(urlPatterns = "/doRegister")
public class AccountController extends HttpServlet {

(就相当于 在web.xml 中配置:

<servlet>
    <!-- 类名 -->
    <servlet-name>Register</servlet-name>
    <!-- 所在的包 -->
    <servlet-class>chatRoom.Controller</servlet-class>
</servlet>
     
<servlet-mapping>
    <servlet-name>Register</servlet-name>
    <!-- 访问的网址 -->
    <url-pattern>/doRegister</url-pattern>
</servlet-mapping>

既然表单信息提交过来了,就需要进行在 doGet 、doPost 中写响应:

package chatRoom.Controller;
import chatRoom.Service.AccountService;
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;
import java.sql.SQLException;


@WebServlet(urlPatterns = "/doRegister")
public class AccountController extends HttpServlet {

    //调用Service层
    private AccountService accountService=new AccountService();

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username=request.getParameter("username");
        String password=request.getParameter("password");
        response.setContentType("text/html;charset=utf8");
        PrintWriter writer=response.getWriter();
        try {
        if(accountService.userResgister(username,password))
                //用户注册成功,弹窗提示
            {
                writer.println("<script>alert(\"注册成功!\");\n"+
                "window.location.href=\"/index.html\";\n"+
                        "\n"+
                        "</script>");
            }


            //用户注册失败
            else
            {
                writer.println("<script>alert(\"注册失败!\");\n"+
                        "window.location.href=\"/registration.html\";\n"+
                        "\n"+
                        "</script>");

            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

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

     如果注册成功,就弹窗提示“注册成功” ,并由当前页面打开 /index.html,进行登录操作;如果注册失败,就会就弹窗提示“注册失败”,并打开 /registeration.html,也就是初始页面,可以重新注册或登录。
运行Tomcat,测试注册结果,OK!


在处理登录前 工具类 CommUtil 类中再写一个方法,用来判断登录时输入的用户名、密码字符串是否为空:

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

Web容器(比如 Tomcat )在启动的时候,它会为每个应用程序都创建一个对应的 ServletContext 对象每个应用都会对应一个。 由于一个应用只有一个 ServletContext ,所有的 Servlet 都要共享它,所以 Servlet 之间可以通过 ServletContext 对象实现通信。


监听器【监听某个对象的的状态变化的组件】:
     用于监听 Web 应用中某些对象、信息的创建、销毁、增加,修改,删除等动作的发生,然后作出相应的响应处理。当范围对象的状态发生变化的时候,服务器自动调用监听器对象中的方法。常用于统计在线人数和在线用户,系统加载时进行信息初始化,统计网站的访问量等等。

     其中 ServletContextListener接口 主要用来监听 ServletContext 的生命周期事件,需要覆写两个方法:
contextInitialized 和 contextDestroyed 会分别在web应用启动和关闭的时候被调用。
     本项目中,会为 freeWorker 设置一个监听器,覆写contextInitialized 方法,使得在 Web 应用启动时就能创建模板对象。而所有的Servlet 都可以访问到应用对应的唯一的那个 ServletContext ,所以在控制层可以获取到模板。


实现 在服务启动的时候能够获取内容:
我们需要做的有:
(1 ) 实现 servletContextListerner 接口 项目中实现类为 FreeMarkerListener 。并将要共享的属性 freeworker 的版本号 通过 setAttribute(name,data) 方法提交到内存中去 :

@WebListener
public class FreeMarkerListener implements ServletContextListener {
 
    public static final String TEMPLATE_KEY="_template_";

    @Override
    public void contextInitialized(ServletContextEvent sce) {
Configuration cfg=new Configuration(Configuration.VERSION_2_3_0);
        sce.getServletContext().setAttribute(TEMPLATE_KEY,cfg);

    }

2 )应用项目通过 getAttribute(name) 将数据取到 。

Configuration cfg=(Configuration) request.getServletContext().getAttribute(FreeMarkerListener.TEMPLATE_KEY);

这样,Web 服务器在启动时,会直接加载监听器,通过以下的应用程序就可以进行数据的访问。


登录之前,需要在实体包中新建两个实体类,一个封装 网页展现给用户的 Message2Client ,需要聊天内容 content 、用户列表 names(SessionID 与 username )作为字段。

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 void setContent(String userName,String msg)
    {this.content=userName+"说:"+msg;}


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

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

&#!60;&#!60;&#!60;&#!60;另一个实体类 MessageFromClient 封装的是用户向服务器发出的信息,需要 聊天消息、聊天类别——私聊 or 群聊、私聊的对象的SessionID :

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

@Data
public class MessageFromClient {
//聊天信息
    private String msg;
     //聊天类别,1表示群聊,2表示私聊
    private String type;
    //私聊的对象SessionID
private String to;

    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;
    }
}

5.2 登录与聊天
5.2.1模板引擎 FreeWorker

     如果登录成功,我们要进入聊天页面,之前导入依赖时有个模板引擎 Freemarker,它是一种比较简单的网页展示技术,是网页模板和数据模型的结合体。用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库。
     聊天页面就是通过这个模板引擎实现的,模板文件 Freemarker使用步骤:
(1)创建一个Configuration对象,直接new一个即可,构造参数是freemarker的版本号。

 //配置版本
Configuration cfg=new Configuration(Configuration.VERSION_2_3_0);

(2)设置模板文件所在的路径,需要给出在磁盘上储存的全路径

  cfg.setDirectoryForTemplateLoading(new File("E:/WebSocketchatRoom/src/main/webapp"));

(3)设置生成的文件的编码格式,一般为utf-8格式

 //配置页面编码
cfg.setDefaultEncoding(StandardCharsets.UTF_8.displayName());

(4)加载模板,创建模板对象
网上有个例子是:

  Template template = configuration.getTemplate("hello.ftl");

     在本项目中,体现解耦思想,在应用开启时创建模板,通过控制层程序获取模板,所以需要提供一个 获取模板的方法:

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;
}

在控制层传入 请求和模板文件名,即可获取模板:

Template template=getTemplate(request,"/chat.ftl");

(5)创建模板使用的数据集,可以使pojo也可以是map类型的,本项目中使用的是 map 类型:

 Map<String,String> map=new HashMap<>();
            map.put("username",userName);
                template.process(map,out);

(6)创建Write流对象,将文件文件输出,需要指定生成的文件的名称
本项目就是在响应的页面输出的。
网上有个例子:

Writer out = new FileWriter(new File("D:/temp/out/hello.html"));

(7)调用模板的process方法,生成相应的文本

  template.process(map,out);

(8)关闭流
     这一步是与(6)相对应的比如:out.close();
     本项目不需要关闭,随着应用被关闭,自然监听器也就结束了,模板也就关闭了。

在 index.html ,也就是注册成功后显示的页面 是这样的:

 <form method="post" action="/login"
  onsubmit="return submitTest()">
5.2.2 登录控制器

     用户输入的用户名、密码的表单数据将通过 psot 方法发送,提交表单时仍是会通过 submitTest() 对输入的格式进行检查。而表单会由映射为 /login 的 Sevlet 进行响应:
在 controller 包中新建这个 Sevlet ,名为 LoginController:

package com.bit.chatroom.controller;

import com.bit.chatroom.config.FreeMarkerListener;
import com.bit.chatroom.service.AccountService;
import com.bit.chatroom.utils.CommUtil;
import com.sun.deploy.net.HttpResponse;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

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;
import java.time.temporal.Temporal;
import java.util.HashMap;
import java.util.Map;

@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))
        {//登录成功,跳转到聊天页面
//需要把用户名传到前端
//加载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();
            }
        }
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);}
    
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;
}
}

可以看到,如果登录成功,会通过 getTemplate 获取模板,调跳转到由 Freeworker 生成的聊天室的前端页面去,来看看这个 /chat.ftl:

  //判断当前浏览器是否支持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连接成功!")
    };

在qq.css中有:


declare var WebSocket: 
... ...
 new(url: string, protocols?: string | string[]): WebSocket;

     如上所示,在支持 Websocket 的浏览器中会提供原生的 WebSocekt 对象,其中对于消息的接收与数据帧处理在浏览器中已经封装好了,使用 new 关键字实例化它:
     接收两个参数:url 表示需要连接的地址,比如:ws://localhost:8080/websocket; (protocols 是可选参数,可以是一个字符串或者一个数组,用来表示子协议,这样做可以让一个服务器实现多种 WebSocket 子协议。项目中没有用到)。

5.3服务层
@ServerEndpoint("/websocket")
public class WebSocket {

      在Service 包需要写一个类 Websocket,并使用注解 @ServerEndPoinr定义服务器端 】,并指定 url,这个注解的源码:

 //
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package javax.websocket.server;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ServerEndpoint {
    String value();

    String[] subprotocols() default {};

    Class<? extends Decoder>[] decoders() default {};

    Class<? extends Encoder>[] encoders() default {};

    Class<? extends Configurator> configurator() default Configurator.class;
}

其中,encoders 和 decoders 用于 Websocket Messages 与传统java 类型之间的转换。
(An encoder takes a Java object and produces a representation that can be transmitted as a WebSocket message)
在这里插入图片描述
群聊需要缓存用户列表 names,key是SessionID ,value 是username(在数据库中是主键):

Map<String,String>names
(1)用户登录成功后

     使用@OnOpen 注解的方法,参数为 Session ,在连接时会被调用,这时前端的模板文件有传来用户名 username,会将用户名和 SessionID 存入 names 中,并在控制台输出 ,这时还需要给所有用户一个上线通知,在这时创建 message2Client 对象,并通过 Gson技术,也就是在 CommUtil类中的 objectToJson 方法,将 message2Client 对象转化成字符串,并把字符串发给每个 WebSocket 对象。

    //建立连接时调用
    @OnOpen
    public void onOpen(Session session) throws IOException {
    
        this.session=session;
        String userName=session.getQueryString().split("=")[1];
//前端发给后端的格式是:chat.ftl第60行 webSocket = new WebSocket('ws://127.0.0.1:8080/websocket?username=' + '${username}');
//"?"表示需要传递的参数
// getQueryString()获取的内容是username=' + '${username}'
//需要的是value值,按照"="拆分即可
       
        this.userName=userName;

        //将客户端聊天实体保存到clients中
        clients.add(this);

        //将当前用户和SessionID保存到用户列表
        names.put(session.getId(),userName);

System.out.println("有新的连接,SessionID为"+session.getId()+",用户名为"+userName);

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

        //发送信息
        String jsonStr=CommUtil.objectToJson(message2Client);
        for(WebSocket webSocket:clients)
        {webSocket.sendMsg(jsonStr);}}
(2)服务器收到消息时

使用@OnMessage 注解的方法,参数为 String ,先用 Gson 技术将收到的消息反序列化为 messageFromClient 类型,进而通过 get 方法获取聊天信息、聊天类型字段,如果是群聊,就 new 一个 Message2Client对象,再通过 Gson技术,转化成 字符串,发给每一个Session ;如果是私聊(只是有别于”群“,不一定是一个人。)会为选中的Session 发送字符串。

   @OnMessage
    public void onMessage(String msg) throws IOException {

        //先将msg反序列化为MessageFronClient
        MessageFromClient messageFromClient= (MessageFromClient) CommUtil.jsonToObject(msg,MessageFromClient.class);
        if(messageFromClient.getType().equals("1"))
        {String context=messageFromClient.getMsg();

            //需要把群聊信息封装在字符串中发给所有客户端
            Message2Client message2Client=new Message2Client();
            message2Client.setContent(context);
            message2Client.setNames(names);
            //群聊发送
            for (WebSocket webSocket:clients)  {webSocket.sendMsg(CommUtil.objectToJson(message2Client));}
        }

        else if(messageFromClient.getType().equals("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())
            {Message2Client message2Client=new Message2Client();
                message2Client.setContent(userName,content);
                message2Client.getNames(names);
                webSocket.sendMsg(CommUtil.objectToJson(message2Client));}
    }
        }
    }

以上的sendMsg 传送消息是调用 Session 类的方法。

  public void sendMsg(String msg) throws IOException
    {this.session.getBasicRemote().sendText(msg);}
(3)连接关闭时

    需要给所有用户发送一个下线通知:

 @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.setNames(names);

        //发送信息
String jsonStr = CommUtil.objectToJson(message2Client);
        for (WebSocket webSocket : clients) {
            webSocket.sendMsg(jsonStr);
        }
    }
(4)出现异常时

    在控制台输出:

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

接下来,对聊天室的性能进行测试,首先,测试 TomCat 下 WebSocket 最大连接数:
先到 apache-tomcat-8.5.42\bin 下找到 catalina.bat ,在第一行写入:
在这里插入图片描述
这是对 JVM 运行时参数进行设置:

-server:一定要作为第一个参数,在多个CPU时性能佳
-Xms:初始Heap大小,使用的最小内存,CPU 性能高时此值应设的大一些
-Xmx:Java heap最大值,使用的最大内存
上面两个值是分配JVM的最小和最大内存,取决于硬件物理内存的大小,建议均设为物理内存的一半。
      <dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-client-api</artifactId>
    <version>1.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.tyrus.bundles</groupId>
    <artifactId>tyrus-standalone-client</artifactId>
    <version>1.12</version>
</dependency>
<dependency>
    <groupId>org.glassfish.tyrus</groupId>
    <artifactId>tyrus-container-grizzly-client</artifactId>
    <version>1.12</version>
</dependency>

考虑到并发,需要线程安全,可以加锁 synchronized:

String jsonStr = CommUtil.objectToJson(message2Client);
    for (WebSocket webSocket : clients) {
    synchronized(webSocket)
        webSocket.sendMsg(jsonStr);
    }

@SendTo 注解来进行广播,代码可以简单很多,出于对大厂的信任,我们应该也无需去担心性能问题。但是由于种种原因,要做的改动太大,我们现在只能考虑在原有代码上做优化而非整个改变底层支持。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值