2017-06-13 17 views
6

Tôi đang làm việc trên một giải pháp sử dụng giao thức socket web để thông báo cho khách hàng (trình duyệt web) khi một số sự kiện xảy ra trên máy chủ (ứng dụng web MVC Core). Tôi sử dụng Microsoft.AspNetCore.WebSockets nuget.MVC Core, Web Sockets and threading

Đây là mã client-side của tôi:

$(function() { 
    var socket = new WebSocket("ws://localhost:61019/data/openSocket"); 

    socket.onopen = function() { 
     $(".socket-status").css("color", "green"); 
    } 

    socket.onmessage = function (message) { 
     $("body").append(document.createTextNode(message.data)); 
    } 

    socket.onclose = function() { 
     $(".socket-status").css("color", "red"); 
    } 
    }); 

Khi quan điểm này được nạp theo yêu cầu ổ cắm ngay lập tức được gửi đến các ứng dụng MVC Core. Đây là hành động của bộ điều khiển:

[Route("data")] 
public class DataController : Controller 
{ 
    [Route("openSocket")] 
    [HttpGet] 
    public ActionResult OpenSocket() 
    { 
     if (HttpContext.WebSockets.IsWebSocketRequest) 
     { 
      WebSocket socket = HttpContext.WebSockets.AcceptWebSocketAsync().Result; 

      if (socket != null && socket.State == WebSocketState.Open) 
      { 
       while (!HttpContext.RequestAborted.IsCancellationRequested) 
       { 
        var response = string.Format("Hello! Time {0}", System.DateTime.Now.ToString()); 
        var bytes = System.Text.Encoding.UTF8.GetBytes(response); 

        Task.Run(() => socket.SendAsync(new System.ArraySegment<byte>(bytes), 
         WebSocketMessageType.Text, true, CancellationToken.None)); 
        Thread.Sleep(3000); 
       } 
      } 
     } 
     return new StatusCodeResult(101); 
    } 
} 

Mã này hoạt động rất tốt. WebSocket ở đây được sử dụng riêng cho việc gửi và không nhận bất cứ thứ gì. Tuy nhiên, vấn đề là vòng lặp while giữ nguyên chuỗi DataController cho đến khi yêu cầu hủy được phát hiện.

Ổ cắm web ở đây bị ràng buộc với đối tượng HttpContext. Ngay sau khi HttpContext cho yêu cầu web bị phá hủy, kết nối socket sẽ bị đóng ngay lập tức.

Câu hỏi 1: Có cách nào mà ổ cắm có thể được bảo quản bên ngoài luồng bộ điều khiển không? Tôi đã thử đưa nó vào một singleton sống trong lớp MVC Core Startup đang chạy trên chuỗi ứng dụng chính. Có cách nào để giữ cho các ổ cắm mở hoặc thiết lập kết nối một lần nữa từ bên trong thread ứng dụng chính chứ không phải giữ giữ thread điều khiển với một vòng lặp while? Thậm chí nếu nó được coi là OK để giữ thread điều khiển cho kết nối socket vẫn mở, tôi không thể nghĩ ra bất kỳ mã tốt nào để đặt bên trong vòng lặp while của OpenSocket. Bạn nghĩ gì về việc có một sự kiện đặt lại thủ công trong bộ điều khiển và đợi nó được đặt bên trong vòng lặp while trong hành động OpenSocket?

Câu hỏi 2: Nếu không thể tách các đối tượng HttpContext và WebSocket trong MVC, các công nghệ thay thế hoặc các mẫu phát triển khác có thể được sử dụng để đạt được tái sử dụng kết nối socket? Nếu bất cứ ai nghĩ rằng SignalR hoặc một thư viện tương tự có một số mã cho phép có ổ cắm độc lập từ HttpContext, hãy chia sẻ một số mã ví dụ. Nếu ai đó nghĩ rằng có một thay thế tốt hơn cho MVC cho kịch bản cụ thể này, xin vui lòng cung cấp một ví dụ, tôi không nhớ chuyển sang ASP.NET tinh khiết hoặc Web API, nếu MVC không có khả năng để xử lý truyền thông socket độc lập.

Câu hỏi 3: Yêu cầu là giữ kết nối ổ cắm còn sống hoặc có thể kết nối lại cho đến khi hết thời gian chờ hoặc yêu cầu hủy bỏ bởi người dùng. Ý tưởng là một số sự kiện độc lập xảy ra trên máy chủ kích hoạt ổ cắm được thiết lập để gửi dữ liệu. Nếu bạn nghĩ rằng một số công nghệ khác với ổ cắm web sẽ hữu ích hơn cho kịch bản này (như HTML/2 hoặc truyền trực tuyến), bạn có thể mô tả mô hình và khuôn khổ bạn sẽ sử dụng không?

P.S. Giải pháp có thể là gửi yêu cầu AJAX mỗi giây để hỏi xem có dữ liệu mới trên máy chủ hay không. Đây là phương sách cuối cùng.

+0

'SignalR' trông rất hứa hẹn nhưng không thể cung cấp bất kỳ mã nào cho bạn vì không biết nhiều về nó. – VMAtm

Trả lời

4

Sau khi nghiên cứu kéo dài, tôi đã kết thúc với giải pháp phần mềm trung gian tùy chỉnh. Đây là lớp trung gian của tôi:

 public class SocketMiddleware 
    { 
     private static ConcurrentDictionary<string, SocketMiddleware> _activeConnections = new ConcurrentDictionary<string, SocketMiddleware>(); 
     private string _packet; 

     private ManualResetEvent _send = new ManualResetEvent(false); 
     private ManualResetEvent _exit = new ManualResetEvent(false); 
     private readonly RequestDelegate _next; 

     public SocketMiddleware(RequestDelegate next) 
     { 
      _next = next; 
     } 

     public void Send(string data) 
     { 
      _packet = data; 
      _send.Set(); 
     } 

     public async Task Invoke(HttpContext context) 
     { 
      if (context.WebSockets.IsWebSocketRequest) 
      {  
       string connectionName = context.Request.Query["connectionName"]); 
       if (!_activeConnections.Any(ac => ac.Key == connectionName)) 
       { 
        WebSocket socket = await context.WebSockets.AcceptWebSocketAsync(); 
        if (socket == null || socket.State != WebSocketState.Open) 
        { 
         await _next.Invoke(context); 
         return; 
        } 
        Thread sender = new Thread(() => StartSending(socket)); 
        sender.Start(); 

        if (!_activeConnections.TryAdd(connectionName, this)) 
        { 
         _exit.Set(); 
         await _next.Invoke(context); 
         return; 
        } 

        while (true) 
        { 
         WebSocketReceiveResult result = socket.ReceiveAsync(new ArraySegment<byte>(new byte[1]), CancellationToken.None).Result; 
         if (result.CloseStatus.HasValue) 
         { 
          _exit.Set(); 
          break; 
         } 
        } 

        SocketHandler dummy; 
        _activeConnections.TryRemove(key, out dummy); 
       } 
      } 

      await _next.Invoke(context); 

      string data = context.Items["Data"] as string; 
      if (!string.IsNullOrEmpty(data)) 
      { 
       string name = context.Items["ConnectionName"] as string; 
       SocketMiddleware connection = _activeConnections.Where(ac => ac.Key == name)?.Single().Value; 
       if (connection != null) 
       { 
        connection.Send(data); 
       } 
      } 
     } 

     private void StartSending(WebSocket socket) 
     { 
      WaitHandle[] events = new WaitHandle[] { _send, _exit }; 
      while (true) 
      { 
       if (WaitHandle.WaitAny(events) == 1) 
       { 
        break; 
       } 

       if (!string.IsNullOrEmpty(_packet)) 
       { 
        SendPacket(socket, _packet); 
       } 
       _send.Reset(); 
      } 
     } 

     private void SendPacket(WebSocket socket, string packet) 
     { 
      byte[] buffer = Encoding.UTF8.GetBytes(packet); 
      ArraySegment<byte> segment = new ArraySegment<byte>(buffer); 
      Task.Run(() => socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None)); 
     } 
    } 

Phần mềm trung gian này sẽ chạy theo mọi yêu cầu. Khi Invoke được gọi là nó sẽ kiểm tra nếu nó là một yêu cầu socket web. Nếu có, phần mềm trung gian sẽ kiểm tra xem kết nối đó đã được mở chưa và nếu không, kết nối bắt tay sẽ được chấp nhận và phần mềm trung gian sẽ thêm nó vào từ điển kết nối. Điều quan trọng là từ điển là tĩnh để nó được tạo ra chỉ một lần trong suốt thời gian ứng dụng.

Bây giờ nếu chúng ta dừng ở đây và di chuyển lên đường ống, HttpContext cuối cùng sẽ bị phá hủy và, vì ổ cắm không được đóng gói đúng cách, nó cũng sẽ bị đóng. Vì vậy, chúng ta phải giữ thread trung gian đang chạy. Nó được thực hiện bằng cách yêu cầu socket để nhận một số dữ liệu.

Bạn có thể hỏi lý do tại sao chúng tôi cần nhận bất kỳ thứ gì nếu yêu cầu chỉ gửi? Câu trả lời là nó là cách duy nhất để phát hiện khách hàng một cách đáng tin cậy. HttpContext.RequestAborted.IsCancellationRequested chỉ hoạt động nếu bạn liên tục gửi trong vòng lặp while. Nếu bạn cần đợi một số sự kiện máy chủ trên WaitHandle, cờ hủy bỏ không bao giờ đúng. Tôi đã cố gắng chờ đợi HttpContext.RequestAborted.WaitHandle như là sự kiện thoát của tôi, nhưng nó không bao giờ được thiết lập. Vì vậy, chúng tôi yêu cầu socket để nhận được một cái gì đó và nếu một cái gì đó đặt CloseStatus.HasValue đúng, chúng ta biết rằng khách hàng bị ngắt kết nối. Nếu chúng tôi nhận được thứ gì đó khác (mã phía máy khách không an toàn), chúng tôi sẽ bỏ qua nó và bắt đầu nhận lại.

Việc gửi được thực hiện trong một chuỗi riêng biệt. Lý do là như nhau, nó không thể phát hiện ngắt kết nối nếu chúng ta chờ đợi trên chủ đề trung gian chính. Để thông báo cho người gửi chủ đề mà khách hàng bị ngắt kết nối, chúng tôi sử dụng biến đồng bộ hóa _exit. Hãy nhớ rằng, nó là tốt để có các thành viên tư nhân ở đây kể từ khi các cá thể SocketMiddleware được lưu trong một thùng chứa tĩnh.

Bây giờ, làm cách nào để chúng tôi thực sự gửi bất kỳ nội dung gì với thiết lập này? Giả sử sự kiện xảy ra trên máy chủ và một số dữ liệu có sẵn. Để đơn giản, hãy giả sử dữ liệu này đến bên trong yêu cầu http bình thường đối với một số hành động điều khiển. SocketMiddleware sẽ chạy cho mọi yêu cầu, nhưng vì nó không phải là yêu cầu ổ cắm web, _next.Invoke (context) được gọi và yêu cầu đạt controller action có thể trông giống như thế này:

[Route("ProvideData")] 
[HttpGet] 
public ActionResult ProvideData(string data, string connectionName) 
{ 
    if (!string.IsNullOrEmpty(data) && !string.IsNullOrEmpty(connectionName)) 
    { 
     HttpContext.Items.Add("ConnectionName", connectionName); 
     HttpContext.Items.Add("Data", data); 
    } 
     return Ok(); 
} 

khiển populates thấy bộ sưu tập đó là được sử dụng để chia sẻ dữ liệu giữa các thành phần. Sau đó, các đường ống trở lại SocketMiddleware một lần nữa, nơi chúng tôi kiểm tra xem có bất cứ điều gì thú vị bên trong context.Items. Nếu có, chúng tôi chọn kết nối tương ứng từ từ điển và gọi phương thức Send() của nó để đặt chuỗi dữ liệu và đặt _send sự kiện và cho phép chạy một lần vòng lặp while bên trong luồng người gửi.

Và thì đấy, chúng tôi có kết nối socket gửi đến sự kiện phía máy chủ. Ví dụ này rất sơ khai và chỉ để minh họa cho khái niệm này. Tất nhiên, để sử dụng middleware này, bạn sẽ cần phải thêm những dòng sau trong lớp Startup của bạn trước khi bạn thêm MVC:

app.UseWebSockets(); 
app.UseMiddleware<SocketMiddleware>(); 

Mã là rất lạ và hy vọng chúng tôi sẽ có thể viết một cái gì đó đẹp hơn nhiều khi SignalR cho dotnetcore là cuối cùng ra ngoài. Hy vọng rằng ví dụ này sẽ hữu ích cho ai đó. Nhận xét và đề nghị được chào đón.