suricata的flow流会话管理分析1

news2024/11/25 9:42:55

《suricata中的线程管理分析》一文中,我们看到suricata中有FlowWorker和FlowManager两个线程来处理流表,说明流表的实现应该不简单,果然,看了流相关的这块代码后,发现确实有点复杂,代码估计得慢慢坑,今天我们就来个对流表的初步分析,细节上咱们先放一放。

1、流表数据结构的初始化:

流表的初始化在FlowInitConfig函数中,函数调用栈如下:

main
    -->SuricataMain
        -->PostConfLoadedSetup
            -->PreRunInit
                -->FlowInitConfig(流相关数据结构初始化)
                    -->FlowQueueInit(flow_recycle_q流回收队列初始化)
                    -->FlowSparePoolInit(flow_spare_pool流节点预分配链表初始化)
                    -->FlowInitFlowProto(设置不同协议不同状态的超时时间)

FlowInitConfig主要是读取配置,对流表的配置结构体FlowConfig flow_config进行初始化、flow_recycle_q流回收队列初始化、flow_spare_pool流节点预分配链表初始化以及给不同协议不同状态设置不同的超时时间。

flow_spare_pool流节点预分配链表会根据配置事先分配一定数量的flow节点,这些flow节点按比例挂在flow_spare_pool链表的每个节点上。创建流表时,先优先从这些节点上获取flow。

2、收包线程FlowWorker模块对流表的处理:

收包线程FlowWorker模块对流表进行处理的函数调用栈如下:

FlowWorker
    -->FlowHandlePacket
        -->FlowGetFlowFromHash
            -->FlowGetNew(新建流)
                -->FlowQueuePrivateGetFromTop(从flow_spare_pool中申请流)
                -->FlowAlloc(flow_spare_pool不够,直接alloc申请)
            -->MoveToWorkQueue(已有流超时的,从哈希桶中删除,并放入work_queue或者evicted链表)
    -->FlowWorkerProcessInjectedFlows(取出flow_queue中的flow放入到work_queue)
    -->FlowWorkerProcessLocalFlows
        -->CheckWorkQueue
            -->FlowClearMemory(清除流信息)
            -->FlowSparePoolReturnFlow(将流归还到flow_spare_pool中)

流表的哈希表本身并不复杂,所有的流节点都是通过全局的FlowBucket *flow_hash哈希桶管理起来的,哈希桶的大小就是FlowInitConfig的hash_size,收包线程收到报文后,根据报文的流的哈希值(以五元组+vlan为key),先查流表,如果没有就创建流表项,如果有就找到对应的流节点。

这里面复杂的几个地方如下:

1)如果包对应的哈希链没有节点,需要获取流节点:先从上面讲到的flow_spare_pool链表中获取,获取不到就看能不能重用,不能重用就直接调用alloc自己创建一个节点。

2)如果哈希链有节点,则遍历链,看有没有超时的,超时就从哈希链中删除,放入work_queue或者evicted链表

3)从flow_queue中取出flow放入到work_queue,flow_queue是FlowManager线程处理时,发现的超时流需要重组时放入的

4)检查work_queue,清楚流节点信息,将流归还到flow_spare_pool链表中。

关键代码加注释如下:

Flow *FlowGetFlowFromHash(ThreadVars *tv, FlowLookupStruct *fls, Packet *p, Flow **dest)
{
    Flow *f = NULL;

/* get our hash bucket and lock it */
const uint32_t hash = p->flow_hash;  //Decode协议解码时已经计算好的哈希值
    FlowBucket *fb = &flow_hash[hash % flow_config.hash_size]; //查找到对应的哈希链
    FBLOCK_LOCK(fb); //每个哈希链一个锁

    SCLogDebug("fb %p fb->head %p", fb, fb->head);

/* see if the bucket already has a flow */
if (fb->head == NULL) {  //哈希链为空
        f = FlowGetNew(tv, fls, p); //新建流
if (f == NULL) {
            FBLOCK_UNLOCK(fb);
return NULL;
        }

/* flow is locked */
        fb->head = f;

/* got one, now lock, initialize and return */
        FlowInit(f, p);
        f->flow_hash = hash;
        f->fb = fb;
        FlowUpdateState(f, FLOW_STATE_NEW);

        FlowReference(dest, f); //给包的流指针赋值

        FBLOCK_UNLOCK(fb);
return f;
    }

const bool emerg = (SC_ATOMIC_GET(flow_flags) & FLOW_EMERGENCY) != 0;
const uint32_t fb_nextts = !emerg ? SC_ATOMIC_GET(fb->next_ts) : 0;
/* ok, we have a flow in the bucket. Let's find out if it is our flow */
    Flow *prev_f = NULL; /* previous flow */
    f = fb->head;
do {  //遍历哈希桶fb
        Flow *next_f = NULL;  //先判断每个flow节点是否超时
const bool timedout = (fb_nextts < (uint32_t)SCTIME_SECS(p->ts) &&
                               FlowIsTimedOut(f, (uint32_t)SCTIME_SECS(p->ts), emerg));
if (timedout) {
            FLOWLOCK_WRLOCK(f);
            next_f = f->next;  //超时Flow从Flow哈希表中移除
            MoveToWorkQueue(tv, fls, fb, f, prev_f);
            FLOWLOCK_UNLOCK(f);
goto flow_removed;
        } else if (FlowCompare(f, p) != 0) {  //查找流节点
            FLOWLOCK_WRLOCK(f);
/* found a matching flow that is not timed out */
if (unlikely(TcpSessionPacketSsnReuse(p, f, f->protoctx) == 1)) {
                Flow *new_f = TcpReuseReplace(tv, fls, fb, f, hash, p);
if (prev_f == NULL) /* if we have no prev it means new_f is now our prev */
                    prev_f = new_f;
                MoveToWorkQueue(tv, fls, fb, f, prev_f); /* evict old flow */
                FLOWLOCK_UNLOCK(f); /* unlock old replaced flow */

if (new_f == NULL) {
                    FBLOCK_UNLOCK(fb);
return NULL;
                }
                f = new_f;
            }
            FlowReference(dest, f);  //给包的流节点赋值
            FBLOCK_UNLOCK(fb);
return f; /* return w/o releasing flow lock */  //流节点已存在
        }
/* unless we removed 'f', prev_f needs to point to
         * current 'f' when adding a new flow below. */
        prev_f = f;
        next_f = f->next;

flow_removed:
if (next_f == NULL) {  //哈希链中没有找到该包对应的flow
            f = FlowGetNew(tv, fls, p);
if (f == NULL) {
                FBLOCK_UNLOCK(fb);
return NULL;
            }

/* flow is locked */

            f->next = fb->head;
            fb->head = f;

/* initialize and return */
            FlowInit(f, p);
            f->flow_hash = hash;
            f->fb = fb;
            FlowUpdateState(f, FLOW_STATE_NEW);
            FlowReference(dest, f);
            FBLOCK_UNLOCK(fb);
return f;
        }
        f = next_f;
    } while (f != NULL);

/* should be unreachable */
    BUG_ON(1);
return NULL;
}





/**
 *  \brief Get a new flow
 *
 *  Get a new flow. We're checking memcap first and will try to make room
 *  if the memcap is reached.
 *
 *  \param tv thread vars
 *  \param fls lookup support vars
 *
 *  \retval f *LOCKED* flow on succes, NULL on error.
 */
