SpringBoot websocket 长连接推送+保活(更新中...)
创始人
2024-06-02 15:39:49
0

提示:长连接,采用了WebSocket,Redis,Quartz,SpringBoot等技术

文章目录

  • 前言
  • 一、搭建环境
    • 1.1 引入依赖和配置环境
    • 1.2 html测试
  • 二、开发
    • 2.1 启动类
    • 2.2 WebSocket
      • 2.2.1 WebSocket配置
      • 2.2.2 WebSocket使用
      • 2.2.3 测试结果
    • 2.3 Redis
      • 2.3.1 Redis配置
      • 2.3.2 Redis封装成工具类
      • 2.3.3 Redis常量记录
    • 2.4 Quartz
      • 2.4.1 Quartz配置
      • 2.4.2 Job
      • 2.4.2 其他
    • 2.5 实体类
      • 2.5.1返回的vo
      • 2.5.2 R
    • 2.6 其他
      • 2.6.1 Controller
      • 2.6.2 工具类
  • 三、总结


前言

在web开发中,服务端和客户端需要一条长连接,用于消息的推送下发。
使用场景:

  1. Android上服务型 APP,需要一条长连接的消息链路,当手机端或其他端触发了数据的更改,需要实时通知APP,比如微信APP有个单独的长连接进程,弹出一个通知窗口;
  • 优点:有一个稳定的长连接,便于消息的下发;进程活着,可操作的空间增大,比如拉活其他APP。
  • 缺点:对于Android,需要考虑一下几个点:可能需要单独开一个push进程,占用资源增加;确保APP的保活,不被系统杀死;设置心跳;断开重连等。
  1. 聊天室:一个发送,一个接受。
  2. 轮询:采用轮询的方式,每隔n秒钟主动请求服务端接口,获取更新的内容。
  • 优点:实现简单,间隔时间http请求
  • 缺点:控制间隔参数是一个经验值;当机器增多时,不断的请求调用同一个接口,增加了服务端的压力。随着用户数量和设备的增加,轮询接口调用的百分比数量急剧增加。

一、搭建环境

1.1 引入依赖和配置环境

在pom.xml中引入依赖


4.0.0org.ympushserver1.0-SNAPSHOT882.7.2org.springframework.bootspring-boot-starter-weborg.projectlomboklombokorg.springframework.bootspring-boot-starter-websocketorg.springframework.bootspring-boot-starter-quartzorg.springframework.bootspring-boot-devtoolsorg.springframework.bootspring-boot-starter-data-redisorg.apache.commonscommons-pool2cn.hutoolhutool-all5.8.9com.google.code.gsongsonorg.springframework.bootspring-boot-dependencies${spring-boot.version}pomimportsrc/main/java**/*.xml**/*.yml**/*.htmltruesrc/main/resources**/*.xml**/*.yml**/*.vm**/*.txt**/*.html/static/true

application.yml中配置:

server:port: 8081spring:application:name: PushServerredis:host: localhostport: 6379lettuce:pool:max-idle: 16max-active: 32min-idle: 8devtools:restart:enabled: true

1.2 html测试

测试路径:http://localhost:8081/push_client.html
链接socket url: ws://127.0.0.1:8081/websocket/"+userId



WebSocket

WebSocket


Please input text in the fields.

二、开发

2.1 启动类

代码如下:

package com.ym.pushserver;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;/*** @Author: Yangmiao* @Date: 2023/3/9 19:09* @Desc:*/
@SpringBootApplication
@EnableScheduling
public class PushServerApplication {public static void main(String[] args) {SpringApplication.run(PushServerApplication.class,args);}
}

2.2 WebSocket

2.2.1 WebSocket配置

package com.ym.pushserver.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** @Author: Yangmiao* @Date: 2023/3/9 19:20* @Desc:*/
@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}
}

2.2.2 WebSocket使用

使用websocket建立连接,并且监听接口,引入Redis
调用路径:/websocket/{userId}

