asp.net实现长连接

前言

  在Web开发中,长连接(Long Connection)指的是客户端与服务器之间保持长时间的连接,而不是像传统的请求那样每次请求都建立一个新的连接。长连接可以用于实时通信、实时数据推送等场景。
  在双向通讯的需求场景中,长连接已经被SignalR方案或WebSocket替代了,但是从查缺补漏的角度,还是有必要看一下,有利于理解其深层道理。还有就是在不支持websocket的场景下可以用。

  请跟我一步一步来学习,不会有太大学历压力。

一个简单的长连接Demo

  首先看一个实时响应的一般处理程序。

public class Connection : IHttpHandler
{
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";
        context.Response.Write("Connect Success");
    }

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

 

  Connection实现了IhttpHandler接口,当我们访问Connection.ashx时,会实时返回Connect Success。

  再来看一个长连接的精简案例

public class LongPolling : IHttpAsyncHandler
{
    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        var asyncResult = new AsyncResult(context, cb, extraData);
        Task.Run(async () =>
        {
            await Task.Delay(5000);
            context.Response.Write("LongPolling");
            asyncResult.CompleteRequest();
        });
        return asyncResult;
    }
    public void EndProcessRequest(IAsyncResult result)
    {
        (result as AsyncResult)?.Dispose();
    }
    public void ProcessRequest(HttpContext context)
    {
    }
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

 

public class AsyncResult : IAsyncResult
{
    private readonly ManualResetEvent _waitHandle;
    public AsyncResult(HttpContext context, AsyncCallback cb, object extraData)
    {
        Context = context;
        CallBack = cb;
        ExtraData = extraData;
        _waitHandle = new ManualResetEvent(false);
    }
    public HttpContext Context
    {
        get;
        set;
    }
    public object ExtraData
    {
        get;
        set;
    }
    public AsyncCallback CallBack
    {
        get;
        set;
    }
    public void CompleteRequest()
    {
        this.IsCompleted = true;
        _waitHandle.Set();//通知其他等待线程
        CallBack?.Invoke(this);
    }
    public void Dispose() {
        _waitHandle.Dispose();
    }
    public bool IsCompleted { get; set; }
    public WaitHandle AsyncWaitHandle
    {
        get;
        set;
    }
    public object AsyncState
    {
        get;
        set;
    }
    public bool CompletedSynchronously
    {
        get { return false; }
    }
}

  效果:当我们访问LongPolling.ashx时,不会立刻返回结果而是五秒后返回。

  需要说明的是,LongPolling实现了IHttpAsyncHandler接口并在BeginProcessRequest返回了实现了IAsyncResult的自定义实例。是的,本案例就是借助IHttpAsyncHandler和IAsyncResult实现了长连接。请求进入BeginProcessRequest后我们可以立即返回asyncResult,然后可以在异步中去做耗时操作,直到结束。

  如何设定程序的结束和响应呢?

  1、context.Response.Write("LongPolling");设定输出内容和结果

  2、IAsyncResult.IsCompleted=true;

  3、 CallBack?.Invoke(this);调用回调函数。

  _waitHandle是阻塞其他线程调用的,可以不实现。截止到此就已经演示了一个长连接的实现Demo了。

会话管理

  为了更加贴近业务场景,我们可以添加一些会话管理来进一步丰富功能,像下面这样。

  添加一个客户端链接管理类,也可以理解为一个Session

public class WebClientConnection
{
    private static readonly ILog _logger = LogManager.GetLogger(typeof(WebClientConnection));

    public WebClientConnection(AsyncResult asyncResult)
    {
        this.CreateTime = DateTime.Now;
        this.SessionId = Guid.NewGuid().ToString().Substring(0, 5);
        this.AsyncResult = asyncResult;
    }

    /// <summary>
    /// 链接创建时间
    /// </summary>
    public DateTime CreateTime { get; set; }
    /// <summary>
    /// 会话唯一标识
    /// </summary>
    public string SessionId { get; set; }
    /// <summary>
    /// 每个连接对应的异步结果
    /// </summary>
    public AsyncResult AsyncResult { get; set; }
    /// <summary>
    /// 是否已经完成
    /// </summary>
    public bool IsCompleted
    {
        get
        {
            if (this.AsyncResult == null)
                return false;
            return this.AsyncResult.IsCompleted;
        }
    }
    /// <summary>
    /// 给客户端发送消息
    /// </summary>
    /// <param name="message"></param>
    public void Send(string message)
    {
        if (this.IsCompleted)
        {
            _logger.Warn("连接已经完成,无法发送消息");
            return;
        }
        this.AsyncResult.Send(message);
    }
}

 

