Java后端WebSocket的Tomcat实现

文章摘要随着互联网的发展,传统的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上才能运行。

新建一个dynamic web项目:

客户端(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>
<html>
  <head>
    <base href="<%=basePath%>">
    <title>My WebSocket</title>
  </head>
  
  <body>
    Welcome<br/>
    <input id="text" type="text" /><button onclick="send()">Send</button>    <button onclick="closeWebSocket()">Close</button>
    <div id="message">
    </div>
  </body>
  
  <script type="text/javascript">
  	var websocket = null;
  	
  	//判断当前浏览器是否支持WebSocket
  	if('WebSocket' in window){
  		websocket = new WebSocket("ws://localhost:8080/MyWebSocket/websocket");
  	}
  	else{
  		alert('Not support websocket')
  	}
  	
  	//连接发生错误的回调方法
  	websocket.onerror = function(){
  		setMessageInnerHTML("error");
  	};
  	
  	//连接成功建立的回调方法
  	websocket.onopen = function(event){
  		setMessageInnerHTML("open");
  	}
  	
  	//接收到消息的回调方法
  	websocket.onmessage = function(event){
  		setMessageInnerHTML(event.data);
  	}
  	
  	//连接关闭的回调方法
  	websocket.onclose = function(){
  		setMessageInnerHTML("close");
  	}
  	
  	//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
  	window.onbeforeunload = function(){
  		websocket.close();
  	}
  	
  	//将消息显示在网页上
  	function setMessageInnerHTML(innerHTML){
  		document.getElementById('message').innerHTML += innerHTML + '<br/>';
  	}
  	
  	//关闭连接
  	function closeWebSocket(){
  		websocket.close();
  	}
  	
  	//发送消息
  	function send(){
  		var message = document.getElementById('text').value;
  		websocket.send(message);
  	}
  </script>
</html>

  Java Web后端代码

package com.chen.websocket;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

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;

//该注解用来指定一个URI,客户端可以通过这个URI来连接到WebSocket。类似Servlet的注解mapping。无需在web.xml中配置。
@ServerEndpoint("/websocket")
public class MyWebSocket {
	//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
	private static int onlineCount = 0;
	
	//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
	private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
	
	//与某个客户端的连接会话,需要通过它来给客户端发送数据
	private Session session;
	
	/**
	 * 连接建立成功调用的方法
	 * @param session  可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
	 */
	@OnOpen
	public void onOpen(Session session){
		this.session = session;
		webSocketSet.add(this);		//加入set中
		addOnlineCount();			//在线数加1
		System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
	}
	
	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public void onClose(){
		webSocketSet.remove(this);	//从set中删除
		subOnlineCount();			//在线数减1		
		System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
	}
	
	/**
	 * 收到客户端消息后调用的方法
	 * @param message 客户端发送过来的消息
	 * @param session 可选的参数
	 */
	@OnMessage
	public void onMessage(String message, Session session) {
		System.out.println("来自客户端的消息:" + message);
		
		//群发消息
		for(MyWebSocket item: webSocketSet){				
			try {
				item.sendMessage(message);
			} catch (IOException e) {
				e.printStackTrace();
				continue;
			}
		}
	}
	
	/**
	 * 发生错误时调用
	 * @param session
	 * @param error
	 */
	@OnError
	public void onError(Session session, Throwable error){
		System.out.println("发生错误");
		error.printStackTrace();
	}
	
	/**
	 * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
	 * @param message
	 * @throws IOException
	 */
	public void sendMessage(String message) throws IOException{
		this.session.getBasicRemote().sendText(message);
		//this.session.getAsyncRemote().sendText(message);
	}

	public static synchronized int getOnlineCount() {
		return onlineCount;
	}

	public static synchronized void addOnlineCount() {
		MyWebSocket.onlineCount++;
	}
	
	public static synchronized void subOnlineCount() {
		MyWebSocket.onlineCount--;
	}
}

该Demo在以下环境测试过:

1.Jdk7+Tomcat7.0.47

2.Jdk7+Tomcat7.0.52

3.Jdk7+Tomcat8.0.3

4.Jdk7+Glassfish4

注意事项:在Eclipse或者MyEclipse中,需要添加Tomcat的library。直接拷贝Tomcat里lib目录下的jar包有时会出错。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

1 条评论
登录 后参与评论

相关文章

来自专栏张戈的专栏

Linux下巧用chattr、watch命令的实例

一、起因 前些日子,张戈在日志备份服务器加入了日志压缩的计划任务(详见服务器日志备份超节省空间的思路),结果发现监控总是发来 tar 的报警,提示存在 tar ...

3338
来自专栏bboysoul

Html编辑器推荐

notepad 介绍:Notepad++ 是一款非常有特色的编辑器,是开源软件,可以免费使用。支持的语言: C, C++ , Java , C#, XML, ...

973
来自专栏技术小黑屋

Android中HTTP相关的API

Android中大多数应用都会发送和接受HTTP请求,在Android API中主要由两个HTTP请求的相关类,一个是HttpURLConnection,另一个...

703
来自专栏琯琯博客

超好用的谷歌浏览器、Sublime Text、Phpstorm、油猴插件合集

一、谷歌浏览器插件 二、Sublime Text 插件 三、Phpstorm 插件 四、油猴脚本 4.1 脚本网站 4.2 自用的脚本 五、相关链接 ? 分享...

5188
来自专栏Web项目聚集地

RESTful 接口实现简明指南

在我所见过的 RESTful 接口的实现中,以 GitHub 最让人惊叹。我第一次如此强烈得感受到 REST 接口的美妙,完全满足了我所期待的「接口的形式美感」...

581
来自专栏张善友的专栏

在你的网站集成Wiki系统 WikiPlex

Wikiplex 是一种在 .NET Framework 上所开发,具有处理 Wiki 编辑宏功能的小型函式库组件,它提供了数种编辑样式的格式,以让使用者可以类...

1988
来自专栏张戈的专栏

WordPress评论ajax动态加载,解决静态缓存下评论不更新问题

这是一个历史遗留问题,自从博客部署了 PHP 纯静态缓存之后,所有页面都是 html 静态内容了,而且在七牛 CDN 静态分离之后,速度更是达到极致! ? 不过...

2986
来自专栏FreeBuf

从远程桌面服务到获取Empire Shell

本文将为大家详细介绍如何在只能访问远程桌面服务,且有 AppLocker 保护 PowerShell 处于语言约束模式下获取目标机器 Empire shell。...

954
来自专栏Java Edge

知识点提纲1. 二叉树的深度数据库:操作系统计算机网络

2987
来自专栏Python中文社区

听说 Django 与 celery 配合更美味

博客主页:https://www.zhihu.com/people/tu-dou-dou-27-10

982

扫码关注云+社区