Springboot&websocket实现IP数据实时统计

66 篇文章 11 订阅

最近想给自己的博客网站实现一个自定义的数据后台系统,实现对外提供api数据接口,和监控站点的访问数据,并且进行数据的实时可视化出来。这可能是偶然看到一个ip的精准定位的页面引起的我的一点兴趣,通过ip获取获取信号的经纬度,来达到一个实时定位的功能。要实现这些并不难,也刚好可以应用最近学的一些东西,使用websocket可以实现完全实时统计在线人数等信息,于是就开始尝试动手干了起来。

需求分析

1、提供博客系统相关数据的api:

使用wordpress的一个插件:JSON API
在这里插入图片描述

2、博客数据可视化:

  • 页头总文章数、昨日访客、总访客数(自己写接口)
  • 最近发布的文章列表
  • 按日期统计文章发表数立方图
  • 文章分类饼图
  • 博客标签词云
  • 实时在线人数面板
  • TOP100访客IP信息和定位地图
  • 你是今天的第几个访问者

3、数据结构(msyql):

  • IP:IP地址
  • address:地区(address)
  • UA:访问来源(浏览器、系统等)
  • time:访问时间
  • axis 坐标:IP来源坐标
  • count 日点击数
  • status 是否在线

实现策略

后台数据策略

1、 使用websocket实时获取在线人数,并且对外提供服务
2.、新建redis表,用来存取每日最新全部访问数据(定时任务进行数据更新每天晚上3点将数据同步到MySQL,redis只用来存当天的访问数据)
3、需要获取访问者的IP等信息,然后新建一张表,对这些信息进行存储,对外提供最近访问的前100条数据
4、过滤重复IP的问题,暂时选择使用:redis使用hset结构记录数据,拿到Redis中的数据的count字段,如果为空就赋值为1,否则的话进行自增。websocket中使用 ConcurrentHashMap<String, Set<WebSocketServer>>数据结构存储(该数据每天晚上3点同步到数据库)
5、提供100条数据的策略:先从redis里查询数据,如果少于100条数据,则不够的从数据库里面取剩余需要的数据
6、判断用户是否在线:websoket主体类中,用户下线就remove对应ip的session,知道map中该ip的session全部移出后,就修改redis对应数据中status的状态值

根据IP获取位置信息的接口

可以采用百度地图或者高德地图提供的api,需要申请
1、https://api.map.baidu.com/location/ip?ak=HQi0eHpVOLlRuIFlsTZNGlYvqLO56un3&coor=bd09ll&ip=221.214.212.103
2、https://restapi.amap.com/v5/ip?key=0347f577***************2573193f16f&type=4&ip=183.17.232.207

遇到的问题

websocket无法直接获取建立连接者的ip

springboot的websocket是无法直接获取客户端ip的,网上也有人很多人用的是netty-websocket-xx 包,这包提供了api用于获取客户端的ip。
换包太麻烦了,即使是在不换包的前提下,使用ServerEndpoint加Fliter过滤器可以解决该问题。
1、定义一个拦截器
此拦截器用于获取ip,并放入session中

package cn.kt.ipcount.filter;

import cn.kt.ipcount.utils.IPUtil;
import nl.bitwalker.useragentutils.Browser;
import nl.bitwalker.useragentutils.OperatingSystem;
import nl.bitwalker.useragentutils.UserAgent;
import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * Created by tao.
 * Date: 2022/1/4 17:11
 * 描述:
 */

@javax.servlet.annotation.WebFilter(filterName = "sessionFilter", urlPatterns = "/*")
@Order(1)
public class WebFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        UserAgent userAgent = UserAgent.parseUserAgentString(req.getHeader("user-agent"));
        String browserName = userAgent.getBrowser().getName();
        String os = userAgent.getOperatingSystem().getName();
        req.getSession().setAttribute("ip", IPUtil.getIpAddress(req));
        req.getSession().setAttribute("ua", browserName + " " + os);
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

2、定义 WebSocketConfigurator
用于将客户端的ip传递给websocket中的session,相当于是一个中介

package cn.kt.ipcount.filter;

import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Enumeration;
import java.util.Map;

/**
 * Created by tao.
 * Date: 2022/1/4 17:12
 * 描述: 服务端点类
 */
public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {

    public static final String IP_ADDR = "IP.ADDR";
    public static final String IP_UA = "IP.UA";

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

        Map<String, Object> attributes = sec.getUserProperties();
        HttpSession session = (HttpSession) request.getHttpSession();
        if (session != null) {
            attributes.put(IP_ADDR, session.getAttribute("ip"));
            attributes.put(IP_UA, session.getAttribute("ua"));
            Enumeration<String> names = session.getAttributeNames();
            while (names.hasMoreElements()) {
                String name = names.nextElement();
                attributes.put(name, session.getAttribute(name));
            }
        }
    }
}
  1. 配置websocket
    主体类用于管理websocket连接,并配置configurator
@Component
@ServerEndpoint(value = "/websocket", configurator = WebSocketConfigurator.class)
@Slf4j
public class WebSocketServer {

    private Session session;

