linux脚本实现登录redis功能,使用MQTT协议+Redis缓存实现APP登录顶号功能

大家在玩游戏或使用QQ等IM工具时,想必都见到过弹出被顶号或者是您的账号于xx时间在另一设备登录,您已被迫下线这样的提示,然后不得不点退出按钮退出整个应用,或者点击重新登录把另一设备再顶下来。最近我参与的一个项目,正好就有这样的需求,而且,由于我们项目中已经使用到了MQTT协议进行消息推送,实现远程控制,后台用Java实现,缓存使用了Redis,因此,正好可以利用现有的技术来实现这个功能。

实现的思路大概如下:首先,登录时不仅需要账号密码,还可以将设备关键信息记录下来,如设备型号(Android|iPhone)、登录时间、登录IP、设备唯一标识(UUID)等,这就需要前台登录功能与后台接口一起配合实现,并在后台把userId已经相关设备信息保存到Redis中,当在另外一台新设备上登录同一帐号时,将userId对应的相关登录设备信息直接进行覆盖,此时如果旧设备进行重连时,因为该uuid已经不是当前服务端的uuid了,所以直接返回下线通知,为了进行友好提示,也可以将新登录设备的主要信息(设备型号、登录时间)进行返回。

下面简单介绍一下实现的方法。

软件安装

Linux下mqtt服务器Apollo的安装

下载

创建broker

一个broker实例是一个文件夹,其中包含所有的配置文件及运行时的数据,不如日志和消息数

据。Apollo强烈建议不要把实例同安装文件放在一起。在linux操作系统下面,建议将实例建在

/var/lib/目录下面

首先解压:tar -zxvf apache-apollo-1.7.1-unix-distro.tar.gz

选择一个目录存放解压后的文件,我放在了/server/下,解压后的文件夹为 apache-apollo-1.7.1

开始创建broker实例:

cd /var/lib

sudo /server/apache-apollo-1.7.1/bin/apollo create mybroker1

2

下图是Apache官方给的一些建议截图:

126452440_1_20180306035848253

启动broker实例

启动broker实例可以有两种方法,如下图中所示:

126452440_2_20180306035848737

可以执行

/var/lib/mybroker/bin/apollo-broker run1

或者

sudo ln -s "/var/lib/mybroker/bin/apollo-broker-service" /etc/init.d/

/etc/init.d/apollo-broker-service start1

2

使其作为一个service进行启动,以后系统重启后只需运行/etc/init.d/apollo-broker-service start

访问Apollo的监控页面: http://localhost:61680/ 默认用户名、密码为为 admin/password

Linux下Redis的安装与配置

Redis的安装非常简单,已经有现成的Makefile文件,解压后在src目录下使用make命令完成编译即可,redis-benchmark、redis-cli、redis-server、redis-stat 这四个文件,加上一个 redis.conf 就构成了整个redis的最终可用包。它们的作用如下:

redis-server:Redis服务器的daemon启动程序

redis-cli:Redis命令行操作工具。当然,你也可以用telnet根据其纯文本协议来操作

redis-benchmark:Redis性能测试工具,测试Redis在你的系统及你的配置下的读写性能

redis-stat:Redis状态检测工具,可以检测Redis当前状态参数及延迟状况

下载安装:

wget http://download.redis.io/redis-stable.tar.gz

tar xzf redis-stable.tar.gz

cd redis-stable

make

make install1

2

3

4

5

启动

编译后生成的可执行文件:

redis-server 是Redis的服务器,启动Redis即运行redis-server

redis-cli 是Redis自带的Redis命令行客户端,学习Redis的重要工具

./redis-server & 不指定配置直接运行,这时采用默认配置,无密码

./redis-server –port 6379 仅指定端口

./redis-server ../redis.conf 指定配置文件

最好还是使用最后一种方式进行启动

如果只是在本机连接,那麽使用默认配置文件不会有什么问题,但是,如果是连接远程服务器端的Redis,则需要对配置文件进行一些修改:

requirepass foobared

#bind 127.0.0.1 ##注释掉

protected-mode no ##从yes改成no1

2

3

至于如何将Redis设置后台服务,开机自启等,这里就不介绍了,可以去搜索一下。

功能实现

后台接口

Redis客户端使用的是Jedis,如下代码是一个对Jedis简单的封装

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import redis.clients.jedis.Jedis;

