การเขียนโปรแกรมเครือข่าย Async โดยใช้ Reactive Extensions


25

หลังจากทำการsocketเขียนโปรแกรมasync "ระดับต่ำ" บางอย่าง (มากหรือน้อย) เมื่อหลายปีก่อน (ในรูปแบบเหตุการณ์แบบ Asynchronous Pattern (EAP) ) และเมื่อเร็ว ๆ นี้ได้ย้าย "up" เป็นรูปTcpListenerแบบการเขียนโปรแกรม Asynchronous (APM)แล้ว พยายามที่จะย้ายไปที่async/await(รูปแบบอะซิงโครนัสตามรูปแบบ(TAP) ) ฉันค่อนข้างจะมีมันด้วยต้องรำคาญกับ 'ประปาในระดับต่ำ' ทั้งหมดนี้ ดังนั้นฉันจึงหา; ทำไมไม่ยอมแพ้RX( ส่วนขยายรีแอคทีฟ ) เพราะมันอาจเข้ากับโดเมนปัญหาของฉันได้

รหัสจำนวนมากที่ฉันเขียนมีให้กับไคลเอนต์จำนวนมากที่เชื่อมต่อผ่าน Tcp ไปยังแอปพลิเคชันของฉันซึ่งจะเริ่มการสื่อสารแบบสองทาง (async) ลูกค้าหรือเซิร์ฟเวอร์อาจตัดสินใจว่าจะต้องส่งข้อความและทำเช่นนั้นดังนั้นนี่ไม่ใช่request/responseการตั้งค่าแบบดั้งเดิมของคุณแต่เป็นแบบเรียลไทม์แบบสองทาง "สาย" เปิดให้ทั้งสองฝ่ายส่งสิ่งที่พวกเขาต้องการ เมื่อใดก็ตามที่พวกเขาต้องการ (หากใครมีชื่อที่เหมาะสมในการอธิบายสิ่งนี้ฉันยินดีที่จะได้ยิน!)

"โปรโตคอล" นั้นแตกต่างกันไปตามแต่ละแอปพลิเคชัน (และไม่เกี่ยวข้องกับคำถามของฉันจริงๆ) ฉันมี แต่คำถามเริ่มต้น:

  1. ระบุว่า "เซิร์ฟเวอร์" เดียวเท่านั้นกำลังทำงานอยู่ แต่จะต้องติดตามการเชื่อมต่อจำนวนมาก (ปกติหลายพัน) ของการเชื่อมต่อ (เช่นลูกค้า) แต่ละคนมี (สำหรับการขาดคำอธิบายที่ดีกว่า) ของพวกเขาเอง "เครื่องรัฐ" เพื่อติดตามภายในของพวกเขา รัฐ ฯลฯ คุณต้องการวิธีการแบบใด EAP / TAP / APM? RX จะพิจารณาเป็นตัวเลือกหรือไม่? ถ้าไม่ทำไม

ดังนั้นฉันต้องทำงาน Async ตั้งแต่) มันไม่ใช่โปรโตคอลคำขอ / ตอบกลับดังนั้นฉันจึงไม่สามารถมีเธรด / ไคลเอนต์ใน "กำลังรอข้อความ" - บล็อกการโทรหรือ "ส่งข้อความ" - บล็อกการโทร (อย่างไรก็ตามถ้าการส่งคือ การปิดกั้นสำหรับลูกค้ารายนั้นเท่านั้นที่ฉันสามารถอยู่กับมันได้) และ b) ฉันต้องจัดการการเชื่อมต่อพร้อมกันจำนวนมาก ฉันไม่เห็นวิธีการทำเช่นนี้ (เชื่อถือได้) โดยใช้การบล็อกการโทร

