ASP.NET DIYの圗星

少し前たで、倧芏暡なASP.NETプロゞェクトの開発の䞀環ずしお、次のサブタスクが発生したした。リアルタむムで曎新される衚圢匏デヌタの芖芚衚瀺を実装するこずです。 曎新スキヌムは非垞に単玔です。぀たり、デヌタはQueryStringを介しおサヌバヌに送信されたす。これにより、ペヌゞ䞊の叀いデヌタをできるだけ早く、ペヌゞなしでペヌゞを曎新するこずなく眮き換える必芁がありたす。 私がすぐに思い぀いた最初の解決策は、タむマヌAJAXリク゚ストの確立されたテクニックを、たずえば5秒ごずに䜿甚するこずでした。 しかし、このアプロヌチを適甚するこずの明らかな欠点はすぐに明らかになりたしたたず、かなりの数の朜圚的なクラむアントが毎回新しい接続を䜜成するたびに5秒ごずにサヌバヌをぎくぎく動かしたす。結局のずころ、仮想的には、デヌタは1秒あたり数回でもサヌバヌに到着する可胜性がありたすたたは、数分でも到着しない堎合がありたす。これは「最初」の堎合です。

この゜リュヌションのアむデアは、Webチャットを䜜成するためのPerlでのCometテクノロゞヌの実装に぀いお説明したHabréの蚘事ぞのリンクを共有した同僚から思いがけなく思い぀いたものです。 「 圗星はあなたが必芁ずするものです 」ず私たちは考え、私はこのこずをASP.NETにねじ蟌む方法を芋぀け始めたした。 実際には、カットの䞋で䜕が議論されたすか。



たず、Cometずは䜕かを考えたしょう。 りィキペディアがこれに぀いお私たちに䌝えおいるこずは次のずおりです。

Comet Web開発は、Webアプリケヌションのモデルを説明する新語です。HTTP接続を䜿甚するず、Webサヌバヌは、ブラりザヌからの远加芁求なしにブラりザヌにデヌタをプッシュできたす。 コメットは、この盞互䜜甚を達成するためのさたざたな手法を瀺すために䜿甚されるハむパヌネヌムです。 これらのメ゜ッドの共通点は、独自のプラグむンではなく、JavaScriptなどのブラりザヌで盎接サポヌトされおいるテクノロゞヌに基づいおいるこずです。 理論的には、Cometのアプロヌチは、ブラりザがペヌゞを曎新するためにペヌゞ党䜓たたは䞀郚を芁求するWorld Wide Webの元の抂念ずは異なりたす。 ただし、実際には、Cometアプリケヌションは通垞、長いポヌリングでAjaxを䜿甚しおサヌバヌ䞊の新しい情報を確認したす。

ですから、この定矩から私たちがずれるキヌワヌドは「Ajax with long polling」です。 それは䜕で、䜕ず䞀緒に食べられたすか 「ロングポヌリング」テクノロゞヌを䜿甚する堎合、クラむアントはサヌバヌにリク゚ストを送信し、...埅機したす。 サヌバヌに新しいデヌタが衚瀺されるのを埅ちたす。 デヌタが到着するず、サヌバヌはそのデヌタをクラむアントに送信し、その埌、新しいリク゚ストを送信しお再び埅機したす。 「゚ンドレスリク゚スト」の代替技術。たずえば、いわゆる 「Forever iframe」 ここでもう少し読むこずができたす は、垞に適甚できるものずはほど遠いです。 タむムアりトなど、これたで誰もキャンセルしおいたせん。

たあ、タスクは非垞に明確です-利甚可胜なツヌルAJAX + ASP.NETで䞊蚘の長いポヌリングを実装する必芁がありたす。 これは最初の問題に぀ながりたす。぀たり、サヌバヌに顧客に送信できる新しいデヌタがあるたでそしお明らかに耇数のクラむアントがあるたで、着信芁求を保存し、応答を提䟛しない方法です。 そしお、ここで非同期HTTPハンドラヌが助けになりたす。

public interface IHttpAsyncHandler : IHttpHandler
{
IAsyncResult BeginProcessRequest( HttpContext ctx,
AsyncCallback cb,
object obj);
void EndProcessRequest(IAsyncResult ar);
}


* This source code was highlighted with Source Code Highlighter .


IHttpHandlerむンタヌフェむスからではなく、IHttpAsyncHandlerからクラスを継承したす。IHttpAsyncHandlerは、䜿い慣れたProcessRequestメ゜ッドず共にBeginProcessRequestずEndProcessRequestの2぀の新しいメ゜ッドを提䟛したす。 特に、最初のものに興味がありたす。なぜなら、 ぀たり、リク゚ストの凊理の開始時に、このリク゚ストを手で取埗する必芁があり、Xが来るたで攟さないでください。 ご芧のずおり、BeginProcessRequestはIAsyncResultむンタヌフェむスを実装するオブゞェクトを返したす。

public interface IAsyncResult
{
public object AsyncState { get ; }
public bool CompletedSynchronously { get ; }
public bool IsCompleted { get ; }
public WaitHandle AsyncWaitHandle { get ; }
}


* This source code was highlighted with Source Code Highlighter .


指定されたむンタヌフェむスを実装する新しいクラスを䜜成し、BeginProcessRequestに送信された芁求デヌタず独自のclientGuidパラメヌタヌのリポゞトリずしおも機胜したす。これは、サヌバヌに接続するクラむアントの䞀意の識別子ずしお将来䜿甚し、䜕らかの方法で芁求を識別したす。

public class CometAsyncRequestState : IAsyncResult
{
private HttpContext _currentContext;
private AsyncCallback _asyncCallback;
private Object _extraData;

private Boolean _isCompleted;
private Guid _clientGuid;
private ManualResetEvent _callCompleteEvent = null ;

public CometAsyncRequestState( HttpContext currentContext, AsyncCallback asyncCallback, Object extraData)
{
_currentContext = currentContext;
_asyncCallback = asyncCallback;
_extraData = extraData;

_isCompleted = false ;
}

public void CompleteRequest()
{
_isCompleted = true ;

lock ( this )
{
if (_callCompleteEvent != null )
_callCompleteEvent.Set();
}

if (_asyncCallback != null )
{
_asyncCallback( this );
}
}

public HttpContext CurrentContext
{
get
{
return _currentContext;
}
set
{
_currentContext = value ;
}
}

public AsyncCallback AsyncCallback
{
get
{
return _asyncCallback;
}
set
{
_asyncCallback = value ;
}
}

public Object ExtraData
{
get
{
return _extraData;
}
set
{
_extraData = value ;
}
}

public Guid ClientGuid
{
get
{
return _clientGuid;
}
set
{
_clientGuid = value ;
}
}

// IAsyncResult implementations
public Boolean CompletedSynchronously
{
get
{
return false ;
}
}

public Boolean IsCompleted
{
get
{
return _isCompleted;
}
}

public Object AsyncState
{
get
{
return _extraData;
}
}

public WaitHandle AsyncWaitHandle
{
get
{
lock ( this )
{
if (_callCompleteEvent == null )
_callCompleteEvent = new ManualResetEvent( false );

return _callCompleteEvent;
}
}
}
}


* This source code was highlighted with Source Code Highlighter .


ご芧のずおり、CompleteRequest関数を呌び出すたで、リク゚ストは完了したず芋なされたせん。 玠晎らしい-それが必芁です。 これらの着信リク゚ストを保存する堎所はどこかにのみ残っおいたす。 この関数およびリク゚スト凊理関数に察しお、静的クラスCometClientProcessorを䜜成したす。

public static class CometClientProcessor
{
private static Object _lockObj;
private static List <CometAsyncRequestState> _clientStateList;

static CometClientProcessor()
{
_lockObj = new Object();
_clientStateList = new List <CometAsyncRequestState>();
}

public static void PushData( String pushedData)
{
List <CometAsyncRequestState> currentStateList = new List <CometAsyncRequestState>();

lock (_lockObj)
{
foreach (CometAsyncRequestState clientState in _clientStateList)
{
currentStateList.Add(clientState);
}
}

foreach (CometAsyncRequestState clientState in currentStateList)
{
if (clientState.CurrentContext.Session != null )
{
clientState.CurrentContext.Response.Write(pushedData);
clientState.CompleteRequest();
}
}
}

public static void AddClient(CometAsyncRequestState state)
{
Guid newGuid;

lock (_lockObj)
{
while ( true )
{
newGuid = Guid .NewGuid();
if (_clientStateList.Find(s => s.ClientGuid == newGuid) == null )
{
state.ClientGuid = newGuid;
break ;
}
}

_clientStateList.Add(state);
}
}

public static void UpdateClient(CometAsyncRequestState state, String clientGuidKey)
{
Guid clientGuid = new Guid (clientGuidKey);

lock (_lockObj)
{
CometAsyncRequestState foundState = _clientStateList.Find(s => s.ClientGuid == clientGuid);

if (foundState != null )
{
foundState.CurrentContext = state.CurrentContext;
foundState.ExtraData = state.ExtraData;
foundState.AsyncCallback = state.AsyncCallback;
}
}
}

public static void RemoveClient(CometAsyncRequestState state)
{
lock (_lockObj)
{
_clientStateList.Remove(state);
}
}
}


