注意修改上一篇准备工作中写的配置文件中的数据库名称::
创建数据库,如果不存在`websocket_chatroom`默认字符集`utf8`;
使用`websocket_chatroom`;
如果用户不存在,则创建表
(id int主键auto_increment注释'用户id',
用户名varchar(20)唯一不为空注释'用户名',
密码varchar(100)不为空注释'md5加密后的密码'
) ; ```
#### 2.三层架构
- 道【数据访问对象,数据访问层】:JAVA操作数据库(增删改查),把信息持久化到数据库中- 服务【服务层】:中间的业务层,具体处理用户业务,调用道- 控制器【控制层】:负责具体的业务模块流程的控制。调用服务,获取数据,返回给客户端/从客户端获得数据。

#### 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);
}
测试均通过。
把前端页面放在webapp包下,img 资源见github 链接:
部署Tomcat, 当Application context是“/”时, 是放在根目录root下,方便访问:
(❀
接下来就可以在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);
}
}
在 registration.html中,没有点击”登录“按钮,也就是还在注册页面时,用户输入的用户名和密码将通过 标签,创建HTML 表单。
<form method="post" action="/doRegister"
onsubmit="return submitTest()">
//若输入框为空,阻止表单的提交
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("");
}
//用户注册失败
else
{
writer.println("");
}
} 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;
}
}
如果登录成功,我们要进入聊天页面,之前导入依赖时有个模板引擎 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()">
用户输入的用户名、密码的表单数据将通过 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("\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("\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 子协议。项目中没有用到)。
@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
❀ 使用@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);}}
❀ 使用@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);}
需要给所有用户发送一个下线通知:
@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);
}
}
在控制台输出:
@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的最小和最大内存,取决于硬件物理内存的大小,建议均设为物理内存的一半。
javax.websocket
javax.websocket-client-api
1.1
org.glassfish.tyrus.bundles
tyrus-standalone-client
1.12
org.glassfish.tyrus
tyrus-container-grizzly-client
1.12
考虑到并发,需要线程安全,可以加锁 synchronized:
String jsonStr = CommUtil.objectToJson(message2Client);
for (WebSocket webSocket : clients) {
synchronized(webSocket)
webSocket.sendMsg(jsonStr);
}
@SendTo 注解来进行广播,代码可以简单很多,出于对大厂的信任,我们应该也无需去担心性能问题。但是由于种种原因,要做的改动太大,我们现在只能考虑在原有代码上做优化而非整个改变底层支持。