แอปพลิเคชันของฉันส่วนใหญ่เกี่ยวข้องกับ VoiP ไม่ว่าจะเป็นข้อความ SIP จากลูกค้า SIP หรือการส่งข้อความ PBX (ที่เกี่ยวข้อง) จากแอปพลิเคชันเช่นFreeSwitch / OpenSIPSเป็นต้น แต่คุณสามารถทำได้ในรูปแบบที่ง่ายที่สุดลองจินตนาการถึงเซิร์ฟเวอร์ "แชท" ที่พยายามจัดการกับลูกค้า "แชท" โปรโตคอลส่วนใหญ่เป็นข้อความ (ASCII)

ดังนั้นหลังจากใช้วิธีการเรียงสับเปลี่ยนของเทคนิคต่าง ๆ ที่กล่าวมาแล้วฉันต้องการทำให้งานของฉันง่ายขึ้นโดยการสร้างวัตถุที่ฉันสามารถยกตัวอย่างบอกสิ่งที่IPEndpointฟังและบอกฉันเมื่อใดก็ตามที่มีสิ่งที่น่าสนใจเกิดขึ้น ใช้เหตุการณ์สำหรับดังนั้น EAP บางอย่างมักจะผสมกับอีกสองเทคนิค) ชั้นไม่ควรกังวลกับการพยายามเข้าใจโปรโตคอล; มันควรจัดการกับสตริงที่เข้า / ส่งออกเท่านั้น และด้วยการที่ฉันจับจ้องที่ RX หวังว่า (ท้ายที่สุด) จะทำให้งานง่ายขึ้นฉันจึงสร้าง "ซอ" ขึ้นมาใหม่ตั้งแต่ต้น:

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Reactive.Linq;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        var f = new FiddleServer(new IPEndPoint(IPAddress.Any, 8084));
        f.Start();
        Console.ReadKey();
        f.Stop();
        Console.ReadKey();
    }
}

public class FiddleServer
{
    private TcpListener _listener;
    private ConcurrentDictionary<ulong, FiddleClient> _clients;
    private ulong _currentid = 0;

    public IPEndPoint LocalEP { get; private set; }

    public FiddleServer(IPEndPoint localEP)
    {
        this.LocalEP = localEP;
        _clients = new ConcurrentDictionary<ulong, FiddleClient>();
    }

    public void Start()
    {
        _listener = new TcpListener(this.LocalEP);
        _listener.Start();
        Observable.While(() => true, Observable.FromAsync(_listener.AcceptTcpClientAsync)).Subscribe(
            //OnNext
            tcpclient =>
            {
                //Create new FSClient with unique ID
                var fsclient = new FiddleClient(_currentid++, tcpclient);
                //Keep track of clients
                _clients.TryAdd(fsclient.ClientId, fsclient);
                //Initialize connection
                fsclient.Send("connect\n\n");

                Console.WriteLine("Client {0} accepted", fsclient.ClientId);
            },
            //OnError
            ex =>
            {

            },
            //OnComplete
            () =>
            {
                Console.WriteLine("Client connection initialized");
                //Accept new connections
                _listener.AcceptTcpClientAsync();
            }
        );
        Console.WriteLine("Started");
    }

    public void Stop()
    {
        _listener.Stop();
        Console.WriteLine("Stopped");
    }

    public void Send(ulong clientid, string rawmessage)
    {
        FiddleClient fsclient;
        if (_clients.TryGetValue(clientid, out fsclient))
        {
            fsclient.Send(rawmessage);
        }
    }
}

public class FiddleClient
{
    private TcpClient _tcpclient;

    public ulong ClientId { get; private set; }

    public FiddleClient(ulong id, TcpClient tcpclient)
    {
        this.ClientId = id;
        _tcpclient = tcpclient;
    }

    public void Send(string rawmessage)
    {
        Console.WriteLine("Sending {0}", rawmessage);
        var data = Encoding.ASCII.GetBytes(rawmessage);
        _tcpclient.GetStream().WriteAsync(data, 0, data.Length);    //Write vs WriteAsync?
    }
}

ฉันทราบว่าใน "ซอ" นี้มีรายละเอียดเฉพาะการใช้งานเล็กน้อย ในกรณีนี้ฉันกำลังทำงานกับFreeSwitch ESLดังนั้น"connect\n\n"ซอในซอร์ฟแวร์ควรจะถูกลบออกไป