添加一个Session管理类
public class SessionManager
{
    private static List<WebClientConnection> _connections = new List<WebClientConnection>();
    private static readonly ILog logger = LogManager.GetLogger(typeof(SessionManager));
    private static object syncObj = new object();
    public static void AddConnection(WebClientConnection connection)
    {
        lock (syncObj)
        {
            _connections.Add(connection);
        }
    }
    public static void RemoveConnection(WebClientConnection connection)
    {
        lock (syncObj)
        {
            _connections.Remove(connection);
        }
    }
    public static WebClientConnection GetConnection(string sessionId)
    {
        lock (syncObj)
        {
            return _connections.FirstOrDefault(c => c.SessionId == sessionId);
        }
    }
    public static void SendAll(string message)
    {
        lock (syncObj)
        {
            foreach (var connection in _connections)
            {
                connection.Send(message);
            }
        }
    }
    public static void Clean()
    {
        // 清理5分钟前的连接或者已经完成的连接
        lock (syncObj)
        {
            var now = DateTime.Now;
            var connections = _connections.Where(c => c == null || c.CreateTime.AddMinutes(5) < now || c.IsCompleted).ToList();
            foreach (var connection in connections)
            {
                _connections.Remove(connection);
            }
            logger.InfoFormat("清理连接数:{0}", connections.Count);
        }
    }

 

 AsyncResult类相应增加一个Send方法,方便外部调用。

/// <summary>
/// 发送消息
/// </summary>
/// <param name="message"></param>
public void Send(string message)
{
    if (this.IsCompleted)
    {
        return;
    }
    if (string.IsNullOrEmpty(message))
    {
        return;
    }
    try
    {
        this.Context.Response.ContentType = "text/plain";
        this.Context.Response.Write(message);
    }
    catch (Exception ex)
    {
        logger.Error("输出到客户端发生错误:" + ex.Message);
    }
    finally
    {
        this.CompleteRequest();
    }
}
View Code

 

然后我们稍微改造一下Connextion.ashx,使得用户可以通过这里模拟登录,拿到sessionid。

 public class Connection : IHttpHandler
 {
     private static readonly ILog logger = LogManager.GetLogger(typeof(Connection));
     public void ProcessRequest(HttpContext context)
     {
         context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
         context.Response.ContentType = "text/plain";
         try
         {
             //假装登录成功,返回一个会话标识
             WebClientConnection webClientConnection = new WebClientConnection(null);
             SessionManager.AddConnection(webClientConnection);
             context.Response.Write(webClientConnection.SessionId);
         }
         catch (Exception ex)
         {
             logger.Error("创建连接发生错误:" + ex.Message);
         }
     }

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

 

然后改造一下LongPolling.ashx,使得用户可以通过sessionid来进行长连接请求。

 public class LongPolling : IHttpAsyncHandler
 {
     public static readonly string SESSIONIDNAME = "sessionid";
     private static readonly ILog logger = LogManager.GetLogger(typeof(LongPolling));
     public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
     {
         context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
         var sessionId = context.Request.Params.Get(SESSIONIDNAME);
         var asyncResult = new AsyncResult(context, cb, extraData);
         if (string.IsNullOrEmpty(sessionId))
         {
             var error = "500 SessionId is null";
             context.Response.StatusCode = 500;
             logger.Error(error);
             asyncResult.Send(error);
         }
         var connection = SessionManager.GetConnection(sessionId);
         if (connection == null)
         {
             var error = "404 SessionId:" + sessionId + " has no connection.";
             context.Response.StatusCode = 404;
             logger.Error(error);
             asyncResult.Send(error);
         }
         //将新的异步请求实例关联对应的connection/session
         connection.AsyncResult = asyncResult;
         return asyncResult;
     }
     public void EndProcessRequest(IAsyncResult result)
     {
         (result as AsyncResult)?.Dispose();
     }
     public void ProcessRequest(HttpContext context)
     {
     }
     public bool IsReusable
     {
         get
         {
             return false;
         }
     }
 }

 

最后在global.asax中做一个延时响应,模拟长连接请求

public class Global : HttpApplication
{

