WebSocket学习

本文会基于javaAPI和spring websocket两种方式总结的项目实践经验,希望能够帮助大家

WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

以下 API 用于创建 WebSocket 对象。

var Socket = new WebSocket(url, [protocol] );

WebSocket 属性

1、Socket.readyState

只读属性 readyState 表示连接状态,可以是以下值:

  • 0 - 表示连接尚未建立。

  • 1 - 表示连接已建立,可以进行通信。

  • 2 - 表示连接正在进行关闭。

  • 3 - 表示连接已经关闭或者连接不能打开

2、Socket.bufferedAmount

   只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

WebSocket 事件

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

 

WebSocket 方法

以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:

Socket.send()

使用连接发送数据

Socket.close()

关闭连接

 

前面介绍了WebSocket的基本定义,以及它属性和方法。下面结合demo讲解如何使用它。

首先讲解使用javaeeAPI的方式开发。

开发准备:下载 javaee-api-7.0.jar ,tomcat版本:7.073

这种方式只需要写一个类,并在类上加上@ServerEndpoint注解,这个类就可以被定义为WebSocket服务器。代码如下

package com.cn;

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;


/*
 * ServerEndpoint 注解是一个类层次的注解,他的功能主要是将目前的类定义成一个WebSocket服务器,
 * 注解的值将被用于监听用户连接的终端访问URL的地址,客户端可以通过这个URL来访问这个WebSocket
 */
@ServerEndpoint("/websocket")
public class WebSocketTest {
	//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
	     private static int onlineCount = 0;
	 
	     //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
	     private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>();
	 
	     //与某个客户端的连接会话,需要通过它来给客户端发送数据
	     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(WebSocketTest 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() {
	         WebSocketTest.onlineCount++;
	     }
	 
	     public static synchronized void subOnlineCount() {
	         WebSocketTest.onlineCount--;
	     }
}

 

前端代码如下:

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

代码很简单,这里我只说下这个路径问题,如果项目启动成功,但是一打开页面就报404或者WebSocket服务已停止。那么你就需要检查下你创建WebSocket的路径了。就是以下这句话,是否按照我说的写。这里要根据你的实际项目和你自己定义的路径来写。

websocket = new WebSocket("ws://localhost:8080/项目名/@ServerEndpoint注解定义的路径");

其实最简单的方法是这样:

<%
String path = request.getContextPath();
//获取当前项目的路径
String basePath = request.getServerName()+":"+request.getServerPort()+path+"/";
%>
var path = '<%=basePath %>';
websocket = new WebSocket("ws://" +path +"webSocket");

最后说下环境问题,我的demo环境时JDK 1.7  tomcat7.0.73   如果时JDK1.8你需要使用tomcat8以上的版本。

下面说下Spring WebSocket的方式开发

首先说下环境:JDK1.7 Spring 版本4.2.2 tomcat 7.0.73 

Spring从4.0开始支持WebSocket,我们需要下载对应的jar包   spring-websocket-4.2.2.RELEASE.jar以及Spring core包就不说了自行下载

环境准备好之后,就可以动手开发了。

首先是配置信息,有两种方式实现。一种是代码方式实现配置,一种是xml编写的方式。首先讲解代码方式实现:

package com.cn.SpringWebSocket.webSocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * [说明/描述]
 * 代码方式配置xml文件
 * @date 2018-7-24 下午3:57:10
 */

@Configuration//指明该类为Spring 配置类
@EnableWebSocket//声明该类支持WebSocket
public class WebSocketConfig implements WebSocketConfigurer {

	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		String webSocketUrl = "webSocket";
		registry.addHandler(getHandler(), webSocketUrl).addInterceptors(new WebSocketInterceptor());
		String SockJSUrl = "sockJS";
		registry.addHandler(getHandler(), SockJSUrl).addInterceptors(new WebSocketInterceptor()).withSockJS();
	}
	@Bean
	public WebSocketHandler getHandler(){
		return new WebSocketHandler();
	}
}

