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(); } }
然后我们稍微改造一下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

浙公网安备 33010602011771号