ฉันยังทราบด้วยว่าฉันจำเป็นต้อง refactor วิธีการที่ไม่ระบุชื่อวิธีการของอินสแตนซ์ส่วนตัวบนคลาสเซิร์ฟเวอร์ ฉันแค่ไม่แน่ใจว่าจะใช้การประชุมแบบไหน (เช่น " OnSomething") สำหรับชื่อเมธอด

นี่คือพื้นฐาน / จุดเริ่มต้น / รากฐานของฉัน (ซึ่งต้องการ "tweaking") ฉันมีคำถามบางอย่างเกี่ยวกับเรื่องนี้:

  1. ดูคำถามข้างต้น "1"
  2. ฉันกำลังติดตามใช่ไหม? หรือ "การออกแบบ" การตัดสินใจของฉันไม่ยุติธรรมหรือไม่?
  3. Concurrency-wise: สิ่งนี้จะรับมือกับลูกค้าหลายพันราย (แยกวิเคราะห์ / จัดการข้อความจริงนอกเหนือ)
  4. ในข้อยกเว้น: ฉันไม่แน่ใจว่าจะรับข้อยกเว้นที่เพิ่มขึ้นภายในไคลเอนต์ "up" ไปยังเซิร์ฟเวอร์ ("RX-wise") ได้อย่างไร อะไรจะเป็นวิธีที่ดี?
  5. ตอนนี้ฉันสามารถรับไคลเอนต์ที่เชื่อมต่อจากคลาสเซิร์ฟเวอร์ของฉัน (โดยใช้มันClientId) โดยสมมติว่าฉันเปิดเผยลูกค้าในทางใดทางหนึ่งและอีกวิธีการโทรกับพวกเขาโดยตรง ฉันยังสามารถเรียกใช้เมธอดผ่านคลาสเซิร์ฟเวอร์ (ตัวอย่างเช่นSend(clientId, rawmessage)เมธอด (ในขณะที่วิธีการหลังจะเป็นวิธี "ความสะดวก" สำหรับการรับข้อความอย่างรวดเร็วไปยังอีกด้านหนึ่ง)
  6. ฉันไม่แน่ใจว่าจะไปจากที่นี่ (และอย่างไร):
    • a) ฉันต้องจัดการกับข้อความที่เข้ามา; ฉันจะตั้งค่านี้ได้อย่างไร ฉันจะได้รับ ofcourse กระแส แต่ที่ฉันจะจัดการกับการเรียกไบต์ได้รับ? ฉันคิดว่าฉันต้องการ "ObservableStream" บางอย่างที่ฉันสามารถสมัครสมาชิกได้บ้าง ฉันจะใส่นี้ในFiddleClientหรือFiddleServer?
    • ข) สมมติว่าผมต้องการที่จะหลีกเลี่ยงการใช้เหตุการณ์เหล่านี้จนกว่าFiddleClient/ FiddleServerชั้นเรียนจะดำเนินการมากขึ้นโดยเฉพาะการตัดโปรแกรมเฉพาะการจัดการ ฯลฯ โดยใช้โปรโตคอลเฉพาะเจาะจงมากขึ้นของพวกเขาFooClient/ FooServerชั้นเรียน: วิธีการที่ฉันจะไปจากการได้รับข้อมูลในต้นแบบ 'Fiddle' เรียนของพวกเขา คู่ที่เฉพาะเจาะจงมากขึ้น?

บทความ / ลิงค์ที่ฉันอ่าน / อ่านแล้ว / ใช้เพื่อการอ้างอิง:


ลองดูที่ห้องสมุดReactiveSocketsที่มีอยู่
Flagbug

