socket截取并封装告警报文学习笔记

不熟悉的类

StringBuffer

StringBuffer 是 Java 中的一个类,表示一个可变的字符序列。它类似于 String 类,但与 String 不同,StringBuffer 对象可以被修改。你可以在 StringBuffer 中追加、插入或删除字符,而不需要创建新的对象。

在 Java 中使用 StringBuffer 的示例:

StringBuffer sb = new StringBuffer();
sb.append("Hello"); // 在 StringBuffer 中追加 "Hello"
sb.append(" World"); // 在 StringBuffer 中追加 " World"
System.out.println(sb.toString()); // 输出 "Hello World"

sb.insert(5, " Java"); // 在索引 5 处插入 " Java"
System.out.println(sb.toString()); // 输出 "Hello Java World"

sb.delete(6, 10); // 删除索引 6 到索引 9 的字符
System.out.println(sb.toString()); // 输出 "HelloWorld"

sb.reverse(); // 反转 StringBuffer
System.out.println(sb.toString()); // 输出 "dlroWolleH"

StringBuffer 类提供了各种方法来操作缓冲区的内容,例如 appendinsertdeletereplacereverse 等。

Selecter

Selector(选择器)是Java NIO(New I/O)中的一个关键组件,用于多路复用非阻塞I/O操作。它用于监视一组通道的状态并判断通道是否准备好进行读取或写入操作。

在Java NIO中,可以通过Selector同时管理多个通道(如SocketChannel或ServerSocketChannel),并通过一个线程对这些通道进行监控。这样就可以在一个线程中处理多个通道的I/O操作,提高了系统的性能和资源利用率。

使用Selector的基本步骤:

  1. 创建一个Selector对象:Selector selector = Selector.open();
  2. 将通道注册到选择器上:channel.register(selector, ops);ops表示感兴趣的操作,如SelectionKey.OP_READ表示对读操作感兴趣。
  3. 不断循环,调用selector.select()方法等待就绪的通道。
  4. 一旦某个通道就绪,可以通过selector.selectedKeys()方法获取就绪的通道集合(SelectionKey集合)。
  5. 遍历就绪的通道集合,执行相应的I/O操作。

使用Selector可以实现高效的事件驱动的非阻塞I/O模型,适用于需要同时处理多个通道的应用场景,如服务器端编程。通过选择器,可以避免为每个通道创建一个线程,从而减少线程的开销和系统资源的消耗。

SocketChannel

SocketChannel 是 Java NIO(New I/O)中用于网络通信的通道之一。它提供了基于TCP协议的可读写的双向数据传输通道。

SocketChannel 继承自 SelectableChannel 类,因此可以与 Selector 配合使用,实现非阻塞的网络通信。

SocketChannel 的基本用法:

  1. 打开一个 SocketChannel:可以使用 SocketChannel.open() 静态方法来创建一个新的 SocketChannel 对象。
  2. 连接到远程服务器:使用 connect() 方法连接到远程服务器,可以指定服务器的 IP 地址和端口号。
  3. 读取和写入数据:通过 read()write() 方法从 SocketChannel 中读取和写入数据。这些方法使用 ByteBuffer 对象作为数据的读写缓冲区。
  4. 关闭通道:使用 close() 方法关闭 SocketChannel

简单示例代码,如何使用 SocketChannel 进行网络通信:

// 创建 SocketChannel
SocketChannel socketChannel = SocketChannel.open();

// 连接到远程服务器
socketChannel.connect(new InetSocketAddress("example.com", 8080));

// 发送数据
String message = "Hello, Server!";
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer);

// 接收数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(readBuffer);
if (bytesRead > 0) {
    readBuffer.flip();
    byte[] data = new byte[bytesRead];
    readBuffer.get(data);
    String response = new String(data);
    System.out.println("Received: " + response);
}

// 关闭 SocketChannel
socketChannel.close();

SocketChannel 提供了许多其他方法,例如设置非阻塞模式、获取底层的套接字等。它是进行网络编程时常用的工具之一,可用于实现客户端和服务器端的网络通信。

