XMxxxxxx

1.登录

1.1用户信息表

在这里插入图片描述

1.2微博社交登录流程

OAuth2.0基本流程
在这里插入图片描述

整合前后端页面进行社交登录(微博)
主要流程:

  • 点击微博登录跳转到微博指定的登录页面
  • (获得)输入账号密码提交 如果输入正确也就是登录微博成功 会向我们指定的跳转页面进行跳转 并带上code
  • 我们的服务拿到code码后,然后根据code码向微博发送请求获取Token和UID等,获取到了说明登录成功。获取不到说明code错误,直接返回错误。
  • 然后调用远程服务进行注册或者登录
  • 远程登录会进行判断 根据UID 查出数据库中如果没有这个数据就进行注册 插入code码和Access Token和UID然后根据UID+TOKEN可以查询到用户的微博公开信息(昵称等)进行
  • 如果原来使用微博登录过 就把code码和Access Token更新。

code码只能用一次,Token一段时间内都可以用

1.3手机验证码登录注册流程

测试接口使用

在这里插入图片描述

1.收集验证码流程
在这里插入图片描述

2.注册流程

前端页面数据包括(用户名(唯一) + 密码 + 手机号(唯一))

页面填写数据完毕提交给认证服务,进行JSR303数据格式校验(注解加在字段上),数据校验通过后,然后根据手机号获取redis中的数据,没有对应的value,直接返回错误,有value取出value进行对比,错误直接返回,正确后先删除redis的kvs,在会员服务进行数据的插入

插入时先判断用户名和手机号是否唯一(使用的是自定义异常机制处理,向controller抛异常)

密码加密:MD5 + 盐 缺点:需要在数据库中建一张盐值表,保存每个用户注册时密码加密的盐值

最终使用springBCrptPasswordEncoder,同一个明文加密得到的密文不一样,而且数据库也不需要存储盐值,原理就是密文中包含了盐值,只有spring知道

登录流程

可以使用用户名 或者 手机号 + 密码进行登录

因为密码是加密存储,所以先根据用户名或者手机号查询密码,然后使用BCrPE将两个密码进行匹配,登录成功说明密码正确,不匹配说明密码错误

1.4SpringSession

Session共享的方案:

  • 1.Nginx的ip_Hash
  • 2.tomcat配置各台服务器的seesion共享
  • 3.SpringSession

登陆成功后 (auth.gulimall.com),将用户信息存储到Session中,key是"loginUser"(随便一个字符串),value是用户的个人信息对象

需要跳转到guilmall.com(商城首页),但是默认的cookie只在本域名下有效,为了让它在父域名下也生效,使用SpringSession在命令浏览器保存cookie时放大作用域即可(放大到一级域名),并将session存储到redis中,保证各个服务之间共享session.

判断用户是否登录

  • 使用SpringMVC的拦截器,实现Interceptor,prehandle(目标方法执行前干的事),postHandel(目标方法执行后干的事),
  • 在preHandel中直接判断session(redis)中是否有用户,有用户说明已经登陆,取出用户的信息放到ThreadLocal中(threadlocal.set),(ThreadLocal配置成静态的),然后其他的controller直接调用threadlocal.get()直接取出用户的数据即可。
  • 判断session中没有用户,直接返回登录页去登录

2.优化商城首页

2.1整体概述

2.2获取三级分类SQL语句加索引

表结构是,cid(主键) parent_id(它的上一级分类id)

第一步,先查出所有的一级分类,根据一级分类的parent_id为0来查询

select cid from xx where parent_id = 0

主键cid是有索引的,但是parent_id是没有索引的,所以这里为parent_id加上索引

2.3Nginx动静分离

项目的整个访问路径如下

使用windows的host文件模拟域名,访问指定域名时解析对应的ip地址,是Linux虚拟机的IP地址,默认端口是80,nginx监听80端口,然后将请求转发给网关服务,网关收到请求,进行断言将请求转发给指定的服务(商品服务订单服务等)

在这里插入图片描述
在这里插入图片描述

动静分离后

