Issue With Simple Client To Server Chat Program

Started by
28 comments, last by Psychopathetica 9 years, 4 months ago

Unfortunately, that code is not thread safe.

There is a simple rule: if a variable is accessed by multiple threads, and at least one could be writing to it, then your code is broken if you are not using thread safe types (e.g. std::atomic) or synchronisation (e.g. mutexes). Not doing so is called a race condition, and attempting to run code with a race condition is undefined.

Unfortunately, the reverse is not true, merely using synchronisation or thread safe locks does not mean your code is not broken. To write thread safe code, you have to clearly define data that is shared and take the appropriate measures to ensure that the data can be updated safely, and ensure that each thread can make progress eventually (e.g. preventing starvation and deadlocks).

Let us consider some scenarios that could arise with your code.

In the first, imagine the server thread has a single space left, and accepts a new client. This spawns a client thread. There is no guarantee that the client thread will run yet, so it is possible that another client could be accepted, because "num clients" has not been updated. What happens? Well, in this case, the server thread's loop to find a new client index won't find one. However, the value of "temp_id" has not been updated, it will still retain the value of the previous client. So now another thread will be spawned with the same "client" reference. Clearly not intended!

So that was a simple case, and we haven't even run into any of the "fun" problems yet!

On a system with multiple processors, each processor has several levels of memory caches. To be efficient, the values from memory in those caches are assumed to be owned by that processor, that is, only certain instructions will cause the cache to be evicted or bypassed to check if the value in main memory has been updated. Also, writes to memory may stay in various buffers in that processor too. The end result of this there is no guarantee when or if an update to a variable in memory may become visible to another, unless one of the special instructions mentioned is executed. So, on some architectures and some machines, your client thread's changes may not been seen by the server thread (or other client threads) for some time, or ever!

Another factor is an optimising compiler. For instance, a common optimisation is to avoid reading and writing to main memory in a loop, but rather to read the value into a register, run the loop, and write any changes after the loop is finished. Compiler writers assume that code is single threaded by default, so unless you use a special type like std::atomic, or a thread synchronisation function, the optimiser might also cause changes to go unpublished to other threads.

In some cases, a changed variable value can actually be only partially published, one more more bytes might appear to have the new value, but the others have the old value. The resulting combination is meaningless, for instance, incrementing 255 to 256 might appear as 511, a value which might be totally out of range!

So, essentially you have to think of race conditions as similar to uninitialised variables, the consequences can be difficult to predict and will often vary from run to run. This makes it very difficult to tell if you've actually fixed them, simply by adding some locking (but not enough), you can get the expected result more often due to the change in timings that creates.

The only way to do multi-threaded programming is to think of a design, and carefully implement it.

Learning how to use select() properly is far simpler, and as the others have mentioned will actually scale better than trying to use a thread per client. More importantly, you'll be able to reason about your program. You have to learn to reason in a multi-threaded way.

Some of the oldest and most mysterious, unreproducible and unsolved bugs in the system I work on are presumed to be subtle race conditions - and that program has on the order of a million lines of code for them to hide in. We have fixed a number of bugs which were race conditions, they are always tricky to find. This system has a team of experience professionals developing and maintaining it, and still doing threading correctly is Hard (with a capital H).

Advertisement
The real trouble here is the general challenge of safe multi-threaded programming, and has nothing to do with networking.
Multi-threading is actually hard, and requires not only an understanding of the problems involved, but also a careful and deliberate approach throughout your entire application.
If you want to learn and practice multi-threading, then that's fine! If you just want to write a server that is robust and works fine, I suggest using a single thread, and select() over all open sockets (including the listening socket.)
enum Bool { True, False, FileNotFound };

Unfortunately, that code is not thread safe.

Also, please DON'T share the socket between threads in a way where you cannot insure that no two or more threads will try to send() and recv() at the same time.

I don't think berkely sockets or winsock is thread safe for that scenario.

Anytime you take something from a library you didn't write yourself, treat it as not thread safe and wrap its use with a mutex.


