9.5 监控端口
在实际的项目中,出于定位问题和统计数据的要求,往往会给一些正在运行的服务开一些对外的“口子”,技术和运维人员可以给正在运行的服务发一些指定得到一些想要的结果。
发送的这些指令即可以是短连接的形式(如发送一个 http 请求)或者长连接的形式,为了方便使用,我们通常不会专门开发一些工具去做这件事,而是利用一些现成的命令如 nc、telnet。
我在我的即时通讯软件 Flamingo 的 chatserver 中实现了这样一个监控功能,使用的是长连接。我们来看一下效果。我的 chatserver 的地址是 127.0.0.1,端口号是 8888。可以使用 nc -v 127.0.0.1 8888
连上服务器,效果如下:
连接成功后,服务器会输出 5 条命令,每条命令都有相应的解释,输入对应的命令可以执行相应的操作。
help 查看所有整个监控端口支持哪些命令;
ul 显示当前内存中在线的用户信息,
su 输出指定用户的信息,需要使用 su userid 格式来指定某个用户的 userid;
elpb 和 dlpb 是启用和禁用是否将网络包的二进制格式写入日志文件,用于排查网络包问题。当然,这两个命令可以合二为一,通过不同的参数来指定。
执行上述命令效果如下:
监控命令的实际用途就是在服务运行过程中查询和修改服务内存中的一些信息。其实现原理也很简单,在即某个端口(这里例子中是 8888)上开启一个侦听,然后接收连接,处理连接上来的端口发送的监控命令。由于我这里使用的是 nc 命令作为监控端口的客户端工具,因此其每条指令都是以 \n 结束的,我们把 \n 作为每个数据包的结束标志。
实现逻辑如下:
void MonitorSession::onRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
{
std::string buf;
std::string substr;
size_t pos;
size_t totalsize = 0;
while (true)
{
//xxx\nyyy\nuuuu\njjjjj
buf.clear();
buf = pBuffer->toStringPiece();
while (true)
{
pos = buf.find("\n");
if (pos != std::string::npos)
{
if (pos == 0)
substr = "\n";
else
substr = buf.substr(0, pos);
totalsize += substr.length();
buf = buf.substr(pos + 1);
LOGI("recv cmd: %s", substr.c_str());
//LOGI << "buf: " << substr;
process(conn, substr);
}
else
{
if (totalsize > 0)
pBuffer->retrieve(totalsize);
return;
}
}// end inner while-loop
}// end outer while-loop
}
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
MonitorSession::onRead 方法中每收到一段含 \n 标志的数据,就会将其当作客户端的一个监控命令,然后调用 process 方法对这条命令进行解析(代码 25 行),process 方法实现如下:
bool MonitorSession::process(const std::shared_ptr<TcpConnection>& conn, const std::string& inbuf)
{
if (inbuf == "\n")
return false;
std::vector<std::string> v;
StringUtil::split(inbuf, v, " ");
if (v.empty())
return false;
else
{
if (v[0] == g_helpInfo[0].cmd)
showHelp();
else if (v[0] == g_helpInfo[1].cmd)
{
if (v.size() >= 2)
showOnlineUserList(v[1]);
else
showOnlineUserList("");
}
else if (v[0] == g_helpInfo[2].cmd)
{
if (v.size() < 2)
{
char tip[32] = { "please specify userid.\n" };
send(tip, strlen(tip));
}
else
{
showSpecifiedUserInfoByID(atoi(v[1].c_str()));
}
}
else if (v[0] == g_helpInfo[3].cmd)
{
//开启日志数据包打印二进制字节
Singleton<ChatServer>::Instance().enableLogPackageBinary(true);
char tip[32] = { "OK.\n" };
send(tip, strlen(tip));
}
else if (v[0] == g_helpInfo[4].cmd)
{
//开启日志数据包打印二进制字节
Singleton<ChatServer>::Instance().enableLogPackageBinary(false);
char tip[32] = { "OK.\n" };
send(tip, strlen(tip));
}
else
{
//客户端发送不支持的命令
char tip[32] = { "cmd not support\n" };
send(tip, strlen(tip));
}
}
return true;
}
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
process 方法的逻辑其实就是简单的字符串匹配,匹配哪条指令,执行哪条指令,执行完了之后将执行结果或状态返回给监控客户端(调用 send 方法)。所有的命令说明定义在全局数据 g_helpInfo 中:
struct HelpInfo
{
std::string cmd;
std::string tip;
};
const HelpInfo g_helpInfo[] = {
{ "help", "show help info" },
{ "ul", "show online user list" },
{ "su", "show userinfo specified by userid: su [userid]" },
{ "elpb", "enable log package binary data" },
{ "dlpb", "disable log package binary data" }
};
2
3
4
5
6
7
8
9
10
11
12
13
如果客户端发送一个不支持的命令,服务器会返回 “cmd not support” 的信息:
关于监控端口我还有几点要提醒下读者:
- 为了服务器的安全性,监控端口的 ip 地址和端口号通常应该配置成只允许内网访问,不建议暴露在公网上;
- 监控端口输出的信息不应该暴露出敏感信息,例如用户的密码,如果必须要访问用户敏感信息,必须通过进一步的指令权限认证才可以访问。
- 监控端口在获取内存中某些数据时,如果这些数据会被多个线程访问,应尽快将这些数据复制一份副本出来,以减小锁的粒度,不要影响程序中正常运行的线程处理这些数据的效率。
关于监控端口的原理很简单,灵活使用它可以为上线后的服务维护工作带来很多便利。