571
社区成员
发帖
与我相关
我的任务
分享Websocket是一种网络通信协议,是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。HTTP协议是一种无状态的、无连接的、单向的应用层协议,它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型的弊端在于HTTP协议无法实现服务器到客户端的消息主动推送,并且如果服务器有连续的状态变化,客户端获得消息是比较麻烦的,很浪费资源。我们介绍的主角是Websocket,其最大特点就是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,这是HTTP协议所不具备的功能。这里我们通过一张图来说明HTTP与Websocket的区别:

通过上图我们可以发现,其原理和TCP基本相同,只需做一个握手动作,就可以形成一条快速通道。 客户端发起HTTP请求,附加头信息为:“Upgrade Websocket”,服务端解析报文,并返回握手信息进而建立链接。链接建立后即可进行数据传输,当传输结束后就可以选择断开链接。当然Websocket还有不少特点,比如数据格式比较轻量,性能开销小,通信高效,可以发送文本,也可以发送二进制数据,与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器等等。我们接下来介绍什么是Websocket协议
Websocket协议有两部分:握手与数据传输
其中,握手部分是基于HTTP协议的,我们来看一个典型的Websocket握手报文:
GET /uin=xxxxxxxx&app=xxxxxxxxx&token=XXXXXXXXXXXX HTTP/1.1
Host: [server.example.cn:443](http://server.example.cn:443)
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
Upgrade: websocket
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: user_id=XXXXX
Sec-WebSocket-Key: 1/2hTi/+eNURiekpNI4k5Q==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Protocol: binary, base64
上面字段中,Connection: Upgrade字段标识HTTP请求是一个协议升级请求。Upgrade: websocket表示协议升级为Websocket协议,Sec-WebSocket-Version: 13则表示客户端支持的Websocket版本。Sec-WebSocket-Key字段是客户端采用base64编码的24位随机字符序列,有点类似接头暗号,是服务器接受客户端新协议的证明。当握手成功时,通信就不采用HTTP协议了,而是采用Websocket独立的数据帧,大致如下图所示:

实现Websocket的浏览器将通过Websocket对象公开所有必须的客户端功能,我们采用以下API创建该项目的Websocket对象:
var ws = new WebSocket("ws://localhost/chat");
其中,Websocket对象的相关事件有:

Websocket对象的相关方法有:

本项目是采用Java的,Java Websocket应用采用一系列的WebSocketEndpoint组成的。其中,Endpoint是一个Java对象,代表Websocket链接的一端,对于服务端,我们可以视为处理具体Websocket消息的接口,这一点就像servlet对于HTTP请求的作用。本项目中,我选择了注解式的方法来定义Endpoint(一个POJO),并添加@ServerEndpoint相关注解。服务端的Endpoint实例在握手时创建,并且在客户端与服务端连接的过程中一直有效(直到链接关闭)。在Endpoint接口中定义了与其生命周期相关的方法如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.1.1-1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
(1)登陆页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="js/jquery.min.js"></script>
<form id="loginForm">
<label>账号 Username: </label><br>
<input type="text" name="user"><br>
<label>密码 Password: </label><br>
<input type="password" name="pwd"><br><br>
<button type="button" id="btn">登录 Login</button>
</form>
<p id="err_msg"></p>
<script>
$("#btn").click(function () {
$.get("toLogin?",$("#loginForm").serialize(),function(res){
if (res.flag){
location.href = "main.html";//异步跳转到main.html的界面
} else {
$("#err_msg").html(res.message);
}
},"json");
})
</script>
</body>
</html>
(2)主界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
#left{
float: left;
width: 30%;
height: 500px;
margin-left: 200px;
}
#right{
margin-top: 20px;
border: cornflowerblue 3px solid;
float: right;
width: 30%;
height: 500px;
margin-right: 300px;
}
#top{
float: top;
width: 30%;
height: 250px;
}
#content{
border: slateblue 3px solid;
width: 100%;
height: 500px;
}
#input{
margin-top: 40px;
width: 100%;
height: 200px;
}
#input input{
border: coral 2px solid;
width: 100%;
height: 100px;
margin-bottom: 20px;
}
#input button{
float: right;
}
#mes_left{
float: left;
}
#mes_right{
float: right;
width: 50%;
text-align: right;
}
</style>
<script src="js/jquery.min.js"></script>
<h3 style="text-align: center" id="username"></h3>
<div>
<div id="left">
<h4 id="new"></h4>
<div id="content">
</div>
<div id="input">
<input type="text" id="input_text">
<button id="submit">发送 Enter</button>
</div>
</div>
<div id="right">
<div id="top">
<p>目前在线的好友</p>
<div id="hylist">
</div>
</div>
<div id="bottom">
<p>系统广播</p>
<div id="xtlist">
</div>
</div>
</div>
</div>
<script>
var username;
$(function () {
var toName;
$.ajax({
url:"getUsername",
success:function (res) {
username = res;
$("#username").html("用户:"+ res +"<span> 在线</span>");
},
async:false //不能是异步请求,因为前后有依赖关系
});
//创建websocket对象
var ws = new WebSocket("ws://localhost/chat");
//给ws绑定上事件
ws.onopen = function (ev) {
//显示在线信息
$("#username").html("用户:"+ username +"<span style='color: green' > 在线</span>");
}
//接收到服务端推送的消息后触发的实践
ws.onmessage = function (ev) {
var dataStr = ev.data;
//将dataStr转化为json对象
var res = JSON.parse(dataStr);
//判断是否是系统消息
if(res.system){
//是系统消息
//1.实现好友列表
//2.实现系统广播
var names = res.message;
var userlistStr = "";
var broadcastListStr = "";
for (var name of names){
if (name != username){
userlistStr += "<a onclick='showChat(\""+name+"\")'>"+ name +"</a></br>";
broadcastListStr += "<p>"+ name +" 上线了</p>";
}
};
//渲染好友列表和系统广播
$("#hylist").html(userlistStr);
$("#xtlist").html(broadcastListStr);
}else {
//不是系统消息
var str = "<span id='mes_left'>"+ res.message +"</span></br>";
if (toName == res.fromName)
$("#content").append(str);
//sessionStorage
var chatdata = sessionStorage.getItem(res.fromName);
if (chatdata != null){
str = chatdata + str;
}
sessionStorage.setItem(res.fromName,str);
};
},
ws.onclose = function (ev) {
//显示离线信息
$("#username").html("用户:"+ username +"<span style='color: red' > 离线</span>");
}
showChat = function(name){
// alert("dsaad");
toName = name;
//清空聊天区
$("#content").html("");
$("#new").html("正在和"+toName+"聊天");
var chatdata = sessionStorage.getItem(toName);
if (chatdata != null){
$("#content").html(chatdata);
}
};
//发送消息
$("#submit").click(function () {
//获取输入的内容
var data = $("#input_text").val();
//清除输入区的内容
$("#input_text").val("");
var json = {"toName": toName ,"message": data};
//将数据展示在聊天区
var str = "<span id='mes_right'>"+ data +"</span></br>";
$("#content").append(str);
var chatdata = sessionStorage.getItem(toName);
if (chatdata != null){
str = chatdata + str;
}
sessionStorage.setItem(toName,str);
//发送数据
ws.send(JSON.stringify(json));
})
})
</script>
</body>
</html>
(1)Message类(浏览器发给服务端的websocket)
public class Message {
private String toName;
private String message;
public String getToName() {
return toName;
}
public void setToName(String toName) {
this.toName = toName;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
(2)Resultmessage类(服务端给浏览器发送的websocket)
public class ResultMessage {
private boolean isSystem;
private String fromName;
private Object message;//如果是系统消息就是数组
public boolean isSystem() {
return isSystem;
}
public void setSystem(boolean system) {
isSystem = system;
}
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName;
}
public Object getMessage() {
return message;
}
public void setMessage(Object message) {
this.message = message;
}
}
(3)Result类(用于登录响应给浏览器的类)
public class Result {
private boolean flag;//登录成功还是失败
private String message;//登录结果提示
}
//用于封装消息的工具类
public class MessageUtils {
public static String getMessage(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 (JsonProcessingException e){
e.printStackTrace();
}
return null;
}
}
@RestController
public class LoginController {
@RequestMapping("/toLogin")
public Result tologin(@RequestParam("user") String user,@RequestParam("pwd") String pwd, HttpSession session){
Result result = new Result();
if (user.equals("SA21225241")&&pwd.equals("123456")){
result.setFlag(true);
session.setAttribute("user",user);
}else if (user.equals("SA21225242")&&pwd.equals("123456")){
result.setFlag(true);
session.setAttribute("user",user);
}else if (user.equals("Administer")&&pwd.equals("000")){
result.setFlag(true);
session.setAttribute("user",user);
}
else if (user.equals("SA21225243")&&pwd.equals("123456")){
result.setFlag(true);
session.setAttribute("user",user);
}else {
result.setFlag(false);
result.setMessage("登录失败");
}
return result;
}
@RequestMapping("/getUsername")
public String getUsername(HttpSession session){
String username = (String) session.getAttribute("user");
return username;
}
}
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
//把http的session对象存到配置对象中
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
@Configuration
public class WebSocketConfig {
@Bean
//注入ServerEndpointExporter bean对象,自动注册使用注解@ServerEndpoint的bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
//注意!虽然之前定义了配置类,但是和ChatEndpoint没关系,因此需要绑定关系,卡了好久
@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){
this.session = session;//将局部session赋值给全局的session
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;//获取对象
String username = (String)httpSession.getAttribute("user");
onlineUsers.put(username,this);//将当前对象存储到容器中
//将当前在线用户的用户名推送给所有的客户端
//1 获取消息
String message = MessageUtils.getMessage(true, null, getNames());
//2 调用方法进行系统消息的推送
broadcastAllUsers(message);
}
private void broadcastAllUsers(String message){
try {
//将消息推送给所有的客户端
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndpoint chatEndpoint = onlineUsers.get(name);
chatEndpoint.session.getBasicRemote().sendText(message);
}
}catch (Exception e){
e.printStackTrace();
}
}
//返回在线用户名
private Set<String> getNames()
{
return onlineUsers.keySet();
}
@OnMessage
public void onMessage(String message,Session session){
//将message数据转换成message对象
try {
ObjectMapper mapper =new ObjectMapper();
Message mess = mapper.readValue(message, Message.class);
//获取要发送的数据给用户
String toName = mess.getToName();
//获取消息数据
String data = mess.getMessage();
String username = (String) httpSession.getAttribute("user");
String resultMessage = MessageUtils.getMessage(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);
MessageUtils.getMessage(true,null,getNames());
}}
@SpringBootApplication
public class ChatroomApp {
public static void main(String[] args)
{
SpringApplication.run(ChatroomApp.class, args);
}
}
server.port=80
spring.thymeleaf.cache=false
(1)登陆界面

(2)聊天界面

总结
由于不太会弄前端的东西,写出来的效果感觉有亿点点难看,不过基本功能是实现了的(虽然折腾了不少时间..)。通过本项目,我较为系统的了解了利用Websocket来编写程序的方法与技巧,在学习过程中认识到了自己的能力还是得多锻炼。
作者 NP241