static Flow *FlowGetNew(ThreadVars *tv, FlowLookupStruct *fls, Packet *p)
{
    const bool emerg = ((SC_ATOMIC_GET(flow_flags) & FLOW_EMERGENCY) != 0);
#ifdef DEBUG
    if (g_eps_flow_memcap != UINT64_MAX && g_eps_flow_memcap == p->pcap_cnt) {
        return NULL;
    }
#endif
    if (FlowCreateCheck(p, emerg) == 0) {
        return NULL;
    }

    /* get a flow from the spare queue */
    Flow *f = FlowQueuePrivateGetFromTop(&fls->spare_queue); //先线程的spare_queue中申请
    if (f == NULL) {
        f = FlowSpareSync(tv, fls, p, emerg); //从全局flow_spare_pool中申请
    }
    if (f == NULL) {  //检查流表分配空间是否使用完
        /* If we reached the max memcap, we get a used flow */
        if (!(FLOW_CHECK_MEMCAP(sizeof(Flow) + FlowStorageSize()))) {
            /* declare state of emergency */
            if (!(SC_ATOMIC_GET(flow_flags) & FLOW_EMERGENCY)) {
                SC_ATOMIC_OR(flow_flags, FLOW_EMERGENCY);
                FlowTimeoutsEmergency();  //流表空间已使用完,进入紧急模式,回收存在时间较长的一些流表
                FlowWakeupFlowManagerThread(); //启动老化线程
            }
            //找到某个索引项下面存在时长最长的那个流,复用其内存(引用计数为0)
            f = FlowGetUsedFlow(tv, fls->dtv, p->ts);
            if (f == NULL) {
                NoFlowHandleIPS(p);
                return NULL;
            }
#if 0
            if (tv != NULL && fls->dtv != NULL) {
#endif
                StatsIncr(tv, fls->dtv->counter_flow_get_used);
#if 0
            }
#endif
            /* flow is still locked from FlowGetUsedFlow() */
            FlowUpdateCounter(tv, fls->dtv, p->proto);
            return f;
        }

        /* now see if we can alloc a new flow */
        f = FlowAlloc();  //流表空间没有使用完,直接申请
        if (f == NULL) {
#if 0
            if (tv != NULL && fls->dtv != NULL) {
#endif
                StatsIncr(tv, fls->dtv->counter_flow_memcap);
#if 0
            }
#endif
            NoFlowHandleIPS(p);
            return NULL;
        }

        /* flow is initialized but *unlocked* */
    } else {
        /* flow has been recycled before it went into the spare queue */

        /* flow is initialized (recylced) but *unlocked* */
    }

    FLOWLOCK_WRLOCK(f);
    FlowUpdateCounter(tv, fls->dtv, p->proto);
    return f;
}

3、FlowManager线程对流表的处理:

FlowManager线程进行流表处理的函数调用栈如下:

FlowManager
    -->FlowTimeoutHash(遍历指定范围哈希表)
        -->FlowManagerHashRowTimeout
            -->FlowManagerFlowTimeout(检查流老化时间)
            -->FlowQueuePrivateAppendFlow(超时流放入Aside_queue临时队列)
        -->FlowManagerHashRowClearEvictedList(遍历Evicted链表)
            -->FlowQueuePrivateAppendFlow(老化流放入Aside_queue临时队列)
        -->ProcessAsideQueue(Aside_queue临时队列出队,超时流需要重组的,放入tv->flow_queue,其它放入flow_recycle_q全局回收队列)

FlowManager线程的主要任务是检查流是否有超时流需要老化删除,如果有,就放入Aside_queue临时队列,另外在收包线程中也将超时的流放入到Aside_queue临时队列了,所以要一起处理一下,处理的结果就是将超时流放入到flow_recycle_q全局回收队列中,但是超时流需要重组的,要放入tv->flow_queue中,这个tv->flow_queue,在收包线程FlowWorker模块中,又会从中把流取出来放入到work_queue中(复杂的地方就在这里,涉及到多个线程操作几个队列或者链表,你放我取,你取我放)

关键代码加注释如下:

/**
 *  \brief time out flows from the hash
 *
 *  \param ts timestamp
 *  \param hash_min min hash index to consider
 *  \param hash_max max hash index to consider
 *  \param counters ptr to FlowTimeoutCounters structure
 *
 *  \retval cnt number of timed out flow
 */
