简易Java web在线聊天-websocket

原创 2017年07月17日 10:59:15

公司做了伪在线客服系统。自己为了技术,提前研究了使用websocket进行长连接通信。写了个简单的在线聊天demo,算是对自己的交代,后期会抽时间优化,完善流程等。
语言:Java
客户端:html5 实现的socket作为客户端。
服务器:使用spring-mvc + spring-websocket+spring-mesaging作为信息接收处理。
测试容器:jetty9+

1.【websocket是什么】

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。
在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
1. Header 互相沟通的Header是很小的-大概只有 2 Bytes
2. Server Push 服务器的推送,服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
------来源百度百科

2.【websocket API简单介绍】

var ws = new WebSocket("ws://www.websocket.org",[protocol]);
说明:WebSocket的构造函数需要一个URL参数和一个可选的协议参数(一个或者多个协议的名字),协议的参数例如XMPP(Extensible Messaging and Presence Protocol)、SOAP(Simple Object Access Protocol)或者自定义协议。而URL参数需要以WS://或者WSS://开头,例如:ws://www.websocket.org。
在第一次握手之后,和协议的名称一起,客户端会发送一个Sec-WebSocket-Protocol 头,服务端会选择0个或一个协议,响应会带上同样的Sec-WebSocket-Protocol 头,否则会关闭连接。通过协议协商(Protocol negotiation ),我们可以知道给定的WebSocket服务器所支持的协议和版本,然后应用选择协议使用。

2.1 【websocket事件】

WebSocket API是纯事件驱动,通过监听事件可以处理到来的数据和改变的链接状态。客户端不需要为了更新数据而轮训服务器。服务端发送数据后,消息和事件会异步到达。
WebSocket编程遵循一个异步编程模型,只需要对WebSocket对象增加回调函数就可以监听事件。你也可以使用addEventListener()方法来监听。而一个WebSocket对象分四类不同事件。

Open事件:一旦服务器响应连接,就会触发open事件。响应的回调函数为onopen。
// Event handler for the WebSocket connection opening
ws.onopen = function(e) {
   console.log("Connection open...");
};
Message事件:open事件后,意味着协议握手结束,服务器端同意客户端连接,进做好通信准备。发送消息触发的回调为:onmessage。
ws.onmessage = function(e) {
   if(typeof e.data === "string"){
      console.log("String message received", e, e.data);
   } else {
      console.log("Other message received", e, e.data);
   }
};
Error事件:如果发生意外的失败则会触发error事件,error事件会导致连接关闭,很快就会收到关闭事件回调。意外失败的回调为:onerror。
ws.onerror = function(e) {
   console.log("WebSocket Error: " , e);
   //Custom function for handling errors
   handleErrors(e);
};
close事件:当连接关闭的时候,触发改事件。对应的回调方法为:onclose。
ws.onclose = function(e) {
   console.log("Connection closed", e);
};

2.2:【websocket方法】

send方法:当onopen触发后,连接正常。则可以使用send方法向服务器发送消息, send方法还可以发送二进制文本。如:ws.send("hello world");
close方法:关闭websocket连接。如果连接已经关闭,则该方法什么也不做。

2.3 【websocket属性】

属性 状态
WebSocket.CONNECTING 0 连接正在进行,但还没有建立
WebSocket.OPEN 1 连接已经建立,可以发送消息。
WebSocket.CLOSING 2 连接正在进行关闭握手
WebSocket.CLOSED 3 连接已经关闭或不能打开

3 【Demo示例】

demo模拟多人在线聊天,当用户选中要发送信息的在线用户,则可进行在线聊天。


