文章目录

一:消息推送常用方式介绍1.1 轮询:浏览器以指定的时间间隔向服务器发出HTTP请求,服务器实时返回数据给浏览器1.2 长轮询:浏览器发出ajax请求,服务器端接收到请求后,会阻塞请求直到有数据或者超时才返回1.3 SSE(server-sent event):服务器发送事件1.4 websocket:是一种在基于TCP连接上进行全双工通信的协议

二:websocket介绍2.1 什么是websocket2.2 websocket的原理2.3 websocket与http的关系2.3.1 相同点2.3.2 不同点2.3.3 联系2.3.4 总体过程

2.4 说明2.5 原理解析2.5.1 请求数据2.5.2 响应数据

三:websocket解决的问题3.1 http存在的问题3.2 long poll(长轮询)3.3 Ajax轮询3.4 websocket的改进

四:websocket API4.1 客户端【浏览器】API4.1.1 websocket对象创建4.1.2 websocket对象相关事件4.1.3 websocket对象提供的方法4.1.4 代码演示

4.2 服务端 API4.2.1 Endpoint说明4.2.2 定义Endpoint4.2.3 生命周期方法4.2.4 服务端如何接收客户端发送的数据4.2.5 服务端如何推送数据给客户端

五:在线聊天室代码实现5.1 流程分析5.2 引入websocket依赖5.2 前端登录界面5.2 前端聊天界面5.3 发送消息实体类5.3.1 客户端 --> 服务端5.3.2 服务端 --> 客户端

5.4 编写websocket配置类5.5 登录以及页面跳转5.6 后台核心代码

一:消息推送常用方式介绍

1.1 轮询:浏览器以指定的时间间隔向服务器发出HTTP请求,服务器实时返回数据给浏览器

1.2 长轮询:浏览器发出ajax请求,服务器端接收到请求后,会阻塞请求直到有数据或者超时才返回

1.3 SSE(server-sent event):服务器发送事件

SSE在服务器和客户端之间打开一个单向通道服务端响应的不再是一次性的数据包,而是text/event-stream类型的数据流信息服务器有数据变更时将数据流式传输到客户端

1.4 websocket:是一种在基于TCP连接上进行全双工通信的协议

二:websocket介绍

2.1 什么是websocket

WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的Websocket是一个持久化的协议

2.2 websocket的原理

websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信在websocket出现之前,web交互一般是基于http协议的短连接或者长连接websocket是一种全新的协议,不属于http无状态协议,协议名为"ws"

2.3 websocket与http的关系

2.3.1 相同点

都是基于tcp的,都是可靠性传输协议都是应用层协议

2.3.2 不同点

WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息HTTP是单向的WebSocket是需要浏览器和服务器握手进行建立连接的而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接

2.3.3 联系

WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的

2.3.4 总体过程

首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。

2.4 说明

全双工(Full Duplex):允许数据在两个方向上同时传输。 半双工(Half Duplex):允许数据在两个方向上传输,但是同一个时间段内只允许一个方向上传输。

2.5 原理解析

2.5.1 请求数据

GET ws://localhost/chat HTTP/1.1

Host: localhost

Connection: Upgrade

Upgrade: websocket

Sec-WebSocket-Version: 13

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Sec-WebSocket-Extensions: permessage-deflate

2.5.2 响应数据

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Extensions: permessage-deflate

三:websocket解决的问题

3.1 http存在的问题

http是一种无状态协议,每当一次会话完成后,服务端都不知道下一次的客户端是谁,需要每次知道对方是谁,才进行相应的响应,因此本身对于实时通讯就是一种极大的障碍http协议采用一次请求,一次响应,每次请求和响应就携带有大量的header头,对于实时通讯来说,解析请求头也是需要一定的时间,因此,效率也更低下最重要的是,需要客户端主动发,服务端被动发,也就是一次请求,一次响应,不能实现主动发送

3.2 long poll(长轮询)

对于以上情况就出现了http解决的第一个方法——长轮询基于http的特性,简单点说,就是客户端发起长轮询,如果服务端的数据没有发生变更,会 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询优点是解决了http不能实时更新的弊端,因为这个时间很短,发起请求即处理请求返回响应,实现了“伪·长连接”张三取快递的例子,张三今天一定要取到快递,他就一直站在快递点,等待快递一到,立马取走推送延迟。服务端数据发生变更后,长轮询结束,立刻返回响应给客户端。服务端压力。长轮询的间隔期一般很长,例如 30s、60s,并且服务端 hold 住连接不会消耗太多服务端资源。

3.3 Ajax轮询

特点:

基于http的特性,简单点说,就是规定每隔一段时间就由客户端发起一次请求,查询有没有新消息,如果有,就返回,如果没有等待相同的时间间隔再次询问优点是解决了http不能实时更新的弊端,因为这个时间很短,发起请求即处理请求返回响应,把这个过程放大n倍,本质上还是request = response举个形象的例子(假设张三今天有个快递快到了,但是张三忍耐不住,就每隔十分钟给快递员或者快递站打电话,询问快递到了没,每次快递员就说还没到,等到下午张三的快递到了,but,快递员不知道哪个电话是张三的,(可不是只有张三打电话,还有李四,王五),所以只能等张三打电话,才能通知他,你的快递到了)