import redis.clients.jedis.JedisPool;

import redis.clients.jedis.JedisPoolConfig;

import redis.clients.jedis.exceptions.JedisException;

import java.util.ResourceBundle;

/**

* Jedis Cache 工具类

*/

public class JedisUtils {

private static Logger logger = LoggerFactory.getLogger(JedisUtils.class);

private static JedisPool jedisPool;

/**

* 读取相关的配置

*/

static {

ResourceBundle resourceBundle = ResourceBundle.getBundle("redis");

int maxActive = Integer.parseInt(resourceBundle.getString("redis.pool.maxActive"));

int maxIdle = Integer.parseInt(resourceBundle.getString("redis.pool.maxIdle"));

int maxWait = Integer.parseInt(resourceBundle.getString("redis.pool.maxWait"));

int port = Integer.parseInt(resourceBundle.getString("redis.port"));

int timeout = Integer.parseInt(resourceBundle.getString("redis.timeout"));

String ip = resourceBundle.getString("redis.ip");

String auth = resourceBundle.getString("redis.auth");

JedisPoolConfig config = new JedisPoolConfig();

//设置最大连接数

config.setMaxTotal(maxActive);

//设置最大空闲数

config.setMaxIdle(maxIdle);

//设置超时时间

config.setMaxWaitMillis(maxWait);

//初始化连接池

jedisPool = new JedisPool(config, ip, port, timeout, auth);

}

/**

* 获取缓存

* @param key 键

* @return 值

*/

public static String get(String key) {

String value = null;

Jedis jedis = null;

try {

jedis = getResource();

if (jedis.exists(key)) {

value = jedis.get(key);

value = StringUtils.isNotBlank(value) && !"nil".equalsIgnoreCase(value) ? value : null;

logger.debug("get {} = {}", key, value);

}

} catch (Exception e) {

logger.warn("get {} = {}", key, value, e);

} finally {

returnResource(jedis);

}

return value;

}

/**

* 设置缓存

* @param key 键

* @param value 值

* @param cacheSeconds 超时时间,0为不超时

* @return

*/

public static String set(String key, String value, int cacheSeconds) {

String result = null;

Jedis jedis = null;

try {

jedis = getResource();

result = jedis.set(key, value);

if (cacheSeconds != 0) {

jedis.expire(key, cacheSeconds);

}

logger.debug("set {} = {}", key, value);

} catch (Exception e) {

logger.warn("set {} = {}", key, value, e);

} finally {

returnResource(jedis);

}

return result;

}

/**

* 删除缓存

* @param key 键

* @return

*/

public static long del(String key) {

long result = 0;

Jedis jedis = null;

try {

jedis = getResource();

if (jedis.exists(key)){

result = jedis.del(key);

logger.debug("del {}", key);

}else{

logger.debug("del {} not exists", key);

}

} catch (Exception e) {

logger.warn("del {}", key, e);

} finally {

returnResource(jedis);

}

return result;

}

/**

* 缓存是否存在

* @param key 键

* @return

*/

public static boolean exists(String key) {

boolean result = false;

Jedis jedis = null;

try {

jedis = getResource();

result = jedis.exists(key);

logger.debug("exists {}", key);

} catch (Exception e) {

logger.warn("exists {}", key, e);

} finally {

returnResource(jedis);

}

return result;

}

/**

* 获取资源

* @return

* @throws JedisException

*/

public static Jedis getResource() throws JedisException {

Jedis jedis = null;

try {

jedis = jedisPool.getResource();

} catch (JedisException e) {

logger.warn("getResource.", e);

returnBrokenResource(jedis);

throw e;

}

return jedis;

}

/**

* 归还资源

* @param jedis

*/

public static void returnBrokenResource(Jedis jedis) {

if (jedis != null) {

jedisPool.returnBrokenResource(jedis);

}

}

/**

* 释放资源

* @param jedis

*/

public static void returnResource(Jedis jedis) {

if (jedis != null) {

jedisPool.returnResource(jedis);

}

}

}1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

然后在登录接口中,当判断完登录的用户名密码正确后,可以参考如下代码的思路去实现,首先判断Redis中是否已保存有这个userId对用的值,有的话说明当前已经有登录,需要被替换到,同时使用MQTT发送消息给客户端使其退出,Redis中不存在则只需保存userId和uuidStr即可