静态资源比如JS CSS等访问路径以static开头,然后将静态资源放到nginx服务器上,请求转到nginx,nginx匹配static直接将静态资源返回,动态请求继续转给网关。
在这里插入图片描述

2.4使用Redis+SpringCache做缓存

2.4.1简单介绍

  • 缓存的一般工作模式:查询时先查缓存,缓存有直接返回,没有去查数据库,查完数据向缓存中放一份。更新数据时将缓存删除,或者直接更新缓存。 注意:缓存一定要设置过期时间,保证数据的一致性。

  • 本地缓存(HashMap,在单机应该上没有问题,分布式下可能会出现多次查缓存(负载均衡到不同的服务器),最大的问题就是缓存一致性问题,查询时数据库数据变了,可能导致每个服务器的缓存时不不一致的)

  • 什么样的数据应该加缓存

    ​ - 访问量大且更新频率不高的(读多写少)

    ​ - 即时性、数据一致性不高

SpringBoot整合redis

  • 导入starter-data-redis

  • 提供了redisTemplate和StringRedisTemplate

缓存改造

Redis中存的: key是方法名,value是使用JSON序列化后的数据

好处:JSON数据可以跨平台其他的业务系统获取JSON后直接反序列化即可。

2.4.2解决缓存问题

缓存穿透

查询一个不存在的key

解决方法1.缓存null值+过期时间 2.布隆过滤器

缓存雪崩

大面积key失效

解决:设置key的过期时间为随机值

缓存击穿

热点key在大量的并发下失效,都去查数据库,导致数据库崩

解决查询数据库加分布式锁, 第一个请求获取锁后查询数据库后放到缓存中,后面的请求直接查缓存。

缓存一致性解决

2.4.3分布式锁

本地锁细节:确认缓存有没有+查数据库+将结果放入缓存 一整个逻辑都在同步代码块里。

本地锁在分布式系统下的问题:锁不住所有,只能锁本地

手动分布式锁

第一阶段

setNx + EX设置过期时间(防止执行业务时宕机没有删除锁导致死锁),注意调用setnx时就设置过期时间(存在这个命令,不需要使用LUA脚本),保证是一个原子性操作

第二阶段

设置了超时时间,但是防止锁过期了,将别的线程的锁删除了,所以设置的锁key的value不使用同一个值,而是使用一个随机值(时间戳+UUID),在删锁时判断一下UUID保证这个锁是自己的锁,那么就不会删除别的线程的锁 (这里也必须保证获取value和删除锁是一个原子操作 使用LUA脚本对比锁+删除锁)

保证加锁原子性(SetNX EX + 大的随机字符串) + 解锁原子性(LUA脚本 对比 + 删除)

但是还有问题:

如果说设置的过期时间到了(即锁删除了),但是业务还未执行完毕,那么其他线程还是可以占锁,所以这里给锁的过期时间设置的长一点

Redission分布式锁

本质是一个redis的客户端,操作redis的,类似jedis lettuced,底层使用lua脚本保证原子性

Redisson的这些锁,直接实现了JUC下的lock接口,本质上就是lock锁的分布式实现

好处

  • 1.不需要手动自旋方式获取锁,senNX 只会尝试一次,如果说想一直尝试的话需要自行的写while进行自旋,但是Redission还是JUC下的AQS实现,直接调用lock()获取不到锁就会自行的自旋尝试。
  • 2.看门狗原理,业务超长时自动给锁续期。锁默认是30秒,业务完不成自动续期,完成之后就不会续期,即使不手动解锁,30秒后自动删除,

Redisson解决:

1.内部的操作使用的都是lua脚本,保证原子性

2.由于实现了lock接口,没获取到锁时自动的自旋

2.每把锁的value(UUID+线程号)都是不一样的,保证不会删除别的线程的锁

3.看门狗原理,自动续期,简单源码:while(true)循环一直获取锁,获取到业务还未完成,在固定秒数时进行续期,锁的默认时间是30s

缓存一致性

1.双写模式(写数据库时同时更新缓存)

可以容忍数据的暂时不一致,等待缓存失效时读请求时就会更新缓存,适用于读多写少的场景,保证最终一致性
在这里插入图片描述

