目录
一、websocket技术
1、websocket技术应用场景
使用java技术快速学习一个简单的在线聊天室系统,该系统具备很强的扩展性,可以根据业务需要,制作在线客服系统、web版的微信、QQ即时通信系统等,使用较为流行的技术,采用积木式的编程思路。
web领域的实时推送技术,也被称为Realtime技术,这种技术要达到的目的是绕过用户不需要刷新浏览器就可以获得实时更新。他有着广泛的应用前景,比如在线聊天室,在线客服系统、评论系统、WebIM等。
2、websocket协议概述
webSocket protocol是HTML5的一种新协议。他实现了浏览器与服务器全双工通信(full-duplex)一开始的握手需要借助HTTP请求完成。
websocket是真正实现了全双工通信的服务器向客户端推的互联网技术。
他是一种在单个TCP连接上进行全双工通讯的协议。webSocket通信协议与2011年被IETF定义为标准RFC 6455,Websocket API 被 W3c定为标准。
3、全双工和单工的区别
- 全双工:是通讯传输的一个术语。通讯允许数据在两个方向上同时传输,它在能力相当于两单工通信方式的结合。全双工指可以同时(瞬时)进行信号的双向传输(A->B , B-A) 指A->B的同时 B->A是瞬时同步的。
- 单工、半双工,所谓半双工就是指一个时间段内只有一个动作发生,举个简单例子,一条窄窄的马路,同时只能有一辆马车通过,当目前有两辆马车时,这种情况就需要一辆等待另一辆通过后再通过,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是基于半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台
4、推的技术和拉的技术(了解)
- 推送(PUSH)技术是一种建立在客户服务器上的机制,就是由服务器主动将信息发往客户端的技术,就像是广播电台播音。
- 同传统的拉Pull技术相比,最主要的区别在于推送Push技术是由服务器主动向客户机发送信息,而拉PULL技术则是由客户机主动请求信息。PUSH技术的优势在于信息的主动性和及时性。
- 简单来说,相对于服务端:拉的技术是被动向客户机端提供数据,推的技术是主动向客户端提供数据
5、互联网技术
互联网技术的定义:互联网技术指在计算机技术的基础上开发建立的一种信息技术(直译:Internet Technology);简称:IT.
该技术把互联网上分散的资源融为有机整体,实现资源的全面共享和有机协作,使人们能够透明的使用资源的整体能力并按需获取信息。
6、Wenbsocket的优越性
以前不管使用HTTP轮询或是使用TCP长连接等方式制作在线聊天系统,都有天然缺陷,随着Html5的发展,其中有一个新的协议Websocket protocol,可实现浏览器与服务器全双工通信,它可以做到:浏览器和服务器只需要一次握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间可以数据互相传输。这个新的协议的特点正好适合这种在线即时通信。
传统的Http协议的实现方式:
http协议可以多次请求,因为每次请求之后,都会关闭连接,下次重新请求数据,需要再次打开连接
传统的Socket技术:
长连接
客户端 ---先连接上去 ----- 服务端
好处:可以实现客户端与服务端的双向通信
缺点:如果大家都不说话,就造成了资源浪费
其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
ws://example.com:80/some/path
二、SpringBoot使用WebSocket实现聊天室
1.实现的结构图
2.消息格式
- 客户端------》服务端
{"toName":"张三","message":"你好"}
toName:发给谁的 message:发送的消息内容
- 服务端-----》客户端
系统消息格式:
{"isSystem":true,"fromName":null,"message":["李四","王五"]}
isSystem:是否是系统消息
fromName:谁发送的
message:发送的消息内容
聊天的消息格式:
{”isSystem":true,"formName:"张三","message":"你好"}
3.功能实现
1.创建项目,导入相关jar包的坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.创建公共的资源
Message类:用于浏览器发送到服务器的webSocket数据
package com.pojo;
import lombok.Data;
//用于浏览器向服务器发送数据
@Data
public class Message {
private String toName;
private String message;
}
ResultMessage :用于服务器发送浏览器的Websocket数据
package com.pojo;
import lombok.Data;
//用于服务器发送浏览器的Websocket数据
@Data
public class ResultMessage {
private boolean isSystem;//是否是系统消息
private String fromName;
private Object message; //如果是系统消息是数组
}
MessageUtils:用来封装消息的工具类
需要将数据转换为Websocket可以识别的格式
package com.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pojo.ResultMessage;
//用来封装消息的工具类
public class MessageUtils {
public static String getMesssge(boolean isSystemMessage,String fromName,Object message){
try {
ResultMessage result = new ResultMessage();
result.setSystem(isSystemMessage);
result.setMessage(message);
if(fromName!=null){
result.setFromName(fromName);
}
ObjectMapper mapper=new ObjectMapper();
return mapper.writeValueAsString(result);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
result:用于登响应给浏览器的数据
package com.pojo;
import lombok.Data;
//用于登录后响应给浏览器的数据
@Data
public class Result {
private boolean flag;
private String message;
}
3.登录功能的实现
- login.html :使用异步进行请求发送
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
<script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>
</head>
<body>
<form id="loginForm">
<input type="text" placeholder="请输入你的姓名..." name="username"/>
<input type="password" name="password"/>
<input type="button" id="btn"/>
</form>
</body>
<script>
$(function () {
$("#btn").click(function () {
$.get("login",$("#loginForm").serialize(),function (res) {
if (res.flag){
//跳转到main.html页面
location.href="main.html";
}else {
$("#err_msg").html(res.message);
}
},"json");
});
})
</script>
</html>
- userController:进行登陆逻辑处理
package com.controller;
import com.pojo.Result;
import com.pojo.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
//userController:进行登陆逻辑处理
@RestController
public class UserController {
@RequestMapping("/login")
public Result login(User user, HttpSession session){
Result result = new Result();
if(user!=null&&"123".equals(user.getPassword())){
result.setFlag(true);
//将用户保存到session对象中
session.setAttribute("user",user.getUsername());
}else {
result.setFlag(false);
result.setMessage("登录失败");
}
return result;
}
@RequestMapping("/getUsername")
public String getUsername(HttpSession session){
String username = (String) session.getAttribute("user");
return username;
}
}
- main.html:聊天功能实现的页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>
</head>
<body>
<div>
<div>
<div>
<!-- <div id="username"></span></div> 用户:张三<span style="float: right;color: green">在线-->
<div id="username"></span></div>
<div id="chatMes">
<!--正在和<font face="楷体”>张三</font>聊天-->
</div>
</div>
<!--聊天区开始-->
<div id="pnlBody">
<div id="initBackground" style="background-color: white;width: 100%">
<div id="chatArea" style="display: none">
<div id="show">
<div id="hists"></div>
<div id="msgs">
<!--消息展示区-->
<div class="msg guest">
<div class="msg-right">
<div>你好</div>
</div>
</div>
<div class="msg robot">
<div class="msg-left">
<div>你好</div>
</div>
</div>
</div>
</div>
<div>
<div>
<textarea id="context_text" wrap="hard" placeholder="在此输入文字信息..."></textarea>
<div id="atcomPnl">
<ul id="atcom"></ul>
</div>
</div>
<div id="submit">
发送
</div>
</div>
</div>
</div>
</div>
<!--聊天区结束-->
<div>
<div></div>
<div>
<div>
<div id="hot-tab">好友列表</div>
</div>
<div>
<ul id="userList">
</ul>
</div>
</div>
<div>
<div>
<div>系统广播</div>
</div>
<div>
<ul id="broadcastList">
</ul>
</div>
</div>
</div>
</div>
</div>
</body>
<script>
var username;
var toName;
function showChat(name) {
toName = name;
//现在聊天对话框
$("#chatArea").css("display", "inline");
//清空聊天对话框
$("#msgs").html("");
//显示”正在和谁聊天“
$("#chatMes").html("正在和<font face=\"楷体\">" + toName + "<font>聊天</font>");
//sessionStorage
var chatData= sessionStorage.getItem(toName);
if(chatData!=null){
//将聊天记录渲染到聊天区
$("#msgs").html(chatData);
}
}
$(function () {
$.ajax({
url: "getUsername",
success: function (res) {
username = res;
//显示在线信息
$("#username").html("用户:" + res + "<span style='float:right;color:green'>在线</span>");
},
async: false //是否异步请求
});
//创建webSocket对象
var ws = new WebSocket("ws://192.168.43.24:8080/chat");
//给ws绑定点击时间
ws.onopen = function () {
//在建立连接后需要做什么事情?
//显示在线信息
$("#username").html("用户:" + username + "<span style='float:right;color:#008000'>在线</span>");
}
//接收到服务端推送的消息后触发
ws.onmessage = function (evt) {
//获取服务器推送过来的消息
var dataStr = evt.data;
//将dataStr转换为Json对象
var res = JSON.parse(dataStr);
console.log(res);
console.log(res.message);
//判断是否是系统消息
if (res.system) {
var names = res.message;
//系统消息
//1.好友列表展示
var userlistStr = "";
var broadcastListStr = "";
for (var name of names) {
if (name != username) {
userlistStr += "<li class=\"rel-item\"><a onclick='showChat(\"" + name + "\")'>" + name + "</a></li>";
broadcastListStr += "<li class=\"rel-item\" style=\"color:#9d9d9d;font-family:宋体\">您的好友 " + name + "已上线</li>";
}
}
//渲染好友列表和系统广播
$("#userList").html(userlistStr);
$("#broadcastList").html(broadcastListStr);
//2.系统广播展示
} else {
//不是系统消息
//服务器端推送的消息进行展示
// var str="<div class='\msg robot\'><div class=\"msg-left\" worker=\"\"><div class =\"msg-host photo\" style='background-image: url(img)'></div>"
var str="<h5 style='text-align: left'>"+res.message+"</h5>";
if(toName==res.fromName){
$("#msgs").append(str);
}
var chatData = sessionStorage.getItem(res.fromName);
if (chatData!=null){
str=chatData+str;
}
sessionStorage.setItem(res.fromName,str);
}
}
ws.onclose = function () {
//显示离线信息
$("#username").html("用户:" + username + "<span style='float:right;color:red'>离线</span>");
}
$("#submit").click(function () {
//获取输入内容
var data=$("#context_text").val();
//清除输入区的内容
$("#context_text").val("");
var json={"toName":toName,"message":data};
//将数据展示在聊天区
var str="<h5 style='text-align: right'>"+data+"</h5>";
$("#msgs").append(str);
//发送消息给服务端
var chatData = sessionStorage.getItem(toName);
if (chatData!=null){
str=chatData+str;
}
sessionStorage.setItem(toName,str);
ws.send(JSON.stringify(json));
});
});
</script>
</html>
- 在UserController中添加一个getUsername方法,用来从Session中获取当前登录的用户名并响应回给浏览器
@RequestMapping("/getUsername")
public String getUsername(HttpSession session){
String username = (String) session.getAttribute("user");
return username;
}
4.配置类的实现
WebSocketConfig :注入一个ServerEndpointExporter实力类对象
package com.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
//注入ServerEndpointExporter bean对象,自动注册使用了@ServerEndPoint
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
5、构建一个服务器类
ChatEndpoint :实现聊天业务主要的功能
package com.ws;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pojo.Message;
import com.utils.MessageUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfigurator.class)
@Component
//每一个客户端都会有一个该对象的引用,每个对象互补影响
public class ChatEndpoint {
//用来储存每一个客户端对象对应的ChatEndpoint对象
private static Map<String, ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();
//声明Session随想,通过该对象可以发送消息给指定的用户
private Session session;
//声明一个HttpSession对象,我们之前在HttpSession对象中存储了用户名
private HttpSession httpSession;
//连接建立时被调用
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
//将局部的session对象赋值给成员变量Session
this.session = session;
//获取httpSession对象
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
//将当前对象存储到容器中
//从HttpSession对象中获取用户名
String username = (String) httpSession.getAttribute("user");
onlineUsers.put(username, this);
//将当前的在线用户的用户名发送给所有的客户端
MessageUtils.getMesssge(true, null, getNames());
//1.获取消息
String message = MessageUtils.getMesssge(true, null, getNames());
//2.调用方法进行系统消息的推送
broadcastAllUsers(message);
}
private Set<String> getNames() {
return onlineUsers.keySet();
}
private void broadcastAllUsers(String message) {
//要将该消息推送给所有的客户端
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndpoint chatEndpoint = onlineUsers.get(name);
try {
chatEndpoint.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//接收到客户端发送的数据时被调用
@OnMessage
public void onMessage(String message, Session session) {
//将Message转换成message对象
ObjectMapper mapper = new ObjectMapper();
try {
Message mess = mapper.readValue(message, Message.class);
//获取要将数据发送给谁
String toName=mess.getToName();
//获取消息数据
String data=mess.getMessage();
//获取当前登录的用户
String username= (String) httpSession.getAttribute("user");
//获取推送给指定用户的消息格式的数据
String resultMessage=MessageUtils.getMesssge(false,username,data);
//发送数据
onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
String username= (String) httpSession.getAttribute("user");
//从容器中删除指定的用户
onlineUsers.remove(username);
//获取推送的额消息
String messsge = MessageUtils.getMesssge(true, null, getNames());
broadcastAllUsers(messsge);
}
}
6、GetHttpSessionConfigurator
将httpSession对象存储到配置对象中
package com.ws;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response){
HttpSession httpSession = (HttpSession) request.getHttpSession();
//将httpSession对象存储到配置对象中
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}