package com.ym.pushserver.service;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.StrUtil;
import com.ym.pushserver.entity.PushCommonVo;
import com.ym.pushserver.entity.R;
import com.ym.pushserver.redis.RedisConstant;
import com.ym.pushserver.redis.RedisUtil;
import com.ym.pushserver.utils.JsonUtil;
import com.ym.pushserver.utils.StrHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;/*** @Author: Yangmiao* @Date: 2023/3/9 19:23* @Desc: 发送消息*/
@Component
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {@Autowiredprivate RedisUtil redisUtil;private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);/*** 统计连接数量*/private static AtomicInteger atomicInteger = new AtomicInteger();/*** 根据userid记录对应的socket,TODO 后期加入设备id,防止统一设备一直重试*/private static Map map= new ConcurrentHashMap<>();private Session session;private String userId = "";/*** 建立连接* @param session* @param userId 用户id*/@OnOpenpublic void onOpen(Session session, @PathParam(value = "userId")String userId){try {this.session = session;this.userId = userId;map.put(userId,this);atomicInteger.getAndIncrement();log.info("onOpen, connect a new Socket {}",userId);}catch (Exception e){log.error("onOpen error {}",e.getMessage());e.printStackTrace();}}/*** 关闭连接*/@OnClosepublic void onClose(){map.remove(this.userId);atomicInteger.getAndDecrement();removeRedisKey();log.info("onClose, disconnect a socket {}",this.userId);}@OnErrorpublic void onError(Throwable throwable){map.remove(this.userId);atomicInteger.getAndDecrement();removeRedisKey();log.error("onError, error! {}",throwable.getMessage());}@OnMessagepublic void onMessage(Session session,String msg){log.info("receive msg from client: {}, session: {}",msg,session);}/*** 获取链接数量* @return*/public int getConNum(){return atomicInteger.intValue();}/*** 发送消息* @param msg*/public void sendMessage(String msg){this.session.getAsyncRemote().sendText(msg);}/*** 向指定用户发送消息* @param userId* @param msg*/public void sendMessageToUser(String userId,String msg){if (map.containsKey(userId)){// 生成key==pushIdString key = StrHelper.getFormatStr(RedisConstant.KEY_PUSH_ID,userId);redisUtil.set(key, StrHelper.getRandNum());// 将消息封装到消息体中PushCommonVo pushCommonVo = PushCommonVo.builder().msg(msg).pushId(key).updateTime(DateTime.now()).build();String jsonStr = JsonUtil.toStr(R.ok(pushCommonVo));map.get(userId).sendMessage(jsonStr);}}/*** 向指定用户群发消息* @param userIds* @param msg*/public void sendBatchMessage(List userIds,String msg){userIds.forEach(userId->{sendMessageToUser(userId,msg);});}/*** 向所有用户发送消息* @param msg*/public void sendMessageToAll(String msg){map.forEach((k,v)->{sendMessageToUser(k,msg);});}/*** 删除redis key* @return*/public boolean removeRedisKey(){if (StrUtil.isNotEmpty(this.userId)) {String key = StrHelper.getFormatStr(RedisConstant.KEY_PUSH_ID, this.userId);log.info("removeRedisKey: "+key);if (redisUtil.hasKey(key)) {redisUtil.deleteByKey(key);return true;}}return false;}/*** 服务端向客户端发送ping*/public static void pingMessage(String userId){byte[] pingByte = new byte[1];pingByte[0]='p';ByteBuffer byteBuffer = ByteBuffer.wrap(pingByte);if (map.containsKey(userId)) {try {map.get(userId).session.getBasicRemote().sendPing(byteBuffer);} catch (IOException e) {log.error("ping error: {}",e.getMessage());e.printStackTrace();}}}
}

2.2.3 测试结果

在这里插入图片描述


2.3 Redis

引入Redis,用于记录下发消息结构体重的pushId。

  1. 推送一条信息,服务端缓存消息体中的pushId,并设置过期时间
  2. 当客户端接受到消息后,http调用并确认
  3. 收到客户端的确认后,删除redis缓存

2.3.1 Redis配置

package com.ym.pushserver.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** @Author: Yangmiao* @Date: 2023/3/11 12:18* @Desc: Redis 配置*/
@Configuration
public class RedisConfig {@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic RedisTemplate redisTemplate(){RedisTemplate redisTemplate = new RedisTemplate<>();redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new StringRedisSerializer());redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}}

2.3.2 Redis封装成工具类

package com.ym.pushserver.redis;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;/*** @Author: Yangmiao* @Date: 2023/3/11 12:22* @Desc:*/
@Component
public class RedisUtil {@Autowiredprivate RedisTemplate redisTemplate;/*** 给一个指定的 key 值附加过期时间** @param key* @param time* @return*/public boolean expire(String key, long time) {return redisTemplate.expire(key, time, TimeUnit.SECONDS);}/*** 根据key 获取过期时间** @param key* @return*/public long getTime(String key) {return redisTemplate.getExpire(key, TimeUnit.SECONDS);}/*** 根据key 获取过期时间** @param key* @return*/public boolean hasKey(String key) {return redisTemplate.hasKey(key);}/*** 移除指定key 的过期时间** @param key* @return*/public boolean persist(String key) {return redisTemplate.boundValueOps(key).persist();}//- - - - - - - - - - - - - - - - - - - - -  String类型 - - - - - - - - - - - - - - - - - - - -/*** 根据key获取值** @param key 键* @return 值*/public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 将值放入缓存** @param key   键* @param value 值* @return true成功 false 失败*/public void set(String key, String value) {redisTemplate.opsForValue().set(key, value);}/*** 将值放入缓存并设置时间** @param key   键* @param value 值* @param time  时间(秒) -1为无期限* @return true成功 false 失败*/public void set(String key, String value, long time) {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(key, value);}}/*** 批量添加 key (重复的键会覆盖)** @param keyAndValue*/public void batchSet(Map keyAndValue) {redisTemplate.opsForValue().multiSet(keyAndValue);}/*** 批量添加 key-value 只有在键不存在时,才添加* map 中只要有一个key存在,则全部不添加** @param keyAndValue*/public void batchSetIfAbsent(Map keyAndValue) {redisTemplate.opsForValue().multiSetIfAbsent(keyAndValue);}/*** 对一个 key-value 的值进行加减操作,* 如果该 key 不存在 将创建一个key 并赋值该 number* 如果 key 存在,但 value 不是长整型 ,将报错** @param key* @param number*/public Long increment(String key, long number) {return redisTemplate.opsForValue().increment(key, number);}/*** 对一个 key-value 的值进行加减操作,* 如果该 key 不存在 将创建一个key 并赋值该 number* 如果 key 存在,但 value 不是 纯数字 ,将报错** @param key* @param number*/public Double increment(String key, double number) {return redisTemplate.opsForValue().increment(key, number);}//- - - - - - - - - - - - - - - - - - - - -  set类型 - - - - - - - - - - - - - - - - - - - -/*** 将数据放入set缓存** @param key 键* @return*/public void sSet(String key, String value) {redisTemplate.opsForSet().add(key, value);}/*** 获取变量中的值** @param key 键* @return*/public Set members(String key) {return redisTemplate.opsForSet().members(key);}/*** 随机获取变量中指定个数的元素** @param key   键* @param count 值* @return*/public void randomMembers(String key, long count) {redisTemplate.opsForSet().randomMembers(key, count);}/*** 随机获取变量中的元素** @param key 键* @return*/public Object randomMember(String key) {return redisTemplate.opsForSet().randomMember(key);}/*** 弹出变量中的元素** @param key 键* @return*/public Object pop(String key) {return redisTemplate.opsForSet().pop("setValue");}/*** 获取变量中值的长度** @param key 键* @return*/public long size(String key) {return redisTemplate.opsForSet().size(key);}/*** 根据value从一个set中查询,是否存在** @param key   键* @param value 值* @return true 存在 false不存在*/public boolean sHasKey(String key, Object value) {return redisTemplate.opsForSet().isMember(key, value);}/*** 检查给定的元素是否在变量中。** @param key 键* @param obj 元素对象* @return*/public boolean isMember(String key, Object obj) {return redisTemplate.opsForSet().isMember(key, obj);}/*** 转移变量的元素值到目的变量。** @param key     键* @param value   元素对象* @param destKey 元素对象* @return*/public boolean move(String key, String value, String destKey) {return redisTemplate.opsForSet().move(key, value, destKey);}/*** 批量移除set缓存中元素** @param key    键* @param values 值* @return*/public void remove(String key, Object... values) {redisTemplate.opsForSet().remove(key, values);}/*** 通过给定的key求2个set变量的差值** @param key     键* @param destKey 键* @return*/public Set difference(String key, String destKey) {return redisTemplate.opsForSet().difference(key, destKey);}//- - - - - - - - - - - - - - - - - - - - -  hash类型 - - - - - - - - - - - - - - - - - - - -/*** 加入缓存** @param key 键* @param map 键* @return*/public void add(String key, Map map) {redisTemplate.opsForHash().putAll(key, map);}/*** 获取 key 下的 所有  hashkey 和 value** @param key 键* @return*/public Map getHashEntries(String key) {return redisTemplate.opsForHash().entries(key);}/*** 验证指定 key 下 有没有指定的 hashkey** @param key* @param hashKey* @return*/public boolean hashKey(String key, String hashKey) {return redisTemplate.opsForHash().hasKey(key, hashKey);}/*** 获取指定key的值string** @param key  键* @param key2 键* @return*/public String getMapString(String key, String key2) {return redisTemplate.opsForHash().get("map1", "key1").toString();}/*** 获取指定的值Int** @param key  键* @param key2 键* @return*/public Integer getMapInt(String key, String key2) {return (Integer) redisTemplate.opsForHash().get("map1", "key1");}/*** 弹出元素并删除** @param key 键* @return*/public String popValue(String key) {return redisTemplate.opsForSet().pop(key).toString();}/*** 删除指定 hash 的 HashKey** @param key* @param hashKeys* @return 删除成功的 数量*/public Long delete(String key, String... hashKeys) {return redisTemplate.opsForHash().delete(key, hashKeys);}/*** 删除指定keys* @param keys*/public void deleteByKey(String ...keys){if (!StringUtils.isEmpty(keys)){if (keys.length==1){redisTemplate.delete(keys);}else {redisTemplate.delete(CollectionUtils.arrayToList(keys));}}}/*** 给指定 hash 的 hashkey 做增减操作** @param key* @param hashKey* @param number* @return*/public Long increment(String key, String hashKey, long number) {return redisTemplate.opsForHash().increment(key, hashKey, number);}/*** 给指定 hash 的 hashkey 做增减操作** @param key* @param hashKey* @param number* @return*/public Double increment(String key, String hashKey, Double number) {return redisTemplate.opsForHash().increment(key, hashKey, number);}/*** 获取 key 下的 所有 hashkey 字段** @param key* @return*/public Set hashKeys(String key) {return redisTemplate.opsForHash().keys(key);}/*** 获取指定 hash 下面的 键值对 数量** @param key* @return*/public Long hashSize(String key) {return redisTemplate.opsForHash().size(key);}//- - - - - - - - - - - - - - - - - - - - -  list类型 - - - - - - - - - - - - - - - - - - - -/*** 在变量左边添加元素值** @param key* @param value* @return*/public void leftPush(String key, Object value) {redisTemplate.opsForList().leftPush(key, value);}/*** 获取集合指定位置的值。** @param key* @param index* @return*/public Object index(String key, long index) {return redisTemplate.opsForList().index("list", 1);}/*** 获取指定区间的值。** @param key* @param start* @param end* @return*/public List range(String key, long start, long end) {return redisTemplate.opsForList().range(key, start, end);}/*** 把最后一个参数值放到指定集合的第一个出现中间参数的前面,* 如果中间参数值存在的话。** @param key* @param pivot* @param value* @return*/public void leftPush(String key, String pivot, String value) {redisTemplate.opsForList().leftPush(key, pivot, value);}/*** 向左边批量添加参数元素。** @param key* @param values* @return*/public void leftPushAll(String key, String... values) {
//        redisTemplate.opsForList().leftPushAll(key,"w","x","y");redisTemplate.opsForList().leftPushAll(key, values);}/*** 向集合最右边添加元素。** @param key* @param value* @return*/public void leftPushAll(String key, String value) {redisTemplate.opsForList().rightPush(key, value);}/*** 向左边批量添加参数元素。** @param key* @param values* @return*/public void rightPushAll(String key, String... values) {//redisTemplate.opsForList().leftPushAll(key,"w","x","y");redisTemplate.opsForList().rightPushAll(key, values);}/*** 向已存在的集合中添加元素。** @param key* @param value* @return*/public void rightPushIfPresent(String key, Object value) {redisTemplate.opsForList().rightPushIfPresent(key, value);}/*** 向已存在的集合中添加元素。** @param key* @return*/public long listLength(String key) {return redisTemplate.opsForList().size(key);}/*** 移除集合中的左边第一个元素。** @param key* @return*/public void leftPop(String key) {redisTemplate.opsForList().leftPop(key);}/*** 移除集合中左边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。** @param key* @return*/public void leftPop(String key, long timeout, TimeUnit unit) {redisTemplate.opsForList().leftPop(key, timeout, unit);}/*** 移除集合中右边的元素。** @param key* @return*/public void rightPop(String key) {redisTemplate.opsForList().rightPop(key);}/*** 移除集合中右边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。** @param key* @return*/public void rightPop(String key, long timeout, TimeUnit unit) {redisTemplate.opsForList().rightPop(key, timeout, unit);}
} 