失效模式

最终一致性,有可能写的时候读来了,读请求快直接读到的时原来的数据,最终将脏数据更新到了缓存中。
在这里插入图片描述

延时双删

最简单的解决办法延时双删

使用伪代码如下:

public void write(String key,Object data){
		Redis.delKey(key);
	    db.updateData(data);
	    Thread.sleep(1000);
	    Redis.delKey(key);
	}

转化为中文描述就是 (1)先淘汰缓存 (2)再写数据库(这两步和原来一样) (3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可

如果使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

我们系统的解决方案

我们的菜单数据是写很少,读很多的数据,并且可以容忍暂时的不一致性,所以最终使用

失效模式设置过期时间+读写锁(解决读写并发时将脏数据更新到缓存中)

如果是读多写多不要用读写锁。

总结

缓存不一致的根本原因:由于读写并发,导致数据库的最后一次更新没有映射到redis中。

在这里插入图片描述

canal

可以模拟是一个mysql的从服务器,读取binlog,然后更新redis

1.解决缓存不一致

2.大数据下解决数据异构,
在这里插入图片描述

SpringCache

读模式下 @Cacheable,将结果放入缓存

写模式@CacheEvit(失效模式) @CacheEvit(双写模式)

2.5其他优化

比如开启thylemeaf缓存等。

3.提交订单原子性验证

  • 来到提交订单页面时生成一个TOKEN,redis里存一份(key是固定前缀,value是TOKEN)页面放一份,然后点击提交订单时使用lua脚本将 获取redis的token + 对比页面提交的token + 删除token变成一个原子性操作,注意:一定要先对比删除后 在执行业务
  • 注意lua脚本最终返回值是0或者1,业务层面判断是否对比成功就是根据lua脚本的返回值来判断的,返回1表示成功然后继续执行业务,返回0表示失败直接返回失败即可。只有一个线程可以成功,删除key后其他线程全部失败,直接全部返回失败。
  • lua脚本的使用流程,写一段字符串的脚本scripts内容,然后使用redisTemplate.execute(scripts,参数等)执行lua脚本获取返回值进行判断即可

4.下订单业务

本地事务提交订单流程:
在这里插入图片描述

整个方法是一个事务 @Transacational注解

  • 1.lua脚本对比提交的令牌,获取所有订单的数据,保存订单到数据库 (方法A)
  • 2.方法A成功后,调用远程库存服务锁定库存 (方法B)远程会员服务扣积分(方法C)
  • 3.使用异常机制进行事务,根据判断远程服务返回的状态码,失败直接抛出异常,进行回滚

本地事务在分布式下的问题

  1. 假设远程服务B出现了假失败,即扣减库存成功,但是由于远程调用可能网络出现了波动,导致远程调用超时抛出异常,然后保存订单回滚了,但是远程扣库存业务回滚不了(已经提交了事务)
  2. 远程事务执行成功也没有出现超时,但是远程调用下面有逻辑出现了异常,那么方法A可以回滚,但是已经执行成功的远程事务无法回滚(已经提交了事务)

控制事务的核心就是这些操作必须在一个连接Connection中,由于是远程调用,都不是一个数据库,也谈不上一个Connection里,并且最大的问题就是,远程的事务可能已经提交了,提交成功的事务是无法回滚的。

事务的传播行为

Spring事务详解

在这里插入图片描述

注意:只有是不同service事务,事务的传播属性才生效,因为事务本质是AOP,如果事务在一个service中,相当于是直接代码赋值粘贴,事务的传播没用

AOP的使用

在这里插入图片描述
在这里插入图片描述

4.2分布式事务详解

CAP + RAFT

分布式系统经常出现的异常

机器宕机,网络异常、消息乱序、数据错误,不可靠的TCP。

CAP定理

  • 一致性C:必须保证每个节点的值时刻都是相同的,如果集群中某个节点炸了,那么整个集群就不向外提供服务,
  • 可用性A:在集群中某些节点炸了,那么整个集群对外还是提供服务的,
  • 分区容错P:区间的通信可能失败

在这里插入图片描述

在这里插入图片描述

分布式系统实现一致性的raft算法

https://www.bilibili.com/video/BV1np4y1C7Yf?p=287

每一个节点都有三种状态:随从follower候选者candidate领导leader

领导选举过程

  • 初始时所有节点启动都以follower随从状态启动,每个节点都有一段随机的自旋时间,
  • 哪个节点的自旋时间最快,那么这个节点(假设A)变为candidate候选者状态
  • 然后A节点给其他节点发送投票消息,如果收到大多数节点的票,那么A就变成leader领导

选举成功后,leader会隔一段时间(很短)向随从节点发送心跳检测,在每个节点收到心跳检测前还会进行自旋,假设在自旋时间内收到了leader的心跳检测,那么自旋时间重置,如果自旋时间过了还未收到心跳检测(判断此时leader挂了),那么节点就会变为candidate候选者状态然后进行新一轮的投票选举复过程

假设选举过程中某些节点的自旋时间相同,选举时票数相同,那么重新进行新一轮的选举

数据修改过程

经过选举领导后,所有的修改都必须先交给领导leader节点

假设客户端让集群保存一个数据set 5

  • leader写一个日志 set 5(未提交状态)
  • leader发送set 5让其他节点保存
  • 其他节点保存后,向leader发送消息告诉leader set 5的日志已经保存(其他节点也是未提交状态)
  • leader收到后,必须要经过大多数随从节点的同意,leader才能进行set 5的提交然后发送消息告诉其他节点进行提交

注意:要保证一致性,必须要选出一个领导出来,选不出领导那么对外的请求直接返回错误,即服务不可用

面临的问题

即在分区容错P的情况下,不保证强一致性C,必须要保证服务对外是可用的A,所以要保证AP

即舍弃CP,保证AP
在这里插入图片描述

BASE理论

即保证AP的情况下,保证不了强一致性(单机数据库,要么成功要么失败,分布式下保证强一致很难,),但是业务肯定要一致性, 我们可以实现弱一致性,即最终一致性
在这里插入图片描述
在这里插入图片描述

基本可用:比如一两台及其挂了,不会导致整个集群不可用,

最终一致性:比如分布式下的保存订单 ——> 远程扣库存 ——>远程扣积分,远程扣积分炸了,保存订单回滚了,但是库存不会滚,使用一些技术比如MQ,过一段时间之后将这些扣掉的库存加回来,保证最终一致性

强一致、弱一致、最终一致
在这里插入图片描述

分布式事务的几种解决方案

2PC
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

柔性事务TCC事务补偿型方案

写三段代码,准备+提交,以及回滚代码,执行失败时调用回滚代码。

在这里插入图片描述
在这里插入图片描述

柔性事务,最大努力通知方案

比如支付宝,支付成功后,支付宝会一直给我们发消息告诉我们支付成功。

在这里插入图片描述

可靠消息+最终一致性

使用MQ的可靠消息机制,比如业务失败后,向库存服务发送MQ,让库存服务去解锁。

  • 跟上面的最大努力通知方案区别在于保证消息是可靠的==(可以使用MQ的消息确认机制等保证消息传输)==,适用于系统都是我们开发的,我们可以保证消息的可靠传输,
  • 而上面的最大努力通知一般用于第三方系统(支付宝,微信)给我们系统发送MQ消息,我们不能保证消息一定能收到,所以第三方就通知多次。

在这里插入图片描述

Seata

seata不使用于分布式的高并发场景,在此项目中适用于:商品的后台页面,保存一些商品的信息,远程调用会员服务保存积分信息,此场景并发很低

而下订单提交订单并发很高,不使用于Seata。seata的实现原理会加锁,降低并发

4.3使用MQ保证最终一致性

可以使用定时任务来做,缺点1.时效性 2.会影响效率

4.3.1延时队列

延时队列实现

一个没有任何消费者监听并且设置了过期时间的队列 + Exchange路由(一个路由可以对应多个交换机)这里称为死信路由 + 一个被消费者监听的队列

实现延时队列

核心就是红色的队列的三个参数

  • x-dead-letter-exchange : 表示消息过期后交给哪个路由
  • x-dead-letter-routing-key:表示交给路由后使用的路由键(设置为最终监听消息的队列的路由键)
  • x-message-ttl:队列的消息过期时间

并设置队列消息过期了不要扔,交给指定的路由

在这里插入图片描述

项目创建延时队列

每一个服务都创建一个路由,
在这里插入图片描述

解锁库存

库存解锁的场景
在这里插入图片描述

skuid 商品数量 订单的id 锁定状态(0 1已锁定 2)

订单+库存服务整体流程

订单设置一个标志位:0新建 1已支付

定时关单

定时关单(前提:所有服务包括远程的扣库存扣积分都成功),30分钟内还未支付,那么就执行定时关单:

订单创建成功时,向延时队列中发送消息(订单的所有内容,此时订单状态为新建),然后30分钟后,消息从延时队列路由到消费者,判断订单的状态还是新建,那么说明订单要被取消,那么直接将订单取消,并且给扣库存扣积分等服务发送消息,发送到各自的解锁业务,

自动解锁库存

假设远程调用锁库存假失败:

在锁库存的时候向延时队列发送消息,然后等待固定时间后获取延时队列的消息,根据库存单查询关联的订单状态,如果订单状态是未完成状态,那么将消息发送给交换机,交换机将消息传给解锁库存服务的队列,消费者获取消息后就进行解锁库存服务。

在这里插入图片描述
在这里插入图片描述

如何防止消息丢失

1.给数据库创建一张表,包含给那个消息的id,交换机发送,路由键是啥,数据(JSON序列化后),以及消息状态,定期扫描数据库,将发送失败的数据拿出来在发送一遍。
在这里插入图片描述

设置两个回调方法

1.消息到达broke,会回调**(ConfirmCallback)**,我们可以知道消息成功抵达了broke

2.消息没有从broke中的交换路由正确的送到指定的队列,也会回调**(setreturnCallback)**,然后我们可以修改数据库的MQ状态为失败

开启手动ACK

默认是手动ack,消费者收到消息后MQ默认删除消息。但是如果消费者收到了消息,但是没有来得及消费,消息没了,就出问题了。

手动ack,当消息真正的接收并且消费完毕后,给MQ发送ack,然后MQ才将消息真正的删除,否则收不到ack不会删除,消息还在(ready状态)。

并且还有一个拒绝,如果收到消息了,但是处理业务失败了,此时不ack,直接拒绝,然后消息又回到队列中。

总结:

做好消息确认机制(客户端+消费者手动ack)

将每一个消息记录起来,然后定期将失败的消息重新发送

最终的流程:

  • 发送消息时插入数据库
  • 消息发送给队列失败时,来到回调函数,此时我们修改数据库中这一条记录的状态为失败,后面定期扫描数据库,将失败的消息重新发送。
  • 手动ACK模式,消息没有收到+处理业务完毕,那么不ack给MQ服务器。

消息重复

在这里插入图片描述

消息积压

在这里插入图片描述

5.秒杀模块

  • 秒杀是一个单独的服务。我们将商品都放到缓存(redis)便于对比信息,
  • 先进行判断是否登录,然后校验合法性(时间是否到,是否已经秒杀过了等)
  • 然后核心使用分布式信号量,tryAcquire()尝试获取信号量,获取失败直接结束(tryAcquire()不会阻塞),获取成功,这里直接快速的创建一个秒杀订单,包括用户id,订单号,商品等信息,然后讲这些信息发送给MQ慢慢处理。
  • 核心:直接让用户确认信息,包括收货地址、支付等,最后直接给用户显示成功,整个过程没有一个远程调用,没有操作一次数据库,只是发了MQ,可以保证在高并发下,用户秒杀成功后可以稳定的填写信息并且稳定的完成支付等。并且让订单服务慢慢处理**(下订单,锁库存等等)**。
    在这里插入图片描述

Netty项目

解决粘包半包 LengthFieldBasedFrameDecoder

用户群组数据结构

用户原始信息存储到Map中,账户和密码

private Map<String, String> allUserMap = new ConcurrentHashMap<>();

每一个客户端连接上服务器时自动触发事件,向服务器发送消息(主要是用户名),然后服务器将用户名<->Channel存到Map中。

    private final Map<String, Channel> usernameChannelMap 
    private final Map<Channel, String> channelUsernameMap //反向查询

群组,群名<->群组对象(Group)

private final Map<String, Group> groupMap = new ConcurrentHashMap<>();

Group数据结构(name + Set集合)

public class Group {
    // 聊天室名称
    private String name;
    // 聊天室成员
    private Set<String> members;

    public static final Group EMPTY_GROUP = new Group("empty", Collections.emptySet());

    public Group(String name, Set<String> members) {
        this.name = name;
        this.members = members;
    }
}

自定义编解码

将客户端发送的消息进行编码(编码成一个ByteBuf),服务器解码(服务器同理)

  • 4个字节的魔数
  • 1字节版本
  • 1字节序列化方式
  • 1字节指令类型 (自定义Message类型,单发,群聊,建群。。。)
  • 4字节xxx
  • 1字节对其填充
  • 4字节 数据长度
  • 数据
@Slf4j
@ChannelHandler.Sharable
public class MessageCodec extends ByteToMessageCodec<Message> {

    @Override
    public void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        // 1. 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4});
        // 2. 1 字节的版本,
        out.writeByte(1);
        // 3. 1 字节的序列化方式 jdk 0 , json 1
        out.writeByte(0);
        // 4. 1 字节的指令类型
        out.writeByte(msg.getMessageType());
        // 5. 4 个字节
        out.writeInt(msg.getSequenceId());
        // 无意义,对齐填充
        out.writeByte(0xff);
        // 6. 获取内容的字节数组 (对象流:将对象转换为字节流)
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();
        // 7. 4字节 长度
        out.writeInt(bytes.length);
        // 8. 写入内容
        out.writeBytes(bytes);
    }

空闲检测+心跳检测

IdleStateHandler

连接假死

原因
  • 网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源。
  • 公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着
  • 应用程序线程阻塞,无法进行数据读写
问题
  • 假死的连接占用的资源不能自动释放
  • 向假死的连接发送数据,得到的反馈是发送超时
服务器端解决
  • 怎么判断客户端连接是否假死呢?如果能收到客户端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
    // 用来触发特殊事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
        IdleStateEvent event = (IdleStateEvent) evt;
        // 触发了读空闲事件
        if (event.state() == IdleState.READER_IDLE) {
            log.debug("已经 5s 没有读到数据了");
            ctx.channel().close(); //关闭连接
        }
    }
});

