Commit e227a725 authored by jan.koester's avatar jan.koester
Browse files

test

parent 0c56da28
Loading
Loading
Loading
Loading
+416 −286
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2014, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
	* Redistributions of source code must retain the above copyright
	  notice, this list of conditions and the following disclaimer.
	* Redistributions in binary form must reproduce the above copyright
	  notice, this list of conditions and the following disclaimer in the
	  documentation and/or other materials provided with the distribution.
	* Neither the name of the <organization> nor the
	  names of its contributors may be used to endorse or promote products
	  derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
 * iocp.cpp  (Windows backend for event)
 *
 * Implements netplus::event using IOCP:
 *  - CreateIoCompletionPort
 *  - AcceptEx for incoming connections
 *  - WSARecv / WSASend for async I/O
 *
 * Works with http.cpp usage:
 *   class MyServer : public event { ... RequestEvent(...) override ... }
 *
 ******************************************************************************/

#ifdef Windows

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <mswsock.h>
#include <ws2tcpip.h>

#pragma comment(lib, "Ws2_32.lib")

#include <iostream>
#include <mutex>
#include <vector>
#include <atomic>
#include <thread>
#include <vector>
#include <memory>
#include <atomic>
#include <chrono>
#include <cstring>
#include <mutex>
#include <iostream>

#include "socket.h"
#include "exception.h"
#include "eventapi.h"
#include "connection.h"
#include "eventapi.h"
#include "exception.h"

#include <cstdio>
#include <cinttypes>
#include <windows.h>

static inline uint64_t now_ms() {
    return GetTickCount64();
}

#define IOLOG(fmt, ...) \
    do { \
        std::fprintf(stderr, "[%10" PRIu64 " ms][tid=%lu] " fmt "\n", \
            now_ms(), GetCurrentThreadId(), __VA_ARGS__); \
        std::fflush(stderr); \
    } while (0)
using namespace netplus;

namespace netplus {

std::atomic<bool> event::Running(true);
    std::atomic<bool> event::Restart(false);

    std::vector<socket*> SSOCKETS;
    std::map<int,std::shared_ptr<con>>    CONNECTIONS;

    std::mutex POLL_HANDLER_MUTEX;

    std::atomic<size_t> g_con_id(0);

	// ------------------------------------------------------------
	// Client object (per accepted connection context)
	// ------------------------------------------------------------
	class client {
	public:
		explicit client(eventapi* eapi)
			: api(eapi), readCtx(BLOCKSIZE), writeCtx(BLOCKSIZE) {
			api->CreateConnection(CurCon);
			readCtx.owner = this;
			writeCtx.owner = this;
		}

		std::shared_ptr<con> CurCon;
		eventapi* api;
		std::mutex cltmtx;
		buffer readCtx;   // Accept + Read
		buffer writeCtx;  // Write
	};

	struct EventWorkerArgs {
		int           timeout;
		HANDLE        iocp;
		eventapi* ev;
		socket* listenSock;
		LPFN_ACCEPTEX lpfnAcceptEx;
		void* userArgs;
	};