3.1主要依赖关系
<!-- spring dependency start -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>${spring-version}</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>${spring-version}</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-support</artifactId>
  <version>${spring-version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>${spring-version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-messaging</artifactId>
  <version>${spring-version}</version>
</dependency>

<!-- spring dependency end -->

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.20</version>
</dependency>

3.2 【消息处理类】

简单的消息处理,继承spring-websocket,文本消息处理类,实现文本消息处理。并缓存当前在线用户,当连接失败、发送消息失败,简单认为用户离线删除session。未处理socket session 连接超时、用户长时间无通信容器session超时、页面刷新重连等。
package cc.hu.test.websocket.skthandler;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import com.alibaba.fastjson.JSON;

import cc.hu.test.websocket.common.User;
import cc.hu.test.websocket.controller.MainController;

/**
 * @author root
 *
 */
@Component
public class MyTextWebSocketHandler extends AbstractWebSocketHandler {
	
	public static Map<Long, WebSocketSession> userWebsocketSessionMap;
	
	static {
		userWebsocketSessionMap = new HashMap<Long, WebSocketSession>();
	}
	
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		Long uid = (Long) session.getAttributes().get("uid");
		userWebsocketSessionMap.put(uid, session);
		User u = MainController.loginUserMap.get(uid);
		CCMsg welcome = new CCMsg();
		welcome.setTo(uid);
		welcome.setMsg("[系统消息]欢迎[" + u.getNickName() + "]进入聊天");
		welcome.setMsgType(MsgType.CHAT_MSG);
		welcome.setFromName("系统");
		welcome.setSendDate(new Date());
		welcome.setFrom(0L);
		session.sendMessage(new TextMessage(JSON.toJSONString(welcome)));
		//群发通知在线列表更新
		sendOnlineUserMsg();
	}

	@Override
	public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
		if (session.isOpen())
			session.close();
		Iterator<Entry<Long, WebSocketSession>> mapItr = userWebsocketSessionMap.entrySet().iterator();
		while (mapItr.hasNext()) {
			final Entry<Long, WebSocketSession> one = mapItr.next();
			Long key = one.getKey();
			WebSocketSession value = one.getValue();
			if (value.getId().equals(session.getId())) {
				mapItr.remove();
				System.out.println("发送消息错误, 移除socket[key:" + key + "\t" + MainController.loginUserMap.get(key));
				//移除在线用户
				MainController.loginUserMap.remove(key);
				break;
			}
		}
		exception.printStackTrace();
		//群发通知在线列表更新
		sendOnlineUserMsg();
	}

	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		System.out.println("websocket用户关闭:" + JSON.toJSONString(status));
		Iterator<Entry<Long, WebSocketSession>> mapItr = userWebsocketSessionMap.entrySet().iterator();
		while (mapItr.hasNext()) {
			final Entry<Long, WebSocketSession> one = mapItr.next();
			Long key = one.getKey();
			WebSocketSession value = one.getValue();
			if (value.getId().equals(session.getId())) {
				mapItr.remove();
				System.out.println("用户关闭, 移除socket[key:" + key + "\t" + MainController.loginUserMap.get(key));
				MainController.loginUserMap.remove(key);
				break;
			}
		}
		//群发通知在线列表更新
		sendOnlineUserMsg();
	}

	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		if (message.getPayloadLength() == 0)
			return;
		System.out.println("[收到消息====>]" + message.getPayload().toString());
		CCMsg msg = JSON.parseObject(message.getPayload().toString(), CCMsg.class);
		WebSocketSession toSession = userWebsocketSessionMap.get(msg.getTo());
		if (toSession != null && toSession.isOpen()) {
			msg.setSendDate(new Date());
			toSession.sendMessage(new TextMessage(JSON.toJSONString(msg)));
		} else  {
			CCMsg welcome = new CCMsg();
			welcome.setTo(msg.getFrom());
			welcome.setMsg("[系统消息]对方已下线,请稍后重试...");
			welcome.setMsgType(MsgType.CHAT_MSG);
			welcome.setFromName("系统");
			welcome.setSendDate(new Date());
			welcome.setFrom(0L);
			session.sendMessage(new TextMessage(JSON.toJSONString(welcome)));
			//群发通知在线列表更新
			sendOnlineUserMsg();
		}
	}
	//发送在线信息更新消息
	public static void sendOnlineUserMsg() {
		CCMsg msg = new CCMsg();
		msg.setMsg(JSON.toJSONString(MainController.getOnLineUserList()));
		msg.setMsgType(MsgType.ONLINE_USER_MSG);
		msg.setFromName("系统");
		msg.setSendDate(new Date());
		msg.setFrom(0L);
		Iterator<Entry<Long, WebSocketSession>> mapItr = userWebsocketSessionMap.entrySet().iterator();
		while (mapItr.hasNext()) {
			final Entry<Long, WebSocketSession> one = mapItr.next();
			Long key = one.getKey();
			msg.setTo(key);
			WebSocketSession value = one.getValue();
			try {
				value.sendMessage(new TextMessage(JSON.toJSONString(msg)));
			} catch (IOException e) {
				e.printStackTrace();
			}
			
		}
	}
	
}

3.3 【websocket握手拦截器】

package cc.hu.test.websocket.skthandler;

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.HandshakeInterceptor;

/**
 * spring websocket 握手拦截器
 * @author sen.hu
 *
 */
public class HandShake implements HandshakeInterceptor {

	@Override
	public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Map<String, Object> attributes) throws Exception {
		System.out.println("Websocket用户:[uid:" + ((ServletServerHttpRequest) request).getServletRequest().getSession(false).getAttribute("uid") + "]已经建立连接" );
		if (request instanceof ServletServerHttpRequest) {
			ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
			HttpSession session = servletRequest.getServletRequest().getSession(false);
			Long uid = (Long) session.getAttribute("uid");
			//标记用户
			if (uid != null) {
				attributes.put("uid", uid);
			} else
				return false;
		}
		return true;
	}

	@Override
	public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
			Exception exception) {
		
	}

}

3.4  【spring websocket配置类】

package cc.hu.test.websocket.skthandler;

import javax.annotation.Resource;

import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

//	@Resource
//	MyWebSocketHandler handler;
	@Resource
	MyTextWebSocketHandler textHandler;
	
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		/*
		registry.addHandler(handler, "/ws").addInterceptors(new HandShake());
		//sock js处理类
		registry.addHandler(handler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();
		*/
		
		registry.addHandler(textHandler, "/ws").addInterceptors(new HandShake());
		//sock js处理类
		registry.addHandler(textHandler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();
	}
	
}

3.5 【聊天页面及JS】

页面显示:
<%@page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>cc的聊天</title>
<link rel="stylesheet" type="text/css" href="http://127.0.0.1:8087/static/css/style.css">
</head>
<body>
	<input type="hidden" value="${uid }" id="uid"/>
	<input type="hidden" value="${userName }" id="username"/>
	<input type="hidden" value="${path }" id="path"/>
	<input type="hidden" value="" id="toUser"/>
	
	<!---->
	<div>欢迎[${userName }]</div>
	<div>
		<div class="online_list">
		  <ul id="onlineList" class="onlinelist"></ul>
		</div>
		<div id="content" class="content"></div>
	</div>

	<input type="text" placeholder="请输入要发送的信息" id="msg" class="msg">
	<input type="button" value="发送" class="send" onclick="sendMsgToServer(2)" >
	<input type="button" value="清空" class="clear" onclick="clearAll()">
	
	<script type="text/javascript" src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
	<script type="text/javascript" src="http://127.0.0.1:8087/static/js/talk.js"></script>
</body>
</html>
页面JS:
/**
 * Created by root on 7/15/17.
 */

window.path = $("#path").val();
window.uid = $("#uid").val();
window.fromUserId = uid;
window.sendTo = $("#toUser").val();
window.fromUserName = $("#username").val();
window.ws;

