最近想给自己的博客网站实现一个自定义的数据后台系统,实现对外提供api数据接口,和监控站点的访问数据,并且进行数据的实时可视化出来。这可能是偶然看到一个ip的精准定位的页面引起的我的一点兴趣,通过ip获取获取信号的经纬度,来达到一个实时定位的功能。要实现这些并不难,也刚好可以应用最近学的一些东西,使用websocket可以实现完全实时统计在线人数等信息,于是就开始尝试动手干了起来。
需求分析
1、提供博客系统相关数据的api:
- 最近发布的十篇文章:http://qkongtao.cn/?json=get_recent_posts
- 按日期统计文章发表数:
- 文章分类数据详情:
- 博客标签数据:
- 博客中页面数据:
使用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中
代码语言:javascript复制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,相当于是一个中介
代码语言:javascript复制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
代码语言:javascript复制@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了。
代码语言:javascript复制@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
代码语言:javascript复制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
代码语言:javascript复制// 获取
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/
2、文章数据可视化:
源码下载
下载链接:https://gitee.com/KT1205529635/ip-count