Omówienie protokołu TCP

Ważne

Klasa jest zdecydowanie zalecana Socket dla zaawansowanych użytkowników, a nie TcpClient i TcpListener.

Aby pracować z protokołem Transmission Control Protocol (TCP), dostępne są dwie opcje: użyj Socket dla maksymalnej kontroli i wydajności lub użyj TcpClient klas pomocnika i .TcpListener TcpClienti są oparte na System.Net.Sockets.Socket klasie i TcpListener dbają o szczegóły przesyłania danych w celu ułatwienia użytkowania.

Klasy protokołów używają podstawowej Socket klasy do zapewnienia prostego dostępu do usług sieciowych bez konieczności utrzymywania informacji o stanie lub znajomości szczegółów konfigurowania gniazd specyficznych dla protokołu. Aby użyć metod asynchronicznych Socket , można użyć metod asynchronicznych dostarczonych przez klasę NetworkStream . Aby uzyskać dostęp do funkcji Socket klasy, które nie są uwidocznione przez klasy protokołów, należy użyć Socket klasy .

TcpClient i TcpListener reprezentują sieć przy użyciu NetworkStream klasy . Metoda służy GetStream do zwracania strumienia sieciowego, a następnie wywoływania metod i NetworkStream.WriteAsync strumieniaNetworkStream.ReadAsync. Element NetworkStream nie jest właścicielem bazowego gniazda klas protokołów, więc zamknięcie nie ma wpływu na gniazdo.

Używanie i TcpClientTcpListener

Klasa TcpClient żąda danych z zasobu internetowego przy użyciu protokołu TCP. Metody i właściwości abstrakcyjne TcpClient szczegóły tworzenia Socket obiektu do żądania i odbierania danych przy użyciu protokołu TCP. Ponieważ połączenie z urządzeniem zdalnym jest reprezentowane jako strumień, dane można odczytywać i zapisywać za pomocą technik obsługi strumienia programu .NET Framework.

Protokół TCP ustanawia połączenie z zdalnym punktem końcowym, a następnie używa tego połączenia do wysyłania i odbierania pakietów danych. Protokół TCP jest odpowiedzialny za zapewnienie, że pakiety danych są wysyłane do punktu końcowego i montowane w odpowiedniej kolejności po ich nadejściu.

Tworzenie punktu końcowego adresu IP

Podczas pracy z usługą System.Net.Socketsreprezentujesz punkt końcowy sieci jako IPEndPoint obiekt. Obiekt IPEndPoint jest skonstruowany z numerem IPAddress i odpowiadającym mu numerem portu. Zanim będzie można zainicjować konwersację za pośrednictwem elementu , należy utworzyć potok danych między aplikacją Socketa zdalnym miejscem docelowym.

Protokół TCP/IP używa adresu sieciowego i numeru portu usługi, aby jednoznacznie zidentyfikować usługę. Adres sieciowy identyfikuje określone miejsce docelowe sieci; numer portu identyfikuje konkretną usługę na tym urządzeniu do nawiązania połączenia. Połączenie adresu sieciowego i portu usługi jest nazywane punktem końcowym, który jest reprezentowany na platformie .NET przez klasę EndPoint . Element potomny elementu jest definiowany dla każdej obsługiwanej EndPoint rodziny adresów; dla rodziny adresów IP klasa to IPEndPoint.

