WebSocket协议是由HTML5定义的,基于TCP协议实现的一种网络协议,通过该协议服务器可以主动向客户端发送信息;
WebSocket 协议在2008年诞生,2011年成为W3C国际标准;
我们已经有了 HTTP 协议,为什么出现一个websocket协议?
http协议是短连接,因为请求之后,都会关闭连接,下次重新请求数据,需要再次打开链接;
WebSocket协议是一种长连接,只需要通过一次请求来初始化连接,然后所有的请求和响应都是通过这个TCP连接进行通讯;
所以HTTP协议通信只能是客户端向服务器发出请求,服务器返回响应结果,HTTP
协议做不到服务器主动向客户端推送信息,而websocket能实现服务器和客户端全双工通信;何谓全双工
信息只能单向传送为单工;信息能双向传送但不能同时双向传送称为半双工,信息能够同时双向传送则称为全双工;
基本实现原理
WebSocket协议基于TCP协议实现,客户端和服务器只需要做一个握手的动作之后,形成了一条基于客户端和服务器之间的快速通道,之后客户端与服务端之间便可以进行多次数据帧双向传输;
这样实现的目的是客户端和服务器进行频繁双向通信时,可以使服务器避免频繁创建HTTP连接,节约资源,提高工作效率和资源利用率;
传统Web推送实现
在没有WebSocket协议之前,服务器如何向浏览器端推送消息?
此时,通常的实现方式是在页面通过Ajax定时轮询,比如每隔1秒中向服务器发送一次HTTP请求,询问服务器是否有新消息,服务器返回结果;
这种形式缺点很明显,浏览器需要不断的向服务器发出HTTP请求,而HTTP请求包含较长的头部,有效信息相对较少,反复的无效请求占用了大量的带宽和
CPU 资源,造成很大的浪费,所以,WebSocket 应运而生;HTML5定义的WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯;
WebSocket协议本质上是一个基于TCP的协议,因此与HTTP协议没有什么关系;
WebSocket的特点
全双工通信,客户端和服务器可以双向平等通信;
建立在TCP协议之上,服务器端的实现比较容易;
数据格式比较轻量,性能开销小,通信高效;
可以发送文本,也可以发送二进制数据;
通信具有更强的实时性;
协议标识符是ws,服务器地址举个例子就是:ws://www.abc.com/some/path
而http协议的地址则是: http://…
websocket业务场景
WebSocket聊天室;
股票实时价格显示等应用;
即时通讯、游戏、可视化大屏展示等领域;
企业内部管理通讯等功能,主要通讯协议websocket;
web网页聊天、客服系统实现;
系统提醒、用户上下线提醒、客户端同步,实时数据更新,多屏幕同步,用户在线状态,消息通知,扫描二维码登录/二维码支付,弹幕、各类信息提醒,在线选座,实时监控大屏等等;
在Java EE 7中Java语言开始支持websocket协议,Java EE 7中定义了一套Websocket
API规范,也就是一系列接口,没有实现,位于包javax.websocket下,包含客户端API和服务端API,WebSocket的Java
API 只是规范,具体实现需要web容器(比如tomcat就实现了Java websocket api)、Java EE服务器或者框架提供;
javax.websocket | This package contains all the WebSocket APIs common to both the client and server side. |
---|---|
javax.websocket.server | This package contains all the WebSocket APIs used only by server side applications. |
1、Tomcat:java中的websocket实现,需要tomcat 7.0.47+以上才支持Java EE7;
2、Spring的websocket,需要Spring 4.x,所以springboot也可以用;
2.1 WebSocket开发中的相关注解及API方法
@ServerEndpoint("/websocket/{uid}")
申明这是一个websocket服务;
需要指定访问该服务的地址,在地址中可以指定参数,需要通过{}进行占位;
@OnOpen
用法:public void onOpen(Session session, @PathParam(“uid”) String uid)
throws IOException{}该方法将在建立连接后执行,会传入session对象,就是客户端与服务端建立的长连接通道,通过@PathParam获取url中声明的参数;
@OnClose
用法:public void onClose() {}
该方法是在连接关闭后执行;
@OnMessage
用法:public void onMessage(String message, Session session) throws
IOException {}该方法用于接收客户端发送的消息;
message:发来的消息数据;session:会话对象(也是长连接通道);
发送消息到客户端;
用法:session.getBasicRemote().sendText(“hello,websocket.”);
通过session进行消息发送;
2.2 前端技术对WebSocket的支持
Websocket是html5规范,主流浏览器都支持;(某些老浏览器不支持)
jQuery、vueJS、React JS、angularjs等都可以支持webscoket对象;
底层是javascript支持的一个webscoket的js对象,通过这个对象可以建立websocket的连接:ws://localhost:8080/websocket/12345
3.1 pom文件中添加相关依赖
<dependencies>
<!-- SpringBoot框架web项目起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot框架websocket起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- SpringBoot框架内嵌Tomcat对jsp的解析依赖 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
<build>
<!-- SpringBoot框架编译打包插件 -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<!-- src/main/webapp下的jsp页面编译到META-INF/resources下才能访问 -->
<resources>
<resource>
<directory>src/main/webapp</directory>
<targetPath>META-INF/resources</targetPath>
<includes>
<include>*.*</include>
</includes>
</resource>
</resources>
</build>
3.2 在核心配置文件中配置视图解析器
#配置视图解析器
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
3.3 加入相关静态资源文件
3.4 编写控制层controller
这其中只有一个请求,/chat,这个请求会跳转到我们的index.jsp页面。
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
*/
@Controller
public class ChatController {
//声明原子变量类,确保服务端和客户端之间操作的原子性和可见性
private AtomicInteger atomicInteger=new AtomicInteger();
@RequestMapping("/chat")
public String chat(Model model) {
model.addAttribute("username","user" + atomicInteger.getAndIncrement());
return "index";
}
}
3.5 写一个配置类,开启SpringBoot对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.server.standard.ServerEndpointExporter;
/**
*
*/
@EnableWebSocket //开启SpringBoot对WebSocket的支持
@Configuration //声明该类是一个配置类
public class ChatConfig {
/**
* 配置ServerEndpointExporter的bean
* 该Bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
* @return
*/
@Bean
public ServerEndpointExporter serverEndpoint() {
return new ServerEndpointExporter();
}
}
6.6 写一个工具类
这个工具类封装了大量静态方法,供外界调用。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 聊天室功能实现的一个工具类
*/
public class ChatUtils {
//定义日志对象
private static final Logger logger= LoggerFactory.getLogger(ChatUtils.class);
//定义map集合,确保数据共享和安全,这里使用ConcurrentHashMap
//用户名为key,session信息为value
public static final Map<String, Session> CLIENTS=new ConcurrentHashMap<>();
/**
* 使用连接发送消息
* @param session 用户的session
* @param message 发送的消息内容
*/
public static void sendMessage(Session session,String message) {
if (session == null) {
return;
}
final RemoteEndpoint.Basic basic=session.getBasicRemote();
if (basic == null) {
return;
}
try {
basic.sendText(message);
} catch (IOException e) {
e.printStackTrace();
logger.error("sendMessage IOException",e);
}
}
/**
* 发送消息给所有人
* @param message
*/
public static void sendMessageAll(String message) {
CLIENTS.forEach((sessionId,session) -> sendMessage(session,message));
}
/**
* 获取所有的在线用户
*/
public static String getOnlineInfo() {
Set<String> userNames=CLIENTS.keySet();
if (userNames.size() == 0) {
return "当前无人在线......";
}
return userNames.toString() + "在线";
}
}
3.7 WebSocket的核心类
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
/**
* @ServerEndpoint注解中指定WebSocket协议的地址
* @OnOpen、@OnMessage、@OnClose、@OnError注解与WebSocket中监听事件对应
*/
@Slf4j //生成一些日志代码
@Component
@ServerEndpoint("/websocket/{username}")
public class ChatServerEndpoint {
/**
* 连接建立时触发
*/
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) {
log.info("用户{}登录",username);
String message= "用户[" + username + "]已进入聊天室!";
//将该用户登录的消息发送给其他人
ChatUtils.sendMessageAll(message);
//将自己的信息添加到map集合中
ChatUtils.CLIENTS.put(username,session);
//获取当前的在线人数,发给自己查看
String onlineInfo=ChatUtils.getOnlineInfo();
ChatUtils.sendMessage(session,onlineInfo);
}
/**
* 客户端接收服务端发来的数据时触发
*/
@OnMessage
public void onMessage(@PathParam("username") String username,String message) {
log.info("发送消息:{}, {}",username,message);
//广播,把消息同步给其他客户端
ChatUtils.sendMessageAll("[" + username + "]: " + message);
}
/**
* 连接关闭时触发
*/
@OnClose
public void onClose(@PathParam("username") String username,Session session) {
//从当前的map集合中移除该用户
ChatUtils.CLIENTS.remove(username);
//将该用户离线的消息通知给其他人
ChatUtils.sendMessageAll("[" + username + "]已离线!");
try {
//关闭WebSocket下的该Seesion会话
session.close();
log.info("{} 已离线......",username);
} catch (IOException e) {
e.printStackTrace();
log.error("onClose error",e);
}
}
/**
* 聊天通信发生错误时触发
*/
@OnError
public void onError(Session session,Throwable throwable) {
try {
//关闭WebSocket下的该Seesion会话
session.close();
} catch (IOException e) {
e.printStackTrace();
log.error("onError Exception",e);
}
log.info("Throwable msg " + throwable.getMessage());
}
}
3.8 最后是我们的jsp页面
这里简单的说一下,当我们发起controller中对应的请求之后,会跳转到这个页面,待页面加载完之后(就是执行完