案例分析: 从例子上来看有两个问题:

假如说,张三打电话的时间间隔为10分钟,当他收到快递前最后一次打电话,快递员说没到,他刚挂掉电话,快递入库了(就是到了),那么等下一次时间到了,张三打电话知道快递到了,那么这样的通讯算不算实时通讯?很显然,不算,中间有十分钟的时间差,还不算给快递员打电话的等待时间(抽象的解释:每次request的请求时间间隔等同于十分钟,请求解析相当于等待)假如说张三所在的小区每天要收很多快递,每个人都采取主动给快递员打电话的方式,那么快递员需要以多快的速度接到,其他人打电话占线也是问题(抽象解释:请求过多,服务端响应也会变慢)

Ajax轮询存在的问题:

推送延迟。服务端压力。配置一般不会发生变化,频繁的轮询会给服务端造成很大的压力。推送延迟和服务端压力无法中和。降低轮询的间隔,延迟降低,压力增加;增加轮询的间隔,压力降低,延迟增高

3.4 websocket的改进

一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实现了“真·长链接”,实时性优势明显。 WebSocket有以下特点:

是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)

四:websocket API

4.1 客户端【浏览器】API

4.1.1 websocket对象创建

let ws = new WebSocket(URL);

url说明: 格式:协议://ip地址/访问路径 协议:协议名称为 ws

4.1.2 websocket对象相关事件

4.1.3 websocket对象提供的方法

4.1.4 代码演示

4.2 服务端 API

4.2.1 Endpoint说明

Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范。Java WebSocket应用由一系列的Endpoint组成。Endpoint 是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口。

4.2.2 定义Endpoint

第一种是编程式, 即继承类 javax.websocket.Endpoint并实现其方法。第二种是注解式, 即定义一个POJO, 并添加 @ServerEndpoint相关注解。

4.2.3 生命周期方法

4.2.4 服务端如何接收客户端发送的数据

编程式:通过添加 MessageHandler 消息处理器来接收消息 注解式:在定义Endpoint时,通过@OnMessage注解指定接收消息的方法

4.2.5 服务端如何推送数据给客户端

发送消息则由 RemoteEndpoint 完成, 其实例由 Session 维护。发送消息有2种方式发送消息 通过session.getBasicRemote 获取同步消息发送的实例 , 然后调用其 sendXxx()方法发送消息 通过session.getAsyncRemote 获取异步消息发送实例,然后调用其 sendXxx() 方法发送消息

五:在线聊天室代码实现

5.1 流程分析

5.2 引入websocket依赖

org.springframework.boot

spring-boot-starter-websocket

5.2 前端登录界面

随意时光-登录

content="Transparent 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"/>

WeChat

账号登录

{{errMessage}}

5.2 前端聊天界面

content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">

WeChat-聊天室

服务器 网络协议 webSocket介绍及项目实战【在线聊天系统】  第1张

用户:{{username}}

在线

离线

正在和 {{toName}} 聊天

{{message.message}}

style="background-image: url(/img/avatar/Member002.jpg)">