String uuidStr = ""; //这个值从APP端传过来

// 先判断Redis中是否已经有,有的话需要替换掉

if(JedisUtils.get(userId) != null && !JedisUtils .get(userId).equals(uuidStr)) {

MqttClient client = MyMqttClient.getInstance();

String topic = "TOPIC/LOGIN_LOGOUT";

client.subscribe(topic, 1);

MyMqttClient.sendMessage("Log out", topic);

client.unsubscribe(topic);

}

JedisUtils.set(userId, uuidStr, 0);1

2

3

4

5

6

7

8

9

10

11

12

至于MQTT协议的实现,这里使用的是Paho,如果后台项目是使用Maven构建的话,在pom.xml中加入如下几行即可:

org.eclipse.paho

org.eclipse.paho.client.mqttv3

1.1.0

1

2

3

4

5

然后对其进行了一个简单的封装

import org.eclipse.paho.client.mqttv3.MqttClient;

import org.eclipse.paho.client.mqttv3.MqttConnectOptions;

import org.eclipse.paho.client.mqttv3.MqttDeliveryToken;

import org.eclipse.paho.client.mqttv3.MqttException;

import org.eclipse.paho.client.mqttv3.MqttMessage;

import org.eclipse.paho.client.mqttv3.MqttTopic;

import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MyMqttClient {

private MyMqttClient() {}

private static MqttClient mqttClientInstance = null;

private static MqttConnectOptions options;

//静态工厂方法

public static synchronized MqttClient getInstance() {

try {

if (mqttClientInstance == null) {

mqttClientInstance = new MqttClient("tcp://125.216.242.151:61613",

MqttClient.generateClientId(), new MemoryPersistence());

options = new MqttConnectOptions();

//设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接

options.setCleanSession(true);

//设置连接的用户名

options.setUserName("admin");

//设置连接的密码

options.setPassword("password".toCharArray());

// 设置超时时间 单位为秒

options.setConnectionTimeout(10);

// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制

options.setKeepAliveInterval(20);

mqttClientInstance.connect(options);

}

return mqttClientInstance;

}catch (Exception e){

e.printStackTrace();

return null;

}

}

public static void sendMessage(String content, String myTopic) {

MqttTopic topic = getInstance().getTopic(myTopic);

MqttMessage message = new MqttMessage();

message.setQos(1);

message.setRetained(false);

message.setPayload(content.getBytes());

try {

MqttDeliveryToken token = topic.publish(message);

} catch (MqttException e) {

e.printStackTrace();

}

}

public static MqttConnectOptions getOptions(){

return options;

}

}1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

app端

客户端的做法思路也很简单,由于使用了MQTT,因此客户端和服务器端其实已经保持了一个长连接,可以为客户端写一个MQTTService,随时监听服务器推送过来的消息进行处理

//为MTQQ client设置回调

client.setCallback(new MqttCallback() {

@Override

public void connectionLost(Throwable cause) {

//连接丢失后,一般在这里面进行重连

}

@Override

public void deliveryComplete(IMqttDeliveryToken token) {

//publish后会执行到这里

}

@Override

public void messageArrived(String topicName, MqttMessage message) throws Exception {

if(message.toString().equals("Log out")) {

handler.post(new Runnable() {

@Override

public void run() {

AlertDialog.Builder builder = new AlertDialog.Builder(getApplicationContext());

builder.setMessage("被顶号了");

builder.setNegativeButton("退出", new DialogInterface.OnClickListener() {

@Override

public void onClick(DialogInterface dialog, int which) {

// TODO 退出当前账号,在这里简单粗暴的结束了应用

stopSelf();

android.os.Process.killProcess(android.os.Process.myPid());

}

});

Dialog dialog = builder.create();

dialog.setCanceledOnTouchOutside(false);

dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);

dialog.show();

}

});

}

}

});1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

总结

上述代码可能在严谨性和可靠性上还会存在一些问题,还需要经过不断的完善,但思路是很明确的。在这里尤其要安利一下MTQQ,现在越来越多的产品都是基于这个协议进行开发,进行消息推送等。它开销很小,支持各种流行编程语言,能够适应不稳定的网络传输需求,在未来几年,相信MQTT的应用会越来越广。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值