Date.prototype.Format = function (fmt) { //author: meizz 
    var o = {
        "M+": this.getMonth() + 1, //月份 
        "d+": this.getDate(), //日 
        "h+": this.getHours(), //小时 
        "m+": this.getMinutes(), //分 
        "s+": this.getSeconds(), //秒 
        "q+": Math.floor((this.getMonth() + 3) / 3), //季度 
        "S": this.getMilliseconds() //毫秒 
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (var k in o)
    if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return fmt;
}

function changeBgColor(id, type) {
	var toid = $("#toUser").val();
    if (type == 1) {//add css
    	$("#li"+ id).css("cursor", "hand");
    	if (toid != id) {
    		$("#li" + toid).css("background", "white");
    	}
        
        $("#li"+ id).css("background", "burlywood");
        
    } else if (type == 2) {
        if (toid != id) {
            $("#li"+ id).css("background", "white");
        }
        $("#li"+ toid).css("background", "burlywood");
        $("#li"+ id).css("cursor", "normal");
    }

}

function sendMsgToServer(type) {
    var msg = "";
    var toUserId = $("#toUser").val();
    if (toUserId == fromUserId) {
        alert("不能给自己发消息!");
        return;
    }
    switch(type) {
        case 1:
            msg = "hello";
            break;
        case 2:
            msg = $("#msg").val();
            break;
        default:
            msg = $("#msg").val();
    }
    msg = $.trim(msg);
    if (msg.length == 0) {
        alert("发送信息不能为空!");
        return;
    }
    if (ws.readyState == WebSocket.OPEN) {
    	
        var sendData = {};
        sendData["from"] = fromUserId;
        sendData["fromName"] = fromUserName;
        sendData["to"] = toUserId;
        sendData["msg"] = msg;
        console.log("发送消息====>" + JSON.stringify(sendData));
        ws.send(JSON.stringify(sendData));
        $("#msg").val("");
        $("#content").append("<div class='tmsg'><label class='name'>我 "+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</label><div class='tmsg_text'>"+sendData.msg+"</div></div>");
    } else {
        alert("Websocket:连接状态异常");
        console.log("Websocket:连接状态异常,readyState=" + ws.readyState);
    }

}

function setToUserId(toUserId) {
    $("#toUser").val(toUserId);
    $("#li"+ toUserId).css("background", "burlywood");
}

function updateOnlineList(rcv) {
    console.log("在线用户更新:" + rcv.msg);
    var onlineList =  eval(rcv.msg);
    var html="";
    for (var o in onlineList) {
        console.log(o);
        html += "<li id='li" + onlineList[o].userId +"' onclick='setToUserId("+ onlineList[o].userId+ ")' onmouseout='changeBgColor("+onlineList[o].userId+", 2)' onmouseenter='changeBgColor("+onlineList[o].userId+", 1)'>" + onlineList[o].nickName + "</li>";
    }
    $("#onlineList").html(html);
}

function updateMsgContent(rcv) {
    var textCss = rcv.from == 0 ? "sfmsg_text" : "fmsg_text";
    var html = "<div class='fmsg'>"
        + "<label class='name'>"
        + rcv.fromName + " " + rcv.sendDate
        + "</label>"
        + "<div class='" + textCss + "'>" + rcv.msg + "</div>"
        + "</div>";
    $("#content").append(html);
    if (rcv.from != 0) {
    	$("#toUser").val(rcv.from);
    	$("#li"+ rcv.from).css("background", "burlywood");
    }
    scrollToBottom();
}

/**初始化websocket*/
function initWebsocket() {
    if ("WebSocket" in window)
        ws = new WebSocket("ws://" + path + "/ws?uid=" + uid);
    else if ("MozWebSocket" in window)
        ws = new WebSocket("ws://" + path + "/ws?uid" + uid);
    else
        ws = new SockJS("http://" + path + "/ws/sockjs?uid=" + uid);
    
    ws.onopen = function(event) {
        console.log("websocket:链接...OK!");
        console.log(event);
        //sendMsgToServer(1);
    };
    ws.onmessage = function (event) {
        var rcv = JSON.parse(event.data);
        console.log("websocket:收到一条信息<=====" + rcv);
        console.log(event);
        var msgType = rcv.msgType;
        switch (msgType) {
            case 0x01:
                updateOnlineList(rcv);
                break;
            case 0x10:
                updateMsgContent(rcv);
                break;
            default:
                updateMsgContent(rcv);
        }

    };
    ws.onerror = function (event) {
        console.log("websocket:发生错误!");
        console.log(event);
    };
    ws.onclose = function (event) {
        console.log("WebSocket:已关闭");
        console.log(event);
    }
}

function scrollToBottom() {
    var div = document.getElementById('content');
    div.scrollTop = div.scrollHeight;
}

function clear() {
    $("#content").empty();
}

window.onload = function() {

    initWebsocket();

    document.onkeydown = function(event) {
        var code;
        if (window.event)
            code = window.event.keyCode;
        else
            code = event.which;
        if (code == 13)
            sendMsgToServer();
    }

}

3.6【登陆界面及登陆处理Controller】

简单的登陆界面及登陆处理逻辑,代码及具体实现【略】


后记:只是一个简单的demo示例,很多细节没有考虑,处理。只实现了最简单文本消息处理,以后可以尝试二进制,增加自定义协议、及安全处理等,完善demo。



版权声明:本文为博主原创文章,未经博主允许不得转载。

jmeter进行websocket压力测试

准备websocket的插件jar包jmeter默认不支持websocket连接,需要下载额外的jar包到jmeter目录的\lib\ext\目录下 下载连接:jmeter的websocket插件j...
  • ifrozen
  • ifrozen
  • 2016年11月11日 11:51
  • 2159

Java-WebSocket 项目的研究(二) :客户端连接服务器并发送消息实例

继续之前的教程,我们先下载项目源代码到本地,我们就能看到项目代码的结构如图一所示: 图一 然后,我们在eclipse中新建一个java project,并将java目录下的org目...

Java WebSocket构建简易聊天web程序

继上一篇我转载的文章:http://blog.csdn.net/snakemoving/article/details/78453550 改善他的代码,进行构建简易聊天web程序 大概思路: ...

java web 在线聊天

  • 2013年12月04日 16:41
  • 1.87MB
  • 下载

Spring -websocket实现简易在线聊天

引入spring-websocket包 org.springframework spring-websocket ${websocket.version} 1....

java WebSocket 简易聊天消息推送

环境:
  • hzw2312
  • hzw2312
  • 2014年11月18日 20:02
  • 43664

Java、WebSocket、HTML简易聊天室

WebSockt协议是一种双向通信的解决方案,减少了频繁建立连接所带来的开销,使得客户端和服务端的通信更加及时。与轮询(polling)和(Comet)技术相比,WebSockt在双端通信上有明显的优...

Java WebSocket编程与网页简易聊天室

在webSocket还未引入前,许多开发人员通过各种非正规手段来完成更新网站的最新信息和到所有当前访问者的任务,其中一种手段就是通过浏览器向服务器轮询更新,但这种手段的网络延迟比较明显,其用户体验比较...
  • liu_c_y
  • liu_c_y
  • 2016年06月29日 21:08
  • 6571

java WebSocket 简易聊天消息推送

环境: JDK.1.7.0_51 apache-tomcat-7.0.53 Java jar包:tomcat-coyote.jar、tomcat-juli.jar、websocket...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:简易Java web在线聊天-websocket
举报原因:
原因补充:

(最多只允许输入30个字)