{{message.message}}

    style="background-color: rgb(32, 196, 202); color: rgb(255, 255, 255);">

    发送

    好友列表

    系统广播

    • 您的好友

      {{name}} 已上线

    5.3 发送消息实体类

    5.3.1 客户端 --> 服务端

    {“toName”:“张三”,“message”:“你好”}

    import lombok.Data;

    /**

    * @version v1.0

    * @ClassName: ClientToServerMessage

    * @Description: 客户端 ->服务端 {"toName":"张三","message":"hello world"}

    * @Author: ikun

    */

    @Data

    public class ClientToServerMessage {

    /**

    * 发送给谁

    */

    private String toName;

    /**

    * 发送的内容

    */

    private String message;

    }

    5.3.2 服务端 --> 客户端

    系统消息格式:{“system”:true,“fromName”:null,“message”:[“李四”,“王五”]}推送给某一个用户的消息格式:{“system”:false,“fromName”:“张三”,“message”:“你好”}

    import lombok.Data;

    /**

    * @version v1.0

    * @ClassName: ServerToClientMessage

    * @Description: 服务端 -> 浏览器

    */

    @Data

    public class ServerToClientMessage {

    /**

    * 是否系统消息

    */

    private boolean isSystem;

    /**

    * 需要发送给那个人,如果是系统消息,可以为空

    */

    private String fromName;

    /**

    * 消息的内容

    * 如果是系统消息是数组

    */

    private Object message;

    }

    5.4 编写websocket配置类

    扫描添加有@ServerEndpoint注解的 Bean

    /**

    * @author ikun

    */

    @Configuration

    public class WebSocketConfig {

    /**

    * 注入ServerEndpointExporter,

    * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint

    */

    @Bean

    public ServerEndpointExporter serverEndpointExporter() {

    return new ServerEndpointExporter();

    }

    }

    获取 HttpSession 对象

    import javax.servlet.http.HttpSession;

    import javax.websocket.HandshakeResponse;

    import javax.websocket.server.HandshakeRequest;

    import javax.websocket.server.ServerEndpointConfig;

    /**

    * @version v1.0

    * @ClassName: GetHttpSessionConfig

    * @Description: TODO(一句话描述该类的功能)

    */

    public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {

    @Override

    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {

    //获取HttpSession对象

    HttpSession httpSession = (HttpSession) request.getHttpSession();

    //将httpSession对象保存起来

    sec.getUserProperties().put(HttpSession.class.getName(),httpSession);

    }

    }

    5.5 登录以及页面跳转

    import com.sysg.websocket.entity.Result;

    import com.sysg.websocket.entity.User;

    import org.springframework.web.bind.annotation.*;

    import javax.servlet.http.HttpSession;

    @RestController

    @RequestMapping("/websocket/user")

    public class WebsocketUserController {

    @RequestMapping("/main.html")

    public String getMainHtml(){

    return "websocket/main";

    }

    /**

    * 登录

    * @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;

    }

    /**

    * 获取用户名

    * @param session

    * @return

    */

    @GetMapping("/getUsername")

    public String getUsername(HttpSession session) {

    return (String) session.getAttribute("user");

    }

    }

    5.6 后台核心代码

    import com.alibaba.fastjson.JSON;

    import com.sysg.websocket.config.GetHttpSessionConfig;

    import com.sysg.websocket.utils.MessageUtils;

    import com.sysg.websocket.ws.pojo.ClientToServerMessage;

    import lombok.extern.slf4j.Slf4j;

    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;

    /**

    * @version v1.0

    * @ClassName: ChatEndpoint

    * @Description: TODO(一句话描述该类的功能)

    */

    @ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)

    @Component

    @Slf4j

    public class ChatEndpoint {

    /**

    *用来存储每一个客户端对象对应的ChatEndpoint对象

    */

    private static final Map onlineUsers = new ConcurrentHashMap<>();

    /**

    * 声明session对象,通过该对象可以发送消息给指定用户

    */

    //private Session session;

    /**

    * httpSession存储了用户名

    */

    private HttpSession httpSession;

    /**

    * 建立websocket连接后,被调用

    * @param session

    */

    @OnOpen

    public void onOpen(Session session, EndpointConfig config) {

    //1,将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);

    }

    /**

    * 获取当前在线用户

    * @return

    */

    public Set getFriends() {

    Set set = onlineUsers.keySet();

    return set;

    }

    /**

    * 广播消息给客户端

    * @param message

    */

    private void broadcastAllUsers(String message) {

    try {

    //遍历map集合

    Set> entries = onlineUsers.entrySet();

    for (Map.Entry entry : entries) {

    //获取到所有用户对应的session对象

    Session session = entry.getValue();

    //发送消息

    session.getBasicRemote().sendText(message);

    }

    } catch (Exception e) {

    //记录日志

    log.error(e.getMessage());

    }

    }

    /**

    * 浏览器发送消息到服务端,该方法被调用

    *

    * 张三 --> 李四

    * @param message

    */

    @OnMessage

    public void onMessage(String message) {

    try {

    //将消息推送给指定的用户

    ClientToServerMessage msg = JSON.parseObject(message, ClientToServerMessage.class);

    //获取 消息接收方的用户名

    String toName = msg.getToName();

    String mess = msg.getMessage();

    //获取消息接收方用户对象的session对象

    Session session = onlineUsers.get(toName);

    String user = (String) this.httpSession.getAttribute("user");

    String msg1 = MessageUtils.getMessage(false, user, mess);

    session.getBasicRemote().sendText(msg1);

    } catch (Exception e) {

    //记录日志

    log.error(e.getMessage());

    }

    }

    /**

    * 断开 websocket 连接时被调用

    * @param session

    */

    @OnClose

    public 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);

    }

    }

    import com.alibaba.fastjson.JSON;

    import com.sysg.websocket.ws.pojo.ServerToClientMessage;

    /**

    * @version v1.0

    * @ClassName: MessageUtils

    * @Description: 封装json格式消息的工具类

    */

    public class MessageUtils {

    /**

    * 将数据转化为json格式的数据

    * @param isSystemMessage

    * @param fromName

    * @param message

    * @return

    */

    public static String getMessage(boolean isSystemMessage,String fromName, Object message) {

    ServerToClientMessage result = new ServerToClientMessage();

    result.setSystem(isSystemMessage);

    result.setMessage(message);

    if(fromName != null) {

    result.setFromName(fromName);

    }

    return JSON.toJSONString(result);

    }

    }

    文章来源

    评论可见,请评论后查看内容,谢谢!!!
     您阅读本篇文章共花了: