Async chat server buffer issue

旧巷老猫 提交于 2019-12-04 14:18:10

Okay so, after messing with this for a long time, I have it relatively stable.

For starters, I added the following state object:

public class StateObject
{
    public Socket workSocket = null;
    public const int BufferSize = 1024;
    public byte[] buffer = new byte[BufferSize];
    public StringBuilder sb = new StringBuilder();
    public bool newConnection = true;
}

This makes it easy to keep track of each connection and gives each connection its own buffer.

The second thing I did was look for a new line in each message. I wasn't looking for this in the original code and I believe this was the root of most issues.

I also gave the responsibility of dealing with username management to the server; something that I should have done from the start obviously.

Here is the current server code:

This code is in no way perfect and I'm continuously finding new errors the more I try to break it. I'm going to keep messing with it for awhile but at the moment, it seems to work decently.

public partial class Login : Form
{
    private ChatWindow cw;
    private Socket serverSocket;
    private List<Socket> socketList;
    private byte[] buffer;
    private bool isHost;
    private bool isClosing;
    private ListBox usernames;

    public Login()
    {
        InitializeComponent();
    }

    private void Login_Load(object sender, EventArgs e)
    {
        ipLabel.Text = getLocalIP();
        cw = new ChatWindow();
        serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socketList = new List<Socket>();
        buffer = new byte[1024];
        isClosing = false;
        usernames = new ListBox();
    }

    public string getLocalIP()
    {
        return Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork).ToString();
    }

    private void joinButton_Click(object sender, EventArgs e)
    {
        try
        {
            int tryPort = 0;
            this.isHost = false;
            cw.callingForm = this;
            if (ipBox.Text == "" || portBox.Text == "" || nicknameBox.Text == "" || !int.TryParse(portBox.Text.ToString(), out tryPort))
            {
                MessageBox.Show("You must enter an IP Address, Port, and Nickname to connect to a server.", "Missing Info");
                return;
            }
            this.Hide();
            cw.Show();
            cw.connectTo(ipBox.Text, int.Parse(portBox.Text), nicknameBox.Text);
        }
        catch(Exception otheree) {
            MessageBox.Show("Error:\n\n" + otheree.ToString(),"Error connecting...");
            cw.Hide();
            this.Show();
        }
    }

    private void hostButton_Click(object sender, EventArgs e)
    {
        int tryPort = 0;
        if (portBox.Text == "" || nicknameBox.Text == "" || !int.TryParse(portBox.Text.ToString(), out tryPort)) {
            MessageBox.Show("You must enter a Port and Nickname to host a server.", "Missing Info");
            return;
        }
        startListening();
    }

    public void startListening()
    {
        try
        {
            this.isHost = true;                                                         //We're hosting this server
            cw.callingForm = this;                                                      //Give ChatForm the login form (this) [that acts as the server]
            cw.Show();                                                                  //Show ChatForm
            cw.isHost = true;                                                           //Tell ChatForm it is the host (for display purposes)
            this.Hide();                                                                //And hide the login form
            serverSocket.Bind(new IPEndPoint(IPAddress.Any, int.Parse(portBox.Text)));  //Bind to our local address
            serverSocket.Listen(1);                                                     //And start listening
            serverSocket.BeginAccept(new AsyncCallback(AcceptCallback), null);          //When someone connects, begin the async callback
            cw.connectTo("127.0.0.1", int.Parse(portBox.Text), nicknameBox.Text);       //And have ChatForm connect to the server
        }
        catch (Exception) {}
    }

    public void AcceptCallback(IAsyncResult AR)
    {
        try
        {
            StateObject state = new StateObject();
            state.workSocket = serverSocket.EndAccept(AR);
            socketList.Add(state.workSocket);
            state.workSocket.BeginReceive(state.buffer, 0, StateObject.BufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallback), state);
            serverSocket.BeginAccept(new AsyncCallback(AcceptCallback), null);
        }
        catch (Exception) {}
    }

    public void ReceiveCallback(IAsyncResult AR)
    {
        try
        {
            if (isClosing)
                return;

            StateObject state = (StateObject)AR.AsyncState;
            Socket s = state.workSocket;
            String content = "";
            int received = s.EndReceive(AR);

            if(received > 0)
                state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, received));

            content = state.sb.ToString();

            if (content.IndexOf(Environment.NewLine) > -1) //If we've received the end of the message
            {

                if (content.StartsWith("!!addlist") && state.newConnection)
                {
                    state.newConnection = false;
                    content = content.Replace("!!addlist", "");
                    string userNick = content.Trim();
                    if (isHost && userNick.StartsWith("!"))
                        userNick = userNick.Replace("!", "");
                    userNick = userNick.Trim();
                    if (userNick.StartsWith("!") || userNick == string.Empty || usernames.Items.IndexOf(userNick) > -1)
                    {
                        //Invalid Username :c get dropped
                        s.Send(Encoding.ASCII.GetBytes("Invalid Username/In Use - Sorry :("));
                        s.Shutdown(SocketShutdown.Both);
                        s.Disconnect(false);
                        s.Close();
                        socketList.Remove(s);
                        return;
                    }
                    usernames.Items.Add(userNick);
                    foreach (string name in usernames.Items)
                    {
                        if (name.IndexOf(userNick) < 0)
                        {
                            s.Send(Encoding.ASCII.GetBytes("!!addlist " + name + "\r\n"));
                            Thread.Sleep(10); //such a hack... ugh it annoys me that this works
                        }
                    }
                    foreach (Socket client in socketList)
                    {
                        try
                        {
                            if (client != s)
                                client.Send(Encoding.ASCII.GetBytes("!!addlist " + userNick + "\r\n"));
                        }
                        catch (Exception) { }
                    }
                }
                else if (content.StartsWith("!!removelist") && !state.newConnection)
                {
                    content = content.Replace("!!removelist", "");
                    string userNick = content.Trim();
                    usernames.Items.Remove(userNick);
                    foreach (Socket client in socketList)
                    {
                        try
                        {
                            if (client != s)
                                client.Send(Encoding.ASCII.GetBytes("!!removelist " + userNick + "\r\n"));
                        }
                        catch (Exception) { }
                    }
                }
                else if (state.newConnection) //if they don't give their name and try to send data, just drop.
                {
                    s.Shutdown(SocketShutdown.Both);
                    s.Disconnect(false);
                    s.Close();
                    socketList.Remove(s);
                    return;
                }
                else
                {
                    foreach (Socket client in socketList)
                    {
                        try
                        {
                            if (client != s)
                                client.Send(System.Text.Encoding.ASCII.GetBytes(content));
                        }
                        catch (Exception) { }
                    }
                }
            }
            Array.Clear(state.buffer, 0, StateObject.BufferSize);
            state.sb.Clear();
            s.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
        }
        catch (Exception) {
            socketList.Remove(((StateObject)AR.AsyncState).workSocket);
        }
    }
    public void SendCallback(IAsyncResult AR)
    {
        try
        {
            StateObject state = (StateObject)AR.AsyncState;
            state.workSocket.EndSend(AR);
        }
        catch (Exception) {}
    }
    private void Login_FormClosed(object sender, FormClosedEventArgs e)
    {
        try
        {
            this.isClosing = true;
            if (this.isHost)
            {
                foreach (Socket c in socketList)
                {
                    if (c.Connected)
                    {
                        c.Close();
                    }
                }
                serverSocket.Shutdown(SocketShutdown.Both);
                serverSocket.Close();
                serverSocket = null;
                serverSocket.Dispose();
            }
            socketList.Clear();
        }
        catch (Exception) { }
        finally
        {
            Application.Exit();
        }
    }
}
public class StateObject
{
    public Socket workSocket = null;
    public const int BufferSize = 1024;
    public byte[] buffer = new byte[BufferSize];
    public StringBuilder sb = new StringBuilder();
    public bool newConnection = true;
}

The client code (work in progress):

public partial class ChatWindow : Form
{
    private Socket clientSocket;
    private Thread chatThread;
    private string ipAddress;
    private int port;
    private bool isConnected;
    private string nickName;
    public bool isHost;
    public Login callingForm;

    private static object conLock = new object();

    public ChatWindow()
    {
        InitializeComponent();
        isConnected = false;
        isHost = false;
    }

    public string getIP() {
        return Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork).ToString();
    }

    public void displayError(string err)
    {
        output(Environment.NewLine + Environment.NewLine + err + Environment.NewLine);
    }

    public void op(string s)
    {
        try
        {
            lock (conLock)
            {
                chatBox.Text += s;
            }
        }
        catch (Exception) { }
    }

    public void connectTo(string ip, int p, string n) {
        try
        {
            this.Text = "Trying to connect to " + ip + ":" + p + "...";
            this.ipAddress = ip;
            this.port = p;
            this.nickName = n;

            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            if (!isHost)
            {
                op("Connecting to " + ipAddress + ":" + port + "...");
            }
            else
            {
                output("Listening on " + getIP() + ":" + port + "...");
            }

            clientSocket.Connect(ipAddress, port);

            isConnected = true;

            if (!isHost)
            {
                this.Text = "Connected to " + ipAddress + ":" + port + " - Nickname: " + nickName;
                output("Connected!");
            }
            else
            {
                this.Text = "Hosting on " + getIP() + ":" + port + " - Nickname: " + nickName;
            }

            chatThread = new Thread(new ThreadStart(getData));
            chatThread.Start();

            nickName = nickName.Replace(" ", "");
            nickName = nickName.Replace(":", "");
            if(nickName.StartsWith("!"))
                nickName = nickName.Replace("!", "");
            namesBox.Items.Add(nickName);

            sendRaw("!!addlist " + nickName);
        }
        catch (ThreadAbortException)
        {
            //do nothing; probably closing chat window
        }
        catch (Exception e)
        {
            if (!isConnected)
            {
                this.Hide();
                callingForm.Show();
                clearText();
                MessageBox.Show("Error:\n\n" + e.ToString(), "Error connecting to remote host");
            }
        }
    }

    public void removeNick(string n)
    {
        if (namesBox.Items.Count <= 0)
            return;
        for (int x = namesBox.Items.Count - 1; x >= 0; --x)
            if (namesBox.Items[x].ToString().Contains(n))
                namesBox.Items.RemoveAt(x);
    }

    public void clearText()
    {
        try
        {
            lock (conLock)
            {
                chatBox.Text = "";
            }
        }
        catch (Exception) { }
    }

    public void addNick(string n)
    {
        if (n.Contains(" ")) //No Spaces... such a headache
            return;
        if (n.Contains(":"))
            return;
        bool shouldAdd = true;
        n = n.Trim();
        for (int x = namesBox.Items.Count - 1; x >= 0; --x)
            if (namesBox.Items[x].ToString().Contains(n))
                shouldAdd = false;
        if (shouldAdd)
        {
            namesBox.Items.Add(n);
            output("Someone new joined the room: " + n);
            //sendRaw("!!addlist " + nickName);
        }
    }

    public void addNickNoMessage(string n)
    {
        if (n.Contains(" ")) //No Spaces... such a headache
            return;
        if (n.Contains(":"))
            return;
        bool shouldAdd = true;
        n = n.Trim();
        for (int x = namesBox.Items.Count - 1; x >= 0; --x)
            if (namesBox.Items[x].ToString().Contains(n))
                shouldAdd = false;
        if (shouldAdd)
        {
            namesBox.Items.Add(n);
            //sendRaw("!!addlist " + nickName);
        }
    }

    public void getData()
    {
        try
        {
            byte[] buf = new byte[1024];
            string message = "";
            while(isConnected)
            {
                Array.Clear(buf, 0, buf.Length);
                message = "";
                int gotData = clientSocket.Receive(buf, buf.Length, SocketFlags.None);
                if (gotData == 0)
                    throw new Exception("I swear, this was working before but isn't anymore...");
                message = Encoding.ASCII.GetString(buf);
                if (message.StartsWith("!!addlist"))
                {
                    message = message.Replace("!!addlist", "");
                    string userNick = message.Trim();
                    if(!namesBox.Items.Contains(userNick))
                    {
                        addNick(userNick);
                    }
                    continue;
                }
                else if (message.StartsWith("!!removelist"))
                {
                    message = message.Replace("!!removelist", "");
                    string userNick = message.Trim();
                    removeNick(userNick);
                    output("Someone left the room: " + userNick);
                    continue;
                }
                output(message);
            }
        }
        catch (Exception)
        {
            isConnected = false;
            output(Environment.NewLine + "Connection to the server lost.");
        }
    }

    public void output(string s)
    {
        try
        {
            lock (conLock)
            {
                chatBox.Text += s + Environment.NewLine;
            }
        }
        catch (Exception) { }
    }

    private void ChatWindow_FormClosed(object sender, FormClosedEventArgs e)
    {
        try
        {
            if(isConnected)
                sendRaw("!!removelist " + nickName);
            isConnected = false;
            clientSocket.Shutdown(SocketShutdown.Receive);
            if (chatThread.IsAlive)
                chatThread.Abort();
            callingForm.Close();
        }
        catch (Exception) { }
    }

    private void sendButton_Click(object sender, EventArgs e)
    {
        if(isConnected)
            send(sendBox.Text);
    }

    private void sendBox_KeyUp(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Enter)
        {
            if (isConnected)
            {
                if (sendBox.Text != "")
                {
                    send(sendBox.Text);
                    sendBox.SelectAll();
                    e.SuppressKeyPress = true;
                    e.Handled = true;
                }
            }
        }
    }

    private void send(string t) {
        try
        {
            byte[] data = System.Text.Encoding.ASCII.GetBytes(nickName + ": " + t + "\r\n");
            clientSocket.Send(data);
            output(nickName + ": " + t);
        }
        catch (Exception e)
        {
            displayError(e.ToString());
        }
    }

    private void sendRaw(string t)
    {
        try
        {
            byte[] data = System.Text.Encoding.ASCII.GetBytes(t + "\r\n");
            clientSocket.Send(data);
        }
        catch (Exception e)
        {
            displayError(e.ToString());
        }
    }

    private void chatBox_TextChanged(object sender, EventArgs e)
    {
        chatBox.SelectionStart = chatBox.Text.Length;
        chatBox.ScrollToCaret();
    }

    private void sendBox_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Enter)
            e.SuppressKeyPress = true;
    }
}

To do:

Add invokes, more delegates, do some more QA and find out what breaks it. Also, I believe there's still the possibility of packet loss due to the client addlist functions being in the read loop. I believe this is why the "crappy hack" using Thread.Sleep(10) in the server callback for name population is an issue.

I think it might be better to either pass the command off to another thread while continuing to read or have the client tell the server it's ready for another name.

Otherwise, there might be some data loss during name updates.

The other thing is that, as was said in the comments above, delegates should be used when updating the UI objects (chatbox and listbox). I wrote the code for these but ultimately removed it because there was no noticeable change and I wanted to keep it simple.

I do still use an object lock when outputting text to the chatbox, but there's no noticeable difference there.

The code should be added as not using delegates is potentially problematic, but I literally caught the chat box in an infinite loop of updates without issue.

I tried breaking it with telnet and was successful so I added a newConnection property to the StateObject to ensure that each client can only send "!!addlist" once.

There are, of course, other ways to abuse the server by creating a client that joins and leaves repeatedly, so ultimately I will probably end up passing the !!removelist handling to the server instead of leaving it up to the client.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!