如何用Restful小成本实现即时通讯(一)用户管理

1.背景

即时通讯(Instant messaging,简称IM),允许两人或多人使用网路即时的传递文字或多媒体信息。把现实世界的信息转变为网络数据,以实现人与人,Host与Host,在网络世界中,“面对面”交流,增加交流效率和交流体验。
国内的QQ、Wechat,国外的MSN等,都是知名的即时通信软件。现在,各大App也有自己简单通讯功能,比如客服服务,即时通讯的Instant,以增加用户体验满意度。

2.框架设计

2.1 主流技术

  • 直接使用TCP/UDP,建立TCP连接,维持"用户—服务器"的长连接,配合UDP,发送消息,响应速度快,但是需要大量资源,实现难度大;
  • WebSocket,用于网页端长连接通信,可看做Web的长连接,但是使用场景有限;
  • 消息推送:采用消息队列,推送用户通知或即时信息,方案简单。

2.2 Restful技术方案

为什么选用Restful?框架设计灵活,实现成本小(运维成本需另考虑),即时通讯是一个状态敏感的场景,里面很多场景可以作为许多微服务项目解决方案参考。
SpringBoot作为服务后台,MongoDB作为用户和数据保存,Redis作为缓存数据库,protobuf作为消息通信编码协议。
在这里插入图片描述

在Restful无状态框架中,解决”实时”敏感状态保存,虽然不是IM最佳解决方案,却是一个问题无聊但是实现有意思的场景。
NoSQL真的适合IM吗,有哪些缺点,Redis是"万能"的O(1)吗。Talking is cheap. Let’s coding.

3.软件依赖

运行环境:freebsd 11,JDK8

软件版本
springboot2.7.4
redis5.0
mongodb4.4
<dependencies>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-data-mongodb</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-logging</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-validation</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.google.protobuf</groupId>
	    <artifactId>protobuf-java</artifactId>
	    <version>3.19.3</version>
	</dependency>
	<dependency>
	    <groupId>org.apache.commons</groupId>
	    <artifactId>commons-lang3</artifactId>
	    <version>3.12.0</version>
	</dependency>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-test</artifactId>
	    <scope>test</scope>
	</dependency>
	</dependencies>

4. 用户数据库设计

先贴上V1的简单用户表吧,MongoDB临时加字段洒洒水啦。

// 用户角色
public enum UserRole {
    COMMON_USER,
    GRAY_USER
}
// 用户表
@Getter
@Setter
@Document(collection = "user")
@Accessors(chain = true)
public class User {
	// 用户ID
    @Id
    private String uid;
	// 注册时间
    @NotNull
    private Date regTime;
	// 用户名称
    @Length(min = 3, max = 18)
    private String name;
	// 密码
    @Length(min = 6, max = 18)
    private String password;
	// 电邮
	@Email
    private String email;
	// 手机号
    @NotBlank
    @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$", message = "phone num error")
    private String phone;
	// 头像信息
    @URL
    private String headUrl;
	// 用户角色
    private UserRole status;
	// 用户好友
    private List<String> friends;
	// 用户群组
	private List<String> groups;
}

这里就能看出,MongoDB对于结构确定的数据存放的缺点,比如用户好友的存放,Mysql分库分表,会使用FriendShip的owner的映射表,而MongoDB存放在一个bson,查找方便,但是添加使用比较麻烦,需要将所有friends全部取出,查看是否存在好友判断也是需要遍历,因此这里会结合redis尽可能高效实现。

4. 注册和登录功能

4.1 MongoRepository使用

MongoDB对于简单操作,仅需用extends MongoRepository,使用时可以按照条件添加简单接口,是个很好的轮子。
比如:按照用户和比吗查找用户:findUserBy + “Name”,定义为函数User findUserByName(String name),即可直接使用。

@Configuration
public class MongoConfig {
    @Autowired
    private MongoDatabaseFactory mongoDatabaseFactory;
    @Autowired
    private MappingMongoConverter mappingMongoConverter;
    @Bean
    public MongoTemplate mongoTemplate() {
        mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return new MongoTemplate(mongoDatabaseFactory, mappingMongoConverter);
    }
}

public interface UserRepository extends MongoRepository<User, String> {
	// 按照name和password查找用户
    User findUserByNameAndPassword(String name, String password);
	// 模糊查找,末尾加Like
    List<User> findUsersByNameLike(String name);
    // 判断email或phone是否已经存在
    boolean existsUserByEmailOrPhone(String email, String phone);
}

4.2 RedisRepository使用

