开头还是说点废话吧。WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

从GitEE上搞了一个项目下来,然后精简和调整,尽量把代码调整到一看就懂。
项目就是普通的SpringBoot,没有用的东西全部移除,只需要一张表,当然也可以不用登陆,这样更精简。
核心代码就是ServerEndpoint的处理类:
package boot.spring.service;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import boot.spring.po.Message;
@ServerEndpoint("/webSocket/{username}")
@Component
public class WebSocketServer {
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
// 建立连接成功调用
@OnOpen
public void onOpen(Session session, @PathParam(value = "username") String userName) {
// 链接建立,存储链接对象
sessionPools.put(userName, session);
// 广播上线消息
Message msg = new Message();
msg.setFrom("系统消息");
msg.setDate(new Date());
msg.setTo("0");
msg.setText(userName);
broadcast(JSON.toJSONString(msg, true));
}
// 关闭连接时调用
@OnClose
public void onClose(@PathParam(value = "username") String userName) {
sessionPools.remove(userName);
// 广播下线消息
Message msg = new Message();
msg.setFrom("系统消息");
msg.setDate(new Date());
msg.setTo("-2");
msg.setText(userName);
broadcast(JSON.toJSONString(msg, true));
}
// 收到客户端信息后,根据接收人的username把消息推下去或者群发
// to=-1群发消息
@OnMessage
public void onMessage(String message) throws IOException {
Message msg = JSON.parseObject(message, Message.class);
msg.setDate(new Date());
if (msg.getTo().equals("-1")) {
broadcast(JSON.toJSONString(msg, true)); // -1群发
} else {
sendInfo(msg.getTo(), JSON.toJSONString(msg, true));
}
}
// 错误时调用
@OnError
public void onError(Session session, Throwable throwable) {
throwable.printStackTrace();
}
// 给指定用户发送信息
public void sendInfo(String userName, String message) {
Session session = sessionPools.get(userName);
try {
sendMessage(session, message);
} catch (Exception e) {
e.printStackTrace();
}
}
// 群发消息
public void broadcast(String message) {
for (Session session : sessionPools.values()) {
try {
sendMessage(session, message);
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
// 发送消息
public void sendMessage(Session session, String message) throws IOException {
if (session != null) {
synchronized (session) {
session.getBasicRemote().sendText(message);
}
}
}
public static ConcurrentHashMap<String, Session> getSessionPools() {
return sessionPools;
}
}聊天室页面代码:
<!DOCTYPE>
<html>
<head>
<title>聊天室</title>
<link rel="stylesheet" href="./css/bootstrap.min.css" />
<script src="./js/jquery-1.12.3.min.js"></script>
<script src="./js/bootstrap.min.js"></script>
<style>
body {
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-3">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">当前登录用户</h3>
</div>
<div class="panel-body">
<div class="list-group">
<a href="#" class="list-group-item">你好,<span id="user"></span></a>
<a href="logout" class="list-group-item">退出</a>
</div>
</div>
</div>
<div class="panel panel-primary" id="online">
<div class="panel-heading">
<h3 class="panel-title">其他在线用户</h3>
</div>
<div class="panel-body">
<div class="list-group" id="users"></div>
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">消息发送</h3>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="msg" /><br>
<button id="broadcast" type="button" class="btn btn-primary">发送</button>
<button id="send" type="button" class="btn btn-primary">私聊发送</button>
</div>
</div>
<div class="col-md-9">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title" id="talktitle"></h3>
</div>
<div class="panel-body">
<div class="well" id="log-container" style="height:400px;overflow-y:scroll"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var username;
var uid;
$(document).ready(function() {
// 指定websocket路径
var websocket;
$.get("/currentuser", function(data) {
username = data.username;
uid = data.uid;
$("#user").html(username);
if ('WebSocket' in window) { // 浏览器支持 WebSocket
websocket = new WebSocket("ws://localhost:8080/webSocket/" + username); // 打开一个 web socket
}
websocket.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.to == 0) { //上线消息
if (data.text != username) {
$("#users").append('<a href="#" onclick="talk(this)" class="list-group-item">' + data.text + '</a>');
$("#log-container").append("<div class='bg-info'><label class='text-danger'>" + data.from + " " + data.date + "</label><div class='text-success'>" + data.text + "上线了" + "</div></div><br>");
scrollToBottom();
}
} else if (data.to == -2) { //下线消息
if (data.text != username) {
$("#users > a").remove(":contains('" + data.text + "')");
$("#log-container").append("<div class='bg-info'><label class='text-danger'>" + data.from + " " + data.date + "</label><div class='text-success'>" + data.text + "下线了" + "</div></div><br>");
scrollToBottom();
}
} else { // 普通消息
$("#log-container").append("<div class='bg-info'><label class='text-danger'>" + data.from + " " + data.date + "</label><div class='text-success'>" + data.text + "</div></div><br>");
scrollToBottom();
}
};
// 加载当前用户
$.post("/onlineusers?currentuser=" + username, function(data) {
for (var i = 0; i < data.length; i++) {
$("#users").append('<a href="#" onclick="talk(this)" class="list-group-item">' + data[i] + '</a>');
}
});
});
// 发送
$("#broadcast").click(function() {
var data = {};
data["from"] = username;
data["to"] = -1;
data["text"] = $("#msg").val();
websocket.send(JSON.stringify(data)); // 使用 send() 方法发送数据
$("#msg").val("");
});
// 私聊发送
$("#send").click(function() {
if ($("body").data("to") == undefined) {
alert("请选择聊天对象");
return false;
}
var data = {};
data["from"] = username;
data["to"] = $("body").data("to");
data["text"] = $("#msg").val();
websocket.send(JSON.stringify(data)); // 使用 send() 方法发送数据
// 单独发送给某人的,不会广播,所以自己这里要手动加上
$("#log-container").append("<div class='bg-success'><label class='text-info'>我 " + new Date().format("yyyy-MM-dd hh:mm:ss") + "</label><div class='text-info'>" + $("#myinfo").val() + "</div></div><br>");
scrollToBottom();
$("#msg").val("");
});
});
function talk(a) {
$("#talktitle").text("与" + a.innerHTML + "的聊天");
$("body").data("to", a.innerHTML);
}
// 滚动条滚动到最低部
function scrollToBottom() {
var div = document.getElementById('log-container');
div.scrollTop = div.scrollHeight;
}
Date.prototype.format = function(fmt) {
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;
}
</script>
</body>
</html>登陆效果图如下,用aa,bb,cc登陆:

三个人依次进入聊天室后效果如下,可以群发消息和点击登陆的用户私聊

私聊时效果如下

代码下载:
END