static uint32_t FlowTimeoutHash(FlowManagerTimeoutThread *td, SCTime_t ts, const uint32_t hash_min,
        const uint32_t hash_max, FlowTimeoutCounters *counters)
{
    uint32_t cnt = 0;
    const int emergency = ((SC_ATOMIC_GET(flow_flags) & FLOW_EMERGENCY));
    const uint32_t rows_checked = hash_max - hash_min;
    uint32_t rows_skipped = 0;
    uint32_t rows_empty = 0;

#if __WORDSIZE==64
#define BITS 64
#define TYPE uint64_t
#else
#define BITS 32
#define TYPE uint32_t
#endif

    const uint32_t ts_secs = SCTIME_SECS(ts);
    for (uint32_t idx = hash_min; idx < hash_max; idx+=BITS) {
        TYPE check_bits = 0;
        const uint32_t check = MIN(BITS, (hash_max - idx));
        for (uint32_t i = 0; i < check; i++) {
            FlowBucket *fb = &flow_hash[idx+i];  //遍历指定范围哈希桶
            check_bits |= (TYPE)(SC_ATOMIC_LOAD_EXPLICIT(
                                         fb->next_ts, SC_ATOMIC_MEMORY_ORDER_RELAXED) <= ts_secs)
                          << (TYPE)i;
        }
        if (check_bits == 0)
            continue;

        for (uint32_t i = 0; i < check; i++) {
            FlowBucket *fb = &flow_hash[idx+i];
            if ((check_bits & ((TYPE)1 << (TYPE)i)) != 0 && SC_ATOMIC_GET(fb->next_ts) <= ts_secs) {
                FBLOCK_LOCK(fb);
                Flow *evicted = NULL;
                if (fb->evicted != NULL || fb->head != NULL) {
                    if (fb->evicted != NULL) {
                        /* transfer out of bucket so we can do additional work outside
                         * of the bucket lock */
                        evicted = fb->evicted;
                        fb->evicted = NULL;
                    }
                    if (fb->head != NULL) {
                        uint32_t next_ts = 0;
                        FlowManagerHashRowTimeout(td, fb->head, ts, emergency, counters, &next_ts); //超时处理,超时的流放到td->aside_queue 这个队列

                        if (SC_ATOMIC_GET(fb->next_ts) != next_ts)
                            SC_ATOMIC_SET(fb->next_ts, next_ts);
                    }
                    if (fb->evicted == NULL && fb->head == NULL) {
                        SC_ATOMIC_SET(fb->next_ts, UINT_MAX);
                    }
                } else {
                    SC_ATOMIC_SET(fb->next_ts, UINT_MAX);
                    rows_empty++;
                }
                FBLOCK_UNLOCK(fb);
                /* processed evicted list */
                if (evicted) { 
                    FlowManagerHashRowClearEvictedList(td, evicted, ts, counters);//处理evicted链的超时流
                }
            } else {
                rows_skipped++;
            }
        }
        if (td->aside_queue.len) {
            cnt += ProcessAsideQueue(td, counters); //处理AsideQueue队列中已经超时的流
        }
    }

    counters->rows_checked += rows_checked;
    counters->rows_skipped += rows_skipped;
    counters->rows_empty += rows_empty;

    if (td->aside_queue.len) {
        cnt += ProcessAsideQueue(td, counters);
    }
    counters->flows_removed += cnt;
    /* coverity[missing_unlock : FALSE] */
    return cnt;
}


//处理超时的流
static uint32_t ProcessAsideQueue(FlowManagerTimeoutThread *td, FlowTimeoutCounters *counters)
{
    FlowQueuePrivate recycle = { NULL, NULL, 0 };
    counters->flows_aside += td->aside_queue.len;

    uint32_t cnt = 0;
    Flow *f;   //Aside队列出队
    while ((f = FlowQueuePrivateGetFromTop(&td->aside_queue)) != NULL) {
        /* flow is still locked */
        //超时流需要重组
        if (f->proto == IPPROTO_TCP && !(f->flags & FLOW_TIMEOUT_REASSEMBLY_DONE) &&
                !FlowIsBypassed(f) && FlowForceReassemblyNeedReassembly(f) == 1) {
            /* Send the flow to its thread */
            FlowForceReassemblyForFlow(f);//把flow放入原线程的tv->flow_queue队列中
            FLOWLOCK_UNLOCK(f);
            /* flow ownership is passed to the worker thread */

            counters->flows_aside_needs_work++;
            continue;
        }
        FLOWLOCK_UNLOCK(f);

        FlowQueuePrivateAppendFlow(&recycle, f);
        if (recycle.len == 100) {
            FlowQueueAppendPrivate(&flow_recycle_q, &recycle);//放入回收线程的回收队列flow_recycle_q,每100个放一次
            FlowWakeupFlowRecyclerThread();
        }
        cnt++;
    }
    if (recycle.len) {
        FlowQueueAppendPrivate(&flow_recycle_q, &recycle);
        FlowWakeupFlowRecyclerThread();
    }
    return cnt;
}

