SpringBoot

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

Nick · 1月10日 · 2022年 · · 本文7244字 · 阅读19分钟571

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

需求分析

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

使用wordpress的一个插件:JSON API
Springboot&websocket实现IP数据实时统计-左眼会陪右眼哭の博客

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

3、配置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;
    }
}

2、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
*/

3、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/
Springboot&websocket实现IP数据实时统计-左眼会陪右眼哭の博客

Springboot&websocket实现IP数据实时统计-左眼会陪右眼哭の博客

2、文章数据可视化:
Springboot&websocket实现IP数据实时统计-左眼会陪右眼哭の博客

源码下载

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

0 条回应
在线人数:1人 来访统计
隐藏