    private static ConcurrentHashMap<String, Set<WebSocketServer>> serverMap = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        Map<String, Object> userProperties = session.getUserProperties();
        // 获取IP和UA
        String ipAddr = (String) userProperties.get(WebSocketConfigurator.IP_ADDR);
        String ua = (String) userProperties.get(WebSocketConfigurator.IP_UA);
        Set<WebSocketServer> webSocketServers = serverMap.containsKey(ipAddr) ? serverMap.get(ipAddr) : new HashSet<>();
        webSocketServers.add(this);
        serverMap.put(ipAddr, webSocketServers);
        webSocketServers.forEach(System.out::println);
        log.info("【websocket消息】有新的连接, 总数:{}", serverMap.size());
        sendMessage(serverMap.size() + "");
    }

    ......
}

注意:加上断点注解:@ServerEndpoint(value = "/websocket", configurator = WebSocketConfigurator.class),然后通过session即可获取Filter中的数据。

websocket无法注入对象

java springboot websocket 不能注入( @Autowired ) service bean 报 null 错误
解决方法:
spring 或 springboot 的 websocket 里面使用 @Autowired 注入 service 或 bean 时,报空指针异常,service 为 null(并不是不能被注入)。
解决方法:将要注入的 service 改成 static,就不会为null了。

@Controller
@ServerEndpoint(value="/chatSocket")
public class ChatSocket {
    //  这里使用静态,让 service 属于类
    private static ChatService chatService;

    // 注入的时候,给类的 service 注入
    @Autowired
    public void setChatService(ChatService chatService) {
        ChatSocket.chatService = chatService;
    }
}

原因:本质原因:spring管理的都是单例(singleton)和 websocket (多对象)相冲突。

iP详细信息和ua的获取并解析

  1. 获取用户的真实ip
    IPUtil.java
package cn.kt.ipcount.utils;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * Created by tao.
 * Date: 2022/1/4 11:08
 * 描述:
 */
public class IPUtil {
    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址。
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
            if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
                //根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ip = inet.getHostAddress();
            }
        }
        return ip;
    }
}
  1. springboot获取请求的ua
// 获取
String userAgent = request.getHeader("user-agent");

/*
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
*/
  1. springboot解析请求的ua
  • 添加依赖
	    <!-- https://mvnrepository.com/artifact/eu.bitwalker/UserAgentUtils -->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>
  • 解析ua
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("user-agent"));
String clientType = userAgent.getOperatingSystem().getDeviceType().toString();
LOGGER.info("clientType = " + clientType);   //客户端类型  手机、电脑、平板
String os = userAgent.getOperatingSystem().getName();
LOGGER.info("os = " + os);    //操作系统类型
String ip = IpUtil.getIpAddress(request);
LOGGER.info("ip = " + ip);    //请求ip
String browser = userAgent.getBrowser().toString();
LOGGER.info("browser = " + browser);    //浏览器类型

websocket压测

正常来说websoket的最大长连接数可以达到16000个。
参考文章:https://blog.csdn.net/lnkToKing/article/details/79493498

实现效果

  1. 来访统计:http://ip.qkongtao.cn/
    在这里插入图片描述

在这里插入图片描述
2. 文章数据可视化:
在这里插入图片描述

源码下载

下载链接:https://gitee.com/KT1205529635/ip-count

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Spring Boot集成WebSocket可以实现后端向前端发送数据的功能。下面是实现该功能的步骤: 1. 在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 创建一个WebSocket配置类,用于配置WebSocket相关的Bean。可以创建一个类,并使用`@Configuration`和`@EnableWebSocket`注解进行标记,如下所示: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/websocket"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } } ``` 3. 创建一个WebSocket处理器类,用于处理WebSocket连接和消息的逻辑。可以创建一个类,并实现`WebSocketHandler`接口,如下所示: ```java public class MyHandler implements WebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 当建立WebSocket连接时调用,可以在这里进行一些初始化操作 } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { // 处理收到的消息 String payload = (String) message.getPayload(); // 向前端发送消息 session.sendMessage(new TextMessage("Hello, " + payload)); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // 当关闭WebSocket连接时调用,可以在这里进行一些清理操作 } @Override public boolean supportsPartialMessages() { return false; } } ``` 4. 在Controller中注入`SimpMessagingTemplate`类,用于发送消息给WebSocket客户端。然后,可以在需要发送消息的地方调用`simpMessagingTemplate.convertAndSend()`方法发送消息,如下所示: ```java @RestController public class MyController { private final SimpMessagingTemplate messagingTemplate; public MyController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } @GetMapping("/send") public String sendMessage() { messagingTemplate.convertAndSend("/websocket", "Hello from the server!"); return "Message sent"; } } ``` 5. 在前端页面中使用JavaScript代码连接WebSocket,并处理接收到的消息。可以使用`new WebSocket()`方法创建WebSocket对象,并使用`onmessage`事件监听接收到的消息,如下所示: ```javascript var websocket = new WebSocket("ws://localhost:8080/websocket"); websocket.onmessage = function(event) { var message = event.data; // 处理接收到的消息 console.log(message); }; ``` 以上就是使用Spring Boot集成WebSocket实现后端向前端发送数据的基本步骤。通过WebSocket连接,后端可以通过WebSocketSession对象向前端发送消息。前端页面可以通过WebSocket对象接收并处理后端发送的消息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不愿意做鱼的小鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值