Java后端WebSocket的Tomcat实现

一.WebSocket简单介绍

  随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。

  我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。

  轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。

  Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。

  这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。

  伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。

  JavaEE 7中出了JSR-356:Java API for WebSocket规范。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat从7.0.27开始支持 WebSocket,从7.0.47开始支持JSR-356,下面的Demo代码也是需要部署在Tomcat7.0.47以上的版本才能运行。

二.WebSocket示例

2.1. 客户端(Web主页)代码:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <base href="<%=basePath%>">
    
    <title>websocket</title>
    
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">    
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<!--
<link rel="stylesheet" type="text/css" href="styles.css">
-->


  <style type="text/css">  
        input#chat {  
            width: 410px  
        }  
  
        #console-container {  
            width: 400px;  
        }  
  
        #console {  
            border: 1px solid #CCCCCC;  
            border-right-color: #999999;  
            border-bottom-color: #999999;  
            height: 170px;  
            overflow-y: scroll;  
            padding: 5px;  
            width: 100%;  
        }  
  
        #console p {  
            padding: 0;  
            margin: 0;  
        }  
    </style>  
    <script type="application/javascript"> 
        "use strict";  
  
        var Chat = {};  
  
        Chat.socket = null;  
  
        Chat.connect = (function(host) {  
            if ('WebSocket' in window) {  
                Chat.socket = new WebSocket(host);  
            } else if ('MozWebSocket' in window) {  
                Chat.socket = new MozWebSocket(host);  
            } else {  
                Console.log('Error: 浏览器不支持WebSocket');  
                return;  
            }  
  
            Chat.socket.onopen = function () {  
                Console.log('Info: WebSocket链接已打开');  
                document.getElementById('chat').onkeydown = function(event) {  
                    if (event.keyCode == 13) {  
                        Chat.sendMessage();  
                    }  
                };  
            };  
  
            Chat.socket.onclose = function () {  
                document.getElementById('chat').onkeydown = null;  
                Console.log('Info: webcocket关闭.');  
            };  
  
            Chat.socket.onmessage = function (message) {  
                Console.log(message.data);  
            };  
        });  
  
        Chat.initialize = function() {  
            if (window.location.protocol == 'http:') {  
                Chat.connect('ws://' + window.location.host + '/WebSocket/chat');  
            } else {  
                Chat.connect('wss://' + window.location.host + '/WebSocket/chat');  
            }  
        };  
  
        Chat.sendMessage = (function() {  
            var message = document.getElementById('chat').value;  
            if (message != '') {  
                Chat.socket.send(message);  
                document.getElementById('chat').value = '';  
            }  
        });  
  
        var Console = {};  
  
        Console.log = (function(message) {  
            var console = document.getElementById('console');  
            var p = document.createElement('p');  
            p.style.wordWrap = 'break-word';  
            p.innerHTML = message;  
            console.appendChild(p);  
            while (console.childNodes.length > 25) {  
                console.removeChild(console.firstChild);  
            }  
            console.scrollTop = console.scrollHeight;  
        });  
  
        Chat.initialize();  
  
        document.addEventListener("DOMContentLoaded", function() {  
            // Remove elements with "noscript" class - <noscript> is not allowed in XHTML  
            var noscripts = document.getElementsByClassName("noscript");  
            for (var i = 0; i < noscripts.length; i++) {  
                noscripts[i].parentNode.removeChild(noscripts[i]);  
            }  
        }, false);  
  
    </script>  
</head>  
<body>  
<div class="noscript"><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable  
    Javascript and reload this page!</h2></div>  
<div>  
    <p>  
        <input type="text" placeholder="输入文字,回车发送" id="chat" /><br>
        注意:输入  消息to用户名   发送给指定用户   比如:  你好to用户1<br>    
                   输入   消息     直接发送给全体用户    
    </p>  
    <div id="console-container">  
        <div id="console"/>  
    </div>  
</div>  
</body>
</html>

2.1. Java Web后端代码

/**
 *  ━━━━━━神兽出没━━━━━━ 
 *   ┏┓   ┏┓ 
 *  ┏┛┻━━━┛┻┓ 
 *     ┃       ┃
 *    ┃   ━   ┃ 
 *     ┃ ┳┛ ┗┳    ┃ 
 *  ┃       ┃ 
 *  ┃   ┻   ┃ 
 *  ┃       ┃ 
 *  ┗━┓   ┏━┛Code is far away from bug with the animal protecting 
 *    ┃   ┃    神兽保佑,代码无bug 
 *    ┃   ┃ 
 *    ┃   ┗━━━┓ 
 *    ┃       ┣┓ 
 *    ┃       ┏┛ 
 *    ┗┓┓┏━┳┓┏┛ 
 *     ┃┫┫ ┃┫┫ 
 *     ┗┻┛ ┗┻┛ 
 * 
 * ━━━━━━感觉萌萌哒━━━━━━ 
 */
package com.wantao.websocket;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

/**
 *  * 类名称:Server.java 类描述:简单的聊天室 作 者:wantao 时 间:2018年03月08日 23:11:48  
 */
@ServerEndpoint(value = "/chat")
public class Server {
	private static final String GUEST_PREFIX = "用户";
	/**
	 * 一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,
	 * 在使用的时候,不可避免的会用到synchronized关键字。 而AtomicInteger则通过一种线程安全的加减操作接口。
	 */
	private static final AtomicInteger connectionIds = new AtomicInteger(0);
	// 存放用户的websocket
	private static final Set<Server> connections = new CopyOnWriteArraySet<>();

	private final String nickname;
	private Session session;

	public Server() {
		nickname = GUEST_PREFIX + connectionIds.getAndIncrement();
	}