Klasa Dns udostępnia usługi nazw domen dla aplikacji korzystających z usług internetowych TCP/IP. Metoda GetHostEntryAsync wysyła zapytanie do serwera DNS, aby zamapować przyjazną dla użytkownika nazwę domeny (na przykład "host.contoso.com") na numeryczny adres internetowy (na przykład 192.168.1.1). GetHostEntryAsync Zwraca element Task<IPHostEntry> , który w przypadku oczekiwania zawiera listę adresów i aliasów dla żądanej nazwy. W większości przypadków można użyć pierwszego adresu zwróconego w tablicy AddressList . Poniższy kod pobiera IPAddress adres IP serwera host.contoso.com.

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("host.contoso.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];

Napiwek

Do celów testowania ręcznego i debugowania zazwyczaj można użyć GetHostEntryAsync metody z wynikową nazwą hosta z Dns.GetHostName() wartości, aby rozpoznać nazwę hosta lokalnego na adres IP. Rozważmy następujący fragment kodu:

var hostName = Dns.GetHostName();
IPHostEntry localhost = await Dns.GetHostEntryAsync(hostName);
// This is the IP address of the local machine
IPAddress localIpAddress = localhost.AddressList[0];

Urząd IANA (Internet Assigned Numbers Authority) definiuje numery portów dla typowych usług. Aby uzyskać więcej informacji, zobacz IANA: Service Name and Transport Protocol Port Number Registry (IANA: nazwa usługi i rejestr numerów portów protokołu transportu). Inne usługi mogą mieć zarejestrowane numery portów w zakresie od 1024 do 65 535. Poniższy kod łączy adres IP z host.contoso.com numerem portu w celu utworzenia zdalnego punktu końcowego dla połączenia.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

Po określeniu adresu urządzenia zdalnego i wybraniu portu do użycia dla połączenia aplikacja może nawiązać połączenie z urządzeniem zdalnym.

Utwórz TcpClient

Klasa TcpClient udostępnia usługi TCP na wyższym poziomie abstrakcji niż Socket klasa. TcpClient służy do tworzenia połączenia klienta z hostem zdalnym. Wiedząc, jak uzyskać element IPEndPoint, załóżmy, że masz element IPAddress do parowania z żądanym numerem portu. W poniższym przykładzie pokazano konfigurowanie TcpClient elementu w celu nawiązania połączenia z serwerem czasu na porcie TCP 13:

var ipEndPoint = new IPEndPoint(ipAddress, 13);

using TcpClient client = new();
await client.ConnectAsync(ipEndPoint);
await using NetworkStream stream = client.GetStream();

var buffer = new byte[1_024];
int received = await stream.ReadAsync(buffer);

var message = Encoding.UTF8.GetString(buffer, 0, received);
Console.WriteLine($"Message received: \"{message}\"");
// Sample output:
//     Message received: "📅 8/22/2022 9:07:17 AM 🕛"

Poprzedni kod języka C#:

  • Tworzy obiekt IPEndPoint na podstawie znanego IPAddress portu i .
  • Utwórz wystąpienie nowego TcpClient obiektu.
  • client Połączenie do zdalnego serwera czasu TCP na porcie 13 przy użyciu polecenia TcpClient.ConnectAsync.
  • Używa elementu do NetworkStream odczytu danych z hosta zdalnego.
  • Deklaruje bufor odczytu bajtów 1_024 .
  • Odczytuje dane z stream buforu odczytu do buforu odczytu.
  • Zapisuje wyniki jako ciąg w konsoli programu .

Ponieważ klient wie, że komunikat jest mały, cały komunikat można odczytać do buforu odczytu w jednej operacji. W przypadku większych komunikatów lub komunikatów o nieokreślonej długości klient powinien używać buforu bardziej odpowiednio i odczytywać while w pętli.

Ważne

Podczas wysyłania i odbierania komunikatów Encoding powinien być znany przed upływem czasu zarówno do serwera, jak i klienta. Jeśli na przykład serwer komunikuje się przy użyciu programu ASCIIEncoding , ale klient spróbuje użyć UTF8Encodingpolecenia , komunikaty będą źle sformułowane.

Utwórz TcpListener

Typ TcpListener służy do monitorowania portu TCP dla żądań przychodzących, a następnie utworzenia elementu Socket lub TcpClient , który zarządza połączeniem z klientem. Metoda Start włącza nasłuchiwanie, a Stop metoda wyłącza nasłuchiwanie na porcie. Metoda AcceptTcpClientAsync akceptuje przychodzące żądania połączenia i tworzy obiekt TcpClient w celu obsługi żądania, a AcceptSocketAsync metoda akceptuje przychodzące żądania połączenia i tworzy obiekt Socket do obsługi żądania.

W poniższym przykładzie pokazano tworzenie serwera czasu sieciowego przy użyciu polecenia TcpListener do monitorowania portu TCP 13. Po zaakceptowaniu przychodzącego żądania połączenia serwer czasu odpowiada z bieżącą datą i godziną z serwera hosta.

var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
TcpListener listener = new(ipEndPoint);

try
{    
    listener.Start();

    using TcpClient handler = await listener.AcceptTcpClientAsync();
    await using NetworkStream stream = handler.GetStream();

    var message = $"📅 {DateTime.Now} 🕛";
    var dateTimeBytes = Encoding.UTF8.GetBytes(message);
    await stream.WriteAsync(dateTimeBytes);

    Console.WriteLine($"Sent message: \"{message}\"");
    // Sample output:
    //     Sent message: "📅 8/22/2022 9:07:17 AM 🕛"
}
finally
{
    listener.Stop();
}

Poprzedni kod języka C#:

  • Tworzy obiekt IPEndPoint z portem IPAddress.Any i .
  • Utwórz wystąpienie nowego TcpListener obiektu.
  • Wywołuje metodę Start , aby rozpocząć nasłuchiwanie na porcie.
  • TcpClient Używa metody z AcceptTcpClientAsync metody do akceptowania przychodzących żądań połączeń.
  • Koduje bieżącą datę i godzinę jako komunikat ciągu.
  • Używa obiektu do zapisywania NetworkStream danych na połączonym kliencie.
  • Zapisuje wysłaną wiadomość do konsoli programu .
  • Na koniec wywołuje metodę Stop , aby zatrzymać nasłuchiwanie na porcie.

Skończona kontrolka TCP z klasą Socket

Zarówno, TcpClient jak i TcpListener wewnętrznie polegaj na Socket klasie, co oznacza, że wszystko, co można zrobić z tymi klasami, można osiągnąć bezpośrednio przy użyciu gniazd. W tej sekcji przedstawiono kilka TcpClient przypadków użycia oraz TcpListener ich Socket odpowiednik, który jest funkcjonalnie równoważny.

Tworzenie gniazda klienta

TcpClientDomyślny konstruktor próbuje utworzyć gniazdo podwójne stosu za pomocą konstruktora Socket(SocketType, ProtocolType). Ten konstruktor tworzy gniazdo dwustosowe, jeśli protokół IPv6 jest obsługiwany, w przeciwnym razie wraca do protokołu IPv4.

Rozważmy następujący kod klienta TCP:

using var client = new TcpClient();

Powyższy kod klienta TCP jest funkcjonalnie równoważny z następującym kodem gniazda:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

Konstruktor TcpClient(AddressFamily)

Ten konstruktor akceptuje tylko trzy AddressFamily wartości. W przeciwnym razie zwróci wartość ArgumentException. Prawidłowe wartości to:

Rozważmy następujący kod klienta TCP:

using var client = new TcpClient(AddressFamily.InterNetwork);

Powyższy kod klienta TCP jest funkcjonalnie równoważny z następującym kodem gniazda:

using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

Konstruktor TcpClient(IPEndPoint)

Po utworzeniu gniazda ten konstruktor będzie również wiązać się z podanym lokalnymIPEndPoint. Właściwość IPEndPoint.AddressFamily służy do określania rodziny adresów gniazda.

Rozważmy następujący kod klienta TCP:

var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var client = new TcpClient(endPoint);

Powyższy kod klienta TCP jest funkcjonalnie równoważny z następującym kodem gniazda:

// Example IPEndPoint object
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);