接下来说下xml配置方式实现:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:tx="http://www.springframework.org/schema/tx" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:websocket="http://www.springframework.org/schema/websocket"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd 
	http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
    http://www.springframework.org/schema/websocket   
    http://www.springframework.org/schema/websocket/spring-websocket.xsd"
	default-lazy-init="true">
	<!-- 开启AOP -->
    <aop:aspectj-autoproxy proxy-target-class="true" />
	<!-- 自动扫描与装配bean -->
	<context:component-scan base-package="com.cn"></context:component-scan>
	<!-- 配置websocket:handlers -->
	<bean id="websocket" class="com.cn.SpringWebSocket.webSocket.WebSocketHandler"/>
	<websocket:handlers>
		<websocket:mapping  path="/webSocket" handler="websocket"/>
		<websocket:handshake-interceptors>
			<bean class="com.cn.SpringWebSocket.webSocket.WebSocketInterceptor"/>
		</websocket:handshake-interceptors>
	</websocket:handlers>
</beans>

主要需要再namespace中添加


xmlns:websocket="http://www.springframework.org/schema/websocket"   
http://www.springframework.org/schema/websocket   
http://www.springframework.org/schema/websocket/spring-websocket.xsd" 

再配置配置websocket:handlers

<bean id="websocket" class="com.cn.SpringWebSocket.webSocket.WebSocketHandler"/>
	<websocket:handlers>
		<websocket:mapping  path="/webSocket" handler="websocket"/>
		<websocket:handshake-interceptors>
			<bean class="com.cn.SpringWebSocket.webSocket.WebSocketInterceptor"/>
		</websocket:handshake-interceptors>
	</websocket:handlers>

效果和上面利用代码配置是一样的,这个使用那种方式看具体项目情况。

根据配置文件我们知道,我们需要创建一个WebSocketHandler类,和WebSocketInterceptor拦截器

那么接下来我们就创建这两个类吧

WebSocketInterceptor拦截器

package com.cn.SpringWebSocket.webSocket;

import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

/**
 * [说明/描述]
 * WebSocket握手拦截器用来拦截和处理客户端和服务器端分别在握手前和握手后的事件,
 * 我们这里添加这个拦截器是为了清晰的知道什么时候以及是否握手成功。以及给WebSocketHandler传递参数
 * @date 2018-7-24 下午4:17:24
 */

public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor{

	@Override
	public void afterHandshake(ServerHttpRequest serverhttprequest,
			ServerHttpResponse serverhttpresponse,
			WebSocketHandler websockethandler, Exception exception) {
		System.out.println("握手之后");
		super.afterHandshake(serverhttprequest, serverhttpresponse, websockethandler,
				exception);
	}

	@Override
	public boolean beforeHandshake(ServerHttpRequest request,
			ServerHttpResponse response, WebSocketHandler wsHandler,
			Map<String, Object> attributes) throws Exception {
		System.out.println("握手之前");
		if(request instanceof ServletServerHttpRequest){
			ServletServerHttpRequest  serverRequest = (ServletServerHttpRequest)request;
			//避免每次session都重新创建,导致获取不到正确的session
			HttpSession session = serverRequest.getServletRequest().getSession(false);
			if(session != null){
				String username = (String) session.getAttribute("username");
				if(username != null){
					//给Handler传递 参数使用
					attributes.put("WENSOCKET", username);
				}
			}
			
			
		}
		
		return super.beforeHandshake(request, response, wsHandler, attributes);
	}
	
}

WebSocketHandler

package com.cn.SpringWebSocket.webSocket;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.alibaba.fastjson.JSON;

/**
 * [说明/描述]
 * WebSocket 处理握手
 * @date 2018-7-24 下午4:02:07
 */
@Service
public class WebSocketHandler extends TextWebSocketHandler{
	private static  ConcurrentMap<String, WebSocketSession> users = new ConcurrentHashMap<String, WebSocketSession>(32); 
	
