完美解决WebSocket 服务器 The WebSocket session [0] has been
最近项目需要web客户端与服务器保持长链接的场景并需要服务器向所有链接的客户端推送消息,于是自然使用了WebSocket技术,自然要考虑到服务器于多个客户端线程安全的问题。于是乎,想当然的在WebSocket服务器端通过一个线程安全的队列来保持所有客户端的Session.
private volatile static List<Session> sessions = Collections.synchronizedList(new ArrayList()); private Session session; /* * 客户端链接成功后讲其保存在线程安全的集合中 */ @OnOpen public void onOpen(Session session) throws IOException { this.session = session; sessions.add(this); } /* * 客户端断开链接后将其从线程安全的集合中移除 */ @OnClose public void onClose() { sessions.remove(this); } //给所有客户端发送消息 public static void sendMessage(String clientInfoJson) { try { if (sessions.size() != 0) { for (session s : sessions) { if (s != null) { s.getBasicRemote().sendText(clientInfoJson); } } } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
上述代码感觉上好像没问题。Session信息是保存在线程安全的集合,又通过volatile变量来修饰保证了内存可见性,但实际运行时却发现并没有想象的那么好。当客户端断开链接,时服务器需要发送消息给客户端时.服务端抛出异常:
IllegalStateException: The WebSocket session [0] has been closed and no method (apart from close()) may be called on a closed session
不难看出,是服务端在关闭Session即将Session从线程安全的队列移除时,在发送消息的方法里应该被移除的Session消息却进入了发送消息的环节,在执行getBasicRemote().sendText(clientInfoJson);操作时发生了异常。
解决方法:
Google了大量资料后发现如果要解决这种线程安全的问题,不能通过线程安全的集合来保存Session解决。而应该保存整个类,并通过CopyOnWriteArraySet容器来操作。
@ServerEndpoint("/getLocation") @Component public class TransmissionLocationWebSocket { @Autowired public TerminalService terminalServiceInWebSocket; private static CopyOnWriteArraySet<TransmissionLocationWebSocket> sessions = new CopyOnWriteArraySet<TransmissionLocationWebSocket>(); /* * 线程不安全 */ //private volatile static List<Session> sessions = Collections.synchronizedList(new ArrayList()); private Session session; /* * 链接成功后的回掉 */ @OnOpen public void onOpen(Session session) throws IOException { System.out.println("链接成功"); this.session = session; sessions.add(this); } public static void sendUserLocal(String clientInfoJson,) { try { if (sessions.size() != 0) { for (TransmissionLocationWebSocket s : sessions) { if (s != null) { // 判断是否为终端信息。如果是终端信息则查询数据库获取detail s.session.getBasicRemote().sendText(clientInfoJson); } } } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @OnClose public void onClose() { System.out.println("设置离线"); sessions.remove(this); } }
完美解决
备注:虽然我上面贴出来的代码是在COW中保存了整个类,但我测试的时候发生,保存Session也是可以的。
Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。