WebSocket笔记——服务端接收移动端定位在前端显示
一、需求
服务端需要对接APP和网页前端,将APP端的定位信息整理并返回给前端。前端需要展示在线人员位置以及基础信息。
一开始想用netty(网络I/0编程框架)实现,后面考虑到成本和时间问题,最后选择简单的webSocket协议。
二、什么是WebSocket?
1、介绍
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
2、配置
<!--WebSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
三、WebSocket和Netty怎么选
主要看并发量。
webSocket和netty本质上是两样不同的东西,webSocket是一种TCP的通信协议,而netty则是一个通信框架,netty不但可以实现webSocket的功能,还可以实现异步I/0操作,解决高并发问题。但需要APP端也编写netty的代码,以客户端和服务端的形式交互。
像这次需求比较简单,时间比较紧的话,使用webSocket就足够了。
四、设计篇
1、交互
APP端发送一串json字符串,后端识别整理并定时推送给前端。
{"userId":10086,"longitude":112.3454897,"latitude":"23.3454897","height":100,"teamName":"2","userName":"张三"}
但考虑到以后需要网页端和APP端都需要连接webSocket,以及往后业务的增加webSocket可能不止用于定位的业务
于是又加入了两个字段
{"jobType":"JOBTYPE_GPS","clientType":"APP"}
后续发现APP端需要接受发送成功的反馈,于是加入时间戳字段,并将时间戳推送给APP端。
{"jobType":"JOBTYPE_GPS","clientType":"APP","userId":10086,"longitude":112.3454897,"latitude":"23.3454897","height":100,"teamName":"2","userName":"张三",time:1607339673336}
返回给APP端。
{time:1607339673336}
设计完对应POJO对象也清楚了
父类:保存公共属性
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonWebSocketJson {
/**
* 上传时间
*/
private Long time;
/**
* 业务类型
*/
private String jobType;
/**
* 客户端类型
*/
private String clientType;
}
业务类:定义交互的格式
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GpsToWebJson extends CommonWebSocketJson{
/**
* 接入的人员的id
*/
private String userId;
/**
* 经度
*/
private Double longitude;
/**
* 纬度
*/
private Double latitude;
/**
* 高程
*/
private Double height;
/**
* 队名
*/
private String teamName;
/**
* 人名
*/
private String userName;
}
2、开发
因为要用到定时任务
所以要加启动类上加上@EnableScheduling
配置定时线程池
@Bean
public TaskScheduler taskScheduler(){
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);//线程池的基本大小
taskScheduler.initialize();
return taskScheduler;
}
clientId :接入的客户端的唯一id,一般用用户id即可
clientType : 接入的客户端类型 ,这里定义两种类型 移动端:APP|网页端:WEB
@ServerEndpoint("/webSocket/{clientId}/{clientType}")
@Component
@Slf4j
public class WebSocketServer {
/**
* 定位业务 在线成员列表
*/
private static Map<String,GpsToWebJson> gpsOnlineMap = new ConcurrentHashMap<>();
/**
* 定位业务 clientId 列表 用来保存需要广播的客户端
*/
private static Set<String> gpsWebClient = new ConcurrentHashSet<>();
/**
* 在线客户端总数
*/
private static int onlineCount = 0;
/**
* 移动端WebSocket连接
*/
private static Map<String,WebSocketServer> appMap = new ConcurrentHashMap<>();
/**
* 网页端WebSocket连接
*/
private static Map<String,WebSocketServer> webMap = new ConcurrentHashMap<>();
/**
* session
*/
private Session session;
/**
* 客户端id
*/
private String clientId;
/**
* 客户端类型
*/
private String clientType;
@OnOpen
public void onOpen(@PathParam("clientId") String clientId,@PathParam("clientType") String clientType, Session session){
this.clientId = clientId;
this.session = session;
this.clientType = clientType;
switch (clientType) {
case ClientType.CLIENT_TYPE_APP:
this.addClientToMap(appMap);
break;
case ClientType.CLIENT_TYPE_WEB:
default:
this.addClientToMap(webMap);
}
}
/**
* 将客户端添加到对应的Map中
* @param webSocketMap
*/
private void addClientToMap(Map<String,WebSocketServer> webSocketMap){
if (webSocketMap.containsKey(clientId)) {
webSocketMap.remove(clientId);
webSocketMap.put(clientId, this);
//加入set中
} else {
webSocketMap.put(clientId, this);
//加入set中
addOnlineCount();
//在线数加1
}
webSocketMap.put(clientId, this);
log.info("用户" + clientId + "连接上webSocket,当前连接人数为" + getOnlineCount());
log.info("当前在线APP数为" + appMap.size());
log.info("当前在线Web数为" + webMap.size());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
switch (clientType) {
case ClientType.CLIENT_TYPE_APP:
this.removeClientFromMap(appMap);
//定位业务中去掉
gpsOnlineMap.remove(clientId);
break;
case ClientType.CLIENT_TYPE_WEB:
default:
this.removeClientFromMap(webMap);
gpsWebClient.remove(clientId);
}
}
/**
* 将客户端从map中移除
* @param webSocketMap
*/
private void removeClientFromMap(Map<String, WebSocketServer> webSocketMap) {
if(webSocketMap.containsKey(clientId)){
webSocketMap.remove(clientId);
//从set中删除
subOnlineCount();
}
log.info("用户退出:"+clientId+",当前在线人数为:" + getOnlineCount());
log.info("当前在线APP数为" + appMap.size());
log.info("当前在线Web数为" + webMap.size());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("app端发来的信息是:"+message);
//根据业务type判断要进行的业务
this.doJob(message);
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:"+this.clientId+",原因:"+error.getMessage());
error.printStackTrace();
}
/**
* 处理业务的方法
* @param message
*/
private void doJob(String message) {
//将消息转化为json字符串
CommonWebSocketJson commonJson = JSONObject.parseObject(message, CommonWebSocketJson.class);
//判断要进行的业务
switch (commonJson.getJobType()){
case JobType.GPS:
//定位业务
this.doGPS(ClientType.CLIENT_TYPE_APP,message);
break;
default:
log.error("WebSocket错误: app传输数据格式错误,找不到业务代码!");
}
}
/**
* 业务:gps定位
* @param message
*/
private void doGPS(String clientType, String message) {
//如果是APP
if(clientType.equals(ClientType.CLIENT_TYPE_APP)){
GpsToWebJson gpsToWebJson = JSONObject.parseObject(message, GpsToWebJson.class);
String userId = gpsToWebJson.getUserId();
if(gpsOnlineMap.containsKey(userId)){
//假如原本就在线,则进行替换
gpsOnlineMap.remove(userId);
gpsOnlineMap.put(userId,gpsToWebJson);
}else {
//不在则加入
gpsOnlineMap.put(userId,gpsToWebJson);
}
String result = "{\"time\":"+gpsToWebJson.getTime()+"}";
this.session.getAsyncRemote().sendText(result);
}
else if (clientType.equals(ClientType.CLIENT_TYPE_WEB)){
}
}
/**
* 定时向前端推送 定位列表
*/
@Scheduled(cron = "* * * * * ?")
public void sendGPSMessage(){
List<GpsToWebJson> onlineList = new ArrayList<>();
for(String key : gpsOnlineMap.keySet()){
onlineList.add(gpsOnlineMap.get(key));
}
//广播给全部web端
Iterator<String> iterator = gpsWebClient.iterator();
while(iterator.hasNext()){
String next = iterator.next();
webMap.get(next).sendMessage(onlineList);
}
}
/**
* 定时清理定位列表(掉线设备)
*/
@Scheduled(cron = "0 * * * * ?")
public void refurbishGPSList(){
gpsOnlineMap = new ConcurrentHashMap<>();
}
/**
* 将web端添加到gpsWebClient
* @param webId
*/
public static void addToGpsWebClient(String webId){
gpsWebClient.add(webId);
}
/**
* 将Web端从gpsWebClient移除
*/
public static void removeFromGpsWebClient(String webId){
gpsWebClient.remove(webId);
}
/**
* 向执行某个任务的客户端发送消息
*/
public void sendMessage(Object message){
//this.session.getAsyncRemote().sendText(JSONObject.toJSONObject(message));
this.session.getAsyncRemote().sendText(JSONArray.toJSONString(message));
}
public static synchronized int getOnlineAppCount() {
return appMap.size();
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
public static synchronized void gpsAppOffLine(String clientId) {
gpsOnlineMap.remove(clientId);
}
}
因为上面说了要区分业务
所以网页端还需要进行上线和下线的操作。
/**
* 添加客户端到gps广播
* @param clientId
* @return
*/
@ApiOperation(value = "添加客户端到gps广播")
@GetMapping("/addWebToGpsClient/{clientId}")
public CommonResult addWebToGpsClient(@PathVariable("clientId") @NotNull String clientId){
WebSocketServer.addToGpsWebClient(clientId);
log.info("有客户端加入进来啦!");
return CommonResult.success("该客户端可以接收到GPS业务的广播了!");
}
/**
* 从gps业务中移除客户端
* @param clientId
* @return
*/
@ApiOperation(value = "从gps业务中移除客户端")
@GetMapping("/removeWebFromGpsClient/{clientId}")
public CommonResult removeWebFromGpsClient(@PathVariable("clientId") @NotNull String clientId){
WebSocketServer.removeFromGpsWebClient(clientId);
return CommonResult.success("该客户端将不再接收到GPS业务的广播!");
}
五、测试效果
进入webSocket测试网站进行webSocket连接
WEB端连接:
127.0.0.1:8081/webSocket/10086/WEB
APP端连接:
127.0.0.1:8081/webSocket/10000/APP
使用postMan进行网页端上线
127.0.0.1:8081/gpsManage/addWebToGpsClient/10086
五、测试效果
进入webSocket测试网站进行webSocket连接
WEB端连接:
127.0.0.1:8081/webSocket/10086/WEB
APP端连接:
127.0.0.1:8081/webSocket/10000/APP
使用postMan进行网页端上线
127.0.0.1:8081/gpsManage/addWebToGpsClient/10086
预览效果: