- System.Net
Core assembly for networking in .NET Framework. Also will be used in our sample project.
- System.Net.Mail
This namespace holds the objects used to send emails, connect to SMTP servers, and other required operations.
- System.Net.Security
Provides us with an object required to generate SSL based streams.
- System.Net.Sockets
This namespace holds the objects for TCP or UDP clients and listeners and clients for working around with network based on these protocols.
We will use the preceding discussed namespaces and create our applications for both server and client use. In both of these scenarios, we would use console projects and write the appropriate code to maintain the server and client applications.
Sample project
For our sample, we will create a sample project that accepts the data from a client. The client sends the data in JSON format, sending the name, email address, and the message of the client to the server. The server (for this example) sends an email to the person (using his email address).
The client asks for the name, email, and message of the user. You can share this client application with anyone on the network, the server would accept the data and respond as required.
Writing the server application
Our server application must be hosted to accept new requests from the client. To host our application, we need to provide an address and port that our application would be listening to. We can use a TcpListener object to create an instance of our server. As a parameter to the constructor, we will pass the IP address endpoint that would be used as the hosting address for our server. Clients need to know this address since they will connect to this server application of ours using this address. Note that I am using the loopback address (127.0.0.1) and a sample port number (1234) because I will use my own machine to act as a server and client. You can use other IP addresses or ports to configure your own servers and provide access to it for your clients in the network.
- IPEndPoint ep = new IPEndPoint(IPAddress.Loopback, 1234);
- TcpListener listener = new TcpListener(ep);
- listener.Start();
At this stage, our server would be up and running and would be ready to accept new requests from TCP clients. Note that networks work with bytes and we would be reading the data in the form of bytes. After accepting the bytes, the decoding is done to convert the data to an appropriate type, text, image, or audio. In this sample, I will show text only that is simple enough. You can use other objects (File for example) and convert the data to an image type using the bytes providing a format, "
image/jpeg" and so on.
Next, we listen to a client. Until a client sends a request, we cannot proceed. To accept the data, we need to have a memory location to store the data sent by the client. Also, we need to store the string representation of the message also. Look below at how I did that.
-
- const int bytesize = 1024 * 1024;
- string message = null;
- byte[] buffer = new byte[bytesize];
-
-
-
-
- var sender = listener.AcceptTcpClient();
-
-
- sender.GetStream().Read(buffer, 0, bytesize);
The preceding code makes our server able to accept a new request and read the data that the client has sent. Until now we have configured the application to be able to start, run, and listen to new requests coming from the server. Our server is currently synchronous, it can only accept one request and would not accept another unless the connection is terminated.
You can use asynchronous programming models to accept multiple requests from multiple clients. In synchronous mode, you cannot accept multiple requests. You need to work with one request, complete the request, and send a response or terminate the connection and then you can move to the next request or client.
Since I told you that bytes are only sent or received, other data types are just fancy items that we use. We need to now convert these bytes into descriptive data. Since we know our client will transmit string data, we can decode the data into string type. Also, note that our buffer size is very large so that we can accept any amount of data. You can shorten it if you want. This much byte data would always contain null characters where there was no character found. Null characters would take much space on your console. I have written a function for that, to remove the null characters from the string.
-
- private static string cleanMessage(byte[] bytes)
- {
-
- string message = System.Text.Encoding.Unicode.GetString(bytes);
-
- string messageToPrint = null;
-
- foreach (var nullChar in message)
- {
-
- if (nullChar != '\0')
- {
- messageToPrint += nullChar;
- }
- }
-
-
- return messageToPrint;
- }
We will use this function in our server as well as the client. To remove null characters from the messages sent or received. Apart from this, I have another 2 functions that are used to handle various functions of our server. One to generate and send the response, the other to send the email to the user as a response.
Sending the email
As for this example, we will use an email as a response to the user. We will send an email to the user to notify him that we have received the data that he sent us. I won't explain how this module is being done, for more information on sending emails in the .NET Framework, please read
this article of mine.
-
- using (SmtpClient client = new SmtpClient("<your-smtp-server>", 25))
- {
-
- client.EnableSsl = true;
- client.Credentials = new NetworkCredential("<email>",
- "<password>");
-
- client.Send(
- new MailMessage("<email-address>", p.Email,
- "Thank you for using the Web Service",
- string.Format(
- @"Thank you for using our Web Service, {0}.We have recieved your message, '{1}'.", p.Name, p.Message
- )
- )
- );
- }
This function would send an email to the client at their email address with this message. You can alter this email address or you can change how the server behaves when the client communicates.
Sending the response
Once everything has been done, it is now time to generate the response to be sent to the client. Similarly, you would send bytes to the client that the application can then convert back to the text or other data that it knows the server sent it.
We can only send a response to the client if he is still connected. Once the connection is lost, we cannot send a response to the client. Since we have the client connected, we can create a separate function to convert bytes of data and stream it down to the client.
-
- private static void sendMessage(byte[] bytes, TcpClient client)
- {
-
- client.GetStream()
- .Write(bytes, 0,
- bytes.Length);
- }
Until now, we have created a server application that can host and accept messages and send responses to them and it can also notify the users using their email addresses.
Complete server program
The complete server program in our console project is as follows:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
- using System.Net.Sockets;
- using System.Net;
- using System.Net.Mail;
- using Newtonsoft.Json;
-
- namespace TcpServer
- {
- class Program
- {
- static void Main(string[] args)
- {
- IPEndPoint ep = new IPEndPoint(IPAddress.Loopback, 1234);
- TcpListener listener = new TcpListener(ep);
- listener.Start();
-
- Console.WriteLine(@"
- ===================================================
- Started listening requests at: {0}:{1}
- ===================================================",
- ep.Address, ep.Port);
-
-
- while (true)
- {
- const int bytesize = 1024 * 1024;
-
- string message = null;
- byte[] buffer = new byte[bytesize];
-
- var sender = listener.AcceptTcpClient();
- sender.GetStream().Read(buffer, 0, bytesize);
-
-
- message = cleanMessage(buffer);
-
-
- Person person = JsonConvert.DeserializeObject<Person>(message);
-
- byte[] bytes = System.Text.Encoding.Unicode.GetBytes("Thank you for your message, " + person.Name);
- sender.GetStream().Write(bytes, 0, bytes.Length);
-
- sendEmail(person);
- }
- }
-
- private static void sendEmail(Person p)
- {
- try
- {
-
- using (SmtpClient client = new SmtpClient("<your-smtp>", 25))
- {
- client.EnableSsl = true;
- client.Credentials = new NetworkCredential("<email>", "<pass>");
-
- client.Send(
- new MailMessage("<email-from>", p.Email,
- "Thank you for using the Web Service",
- string.Format(
- @"Thank you for using our Web Service, {0}.We have recieved your message, '{1}'.", p.Name, p.Message
- )
- )
- );
- }
-
- Console.WriteLine("Email sent to " + p.Email);
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- }
- }
-
- private static string cleanMessage(byte[] bytes)
- {
- string message = System.Text.Encoding.Unicode.GetString(bytes);
-
- string messageToPrint = null;
- foreach (var nullChar in message)
- {
- if (nullChar != '\0')
- {
- messageToPrint += nullChar;
- }
- }
- return messageToPrint;
- }
-
-
- private static void sendMessage(byte[] bytes, TcpClient client)
- {
- client.GetStream()
- .Write(bytes, 0,
- bytes.Length);
- }
- }
-
- class Person
- {
- public string Name { get; set; }
- public string Email { get; set; }
- public string Message { get; set; }
- }
- }
Our application, once running looks like this:
Note: Most of the lines are fancy text in our console. Do not consider them the required fields. I will explain the person's class in the client application. I have also used the Newtonsoft.Json library to convert the data from JSON to our object, I will explain that in a minute. Keep reading!
Writing the client application
Now that our server is up and running, we now need to create an application that would act as our client. The client would make a connection to our server, send some data to it then wait for the server to send a response back to our client application. In this application, we will ask the client for their name, email address, and a message. We will then submit these details as members of a Person object. The person's objects would be used on the server and appropriate functions would be triggered.
Our person object has the following 3 members:
- class Person
- {
- public string Name { get; set; }
- public string Email { get; set; }
- public string Message { get; set; }
- }
We will use this object on our server. I did not explain it before, now I will because I will be using these members to generate a request. Our application asks for user's names, email addresses, and the message that they have. Then it would send that message to the server and the server would send an email to them as a response.
-
- Person person = new Person();
-
- Console.Write("Enter your name: ");
- person.Name = Console.ReadLine();
- Console.Write("Enter your email address: ");
- person.Email = Console.ReadLine();
- Console.Write("Enter your message: ");
- person.Message = Console.ReadLine();
-
-
- byte[] bytes = sendMessage(System.Text.Encoding.Unicode.GetBytes(person.ToJSON()));
In the client, I also created a function to send the message to the server. In this function, I connect to the server using an address and port that the server is running at. Then, it would send the data to the server in bytes. The server would respond to the request in the same function. That is why the function has a return type of byte array.
- private static byte[] sendMessage(byte[] messageBytes)
- {
- const int bytesize = 1024 * 1024;
- try
- {
- System.Net.Sockets.TcpClient client = new System.Net.Sockets.TcpClient("127.0.0.1", 1234);
- NetworkStream stream = client.GetStream();
-
- stream.Write(messageBytes, 0, messageBytes.Length);
- Console.WriteLine("================================");
- Console.WriteLine("= Connected to the server =");
- Console.WriteLine("================================");
- Console.WriteLine("Waiting for response...");
-
- messageBytes = new byte[bytesize];
-
-
- stream.Read(messageBytes, 0, messageBytes.Length);
-
-
- stream.Dispose();
- client.Close();
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- }
-
- return messageBytes;
- }
In the preceding code, our client application attempts to connect to the server and sends the request to the server with the data provided. The server reads the data, works, and then provides a response string, in bytes form. We return those bytes so that the application can clean up our data and show it on the screen. Also, you should use it in a
try-catch block because maybe the server is not available. Instead of breaking the application, you should simply show that server is down and close the application.
Upon running our application, we get this screen.
Now upon submitting, the application would connect and send the data to the server. What the server has for us is not visible in this program. Instead, we need to move to our server application to see what is going on there.
While running the application, I was not connected to the internet so I was provided with that error message. Upon connecting, I was able to get the second line of success message.
After this line of code, the server would send back the response to the client. The client application would be able to show the following message on the console screen to the user:
So, great until now. Server and client applications are doing their work now.
Points of Interest
In this article, I showed how to create a web service or a server/client application using native .NET Framework libraries and assemblies. You can create various types of projects that use a centralized server and distributed client applications.
For security purposes, you should always consider using a SslStream object from the System.Net.Security namespace. Also remember, this type of application framework is synchronous. Only one client can connect at a time. If another client must connect, it would wait until the server is released for further requests and processes. You can use async/await operators to convert the current model to an asynchronous one. I would also post the source code on MSDN galleries for you to download and work with.
Initially, I said that I want to create a program that runs under 10 MB of RAM consumption, after running this application set (of both server and client) we can see that they both run under 5 MB of RAM consumption. Later when uploading starts and email is being sent, it might increase to a 10 MB memory benchmark, but that still is what we were looking for. :)
TcpClient is the client program running and asking the user for input. Whereas vshost32.exe (if you have some experience with Visual Studio) is the program that is attached as a debugger to run the TcpServer.
I hope I have helped you with this article. :) I will be sharing more code for this framework type in the future.