WebSocket

1. WebSocket介绍

WebSocket是基于TCP连接实现双向绑定的协议,提供了持久化连接,也就是说不需要每次都重新连接服务器去接收消息,服务器可以往客户端发送消息, 客户端也可以往服务器发送消息,实时通信器;

2. WebSocket的前身

2.1 轮询

在早期,WebSocket所用的技术是轮询,轮询分为两种一个是短轮询,另外一个是长轮询:

l 短轮询:其技术就是需要在一段时间才会向服务器发送请求,然后才能返回最新的数据;

l 长轮询:就是他会一定时间内会去问发送请求,如没有数据,他则会一种处于等待状态,等待服务器提供新的消息,有则会立刻对网页更新,但不会一直保持与服务器连接

2.2 轮询中短轮询与长轮询的区别:

短轮询是客户端不断地、周期性地向服务器发送请求;

长轮询是客户端发送请求后,服务器会保持连接直到有数据可发送。长轮询通常比短轮询更高效,因为它减少了不必要的请求次数;

2.3 轮询与WebSocket区别:

WebSocket:适用于需要实时数据传输的场景,如在线游戏、实时聊天应用等,因为它提供了低延迟的双向通信。

3. WebSocket生命周期

3.1 六大生命周期

WebSocket共有六个生命周期:分别是:握手.连接建立.数据传输.心跳检测.连接关闭.错误处理 这六种

  1. 握手 (Handshake) :客户端通过HTTP请求发起WebSocket连接,服务器响应建立连接;

  2. 连接建立(Connection Establishment):握手成功后,TCP连接升级为WebSocket连接,双方开启通信;

  3. 数据传输(Data Transfer):客户端和服务器交换文本或二进制数据;

  4. 心跳检测(Heartbeat):为了保持连接的活跃性,WebSocket协议允许发送心跳(ping/pong)帧。这些帧用于检测连接是否仍然可用,并防止因为网络空闲而被某些中间设备(如NAT)关闭;

  5. 连接关闭(Connection Closure):当通信结束时,通过发送特定的关闭帧来关闭WebSocket连接,双方确认后断开TCP连接;

  6. 错误处理 (Error Handling) :就是字面意思去处理掉WebSocket执行时发生的异常;

3.2 生命周期核心与生命周期示意图

最主要的生命周期:就是连接建立,数据交换以及连接关闭,这三个阶段涵盖了WebSocket从建立到结束的整个生命周期,是WebSocket通信过程中最核心的部分;

握手请求:客户端(Client)向服务器(Server)发送一个握手请求。这个请求通常是HTTP请求,包含了特定的头部信息,告诉服务器客户端想要建立WebSocket连接。

握手响应:服务器接收到握手请求后,如果同意建立WebSocket连接,会发送一个握手响应给客户端。这个响应也是HTTP响应,包含了确认连接的相关信息。

WebSocket : 连接开放:一旦握手成功,客户端和服务器之间的WebSocket连接就被认为是开放的,可以开始交换数据。

WebSocket : 连接关闭:当通信结束时,任一端可以发起关闭WebSocket连接的请求。

WebSocket : 连接关闭完成:另一端接收到关闭请求后,会确认关闭连接,至此WebSocket连接完全关闭。

下方是生命周期示意图:

4. WebSocket特点及对象

4.1 特点

  • 全双工通信:WebSocket协议支持服务器和客户端之间的全双工通信,客户端和服务器可以同时发送消息。

  • 持久连接:WebSocket连接一旦建立,将持续保持打开状态,直到客户端或服务器关闭连接。

  • 跨域通信:WebSocket协议支持跨域通信,允许不同域的服务器与客户端建立连接。

  • 限制:不提供加密功能,如果在安全上有需求,可以采用如:SSL协议,限制访问权限设置白名单只允许特定的IP地址或者域名的客户端连接

  • 兼容:在IE10之前的版本是不知道,需要使用AJAX对其替代数据传输

4.2 对象

4.2.1 对象的创建

通过let ws=new WebSocket(URL); URL说明: 1.各式:协议://ip地址/访问路径 2.协议:协议名称为WS;

