ASP.NETにおける長い接続の実装手法

シンプルな長い接続デモ

リアルタイム応答の基本ハンドラ実装例:

public class SimpleHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext ctx) 
    {
        ctx.Response.ContentType = "text/plain";
        ctx.Response.Write("接続成功");
    }

    public bool IsReusable => false;
}

長時間接続を実現する非同期ハンドラ:

public class AsyncPollingHandler : IHttpAsyncHandler
{
    public IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback callback, object state)
    {
        var asyncOp = new PollingOperation(ctx, callback, state);
        Task.Run(async () => 
        {
            await Task.Delay(5000);
            ctx.Response.Write("ポーリング応答");
            asyncOp.Complete();
        });
        return asyncOp;
    }
    
    public void EndProcessRequest(IAsyncResult ar) => (ar as PollingOperation)?.Dispose();
    public void ProcessRequest(HttpContext ctx) { }
    public bool IsReusable => false;
}

public class PollingOperation : IAsyncResult
{
    private readonly ManualResetEvent _syncLock;
    public PollingOperation(HttpContext ctx, AsyncCallback cb, object data)
    {
        Context = ctx;
        Callback = cb;
        _syncLock = new ManualResetEvent(false);
    }
    
    public HttpContext Context { get; }
    public AsyncCallback Callback { get; }
    
    public void Complete()
    {
        IsCompleted = true;
        _syncLock.Set();
        Callback?.Invoke(this);
    }
    
    public void Dispose() => _syncLock.Dispose();
    public bool IsCompleted { get; private set; }
    public WaitHandle AsyncWaitHandle => _syncLock;
    public object AsyncState => null;
    public bool CompletedSynchronously => false;
}

セッション管理システム

クライアント接続管理クラス:

public class ClientSession
{
    public ClientSession(PollingOperation op)
    {
        Created = DateTime.Now;
        SessionID = Guid.NewGuid().ToString("N")[..8];
        Operation = op;
    }

    public DateTime Created { get; }
    public string SessionID { get; }
    public PollingOperation Operation { get; set; }
    public bool IsCompleted => Operation?.IsCompleted ?? false;

    public void Send(string msg)
    {
        if (IsCompleted) return;
        Operation.Context.Response.Write(msg);
        Operation.Complete();
    }
}

セッション管理クラス:

public class SessionPool
{
    private static readonly List<ClientSession> _sessions = new();
    private static readonly object _lock = new();

    public static void Add(ClientSession session)
    {
        lock (_lock) { _sessions.Add(session); }
    }

    public static void Remove(ClientSession session)
    {
        lock (_lock) { _sessions.Remove(session); }
    }

    public static ClientSession Find(string id)
    {
        lock (_lock) { return _sessions.FirstOrDefault(s => s.SessionID == id); }
    }

    public static void Broadcast(string msg)
    {
        lock (_lock) 
        { 
            foreach (var session in _sessions) 
                session.Send(msg); 
        }
    }

    public static void CleanExpired()
    {
        lock (_lock)
        {
            var expired = _sessions
                .Where(s => s.Created.AddMinutes(5) < DateTime.Now || s.IsCompleted)
                .ToList();
                
            foreach (var session in expired) 
                _sessions.Remove(session);
        }
    }
}

統合実装例

接続初期化ハンドラ:

public class InitHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext ctx)
    {
        ctx.Response.Cache.SetCacheability(HttpCacheability.NoCache);
        var session = new ClientSession(null);
        SessionPool.Add(session);
        ctx.Response.Write(session.SessionID);
    }
    public bool IsReusable => false;
}

非同期ポーリングハンドラ改良版:

public class EnhancedPollingHandler : IHttpAsyncHandler
{
    public IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback cb, object data)
    {
        var sessionId = ctx.Request["sessionid"];
        var asyncOp = new PollingOperation(ctx, cb, data);
        
        if (string.IsNullOrEmpty(sessionId))
        {
            ctx.Response.StatusCode = 400;
            asyncOp.Send("セッションIDが必要");
            return asyncOp;
        }
        
        var session = SessionPool.Find(sessionId);
        session.Operation = asyncOp;
        return asyncOp;
    }
    
    // その他のメソッドは前出と同じ
}

バックグラウンド処理

定期的なブロードキャスト実装:

public class Global : HttpApplication
{
    protected void Application_Start()
    {
        Task.Run(async () => 
        {
            while (true)
            {
                await Task.Delay(180000);
                SessionPool.Broadcast("サーバー通知: 3分経過");
                SessionPool.CleanExpired();
            }
        });
    }
}

クライアントサイド実装

<script>
function startPolling(sessionId) {
    $.post("/EnhancedPollingHandler.ashx", { sessionid: sessionId }, function(res) {
        $("#output").text(res);
        startPolling(sessionId);
    });
}

$("#connect").click(function() {
    $.post("/InitHandler.ashx", function(sessionId) {
        $("#sessionId").val(sessionId);
        startPolling(sessionId);
    });
});
</script>

<button id="connect">接続</button>
<input id="sessionId" readonly>
<div id="output"></div>

タグ: ASP.NET C# HTTP-Long-Polling IHttpAsyncHandler Session-Management

7月1日 22:01 投稿