Also, thread-per-client is an anti-pattern in multi-user games programming. You either want single-thread (and scale on a machine by using multiple processes) or thread-per-subsystem (physics, networking, AI, etc,) or thread-per-CPU (with a work item queue.)

It would also become slower performance wise to have a thread per client due to all of the expensive context switches.

Unless of course you actually had a server with 1 CPU core per every client you ever expected to have :p.

thread-per-CPU (with a work item queue.)

I've been fooling around with this one. It's a steep learning curve but once you figure out how to structure it in C++ it becomes really cool.

please DON'T share the socket between threads in a way where you cannot insure that no two or more threads will try to send() and recv() at the same time.


Under Windows NT kernels (so, Windows 2000, Windows XP, and onwards,) the sockets API is thread "safe" in the sense that well-defined, reasonably outcomes will happen. Make sure to specify version 2,2 of the WinSock API when you initialize it.
On UNIX, sockets are just file descriptors, which are inherently thread safe, as far as the kernel API goes.
Thus, it is totally "safe" to use one thread for reading, and one thread for writing, for the same socket.

The real problem comes when you may potentially read from more than one thread, or write from more than one thread. There is no inherent ordering between the threads, and thus the data you think you read (or write) may be somewhat jumbled between the two threads. It's totally possible that thread A dequeues some data, then gets pre-empted, then thread B dequeues some LATER data, and returns to the application, before the application sees the data in thread A.
This may be what Gl2eenDl2agon was really trying to warn against.

thread-per-CPU (with a work item queue.)

once you figure out how to structure it in C++ it becomes really cool


Word!
enum Bool { True, False, FileNotFound };
I havent displayed the new code, but i removed a number of things from the process client thread from the server, such as tempid and numclients. I count the clients now through a for loop seeing how many active sockets exist, and it works. Im not sending/receiving at the same time as some of you claim because it would be impossible. Both loops iterate once and get caught, either on accept() or recv(). It only sends the id to that particular client after accepting. In my new code, i also made it to where itll send a server is full message to the client that over exceeded the max number of clients. It works great and i have no problems with it so far. Ill share the new code when i get home, as im at work at the moment.

I'm home from work. I was curious on everyone's opinion on my new code. This new code should be thread safe, and it works great. Like I said in the last post, I removed some variables from the thread function, counted my clients differently, and when the server is full, any new clients that log in get sent the message that the server is full. It runs flawlessly thus far.

Server:

#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
#include <thread>
#include <vector>

#pragma comment (lib, "Ws2_32.lib")

#define IP_ADDRESS "192.168.56.1"
#define DEFAULT_PORT "3504"
#define DEFAULT_BUFLEN 512

struct client_type
{
    int id;
    SOCKET socket;
};

const char OPTION_VALUE = 1;
const int MAX_CLIENTS = 5;

//Function Prototypes
int process_client(client_type &new_client, std::vector<client_type> &client_array, std::thread &thread);
int main();

int process_client(client_type &new_client, std::vector<client_type> &client_array, std::thread &thread)
{
    std::string msg = "";
    char tempmsg[DEFAULT_BUFLEN] = "";

    //Session
    while (1)
    {
        memset(tempmsg, 0, DEFAULT_BUFLEN);

        if (new_client.socket != 0)
        {
            int iResult = recv(new_client.socket, tempmsg, DEFAULT_BUFLEN, 0);

            if (iResult != SOCKET_ERROR)
            {
                if (strcmp("", tempmsg))
                    msg = "Client #" + std::to_string(new_client.id) + ": " + tempmsg;

                std::cout << msg.c_str() << std::endl;

                //Broadcast that message to the other clients
                for (int i = 0; i < MAX_CLIENTS; i++)
                {
                    if (client_array[i].socket != INVALID_SOCKET)
                        if (new_client.id != i)
                            iResult = send(client_array[i].socket, msg.c_str(), strlen(msg.c_str()), 0);
                }
            }
            else
            {
                msg = "Client #" + std::to_string(new_client.id) + " Disconnected";

                std::cout << msg << std::endl;

                closesocket(new_client.socket);
                closesocket(client_array[new_client.id].socket);
                client_array[new_client.id].socket = INVALID_SOCKET;

                //Broadcast the disconnection message to the other clients
                for (int i = 0; i < MAX_CLIENTS; i++)
                {
                    if (client_array[i].socket != INVALID_SOCKET)
                        iResult = send(client_array[i].socket, msg.c_str(), strlen(msg.c_str()), 0);
                }

                break;
            }
        }
    } //end while

    thread.detach();

    return 0;
}

