尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java 7老后台加WebSocket群聊:JSR-356与ExtJS实践

Java 7老后台加WebSocket群聊:JSR-356与ExtJS实践 简介基于Tomcat8、Java7与ExtJS构建的WebSocket聊天室完整工程源码面向具备一定Java Web基础、希望打通实时通信链路的开发者与课程设计学习者。工程以JSR 356标准的javax.websocket端点承载服务端连接管理配合ExtJS组件搭建聊天窗口、输入框与消息列表可直接部署在Tomcat8容器中运行用于理解握手、消息广播与在线状态维护等环节。压缩包共1753个文件、约7.01MB含3个java源文件、6个class与11个jar依赖另有大体量gif与png图像、scss与css样式、50个js脚本构成界面皮肤与前端交互层xml、jsp及Eclipse工程配置文件保证目录可直接导入。已有263人学习。读者可据此复现一个可运行的实时聊天原型对照服务端生命周期回调与客户端封装代码梳理消息推送流程并扩展登录校验、历史记录与并发连接处理。1. 内网客服台要加群聊老后台只能上 WebSocket内网客服台要加一个群聊窗口前端是十几年前搭起来的 ExtJS 后台后端只能是 Java 7容器锁定 Tomcat 8。我拿到这份源码时先把它当成一个可拆的工具而不是完整产品服务端用 JSR-356 的ServerEndpoint接连接前端用 ExtJS 包一层 WebSocket消息走 JSON 文本。它适合维护老后台、要给内部系统加即时通知、又不想引入新中间件的开发者。下面按能跑起来、能改动的顺序拆。2. 服务端握手与会话管理ServerEndpoint背后的连接生命周期Tomcat 8 自带 WebSocket 实现Java 7 也能跑但工程边界要先定死一个 Maven WARmaven.compiler.source/target都是1.7javax.websocket-api只放在编译期作用域provided。这样 WAR 里不会把容器的 WebSocket 实现打进去部署时少一类类加载冲突。目录我一般这样摆chatroom/ pom.xml src/main/java/demo/chat/ endpoint/ChatEndpoint.java endpoint/OriginConfigurator.java model/ChatMessage.java util/JsonUtil.java src/main/webapp/ WEB-INF/web.xml index.html app.js app/view/ChatPanel.js app/ux/Socket.jsendpoint只放连接入口model放协议对象util放 Gson 单例前端按 ExtJS 的view、ux分开。不要把广播、在线表、JSON 解析全塞进一个类后面加房间和私聊会很难受。2.1 依赖与编译级别Java 7 下别写 lambdapom.xml里最关键的是两个provided和一个 Java 7 兼容的 JSON 库。WebSocket API 由 Tomcat 8 提供Servlet API 同理Gson 用你本地仓库里能跑在 Java 7 上的版本不要随手拿需要 Java 8 的新版。project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion groupIddemo.chat/groupId artifactIdchatroom/artifactId packagingwar/packaging properties maven.compiler.source1.7/maven.compiler.source maven.compiler.target1.7/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding websocket.api.version1.1/websocket.api.version gson.version${gson.java7.version}/gson.version /properties dependencies dependency groupIdjavax.websocket/groupId artifactIdjavax.websocket-api/artifactId version${websocket.api.version}/version scopeprovided/scope /dependency dependency groupIdcom.google.code.gson/groupId artifactIdgson/artifactId version${gson.version}/version /dependency /dependencies build finalNamechatroom/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId configuration failOnMissingWebXmlfalse/failOnMissingWebXml /configuration /plugin /plugins /build /projectwebsocket.api.version对应 JSR-356 APITomcat 8 容器里有实现所以scope必须是providedgson.version换成你仓库里 Java 7 能加载的版本别把新语法带进来。finalName决定 WAR 名后面上下文路径就是/chatroom。Java 7 没有 lambdaonMessage的回调、SendHandler都老老实实写匿名类别在 IDE 里自动补成() - {}否则编译级别一卡就是一片红。2.2onOpen里做三件事拿用户、登记、发系统消息端点路径写成/chat/{user}用户从 URL 路径里取前端用encodeURIComponent处理昵称。ServerEndpoint的configurator指向自定义握手拦截器后面 2.4 再补。ServerEndpoint(value /chat/{user}, configurator OriginConfigurator.class) public class ChatEndpoint { private static final ConcurrentHashMapString, Session ONLINE new ConcurrentHashMapString, Session(); OnOpen public void onOpen(Session session, PathParam(user) String user) throws IOException { String name user null || user.trim().length() 0 ? guest : user.trim(); session.getUserProperties().put(user, name); session.getUserProperties().put(lastActive, System.currentTimeMillis()); session.setMaxTextMessageBufferSize(8 * 1024); Session old ONLINE.put(name, session); if (old ! null old.isOpen()) { old.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, duplicate login)); } ChatMessage msg ChatMessage.system(name 进入聊天室, ONLINE.size()); broadcast(msg); } OnClose public void onClose(Session session) { String name (String) session.getUserProperties().get(user); if (name null) { return; } ONLINE.remove(name, session); broadcast(ChatMessage.system(name 离开聊天室, ONLINE.size())); } // onMessage、broadcast 在下一章补全 }ServerEndpoint的value是相对上下文路径的端点地址部署成chatroom.war后完整地址是ws://host:8080/chatroom/chat/张三。PathParam把路径里的{user}注入方法参数getUserProperties()是每个连接自己的小仓库用来存昵称和最后活跃时间不要用静态变量存“当前用户”。setMaxTextMessageBufferSize限制单条文本大小聊天室给 8KB 足够给太大容易被一条长消息拖住。ONLINE.put会返回同名的旧连接重复登录时把旧连接关掉避免同一个昵称有两个窗口都在线。2.3 在线表为什么用ConcurrentHashMap在线表就是MapString, Session但别用HashMap也别用Collections.synchronizedMap然后在广播时手动synchronized整张表。WebSocket 的onOpen、onClose、onMessage可能来自不同线程广播时一边遍历一边有人下线普通 Map 会直接抛ConcurrentModificationException。ConcurrentHashMap的迭代是弱一致的广播时看到的是某一刻的快照不会因为别人下线就崩。另一个血泪经验是下线删除要写成remove(key, value)不要只写remove(key)。假设用户 A 断线重连新连接已经放进去了旧连接的onClose才姗姗来迟如果直接按昵称删会把新连接一起删掉表现就是“人明明在线消息发不过去”。ONLINE.remove(name, session)只有在值还是旧Session时才删能避开这个回马枪。广播时如果想拿稳定快照可以new ArrayListSession(ONLINE.values())再遍历但单房间聊天室直接用弱一致迭代也够。2.4 握手拦截别让任意页面都能连OriginConfigurator只做一件事检查Origin头。内网也会有人把页面挂到别的域下或者用工具直接连能拦一层是一层。public class OriginConfigurator extends ServerEndpointConfig.Configurator { private static final String ALLOWED http://localhost:8080; Override public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { ListString origins request.getHeaders().get(Origin); if (origins null || origins.isEmpty() || !ALLOWED.equals(origins.get(0))) { throw new RuntimeException(Origin not allowed); } sec.getUserProperties().put(origin, origins.get(0)); } }modifyHandshake在握手阶段执行拿不到Session只能改ServerEndpointConfig的userProperties。Origin头可能没有比如某些非浏览器客户端内网如果确实有这种调用方就加白名单而不是直接放行。反向代理后面Origin可能变成代理地址部署到那一步时把ALLOWED改成实际访问域名别写死localhost之后又奇怪为什么生产连不上。3. 消息协议与广播链路一条聊天消息从 ExtJS 到所有在线窗口服务端连接通了只是第一步真正决定后面好不好改的是消息协议。聊天室不需要一上来就上 Protobuf文本 JSON 足够type区分加入、离开、聊天、心跳from是发送者content是正文time给界面显示online给在线人数。字段少排查时一眼能看懂。3.1ChatMessage与 Gson 单例public class ChatMessage { private String type; // JOIN / LEAVE / SYSTEM / CHAT / PING / PONG private String from; private String content; private String time; private int online; public static ChatMessage system(String content, int online) { ChatMessage m new ChatMessage(); m.setType(SYSTEM); m.setFrom(system); m.setContent(content); m.setTime(new SimpleDateFormat(HH:mm:ss).format(new Date())); m.setOnline(online); return m; } public String getType() { return type; } public void setType(String type) { this.type type; } public String getFrom() { return from; } public void setFrom(String from) { this.from from; } public String getContent() { return content; } public void setContent(String content) { this.content content; } public String getTime() { return time; } public void setTime(String time) { this.time time; } public int getOnline() { return online; } public void setOnline(int online) { this.online online; } }type用字符串而不是枚举前端加新类型时不用同步改枚举序列化代价是服务端要自己兜底未知类型。time用HH:mm:ss就够了聊天室不靠时间排序靠服务端到达顺序。SimpleDateFormat不是线程安全的别写成静态共享实例每次new一个量级不大省心。Gson 单例放在JsonUtil里public final class JsonUtil { public static final Gson GSON new Gson(); private JsonUtil() {} }Gson本文还有配套的精品资源点击获取
返回列表