2009-07-22 15 views
29

Widziałem wiele zasobów tutaj na SO o gniazdach. Sądzę, że żadne z nich nie zawierało szczegółów, które chciałem poznać. W mojej aplikacji serwer wykonuje całe przetwarzanie i wysyła okresowe aktualizacje do klientów.Pierwsze kroki z programowaniem gniazd w języku C# - Najlepsze praktyki

Celem tego wpisu jest omówienie podstawowych pojęć wymaganych przy opracowywaniu aplikacji gniazd i omówienie najlepszych praktyk. Oto podstawowe rzeczy, które można zobaczyć w przypadku prawie wszystkich aplikacji opartych na gniazdach.

1 - Binding i słuchać na gnieździe

Używam następujący kod. Działa dobrze na moim komputerze. Czy muszę dbać o coś innego, kiedy wdrażam to na prawdziwym serwerze?

IPHostEntry localHost = Dns.GetHostEntry(Dns.GetHostName()); 
IPEndPoint endPoint = new IPEndPoint(localHost.AddressList[0], 4444); 

serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
        ProtocolType.Tcp); 
serverSocket.Bind(endPoint); 
serverSocket.Listen(10); 

2 - odbieranie danych

I stosuje się tablicę wielkości 255 bajtów. Więc gdy otrzymuję dane, które mają więcej niż 255 bajtów, muszę wywołać metodę odbierania, dopóki nie otrzymam pełnych danych, prawda? Gdy otrzymam pełne dane, muszę dołączyć wszystkie otrzymane bajty, aby uzyskać pełny komunikat. Czy to jest poprawne? Czy istnieje lepsze podejście?

3 - Wysyłanie danych i określania długości danych

Ponieważ nie ma sposobu, aby znaleźć w sieci TCP długość wiadomości otrzymywać, Mam zamiar dodać długości do wiadomości. Będzie to pierwszy bajt pakietu. Zatem systemy klienckie wiedzą, ile danych można odczytać.

Jakieś inne lepsze podejście?

4 - Zamykanie klienta

Kiedy klient jest zamknięty, to wyśle ​​wiadomość do serwera wskazuje na ścisły. Serwer usunie szczegóły klienta z jego listy klientów. Poniżej przedstawiono kod używany po stronie klienta do odłączenia gniazda (część komunikatu nie jest pokazana).

client.Shutdown(SocketShutdown.Both); 
client.Close(); 

Wszelkie sugestie lub problemy?

5 - zamknięcie serwera

serwer wysyła wiadomość do wszystkich klientów wskazującymi wyłączył. Każdy klient rozłączy gniazdo po otrzymaniu tego komunikatu. Klienci wyślą zamkniętą wiadomość do serwera i zamkną. Gdy serwer otrzyma komunikat zamknięcia od wszystkich klientów, odłącza gniazdo i przestaje nasłuchiwać. Zadzwoń pod numer Na każdym gniazdu klienta usuń numer, aby zwolnić zasoby. Czy to właściwe podejście?

6 - nieznany rozłączenia klienta

Zdarza się, że klient może odłączyć bez informowania serwer. Mój plan poradzenia sobie z tym to: Kiedy serwer wysyła wiadomości do wszystkich klientów, sprawdź stan gniazda. Jeśli nie jest podłączony, usuń tego klienta z listy klientów i zamknij gniazdo dla tego klienta.

Każda pomoc będzie świetna!

+0

miałem problemy z IPaddress nasłuchuje na jakiejś starszej wersji systemu Windows. Używanie IPAddress ipAddress = new IPAddress (new byte [] {0, 0, 0, 0}); wydaje się być najlepszy dla mnie! – clamchoda

Odpowiedz

24

Ponieważ jest to "pierwsze kroki", moja odpowiedź będzie raczej prosta, niż wysoce skalowalna. Najlepiej najpierw poczuć się komfortowo dzięki prostemu podejściu, zanim zrobisz coś bardziej skomplikowanego.

1 - Oprawa i słuchania
kodzie wydaje się dobrze do mnie, osobiście używam:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

Zamiast będzie drogi DNS, ale nie sądzę, że jest to prawdziwy problem albo droga.

1.5 - Przyjmowanie połączeń klienckich
Wystarczy wspomnieć o tym kątem kompletności boską ... Ja zakładając robisz to inaczej nie dostanie do kroku 2.