* This source code was highlighted with Source Code Highlighter .


CometClientProcessorには、珟圚保持されおいるリク゚ストのリスト、リク゚ストを远加するためのAddClient関数新しいクラむアントを接続する堎合、リク゚ストを曎新するためのUpdateClient既に接続されおいるクラむアントが新しいリク゚ストを送信する堎合、およびリク゚ストを削陀するためのRemoveClientクラむアントが切断する堎合、メむンPushDataメ゜ッド。 わかりやすくするために、最も単玔なデヌタ、぀たりURLのパラメヌタヌを介しおサヌバヌに送られる行を「プッシュ」したす。 ご芧のずおり、すべおが非垞に単玔です。珟圚保持されおいる芁求を実行し、サヌバヌからのデヌタを応答に曞き蟌み、CompleteRequest関数を呌び出しお、芁求を解攟し、クラむアントに応答を送信したす。 この䟋では、唯䞀のペヌゞのPage_Load関数からPushData呌び出しが行われたす。

protected void Page_Load( object sender, EventArgs e)
{
if (!IsPostBack)
{
if (Request.QueryString[ "x" ] != null )
{
CometClientProcessor.PushData(Request.QueryString[ "x" ].ToString());
}
}
}


* This source code was highlighted with Source Code Highlighter .


前述のように、デヌタはURLのパラメヌタヌを介しお取埗されたす。この堎合、わかりやすくするために「x」ず呌ばれたす。 サヌバヌ郚分では、実際には非同期ハンドラヌ自䜓を実装するだけです。 しかし、最初に、クラむアント郚分に目を向けおjQueryラむブラリヌの助けなしではなくいく぀かのかなり普通のJavaScript関数を䜜成したしょう。

var clientGuid

$( document ).ready( function () {
var str = window.location.href;
if (str.indexOf( "?" ) < 0)
Connect();
});

$(window).unload( function () {
var str = window.location.href;
if (str.indexOf( "?" ) < 0)
Disconnect();
});

function SendRequest() {
var url = './CometAsyncHandler.ashx?cid=' + clientGuid;
$.ajax({
type: "POST" ,
url: url,
success: ProcessResponse,
error: SendRequest
});
}

function Connect() {
var url = './CometAsyncHandler.ashx?cpsp=CONNECT' ;
$.ajax({
type: "POST" ,
url: url,
success: OnConnected,
error: ConnectionRefused
});
}

function Disconnect() {
var url = './CometAsyncHandler.ashx?cpsp=DISCONNECT' ;
$.ajax({
type: "POST" ,
url: url
});
}

function ProcessResponse(transport) {
$( "#contentWrapper" ).html(transport);
SendRequest();
}

function OnConnected(transport) {
clientGuid = transport;
SendRequest();
}

function ConnectionRefused() {
$( "#contentWrapper" ).html( "Unable to connect to Comet server. Reconnecting in 5 seconds..." );
setTimeout(Connect(), 5000);
}


* This source code was highlighted with Source Code Highlighter .


ドキュメントが読み蟌たれるずすぐに、URLでパラメヌタヌの存圚を確認しパラメヌタヌ化されたURL、もう䞀床お知らせしたす-これはプッシュのためにサヌバヌにデヌタを送信しおいたす、Connect関数を呌び出したす。 これは、順番に、すでにハンドラヌずの通信を開始しおいたす。 わかりやすいように、アクションを定矩するサヌビスワヌドCONNECT / DISCONNECTは、cpspパラメヌタヌを介しお枡されたす。 したがっお、ConnectはサヌバヌでAddClient呌び出しを開始し、Disconnect-RemoveClientを開始する必芁がありたす。 接続が確立され、クラむアントがclientGuidを受信するず、SendRequest関数が呌び出されたす。これは、クラむアントが接続を切断するこずを決定するたでサヌバヌを「ロングフィル」したす。 各SendRequest呌び出しは、サヌバヌ䞊でUpdateClient関数の実行を開始し、このクラむアントはコンテキストずコヌルバックを曎新したす。

さお、ほずんどすべおの準備が敎ったので、䞊蚘のメカニズム党䜓の䞭栞である非同期ハンドラヌを実装するずきが来たした。

public enum ConnectionCommand
{
CONNECT,
DISCONNECT
}

public static class ConnectionProtocol
{
public static String PROTOCOL_GET_PARAMETER_NAME = "cpsp" ;
public static String CLIENT_GUID_PARAMETER_NAME = "cid" ;
}


* This source code was highlighted with Source Code Highlighter .


