how do i get TcpListener to accept multiple connections and work with each one individually?
Asked Answered
G

5

29

I have an SMTP listener that works well but is only able to receive one connection. My C# code is below and I am running it as a service. My goal is to have it runnign on a server and parsing multiple smtp messages sent to it.

currently it parses the first message and stops working. how can I get it to accept the 2nd, 3rd, 4th... SMTP message and process it like it does the first?

here is my code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;  

namespace SMTP_Listener
{
    class Program
    {
        static void Main(string[] args)
        {


            TcpListener listener = new TcpListener(IPAddress.Any , 8000);
            TcpClient client;
            NetworkStream ns;

            listener.Start();

            Console.WriteLine("Awaiting connection...");
            client = listener.AcceptTcpClient();
            Console.WriteLine("Connection accepted!");

            ns = client.GetStream();

            using (StreamWriter writer = new StreamWriter(ns))
            {
                writer.WriteLine("220 localhost SMTP server ready.");
                writer.Flush();

                using (StreamReader reader = new StreamReader(ns))
                {
                    string response = reader.ReadLine();

                    if (!response.StartsWith("HELO") && !response.StartsWith("EHLO"))
                    {
                        writer.WriteLine("500 UNKNOWN COMMAND");
                        writer.Flush();
                        ns.Close();
                        return;
                    }

                    string remote = response.Replace("HELO", string.Empty).Replace("EHLO", string.Empty).Trim();

                    writer.WriteLine("250 localhost Hello " + remote);
                    writer.Flush();

                    response = reader.ReadLine();

                    if (!response.StartsWith("MAIL FROM:"))
                    {
                        writer.WriteLine("500 UNKNOWN COMMAND");
                        writer.Flush();
                        ns.Close();
                        return;
                    }

                    remote = response.Replace("RCPT TO:", string.Empty).Trim();
                    writer.WriteLine("250 " + remote + " I like that guy too!");
                    writer.Flush();

                    response = reader.ReadLine();

                    if (!response.StartsWith("RCPT TO:"))
                    {
                        writer.WriteLine("500 UNKNOWN COMMAND");
                        writer.Flush();
                        ns.Close();
                        return;
                    }

                    remote = response.Replace("MAIL FROM:", string.Empty).Trim();
                    writer.WriteLine("250 " + remote + " I like that guy!");
                    writer.Flush();

                    response = reader.ReadLine();

                    if (response.Trim() != "DATA")
                    {
                        writer.WriteLine("500 UNKNOWN COMMAND");
                        writer.Flush();
                        ns.Close();
                        return;
                    }

                    writer.WriteLine("354 Enter message. When finished, enter \".\" on a line by itself");
                    writer.Flush();

                    int counter = 0;
                    StringBuilder message = new StringBuilder();

                    while ((response = reader.ReadLine().Trim()) != ".")
                    {
                        message.AppendLine(response);
                        counter++;

                        if (counter == 1000000)
                        {
                            ns.Close();
                            return;  // Seriously? 1 million lines in a message?
                        }
                    }

                    writer.WriteLine("250 OK");
                    writer.Flush();
                    ns.Close();
                    // Insert "message" into DB
                    Console.WriteLine("Received message:");
                    Console.WriteLine(message.ToString());
                }
            }

            Console.ReadKey();
        }
    }
}
Gliadin answered 17/3, 2011 at 13:38 Comment(0)
S
42

You can factor out most of your code into a separate thread:

static void Main(string[] args)
{
    TcpListener listener = new TcpListener(IPAddress.Any , 8000);
    TcpClient client;
    listener.Start();

    while (true) // Add your exit flag here
    {
        client = listener.AcceptTcpClient();
        ThreadPool.QueueUserWorkItem(ThreadProc, client);
    }
}
private static void ThreadProc(object obj)
{
    var client = (TcpClient)obj;
    // Do your work here
}
Schmaltzy answered 17/3, 2011 at 13:46 Comment(3)
Why not with BeginAcceptTcpClient? In a very simple example like this it is not necessary, but if there is any GUI, the async BeginAcceptTcpClient will avoid freezing.Procurator
All this is good and all, but I'm dealing with binary data, not nice strings. How do I tell the listner object I want the raw data in byte[] array form?Geis
@BingBang You don't tell the listener object anything. You get the client from the listener and from the client you get your stream, which is a byte[] by default.Tonality
S
28

You almost certainly want to spin each connection into another thread. So you have the "accept" call in a loop:

while (listening)
{
    TcpClient client = listener.AcceptTcpClient();
    // Start a thread to handle this client...
    new Thread(() => HandleClient(client)).Start();
}

Obviously you'll want to adjust how you spawn threads (maybe use the thread pool, maybe TPL etc) and how you stop the listener gracefully.

Stereograph answered 17/3, 2011 at 13:44 Comment(6)
how would this solution scale? would it be prudent to have two threads- one thread to spool the incoming requests and another to iterate through hem and process them?Gliadin
@kacalapy: It will scale fine for most situations, although you'd probably want to use a thread pool. You don't want one connection having to wait for another to be fully processed before it gets a turn.Stereograph
@JonSkeet What would you recommend for best result? Using threadpool like ThePretender answer?Heterophyte
@publicENEMY: Actually I'd typically recommend using the Task Parallel Library if you can...Stereograph
@JonSkeet do you know some sample that dose not spin up a new thread for receiving data from each client?Striper
@SHM: Well you could use a thread pool as per the accepted answer - or you could use NIO and asynchronous code, but that would be significantly more complex.Stereograph
K
8

I know this is old question but I am sure many will like this answer.

// 1
while (listening)
{
    TcpClient client = listener.AcceptTcpClient();
    // Start a thread to handle this client...
    new Thread(() => HandleClient(client)).Start();
}

// 2
while (listening)
{
    TcpClient client = listener.AcceptTcpClient();
    // Start a task to handle this client...
    Task.Run(() => HandleClient(client));
}

// 3
public async void StartListener() //non blocking listener
{
    listener = new TcpListener(ipAddress, port);
    listener.Start();
    while (listening)
    {
        TcpClient client = await listener.AcceptTcpClientAsync().ConfigureAwait(false);//non blocking waiting                    
        // We are already in the new task to handle this client...   
        HandleClient(client);
    }
}
//... in your code
StartListener();
//...
//use Thread.CurrentThread.ManagedThreadId to check task/thread id to make yourself sure
Kieffer answered 30/1, 2016 at 13:49 Comment(3)
What if HandleClient() is async because in that function we are awaiting ReadLineAsync() from a streamreader?Fleischman
In 3) I think it shuld be async but without await, just HandleClient(client)Kieffer
To answer my previous question, I believe the code would have to be await HandleClient() instead of just simply HandleClient(). @Kieffer I think you are referring to a fire and forget as well if I remember correctly? Or do you mean to remove the await listener... line above HandleClient()?Fleischman
P
2

Late answer because this question did not had the answer that I was looking for. Here is what I was looking for:

using System.Net.Sockets;

class Program
{
    static void Main()
    {
        var server = new TcpListener(System.Net.IPAddress.Any, 80);
        server.Start();

        // Wait for connection...
        server.BeginAcceptTcpClient(OnClientConnecting, server);

        Console.ReadLine();        
    }

    static void OnClientConnecting(IAsyncResult ar)
    {
        try
        {
            Console.WriteLine("Client connecting...");

            if (ar.AsyncState is null)
                throw new Exception("AsyncState is null. Pass it as an argument to BeginAcceptSocket method");

            // Get the server. This was passed as an argument to BeginAcceptSocket method
            TcpListener s = (TcpListener)ar.AsyncState;

            // listen for more clients. Note its callback is this same method (recusive call)
            s.BeginAcceptTcpClient(OnClientConnecting, s);

            // Get the client that is connecting to this server
            using TcpClient client = s.EndAcceptTcpClient(ar);

            Console.WriteLine("Client connected succesfully");

            // read data sent to this server by client that just connected
            byte[] buffer = new byte[1024];
            var i = client.Client.Receive(buffer);
            Console.WriteLine($"Received {i} bytes from client");

            // reply back the same data that was received to the client
            var k = client.Client.Send(buffer, 0, i, SocketFlags.None);
            Console.WriteLine($"Sent {k} bytes to slient as reply");

            // close the tcp connection
            client.Close();
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }
}

Then when I make a request to http://localhost/foo on my browser I get this:

GET /test HTTP/1.1
Host: localhost
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="106", "Google Chrome";v="106", "Not;A=Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,es;q=0.8
Proptosis answered 26/10, 2022 at 20:47 Comment(0)
D
1

As per your code, you are starting one listener and receiving and processing message and closing program.

You need to maintain a listener and TcpClient object can be passed to another function to process the message received. listener.Start();

        Console.WriteLine("Awaiting connection...");
        client = listener.AcceptTcpClient();
        Console.WriteLine("Connection accepted!");
Dosser answered 15/9, 2020 at 13:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.