TCP connections require a three-stage handshaking mechanism:
First, the server must enter listening mode by performing a passive open. At this point, the server will be idle, waiting for an incoming request.
A client can then use the IP address and port number to perform an active open. The server will respond with an acknowledgment message in a predetermined format that incorporates the client sequence number.
Finally, the client will respond to the acknowledgment. At this point, the connection is ready to transmit data in either direction.
In .NET, you perform the passive open with the server by using the TcpListener.Start() method. This method initializes the port, sets up the underlying socket, and begins listening, although it doesn't block your code. After this point, you can call the Pending() method to determine if any connection requests have been received. Pending() examines the underlying socket and returns True if there's a connection request. You can also call AcceptTcpClient() at any point to retrieve the connection request, or block the application until a connection request is received.
When AcceptTcpClient() returns, it provides a TcpClient object that can be used to retrieve and send data. The easiest approach is to create a NetworkStream by calling the TcpClient.GetStream() method. After this, communication is simply a matter of reading and writing to a stream, and it can be performed in more or less the same way you would access a file on your computer's hard drive.
The following example shows a console server that waits for a TCP connection request on port 11000. When a connection request is received, it accepts it automatically and starts a new thread to listen for data received from the client. When a message is received, a BinaryReader is used to retrieve it, and the message is displayed in the console window. At the same time, the main application thread loops continuously, prompting the user for input, and sends input strings to the client using a BinaryWriter.
Imports System.Net Imports System.Net.Sockets Imports System.IO Imports System.Threading Module TcpServerConsole Private Stream As NetworkStream Public Sub Main() ' Create a new listener on port 11000. Dim Listener As New TcpListener(11000) ' Initialize the port and start listening. Listener.Start() Console.WriteLine("* TCP Server *") Console.WriteLine("Waiting for a connection...") Try ' Wait for a connection request ' and return a TcpClient initialized for communication. Dim Client As TcpClient = Listener.AcceptTcpClient() Console.WriteLine("Connection accepted.") Console.WriteLine(New String("-", 40)) Console.WriteLine() ' Retrieve the network stream. Stream = Client.GetStream() ' Create a new thread for receiving incoming messages. Dim ReceiveThread As New Thread(AddressOf ReceiveData) ReceiveThread.IsBackground = True ReceiveThread.Start() ' Create a BinaryWriter for writing to the stream. Dim w As New BinaryWriter(Stream) ' Loop until the word QUIT is entered. Dim Text As String Do Text = Console.ReadLine() ' Send the text to the remote client. If Text <> "QUIT" Then w.Write(Text) Loop Until Text = "QUIT" ' Terminate the receiving thread. ReceiveThread.Abort() ' Close the connection socket. Client.Close() ' Close the underlying socket (stop listening for new requests). Listener.Stop() Catch Err As Exception Console.WriteLine(Err.ToString()) End Try End Sub Private Sub ReceiveData() ' Create a BinaryReader for the stream. Dim r As New BinaryReader(Stream) Do ' Display any received text. Try If Stream.DataAvailable Then Console.WriteLine(("*** RECEIVED: " + r.ReadString())) End If Catch Err As Exception Console.WriteLine(Err.ToString()) End Try Loop End Sub End Module
This demonstrates one important aspect of socket programming with .NET—you can write and read data asynchronously.
The client code uses the TcpClient.Connect() method to initiate the connection. After that point, the stream is retrieved from the GetStream() method, and the code is almost identical.
Imports System.Net Imports System.Net.Sockets Imports System.IO Imports System.Threading Module TcpClientConsole Private Stream As NetworkStream Public Sub Main() Dim Client As New TcpClient() Try ' Try to connect to the server on port 11000. Client.Connect(IPAddress.Parse("127.0.0.1"), 11000) Console.WriteLine("* TCP Client *") Console.WriteLine("Connection established.") Console.WriteLine(New String("-", 40)) Console.WriteLine() ' Retrieve the network stream. Stream = Client.GetStream() ' Create a new thread for receiving incoming messages. Dim ReceiveThread As New Thread(AddressOf ReceiveData) ReceiveThread.IsBackground = True ReceiveThread.Start() ' Create a BinaryWriter for writing to the stream. Dim w As New BinaryWriter(Stream) ' Loop until the word QUIT is entered. Dim Text As String Do Text = Console.ReadLine() ' Send the text to the remote client. If Text <> "QUIT" Then w.Write(Text) Loop Until Text = "QUIT" ' Close the connection socket. Client.Close() Catch Err As Exception Console.WriteLine(Err.ToString()) End Try End Sub Private Sub ReceiveData() ' Create a BinaryReader for the stream. Dim r As New BinaryReader(Stream) Do ' Display any received text. Try If Stream.DataAvailable Then Console.WriteLine(("*** RECEIVED: " + r.ReadString())) End If Catch Err As Exception Console.WriteLine(Err.ToString()) End Try Loop End Sub End Module
Figure 7-4 shows both parts of the applications as they interact.
Newcomers to network programming often wonder how they can handle more than one simultaneous request, and they sometimes assume that multiple server reports are required. This isn't the case—if it were, a small set of applications could quickly exhaust the available ports.
Instead, server applications handle multiple requests with the same port. This process is almost completely transparent because the underlying TCP architecture in Windows automatically identifies messages and routes them to the appropriate object in your code. Connections are uniquely identified based on four pieces of information: the IP address and server port, and the IP address and client port. For example, Figure 7-5 shows a server with connections to two different clients. The server endpoint is the same, but the connections are uniquely identified at the operating system level based on the client's IP address and port number.
Remember, unless you specify otherwise, the client's port is chosen dynamically from the set of available ports when the connection is created. That means that you could create a client that opens multiple connections to the same server. On the server side, each connection would be dealt with uniquely, because each connection would have a different client port number.
Of course, to handle simultaneous connections you'll need to use multi-threading. Here's an outline of the basic pattern you would use on the server:
Dim Listener As New TcpListener(11000) Dim Client As TcpClient ' Initialize the port and start listening. Listener.Start() Do ' Wait for a connection request ' and return a TcpClient initialized for communication. Client = Listener.AcceptTcpClient() ' Create a new object to handle this connection. Dim Handler As New MyTcpClientHandler(Client) ' Start this object working on another thread. Dim HandlerThread As New Thread(AddressOf Handler.Process()) HandlerThread.Start() Loop ' Close the underlying socket (stop listening for new requests). Listener.Stop()
In addition, the main listener thread would probably use some sort of collection to track in-progress connections. Chapter 9 presents a complete example of a multithreaded server and client, with a file-sharing application that uses TCP connections to transfer file data.