4.2.2 对象的相关的事件

WebSocket的对象事件有4种 :Open(连接),Message(接受发送消息),Close(断开连接),Error(处理异常):

事件 事件处理程序 描述
open ws.onopen 就像你拨通电话后听到"嘟"声,表示你和服务器的通话(连接)已经接通了,现在可以开始聊天(发送消息)了。
message ws.onmessage 当你听到对方(服务器)说话(发送消息)时,这个事件就会提醒你,你就可以听到(接收)对方说了什么。
close ws.onclose 就像通话结束,电话挂断了。这个事件告诉你连接已经断开,你可以检查一下是不是要重新拨号(尝试重新连接)。
error ws.onerror 如果通话过程中出现了问题,比如信号不好或者电话坏了,这个事件就会告诉你出了什么问题,你可以尝试解决或者告诉别人电话出问题了。
4.2.3 对象所提供的方法
方法/事件 描述
WebSocket(url, [protocol]) 创建一个新的WebSocket连接到指定的服务器URL,可选地指定子协议。
close([code], [reason]) 关闭WebSocket连接,可以指定一个状态码和关闭原因。
send(data) 向服务器发送数据,数据可以是字符串、二进制数据或Blob对象。
onopen 连接成功建立时触发的事件处理程序。
onmessage 从服务器接收到消息时触发的事件处理程序。
onerror WebSocket连接遇到错误时触发的事件处理程序。
onclose WebSocket连接关闭时触发的事件处理程序。

在这之中最主要的方法有三个,分别是 : WebSocket(url,protocol) , Send(data) , Close(code,reason);

通俗来说就是:

(1)这就像是拿起电话,拨通服务器的号码。你需要告诉 WebSocket 你要连接的服务器地址(URL),有时候还需要指定你们通话用的语言(协议)。

(2)一旦电话接通,你就可以用这个方法来发送消息给服务器,就像你对着电话说话一样。

(3) 当你说完话,想要挂断电话时,这个方法就派上用场了。你可以告诉服务器你要挂断(关闭连接),有时候还可以给个理由(关闭原因)。

5 WebSocket使用

5.1 导入依赖

这边使用的是Spring Boot 开发的项目,使用的JDK版本是:1.8,首先先再pom.xml中添加依赖为:

XML 复制代码
 <dependencies>
     <!--引入 WebSocket 依赖 -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-websocket</artifactId>
     </dependency>
 ​
     <!--为使用了注解的实体提供get,set.toString方法等-->
     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
     </dependency>
 ​
     <!--阿里提供的:用于JSON与字符串之间转换-->
     <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.78</version>
     </dependency>
     
     <!--这边使用的页面是JSP所以需要用到Web的依赖 -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
 </dependencies>
      <!--插件 -->
     <build>
         <finalName>chat</finalName>
         <plugins>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
         </plugins>
     </build>

5.2 构造初始项目:

5.2.1 创建pojo

这里面有两个实体类分别是Result和User

java 复制代码
 @Data
 public class Result {
     private boolean flag;
     private String message;
 }

java 复制代码
 @Data
 public class User {
 ​
     private String userId;
     private String username;
     private String password;
 }
5.2.2 创建工具包Utils
java 复制代码
 @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;
     }
     /**
      * 获取用户名
      * @param session
      * @return
      */
     @GetMapping("/getUsername")
     public String getUsername(HttpSession session) {
         String username = (String) session.getAttribute("user");
         return username;
     }
 }
5.2.3 创建ws

ws就是WebSocket的缩写用来完成数据传输的;

里面也有一个pojo,实体类有两个分别是:Message与ResultMessage

java 复制代码
 @Data
 public class Message {
     private String toName;
     private String message;
 }

java 复制代码
 @Data
 public class ResultMessage {
 ​
     private boolean isSystem;
     private String fromName;
     private Object message;//如果是系统消息是数组
 }
5.2.4 创建与编写Controller

在这我们创建一个controller包在这里面在创建一个UserControllerl里面包含登录与获取用户名的业务功能

