OS监控
项目源码:https://github.com/Huidaka/Java-code/tree/master/OS%E7%9B%91%E6%8E%A7
项目描述:
用来监控主机的CPU使用率;
核心功能:
- 采集需要监控主机的CPU使用率并上报给服务器,然后通过服务器将数据发送给客户端;
项目技术:
- 网络理论 + 网络中的Soket编程(TCP/UDP)
- 并发编程
- Server部署到公网ip位置,部署到云服务器上,Linux操作
- Client是图形化编程——JavaFX
技术选型
为什么Agent-Server要用UDP传输,而Server-Client要使用TCP传输?
先列举一下TCP和UDP的优缺点
TCP优点:有连接,可靠 TCP缺点:网络质量较好时,传输效率低,有连接,所以TCP被动连接方(服务器)需要维护连接很多数据
UDP的优缺点刚好和TCP相反。
Agent的数量:万级(时时刻刻) Client 的数量 :百级
Agent上报的频率是秒级,偶尔有一两个数据丢了也没事,关系不大
Agent到Client一般是内网,质量好,Client对外服务,相对网络质量较差,所以Agent到Server不容易丢包,真丢几个包也没关系。如果用TCP的话,那时时刻刻都要维护万级的的TCP连接。所以选择UDP。
Server到Client网络质量相对较差,人数少,对可靠性要求高,所以使用TCP
整体架构:
主体架构
- 采用Agent + Server + Client架构;
Server架构
UDPServer + TcpServer
项目实现:
Agent实现
Agent目标:
- Agent 代码只有一-份,但需要部署到不同的电脑,进行采集,同时,不同的电脑上,有不同的hos tname
意味着,Agent代码中,必须具备,动态指定hostname 的能力 - Agent 可以采集CPU的使用率
- Agent 需要定期向Server 进行数据的汇报(频率可以静态的,也可以动态的)
- Agent 向Server 进行汇报时,需要明确Server 的ip和udp port, 这部分信息也最好动态获取
数据格式:
代码实现:
//收集数据端
public class Agent{
//通过运行时参数将hostName,interval,serverHost,port传入
public static void main(String[] args) {
if (args.length < 4) {
System.out.println("请按照规定传入运行时参数");
return;
}
//获取运行时传入的参数
String hostName = args[0];
long interval = Long.parseLong(args[1]);
String serverHost = args[2];
int port = Integer.parseInt(args[3]);
//利用系统的Soket采用UDP协议进行发送
try (DatagramSocket socket = new DatagramSocket();) { //初始化发送数据包的skoet
while (true) {
//记录当前系统时间
long time = System.currentTimeMillis();
//获取Cpu使用率
OperatingSystemMXBean platformMXBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
double percent = platformMXBean.getSystemCpuLoad();
//封装消息
String message = String.format("%s$%d$%f", hostName, time, percent);
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket datagramPacket = new DatagramPacket(
bytes, 0, bytes.length, InetAddress.getByName(serverHost), port);
//发送数据
socket.send(datagramPacket);
System.out.println(message);
//间隔秒数
TimeUnit.SECONDS.sleep(interval);
}
} catch (SocketException | UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Server实现
举例:
其实Server的实现就和邮局工作送报纸的流程一样,首先UDPServer把上层Agent送来的报纸统一存放到仓库,Litener就是发报员,负责给下层Client的送报纸,然后TCPServer是负责统计都有谁订阅的哪份报纸,然后告诉Listener,最后Listener根据TCPServer给的信息来发报纸。
数据存放问题:
数据从Agent端传过来,我们需要先对数据进行分析,解析成,主机名:主机相关数据 的形式;
看到这种键值对的形式,我们首先想到的就是HashMap,然后出于线程安全问题方面的考虑,使用ConcurrentHashMap来存放数据。然后主机相关数据我们可以新建一个类ReportData来存放,在网络传输过程有可能会阻塞,所以我们不能只存这个主机的一条数据,我们可以存一分钟的数据。如果我们使用队列来存,那么Agent会一直发数据,放满了一分钟的数据就没办法了,所以我们这里可以使用一个环形对列CycleQueue,这样里面保存的就一直是最新的一分钟数据。
存放数据的数据结构: Map< hostname,CycleQueue< ReportData > >
数据存放代码实现:
//环形队列
public class CycleQueue<T> {
public T[] array;
private int size;
public int frontIndex;
public int rearIndex;
public CycleQueue(int capacity) {
this.array = (T[])new Object[capacity]; //底层数组初始化
this.size = 0;
this.frontIndex = 0;
this.rearIndex = 0;
}
public synchronized void push(T value){
array[rearIndex] = value;
rearIndex = (rearIndex + 1)%array.length; //通过模上数组长度计算rearIndex的下标
if(size == array.length){
frontIndex = rearIndex; //只有当队列放满的时候再添加元素,队列头才会向后移
} //如果队列放满了,就让队列头跟着队列尾往后走
size++;
}
public synchronized T pop() throws RuntimeException{
if(size == 0){
throw new RuntimeException("队列为空");
}
T ret = array[frontIndex]; //得到要返回的队头元素数值
frontIndex = (frontIndex+1)%array.length; //通过模上数组长度下标计算frontIndex的下标
size--;
return ret;
}
}
public class ReportData {
public String hostName;
public long time;
public double percent;
public ReportData(String hostName, long time, double percent) {
this.hostName = hostName;
this.time = time;
this.percent = percent;
}
}
public class Storage {
//存放数据的仓库
public static ConcurrentHashMap<String,CycleQueue<ReportData>> map = new ConcurrentHashMap<>();
//初始化仓库
public static void put(String hostName, long time, double percent) {
ReportData data = new ReportData(hostName,time,percent);
CycleQueue<ReportData> queue = map.computeIfAbsent(hostName,k->new CycleQueue<>(60));
queue.push(data);
}
//获取仓库数据
public static ReportData getNewReportData(String hostName) throws RuntimeException{
if(!map.containsKey(hostName)){
return null;
}
return map.get(hostName).pop();
}
}
工作流程:
UDPServer负责收集数据
TCPServer负责建立TCP连接,并统计,然后对统计好的连接进行推送数据
UDPServer实现
public class UDPServer {
Thread worker;
public void startWork(){
worker = new Thread(){
@Override
public void run() {
work(); //收集数据
}
};
worker.start(); //启动收集数据的工作线程
}
public void work() {
byte[] bytes = new byte[1024];
try (DatagramSocket socket = new DatagramSocket(8000)) {
while (true) {
DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length);
//该方法会阻塞,直到有消息传过来
socket.receive(packet);
String message = new String(bytes, 0, bytes.length, "ASCII");
//按照事先约定好的格式进行拆分
String[] messages = message.split("\\$");
String hostName = messages[0];
long time = Long.parseLong(messages[1]);
double percent = Double.parseDouble(messages[2]);
Storage.put(hostName, time, percent);
//打印收集的日志信息
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String data = simpleDateFormat.format(new Date(time));
System.out.println(hostName + ": " + data + " " + percent);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCPServer实现
public class TCPServer {
public static void startWork(){
//Tcp工作线程
Runnable runnable = new Runnable() {
@Override
public void run() {
work();
}
};
Thread thread = new Thread(runnable);
//启动线程
thread.start();
}
private static void work(){
CommandBuilder commandBuilder = new CommandBuilder();
try(ServerSocket serverSocket = new ServerSocket(8001)){
while (true) {
//建立TCP连接
Socket socket = serverSocket.accept();
//分析Client发送的数据,并生成相应的命令
commandBuilder.buildAndRun(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//命令接口
public interface Command {
void run(Socket socket,String[] args);
}
//将Client发过来的数据生成相应命令
public class CommandBuilder {
public void buildAndRun(Socket socket){
try {
Command command;
// 按行读取 client 发送的命令
// 目前支持两种命令: list / get
Scanner scanner = new Scanner(socket.getInputStream(),"utf-8");
String string = scanner.nextLine();
String[] group = string.split(" ");
String commandName = group[0];
String[] args = Arrays.copyOfRange(group,1,group.length);
if(commandName.equalsIgnoreCase("list")){
//list命令,查看所有主机列表
command = new ListCommand();
}
else if(commandName.equalsIgnoreCase("get")){
//get命令,查看这个主机的CPU信息
command = new GetCommand();
}
else {
//如果是其他命令,就直接关闭连接
command = new NotFountCommand();
}
//根据创建的实现接口类的对应实例,执行相应的命令
command.run(socket,args);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class GetCommand implements Command{
@Override
public void run(Socket socket, String[] args) {
if(args.length == 0){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Listener listener = new Listener();
listener.subscribe(socket,args[0]);
}
}
public class ListCommand implements Command{
@Override
public void run(Socket socket, String[] args) {
try {
//把存放的主机信息列表都发给Client
for (String hostName : Storage.map.keySet()) {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(hostName + "\r\n");
//刷新缓冲区
bufferedWriter.flush();
}
//关闭连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class NotFountCommand implements Command{
@Override
public void run(Socket socket, String[] args) {
//无法处理的命令直接关闭连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Listener {
//订阅表:都有谁订阅哪个主机的信息
public static ConcurrentHashMap<String, List<Socket>> subscribers = new ConcurrentHashMap<>();
//启动推送数据线程
public static void startWork(){
Listener listener = new Listener();
try {
listener.work();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//添加订阅
public void subscribe(Socket socket,String hostName){
List<Socket> sockets = subscribers.computeIfAbsent(hostName,k->new ArrayList<>());
synchronized (sockets) {
sockets.add(socket);
}
}
//工作线程:负责给Client推送数据
private void work() throws InterruptedException {
while (true) {
for(String hostName : subscribers.keySet()){
ReportData reportData;
try {
reportData = Storage.getNewReportData(hostName);
}catch (RuntimeException runtimeException){
runtimeException.printStackTrace();
System.out.println("队列为空");
reportData = null;
}
//返回值为null,说明没有这个主机数据,直接跳过即可
if(reportData == null){
continue;
}
List<Socket> sokets = subscribers.get(hostName);
//返回值为null,说明没有连接需要这个主机的信息,跳过即可
if(sokets == null){
continue;
}
//复制一份订阅信息,因为这个sockets一直再添加删除元素,我们为了线程安全会给他加锁,这样就会造成线程阻塞
//从而影响另一个推送数据的工作效率,所以我们可以拷贝一份这个表,交给另一个推送线程就可以了
List<Socket> socketCopy;
synchronized (sokets) {
socketCopy = new ArrayList<>(sokets);
}
//推送数据
pushMessage(socketCopy,hostName,reportData);
}
//每隔一秒推送一次数据
TimeUnit.SECONDS.sleep(1);
}
}
//推送数据具体工作
private void pushMessage(List<Socket> socketCopy,String hostName,ReportData reportData) {
BufferedWriter bufferedWriter;
for(Socket socket : socketCopy){
try {
//因为发送的都是字符数据,所以这里选用字符流进行发送
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//获取格式化数据
String message = reportDataToByte(reportData);
bufferedWriter.write(message);
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
//一待出现异常,就直接关闭这个连接
unSubscribers(hostName,socket);
}
}
}
//从订阅表删除这个连接的订阅 ,并关闭
public void unSubscribers(String hostName, Socket socket){
//删除订阅的连接,需要删除原链表的socket,不能删除复制过来的sockets
List<Socket> sockets = subscribers.get(hostName);
if(sockets == null){
return;
}
synchronized (sockets) {
sockets.remove(socket);
if(sockets.size() == 0){
//如果一个主机没有Client订阅它的信息了,就把他从订阅表中删除
subscribers.remove(hostName);
}
}
try {
//关闭这个连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//将需要推送的数据取出并进行格式化
public String reportDataToByte(ReportData data){
//将时间戳转换成格式化日期
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = simpleDateFormat.format(new Date(data.time));
String message = String.format("%s %s %.2f%%\r\n",data.hostName,time,data.percent*100);
return message;
}
}
Client实现
Client目前使用的是windows自带的telnet客户端
项目难点:怎么保证项目的线程安全问题?
首先分析出项目每个线程对应执行的代码块,其次分析每个线程中对数据进行操作的地方是否会触发线程安全问题,然后通过ConcurrentHashMap,以及使用synchornized加锁来解决问题
从架构流程图中我们可以看到Server中一共有三个线程,分别是收集数据线程(A),建立管理TCP连接线程(C),推送数据线程(B)。
线程A和线程C会同时操作Map中的数据,所以使用ConcurrentHashMap来解决这个问题;
线程A和线程B又会同时操作CycleQueue里面的数据,所以我们把CycleQueue改成线程安全版本的,给push和pop方法加锁;
因为线程B和线程C之间还维护了一个订阅表,也是一个Map<hostName , List >,我们也使用ConcurrentHashMap。
最后线程B和线程C也会同时操作List里的Socket,所以这里也会涉及到线程安全问题,在这里我们采用的是给涉及到操作List的时候都加上锁
后续拓展
把Client用图形化编程——JavaFX实现等。