liusuyi
2026-06-01 3496700a5ba18be8ca0590a79e21a867782d1ea9
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
package com.ard.work.sdk.zlxd.netty.tcp;
 
import com.ard.common.core.constant.CacheConstants;
import com.ard.common.redis.service.RedisService;
import com.ard.work.api.domian.ArdCamera;
import com.ard.work.event.LoginEvent;
import com.ard.work.sdk.zlxd.netty.message.response.LoginResponse;
import com.ard.work.sdk.zlxd.service.ErrorCodeEnum;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationEventPublisher;
 
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * 自定义HTTP协议处理器(直接处理原始ByteBuf,无任何Netty HTTP编解码器依赖)
 * 适配设备双向HTTP报文:
 * 1. 设备主动发:POST * HTTP/1.1(业务请求/Heart_Beat心跳,Content-Type可text/html/application/xml)
 * 2. 设备响应发:HTTP/1.1 200 OK(客户端请求的应答)
 * 3. 序列号格式:CSeq: X POST
 * 4. 基于Content-Length+\\r\\n\\r\\n处理TCP粘包拆包
 */
@Slf4j
public class CustomHttpChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
    private final ApplicationEventPublisher eventPublisher;
 
    // 相机唯一ID(区分多相机)
    private final String cameraId;
    // TCP粘包拆包核心:缓存未解析的字节流
    private final ByteBuf cacheBuf = Unpooled.buffer();
    // 解析CSeq: X POST 中的数字X(正则匹配)
    private static final Pattern CSEQ_PATTERN = Pattern.compile("CSeq:\\s*(\\d+)\\s*POST", Pattern.CASE_INSENSITIVE);
    // 心跳标识:XML中的Message_Type="Heart_Beat"
    private static final String HEART_BEAT_FLAG = "Message_Type=\"Heart_Beat\"";
    // 心跳应答XML模板(匹配设备格式,填充序列号/会话ID)
    private static final String HEART_BEAT_RSP_TPL = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" +
            "<Message Version=\"1.0\">" +
            "    <Header Message_Type=\"Heart_Beat_Rsp\" Sequence_Number=\"%s\" Session_ID=\"%s\" Source_ID=\"\" " +
            "Destination_ID=\"\" />" +
            "    <result code=\"0\" />" +
            "</Message>";
 
    public CustomHttpChannelHandler(String cameraId, ApplicationEventPublisher eventPublisher) {
        this.cameraId = cameraId;
        this.eventPublisher = eventPublisher;
    }
 
    // ===================== 核心方法:处理所有原始ByteBuf字节流 =====================
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        // 1. 新字节流写入缓存,解决TCP粘包拆包
        cacheBuf.writeBytes(msg);
        // 2. 循环解析缓存,直到无完整HTTP包(粘包时一次解析多个,拆包时等待下一次)
        while (parseOneCompleteHttpPackage(ctx)) {
            // 空循环,核心逻辑在parseOneCompleteHttpPackage中
        }
    }
 
    // ===================== 关键方法:解析单个完整的HTTP包(核心逻辑)=====================
    private boolean parseOneCompleteHttpPackage(ChannelHandlerContext ctx) {
        // 1. 查找HTTP头和体的分隔符:\r\n\r\n(必须找到才是完整包的前提)
        int delimiterIndex = findDelimiterIndex(cacheBuf, "\r\n\r\n".getBytes(StandardCharsets.UTF_8));
        if (delimiterIndex == -1) {
            return false; // 无分隔符,等待下一次字节流
        }
 
        try {
            // 2. 读取并解析HTTP头(0 -> 分隔符索引)
            ByteBuf headerBuf = cacheBuf.readRetainedSlice(delimiterIndex);
            String headerStr = headerBuf.toString(StandardCharsets.UTF_8).trim();
            headerBuf.release();
            cacheBuf.skipBytes(4); // 跳过分隔符\r\n\r\n(4个字节)
 
            // 3. 解析Content-Length(必须有,否则无法获取体长度)
            int contentLength = parseContentLength(headerStr);
            if (contentLength <= 0) {
                log.warn("【相机{}】解析HTTP包失败:Content-Length无效 | 头内容:{}", cameraId, headerStr);
                return true;
            }
 
            // 4. 校验缓存体字节是否足够(拆包场景,等待下一次字节流)
            if (cacheBuf.readableBytes() < contentLength) {
                cacheBuf.readerIndex(0); // 重置读指针,等待后续字节流
                return false;
            }
 
            // 5. 读取HTTP体(严格按Content-Length读取,避免粘包)
            ByteBuf bodyBuf = cacheBuf.readRetainedSlice(contentLength);
            String bodyStr = bodyBuf.toString(StandardCharsets.UTF_8).trim();
            bodyBuf.release();
 
            // 6. 按首行区分【设备请求】和【设备响应】(核心区分逻辑,匹配你的报文)
            String firstLine = headerStr.split("\r\n")[0].trim();
            if (firstLine.startsWith("POST")) {
                // 设备主动发的请求(心跳/主动业务请求)→ 处理心跳/请求
                handleDeviceRequest(ctx, headerStr, bodyStr);
            } else if (firstLine.startsWith("HTTP/1.1")) {
                // 设备对客户端的响应(GetPtzInfo/GetDeviceState)→ 完成Future回调
                handleDeviceResponse(ctx, headerStr, bodyStr);
            } else {
                log.warn("【相机{}】解析到未知HTTP包,忽略 | 首行:{}", cameraId, firstLine);
            }
            return true;
        } catch (Exception e) {
            log.error("【相机{}】解析HTTP包异常,重置缓存", cameraId, e);
            cacheBuf.readerIndex(0); // 解析异常重置指针,避免后续解析全部失效
            return false;
        }
    }
 
    // ===================== 处理设备主动请求(心跳/主动业务请求)=====================
    private void handleDeviceRequest(ChannelHandlerContext ctx, String headerStr, String bodyStr) {
        // 解析核心参数:CSeq、Session_ID、Sequence_Number、Content-Type
        String cseqHeader = parseHeaderValue(headerStr, "CSeq");
        String sessionId = parseXmlAttr(bodyStr, "Session_ID=\"");
        String seqNum = parseXmlAttr(bodyStr, "Sequence_Number=\"");
        String contentType = parseHeaderValue(headerStr, "Content-Type");
 
        //log.debug("【相机{}】收到设备主动请求 | 首行:{} | CSeq:{} | Content-Type:{} | 体长度:{}",
               // cameraId, headerStr.split("\r\n")[0].trim(), cseqHeader, contentType, bodyStr.length());
 
        // 心跳识别&自动应答(核心:匹配XML中的Heart_Beat标识)
        if (bodyStr.contains(HEART_BEAT_FLAG)) {
            //log.debug("【相机{}】处理设备心跳 | CSeq:{} | Session_ID:{} | 开始发送应答",
                    //cameraId, cseqHeader, sessionId);
            // 填充心跳应答XML(匹配设备格式,序列号/会话ID和请求一致)
            String rspXml = String.format(HEART_BEAT_RSP_TPL,
                    StringUtils.isBlank(seqNum) ? "0" : seqNum,
                    StringUtils.isBlank(sessionId) ? "" : sessionId);
            // 手动拼接HTTP应答并发送
            sendHttpResponse(ctx, cseqHeader, rspXml);
            //log.debug("【相机{}】心跳应答发送成功 | CSeq:{} | 应答体长度:{}", cameraId, cseqHeader, rspXml.length());
            return;
        }
 
        // 扩展:处理设备其他主动业务请求(如有,在这里添加逻辑)
        log.info("相机【{}】收到设备主动业务请求 | 体内容:{}", cameraId, bodyStr);
    }
 
    // ===================== 处理设备响应(客户端请求的返回,完成Future回调)=====================
    private void handleDeviceResponse(ChannelHandlerContext ctx, String headerStr, String bodyStr) {
        // 1. 解析CSeq序列号(优先从HTTP头解析,兼容XML解析)
        Integer cseq = parseCSeq(headerStr);
        if (cseq == null) {
            cseq = parseSeqFromXml(bodyStr);
            if (cseq == null) {
                log.debug("【相机{}】收到设备响应,无有效CSeq,忽略 | 体内容:{}", cameraId, bodyStr);
                return;
            }
            //log.debug("【相机{}】响应头无有效CSeq,从XML解析到:{}", cameraId, cseq);
        }
 
        // 2. 空体校验(无效响应直接忽略)
        if (StringUtils.isBlank(bodyStr)) {
            log.debug("【相机{}】收到设备响应,体为空,忽略 | CSeq:{}", cameraId, cseq);
            return;
        }
 
        // 3. 完成Future回调(核心:和sendXmlAndWait中的唯一键匹配)
        String uniqueKey = cameraId + "_" + cseq;
        //log.debug("【相机{}】收到设备正常响应 | CSeq:{} | 体长度:{} | 完成Future回调", cameraId, cseq, bodyStr.length());
        ResponseFutureHolder.complete(uniqueKey, bodyStr);
    }
 
    // ===================== 手动拼接HTTP应答并发送(心跳/业务应答通用)=====================
    private void sendHttpResponse(ChannelHandlerContext ctx, String cseqHeader, String bodyStr) {
        try {
            byte[] bodyBytes = bodyStr.getBytes(StandardCharsets.UTF_8);
            // 拼接HTTP应答头(严格匹配设备的响应格式:HTTP/1.1 200 OK、CSeq一致)
            StringBuilder rspHeader = new StringBuilder();
            rspHeader.append("HTTP/1.1 200 OK\r\n");
            if (StringUtils.isNotBlank(cseqHeader)) {
                rspHeader.append("CSeq: ").append(cseqHeader).append("\r\n");
            }
            rspHeader.append("Content-Length: ").append(bodyBytes.length).append("\r\n");
            rspHeader.append("Content-Type: application/xml; charset=UTF-8\r\n");
            rspHeader.append("\r\n"); // 头和体的分隔符
 
            // 拼接完整应答字节流(头+体),发送原始ByteBuf
            byte[] rspBytes = (rspHeader.toString() + bodyStr).getBytes(StandardCharsets.UTF_8);
            ByteBuf rspBuf = Unpooled.copiedBuffer(rspBytes);
            ctx.writeAndFlush(rspBuf).addListener(f -> {
                if (!f.isSuccess()) {
                    log.error("【相机{}】发送HTTP应答失败 | CSeq:{}", cameraId, cseqHeader, f.cause());
                }
            });
        } catch (Exception e) {
            log.error("【相机{}】拼接/发送HTTP应答异常", cameraId, e);
        }
    }
 
    // ===================== 工具方法:解析HTTP头中的Content-Length =====================
    private int parseContentLength(String headerStr) {
        String[] headerLines = headerStr.split("\r\n");
        for (String line : headerLines) {
            if (line.trim().toLowerCase().startsWith("content-length:")) {
                try {
                    return Integer.parseInt(line.split(":", 2)[1].trim());
                } catch (Exception e) {
                    log.warn("【相机{}】解析Content-Length失败 | 行:{}", cameraId, line);
                }
            }
        }
        return -1;
    }
 
    // ===================== 工具方法:解析HTTP头中的指定字段值(大小写不敏感)=====================
    private String parseHeaderValue(String headerStr, String headerKey) {
        String[] headerLines = headerStr.split("\r\n");
        String lowerKey = headerKey.toLowerCase() + ":";
        for (String line : headerLines) {
            if (line.trim().toLowerCase().startsWith(lowerKey)) {
                return line.split(":", 2)[1].trim();
            }
        }
        return null;
    }
 
    // ===================== 工具方法:解析CSeq: X POST 中的数字X =====================
    private Integer parseCSeq(String headerStr) {
        String cseqHeader = parseHeaderValue(headerStr, "CSeq");
        if (StringUtils.isBlank(cseqHeader)) {
            return null;
        }
        Matcher matcher = CSEQ_PATTERN.matcher(cseqHeader);
        if (matcher.find()) {
            try {
                return Integer.parseInt(matcher.group(1));
            } catch (Exception e) {
                log.warn("【相机{}】解析CSeq失败 | 值:{}", cameraId, cseqHeader);
            }
        }
        return null;
    }
 
    // ===================== 工具方法:从XML中解析指定属性值(如Session_ID="123" → 123)=====================
    private String parseXmlAttr(String xml, String attrPrefix) {
        if (StringUtils.isBlank(xml) || StringUtils.isBlank(attrPrefix)) {
            return null;
        }
        int startIdx = xml.indexOf(attrPrefix);
        if (startIdx == -1) {
            return null;
        }
        startIdx += attrPrefix.length();
        int endIdx = xml.indexOf("\"", startIdx);
        if (endIdx == -1 || startIdx >= endIdx) {
            return null;
        }
        return xml.substring(startIdx, endIdx).trim();
    }
 
    // ===================== 工具方法:从XML中解析Sequence_Number为数字 =====================
    private Integer parseSeqFromXml(String xml) {
        String seqStr = parseXmlAttr(xml, "Sequence_Number=\"");
        if (StringUtils.isBlank(seqStr)) {
            return null;
        }
        try {
            return Integer.parseInt(seqStr);
        } catch (Exception e) {
            log.warn("【相机{}】从XML解析Sequence_Number失败 | 值:{}", cameraId, seqStr);
            return null;
        }
    }
 
    // ===================== 工具方法:查找字节流中的分隔符索引(如\r\n\r\n)=====================
    private int findDelimiterIndex(ByteBuf buf, byte[] delimiter) {
        int readerIndex = buf.readerIndex();
        int readableBytes = buf.readableBytes();
        int delimiterLen = delimiter.length;
 
        for (int i = 0; i <= readableBytes - delimiterLen; i++) {
            boolean match = true;
            for (int j = 0; j < delimiterLen; j++) {
                if (buf.getByte(readerIndex + i + j) != delimiter[j]) {
                    match = false;
                    break;
                }
            }
            if (match) {
                return i;
            }
        }
        return -1;
    }
 
    // ===================== 通道事件处理(空闲/断连/异常)=====================
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 30秒读空闲 → 判定设备断连,关闭通道
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.error("【相机{}】30秒读空闲,判定设备断连,关闭通道", cameraId);
                ctx.close();
                // 清理该相机的所有未完成Future,避免内存泄漏
                ResponseFutureHolder.clearByCamera(cameraId);
            }
        }
        super.userEventTriggered(ctx, evt);
    }
 
    // 通道断开 → 清理Future
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.warn("【相机{}】TCP通道已断开", cameraId);
        NettyTcpClient.CAMERA_CHANNEL_MAP.remove(cameraId);
        ResponseFutureHolder.clearByCamera(cameraId);
        handleTcpConnectFail();
        super.channelInactive(ctx);
    }
 
    /**
     * 断开处理事件
     */
    private void handleTcpConnectFail() {
        ArdCamera camera = new ArdCamera();
        camera.setId(cameraId);
        camera.setChanNum(0);
        camera.setLoginId(-1L);
        camera.setState("0");
        camera.setChannelList(null);
        eventPublisher.publishEvent(new LoginEvent(camera));
        log.warn("设备[{}]TCP连接断开", cameraId);
    }
 
    // 通道异常 → 关闭通道+清理Future
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("【相机{}】TCP通道发生异常,关闭通道", cameraId, cause);
        NettyTcpClient.CAMERA_CHANNEL_MAP.remove(cameraId);
        ResponseFutureHolder.clearByCamera(cameraId);
        ctx.close();
    }
 
    // 处理器移除 → 释放缓存字节流,避免内存泄漏
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        if (cacheBuf.isReadable()) {
            cacheBuf.release();
        }
        super.handlerRemoved(ctx);
    }
}