Redis操作可以将RedisTemplate常用操作封装在RedisRepository。

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
        template.setValueSerializer(serializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

@Slf4j
@Repository
public class RedisRepository {
    private final RedisTemplate<Object, Object> redisTemplate;

    public RedisRepository(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void put(String key, byte[] value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public void mput(Map<String, byte[]> map) {
        redisTemplate.opsForValue().multiSet(map);
    }

    public void set(String key, String value, Duration time) {
        redisTemplate.opsForValue().set(key, value, time);
    }

    public String get(String key) {
        Object obj =redisTemplate.opsForValue().get(key);
        if (obj != null && obj instanceof String) {
            return (String) obj;
        }
        return "";
    }

    public Long increment(String key) {
        Long times = redisTemplate.boundValueOps(key).increment(1);
        if (times != null && times == 1) {
            redisTemplate.boundValueOps(key).expire(60, TimeUnit.SECONDS);
        }
        return times;
    }

    public Boolean del(String key) {
        return redisTemplate.delete(key);
    }
}

4.3 注册和登录

  1. 注册
  • 用户基本信息填写正确,如name、email、phone合法
  • 用户emial、phone未被注册,否则视为用户已存在
  1. 登录
  • 用户账户和密码匹配
  • 用户是未登录状态
  • 用户登录尝试次数超过3次,将锁定用户1分钟
  • 登录成功更新用户状态

这里用户状态、尝试超次和锁定等,均暂存于redis中。

// Service实现
@Slf4j
@Service
public class UserMgrService {
    private final UserRepository userRepository;

    private final RedisRepository redisRepository;

    public UserMgrService(UserRepository userRepository, RedisRepository redisRepository) {
        this.userRepository = userRepository;
        this.redisRepository = redisRepository;
    }

    public boolean loginUser(String name, String password) {
        final String PREFIX_LOGIN_STATUS = "LOG:";
        final String LOGIN_TIME = "TIME:";
        String logKey = PREFIX_LOGIN_STATUS + LOGIN_TIME + name;
        if (redisRepository.increment(logKey) > 3) {
            log.warn("login tried too more time");
            return false;
        }
        User user = userRepository.findUserByNameAndPassword(name, password);
        if (user == null) {
            return false;
        }
        redisRepository.del(logKey);
        final String LOGIN_STATUS = "TIME:";
        String logStatusKey = PREFIX_LOGIN_STATUS + LOGIN_STATUS + name;
        final String STATUS_ONLINE = "ONLINE";
        if (STATUS_ONLINE.equals(redisRepository.get(logStatusKey))) {
            log.warn("{} had login", name);
            return false;
        }
        redisRepository.set(logStatusKey, STATUS_ONLINE, Duration.ofMinutes(5));
        return true;
    }

    public boolean registerUser(User user) {
        if (userRepository.existsUserByEmailOrPhone(user.getEmail(), user.getPhone())) {
            log.warn("phone {} or email {} existed", user.getPhone(), user.getEmail());
            return false;
        }
        userRepository.insert(user);
        return true;
    }

    public List<User> searchUserByName(String name) {
        return userRepository.findUsersByNameLike(name);
    }
}

// Controller实现
@Validated
@RestController
@RequestMapping("/user")
public class UserMgrController {
    @Autowired
    private UserMgrService userMgrService;

    @PostMapping("/register")
    public String registerUser(@Valid User user) {
        return userMgrService.registerUser(user) ? "OK" : "ERROR";
    }

    @PostMapping("/login")
    public String loginUser(@RequestParam("name") String name,
                            @RequestParam("password") String password) {
        return userMgrService.loginUser(name, password) ? "OK" : "ERROR";
    }

    @GetMapping("/search")
    public List<User> searchUserByName(@RequestParam("name") String name) {
        if (StringUtils.isEmpty(name)) {
            return Collections.emptyList();
        }
        return userMgrService.searchUserByName(name);
    }
}

简单注册和登录功能已实现,后续有空增加用户好友申请和群组管理,消息发送与接收,尤其是群消息的接受框架实现,对于TCP和UDP反而简单,但是对Restful是个挑战。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,以下是一个简单的使用RESTful API进行数据传输的示例: 假设我们有一个client项目和一个server项目,client需要向server发送一个用户信息,server接收到信息后将该用户信息存储到数据库中。 首先,我们需要在server项目中定义一个RESTful API接口: ```java @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity<Void> createUser(@RequestBody User user) { userService.createUser(user); return ResponseEntity.ok().build(); } } ``` 其中,@RestController注解表示该类是一个控制器,@RequestMapping注解表示该控制器处理的请求路径是/user,@PostMapping注解表示该方法处理的请求方式是POST,@RequestBody注解表示该方法接收一个请求体,即客户端发送过来的用户信息。 接下来,我们需要在client项目中发送一个POST请求来调用该API接口: ```java public class UserClient { public void createUser(User user) { RestTemplate restTemplate = new RestTemplate(); String url = "http://server-url/user"; restTemplate.postForObject(url, user, Void.class); } } ``` 其中,RestTemplate是Spring提供的一个用于发送HTTP请求的工具类,postForObject方法用于发送一个POST请求,并将user对象作为请求体发送到server项目的/user接口中。 这样,client就成功向server发送了一个用户信息,并且server项目成功接收到了该信息并存储到了数据库中。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值