客户端定时心跳

  • 客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
    // 用来触发特殊事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
        IdleStateEvent event = (IdleStateEvent) evt;
        // 触发了写空闲事件
        if (event.state() == IdleState.WRITER_IDLE) {
            //log.debug("3s 没有写数据了,发送一个心跳包");
            ctx.writeAndFlush(new PingMessage());
        }
    }
});

主要流程

所有指令都是固定格式

System.out.println(" --- 菜单 --- ");
System.out.println("send [username] [content]");
System.out.println("gsend [group name] [content]");
System.out.println("gcreate [group name] [m1, m2, m3 ...]");
System.out.println("gmembers [group name]");
System.out.println("gjoin [group name]");
System.out.println("gquit [group name]");
System.out.println("quit");

不同种类的消息由不同种类的Handler处理,继承SimpleChannelInboundHandler,泛型是指定的消息类型,

public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {

核心流程

  • 连接建立后,客户端触发连接事件,向服务器发送自己的用户名,然后服务器保存username-channel到map中
  • 单聊,对方的用户名+消息,服务器根据Map,获取对方的Channel,然后发送消息
  • 建群,发送群名,以及成员名(Set),服务器收到直接存到Map中
  • 发送群消息,服务器获取群名,拿到群中的用户名然后根据用户名获取Channel发送消息。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

shstart7

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

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

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

打赏作者

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

抵扣说明:

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

余额充值