Konstruktor TcpClient(String, Int32)

Ten konstruktor spróbuje utworzyć podwójny stos podobny do konstruktora domyślnego i połączyć go z zdalnym punktem końcowym DNS zdefiniowanym przez hostname parę iport.

Rozważmy następujący kod klienta TCP:

using var client = new TcpClient("www.example.com", 80);

Powyższy kod klienta TCP jest funkcjonalnie równoważny z następującym kodem gniazda:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

Nawiąż połączenie z serwerem

Wszystkie Connectprzeciążenia , BeginConnectConnectAsynci EndConnect w systemie TcpClient są funkcjonalnie równoważne odpowiednim Socket metodom.

Rozważmy następujący kod klienta TCP:

using var client = new TcpClient();
client.Connect("www.example.com", 80);

Powyższy TcpClient kod jest odpowiednikiem następującego kodu gniazda:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

Tworzenie gniazda serwera

Podobnie jak TcpClient wystąpienia mające równoważność funkcjonalną ze swoimi nieprzetworzonymi Socket odpowiednikami, ta sekcja mapuje TcpListener konstruktory na odpowiadający im kod gniazda. Pierwszym konstruktorem, który należy wziąć pod uwagę, jest .TcpListener(IPAddress localaddr, int port)

var listener = new TcpListener(IPAddress.Loopback, 5000);

Powyższy kod odbiornika TCP jest funkcjonalnie odpowiednikiem następującego kodu gniazda:

var ep = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

Rozpocznij nasłuchiwanie na serwerze

Metoda Start() jest otoką łączącą Socketfunkcje i sBind.Listen()

Rozważmy następujący kod odbiornika TCP:

var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start(10);

Powyższy kod odbiornika TCP jest funkcjonalnie odpowiednikiem następującego kodu gniazda:

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);
try
{
    socket.Listen(10);
}
catch (SocketException)
{
    socket.Dispose();
}

Akceptowanie połączenia z serwerem

Pod maską przychodzące połączenia TCP zawsze tworzą nowe gniazdo po zaakceptowaniu. TcpListenermoże zaakceptować Socket wystąpienie bezpośrednio (za pośrednictwem lub AcceptSocketAsync()) lub zaakceptować TcpClient element (za pośrednictwem AcceptSocket() i AcceptTcpClient()AcceptTcpClientAsync()).

Rozważ następujący TcpListener kod:

var listener = new TcpListener(IPAddress.Loopback, 5000);
using var acceptedSocket = await listener.AcceptSocketAsync();

// Synchronous alternative.
// var acceptedSocket = listener.AcceptSocket();

Powyższy kod odbiornika TCP jest funkcjonalnie odpowiednikiem następującego kodu gniazda:

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
using var acceptedSocket = await socket.AcceptAsync();

// Synchronous alternative
// var acceptedSocket = socket.Accept();

Tworzenie elementu NetworkStream do wysyłania i odbierania danych

Aby TcpClient móc wysyłać i odbierać dane, należy utworzyć wystąpienie NetworkStream obiektu za GetStream() pomocą metody . W programie Socketnależy ręcznie utworzyć NetworkStream plik .

Rozważ następujący TcpClient kod:

using var client = new TcpClient();
using NetworkStream stream = client.GetStream();

Co jest równoważne następującemu kodowi gniazda:

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

// Be aware that transferring the ownership means that closing/disposing the stream will also close the underlying socket.
using var stream = new NetworkStream(socket, ownsSocket: true);

Napiwek

Jeśli kod nie musi pracować z wystąpieniem Stream , możesz opierać się na Socketmetodach Send/Receive (Send, SendAsync, Receive i ReceiveAsync) bezpośrednio zamiast tworzyć NetworkStream.

Zobacz też