System.currentTimeMillis();

currentTimeMillis 是一个静态方法,属于 System 类的一部分,用于获取当前的系统时间(以毫秒为单位)。它返回自 1970 年 1 月 1 日 00:00:00 GMT(格林尼治标准时间)以来经过的毫秒数。

在Java中,可以使用 System.currentTimeMillis() 方法来获取当前时间的毫秒表示。该方法返回一个 long 类型的值,表示当前时间与1970年1月1日之间的时间差。

使用 currentTimeMillis 方法的示例:

long currentTime = System.currentTimeMillis();
System.out.println("Current time in milliseconds: " + currentTime);

输出类似于:

Current time in milliseconds: 1631234567890

currentTimeMillis 方法在许多情况下非常有用,例如计算程序的执行时间、生成不重复的时间戳或记录时间戳等。注意,该方法返回的是系统的当前时间,并不具备高精度性,因此不适合用于测量较小时间间隔的操作。对于需要更高精度的时间操作,可以考虑使用 java.time 包中的类,如 InstantLocalDateTime

SelectionKey()

在 Java NIO 的 Selector 中,当调用 select() 方法后,可以通过 selectedKeys() 方法获取就绪的通道集合,该集合中的每个元素都是一个 SelectionKey 对象,代表一个就绪的通道。

通过调用 iterator() 方法,我们可以获得这个就绪通道集合的迭代器,然后可以使用迭代器的方法(如 hasNext()next())来迭代遍历集合中的每个 SelectionKey 对象。

示例代码,如何使用迭代器遍历就绪通道集合:

Iterator<SelectionKey> iter = this.selector.selectedKeys().iterator();

while (iter.hasNext()) {
    SelectionKey key = iter.next();
    
    // 处理就绪的通道
    if (key.isReadable()) {
        // 处理可读事件
    } else if (key.isWritable()) {
        // 处理可写事件
    }
    
    // 处理完毕后,需要将该就绪的通道从集合中移除
    iter.remove();
}

示例通过 hasNext() 方法判断是否还有下一个就绪通道,然后使用 next() 方法获取下一个 SelectionKey 对象。在处理完通道后,需要使用迭代器的 remove() 方法将该就绪通道从集合中移除,以确保下一次 select() 方法只返回新的就绪通道。

使用迭代器遍历就绪通道集合可以对每个通道执行相应的操作,例如读取数据、写入数据或者处理其他事件。

TransferSocketClient分析

1、监听socket通道

  1. 初始化 Socket 客户端并连接到服务器。

  2. 打印服务器启动信息。

  3. 进入无限循环,监听与服务器的通信。

  4. 超时返回条件判断:如果距离上次接收心跳消息的时间和上次接收消息的时间都超过了 3 倍心跳周期的时间,则打印警告信息,关闭通道,并返回。

  5. 如果通道已连接:

    • 判断是否需要发送心跳消息:如果当前时间超过上次发送心跳消息的时间加上心跳周期的时间,则设置需要发送心跳响应的标志,并打印相关信息。
  6. 调用 Selector 的 select 方法等待事件发生,最长等待时间为 1000 毫秒(1 秒)。

  7. 获取就绪的事件集合。

  8. 遍历处理就绪的事件:

    • 如果事件是可连接的:

      • 获取通道,并判断通道是否处于连接挂起状态,如果是,则完成连接。

      • 将通道配置为非阻塞模式,并将通道注册到 Selector 上,关注读事件。

      • 创建登录信息对象,并将其转换为 JSON 字符串。

      • 将登录信息写入通道。

    • 如果事件是可读的:

      • 初始化一个变量 num 并将其初始值设为 0。
      • 调用 read 方法从通道中读取数据,并将实际读取的字节数保存到 num 变量中。
      • 如果读取的字节数小于 0,表示连接已关闭:
        • 等待 5 秒钟。
        • 返回方法。
  9. 继续下一轮循环,等待下一次与服务器的通信。

/**
 * 监听方法,用于处理与服务器的通信
 *
 * @throws Exception 异常
 */