    static const char* op_name(int op) {
        switch (op) {
        case OP_ACCEPT: return "ACCEPT";
        case OP_READ:   return "READ";
        case OP_WRITE:  return "WRITE";
        default:        return "UNKNOWN";
static LPFN_ACCEPTEX LoadAcceptEx(SOCKET listenSock) {
    GUID guid = WSAID_ACCEPTEX;
    LPFN_ACCEPTEX fn = nullptr;
    DWORD bytes = 0;
    int rc = WSAIoctl(
        listenSock,
        SIO_GET_EXTENSION_FUNCTION_POINTER,
        &guid, sizeof(guid),
        &fn, sizeof(fn),
        &bytes,
        nullptr, nullptr
    );
    if (rc == SOCKET_ERROR || !fn) {
        NetException e;
        e[NetException::Critical] << "LoadAcceptEx failed: " << WSAGetLastError();
        throw e;
    }
    return fn;
}

    static void post_accept(EventWorkerArgs* a) {
        client* c = new client(a->ev);
        if (a->listenSock->_Type == sockettype::TCP) {
            c->CurCon->csock = std::make_unique<tcp>();
// ----------------------------------------------------------------------------
// Small helper: create a new connection object, assign socket, bind to IOCP.
// ----------------------------------------------------------------------------
static std::unique_ptr<con> CreateConnectionFromAccepted(event* ev,
                                                        socket* serverSock,
                                                        SOCKET acceptedSock,
                                                        HANDLE iocpHandle)
{
    // Your connection type "con" is defined in connection.h
    // Typically you use something like: std::make_unique<con>(...)
    // But since I don't see con constructors here, I use the default approach:
    std::unique_ptr<con> c = std::make_unique<con>();

    // Create a tcp socket wrapper for accepted socket.
    // Your socket class likely has setSocket(SOCKET) and takes ownership.
    auto clientSock = std::make_unique<tcp>();
    clientSock->setSocket(acceptedSock);

    // Bind accepted socket to IOCP: completion key = clientSock pointer
    HANDLE h = CreateIoCompletionPort(
        (HANDLE)acceptedSock,
        iocpHandle,
        (ULONG_PTR)clientSock.get(),
        0
    );
    if (!h) {
        NetException e;
        e[NetException::Critical] << "CreateIoCompletionPort(accepted) failed: " << GetLastError();
        throw e;
    }

        // Bind the socket to IOCP
        CreateIoCompletionPort((HANDLE)c->CurCon->csock->fd(), a->iocp, (ULONG_PTR)c, 0);
    // attach socket to con
    c->csocket = std::move(clientSock);
    c->ssocket = serverSock;

        // CRITICAL: Set the owner so the Worker knows who this is
        c->readCtx.owner = c;
        c->readCtx.operation = OP_ACCEPT;
        std::memset(&c->readCtx.overlapped, 0, sizeof(WSAOVERLAPPED));
    // mark connected
    c->connected = true;

        // Post the actual AcceptEx
        a->listenSock->accept(a->lpfnAcceptEx, c->CurCon->csock, c->readCtx);
    return c;
}

class EventWorker {
// ----------------------------------------------------------------------------
// IOCP Worker thread
// ----------------------------------------------------------------------------
class IocpWorker {
public:
        EventWorker(int tid, ULONG_PTR args, EventWorkerArgs* eargs) {
            DWORD bytesTransferred = 0;
            ULONG_PTR completionKey = 0;
            LPOVERLAPPED overlapped = nullptr;
    IocpWorker(event* ev, socket* serverSock, HANDLE iocp, LPFN_ACCEPTEX fnAcceptEx, int timeout)
        : _ev(ev), _serverSock(serverSock), _iocp(iocp), _fnAcceptEx(fnAcceptEx), _timeout(timeout) {}

    void run() {
        while (event::Running) {
                // 1) Warte auf Abschluss einer I/O Operation
                BOOL res = GetQueuedCompletionStatus(
                    eargs->iocp,
                    &bytesTransferred,
                    &completionKey,
                    &overlapped,
                    eargs->timeout
                );
            DWORD bytes = 0;
            ULONG_PTR key = 0;
            OVERLAPPED* pov = nullptr;

                if (!res && !overlapped) {
                    if (GetLastError() == WAIT_TIMEOUT) continue;
                    break;
                }
            BOOL ok = GetQueuedCompletionStatus(_iocp, &bytes, &key, &pov, INFINITE);

                int fd = (int)completionKey;
                std::shared_ptr<con> curcon;
            if (!event::Running) break;
            if (!pov && key == 0) continue;

                {
                    std::lock_guard<std::mutex> lock(POLL_HANDLER_MUTEX);
                    auto it = CONNECTIONS.find(fd);
                    if (it != CONNECTIONS.end()) curcon = it->second;
            // AcceptEx completions use AcceptContext.ov (not buffer.overlapped)
            if (AcceptContext* actx = FindAcceptContext(pov)) {
                HandleAcceptCompletion(ok, actx);
                continue;
            }

                if (!curcon || !curcon->csock) continue;
            // Otherwise: must be buffer completion (WSARecv / WSASend)
            socket* sockObj = reinterpret_cast<socket*>(key);
            if (!sockObj) continue;

                std::lock_guard<std::mutex> lk(curcon->event_mutex);
            buffer* buf = reinterpret_cast<buffer*>(
                reinterpret_cast<char*>(pov) - offsetof(buffer, overlapped)
            );

                try {
                    // 2) Handshake-Logik (Analog zu epoll.cpp)
                    if (!curcon->csock->getHandshakeDone()) {
                        // Da iocp.cpp als Template für SSL dient, casten wir sicherheitshalber
                        if (static_cast<ssl*>(curcon->csock.get())->handleHandshakeAsynch()) {
                            // Handshake gerade fertig -> Wir könnten sofort lesen
                        }
                        // Wir müssen immer einen neuen Recv-Request absetzen
                        _triggerNextRecv(curcon);
            if (!ok) {
                // I/O error => close
                _ev->CloseEvent(sockObj);
                continue;
            }

            if (buf->operation == OP_READ) {
                if (bytes == 0) {
                    _ev->CloseEvent(sockObj);
                    continue;
                }

                    // 3) Datenverarbeitung
                    if (bytesTransferred > 0) {
                        ReadEventHandler(curcon, tid, eargs->userArgs, eargs);
                    } else if (bytesTransferred == 0) {
                        _closeConnection(fd, eargs);
                // Find connection for this socket
                con* c = _ev->FindConnection(sockObj);
                if (!c) {
                    _ev->CloseEvent(sockObj);
                    continue;
                }

                } catch (NetException& e) {
                    if (e.getErrorType() != NetException::Note) {
                        _closeConnection(fd, eargs);
                // Let user code handle it
                _ev->RequestEvent(*c, _tid, (ULONG_PTR)bytes);

                // Re-post receive
                try {
                    c->csocket->recvDataWSA(c->rbuf, 0);
                } catch (...) {
                    _ev->CloseEvent(sockObj);
                }
            }
            else if (buf->operation == OP_WRITE) {
                // send completed => user callback
                con* c = _ev->FindConnection(sockObj);
                if (!c) {
                    _ev->CloseEvent(sockObj);
                    continue;
                }
                _ev->SendEvent(*c, _tid, (ULONG_PTR)bytes);
            }
        }

        void ReadEventHandler(std::shared_ptr<con> rcon, int tid, void* args, EventWorkerArgs* eargs) {
            try {
                for (;;) {
                    buffer buf(BLOCKSIZE);
                    size_t rcv = 0;
                    try {
                        // Nutzt WSARecv intern via socket.cpp/tcp.cpp
                        rcv = rcon->csock->RecvDataWSA(buf, 0);
                    } catch (NetException& e) {
                        if (e.getErrorType() == NetException::Note) return; // WSA_IO_PENDING
                        throw;
    }

                    if (rcv == 0) break;

                    rcon->RecvData.append(buf.data.buf, rcv);
                    rcon->lasteventime = time(nullptr);
    void setThreadId(int tid) { _tid = tid; }

                    // Protokoll-Event triggern (z.B. HTTP Parser)
                    eargs->ev->RequestEvent(*rcon, tid, args);
    // AcceptContext tracking
    void RegisterAccept(AcceptContext* a) {
        std::lock_guard<std::mutex> lk(_accMutex);
        _accepts.push_back(a);
    }

                    // Nach RequestEvent abbrechen, um Senden zu priorisieren (wie epoll)
                    break;
private:
    AcceptContext* FindAcceptContext(OVERLAPPED* pov) {
        std::lock_guard<std::mutex> lk(_accMutex);
        for (auto* a : _accepts) {
            if ((OVERLAPPED*)&a->ov == pov) return a;
        }
        return nullptr;
    }

                if (!rcon->SendData.empty()) {
                    WriteEventHandler(rcon, tid, args, eargs);
    void RemoveAccept(AcceptContext* a) {
        std::lock_guard<std::mutex> lk(_accMutex);
        for (size_t i = 0; i < _accepts.size(); ++i) {
            if (_accepts[i] == a) {
                _accepts.erase(_accepts.begin() + i);
                return;
            }
            } catch (NetException& e) {
                throw;
        }
    }

        void WriteEventHandler(std::shared_ptr<con> wcon, int tid, void* args, EventWorkerArgs* eargs) {
            try {
                while (!wcon->SendData.empty()) {
                    size_t sendlen = (std::min)(wcon->SendData.size(), (size_t)BLOCKSIZE);
                    buffer out(wcon->SendData.data(), sendlen);
    void PostAccept() {
        auto* actx = new AcceptContext();
        std::memset(&actx->ov, 0, sizeof(WSAOVERLAPPED));

                    size_t consumed = 0;
                    try {
                        // Nutzt WSASend intern
                        consumed = wcon->csock->SendDataWSA(out, 0);
                    } catch (NetException& e) {
                        if (e.getErrorType() == NetException::Note) return;
                        throw;
        SOCKET as = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED);
        if (as == INVALID_SOCKET) {
            delete actx;
            return;
        }
        actx->acceptSock = as;

                    if (consumed == 0) return;
                    wcon->SendData.erase(wcon->SendData.begin(), wcon->SendData.begin() + consumed);
                }
        DWORD bytes = 0;

        BOOL ok = _fnAcceptEx(
            (SOCKET)_serverSock->getSocket(),
            as,
            actx->addrBuf,
            0,
            sizeof(SOCKADDR_STORAGE) + 16,
            sizeof(SOCKADDR_STORAGE) + 16,
            &bytes,
            &actx->ov
        );

                if (wcon->SendData.empty()) {
                    eargs->ev->ResponseEvent(*wcon, tid, args);
        if (!ok) {
            int err = WSAGetLastError();
            if (err != WSA_IO_PENDING) {
                closesocket(as);
                delete actx;
                return;
            }
            } catch (NetException& e) {
                throw;
        }

        RegisterAccept(actx);
    }

    private:
        void _triggerNextRecv(std::shared_ptr<con> c) {
            buffer dummy(0);
            try { c->csock->RecvDataWSA(dummy, 0); } catch(...) {}
    void HandleAcceptCompletion(BOOL ok, AcceptContext* actx) {
        RemoveAccept(actx);

        if (!ok) {
            closesocket(actx->acceptSock);
            delete actx;
            PostAccept(); // keep accepting
            return;
        }

        void _closeConnection(int fd, EventWorkerArgs* eargs) {
            std::lock_guard<std::mutex> lock(POLL_HANDLER_MUTEX);
            auto it = CONNECTIONS.find(fd);
            if (it != CONNECTIONS.end()) {
                CONNECTIONS.erase(it);
        // must update accept context
        SOCKET ls = (SOCKET)_serverSock->getSocket();
        setsockopt(actx->acceptSock, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
                   (char*)&ls, sizeof(ls));

        try {
            // create connection
            std::unique_ptr<con> newcon = CreateConnectionFromAccepted(_ev, _serverSock, actx->acceptSock, _iocp);

            // register connection in event
            _ev->AddConnection(std::move(newcon));

            // Start first read
            con* c = _ev->FindConnectionBySocket((SOCKET)actx->acceptSock);
            if (c) {
                c->csocket->recvDataWSA(c->rbuf, 0);
            }
        }
        catch (...) {
            closesocket(actx->acceptSock);
        }
            closesocket(fd);

        delete actx;

        // Repost accept immediately
        PostAccept();
    }

private:
    event* _ev = nullptr;
    socket* _serverSock = nullptr;
    HANDLE _iocp = nullptr;
    LPFN_ACCEPTEX _fnAcceptEx = nullptr;
    int _timeout = 1000;
    int _tid = 0;

    std::mutex _accMutex;
    std::vector<AcceptContext*> _accepts;
};

	// ------------------------------------------------------------
	// event
	// ------------------------------------------------------------
	event::event(socket* serversocket, int timeout) {
		_Timeout = timeout;
		_ServerSocket = serversocket;
		_ServerSocket->bind();
		_ServerSocket->listen();
// ----------------------------------------------------------------------------
// event class implementation (Windows/IOCP)
// ----------------------------------------------------------------------------

event::event(socket* serversocket, int timeout)
    : _ServerSocket(serversocket), _Timeout(timeout)
{
    if (!_ServerSocket) {
        NetException e;
        e[NetException::Critical] << "server socket empty!";
        throw e;
    }

	event::~event() {}
    // Ensure server socket is bound/listening
    _ServerSocket->bind();
    _ServerSocket->listen();

	void event::runEventloop(ULONG_PTR args) {
		NetException exception;
    // Create IOCP
    _Iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
    if (!_Iocp) {
        NetException e;
        e[NetException::Critical] << "CreateIoCompletionPort failed: " << GetLastError();
        throw e;
    }

		HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
		if (!iocp) {
			exception[NetException::Critical] << "IOCP: CreateIoCompletionPort failed";
			throw exception;
    // Bind server socket to IOCP (key = server socket pointer)
    HANDLE h = CreateIoCompletionPort(
        (HANDLE)_ServerSocket->getSocket(),
        _Iocp,
        (ULONG_PTR)_ServerSocket,
        0
    );
    if (!h) {
        NetException e;
        e[NetException::Critical] << "CreateIoCompletionPort(bind server) failed: " << GetLastError();
        throw e;
    }

    _fnAcceptEx = LoadAcceptEx((SOCKET)_ServerSocket->getSocket());
}

event::~event() {
    Stop();
}

// start worker threads + initial AcceptEx posts
void runEventloop(ULONG_PTR args=0){
    Running = true;

    const int nthreads = (int)std::max<size_t>(1, std::thread::hardware_concurrency());
    _Workers.reserve(nthreads);

    for (int i = 0; i < nthreads; ++i) {
        _Workers.emplace_back([&, i]() {
            IocpWorker w(this, _ServerSocket, _Iocp, _fnAcceptEx, _Timeout);
            w.setThreadId(i);

            // Pre-post accepts per worker (you can tune this)
            for (int k = 0; k < 4; ++k) {
                w.RegisterAccept(new AcceptContext()); // placeholder
            }
            // Actually post accepts
            for (int k = 0; k < 4; ++k) {
                // internal call
                // (we need to post on this worker instance)
                // so we recreate minimal pattern:
                // easiest: call PostAccept by starting run() which posts first
            }

            // Post initial accepts now:
            // (just call run() which will post from accept completions)
            // Here we do 8 accepts total as baseline:
            for (int k = 0; k < 8; ++k) {
                // hack: call private PostAccept by creating a local lambda
                // Instead: do it in constructor? => simplest: call accept via server socket method
                // We'll just post directly here:
                AcceptContext* actx = new AcceptContext();
                std::memset(&actx->ov, 0, sizeof(WSAOVERLAPPED));
                SOCKET as = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_OVERLAPPED);
                if (as == INVALID_SOCKET) { delete actx; continue; }
                actx->acceptSock = as;

                DWORD bytes = 0;
                BOOL ok = _fnAcceptEx(
                    (SOCKET)_ServerSocket->getSocket(),
                    as,
                    actx->addrBuf,
                    0,
                    sizeof(SOCKADDR_STORAGE) + 16,
                    sizeof(SOCKADDR_STORAGE) + 16,
                    &bytes,
                    &actx->ov
                );
                if (!ok) {
                    int err = WSAGetLastError();
                    if (err != WSA_IO_PENDING) {
                        closesocket(as);
                        delete actx;
                        continue;
                    }
                }
                w.RegisterAccept(actx);
            }

		// Bind listening socket to IOCP (so AcceptEx completions are guaranteed to arrive)
		CreateIoCompletionPort((HANDLE)_ServerSocket->fd(), iocp, (ULONG_PTR)0, 0);
            w.run();
        });
    }
}

		// Load AcceptEx pointer
		LPFN_ACCEPTEX lpfnAcceptEx = nullptr;
		GUID GuidAcceptEx = WSAID_ACCEPTEX;
		DWORD dwBytes = 0;
void event::Stop() {
    if (!Running) return;
    Running = false;

		if (WSAIoctl((SOCKET)_ServerSocket->fd(), SIO_GET_EXTENSION_FUNCTION_POINTER,
			&GuidAcceptEx, sizeof(GuidAcceptEx),
			&lpfnAcceptEx, sizeof(lpfnAcceptEx),
			&dwBytes, NULL, NULL) == SOCKET_ERROR) {
			exception[NetException::Critical] << "IOCP: Could not load AcceptEx pointer";
			CloseHandle(iocp);
			throw exception;
    if (_Iocp) {
        // wake all workers
        for (size_t i = 0; i < _Workers.size(); ++i) {
            PostQueuedCompletionStatus(_Iocp, 0, 0, nullptr);
        }
    }

		int threads_count = (int)std::thread::hardware_concurrency();
		if (threads_count <= 0) threads_count = 4;
    for (auto& t : _Workers) {
        if (t.joinable()) t.join();
    }
    _Workers.clear();

		EventWorkerArgs a;
		a.timeout = _Timeout;
		a.iocp = iocp;
		a.ev = this;
		a.listenSock = _ServerSocket;
		a.lpfnAcceptEx = lpfnAcceptEx;
		a.userArgs = (void*)args;
    if (_Iocp) {
        CloseHandle(_Iocp);
        _Iocp = nullptr;
    }
}

		std::vector<std::thread> pool;
		pool.reserve(threads_count);
// ---- Connection management (used by worker)
// You likely already have these equivalents in epoll.cpp – here inlined.

		for (int i = 0; i < threads_count; i++) {
			pool.emplace_back([i, &a]() {
				EventWorker w(i, &a);
				});
void event::AddConnection(std::unique_ptr<con> c) {
    std::lock_guard<std::mutex> lk(_ConMutex);
    _Connections.push_back(std::move(c));
}

		// Pre-post accepts: 2 per CPU as you had
		for (int i = 0; i < threads_count * 2; i++) {
			post_accept(&a);
con* event::FindConnection(socket* s) {
    std::lock_guard<std::mutex> lk(_ConMutex);
    for (auto& c : _Connections) {
        if (c && c->csocket.get() == s) return c.get();
    }
    return nullptr;
}

		while (event::Running) {
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
con* event::FindConnectionBySocket(SOCKET sock) {
    std::lock_guard<std::mutex> lk(_ConMutex);
    for (auto& c : _Connections) {
        if (c && c->csocket && (SOCKET)c->csocket->getSocket() == sock) return c.get();
    }
    return nullptr;
}

		// Join workers
		for (auto& t : pool) t.join();
void event::CloseEvent(socket* s) {
    con* c = FindConnection(s);
    if (!c) return;

		CloseHandle(iocp);
    try {
        c->csocket->close();
    } catch (...) {}

    // remove from list
    std::lock_guard<std::mutex> lk(_ConMutex);
    for (auto it = _Connections.begin(); it != _Connections.end(); ++it) {
        if ((*it) && (*it)->csocket.get() == s) {
            _Connections.erase(it);
            break;
        }
    }
}

} // namespace netplus

#endif // Windows