4、FlowRecycler线程对流表的处理:

FlowRecycler线程的函数调用栈如下:

FlowRecycler
    -->FlowQueuePrivateGetFromTop(flow_recycle_q队列出队)    
    -->Recycler
        -->FlowClearMemory(清除流信息)
        -->FlowSparePoolReturnFlow(将流归还到flow_spare_pool中)

FlowRecycler线程对流表的处理相对简单,就是将flow_recycle_q全局流回收队列的流出队,清除流信息,然后归还到flow_spare_pool流预分配链表中,方便后面的流使用。

关键代码加注释如下:

static void Recycler(ThreadVars *tv, FlowRecyclerThreadData *ftd, Flow *f)
{
    FLOWLOCK_WRLOCK(f);

    (void)OutputFlowLog(tv, ftd->output_thread_data, f);

    FlowEndCountersUpdate(tv, &ftd->fec, f);
    if (f->proto == IPPROTO_TCP && f->protoctx != NULL) {
        StatsDecr(tv, ftd->counter_tcp_active_sessions);
    }
    StatsDecr(tv, ftd->counter_flow_active);

    FlowClearMemory(f, f->protomap); //清除流信息
    FLOWLOCK_UNLOCK(f);
    FlowSparePoolReturnFlow(f);  //放入流备用队列
}

5、其它说明:

1)就是当流表内存不够用时,会有一个紧急状态机制FLOW_EMERGENCY,这个时候不同协议以及不同流状态的老化时间会大大的缩短,加速流表老化,以腾出更多的内存空间为新流所用,例如紧急状态下TCP流的初建阶段的老化时长默认值为FLOW_IPPROTO_TCP_EMERG_NEW_TIMEOUT 10秒,而对于TCP握手完成之后的流老化时长默认为FLOW_IPPROTO_TCP_EMERG_EST_TIMEOUT 100秒。

2)前面说过FlowManager和FlowRecycler线程个数是可以配置的,如果配置多个线程,在遍历FlowBucket *flow_hash哈希桶时,在FlowManagerThreadInit函数中是有一个计算方法的,就是每个线程只遍历其中的一部分哈希链。

3)Flow节点的结构体加注释如下:

/**
 *  \brief Flow data structure.
 *
 *  The flow is a global data structure that is created for new packets of a
 *  flow and then looked up for the following packets of a flow.
 *
 *  Locking
 *
 *  The flow is updated/used by multiple packets at the same time. This is why
 *  there is a flow-mutex. It's a mutex and not a spinlock because some
 *  operations on the flow can be quite expensive, thus spinning would be
 *  too expensive.
 *
 *  The flow "header" (addresses, ports, proto, recursion level) are static
 *  after the initialization and remain read-only throughout the entire live
 *  of a flow. This is why we can access those without protection of the lock.
 */