public void listen() throws Exception {
    // 初始化 Socket 客户端并连接到服务器
    initSocketClient(serverIp, serverPort);

    // 打印服务器启动信息
    System.out.println("<ALARM>\n" + "server" + serverIp + ", started." + "\n</ALARM>");

    while (true) {
        // 超时返回条件判断
        if ((System.currentTimeMillis() > this.lastRecieveHeartBeatTime + 3 * this.heart_beat_period * 1000)
            && (System.currentTimeMillis() > this.lastRecieveTime + 3 * this.heart_beat_period * 1000)) {
            System.out.println("<ALARM>\n" + "server " + this.serverIp + "long time no msg recieved, close connection, waiting for reconnect" + "\n</ALARM>");
            this.channel.close();
            return;
        }

        // 连接已建立的通道
        if (this.channel.isConnected()) {
            // 判断是否需要发送心跳消息
            if (System.currentTimeMillis() > this.lastSendHeartBeatTime + this.heart_beat_period * 1000) {
                this.lastSendHeartBeatTime = System.currentTimeMillis();
                this.needHeartBeatRespon = true;
                System.out.println("this.channel.isConnected : " + this.needHeartBeatRespon);
            }
        }

        // 调用 Selector 的 select 方法,等待事件发生,最长等待时间为 1000 毫秒(1 秒)
        selector.select(1000);

        // 获取就绪的事件集合
        Iterator<SelectionKey> iter = this.selector.selectedKeys().iterator();

        // 遍历处理就绪的事件
        while (iter.hasNext()) {
            SelectionKey key = (SelectionKey) iter.next();
            iter.remove();

            // 如果事件是可连接的
            if (key.isConnectable()) {
                SocketChannel channel = (SocketChannel) key.channel();

                // 如果通道处于连接挂起状态,完成连接
                if (channel.isConnectionPending()) {
                    channel.finishConnect();
                }

                // 配置通道为非阻塞模式,并将通道注册到 Selector 上,关注读事件
                channel.configureBlocking(false);
                channel.register(this.selector, SelectionKey.OP_READ);

                // 创建登录信息对象
                Login login = new Login();
                login.setUser(this.userName);
                login.setPasswd(this.passWord);
                String loginJson = JSON.toJSONString(login);

                // 将登录信息写入通道
                channel.write(ByteBuffer.wrap(loginJson.getBytes()));
            }
            // 如果事件是可读的
            else if (key.isReadable()) {
                // 如果通道可读
                int num = 0;
                // 调用 read 方法读取数据
                num = read(key);
                // 检查读取结果
                if (num < 0) {
                    // 如果读取结果小于 0,表示连接已关闭
                    try {
                        // 等待 5 秒钟
                        Thread.sleep(5 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 返回,结束当前方法的执行
                    return;
                }
            }
        }
    }
}

2、读取socket返回报文

1.通过 SelectionKey 对象获取与其关联的 SocketChannel

2.创建一个 ByteBuffer 缓冲区,大小为 300,000 字节。

3.调用 channel.read(buffer) 方法从通道中读取数据,并将读取的字节数保存在 num 变量中。

  • 如果 num 大于 0,表示成功读取了数据。在这种情况下,根据读取的字节数创建一个字节数组 data,并从缓冲区中复制数据到 data 数组中。然后,对 data 数组进行解码处理,并打印输出其内容。

  • 如果 num 等于 -1,表示连接已关闭。在这种情况下,取消 SelectionKey 的注册,并关闭对应的 SocketChannel。还会打印输出一个关于服务器关闭连接的警告信息。

4.返回 num,即读取的字节数。

/**
* 从通道中读取数据
*
* @param key 选择键对象
* @return 读取的字节数
* @throws Exception 异常
*/
public int read(SelectionKey key) throws Exception {
    // 获取与选择键关联的 SocketChannel
    SocketChannel channel = (SocketChannel) key.channel();

    // 创建一个 ByteBuffer 缓冲区,大小为 300,000 字节
    ByteBuffer buffer = ByteBuffer.allocate(300000);

    // 读取的字节数
    int num = 0;

    // 从通道中读取数据到缓冲区
    num = channel.read(buffer);

    // 如果读取的字节数大于 0,表示成功读取了数据
    if (num > 0) {
        // 创建一个字节数组,用于保存读取的数据
        byte[] data = new byte[num];

        // 从缓冲区中获取字节数组
        byte[] recBytes = buffer.array();

        // 将读取的数据复制到 data 数组中
        System.arraycopy(recBytes, 0, data, 0, num);

        // 对读取的数据进行解码处理
        decodeMsg(data);

        // 打印输出读取的数据内容(去除首尾的空白字符)
        System.out.println(new String(data).trim());
    }
    // 如果读取的字节数等于 -1,表示连接已关闭
    else if (num == -1) {
        // 取消选择键的注册
        key.cancel();

        // 关闭 SocketChannel
        channel.close();

        // 打印输出服务器关闭连接的警告信息
        System.out.println("<ALARM>\n" + "server " + this.serverIp + " close connection, waiting for reconnect" + "\n</ALARM>");
    }

    // 返回读取的字节数
    return num;
}

3、截取告警信息,解析告警信息封装到告警

实现解码消息的方法,用于将字节数组解码为告警信息对象

  1. 将传入的字节数组数据放入剩余缓冲区 (remainBuffer) 中。
  2. 切换剩余缓冲区为读模式,以准备读取数据。
  3. 尝试从剩余缓冲区中读取告警字符串,调用 tryReadAlarmFromBuffer() 方法。
  4. 将读取到的告警字符串转换为告警类对象,调用 alarmStrToAlarmObj() 方法。

tryReadAlarmFromBuffer() 方法的实现如下:

  1. 初始化一个空字符串 alarmStr
  2. 循环读取剩余缓冲区中的数据,直到剩余缓冲区中的数据长度小于 16 个字节(假设告警字符串的最小长度为 16)。
  3. 将剩余缓冲区中的数据转换为字符串,从索引 0 开始读取剩余缓冲区中的所有数据。
  4. 切换剩余缓冲区为写模式,为下次写入数据做准备。
  5. 返回读取到的告警字符串。
/**
 * 解码消息方法,用于将字节数组解码为告警信息对象
 *
 * @param data 字节数组
 * @throws Exception 异常
 */
private void decodeMsg(byte[] data) throws Exception {
    remainBuffer.put(data); // 将数据放入剩余缓冲区

    remainBuffer.flip(); // 切换为读模式,准备读取数据

    String alarmStr = tryReadAlarmFromBuffer(); // 尝试从缓冲区中读取告警字符串

    alarmStrToAlarmObj(alarmStr); // 将告警字符串封装为告警类对象
}

/**
 * 尝试从缓冲区中读取告警字符串
 *
 * @return 告警字符串
 * @throws IOException IO 异常
 */
private String tryReadAlarmFromBuffer() throws IOException {
    String alarmStr = "";

    while (remainBuffer.remaining() >= 16) {
        alarmStr = new String(remainBuffer.array(), 0, remainBuffer.remaining()); // 将剩余缓冲区中的数据转换为字符串
    }

    remainBuffer.compact(); // 切换为写模式,为下次写入数据做准备

    return alarmStr;
}

4、实现线程的 run 方法。

run 方法中,使用一个无限循环 while (true) 来执行下面的代码块。

首先,设置 lastSendHeartBeatTimelastRecieveTimelastRecieveHeartBeatTime 的值为当前时间戳,用于记录最后发送心跳、最后接收时间和最后接收心跳的时间。

然后,通过 Thread.sleep(5 * 1000) 方法使线程暂停 5 秒。

接下来,调用 listen() 方法进行监听操作。

如果在 listen() 方法中捕获到 IOException 异常,则打印带有错误消息的日志,并打印异常堆栈跟踪。然后关闭连接通道(channel)、暂停线程 5 分钟,并继续下一次循环。

如果捕获到其他异常(Exception),则打印带有错误消息的日志,并打印异常堆栈跟踪。然后关闭连接通道(channel)、暂停线程 5 分钟,并继续下一次循环。

由于循环条件是 true,因此该线程将一直运行,不会自动结束。

@Override
public void run() {
    while (true) {
        try {
            // 设置最后发送心跳时间、最后接收时间和最后接收心跳时间为当前时间戳
            this.lastSendHeartBeatTime = System.currentTimeMillis();
            this.lastRecieveTime = System.currentTimeMillis();
            this.lastRecieveHeartBeatTime = System.currentTimeMillis();

            // 线程休眠 5 秒
            Thread.sleep(5 * 1000);

            // 执行监听操作
            listen();
        } catch (IOException e) {
            // 捕获 IOException 异常
            System.out.println("<ALARM>\n" + "server " + this.serverIp + e.getMessage() + "\n</ALARM>");
            e.printStackTrace();

            try {
                // 关闭连接通道
                this.channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }

            try {
                // 线程休眠 5 分钟
                Thread.sleep(5 * 60 * 1000);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        } catch (Exception ex) {
            // 捕获其他异常
            System.out.println("<ALARM>\n" + "server " + this.serverIp + ex.getMessage() + "\n</ALARM>");
            ex.printStackTrace();

            try {
                // 关闭连接通道
                this.channel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }

            try {
                // 线程休眠 5 分钟
                Thread.sleep(5 * 60 * 1000);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
    }
}

TransferSocketConsumer分析

实现 Kafka 消费者。

首先,定义一个名为 TransferSocketConsumer 的类。

使用 @KafkaListener 注解标记了 allNodeKafkaMsg 方法,表示该方法是一个 Kafka 消息监听器,用于监听指定的 Kafka 主题。

@KafkaListener 注解的参数包括:

  • topics:要监听的 Kafka 主题,使用占位符 ${com.ai.kafka.topic.alarm.autodeal.consumer} 从配置文件中读取该值。
  • concurrency:并发度,使用占位符 ${spring.kafka.listener.concurrency} 从配置文件中读取该值。
  • groupId:消费者组 ID,使用占位符 ${group.id:real_alarm_to_center_for_hlj} 从配置文件中读取该值。如果配置文件中不存在该值,则使用默认值 real_alarm_to_center_for_hlj

allNodeKafkaMsg 方法接收一个 List<ConsumerRecord<String, String>> 类型的参数 rds,表示一批从 Kafka 主题接收到的消费者记录。

在方法内部,通过一个 for 循环遍历每个消费者记录。

在循环内部,通过 record.topic() 获取当前记录的 Kafka 主题,通过 record.value() 获取当前记录的消息内容。

首先,通过 StrUtil.isNotBlank(alarmMsg) 检查消息内容是否为非空白字符串。

如果消息内容是非空白字符串,则使用 log.info 方法将消息内容记录到日志。

然后,尝试执行以下操作:

  • 创建一个 TransferSocketClient 实例,并指定 IP 地址为 172.20.27.66,端口号为 3001,其他参数为 null
  • 调用 transferSocketClient.start() 方法启动 TransferSocketClient
  • 使用 transferSocketClient.getAlarm() 方法获取 TransferAlarm 对象。
  • 使用 TransferAlarmPrint.print 方法打印 TransferAlarm 对象,指定 IP 地址为 172.20.27.66

如果在执行上述操作时捕获到异常,则捕获异常并使用 log.info 方法记录错误消息到日志。

如果在整个过程中捕获到异常,则捕获异常并使用 log.error 方法记录带有 Kafka 主题和消息内容的错误消息到日志。

public class TransferSocketConsumer {

    // 使用@KafkaListener注解,指定监听的Kafka主题,以及并发度和消费者组ID
    @KafkaListener(
        topics = {"${com.ai.kafka.topic.alarm.autodeal.consumer}"},
        concurrency = "${spring.kafka.listener.concurrency}",
        groupId = "${group.id:real_alarm_to_center_for_hlj}"
    )
    public void allNodeKafkaMsg(List<ConsumerRecord<String, String>> rds) {
        for (ConsumerRecord<String, String> record : rds) {
            String kafkaTopic = record.topic(); // 获取当前记录的Kafka主题
            String alarmMsg = record.value(); // 获取当前记录的消息内容
            try {
                if (StrUtil.isNotBlank(alarmMsg)) { // 检查消息内容是否非空白字符串
                    log.info(">>>>>> Kafka " + alarmMsg); // 记录消息内容到日志
                    try {
                        // 创建TransferSocketClient实例,并指定IP地址、端口号等参数
                        TransferSocketClient transferSocketClient = new TransferSocketClient("172.20.27.66","3001",null,null);
                        transferSocketClient.start(); // 启动TransferSocketClient
                        TransferAlarm transferAlarm = transferSocketClient.getAlarm(); // 获取TransferAlarm对象
                        TransferAlarmPrint.print(transferAlarm,"172.20.27.66"); // 打印TransferAlarm对象
                    } catch (Exception e) {
                        // 捕获异常,并记录错误消息到日志
                        log.info("<ALARM>\n" + "parserror server " + e.getMessage() + "\n</ALARM>");
                    }
                }
            } catch (Exception ex) {
                // 捕获异常,并记录带有Kafka主题和消息内容的错误消息到日志
                log.error(">>>>>> Kafka Topic: " + kafkaTopic + ", Message: " + alarmMsg + ",", ex);
            }
        }
    }
}

TransferAlarmPrint分析

TransferAlarm对象转换为一段格式化的报警信息,并将其打印到控制台。

报警信息的格式类似于XML,使用了一些特殊的分隔符来表示字段名和字段值的关系。

如果在转换过程中发生异常,将会打印相应的错误信息到控制台。

最后返回一个报警序列号(alarmSequenceId

public class TransferAlarmPrint {

    public static synchronized int print(TransferAlarm transferAlarm, String serverIp) {
        int alarmSequenceId = 0;

        try {
            //TransferAlarm alarmObj = JSON.parseObject(alarm, TransferAlarm.class);

            // 获取 TransferAlarm 类的所有字段
            Field[] fields = transferAlarm.getClass().getDeclaredFields();

            // 创建一个 StringBuffer 对象,用于存储最终的报警信息
            StringBuffer stringBufferAlarm = new StringBuffer();
            stringBufferAlarm.append("<ALARM>\n");
            stringBufferAlarm.append("type~=~Alarm\n");

            // 遍历字段数组
            for (int j = 0; j < fields.length; j++) {
                // 设置字段的可访问性为 true,以便获取字段的值
                fields[j].setAccessible(true);

                // 将字段名和字段值添加到报警信息中
                stringBufferAlarm.append(fields[j].getName() + "=~");
                Object _Object = null;
                try {
                    // 获取字段的值
                    _Object = fields[j].get(transferAlarm);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

                // 检查字段值是否为 null,如果不为 null,则将其转换为字符串并添加到报警信息中
                if (_Object != null) {
                    stringBufferAlarm.append(_Object.toString() + "\n");
                } else {
                    // 如果字段值为 null,则添加 "null" 到报警信息中
                    stringBufferAlarm.append("null\n");
                }
            }

            // 添加服务器 IP 到报警信息中
            stringBufferAlarm.append("serverIp=~" + serverIp + "\n");
            stringBufferAlarm.append("</ALARM>");

            // 打印报警信息到控制台
            System.out.println(stringBufferAlarm.toString());

        } catch (JSONException e) {
            // 如果发生 JSONException 异常,则打印错误信息到控制台
            System.out.println("<ALARM>\n" + "parserror server " + serverIp + e.getMessage() + "\n</ALARM>");
            e.printStackTrace();
        } catch (Exception ex) {
            // 如果发生其他异常,则打印错误信息到控制台
            System.out.println("<ALARM>\n" + "parserror server " + serverIp + ex.getMessage() + "\n</ALARM>");
            ex.printStackTrace();
        }

        // 返回报警序列号
        return alarmSequenceId;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值