	/**
	 * 创建连接时间调用的方法  
	 * 
	 * @param session
	 */
	@OnOpen
	public void start(Session session) {
		this.session = session;
		connections.add(this);
		String message = String.format("* %s %s", nickname, "加入聊天室");
		// 上线通知
		broadcast(message);
		try {
			// 系统问候语
			SendHello(this.nickname);
			// 返回在线用户
			onlineList();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/**
	 * 链接关闭时调用方法
	 */
	@OnClose
	public void end() {
		connections.remove(this);
		String message = String.format("* %s %s", nickname, "退出聊天室");
		broadcast(message);
	}

	/**
	 * 传输信息过程中调用方法
	 * 
	 * @param message
	 */
	@OnMessage
	public void incoming(String message) {
		// Never trust the client
		// TODO: 过滤输入的内容
		String m = String.format("* %s %s", nickname, message);
		if (m.contains("to")) {
			// 点对点发送
			broadcastOneToOne(m, nickname);
		} else {
			// 群发
			broadcast(m);
		}
	}

	/**
	 * 发生错误是调用方法
	 * 
	 * @param t
	 * @throws Throwable
	 */
	@OnError
	public void onError(Throwable t) throws Throwable {
		System.out.println("错误: " + t.toString());
	}

	/**
	 * 消息广播 通过connections,对所有其他用户推送信息的方法
	 * 
	 * @param msg
	 */
	private static void broadcast(String msg) {
		for (Server client : connections) {
			try {
				synchronized (client) {
					client.session.getBasicRemote().sendText(msg);
				}
			} catch (IOException e) {
				System.out.println("错误:向客户端发送消息失败");
				connections.remove(client);
				try {
					client.session.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
				String message = String.format("* %s %s", client.nickname,
						"退出聊天室");
				broadcast(message);
			}
		}
	}

	/**
	 * 点对点发送消息 通过connections,对所有其他用户推送信息的方法
	 * 
	 * @param msg
	 */
	private static void broadcastOneToOne(String msg, String nickName) {
		String[] arr = msg.split("to");
		for (Server client : connections) {
			try {
				if (arr[1].equals(client.nickname)
						|| nickName.equals(client.nickname)) {
					synchronized (client) {
						client.session.getBasicRemote().sendText(arr[0]);
					}
				}
			} catch (IOException e) {
				System.out.println("错误:向客户端发送消息失败");
				connections.remove(client);
				try {
					client.session.close();
				} catch (IOException e1) {
					e1.printStackTrace();
				}
				String message = String.format("* %s %s", client.nickname,
						"退出聊天室");
				broadcast(message);
			}
		}
	}

	// 系统问候语
	private static void SendHello(String nickName) throws IOException {
		String m = String.format("* %s %s", nickName, "你好");
		for (Server client : connections) {
			if (client.nickname.equals(nickName)) {
				client.session.getBasicRemote().sendText(m);
			}
		}
	}

	// 在线用户
	private static void onlineList() throws IOException {
		String online = "";
		for (Server client : connections) {
			if (online.equals("")) {
				online = client.nickname;
			} else {
				online += "," + client.nickname;
			}
		}
		String m = String.format("* %s %s", "当前在线用户", online);
		for (Server client : connections) {
			client.session.getBasicRemote().sendText(m);
		}
	}
}

三.WebSocket效果图

备注:

package com.test;

import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

public class Server extends WebSocketServer {

	private static final String USER_KEY = "userkey";
	private static final String WEBSOCKET_KEY = "websocketkey";

	/**
	 * 记录当前在线人数
	 */
	private static Map<String, Map<String, Object>> onLineMap = new HashMap<String, Map<String, Object>>();
	/**
	 * 记录在线人的WebSocket对象
	 */
	private static Map<WebSocket, String> userWebSocketMap = new HashMap<WebSocket, String>();
	/**
	 * 记录所有用户
	 */
	private static List<User> users = null;

	private String userId;

	public Server(int port) throws UnknownHostException {
		super(new InetSocketAddress(8080));
	}

	@Override
	public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) {
		// 获取用户id
		String param = clientHandshake.getResourceDescriptor();
		userId = param.substring(1, param.length());
		if (userWebSocketMap.containsValue(userId)) {
			Iterator it = userWebSocketMap.entrySet().iterator();
			while (it.hasNext()) {
				Map.Entry<WebSocket, String> entry = (Entry<WebSocket, String>) it
						.next();
				WebSocket key = entry.getKey();
				String value = entry.getValue();
				if (value.equals(userId)) {
					Message msg = new Message();
					msg.setType("-1");
					sendMessage(key, JsonKit.toJson(msg));
					userWebSocketMap.remove(key);
					key.close();
				}
			}
			userWebSocketMap.put(webSocket, userId);
		} else {
			userWebSocketMap.put(webSocket, userId);
		}
		// 将当前用户id和WebSocket对象加入到在线清单
		Map<String, Object> webSocketMap = null;
		// 从缓存中读取用户信息
		User user = User.cacheGetByUserId(userId);
		if (user != null) {
			webSocketMap = new HashMap<>();
			webSocketMap.put(USER_KEY, user);
			webSocketMap.put(WEBSOCKET_KEY, webSocket);
			onLineMap.put(userId, webSocketMap);
		}
		// 推送消息
		Map<String, Object> msgMap = null;
		msgMap = new HashMap<String, Object>();
		msgMap.put("onLineList", getOnLineUserList());
		msgMap.put("time", new Date());

		// 推送所有用户到前端
		users = User.dao.find(SqlXmlKit.getSql(User.SqlId.ALL));
		msgMap.put("users", users);

		// 推送所有管理员到前端
		List<UserGroup> admins = UserGroup.dao.find(
				SqlXmlKit.getSql(UserGroup.SqlId.ALL), adminId);
		msgMap.put("admins", admins);

		// 推送消息给所有用户
		Iterator<Map.Entry<String, Map<String, Object>>> it = onLineMap
				.entrySet().iterator();
		while (it.hasNext()) {
			Map.Entry<String, Map<String, Object>> entry = it.next();
			String key = entry.getKey();
			WebSocket tempWebSocket = (WebSocket) onLineMap.get(key).get(
					WEBSOCKET_KEY);
			if (tempWebSocket.isClosed()) {
				it.remove();
			}
			sendMessage(tempWebSocket, JsonKit.toJson(msgMap));
		}
		// 根据用户查询未读消息
		List<Message> unReadMsg = Message.dao.find(SqlXmlKit
				.getSql(Message.SqlId.ALL));
		msgMap.put("unReadMsg", unReadMsg);
		sendMessage(webSocket, JsonKit.toJson(msgMap));
		userId = null;
	}

	@Override
	public void onMessage(WebSocket webSocket, String jsonMessage) {
		Message message = JsonKit.parse(jsonMessage,Message.class);
		//根据type类型获取消息(0.群体 1.个人 2.公告 3下线 4.在线诊断 -2已读消息)
		if("0".equals(message.getType())){
			for(String key:onLineMap.keySet()){
				if(key.equals(message.getMfrom())) continue;
					sendMessage((WebSocket)onLineMap.get(key).get(WEBSOCKET_KEY),JsonKit.toJson(message));
					message.save();
				}
		}else if("1".equals(message.getType())){
				sendMessage((WebSocket)onLineMap.get(message.getMto()).get(WEBSOCKET_KEY),JsonKit.toJson(message));
				message.save();
		}else if("2".equals(message.getType())){
				String[] ids = JsonKit.parse(message.getMto(),String[].class);
				for(String id :ids){
					for(String key : onLineMap.keySet()){
						if(onLineMap.containsKey(id)){
							message.setMto(id);
							message.setIstransport("0");
							sendMessage((WebSocket)onLineMap.get(message.getMto()).get(WEBSOCKET_KEY),JsonKit.toJson(message));
							message.save();
						}else{
							message.setMto(id);
							message.setIstransport("1");
							message.save();
						}
						break;
					}
				}
		}else if("3".equals(message.getType())){
			userId = message.getMfrom();
			userWebSocketMap.remove(webSocket);
		}else if("-2".equals(message.getType())){
			String ids = message.getIds();
			List<Message> unMsg = Message.dao.find(,ids);
			for(int i = 0; i<unMsg.size();i++){
				Message msg = (Message)unMsg.get(i);
				msg.setIsTransport("0");
				message.update();
			}
		}else{
			for(String key : onLineMap.keySet()){
				for(int i = 0;i<=3;i++){
					message.setContent("Hello");
					sendMessage((WebSocket)onLineMap.get(key).get(WEBSOCKET_KEY),JsonKit.toJson(message));
					if(i==3){
						WebSocket tempWebSocket = (WebSocket) onLineMap.get(key).get(WEBSOCKET_KEY);
						if(tempWebSocket.isClosed()){
							onLineMap.remove(key);
							userWebSocketMap.remove(tempWebSocket);
						}
						if(tempWebSocket.isClosing()){
							onLineMap.remove(key);
							userWebSocketMap.remove(tempWebSocket);
						}
						if(tempWebSocket.isConnecting()){
							onLineMap.remove(key);
							userWebSocketMap.remove(tempWebSocket);
						}
					}
				}
			}
		}
	
	}

	@Override
	public void onClose(WebSocket arg0, int arg1, String arg2, boolean arg3) {
		if (onLineMap.containsKey(userId)) {
			onLineMap.remove(userId);
			Map<String, Object> msgMap = null;
			for (String key : onLineMap.keySet()) {
				if (key.equals(userId))
					continue;
				WebSocket tempWebSocket = (WebSocket) onLineMap.get(key).get(
						WEBSOCKET_KEY);
				msgMap = new HashMap<String, Object>();
				msgMap.put("onLineList", getOnLineUserList());
				msgMap.put("time", new Date());
				sendMessage(tempWebSocket, JsonKit.toJson(msgMap));
			}
		}
	}

	@Override
	public void onError(WebSocket webSocket, Exception e) {
		e.printStackTrace();
	}

	public void sendMessage(WebSocket webSocket, String message) {
		webSocket.send(message);
	}

	/**
	 * 获取json格式的消息
	 * 
	 * @param message
	 * @return
	 */
	public String getMessage(Message message) {
		return JsonKit.toJson(message);
	}

	public int getOnLineCount() {
		return onLineMap.size();
	}

	public List<User> getOnLineUserList() {
		List<User> userList = new ArrayList<>();
		for (String key : onLineMap.keySet()) {
			userList.add((User) onLineMap.get(key).get(USER_KEY));
		}
		return userList;
	}

}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值