    void Application_Start(object sender, EventArgs e)
    {
        Task.Run(() =>
        {
            //3分钟后给所有客户端发送消息
            Task.Delay(3 * 60 * 1000).ContinueWith(t =>
            {
                Http.SessionManager.SendAll("服务器消息:3分钟到了");
            });
        });
    }
}

 

接下来的效果就是,先拿到sessionid,可以请求多次,拿到多个。

然后带着sessionid请求longpolling.ashx,间隔几分钟后获取到结果返回。

 进一步完善,定时推送和自动清理

引入Quartz.Net完成后台调度

增加清理任务

public class CleanJob : IJob
{
    private ILog logger = LogManager.GetLogger(typeof(CleanJob));
    public void Execute(IJobExecutionContext context)
    {
        //清理sesion
        SessionManager.Clean();
        logger.Info("完成一次清理任务:" + DateTime.Now.ToLongTimeString());
    }
}

增加心跳回复

public class HeartJob : IJob
{
    private ILog logger = LogManager.GetLogger(typeof(CleanJob));
    public void Execute(IJobExecutionContext context)
    {
        var time = DateTime.Now.ToLongTimeString();
        SessionManager.SendAll("服务器返回,当前时间:" + time);
        logger.Info("完成一次客户端回复:" + time);
    }
}

Global核心代码

void Application_Start(object sender, EventArgs e)
{
    log4net.Config.XmlConfigurator.Configure();
    //启动清理任务调度
    NameValueCollection config = new NameValueCollection();
    config["quartz.scheduler.instanceName"] = "SchedulerTest_Scheduler";
    config["quartz.scheduler.instanceId"] = "AUTO";
    config["quartz.threadPool.threadCount"] = "2";
    config["quartz.threadPool.type"] = "Quartz.Simpl.SimpleThreadPool, Quartz";

    sched = new StdSchedulerFactory(config).GetScheduler();

    IJobDetail job = JobBuilder.Create()
                               .OfType<CleanJob>()
                               .WithIdentity("c1")
                               .StoreDurably()
                               .Build();

    string cronExpr = ConfigurationManager.AppSettings["cronExpr"];

    var trigger = (ICronTrigger)TriggerBuilder.Create()
                         .WithIdentity("c1")
                         .WithCronSchedule(cronExpr)
                         .Build();

    sched.ScheduleJob(job, trigger);

    IJobDetail job2 = JobBuilder.Create()
                               .OfType<HeartJob>()
                               .WithIdentity("c30")
                               .StoreDurably()
                               .Build();

    string hertExpr = ConfigurationManager.AppSettings["heartExpr"];
    var heartTrigger = (ICronTrigger)TriggerBuilder.Create()
             .WithIdentity("c30")
             .WithCronSchedule(hertExpr)
             .Build();
    sched.ScheduleJob(job2, heartTrigger);

    sched.Start();
} 

 进一步完善,增加客户端调用效果

 

<script type="text/javascript" src="public/jquery-1.7.2.min.js"></script>
<script type="text/javascript">

    function requestLongHttp(sessionid) {
        $.post("LongPolling.ashx", { 'sessionid': sessionid }, function (r) {
            if (r) {
                requestLongHttp(sessionid);
                //给id为showMsg的标签赋值
                $("#showMsg").html(r);
            }
        }, 'text');
    }
    $(function () {
        $("#btnConnect").click(function () {
            $.post("connection.ashx", {}, function (r) {
                if (r) {
                    $("#txtSessionId").val(r);
                    requestLongHttp(r);
                }
            }, 'text');

        });
    });
</script>
 <div>
     <input id="btnConnect" type="button" value="连接" /><br />
     sessionid:<input id="txtSessionId" readonly="readonly" type="text" value="" /><br />
     <label id="showMsg"></label>
 </div>

最终效果

开多个页面请求进行请求后会挂起,30秒后统一获取结果返回

 

源码地址:https://gitee.com/xiaoqingyao/keep-alive-http-demo.git

参考:Asp.Net实现Http长连接推送 - 奥玛尔 - 博客园

参考:deepseek

posted @ 2025-02-26 19:00  chenxizhaolu  阅读(130)  评论(0)    收藏  举报