2
ฉันไม่ได้มองหาห้องสมุดหรือลิงค์ (แม้ว่าจะอ้างอิงพวกเขาชื่นชม) แต่สำหรับการป้อนข้อมูล / คำแนะนำ / ความช่วยเหลือเกี่ยวกับคำถามและการตั้งค่าทั่วไปของฉัน ฉันต้องการเรียนรู้ที่จะพัฒนาโค้ดของตัวเองและสามารถตัดสินใจเกี่ยวกับทิศทางที่จะรับสิ่งนี้ได้ดีขึ้นชั่งน้ำหนักของผู้เล่นและข้อเสียอื่น ๆ ไม่อ้างอิงห้องสมุดบางแห่งวางมันลงไปแล้วไปต่อ ฉันต้องการเรียนรู้จากประสบการณ์นี้และรับประสบการณ์มากขึ้นด้วยการเขียนโปรแกรม Rx / เครือข่าย
RobIII

แน่นอน แต่เนื่องจากห้องสมุดเป็นโอเพนซอร์สคุณสามารถดูวิธีการนำไปปฏิบัติที่นั่นได้
Flagbug

1
แน่นอน แต่การดูซอร์สโค้ดไม่ได้อธิบายว่าเพราะเหตุใดการตัดสินใจออกแบบจึงเกิดขึ้น และเนื่องจากฉันค่อนข้างใหม่กับ Rx ร่วมกับการเขียนโปรแกรมเครือข่ายฉันไม่มีประสบการณ์เพียงพอที่จะบอกได้ว่าไลบรารี่นี้ดีหรือไม่ถ้าการออกแบบเหมาะสมถ้าการตัดสินใจถูกต้องและแม้ว่ามันจะเหมาะกับฉันก็ตาม
RobIII

ฉันคิดว่ามันจะเป็นการดีถ้าคิดใหม่เกี่ยวกับเซิร์ฟเวอร์ในฐานะสมาชิกที่ใช้งานอยู่ของการจับมือกันดังนั้นเซิร์ฟเวอร์นั้นจะเริ่มการเชื่อมต่อแทนที่จะฟังการเชื่อมต่อ ตัวอย่างเช่นที่นี่: codeproject.com/Articles/20250/Reverse-Connection-Shell

คำตอบ:


1

... ฉันต้องการทำให้งานของฉันง่ายขึ้นโดยการสร้างวัตถุที่ฉันสามารถยกตัวอย่างบอกได้ว่า IPEndpoint ใดรับฟังและบอกให้ฉันทราบเมื่อมีสิ่งที่น่าสนใจเกิดขึ้น ...

หลังจากอ่านข้อความนั้นฉันก็คิดว่า "นักแสดง" ทันที นักแสดงจะคล้ายกับวัตถุมากยกเว้นพวกเขาจะมีเพียงอินพุตเดียวที่คุณส่งข้อความ (แทนที่จะเรียกวิธีการของวัตถุโดยตรง) และพวกเขาทำงานแบบอะซิงโครนัส ในตัวอย่างที่ง่ายมาก ... คุณจะสร้างนักแสดงและส่งข้อความพร้อม IPEndpoint และที่อยู่ของนักแสดงเพื่อส่งผลลัพธ์ไปให้ มันจะดับลงและทำงานในพื้นหลัง คุณจะได้รับการตอบกลับเมื่อ "สิ่งที่น่าสนใจ" เกิดขึ้น คุณสามารถยกตัวอย่างนักแสดงได้มากเท่าที่คุณต้องการเพื่อรับภาระ

ฉันไม่คุ้นเคยกับห้องสมุดนักแสดงในสุทธิ แต่ฉันรู้ว่ามีบางอย่าง ฉันคุ้นเคยกับห้องสมุด TPL Dataflow (จะมีส่วนที่ครอบคลุมอยู่ในหนังสือของฉันที่http://DataflowBook.com ) และควรง่ายต่อการใช้โมเดลนักแสดงที่เรียบง่ายกับห้องสมุดนั้น


นั่นดูน่าสนใจ ฉันจะตั้งค่าโครงการของเล่นเพื่อดูว่ามันเหมาะกับฉันหรือไม่ ขอบคุณสำหรับคำแนะนำ
RobIII
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.