Редагувати

Поділитися через


TCP overview

Important

The Socket class is highly recommended for advanced users, instead of TcpClient and TcpListener.

To work with Transmission Control Protocol (TCP), you have two options: either use Socket for maximum control and performance, or use the TcpClient and TcpListener helper classes. TcpClient and TcpListener are built on top of the System.Net.Sockets.Socket class and take care of the details of transferring data for ease of use.

The protocol classes use the underlying Socket class to provide simple access to network services without the overhead of maintaining state information or knowing the details of setting up protocol-specific sockets. To use asynchronous Socket methods, you can use the asynchronous methods supplied by the NetworkStream class. To access features of the Socket class not exposed by the protocol classes, you must use the Socket class.

TcpClient and TcpListener represent the network using the NetworkStream class. You use the GetStream method to return the network stream, and then call the stream's NetworkStream.ReadAsync and NetworkStream.WriteAsync methods. The NetworkStream does not own the protocol classes' underlying socket, so closing it does not affect the socket.

Use TcpClient and TcpListener

The TcpClient class requests data from an internet resource using TCP. The methods and properties of TcpClient abstract the details for creating a Socket for requesting and receiving data using TCP. Because the connection to the remote device is represented as a stream, data can be read and written with .NET Framework stream-handling techniques.

The TCP protocol establishes a connection with a remote endpoint and then uses that connection to send and receive data packets. TCP is responsible for ensuring that data packets are sent to the endpoint and assembled in the correct order when they arrive.

Create an IP endpoint

When working with System.Net.Sockets, you represent a network endpoint as an IPEndPoint object. The IPEndPoint is constructed with an IPAddress and its corresponding port number. Before you can initiate a conversation through a Socket, you create a data pipe between your app and the remote destination.

TCP/IP uses a network address and a service port number to uniquely identify a service. The network address identifies a specific network destination; the port number identifies the specific service on that device to connect to. The combination of network address and service port is called an endpoint, which is represented in the .NET by the EndPoint class. A descendant of EndPoint is defined for each supported address family; for the IP address family, the class is IPEndPoint.

The Dns class provides domain-name services to apps that use TCP/IP internet services. The GetHostEntryAsync method queries a DNS server to map a user-friendly domain name (such as "host.contoso.com") to a numeric Internet address (such as 192.168.1.1). GetHostEntryAsync returns a Task<IPHostEntry> that when awaited contains a list of addresses and aliases for the requested name. In most cases, you can use the first address returned in the AddressList array. The following code gets an IPAddress containing the IP address for the server host.contoso.com.

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

Tip

For manual testing and debugging purposes, you can typically use the GetHostEntryAsync method with the resulting host name from the Dns.GetHostName() value to resolve the localhost name to an IP address. Consider the following code snippet:

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

The Internet Assigned Numbers Authority (IANA) defines port numbers for common services. For more information, see IANA: Service Name and Transport Protocol Port Number Registry). Other services can have registered port numbers in the range 1,024 to 65,535. The following code combines the IP address for host.contoso.com with a port number to create a remote endpoint for a connection.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

After determining the address of the remote device and choosing a port to use for the connection, the app can establish a connection with the remote device.

Create a TcpClient

The TcpClient class provides TCP services at a higher level of abstraction than the Socket class. TcpClient is used to create a client connection to a remote host. Knowing how to get an IPEndPoint, let's assume you have an IPAddress to pair with your desired port number. The following example demonstrates setting up a TcpClient to connect to a time server on TCP port 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 🕛"

The preceding C# code:

  • Creates an IPEndPoint from a known IPAddress and port.
  • Instantiate a new TcpClient object.
  • Connects the client to the remote TCP time server on port 13 using TcpClient.ConnectAsync.
  • Uses a NetworkStream to read data from the remote host.
  • Declares a read buffer of 1_024 bytes.
  • Reads data from the stream into the read buffer.
  • Writes the results as a string to the console.

Since the client knows that the message is small, the entire message can be read into the read buffer in one operation. With larger messages, or messages with an indeterminate length, the client should use the buffer more appropriately and read in a while loop.

Important

When sending and receiving messages, the Encoding should be known ahead of time to both server and client. For example, if the server communicates using ASCIIEncoding but the client attempts to use UTF8Encoding, the messages will be malformed.