	/**
	 * 通讯连接时,调用
	 */
	@Override
	public void afterConnectionEstablished(WebSocketSession websocketsession)
			throws Exception {
		String username = (String)websocketsession.getAttributes().get("WENSOCKET");
		users.put(username, websocketsession);
		websocketsession.sendMessage(new TextMessage(username + "上线了..."));
	}
	/**
	 * 连接关闭时,调用这个方法
	 */
	@Override
	public void afterConnectionClosed(WebSocketSession websocketsession,
			CloseStatus closestatus) throws Exception {
		String username = (String)websocketsession.getAttributes().get("WENSOCKET");
		websocketsession.sendMessage(new TextMessage(username + "下线了..."));
		users.remove(username);
	}
	/**
	 * 处理字符串消息
	 */
	@Override
	protected void handleTextMessage(WebSocketSession websocketsession,
			TextMessage textmessage) throws Exception {
		String username = (String)websocketsession.getAttributes().get("WENSOCKET");
		System.out.println(username);
		System.out.println(new String(textmessage.asBytes(),"utf-8"));
        //解析客户端传来的json格式的数据
		String json = new String(textmessage.asBytes(),"utf-8");
		Map maps = (Map)JSON.parse(json);
		String toUser = (String) maps.get("toName");
		String tomessage = (String) maps.get("tomessage");
		System.out.println(toUser + ":" + tomessage);
		sendMessageToUser(toUser,tomessage);
		websocketsession.sendMessage(new TextMessage(textmessage.getPayload()));
	}
	/**
	 * 传出错误的处理
	 */
	@Override
	public void handleTransportError(WebSocketSession websocketsession,
			Throwable throwable) throws Exception {
		System.out.println("发送错误了...");
	}
	/**
	 * 处理 WebSocketMessage 消息
	 *//*
	@Override
	public void handleMessage(WebSocketSession session,
			WebSocketMessage<?> message) throws Exception {
		String username = (String)session.getAttributes().get("WENSOCKET");
		System.out.println(11111);
		session.sendMessage(new TextMessage((CharSequence) message.getPayload()));
	}*/
	/**
	 * ping-pong
	 */
	@Override
	protected void handlePongMessage(WebSocketSession websocketsession,
			PongMessage pongmessage) throws Exception {
		
	}
	public void sendMessageToUser(String username,String message){
		if(users.containsKey(username)){
			WebSocketSession session = (WebSocketSession)users.get(username);
			try {
				session.sendMessage(new TextMessage(message));
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	
}

这里有一个坑,

protected void handleTextMessage(WebSocketSession websocketsession,
			TextMessage textmessage) throws Exception {}

public void handleMessage(WebSocketSession session,
			WebSocketMessage<?> message) throws Exception {}

注意:这两个类不可以同时重写。只能重写一个,不然客服端调用send()方法,没反应。

客户端代码:

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

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <base href="<%=basePath%>">
    
    <title>Spring 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">
  </head>
  
  <body>
  
  连接服务器:<input type="button" name="connect" onclick="ConnectWeb()" value="连接服务器" /><br/>
  消息接收方:<input type="text"  name="toName" id ="toName"/><br/>
  消&nbsp息&nbsp内&nbsp容:<input type="text"  name="message" id ="message"/><br/>
  <input type="button" onclick="sendmsg()" value="发送" /><hr/>
  <div id="outMessage">123</div>
 </body>
  <script type="text/javascript">
  	var websocket;
  	var path = '<%=basePath2%>';
  	//建立连接,连接服务器
  	function ConnectWeb(){
  		if('WebSocket' in window){
  			websocket = new WebSocket("ws://" +path +"webSocket");
  		}else if('MozWebSocket' in window ){
  			websocket = new MozWebSocket("ws://" +path +"webSocket");
  		}else{
			websocket = new SockJS("http://" +path +"sockJS");	
  		}
  	
  		websocket.onmessage = function(event){
  			document.getElementById('outMessage').innerHTML += event.data +'<br/>';
  		};
  		websocket.onerror = function(event){
  		};
  		websocket.onopen = function(event){
  		
  		};
  	}
  	function sendmsg(){
  		var tomessage = document.getElementById('message').value;
  		//发送给谁
  		var toName = document.getElementById('toName').value;
  		var obj = {"toName":toName,"tomessage":tomessage};
  		console.log(obj);
  		websocket.send(JSON.stringify(obj));
  	}
  </script>
</html>

这里我再客户端使用了json数据格式,传递到后台需要根据二进制再解码以及自行解析。

到此代码全部写完了。运行下看看效果吧

以上是我的代码,有不足之处请各位大神多多指教!

没有更多推荐了,返回首页