2 - odbieranie danych
Uczyniłbym bufor nieco dłuższy niż 255 bajtów, chyba że można się spodziewać, że wszystkie wiadomości serwera mają najwyżej 255 bajtów. Myślę, że potrzebujesz bufora, który prawdopodobnie będzie większy niż rozmiar pakietu TCP, dzięki czemu unikniesz wielokrotnych odczytów w celu otrzymania pojedynczego bloku danych.

Powiedziałbym, że zbieranie 1500 bajtów powinno być w porządku, a może nawet 2048 dla ładnej liczby okrągłej.

Alternatywnie, może można uniknąć stosując byte[] do przechowywania fragmentów danych, a zamiast owinąć gniazdo klienta po stronie serwera w NetworkStream, owinięte w BinaryReader, dzięki czemu można czytać składniki wiadomości direclty z gniazdka bez obawy o rozmiary buforów.

3 - Wysyłanie danych i określania długości danych
Twoje podejście będzie działać dobrze, ale to nie wymagają oczywiście, że łatwo jest obliczyć długość pakietu przed rozpoczęciem jej wysłaniem.

Alternatywnie, jeśli format wiadomości (kolejność jego składników) został zaprojektowany w taki sposób, aby w dowolnym momencie klient mógł określić, czy po danych powinno być więcej danych (na przykład kod 0x01 oznacza, że ​​następny będzie int i ciąg, kod 0x02 oznacza następny będzie 16 bajtów, itp., itp.). W połączeniu z podejściem NetworkStream po stronie klienta może to być bardzo skuteczne podejście.

Aby być po bezpiecznej stronie, możesz dodać sprawdzanie poprawności odbieranych komponentów, aby upewnić się, że przetwarzasz tylko prawidłowe wartości. Na przykład, jeśli otrzymasz wskazówkę dla ciągu o długości 1 TB, możesz mieć gdzieś uszkodzenie pliku i może być bezpieczniej zamknąć połączenie i zmusić klienta do ponownego połączenia i "rozpoczęcia od nowa". Takie podejście zapewnia bardzo dobre zachowanie typu catch-all w przypadku nieoczekiwanych awarii.

4/5 - Zamykanie klienta i serwera
Osobiście wybrałbym właśnie Close bez dalszych komunikatów; gdy połączenie zostanie zamknięte, otrzymasz wyjątek od blokowania odczytu/zapisu na drugim końcu połączenia, który będziesz musiał uwzględnić.

Ponieważ w każdym razie musisz zaspokoić "nieznane rozłączenia", aby uzyskać solidne rozwiązanie, dzięki czemu odłączanie bardziej skomplikowane jest na ogół bezcelowe.

6 - nieznany rozłączenia
nie będę ufać nawet stan gniazda ... możliwe jest połączenie zginąć gdzieś po drodze między klient/serwer bez klienta lub odnotowanie serwera.

Jedynym gwarantowanym sposobem powiadomienia połączenia, które niespodziewanie zginęło, jest następna próba wysłania wysyłania czegoś wzdłuż połączenia. W tym momencie zawsze otrzymasz wyjątek wskazujący niepowodzenie, jeśli coś poszło nie tak z połączeniem.

W rezultacie jedynym bezpiecznym sposobem wykrywania wszystkich nieoczekiwanych połączeń jest wdrożenie mechanizmu "pingowania", w którym klient i serwer będą okresowo wysyłać wiadomość na drugi koniec, który powoduje tylko odpowiedź komunikat informujący, że odebrano polecenie "ping".

Aby zoptymalizować niepotrzebne pingi, może być potrzebny mechanizm "limitu czasu", który wysyła sygnał ping tylko wtedy, gdy inny ruch nie został odebrany z drugiego końca przez określony czas (na przykład, jeśli ostatnia wiadomość z serwera jest starsza niż x sekund, klient wysyła polecenie ping, aby upewnić się, że połączenie nie zginęło bez powiadomienia).

Bardziej zaawansowane
Jeśli chcesz wysoką skalowalność trzeba będzie zajrzeć do asynchronicznych metod dla wszystkich operacji gniazda (Accept/Wyślij/Odbierz). Są to warianty "Rozpocznij/Zakończ", ale są one o wiele bardziej skomplikowane w użyciu.