Create a TcpListener

The TcpListener type is used to monitor a TCP port for incoming requests and then create either a Socket or a TcpClient that manages the connection to the client. The Start method enables listening, and the Stop method disables listening on the port. The AcceptTcpClientAsync method accepts incoming connection requests and creates a TcpClient to handle the request, and the AcceptSocketAsync method accepts incoming connection requests and creates a Socket to handle the request.

The following example demonstrates creating a network time server using a TcpListener to monitor TCP port 13. When an incoming connection request is accepted, the time server responds with the current date and time from the host server.

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();
}

The preceding C# code:

  • Creates an IPEndPoint with IPAddress.Any and port.
  • Instantiate a new TcpListener object.
  • Calls the Start method to start listening on the port.
  • Uses a TcpClient from the AcceptTcpClientAsync method to accept incoming connection requests.
  • Encodes the current date and time as a string message.
  • Uses a NetworkStream to write data to the connected client.
  • Writes the sent message to the console.
  • Finally, calls the Stop method to stop listening on the port.

Finite TCP control with the Socket class

Both TcpClient and TcpListener internally rely on the Socket class, meaning anything you can do with these classes can be achieved using sockets directly. This section demonstrates several TcpClient and TcpListener use cases, along with their Socket counterpart that is functionally equivalent.

Create a client socket

TcpClient's default constructor tries to create a dual-stack socket via the Socket(SocketType, ProtocolType) constructor. This constructor creates a dual-stack socket if IPv6 is supported, otherwise, it falls back to IPv4.

Consider the following TCP client code:

using var client = new TcpClient();

The preceding TCP client code is functionally equivalent to the following socket code:

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

The TcpClient(AddressFamily) constructor

This constructor accepts only three AddressFamily values, otherwise it will throw a ArgumentException. Valid values are:

Consider the following TCP client code:

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

The preceding TCP client code is functionally equivalent to the following socket code:

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

The TcpClient(IPEndPoint) constructor

Upon creating the socket, this constructor will also bind to the provided local IPEndPoint. The IPEndPoint.AddressFamily property is used to determine the address family of the socket.

Consider the following TCP client code:

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

The preceding TCP client code is functionally equivalent to the following socket code:

// 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);

The TcpClient(String, Int32) constructor

This constructor will attempt to create a dual-stack similar to the default constructor and connect it to the remote DNS endpoint defined by the hostname and port pair.

Consider the following TCP client code:

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

The preceding TCP client code is functionally equivalent to the following socket code:

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

Connect to server

All Connect, ConnectAsync, BeginConnect and EndConnect overloads in TcpClient are functionally equivalent to the corresponding Socket methods.

Consider the following TCP client code:

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

The above TcpClient code is equivalent to the following socket code:

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

Create a server socket

Much like TcpClient instances having functional equivalency with their raw Socket counterparts, this section maps TcpListener constructors to their corresponding socket code. The first constructor to consider is the TcpListener(IPAddress localaddr, int port).

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

The preceding TCP listener code is functionally equivalent to the following socket code:

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

Start listening on the server

The Start() method is a wrapper combining Socket's Bind and Listen() functionality.

Consider the following TCP listener code:

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

The preceding TCP listener code is functionally equivalent to the following socket code:

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();
}

Accept a server connection

Under the hood, incoming TCP connections are always creating a new socket when accepted. TcpListener can accept a Socket instance directly (via AcceptSocket() or AcceptSocketAsync()) or it can accept a TcpClient (via AcceptTcpClient() and AcceptTcpClientAsync()).

Consider the following TcpListener code:

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

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

The preceding TCP listener code is functionally equivalent to the following socket code:

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();

Create a NetworkStream to send and receive data

With TcpClient you need to instantiate a NetworkStream with the GetStream() method to be able to send and receive data . With Socket, you have to do the NetworkStream creation manually.

Consider the following TcpClient code:

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

Which is equivalent to the following socket code:

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);

Tip

If your code doesn't need to work with a Stream instance, you can rely on Socket's Send/Receive methods (Send, SendAsync, Receive and ReceiveAsync) directly instead of creating a NetworkStream.

See also