typedef struct Flow_
{
    /* flow "header", used for hashing and flow lookup. Static after init,
     * so safe to look at without lock */
    FlowAddress src, dst;  //源目的IP
    union {
        Port sp;        /**< tcp/udp source port */
        struct {
            uint8_t type;   /**< icmp type */
            uint8_t code;   /**< icmp code */
        } icmp_s;

        struct {
            uint32_t spi; /**< esp spi */
        } esp;
    };
    union {
        Port dp;        /**< tcp/udp destination port */
        struct {
            uint8_t type;   /**< icmp type */
            uint8_t code;   /**< icmp code */
        } icmp_d;
    };
    uint8_t proto;   //传输层协议号
    uint8_t recursion_level;
    uint16_t vlan_id[2];  //两层vlan

    uint8_t vlan_idx;  //vlan的层数

    /* track toserver/toclient flow timeout needs */
    union {
        struct {
            uint8_t ffr_ts:4;
            uint8_t ffr_tc:4;
        };
        uint8_t ffr;
    };

    /** timestamp in seconds of the moment this flow will timeout
     *  according to the timeout policy. Does *not* take emergency
     *  mode into account. */
    uint32_t timeout_at;

    /** Thread ID for the stream/detect portion of this flow */
    FlowThreadId thread_id[2];

    struct Flow_ *next; /* (hash) list next */  //下一个flow节点
    /** Incoming interface */
    struct LiveDevice_ *livedev;  //接口信息(第一个发起的报文的接口)

    /** flow hash - the flow hash before hash table size mod. */
    uint32_t flow_hash;  //流的哈希值

    /** timeout policy value in seconds to add to the lastts.tv_sec
     *  when a packet has been received. */
    uint32_t timeout_policy;

    /* time stamp of last update (last packet). Set/updated under the
     * flow and flow hash row locks, safe to read under either the
     * flow lock or flow hash row lock. */
    SCTime_t lastts;  //流的最后一个包的时间

    FlowStateType flow_state;  //流的状态 FlowState

    /** flow tenant id, used to setup flow timeout and stream pseudo
     *  packets with the correct tenant id set */
    uint32_t tenant_id;

    uint32_t probing_parser_toserver_alproto_masks;
    uint32_t probing_parser_toclient_alproto_masks;

    uint32_t flags;  //ipv4/ipv6标记       /**< generic flags */

    uint16_t file_flags;    /**< file tracking/extraction flags */

    /** destination port to be used in protocol detection. This is meant
     *  for use with STARTTLS and HTTP CONNECT detection */
    uint16_t protodetect_dp; /**< 0 if not used */

    /* Parent flow id for protocol like ftp */
    int64_t parent_id;  //父流节点,例如ftp-data的控制流的信息

#ifdef FLOWLOCK_RWLOCK
    SCRWLock r;
#elif defined FLOWLOCK_MUTEX
    SCMutex m;
#else
    #error Enable FLOWLOCK_RWLOCK or FLOWLOCK_MUTEX
#endif

    /** protocol specific data pointer, e.g. for TcpSession */
    void *protoctx;  //TcpSession

    /** mapping to Flow's protocol specific protocols for timeouts
        and state and free functions. */
    uint8_t protomap; //FLOW_PROTO_MAX  传输层协议id

    uint8_t flow_end_flags;
    /* coccinelle: Flow:flow_end_flags:FLOW_END_FLAG_ */

    AppProto alproto; /**< \brief application level protocol */ //应用id enum AppProtoEnum
    AppProto alproto_ts;
    AppProto alproto_tc;

    /** original application level protocol. Used to indicate the previous
       protocol when changing to another protocol , e.g. with STARTTLS. */
    AppProto alproto_orig;
    /** expected app protocol: used in protocol change/upgrade like in
     *  STARTTLS. */
    AppProto alproto_expect;

    /** detection engine ctx version used to inspect this flow. Set at initial
     *  inspection. If it doesn't match the currently in use de_ctx, the
     *  stored sgh ptrs are reset. */
    uint32_t de_ctx_version;  //检测引擎规则版本,检测规则变化,会导致流重新匹配规则

    /** ttl tracking */
    uint8_t min_ttl_toserver;
    uint8_t max_ttl_toserver;
    uint8_t min_ttl_toclient;
    uint8_t max_ttl_toclient;

    /** application level storage ptrs.
     *
     */
    AppLayerParserState *alparser;     /**< parser internal state */
    void *alstate;      /**< application layer state */

    /** toclient sgh for this flow. Only use when FLOW_SGH_TOCLIENT flow flag
     *  has been set. */
    const struct SigGroupHead_ *sgh_toclient;
    /** toserver sgh for this flow. Only use when FLOW_SGH_TOSERVER flow flag
     *  has been set. */
    const struct SigGroupHead_ *sgh_toserver;

    /* pointer to the var list */
    GenericVar *flowvar;

    struct FlowBucket_ *fb;  //流所在的哈希桶的指针

    SCTime_t startts;  //流创建的时间
    //流量统计
    uint32_t todstpktcnt;  //到目的IP的包数统计
    uint32_t tosrcpktcnt;  //到源IP的包数统计
    uint64_t todstbytecnt; //到目的IP的字节数统计
    uint64_t tosrcbytecnt; //到源IP的字节数统计
} Flow;

4)加了一个查看flow节点信息的命令:flows-list,看看效果:

流节点格式为:流ID编号)源IP:源端口 --> 目的IP:目的端口,传输层协议号,到目的方向包数:到目的方向字节数<-->到源方向包数:到源方向字节数,流首包的网口的pcie地址

6、结尾:

好了,关于suricata的flow流管理分析就到这里了,目前只能是一个初步分析,细节上理解还不够,其实要真正理解的是作者的设计思想,就是他为什么要这样去设计,这样设计的好处是什么,这个太难了!

有问题或者需要自定义命令源码的朋友,可以进网络技术开发交流群提问(先加我wx,备注加群)。喜欢文章内容的朋友,加个关注呗~~

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/613174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[SpringMVC]Controller控制器、Interceptor拦截器、RestFul风格、异常处理、JSON数据格式与AJAX请求

文章目录 MVC理论基础配置环境并搭建项目Controller控制器配置视图解析器和控制器RequestMapping详解RequestParam和RequestHeader详解CookieValue和SessionAttrbutie重定向和请求转发Bean的Web作用域 RestFul风格Interceptor拦截器创建拦截器多级拦截器 异常处理JSON数据格式与…

C# Socket入门编程winform案例(附下载链接)

C# socket编程实现信息的接收&#xff08;winform&#xff09; 点我下载项目资源 服务器端&#xff1a; 第一步&#xff1a;建立一个用于通信的Socket对象 第二步&#xff1a;使用bind绑定IP地址和端口号 第三步&#xff1a;使用listen监听客户端 第四步&#xff1a;使用accep…

宝安西乡产业园变九年制学校,新增宅地、商地。

6月5日&#xff0c;宝安区城市更新和土地整备局发布《西乡街道盐田社区银田地块土地整备利益统筹项目土地整备规划&#xff08;草案&#xff09;》&#xff08;以下简称草案&#xff09;。 草案显示&#xff0c;该项目经过调整后&#xff1a; ● 新增一块二类居住用地&#xf…

王者荣耀战区活跃度排名怎么实现的?这篇文章给你答案!

&#x1f349;博客主页:阿博历练记 &#x1f4d6;文章专栏:数据结构与算法 &#x1f68d;代码仓库:阿博编程日记 &#x1f365;欢迎关注:欢迎友友们点赞收藏关注哦&#x1f339; 文章目录 &#x1f308;前言&#x1f36a;堆的实现&#x1f50d;1.堆的结构框架&#x1f50d;2.堆…

通过ChatGPT打造10W+公众号文章

大家好&#xff0c;我是可夫小子&#xff0c;关注AIGC、读书和自媒体。解锁更多ChatGPT、AI绘画玩法。加&#xff1a;keeepdance&#xff0c;备注&#xff1a;chatgpt。 这是一篇非常具有实操性的指南&#xff0c;可能会动到一些某些行业人的蛋糕&#xff0c;但我无益于此。我是…

pnpm的安装和使用

1 安装 1.1 安装教程 npm全局安装pnpm npm install -g pnpm设置镜像地址 获取当前配置的镜像地址 pnpm get registry设置新的镜像地址 pnpm set registry https://registry.npm.taobao.org设置包存放地址 pnpm config set store-dir E:/xxx1.2 安装问题 当在vscode上使用…

在labview里使用LabSQL连接ACCESS数据库

使用LabSQL连接ACCESS数据库 写在前面ODBC数据源管理器的配置LV软件里使用结束 写在前面 ACCESS数据库一般包含在Office组件里&#xff0c;安装完Office后就可以直接拿来使用&#xff0c;要求不高的场合适合使用。 LabSQL工具包直接放进LV的安装目录下&#xff0c;打开软件后在…

SpringBootWeb案例-1(下: 来源黑马程序员)

3. 员工管理 完成了部门管理的功能开发之后&#xff0c;我们进入到下一环节员工管理功能的开发。 基于以上原型&#xff0c;我们可以把员工管理功能分为&#xff1a; 分页查询带条件的分页查询删除员工新增员工修改员工 3.1 分页查询 3.1.1 基础分页 3.1.1.1 需求分析 我…

YOLOV8 Onnxruntime Opencv DNN C++部署

1.Opencv介绍 OpenCV由各种不同组件组成。OpenCV源代码主要由OpenCV core(核心库)、opencv_contrib和opencv_extra等子仓库组成。近些年,OpenCV的主仓库增加了深度学习相关的子仓库:OpenVINO(即DLDT, Deep Learning Deployment Toolkit)、open_model_zoo,以及标注工具CV…