Polecam, nie próbując tego, dopóki nie masz prostej wersji i działa.

Należy również pamiętać, że jeśli nie planuje się skalowania dalej niż kilkudziesięciu klientów, nie będzie to stanowić problemu, niezależnie od tego. Techniki asynchroniczne są naprawdę potrzebne tylko wtedy, gdy zamierzasz skalować się do tysięcy lub setek tysięcy połączonych klientów, podczas gdy twój serwer nie umiera.

pewnie zapomnieli całą masę innych ważnych sugestii, ale to powinno wystarczyć, aby dostać się dość solidną i rzetelną realizację zacząć

+1

To było świetne wyjaśnienie. Dzięki –

+0

Pamiętaj, że gdy już wszystko zrobisz, prawdopodobnie więcej rzeczy do wzięcia pod uwagę, o których zapomniałem wspomnieć ... – jerryjvl

13

1 - oprawy i słuchać na gnieździe

Wygląda dobrze dla mnie. Twój kod będzie jednak wiązał gniazdo tylko do jednego adresu IP. Jeśli chcesz po prostu słuchać na dowolnym interfejsie adres IP/sieci, użyj IPAddress.Any:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

Aby mieć możliwość rozbudowy, może chcesz obsługiwać IPv6. Aby słuchać na dowolnym adresie IPv6, użyj IPAddress.IPv6Any zamiast IPAddress.Any.

Należy pamiętać, że nie można nasłuchiwać na żadnym adresie IPv4 i dowolnym adresie IPv6 w tym samym czasie, z wyjątkiem sytuacji, gdy użytkownik korzysta z Dual-Stack Socket.Będzie to wymagać, aby rozbroić opcję IPV6_V6ONLY Gniazdo:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0); 

Aby umożliwić Teredo z gniazdka, należy ustawić opcję PROTECTION_LEVEL_UNRESTRICTED Gniazdo:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10); 

2 - odbierania danych

Zaleca się używanie NetworkStream, który owija gniazdo w Stream zamiast ręcznego czytania porcji.

Odczytywanie określonej liczby bajtów jest nieco niewygodne jednak:

using (var stream = new NetworkStream(serverSocket)) { 
    var buffer = new byte[MaxMessageLength]; 
    while (true) { 
     int type = stream.ReadByte(); 
     if (type == BYE) break; 
     int length = stream.ReadByte(); 
     int offset = 0; 
     do 
     offset += stream.Read(buffer, offset, length - offset); 
     while (offset < length); 
     ProcessMessage(type, buffer, 0, length); 
    } 
} 

Gdzie NetworkStream naprawdę błyszczy jest to, że można go używać jak każdy inny Stream. Jeśli bezpieczeństwo jest ważne, po prostu zapisz NetworkStream w SslStream, aby uwierzytelnić serwer i (opcjonalnie) klientów z certyfikatami X.509. Kompresja działa w ten sam sposób.

var sslStream = new SslStream(stream, false); 
sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true); 
// receive/send data SSL secured 

3 - Wysyłanie danych i określania długości danych

Twoje podejście powinno działać, choć prawdopodobnie nie chce zejść z drogi do wyważania otwartych drzwi i zaprojektować nowy protokół do tego. Spójrz na BEEP, a może nawet coś prostego, na przykład protobuf.

W zależności od celów warto rozważyć wybór abstrakcji powyżej gniazd takich jak WCF lub inny mechanizm RPC.

4/5/6 - Zamykanie & nieznany rozłączenia

Co jerryjvl wspomniane :-) jedynym wiarygodnym mechanizmem wykrywania są pingi lub wysyłania keep-alives gdy połączenie jest bezczynny.

Mimo że w każdym przypadku musisz uporać się z nieznanymi rozłączeniami, osobiście zostawiłbym jakiś element protokołu, aby zamknąć połączenie w obopólnym porozumieniu, zamiast po prostu go zamknąć bez ostrzeżenia.

+0

Mimo, że nie może to zranić, odkryłem, że w praktyce koncentruję się na solidnym wyleczeniu z nieoczekiwanego działa lepiej niż wiadomości "rozłączne" ... YMMV :) – jerryjvl

+0

Świetny post. Dzięki. Czy masz więcej informacji o korzystaniu z NetworkStream? Dowolne linki by to zrobiły. –

+0

Link i przykład dodany do odpowiedzi. – dtb