荣添创意网站建设,涿州网站建设有限公司,桂林网站建设凡森网络,静态网站建设参考文献一、引言
1. 问题引入
Hypertext Transfer Protocol (HTTP) 协议 一种无状态的、应用层的、以请求/应答方式运行的协议#xff0c;它使用可扩展的语义和自描述消息格式#xff0c;与基于网络的超文本信息系统灵活的互动. 因为http 通信只能由客户端发起,服务器返回查询结果…一、引言
1. 问题引入
Hypertext Transfer Protocol (HTTP) 协议 一种无状态的、应用层的、以请求/应答方式运行的协议它使用可扩展的语义和自描述消息格式与基于网络的超文本信息系统灵活的互动. 因为http 通信只能由客户端发起,服务器返回查询结果,HTTP 协议做不到服务器主动向客户端推送信息,
服务器有连续的状态变化客户端要获知就非常麻烦。
我们只能使用轮询每隔一段时候就发出一个询问了解服务器有没有新的信息。最典型的场景就是聊天室。
轮询的效率低非常浪费资源因为必须不停连接或者 HTTP 连接始终打开;
2. 消息推送常见方式
常见的消息推送发送轮询长轮询SSEWebSocket
轮询方式 SSEserver-sent event服务器发送事件
SSE在服务器和客户端之间打开一个单向通道服务端响应的不再是一次性的数据包而是text/event-stream类型的数据流信息服务器有数据变更时将数据流式传输到客户端 二、WebSocket
1. WebSocket介绍
WebSocket 是一种网络通信协议。RFC6455 定义了它的通信标准。
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起服务端对请求做出应答处理。
这种通信模型有一个弊端HTTP 协议无法实现服务器主动向客户端发起消息。
这种单向请求的特点注定了如果服务器有连续的状态变化客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 AJAX 请求实现长轮询。轮询的效率低非常浪费资源因为必须不停连接或者 HTTP 连接始终打开。
http协议 websocket协议 2. websocket协议
本协议有两部分握手和数据传输。
握手是基于http协议的。
来自客户端的握手看起来像如下形式
GET ws://localhost/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Version: 13来自服务器的握手看起来像如下形式
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo
Sec-WebSocket-Extensions: permessage-deflate字段说明
头名称说明ConnectionUpgrade标识该HTTP请求是一个协议升级请求Upgrade: WebSocket协议升级为WebSocket协议Sec-WebSocket-Version: 13客户端支持WebSocket的版本Sec-WebSocket-Key客户端采用base64编码的24位随机字符序列服务器接受客户端HTTP协议升级的证明。要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答Sec-WebSocket-Extensions协议扩展类型
3. 客户端浏览器实现
3.1 websocket对象
实现 WebSockets 的 Web 浏览器将通过 WebSocket 对象公开所有必需的客户端功能主要指支持 Html5 的浏览器。
以下 API 用于创建 WebSocket 对象
var ws new WebSocket(url);参数url格式说明 ws://ip地址:端口号/资源名称
3.2 websocket事件
WebSocket 对象的相关事件
事件事件处理程序描述openwebsocket对象.onopen连接建立时触发messagewebsocket对象.onmessage客户端接收服务端数据时触发errorwebsocket对象.onerror通信发生错误时触发closewebsocket对象.onclose连接关闭时触发
3.3 WebSocket方法
WebSocket 对象的相关方法:
方法描述send()使用连接发送数据
4. 服务端实现
Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)。
Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint 是一个java对象代表WebSocket链接的一端对于服务端我们可以视为处理具体WebSocket消息的接口 就像Servlet之与http请求一样。
我们可以通过两种方式定义Endpoint:
第一种是编程式 即继承类 javax.websocket.Endpoint并实现其方法。第二种是注解式, 即定义一个POJO, 并添加 ServerEndpoint相关注解。
Endpoint实例在WebSocket握手时创建并在客户端与服务端链接过程中有效最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法 规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下
方法含义描述onClose当会话关闭时调用。onOpen当开启一个新的会话时调用, 该方法是客户端与服务端握手成功后调用的方法。onError当连接过程中异常时调用。
服务端如何接收客户端发送的数据呢 编程式 通过添加 MessageHandler 消息处理器来接收消息 注解式 在定义Endpoint时通过OnMessage注解指定接收消息的方法
服务端如何推送数据给客户端呢
发送消息则由 RemoteEndpoint 完成 其实例由 Session (websocket.Session) 维护。发送消息有2种方式发送消息通过session.getBasicRemote 获取同步消息发送的实例 然后调用其 sendXxx()方法发送消息通过session.getAsyncRemote 获取异步消息发送实例然后调用其 sendXxx() 方法发送消息
服务端代码 三、基于WebSocket的网页聊天室
完整项目地址 https://gitee.com/lxxkobe/lxx_chat.git 1. 需求
通过 websocket 实现一个简易的聊天室功能 。
1. 登陆聊天室 2. 登陆之后进入聊天界面进行聊天
登陆成功后呈现出以后的效果 当我们想和李四聊天时就可以点击 好友列表 中的 李四效果如下 接下来就可以进行聊天了张三 的界面如下 李四 的界面如下 2. 实现流程 3. 消息格式
张三 给李四发消息 客户端 -- 服务端 {“toName”:“李四”,“message”:“你好”} 服务端 -- 客户端 推送给某一个的消息格式{“isSystem”:false,“fromName”:“张三”,“message”“你好”} 系统消息格式{“isSystem”:true,“fromName”:null,“message”[“李四”,“王五”]}
4. 功能实现
4.1 创建项目导入相关jar包的坐标
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.7.17/versionrelativePath/ /parentgroupIdcom.lxx/groupIdartifactIdlxx_chat/artifactIdversion0.0.1-SNAPSHOT/versionnamelxx_chat/namedescriptionlxx_chat/descriptionpropertiesjava.version11/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-websocket/artifactId/dependencydependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion1.2.78/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project
4.2 引入静态资源文件 4.3 引入公共资源
pojo类
package com.lxx.pojo;import lombok.Data;
//用于登陆响应回给浏览器的数据
Data
public class Result {private boolean flag;private String message;
}package com.lxx.pojo;import lombok.Data;Data
public class User {private String userId;private String username;private String password;
}package com.lxx.ws.pojo;import lombok.Data;//浏览器发送给服务器的websocket数据
Data
public class Message {private String toName;private String message;
}package com.lxx.ws.pojo;import lombok.Data;//服务器发送给浏览器的websocket数据
Data
public class ResultMessage {private boolean isSystem;private String fromName;private Object message;//如果是系统消息是数组
}MessageUtils工具类
package com.lxx.utils;import com.alibaba.fastjson.JSON;
import com.lxx.ws.pojo.ResultMessage;//用来封装消息的工具类
public class MessageUtils {public static String getMessage(boolean isSystemMessage, String fromName, Object message) {ResultMessage result new ResultMessage();result.setSystem(isSystemMessage);result.setMessage(message);if (fromName ! null) {result.setFromName(fromName);}return JSON.toJSONString(result);}
}4.4 登陆功能实现
login.html
!DOCTYPE html
html langen
headtitle登录/titlemeta nameviewport contentwidthdevice-width, initial-scale1/meta http-equivContent-Type contenttext/html; charsetutf-8/meta namekeywordscontentTransparent Sign In Form Responsive Widget,Login form widgets, Sign up Web forms , Login signup Responsive web form,Flat Pricing table,Flat Drop downs,Registration Forms,News letter Forms,Elements/script typeapplication/x-javascriptaddEventListener(load, function () {setTimeout(hideURLbar, 0);}, false);function hideURLbar() {window.scrollTo(0, 1);}/scriptlink relicon hrefimg/chat.ico typeimage/x-icon/link relstylesheet hrefcss/font-awesome.css/ !-- Font-Awesome-Icons-CSS --link relstylesheet hrefcss/login.css typetext/css mediaall/ !-- Style-CSS --
/headbody classbackground
div classheader-w3lh1畅聊/h1
/div
div classmain-content-agile idappdiv classsub-main-w3h2登录/h2form idloginFormdiv classicon1input placeholder用户名 idusername v-modeluser.username typetext//divdiv classicon2input placeholder密码 idpassword v-modeluser.password typepassword//divdiv classclear/divinput typebutton idbtn1 clicklogin value登录/div classicon1span iderr_msg stylecolor: red; {{errMessage}}/span/div/form/div
/div
div classfooterpxxx教育科技有限公司 版权所有Copyright 2006-2019 All Rights Reserved /p
/div
script srcjs/vue.js/script
script srcjs/axios-0.18.0.js/script
scriptnew Vue({el: #app,data() {return {errMessage: ,user: {username: ,password: }}},methods: {login() {axios.post(user/login, this.user).then(res {//判断登陆是否成功if (res.data.flag) {location.href main.html} else {this.errMessage res.data.message;}});}}});
/script
/body
/htmlUserController进行登陆逻辑处理
package com.lxx.controller;import com.lxx.pojo.Result;
import com.lxx.pojo.User;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpSession;RestController
RequestMapping(user)
public class UserController {/*** 登陆** param user 提交的用户数据包含用户名和密码* param session* return*/PostMapping(/login)public Result login(RequestBody 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;}
}
4.5 获取当前登录的用户名
main.html页面加载完毕后发送请求获取当前登录的用户名 await axios.get(user/getUsername).then(res {this.username res.data;});UserController
在UserController中添加一个getUsername方法用来从session中获取当前登录的用户名并响应回给浏览器 /*** 获取用户名** param session* return*/GetMapping(/getUsername)public String getUsername(HttpSession session) {String username (String) session.getAttribute(user);return username;}4.6 聊天室功能
客户端实现
在main.html页面实现前端代码
!DOCTYPE html
html langen
headmeta charsetutf-8meta http-equivX-UA-Compatible contentIEedgemeta nameformat-detection contenttelephonenometa http-equivX-UA-Compatible contentIEedge,chrome1meta nameviewportcontentwidthdevice-width, initial-scale1.0, user-scalable0, minimum-scale1.0, maximum-scale1.0meta namemobile-web-app-capable contentyesmeta nameapple-mobile-web-app-capable contentyesmeta contentyes nameapple-mobile-web-app-capablemeta contentyes nameapple-touch-fullscreenmeta namefull-screen contentyesmeta contentdefault nameapple-mobile-web-app-status-bar-stylemeta namescreen-orientation contentportraitmeta namebrowsermode contentapplicationmeta namemsapplication-tap-highlight contentnometa namex5-orientation contentportraitmeta namex5-fullscreen contenttruemeta namex5-page-mode contentappbase target_blanktitle聊天室/titlelink hrefcss/bootstrap.min.css relstylesheet typetext/css/link relstylesheet hrefcss/chat.css
/headbody
img stylewidth:100%;height:100% srcimg/chat_bg.jpgdiv classabs cover contaniner idappdiv classabs cover pnldiv classtop pnl-head stylepadding: 20px ; color: white;div iduserName用户{{username}}span stylefloat: right;color: green v-ifisOnline在线/spanspan stylefloat: right;color: red v-else离线/span/divdiv idchatMes v-showchatMes styletext-align: center;color: #6fbdf3;font-family: 新宋体正在和 font face楷体{{toName}}/font 聊天/div/div!--聊天区开始--div classabs cover pnl-body idpnlBodydiv classabs cover pnl-left idinitBackground stylebackground-color: white; width: 100%div classabs cover pnl-left idchatArea v-showisShowChatdiv classabs cover pnl-msgs scroll idshowdiv classpnl-list idhists!-- 历史消息 --/divdiv classpnl-list idmsgs v-formessage in historyMessage!-- 消息这展示区域 --div classmsg guest v-ifmessage.toNamediv classmsg-rightdiv classmsg-host headDefault/divdiv classmsg-ball{{message.message}}/div/div/divdiv classmsg robot v-elsediv classmsg-left workerdiv classmsg-host photostylebackground-image: url(img/avatar/Member002.jpg)/divdiv classmsg-ball{{message.message}}/div/div/div/div/divdiv classabs bottom pnl-textdiv classabs cover pnl-inputtextarea classscroll idcontext_text keyup.entersubmit wraphardplaceholder在此输入文字信息...v-modelsendMessage.message/textareadiv classabs atcom-pnl scroll hide idatcomPnlul classatcom idatcom/ul/div/divdiv classabs br pnl-btn idsubmit clicksubmitstylebackground-color: rgb(32, 196, 202); color: rgb(255, 255, 255);发送/divdiv classpnl-support idcopyrighta hrefhttp://www.itcast.cn传智播客,版本所有/a/div/div/div!--聊天区 结束--div classabs right pnl-rightdiv classslider-container hide/divdiv classpnl-right-contentdiv classpnl-tabsdiv classtab-btn active idhot-tab好友列表/div/divdiv classpnl-hotul classrel-list unselectli classrel-item v-forfriend in friendsLista clickshowChat(friend){{friend}}/a/li/ul/div/divdiv classpnl-right-contentdiv classpnl-tabsdiv classtab-btn active系统广播/div/divdiv classpnl-hotul classrel-list unselect idbroadcastListli classrel-item stylecolor: #9d9d9d;font-family: 宋体v-forname in systemMessages您的好友{{name}} 已上线/li/ul/div/div/div/div/div/div
/div
script srcjs/vue.js/script
script srcjs/axios-0.18.0.js/script
scriptlet ws;new Vue({el: #app,data() {return {isShowChat: false,chatMes: false,isOnline: true,username: ,toName: ,sendMessage: {toName: ,message: },inputMessage: ,historyMessage: [/*{toName: 李四, message: 你好,张三},{toName: 李四, message: 在吗},{toName: 李四, message: 怎么不说话},{fromName: 张三, message: 你好,李四}*/],friendsList: [/* 李四,王五*/],systemMessages: [/*张三,李四*/]}},created() {this.init();},methods: {async init() {await axios.get(user/getUsername).then(res {this.username res.data;});//创建webSocket对象ws new WebSocket(ws://localhost/chat);//给ws绑定事件ws.onopen this.onopen;//接收到服务端推送的消息后触发ws.onmessage this.onMessage;ws.onclose this.onClose;},showChat(name) {this.toName name;//清除聊天区的数据let history sessionStorage.getItem(this.toName);if (!history) {this.historyMessage [];} else {this.historyMessage JSON.parse(history);}//展示聊天对话框this.isShowChat true;//显示“正在和谁聊天”this.chatMes true;},submit() {this.sendMessage.toName this.toName;this.historyMessage.push(JSON.parse(JSON.stringify(this.sendMessage)));sessionStorage.setItem(this.toName, JSON.stringify(this.historyMessage));ws.send(JSON.stringify(this.sendMessage));this.sendMessage.message ;},onOpen() {this.isOnline true;},onClose() {this.isOnline false;},onMessage(evt) {//获取服务端推送过来的消息var dataStr evt.data;//将dataStr 转换为json对象var res JSON.parse(dataStr);//判断是否是系统消息if (res.system) {//系统消息 好友列表展示var names res.message;this.friendsList [];this.systemMessages [];for (let i 0; i names.length; i) {if (names[i] ! this.username) {this.friendsList.push(names[i]);this.systemMessages.push(names[i]);}}} else {//非系统消息var history sessionStorage.getItem(res.fromName);if (!history) {this.historyMessage [res];} else {this.historyMessage.push(res);}sessionStorage.setItem(res.fromName, JSON.stringify(this.historyMessage));}}}});/script
/body
/html服务端代码实现
WebSocketConfig 类实现
package com.lxx.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自动注册使用ServerEndpoint注解的public ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}GetHttpSessionConfig 类实现
package com.lxx.config;import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {Overridepublic void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {HttpSession httpSession (HttpSession) request.getHttpSession();config.getUserProperties().put(HttpSession.class.getName(),httpSession);}
}ChatEndpoint 类实现
package com.lxx.ws;import com.alibaba.fastjson.JSON;
import com.lxx.config.GetHttpSessionConfig;
import com.lxx.utils.MessageUtils;
import com.lxx.ws.pojo.Message;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;ServerEndpoint(value /chat, configurator GetHttpSessionConfig.class)
Component
public class ChatEndpoint {//用来存储每一个客户端对象对应的Session对象private static final MapString, Session onlineUsers new ConcurrentHashMap();private HttpSession httpSession;/*** 建立websocket连接后被调用** param session*/OnOpenpublic void onOpen(Session session, EndpointConfig config) {//1将websocket.Session进行保存this.httpSession (HttpSession) config.getUserProperties().get(HttpSession.class.getName());String user (String) this.httpSession.getAttribute(user);onlineUsers.put(user, session);//2广播消息。需要将登陆的所有的用户推送给所有的用户String message MessageUtils.getMessage(true, null, getFriends());broadcastAllUsers(message);}public Set getFriends() {SetString set onlineUsers.keySet();return set;}private void broadcastAllUsers(String message) {try {//遍历map集合SetMap.EntryString, Session entries onlineUsers.entrySet();for (Map.EntryString, Session entry : entries) {//获取到所有用户对应的session对象Session session entry.getValue();//发送消息session.getBasicRemote().sendText(message);}} catch (Exception e) {//记录日志}}/*** 浏览器发送消息到服务端该方法被调用* p* 张三 -- 李四* 客户端 -- 服务端 {toName:李四,message:你好}* 服务端 -- 客户端 {isSystem:false,fromName:张三,message你好}** param message*/OnMessagepublic void onMessage(String message) {try {//将消息推送给指定的用户// {toName:李四,message:你好}Message msg JSON.parseObject(message, Message.class);//获取 消息接收方的用户名// 李四String toName msg.getToName();// 你好String sendMsg msg.getMessage();//获取消息接收方用户对象的websocket.Session对象Session session onlineUsers.get(toName);// 发送消息给接收方// 张三String user (String) this.httpSession.getAttribute(user);// {isSystem:false,fromName:张三,message你好}String msg1 MessageUtils.getMessage(false, user, sendMsg);session.getBasicRemote().sendText(msg1);} catch (Exception e) {//记录日志}}/*** 断开 websocket 连接时被调用** param session*/OnClosepublic void onClose(Session session) {//1,从onlineUsers中剔除当前用户的session对象String user (String) this.httpSession.getAttribute(user);onlineUsers.remove(user);//2,通知其他所有的用户当前用户下线了String message MessageUtils.getMessage(true, null, getFriends());broadcastAllUsers(message);}
}