项目总结
项目介绍
用户模块
尝试开发用户模块,先申请一个公众号的测试号,开发登录模块
在这里你会涉及到netty的使用,token的解析模块,同时登录用户的ip解析,你也能顺便实现了。登录过程中涉及到的上下线推送,可以先把事件写好,具体逻辑空着,之后补充。
用户模块基本上是最麻烦的,你需不断的去完善项目的基建,比如为了解析用户,需要把几个拦截器先完善了,进行用户解析。
开发用户的基本信息接口com.abin.mallchat.custom.user.controller.UserController。
用户的批获取接口。用户模块就开始涉及到大量缓存,redis缓存相关的工具类,本地缓存caffeine,批量缓存框架。
涉及到线程池的管理。
黑名单模块
用户模块开发完,可以开发黑名单模块。涉及到一个拦截器
的校验,缓存的读取。拉黑用户还涉及到了权限角色的设计与判断。
群成员模块
用户的上下线推送就已经涉及到群成员的变更了。群成员的翻页,首先要了解到我们项目用的都是游标翻页
,群成员的具体缓存设计,完善群成员列表。
会话列表
会话列表比较简单,只有一个房间,前端对会话列表也是 写死的,这里暂时不用开发
消息模块
消息模块涉及到的东西就比较多了。 开发消息的发送功能,这里会涉及到消息的策略模式,优先开发文本消息的发送。文本消息需要url标题解析
,消息回复跳转
的前置保存。消息发送后的推送,封装了个通用的消息查询方法。
发送消息还会涉及到频率的控制
,消息回复的实现 ,@功能,发送图片,撤回功能等等,是项目的第二大复杂模块。
标记模块
有了消息,可以开始对消息进行点赞点踩。消息的标记涉及到分布式锁
,以及消息标记
还有一些设计模式的使用。
徽章发放
对于用户注册后发送徽章,以及消息标记后发送徽章,需要涉及到幂等发放。
WebSocket模块
IM通讯系统,在很多业务中都会有服务端需要主动推送web的场景。比如小红点提醒,新消息提醒,审批流提醒等
实现原理:客户端和服务器之间维持一个 TCP/IP 长连接,全双工通道
选用netty不用tomcat?
- netty是nio基于事件驱动的多路复用框架,使用单线程或少量线程处理大量的并发连接。相比之下,Tomcat 是基于多线程的架构,每个连接都会分配一个线程,适用于处理相对较少的并发连接。最近的 Tomcat 版本(如 Tomcat 8、9)引入了 NIO(New I/O)模型。所以这个点并不是重点。
- Netty 提供了丰富的功能和组件,可以灵活地构建自定义的网络应用。它具有强大的编解码器和处理器,可以轻松处理复杂的协议和数据格式。Netty 的扩展性也非常好,可以根据需要添加自定义的组件。比如我们可以用netty的pipeline方便的进行前置后置的处理,可以用netty的心跳处理器来检查连接的状态。这些都是netty的优势。
websocket连接过程
客户端依靠发起HTTP握手,告诉服务端进行WebSocket协议通讯,并告知WebSocket协议版本。服务端确认协议版本,升级为WebSocket协议。之后如果有数据需要推送,会主动推送给客户端。websocket初期是通过http请求,进行升级,建立双方的连接。
获取用户ip和连接token
我们获取用户ip的点有两处,注册和连接认证。两处都是需要从socket建立连接的时候,赶在协议升级前,保存用户的ip地址。获取请求头里的ip,保存到channel的附件中,以后需要的时候都能提取。
心跳包
如果用户突然关闭网页,是不会有断开通知给服务端的。那么服务端永远感知不到用户下线。因此需要客户端维持一个心跳,当指定时间没有心跳,服务端主动断开,进行用户下线操作。
直接接入netty的现有组件new IdleStateHandler(30, 0, 0)可以实现30秒链接没有读请求,就主动关闭链接。我们的web前端需要保持每10s发送一个心跳包。
我们用websocket的目的,主要是用于后端推送前端,前端能用http的就尽量用http。这样的好处是,http丰富的拦截器,注解,请求头等功能,可以更好地实现或者是收口我们想要的功能。尽量对websocket的依赖降到最低。
前后端的交互用的是json串,里面通过type标识次此次的事件类型。
扫码登录方案(集成微信登陆SDK)
我最讨厌这种明明支持扫码或者短信登录给我选的,结果我为了方便不接验证码,选择扫码登录,登录成功后又要求我绑定手机,非常难受。
和微信的交互主要有三个点,申请带参二维码,扫码事件通知,授权事件通知
- 建立websocket连接
进入到首页的时候,前端就开始和后端建立了websocket,这时候就已经可以接受新消息推送啦。不过目前这个连接是一个未登录的用户。需要进行登录认证。后端通过在netty中添加处理器,记录并管理连接。将连接放入一个map中管理,目前该连接是未登录态。 - 前端请求扫码登录
前端点击登录,会发出一个请求登录二维码的websocket信息。后端做了三件事
- 1.生成一个不重复的登录码。并且将登录码和这个Channel关联起来。
- 2.然后请求微信的接口,将这个登录码转成一个带参数的二维码链接
- 3.返回给前端,前端接收二维码链接转成二维码图片展示。
- 用户扫码
当用户扫码后关注公众号。公众号会给我们后台回调一个关注事件 OR 扫码事件。
取决于用户是已关注还是未关注。不管啥事件,都会携带二维码参数 即登录码 - 用户注册
用户扫码的回调,我们能够获得用户的openid。和登录码。再额外给用户发一个推送,请求用户授权。
用户注册后,会先临时保存一个openid和前端登录码的关系映射。 - 授权用户信息
我们推送的授权信息是一个微信授权地址,用户点击后,微信就会回调我们的系统,并且获取一个重定向的地址给用户展示。用户看到这样一条信息,点击登录,微信就会给我们发送授权成功的回调。通过openid关联出之前的事件码,然后复用已注册的登录逻辑。这时候会生成用户的token。然后将连接标识为登录连接.
用户认证技术Token方案
JWT实现Token
简单来说JWT就是通过可逆加密算法,生成一串包含用户、过期时间等关键信息的Token,每次请求服务器拿到这个Token解密出来就能得到用户信息,从而判断用户状态。
优点
JWT的最大特点是服务器不保存会话状态,无论哪个服务器解析出来的Token信息都一样,而且不需要做任何查询操作,省掉了数据库/Redis的开销
缺点正式因为JWT的特点,使用期间不可能取消令牌或更改令牌的权限,一旦JWT签发,在有效期内将会一直有效。
- 无法主动更新Token的有效性,只要用户传回来的Token没有过期,服务器就会认为这个用户操作是有效的。比如一下这个场景:某用户被封禁,此时该用户所有操作都应该被禁止,但是由于之前发给用户的JWT Token还没有过期,服务器仍然认为该用户操作合法。有一个解决方案是维护一张JWT黑名单表,只有没在表上的用户的JWT是有效的。但是随之而来又有一个问题便是这个JWT黑名单表存在哪里。存在服务器,那么又要搞多服务器同步。存在关系数据库,那么查数据库效率又低。存在Redis,则又回到了Token丢失问题。
3.其实解析JWT Token也是消耗服务器CPU的
总结
由于jwt是无状态的,它一发布开始,就意味着固定了过期时间。我们没法对他做失效,没法实现续期,它的好处也是显而易见的。不需要任何一个中心化的地方去保存它,管理它,查询它,比对它。
JWT碰巧有去中心化的特性,但为了能够控制它的上下线,主动下线 ,登录续期等功能。我们依然可以对它进行中心化的管理。
用户背包表&发放物品的幂等设计
一个用户可能拥有多个物品,比如改名卡。你如确保给用户发很多次,最终奖励不对多,用到幂等的设计了。
购买渠道:同一个订单号最多只能发一次。
注册渠道:同一个uid只能发一次。
消息点赞渠道:同一条消息最多发一次。
有了这些幂等标识,用来做数据库的唯一键,就可以保证不会多发了。
这个幂等键的设置就比较明确了,针对某个物品,在某个渠道下,的具体发放标识,不能重复。
幂等号=itemId+source+businessId
这里用到分布式锁,根据用户来做资源的隔离,确保你在一个串行的环境下,判断幂等是否重复。
幂等判断的三件套:
- 组装幂等号
- 加锁,分布式锁注解
- 重复校验
其实幂等校验和redis盘路缓存有点像。存在就返回,不存在就走另一个逻辑
在用户背包表记录这条记录,就算物品发放成功,然后发送一个用户收到物品的事件出去。
未来这个事件会转成MQ.
黑名单
权限设计
拉黑功能是管理员专属的功能。为了前后端能够识别管理员,我们还需要专门设计一套权限管理系统。
传统的权限设计应该是这样的:
- 一个用户可以有多个角色
- 一个角色对应多种权限
- 权限包含多种资源,比如接口,按钮,菜单
最后整合起来,就是用户有多少资源,来判断用户能否访问。角色只是个桥梁。
拦截时间点
通过拦截器,对每一个接口的请求都进行黑名单的校验。
数据源
黑名单是一个相对静态的数据,并且需要比较实时的判断,可以将它缓存在redis里。对于单机来说,也能缓存在本地内存里。
用户拉黑
将用户的uid,和ip加入黑名单,入库。
发出拉黑事件
发送拉黑事件,关心该事件的消费者做了三件事
- 清除相关的缓存,让拉黑立马生效
- 对该用户所有的消息都进行删除,以后不会出现在消息列表
- 给所有在线用户推送用户拉黑事件
websocket推送拉黑事件
拉黑一般是某些用户进行了不恰当的发言。这时候需要立马通知到所有在线用户。前端接收到拉黑通知,会对消息列表和成员列表对拉黑的uid进行匹配,把该uid的所有消息从前端缓存中删除。
拉黑用户拦截
被拉黑的用户以后再也没法访问该网站了。
在拦截器中,从内存取出所有的黑名单列表Set,对请求者的ip和uid进行匹配。如果存在,就拒绝访问。
消息列表的所有交互刨析
发送消息
发消息是需要用户态的,我们是通过拦截器解析出的uid。RequestHolder.get().getUid()获得
历史消息列表
用户一进入页面,首先会拉取一页最新的消息。访问的就是我们的历史消息列表。
并且如果用户的鼠标滚轮不断的往上滑,还需要加载更多的历史列表,这个就涉及到普通翻页和游标翻页的问题了,
- 游标查询:通过游标分页的工具类就很方便的实现这个功能,我们的消息目前都在一个表,主键自增,暂时可以通过唯一的id来做游标查询,再限制会话id,以及限制消息需要是一条正常消息。不需要查询禁用的消息,或撤回的消息
- 消息组装:消息组装将消息列表查到的原始数据,再聚合上用户信息,消息标记(点赞点踩),消息url识别等前端需要展示的信息就行了。
消息撤回
消息撤回也依然还是一条消息,只是消息的类型不同罢了。不同的消息类型,会有不同的展示方法。后期的图片,语音,也都会有不同的展示。
首先我们要搭建好消息类型的框架,不同的消息类型,根据type会有不同的输入参,有了这样的框架够,撤回无非就是改一下消息的类型罢了。
整合minio,实现文件上传
为什么用minio
传统的文件服务的用法,可以本地搭建fastdfs,也可以使用云厂商的oss,cos,七牛云等。按量收费,或者按月收费,非常灵活。
最大的问题,就是这几年,各式各样的云服务被盗刷现象层出不穷。经常一会儿流量就刷光,甚至停止的不及时,直接欠费几百上千。这些云服务商的限制手段,都做的没那么好。虽然有防盗链,临时授权等,依然解决不了盗刷问题。因此我们直接选用本地搭建minio,目前非常好用的一款分布式存储。
使用姿势
- 引入依赖,直接创建一个模块,复制所有类
表情包功能
存储占用
表情包最大的特点,就是他的传播性。所以对于这种在市面上广泛流传的表情包,如果斗图的时候每发一张表情包,都是在minio里面存入了一张图片,对存储的压力是非常大的,也没多大必要。对于这种大家发一样的图片。我们需要做到所有图片都指向同一地址。无论多少人,发多少次表情包,对于minio里面只存了一份。
带宽占用
一个10m的表情包,如果在市面上流通,对于我们的应用来说,会占用非常大的带宽流量。所以我们会强制要求市面上的表情包在上传的时候,就会自动通过算法对表情包进行压缩,将表情包的大小压缩到接近100k。这也是为什么电子包浆产生的原因。不同平台的表情包在保存的时候,都会有算法进行一定的压缩,压缩久了就有点失真了。
保存表情包的时间点
对于微信来说保存表情包的时间点有两个,一个是自己主动上传,还有一个是添加他们的表情包。我们比他多了一个,对于他人发的图片,我们也可以选择添加到表情包。在这个过程中,前端会把图片拉取到本地,自动进行压缩算法,压缩到合适的大小,再调用表情包的保存功能。实际上这三个表情包的保存,最终调用的都是同一个保存接口。
过期时间
由于表情包是高度流通,且不太耗费存储空间的图片。因此我们将它和普通的聊天图片区别开。这样等聊天图片满了可以删除几个月前的,聊天图片最好是不要删除。
【doing】GPT机器人
很多人都在为解决无法正常访问 OpenAI 的 API 而苦恼,最常见解决方案是使用一台海外服务器来进行反向代理,但这样又徒增了一些成本。实际上我们只想要请求一个能通的域名,并不需要额外的精力去管理一台服务器。
我们可以通过白嫖Cloudflare的 Workers 来代理 OpenAI 的 API 地址,配合自己的域名即可在境内实现访问。
Cloudflare 的Workers 有每天免费 10 万次的请求额度,这个方案理论上支持所有你想要访问的外网api,不仅限于openai。
会话列表
给好友发送消息
给好友发送消息的时候,需要根据好友uid,定位到两人的聊天房间,才能发送消息。这时候也需要开个新接口,根据uid获取会话素材。
拦截器
黑名单拦截器BlackInterceptor
对拉黑的用户进行拦截,
请求上下文RequestHolder
对于登录的用户,我们会将uid设置为请求属性,在CollectorInterceptor中统一收集。
AOP请求日志WebLogAspect
打印接口的请求,以及耗时,便于排查问题,
鉴权拦截器TokenInterceptor
针对所有接口,都会走这个拦截器,只要请求头附带token,就会进行鉴权。
整合redis,以及相关工具类
整合redis
- 引入依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> - 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22spring:
redis:
# Redis服务器地址
host: 19.1.5.11
# Redis服务器端口号
port: 6379
# 使用的数据库索引,默认是0
database: 0
# 连接超时时间
timeout: 1800000
# 设置密码
password: "123456"
lettuce:
pool:
# 最大阻塞等待时间,负数表示没有限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 5
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中最大连接数,负数表示没有限制
max-active: 20引入redisson
分布式锁是我们经常需要用的,因此需要引入redisson。
整合mybatisPlus
- 引入依赖
1
2
3
4
5<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency> - 引入配置
1
2
3
4
5
6
7
8mybatis-plus:
# xml地址
mapper-locations: classpath:mapper/**/*.xml
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#开启驼峰命名法
map-underscore-to-camel-case: true - 引入分页插件
要想用mybatisplus的分页插件,必须要引入该分页拦截器。1
2
3
4
5
6
7
8
9
10
11
12
public class MybatisPlusConfig {
/**
* 新增分页拦截器,并设置数据库类型为mysql
*/
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
本地消息表
这时候就需要引入分布式事务。常见的分布式事务tcc,2pc,3pc,本地消息表。咱们介绍一下最常用的本地消息表。
我们的目的,就是确保本地的操作和第三方操作保证一致。要么都成功,要么都失败。由于网络的不可靠性,会出现本地成功了,但是第三方可能网络波动,没有执行成功。事务消息保证的就是第三方一定要执行成功,达到最终一致性。
把对第三方的操作,都变成一条本地的记录和我们的本地事务一起入库。这样就保证了持久化,至少这个操作不会丢失了。
然后通过定时任务来保证这条操作一定会被执行成功,如果一次遇到网络波动,返回超时,没有关系。定时任务会一直查出来,最终返回一个确定的结果(成功或失败)。这时候就可以删除这条操作明细了。保证了最终一致性。
请求折叠工具类
在大流量高并发场景下,我们项目通常会达到性能的瓶颈。这些瓶颈总结起来,大概有两种,读瓶颈,和写瓶颈。
这些瓶颈有个通用的解决方案,如果是不重要请求,可以无脑三板斧 熔断,限流,降级。
kafka实现
首先kafka有个RecordAccumulator,来实现消息的批量发送。1
2
3
4
5<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.5.10.RELEASE</version>
</dependency>
通过
batch.size=16kb
linger.ms=10
就可以配置根据数量或者大小的批量发送
优点:
1.支持分组通过map的方式存放
2.支持返回结果,返回future,成功后设置进future。
缺点:
1.满了后并不会直接发送,还是等下一次扫描
思考:可能是kafka对于消息的处理速度,要求没有那么严格。它更在乎整体的吞吐量。
2.没有为分组消息分配线程池
因为kafka都是单线程进行消息的发送,没有这方面的需求。
握手认证功能
http请求携带token
每次请求都需要用户携带token,在拦截器校验用户登录态。
对channel连接进行认证
用户在首次扫码登录的时候,后端会将channel(连接)关联上uid。这样后期对用户定向推送,才知道要把消息推到那个channel上。
但是channel是个很不稳定的东西。前端只要稍微刷新页面,channel就断开了。就需要重连。重连的channel要怎么知道它是谁呢?总不可能让用户再重新扫码登录吧?
好在前端登录后就保存了token,他只需要拿着token从channel发送过来认证一下,我们就又能建立起channel和uid的映射关系了。
【游标翻页】应对复杂变换的列表
深翻页问题
普通翻页前端一般会有个分页条。能够指定一页的条数,以及任意选择查看第几页。
假设前端想要查看第11页的内容,传的值pageNo=11,pageSize=10
对于数据库的查询语句就是。1
select * from table limit 100,10
其中100代表需要跳过的条数,10代表跳过指定条数后,往后需要再取的条数。
需要在数据库的位置先读出100条,然后丢弃。丢弃完100条后,再继续取10条选用。
如果我们翻页到了很深的地方,比如读到了第1000页,对应的sql语句就是1
select * from table limit 10000,10
需要先查询10000条进行丢弃,再取那么个10条选用。这个效率也太低了,目前的问题在于每次翻页都需要花时间扫描一些不需要的记录,然后丢弃。那么是不是可以优化这个步骤呢?以后不论第几页,我们都不需要跳过一些值。直接取limit 0,10。这样语句变成了1
select * from table limit 0,10
没关系,再加一个条件1
select * from table where id>100 order by id limit 0,10
只要id这个字段有索引,就能直接定位到101这个字段,然后去10条记录。以后无论翻页到多大,通过索引直接定位到读取的位置,效率基本是一样的。这个id>100就是我们的游标,这就是游标翻页。游标翻页可以完美的解决深翻页问题,依赖的就是我们的游标,即cursor。针对mysql的游标翻页,我们需要通过cursor快速定位到指定记录,意味着游标必须添加索引。
总结
游标翻页的优点:
1.解决深翻页问题
2.解决频繁变动的列表翻页问题。
缺点:
1.无法跳页,只能不断往下翻
游标翻页更适合c端场景,用户只能不断下滑翻页。
普通翻页更适合B端场景。用户能看见总页数,能随意跳页
游标翻页技术细节
我们的项目大量使用到了游标翻页,比如会话列表,消息列表,成员列表。其中数据库涉及了redis和mysql。并且封装了分页工具类。
一个注解,简洁实现分布式锁
1、原始写法
我们平常使用redisson的分布式锁是基本都用的模板,既然是模板,那为何不把他抽出来呢?1
2
3
4
5
6
7
8
9// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...业务代码
} finally {
lock.unlock();
}
}
2、抽出分布式锁工具类
我们可以抽出一个LockService方法,把锁的模板写在方法里,调用的时候只需要指定key,把锁内的代码块用supplier函数传进来1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LockService {
private RedissonClient redissonClient;
public <T> T executeWithLock(String key, int waitTime, TimeUnit unit, SupplierThrow<T> supplier) throws Throwable {
RLock lock = redissonClient.getLock(key);
boolean lockSuccess = lock.tryLock(waitTime, unit);
if (!lockSuccess) {
throw new BusinessException(CommonErrorEnum.LOCK_LIMIT);
}
try {
return supplier.get();//执行锁内的代码逻辑
} finally {
lock.unlock();
}
}
}
使用起来就方便了1
2
3
4
5lockService.executeWithLock(key, 10, TimeUnit.SECONDS, ()->{
//执行业务逻辑
。。。。。
return null;
});
如果我们不需要排队等锁,甚至还能重载方法减少两个参数。1
2
3
4
5lockService.executeWithLock(key, ()->{
//执行业务逻辑
。。。。。
return null;
});
一个注解,简洁实现频率控制
我们的项目是一个即时的IM通信项目,并且有着万人大群。但凡有几个人刷屏,那消息爆炸的场景,都不敢想象。
因此我们需要对项目特定的接口进行频率控制,不仅是业务上的功能,同样也保护了项目的监控运行。而频控又是个很通用东西,好多地方都要用到,因此决定把它实现为一个小组件,也就是注解的形式使用。
通过频控注解,很轻松的就实现接口的请求频率控制,防止有人瞎点。
有些接口还需要配置多种频控策略,这种我们可以再加个注解,将多个策略包起来。
我们的频控大多是用在接口上的,并且接口拦截器会解析出用户的ip和uid。而很多的场景是直接对uid或者ip做频率控制的。针对这种情况,我们指定了uid后,切面会自动从上下文中获取uid,让注解的实现更加简洁。
AOP实现接口请求日志,耗时记录
很多时候后端排查问题,我们要确认用户到底请求了哪些接口,接口的入参和返回是怎么样的。又或者是接口耗时有多高。这些东西都可以通过日志去排查,我们只需要将每次的请求记录日志。要如何优雅的实现接口日志呢?
有的人喜欢用拦截器,有的人喜欢用aop。后面我们会讨论下这两种方案的优劣。
AOP切面
通过springaop对controller包下的所有接口进行拦截
@Around(“execution( com..controller...*(..))”)
过滤器,拦截器,aop
SpringAOP拦截器:只能拦截Spring管理Bean的访问(业务层Service)。
Interceptor拦截器:拦截以.action结尾的url,拦截Action的访问。
Filter过滤器:拦截web访问url地址。
Filter与Interceptor联系与区别
- 拦截器是基于java的反射机制,使用代理模式,而拦截器是基于函数回调。
- 拦截器不依赖servlet容器,过滤器依赖于serlet容器。
- 拦截器只能对action起作用,而过滤器可以对几乎所有请求起作用(可以保护资源)。
- 拦截器可以访问action上下文,堆栈里面的对象,而过滤器不可以。
- 执行顺序:过滤器前-拦截前-action处理-拦截后-过滤后。
拦截器更关注controller ,url的请求。aop更关注方法的访问,出入参。
拦截器想拿到方法,可以用反射。aop想拿到url,可以用RequestContextHolder。所以两个能实现一样的功能可以说是都有取巧的办法。
- 我们更关注的一点是,aop是更内层的,统计的业务耗时更精确。同时它更关注请求的出入参。而拦截器需要反射获取出入参,会更耗时一些。
- 而且在拦截器层面的时候,参数还没有转化为具体controller方法的入参,因此前端多传的很多请求头,或者参数,都会被打印出来,不够纯粹。尽管有办法打印出纯粹的,但是需要我们处理写起来更加麻烦。
因此,我选择用aop来实现请求日志。
ip获取+归属地怎么实现
websocket请求获取ip
对于websocket请求获取ip就会麻烦一些。
首先我们要有个概念,websocket初期会借助http来升级协议。所以我们需要在http升级之前就要获取ip,并且将用户ip保存起来。在协议升级前,我们加入了HttpHeadersHandler处理器,这时候还是能拿到http的request的。之后协议升级后,请求就不会再走这个处理器了,所以我们的ip需要保存起来。正好channel其实有个附件功能,我们可以直接把ip作为附件保存进channel。之后每次的websocket请求,都用的是同一个channel,从里面取ip就好了。
正好所有连接的用户,我们也会去保存uid和channel的映射关系,保存这个channel。
ip的更新
ip的更新时机其实也是一个话题。总不可能用户每次请求,我们都要去做一次ip更新吧,那也太麻烦了。我们可以在用户首次认证去更新ip即可。
用户首次认证有两个场景:
1.用户浏览器里有token。前端拿它来后端认证下即可。
2.用户token失效重新扫码登录。
针对于第二种登录,扫码的时候是wx给我们的回调。我们通过回调的code,去找出code对应的连接channel。再从channel里找到用户信息以及ip
我们可以选用用户认证的时间点来触发ip的刷新。
ip归属地解析
ip的归属地解析也是个很有意思的话题,本质就是利用ip解析出所属地区。
基于ip2region文件索引
github上有个开源的ip地址库ip2region,需要在项目启动的时候预加载文件数据,里面的地址不会实时更新,如果需要更新还需要根据指引写爬虫更新
基于淘宝开放接口
淘宝有提供了ip地址库的查询接口,大家可以自己postman测试下。
淘宝的IP地址库API可以提供IP地址的详细信息,包括国家、省份、城市、经纬度等。使用淘宝的IP地址库API,可以轻松获取IP地址的详细信息,从而获取IP地址的地理位置。淘宝自己的地址库会一直更新,比较全。而且没有任何依赖,直接接口解析。