<%@ WebHandler Language= "C#" Class= "CometAsyncHandler" %>

using System;
using System.Web;

using DevelopMentor;

public class CometAsyncHandler : IHttpAsyncHandler, System.Web.SessionState.IRequiresSessionState
{
static private ThreadPool _threadPool;

static CometAsyncHandler()
{
_threadPool = new ThreadPool(2, 50, "Comet Pool" );
_threadPool.PropogateCallContext = true ;
_threadPool.PropogateThreadPrincipal = true ;
_threadPool.PropogateHttpContext = true ;
_threadPool.Start();
}

public IAsyncResult BeginProcessRequest( HttpContext ctx, AsyncCallback cb, Object obj)
{
CometAsyncRequestState currentAsyncRequestState = new CometAsyncRequestState(ctx, cb, obj);
_threadPool.PostRequest( new WorkRequestDelegate(ProcessServiceRequest), currentAsyncRequestState);

return currentAsyncRequestState;
}

private void ProcessServiceRequest(Object state, DateTime requestTime)
{
CometAsyncRequestState currentAsyncRequestState = state as CometAsyncRequestState;

if (currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.PROTOCOL_GET_PARAMETER_NAME] ==
ConnectionCommand.CONNECT.ToString())
{
CometClientProcessor.AddClient(currentAsyncRequestState);
currentAsyncRequestState.CurrentContext.Response.Write(currentAsyncRequestState.ClientGuid.ToString());
currentAsyncRequestState.CompleteRequest();
}
else if (currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.PROTOCOL_GET_PARAMETER_NAME] ==
ConnectionCommand.DISCONNECT.ToString())
{
CometClientProcessor.RemoveClient(currentAsyncRequestState);
currentAsyncRequestState.CompleteRequest();
}
else
{
if (currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.CLIENT_GUID_PARAMETER_NAME] != null )
{
CometClientProcessor.UpdateClient(currentAsyncRequestState,
currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.CLIENT_GUID_PARAMETER_NAME].ToString());
}
}
}

public void EndProcessRequest(IAsyncResult ar)
{
}

public void ProcessRequest( HttpContext context)
{
}

public bool IsReusable
{
get
{
return true ;
}
}
}

* This source code was highlighted with Source Code Highlighter .

䞊蚘のすべおの埌、泚意深い読者が持぀かもしれない唯䞀の質問は、「カスタムスレッドプヌルを䜿甚する理由」です。 完党に明らかではありたせんが、答えは非垞に単玔です。ASP.NETトレッドプヌルのワヌクフロヌをできるだけ早く「解攟」しお、着信芁求の凊理を継続し、芁求の盎接凊理を「内郚」スレッドに転送できるようにするためです。 これが行われない堎合、十分な数の着信芁求があるず、䞀芋したずころ䞀芋銬鹿げた「ギャグ」が発生する可胜性がありたす。「ASP.NETはワヌクフロヌを䜿い果たしたした」。 同じ理由で、BeginInvokeメ゜ッドたたは暙準のスレッドプヌルメ゜ッドThreadPool.QueueUserWorkItemによっお励起された非同期デリゲヌトを䜿甚するこずはできたせん。 どちらの堎合も、スレッドは同じASP.NETトレッドミルから削陀され、「石鹞で瞫い付けられた」状況になりたす。 この䟋では、Mike Woodringによっお実装されたカスタムスレッドプヌルが䜿甚されたす。 これず圌の他の倚くの発展はここで芋られたす 。

基本的には以䞊です。 圓初のように難しくはありたせんでした。 クラむアントはDefault.aspxを呌び出しおCometサヌバヌに接続し、GETパラメヌタヌala Default.aspxX = Happy_New_Yearを同じペヌゞに枡すこずでデヌタをプッシュしたす。 残念ながら、このアプロヌチのスケヌラビリティの倧芏暡なテストはただ可胜ではありたせんが、だれかがこれに぀いおアむデアを持っおいるなら、曞いお、恥ずかしがらないでください。

ご枅聎ありがずうございたした。

UPD サンプルプロゞェクトのリンクをアヌカむブに远加したす 〜30 KB。 衚瀺方法VSでは、CometPage.aspxをスタヌトペヌゞずしお蚭定し、起動し、ブラりザヌ/ブラりザヌで同じURLで耇数のタブを開きこれらのブラりザヌでの同時接続数の制限を芚えおいるだけです 、パラメヌタヌを远加したすかX = [any_text]を開き、開いおいるすべおのタブでパラメヌタヌの倀がどのように衚瀺されるかを確認したす。

Source: https://habr.com/ru/post/J82015/


All Articles