java 复制代码
 @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;
     }
 ​
     /**
      * 获取用户名
      * @param session
      * @return
      */
     @GetMapping("/getUsername")
     public String getUsername(HttpSession session) {
         String username = (String) session.getAttribute("user");
         return username;
     }
     
 }
5.2.5 创建Config配置类

配置类分为两个:1.GetHttpSessionConfig 2.WebSocketConfig

1.GetHttpSessionConfig :用来将登录就进来的用户以Session来保存起来

java 复制代码
 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);
     }
 }

2.WebSocketConfig:要来将使用了@ServerEndpoint 注解的类交给Spring 去管理

java 复制代码
@CacheConfig
//表示这是一个配置类
public class WebSocketConfig {
    @Bean
    // 注入ServerEndpointExporter bean,用来自动扫描使用了 @ServerEndpoint 注解的类
 public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter();
  }

}

这些是在WS文件包外面创建的,接着写完以上两个配置类我们还需要为WebSocket创建他的配置类就在WS文件夹内创建ChatEndpoint这个类是WebSocket的生命周期;

里面有建立连接onOpen,处理消息onMessage,以及关闭连接onClose

java 复制代码
@ServerEndpoint("/chat")
@Component  //交给Spring去管理
public class ChatEndpoint {

    @OnOpen
    // 连接建立
    public void onOpen(Session session, EndpointConfig config) {
    }

    @OnMessage
    // 接收消息
    public void onMessage(String message) {
    }

    @OnClose
    // 连接关闭
    public void onClose(Session session) {
    }

}

5.3 正式开始编写代码

5.3.1编写onOpen()

在onOpen中需要实现两个步骤,(1):将session进行保存 (2)广播消息,将登录的用户互相推送

首先编写一个用来存储的Map集合 :

定义为常量是因为需要将他们全部存入一个地方,如果不使用,则它每次都会创建一个新的Map集合

在继续编写onOpen方法:

步骤一:

我们说过了他步骤一是存储session 所以我们需要先使用上面的配置类 GetHttpSessionConfig

在本类的注解上添加:

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

在使用 EndpointConfig config 去获取我们之前存储的用户;

getUserProperties().get(HttpSession.class.getName());

在通过 httpSession.getAttribute("user") 去获取用户名;

具体代码如下:

java 复制代码
//1,将session进行保存
this.httpSession = (HttpSession) config.getUserProperties()
                                       .get(HttpSession.class.getName());
String user = (String) this.httpSession.getAttribute("user");
 // user是在登录时,存储时的key
onlineUsers.put(user,session);

步骤二:

广播消息推送所有用户

首先在定义一个方法用来遍历Map集合,先使用 onlineUsers.entrySet(); 创建出一个Set集合在使用 .for 去增强循环,从中获取到所有用户对应的session对象 最后将传入的message为其发送出去;

具体代码如下:

java 复制代码
private void broadcastAllUsers(String message) {
    try {
        //遍历map集合
        Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
        for (Map.Entry<String, Session> entry : entries) {
            //获取到所有用户对应的session对象
            Session session = entry.getValue();
            //发送同步消息
            session.getBasicRemote().sendText(message);
        }
    } catch (Exception e) {
        //记录日志
    }
}

在到上面调用它, 然后在使用工具类去从中获取到message,

MessageUtils.getMessage(是不是系统消息,null,系统给的消息还需从别处获取)
/* 三个值的解释 1.表示这不是系统消息。2.发送消息的用户名。 3.实际的消息内容。*/

在为其编写一个方法 : getFriends()用来获取用户名称,在步骤一时我们已经存储了用户名称,就使用Set来存储一下他们的名字所以getFriends的代码就是:

public Set getFriends() {
Set<String> set = onlineUsers.keySet();
return set;
}

最后步骤二的代码如下:

java 复制代码
//2:广播消息,将登录的用户互相推送
String message = MessageUtils.getMessage (true, null, getFriends () );
broadcastAllUsers(message);

onOpen建立连接就完成了!

5.3.2编写onMessage()

该方法需要实现的就是将消息发送给服务器,服务器再将该消息转发给另外一个浏览器,我们也分为以下几个步骤:

将消息具体推送给具体用户:在输消息时我们使用字符串不太方便所以我们需要转换JSON类型然后我们之前就已经注入了JSON之间转换的依赖了所以我们可以直接使用;

代码如下:

java 复制代码
Message msg = JSON.parseObject(message, Message.class);

Message是一个实体里面包含了你要发送给谁,以及具体消息所以再这就是将message中的值为该实体赋值;

接下来我们再转出具体的你要发送给谁的那个名字以及具体的消息,通过上面赋值了的获取就好了:

String toName = msg.getToName(); //要发送给谁
String mess = msg.getMessage(); //具体消息

接下来我们再从之前存储Session的集合中获取toName接受方的session

Session session = onlineUsers.get(toName);

然后获取到了接收方的Session 这时候我们就可以将消息传输给服务器,再通过服务器传输给客户端,首先我们再通过MessageUtils这个工具类,去实现传输,首先我们需要获取到我们当前登录的用户名称,再onOpen方法中以及使用到了所以我们还是可以copy下来;

具体的代码如下:

java 复制代码
String user = (String) this.httpSession.getAttribute("user");  
//获取当前登录的用户名称
String msg1 = MessageUtils.getMessage(false, user, mess);
/* false:表示这不是系统消息。
   user:发送消息的用户名。
   mess:实际的消息内容。*/

最后我们再将他使用 session的.getBasicRemote().setdText()方法为其传输消息:

session.getBasicRemote().sendText(msg1);

5.3.3 编写 onClose()

在断开连接时调用的方法首先第一步:从onlineUsers中剔除当初用户的对象session; 第二步通知其他用户自己退出了连接;

下面来编写第一步剔除当前用户: onlineUsers.remove(用户名); 这时候的用户名之前在onOpen中就获取过了所以我们可以去copy下来 ;

代码如下:

//1.将当前用户下线,剔除;
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.remove(user);

步骤二:

通知其他用户的方法也在onOpen中写过了也可以copy下来:

//2,广播消息,将还在登录状态的用户互相推送
String message = MessageUtils.getMessage (true, null, getFriends () );
broadcastAllUsers(message);

步骤二的原理就等于是刷新了页面,然后将下线的用户删除掉,保留了还在线的用户

onClose方法也就实现完成了;

5.3.4 ChatEndpoint类完整代码

最后完整代码如下:

java 复制代码
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {

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

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

    public Set getFriends() {
        Set<String> set = onlineUsers.keySet();
        return set;
    }
    
    private void broadcastAllUsers(String message) {
        try {
            //遍历map集合
            Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
            for (Map.Entry<String, Session> entry : entries) {
                //获取到所有用户对应的session对象
                Session session = entry.getValue();
                //发送消息
                session.getBasicRemote().sendText(message);
            }
        } catch (Exception e) {
            //记录日志
        }
    }

    /**
     * 浏览器发送消息到服务端,该方法被调用
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        try {
            //将消息推送给指定的用户
            Message msg = JSON.parseObject(message, Message.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) {
            //记录日志
        }
    }

    /**
     * 断开 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);
    }
}
相关推荐
omegayy20 分钟前
KCP解读:拥塞控制
服务器·网络·网络协议·计算机网络·c#·游戏程序·kcp
狄加山67521 分钟前
系统编程(网络,文件基础)
网络·智能路由器
至天24 分钟前
Laravel 新 WebSocket 服务 Reverb 使用指南
websocket·php·laravel·broadcast·composer·队列·reverb
AIHE-TECH2 小时前
PLC实现HTTP协议JSON格式数据上报对接的参数配置说明
网络协议·http·json·url·网页·西门子plc·mes
LLLuckyGirl~2 小时前
计算机网络之---MAC协议
网络·计算机网络·macos
WoTrusSSL4 小时前
SSL 证书格式和证书文件扩展名:完整指南
网络·网络协议·ssl
wang09074 小时前
SSL,TLS协议分析
网络·网络协议·ssl
秋说6 小时前
【网络协议】静态路由详解
网络协议·静态路由
网络安全-杰克8 小时前
开源靶场1
网络·web安全·开源
Hacker_Nightrain8 小时前
CTF知识点总结(三)
网络·安全·web安全