2.3.3 Redis常量记录

package com.ym.pushserver.redis;/*** @Author: Yangmiao* @Date: 2023/3/11 12:24* @Desc:*/
public class RedisConstant {/*** push结构体中携带pushId*/public static final String KEY_PUSH_ID = "pushId_%s";/*** 5分钟*/public static final long COMMON_EXPIRE_TIME = 300;
}

2.4 Quartz

引入Quartz定时任务,用于定时触发消息的下发

2.4.1 Quartz配置

配置JobDetail,Trigger,CronTrigger

package com.ym.pushserver.quartz;import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @Author: Yangmiao* @Date: 2023/3/10 20:49* @Desc:*/
@Configuration
public class QuartzConfig {@Beanpublic JobDetail getJobDetail(){return JobBuilder.newJob(PushJob.class).withIdentity(QuartzConstant.IDENTITY).storeDurably().build();}@Beanpublic Trigger trigger(){return TriggerBuilder.newTrigger().forJob(getJobDetail()).withIdentity(QuartzConstant.IDENTITY).withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)).build();}@Beanpublic CronTrigger cronTrigger(){return TriggerBuilder.newTrigger().withIdentity(QuartzConstant.IDENTITY).startNow().withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(10,0)).build();}
}

2.4.2 Job

package com.ym.pushserver.quartz;import com.ym.pushserver.service.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;/*** @Author: Yangmiao* @Date: 2023/3/10 20:47* @Desc:*/
@Component
@Slf4j
public class PushJob extends QuartzJobBean {@Autowiredprivate WebSocketServer webSocketServer;@Overrideprotected void executeInternal(JobExecutionContext context) throws JobExecutionException {
//        log.info("start task!");// TODO 向客户端定时推送消息webSocketServer.sendMessageToAll("我成功连接了!");}
}

2.4.2 其他

定义job名称常量

package com.ym.pushserver.quartz;/*** @Author: Yangmiao* @Date: 2023/3/11 09:41* @Desc:*/
public class QuartzConstant {public static final String IDENTITY = "PushJob";
}

2.5 实体类

2.5.1返回的vo

BaseVo

package com.ym.pushserver.entity;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;import java.io.Serializable;
import java.util.Date;/*** @Author: Yangmiao* @Date: 2023/3/11 10:34* @Desc: 返回消息给客户端*/
@Data
@SuperBuilder
public class BaseVo implements Serializable {public static final long serialVersionUID = 1L;/*** 每条消息自带一个ID*/public String pushId;/*** 下发消息的时间*/public Date updateTime;
}
package com.ym.pushserver.entity;import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.SuperBuilder;import java.util.Date;
import java.util.Objects;/*** @Author: Yangmiao* @Date: 2023/3/11 11:31* @Desc:*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@SuperBuilder
public class PushCommonVo extends BaseVo{private String msg;//    @Builder
//    public  PushCommonVo(String pushId, Date updateTime,T msg){
//        super(pushId, updateTime);
//        this.msg = msg;
//    }}

2.5.2 R

package com.ym.pushserver.entity;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;/*** @Author: Yangmiao* @Date: 2023/3/11 12:57* @Desc: 基础信息*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class R implements Serializable {public static final long serialVersionUID = 1L;private T data;private int code;private String msg;public static final String MSG_SUCCESS = "success";public static final String MSG_FAIL = "服务开了一会儿小差,请稍后重试";public static final int SUCCESS = 200;public static final int FAIL = 500;public static R ok(){return R(SUCCESS,null,MSG_SUCCESS);}public static  R ok(T data){return R(SUCCESS,data,MSG_SUCCESS);}public static  R fail(){return R(FAIL,null,MSG_FAIL);}public static  R fail(String msg){return R(FAIL,null,msg);}public static  R fail(int code,String msg){return R(code,null,msg);}public static  R R(int code, T data, String msg){return R.builder().code(code).data(data).msg(msg).build();}
}

2.6 其他

2.6.1 Controller

package com.ym.pushserver.controller;import cn.hutool.core.util.StrUtil;
import com.ym.pushserver.entity.R;
import com.ym.pushserver.redis.RedisUtil;
import com.ym.pushserver.service.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @Author: Yangmiao* @Date: 2023/3/9 20:00* @Desc:*/
@RestController
@RequestMapping("/push")
public class PushController {@Autowiredprivate WebSocketServer webSocketServer;@Autowiredprivate RedisUtil redisUtil;@GetMapping("/test/{msg}")public R test(@PathVariable("msg")String msg){webSocketServer.sendMessageToUser("1","hahhhh"+msg);return R.ok("测试成功: "+msg);}@GetMapping("/pushAck")public R pushAck(String pushId){if (StrUtil.isEmpty(pushId)){return R.fail("请求参数为空!");}redisUtil.deleteByKey(pushId);return R.ok();}}

2.6.2 工具类

Gson工具类封装,用于序列化和反序列化数据

package com.ym.pushserver.utils;import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ssh.JschUtil;
import cn.hutool.json.JSONUtil;
import com.google.gson.Gson;
import org.springframework.util.StringUtils;import java.util.Objects;/*** @Author: Yangmiao* @Date: 2023/3/11 13:20* @Desc:*/
public class JsonUtil {public static final String EMPTY = "";private static Gson gson = new Gson();/*** 将object转为string* @param object* @param * @return*/public static  String toStr(T object){if (Objects.isNull(object)){return EMPTY;}return gson.toJson(object);}/*** string转为object对象* @param jsonStr* @param clazz* @param * @return*/public static  T toJson(String jsonStr, Class clazz){return gson.fromJson(jsonStr,clazz);}
}

str工具类封装

package com.ym.pushserver.utils;import java.util.UUID;/*** @Author: Yangmiao* @Date: 2023/3/9 20:07* @Desc:*/
public class StrHelper {/*** 获取随机数* @return*/public static String getRandNum(){return UUID.randomUUID().toString().replace("-","");}public static String getFormatStr(String format,String...args){return String.format(format,args);}
}

三、总结

引入了websocket,redis,quartz,springboot等技术,实现了一个长连接Demo
存在问题:

  • 客户端设置心跳,ping数据
  • 客户端保活
  • 可连接数量

待完成

  1. Android客户端
  2. 服务端的稳定,连接数量测试
  3. 鉴权,防止任何客户端都连接服务端
  4. 数据加解密

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...