int main()
{
    WSADATA wsaData;
    struct addrinfo hints;
    struct addrinfo *server = NULL;
    SOCKET server_socket = INVALID_SOCKET;
    std::string msg = "";
    std::vector<client_type> client(MAX_CLIENTS);
    int num_clients = 0;
    int temp_id = -1;
    std::thread my_thread[MAX_CLIENTS];

    //Initialize Winsock
    std::cout << "Intializing Winsock..." << std::endl;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    //Setup hints
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;

    //Setup Server
    std::cout << "Setting up server..." << std::endl;
    getaddrinfo(static_cast<LPCTSTR>(IP_ADDRESS), DEFAULT_PORT, &hints, &server);

    //Create a listening socket for connecting to server
    std::cout << "Creating server socket..." << std::endl;
    server_socket = socket(server->ai_family, server->ai_socktype, server->ai_protocol);

    //Setup socket options
    setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &OPTION_VALUE, sizeof(int)); //Make it possible to re-bind to a port that was used within the last 2 minutes
    setsockopt(server_socket, IPPROTO_TCP, TCP_NODELAY, &OPTION_VALUE, sizeof(int)); //Used for interactive programs

    //Assign an address to the server socket.
    std::cout << "Binding socket..." << std::endl;
    bind(server_socket, server->ai_addr, (int)server->ai_addrlen);

    //Listen for incoming connections.
    std::cout << "Listening..." << std::endl;
    listen(server_socket, SOMAXCONN);

    //Initialize the client list
    for (int i = 0; i < MAX_CLIENTS; i++)
    {
        client[i] = { -1, INVALID_SOCKET };
    }

    while (1)
    {

        SOCKET incoming = INVALID_SOCKET;
        incoming = accept(server_socket, NULL, NULL);

        if (incoming == INVALID_SOCKET) continue;

        //Reset the number of clients
        num_clients = -1;

        //Create a temporary id for the next client
        temp_id = -1;
        for (int i = 0; i < MAX_CLIENTS; i++)
        {
            if (client[i].socket == INVALID_SOCKET && temp_id == -1)
            {
                client[i].socket = incoming;
                client[i].id = i;
                temp_id = i;
            }

            if (client[i].socket != INVALID_SOCKET)
                num_clients++;

            //std::cout << client[i].socket << std::endl;
        }

        if (temp_id != -1)
        {
            //Send the id to that client
            std::cout << "Client #" << client[temp_id].id << " Accepted" << std::endl;
            msg = std::to_string(client[temp_id].id);
            send(client[temp_id].socket, msg.c_str(), strlen(msg.c_str()), 0);

            //Create a thread process for that client
            my_thread[temp_id] = std::thread(process_client, std::ref(client[temp_id]), std::ref(client), std::ref(my_thread[temp_id]));
        }
        else
        {
            msg = "Server is full";
            send(incoming, msg.c_str(), strlen(msg.c_str()), 0);
            std::cout << msg << std::endl;
        }
    } //end while


    //Close listening socket
    closesocket(server_socket);

    //Close client socket
    for (int i = 0; i < MAX_CLIENTS; i++)
    {
        my_thread[i].detach();
        closesocket(client[i].socket);
    }

    //Clean up Winsock
    WSACleanup();
    std::cout << "Program has ended successfully" << std::endl;

    system("pause");
    return 0;
}

Client:

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <string>
#include <thread>

using namespace std;

#pragma comment (lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 512            
#define IP_ADDRESS "192.168.56.1"
#define DEFAULT_PORT "3504"

struct client_type
{
    SOCKET socket;
    int id;
    char received_message[DEFAULT_BUFLEN];
};

int process_client(client_type &new_client);
int main();

int process_client(client_type &new_client)
{
    while (1)
    {
        memset(new_client.received_message, 0, DEFAULT_BUFLEN);

        if (new_client.socket != 0)
        {
            int iResult = recv(new_client.socket, new_client.received_message, DEFAULT_BUFLEN, 0);

            if (iResult != SOCKET_ERROR)
                cout << new_client.received_message << endl;
            else
            {
                cout << "recv() failed: " << WSAGetLastError() << endl;
                break;
            }
        }
    }

    if (WSAGetLastError() == WSAECONNRESET)
        cout << "The server has disconnected" << endl;

    return 0;
}

int main()
{
    WSAData wsa_data;
    struct addrinfo *result = NULL, *ptr = NULL, hints;
    string sent_message = "";
    client_type client = { INVALID_SOCKET, -1, "" };
    int iResult = 0;
    string message;

    cout << "Starting Client...\n";

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2, 2), &wsa_data);
    if (iResult != 0) {
        cout << "WSAStartup() failed with error: " << iResult << endl;
        return 1;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    cout << "Connecting...\n";

    // Resolve the server address and port
    iResult = getaddrinfo(static_cast<LPCTSTR>(IP_ADDRESS), DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        cout << "getaddrinfo() failed with error: " << iResult << endl;
        WSACleanup();
        system("pause");
        return 1;
    }

    // Attempt to connect to an address until one succeeds
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {

        // Create a SOCKET for connecting to server
        client.socket = socket(ptr->ai_family, ptr->ai_socktype,
            ptr->ai_protocol);
        if (client.socket == INVALID_SOCKET) {
            cout << "socket() failed with error: " << WSAGetLastError() << endl;
            WSACleanup();
            system("pause");
            return 1;
        }

        // Connect to server.
        iResult = connect(client.socket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(client.socket);
            client.socket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (client.socket == INVALID_SOCKET) {
        cout << "Unable to connect to server!" << endl;
        WSACleanup();
        system("pause");
        return 1;
    }

    cout << "Successfully Connected" << endl;

    //Obtain id from server for this client;
    recv(client.socket, client.received_message, DEFAULT_BUFLEN, 0);
    message = client.received_message;

    if (message != "Server is full")
    {
        client.id = atoi(client.received_message);

        thread my_thread(process_client, client);

        while (1)
        {
            getline(cin, sent_message);
            iResult = send(client.socket, sent_message.c_str(), strlen(sent_message.c_str()), 0);

            if (iResult <= 0)
            {
                cout << "send() failed: " << WSAGetLastError() << endl;
                break;
            }
        }

        //Shutdown the connection since no more data will be sent
        my_thread.detach();
    }
    else
        cout << client.received_message << endl;

    cout << "Shutting down socket..." << endl;
    iResult = shutdown(client.socket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        cout << "shutdown() failed with error: " << WSAGetLastError() << endl;
        closesocket(client.socket);
        WSACleanup();
        system("pause");
        return 1;
    }

    closesocket(client.socket);
    WSACleanup();
    system("pause");
    return 0;
}

There is still the possibility that your code will attempt to send data on an invalid socket.

You see, each thread could possibly be running on a different CPU with each having their own cached view of parts of RAM.

The the cache on one CPU says that the socket is invalid, but the cache on another hasn't been updated yet...you see where this is going.

You use what are called "atomic" operations for situations like this.

Although i think it would just silently throw an error and continue working in this case.

You see, each thread could possibly be running on a different CPU with each having their own cached view of parts of RAM.
The the cache on one CPU says that the socket is invalid, but the cache on another hasn't been updated yet...you see where this is going.


This is not actually a problem with any currently active CPU architecture. Especially not with strongly-consistent x86/x64 architectures.

However, code that could run into a weakly-ordered memory race for this kind of test, would be much more likely to run into a weakly-ordered scheduling race (even on a single CPU, with multithreading,) that would expose the same kind of bug, so the recommendation to make sure there are no races is still sound!
enum Bool { True, False, FileNotFound };

Here is a simple program like yours, but using select() instead of threads:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define PORT 2020
 
int prepareListenSocket()
{
    int listenSocket = socket(AF_INET, SOCK_STREAM, 0);
    if(listenSocket == -1)
    {
        perror("Failed to create listen socket");
        exit(1);
    }
 
    int yes = 1;
    if(setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
    {
        perror("Failed to SO_REUSEADDR on listen socket");
    }
    
    sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    serveraddr.sin_port = htons(PORT);
    memset(&(serveraddr.sin_zero), '\0', 8);
     
    if(bind(listenSocket, (sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
    {
        perror("Failed to bind listen socket");
        exit(1);
    }
     
    if(listen(listenSocket, 10) == -1)
    {
         perror("Failed to listen");
         exit(1);
    }
 
    printf("Listening on port %d...\n", PORT);
    return listenSocket;
}
 
struct Server
{
    int listenSocket;
    fd_set socketList;
    int maxSocket;
};
 
void broadcastMessage(Server &server, int socketToSkip, const char *message, int length)
{
    for(int socket = 0; socket <= server.maxSocket; socket++)
    {
        if(FD_ISSET(socket, &server.socketList) && socket != server.listenSocket && socket != socketToSkip)
        {
            if(send(socket, message, length, 0) == -1)
            {
                perror("send() error occurred");
            }
        }
    }
}
 
void handleNewConnection(Server &server)
{
    sockaddr_in clientaddr;
    socklen_t addrlen = sizeof(clientaddr);
 
    int clientSocket = accept(server.listenSocket, (struct sockaddr *)&clientaddr, &addrlen);
    if(clientSocket == -1)
    {
        perror("Failed to accept");
    }
    else
    {
        printf("New connection from %s on socket %d\n", inet_ntoa(clientaddr.sin_addr), clientSocket);
 
        FD_SET(clientSocket, &server.socketList);
        if(clientSocket > server.maxSocket)
        {
            server.maxSocket = clientSocket;
        }
        const char *message = "New client connected!\n";
        broadcastMessage(server, clientSocket, message, strlen(message));
    }
}
 
void handleClient(int clientSocket, Server &server)
{
    char buf[1024];
    int nbytes = recv(clientSocket, buf, sizeof(buf), 0);
    if(nbytes <= 0)
    {
        if(nbytes == 0)
        {
            printf("Client %d closed\n", clientSocket);
        }
        else
        {
            perror("Error with recv");
        }
 
        close(clientSocket);
        FD_CLR(clientSocket, &server.socketList);
 
        const char *message = "A client disconnected!\n";
        broadcastMessage(server, clientSocket, message, strlen(message));
    }
    else
    {
        broadcastMessage(server, clientSocket, buf, nbytes);
    }
}
 
int main(int argc, char *argv[])
{
    Server server;
    server.listenSocket = prepareListenSocket();
    FD_ZERO(&server.socketList);
    FD_SET(server.listenSocket, &server.socketList);
    server.maxSocket = server.listenSocket;
    for(;;)
    {
        fd_set copy = server.socketList;
        int maxSockets = server.maxSocket;
        if(select(maxSockets + 1, &copy, NULL, NULL, NULL) == -1)
        {
            perror("Failed to select");
            exit(1);
        }
        
        for(int socket = 0; socket <= maxSockets; socket++)
        {
            if(FD_ISSET(socket, &copy))
            {
                if(socket == server.listenSocket)
                {
                     handleNewConnection(server);
                }
                else
                {
                    handleClient(socket, server);
                }
            }
        }
    }
    return 0;
}

Now, I adapted this from a random example I found on the internet, and it is written for Unix, but you can see that it is quite easy to read and follow. More importantly, it cannot contain race conditions because it does not use threads.

Thanks for the feedback. But so far there has been no problems at all. So I'm gonna walk you guys through the code just to prove my point.

In the server, lets disregard the initialization process, as thats a given. In the actual loop in main(), I created a temporary variable SOCKETS incoming to be used to accept the next client.


    while (1)
    {

        SOCKET incoming = INVALID_SOCKET;
        incoming = accept(server_socket, NULL, NULL);

That is where it gets caught and wont continue unless a client logs onto my server. When a client does, it checks if its an INVALID_SOCKET still. If it is, it skips all the code in the loop and starts the loop over to try accepting another client. When and if it succeeds, it can be put into my client array with an id and a socket by finding the next empty client. The for loop will also count the number of valid client sockets, giving me the proper number of clients.


        if (incoming == INVALID_SOCKET) continue;

        //Reset the number of clients
        num_clients = -1;

        //Create a temporary id for the next client
        temp_id = -1;
        for (int i = 0; i < MAX_CLIENTS; i++)
        {
            if (client[i].socket == INVALID_SOCKET && temp_id == -1)
            {
                client[i].socket = incoming;
                client[i].id = i;
                temp_id = i;
            }

            if (client[i].socket != INVALID_SOCKET)
                num_clients++;

            //std::cout << client[i].socket << std::endl;
        }

If the temp id is not -1, it can proceed in sending that id given to that client, which only happens once the client logs on. It can then run the other thread. If the temp id is still -1, then the server is full. On top of that, itll send the message using the "incoming" socket (my temp socket I used earlier) to that client that attempted to log on with the message "server is full".


        if (temp_id != -1)
        {
            //Send the id to that client
            std::cout << "Client #" << client[temp_id].id << " Accepted" << std::endl;
            msg = std::to_string(client[temp_id].id);
            send(client[temp_id].socket, msg.c_str(), strlen(msg.c_str()), 0);

            //Create a thread process for that client
            my_thread[temp_id] = std::thread(process_client, std::ref(client[temp_id]), std::ref(client), std::ref(my_thread[temp_id]));
        }
        else
        {
            msg = "Server is full";
            send(incoming, msg.c_str(), strlen(msg.c_str()), 0);
            std::cout << msg << std::endl;
        }
    } //end while

My loop will cycle again, and once again get caught on accept. That is thread 1. It loops once and gets caught on accept() every time, and will only do so when a client logs on.

Now my other thread (which is actually an array of threads), is created for every new client. And those loop only once before getting caught on recv(). So lets say I have all 5 clients logged on:

  • ---
  • ---
  • ---
  • ---
  • -0---1-----2----3----4

    I have 5 different threads each receiving messages from those clients. It never has once gotten confused which socket to receive from because I'm using an array of threads, each corresponding to a particular client, and never a mishmash of data due do race conditions as you all propose would happen. Because each index of the thread array is working with each index of my client array. 0 is working with 0, 1 is working with 1, 2 is working with 2, etc. It is impossible for thread 0 to receive data from client 2, or thread 3 to receive data from client 1, or whatever the case may be, it has not happened nor will it ever happen. It is not continuous loop that goes for whatever frames per second, but rather a loop that loops once before getting caught, just like the loop in my main(). It will only cycle if it receives a message, all 5 of my threads. If a client disconnects, that thread will detach. And can be recreated if that particular client logs back on.

    So with main() accepting clients on one thread while at the same time 5 other threads are receiving messages from public clients, I still cant see where a race condition would occur. Because my loops only begin to cycle when something happens, similar to an event. The only thing that could ever happen is during the console window, text from one client could potentially continue on from another if they sent their messages at the same time. This is unavoidable in a console window, but controllable in a windows GUI or even a DirectX game environment. And would probably look something like this:

    2 messages being displayed simultaniously

    Hey hoHEY WHATS UP!w are you?
    

    Unless of course I can capture the messages and shove them into a string array list of messages and display them appropriately. That is so far the only problem with it, but other than that, has worked flawlessly, mostly thanks to you guys for leading me in the right direction. I know I could use Select(), Event based, or even Asyncrounous sockets, but I wanted to see how possible it would be to attempt a multithread version of a simple chat program.

    This topic is closed to new replies.

    Advertisement