C++标准模板库 队列容器的使用

队列&#xff1a;在数据结构中也成为操作受限的线性表&#xff0c;是一种只允许在表的一端插入&#xff0c;在另一端删除的线性表 特点&#xff1a;先进先出&#xff0c;像打饭《排在最前面的先买&#xff0c;后到的排在队尾&#xff0c;即删除在队头&#xff0c;插入在队尾》…

面试测试工程师,都要考察什么?

今年刚接触了&#xff08;功能&#xff09;测试工程师的面试工作&#xff0c;有遇到对信贷业务流程较熟悉的、工作内容纯测试app功能的、什么都接触过但是不够深入的&#xff0c;发现简历上写的东西和实际真的有点差距&#xff0c;面试也是一个艺术活。 如果你想学习自动化测试…

Security Onion(安全洋葱)开源入侵检测系统(ids)安装

文章目录 Security Onion介绍安装配置&#xff08;最低&#xff09;安装步骤web界面 Security Onion介绍 Security Onion是一款专为入侵检测和NSM(网络安全监控)设计的Linux发行版。其安装过程很简单&#xff0c;在短时间内就可以部署一套完整的NSM收集、检测和分析的套件。Se…

高完整性系统(6)Alloy核心语法 + 有限状态机(Finite State Machines);check assertion amination

文章目录 Alloy 核心内容sig数据类型varonefactpre-conditionfunpred 谓词谓词的构成谓词的结果 / 普通条件约束和 pre-post condition 的区别 Finite State Machines 有限状态机FSM in Alloy Check Specification动画&#xff08;Animation&#xff09;&#xff1a;run 关键字…

安装superset并连接clickhouse

说明&#xff1a; Apache Superset是一个现代的数据探索和可视化平台。它功能强大且十分易用&#xff0c;可对接各种数据源&#xff0c;包括很多现代的大数据分析引擎&#xff0c;拥有丰富的图表展示形式&#xff0c;并且支持自定义仪表盘。 使用的服务器操作系统为CentOS 7&a…

路径规划算法:基于天牛须优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于天牛须优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于天牛须优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法…

【数据结构与算法篇】深入浅出——二叉树(详解)

​&#x1f47b;内容专栏&#xff1a;《数据结构与算法专栏》 &#x1f428;本文概括&#xff1a; 二叉树是一种常见的数据结构&#xff0c;它在计算机科学中广泛应用。本博客将介绍什么是二叉树、二叉树的顺序与链式结构以及它的基本操作&#xff0c;帮助读者理解和运用这一重…

微信开发框架WxJava之微信公众号开发的入门使用篇

微信开发框架WxJava之微信公众号开发的入门使用篇 WxJava介绍微信公众号申请测试公众号测试公众号配置 WxJava微信公众号开发添加依赖配置微信参数实例化WxMpService对接微信公众号回调接收与回复消息 微信消息路由器WxMpMessageHandlerWxMessageInterceptor自定义Handle自定义…

Vue.js devtools运行但调试窗口未出现的解决方案

Vue.js devtools是一款基于Chrome浏览器的调试Vue.js应用的扩展程序。然而&#xff0c;有时即使该插件已经在运行&#xff0c;调试窗口也可能未出现。这主要可能有以下几个原因&#xff0c;并附有相应的解决方法&#xff1a; 1. Chrome扩展程序选项的问题 首先&#xff0c;右上…

关于数据挖掘的问题之经典案例

依据交易数据集 basket_data.csv挖掘数据中购买行为中的关联规则。 问题分析&#xff1a; 如和去对一个数据集进行关联规则挖掘&#xff0c;找到数据集中的项集之间的关联性。 处理步骤&#xff1a; 首先导入了两个库&#xff0c;pandas 库和 apyori 库。pandas 库是 Pytho…

二叉树基础知识力扣题构造二叉树总结

二叉树 如何理解二叉树&#xff0c;This is a question! 作者在去年被布置要求学习二叉树时对二叉树的理解并不是很深刻&#xff0c;甚至可以说是绕道走&#xff0c;但是Luck of the draw only draws the unlucky&#xff0c;在学期初考核时&#xff0c;作者三道二叉树题都没…