c# - 异步聊天服务器缓冲区问题

标签 c# multithreading sockets asynchronous chat

有人可以帮我解决这个问题吗……我一整天都在挣扎。

所以我正在尝试学习异步套接字,这一直给我带来麻烦。

问题基本上是我用加入聊天室的人的名字更新列表框的方式:

Chat Window

基本上我所做的是让每个客户端在加入服务器时发送"!!addlist [昵称]"

它并不理想,因为它不检查重复项等。但现在我只想知道为什么它不起作用。 每当有人添加他们以前从未见过的名字时,他们也会发送 "!!addlist [nick]"

这样,每次有人加入时,列表都应该为每个人更新。 问题似乎是所有客户端同时开始通信,这会干扰缓冲区。

我试过为每个客户端使用一个单独的缓冲区,所以这不是问题所在。 我试过使用 lock() 但它似乎也不起作用。

基本上发生的事情是缓冲区似乎被截断了;同一缓冲区中有来自两个不同人的数据。

请告诉我我在缓冲区或客户端做错了什么:

请注意,异步套接字使用的是 Send 而不是 BeginSend。 我已经尝试了这两种方法,但它们都遇到了同样的问题……所以这可能是客户端问题?

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

    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) { /*MessageBox.Show("Error:\n\n" + e.ToString());*/ }           //Let us know if we ran into any errors
    }

    public void AcceptCallback(IAsyncResult AR)
    {
        try
        {
            Socket s = serverSocket.EndAccept(AR);                                                              //When someone connects, accept the new socket
            socketList.Add(s);                                                                                  //Add it to our list of clients
            s.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), s);  //Begin the async receive method using our buffer
            serverSocket.BeginAccept(new AsyncCallback(AcceptCallback), null);                                  //And start accepting new connections
        }
        catch (Exception) {}
    }

    public void ReceiveCallback(IAsyncResult AR)                //When a message from a client is received
    {
        try
        {
            if (isClosing)
                return;

            Socket s = (Socket)AR.AsyncState;                   //Get the socket from our IAsyncResult

            int received = s.EndReceive(AR);                    //Read the number of bytes received (*need to add locking code here*)
            byte[] dbuf = new byte[received];                   //Create a temporary buffer to store just what was received so we don't have extra data

            Array.Copy(buffer, dbuf, received);                 //Copy the received data from our buffer to our temporary buffer

            foreach (Socket client in socketList)               //For each client that is connected
            {
                try
                {
                    if (client != (Socket)AR.AsyncState)        //If this isn't the same client that just sent a message (*client handles displaying these*)
                        client.Send(dbuf);                      //Send the message to the client
                }
                catch (Exception) {  }
            }                                                  //Start receiving new data again
            s.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), s);
        }
        catch (Exception) { /*cw.output("\n\nError:\n\n" + e.ToString());*/ }
    }

    public void SendCallback(IAsyncResult AR)
    {
        try
        {
            Socket s = (Socket)AR.AsyncState;
            s.EndSend(AR);
        }
        catch (Exception) { /*cw.output("\n\nError:\n\n" + e.ToString());*/ }
    }

这是客户端:

    public void getData()
    {
        try
        {
            byte[] buf = new byte[1024];
            string message = "";
            while(isConnected)
            {
                Array.Clear(buf, 0, buf.Length);
                message = "";
                clientSocket.Receive(buf, buf.Length, SocketFlags.None);
                message = Encoding.ASCII.GetString(buf);
                if (message.StartsWith("!!addlist"))
                {
                    message = message.Replace("!!addlist", "");
                    string userNick = message.Trim();
                    if (!namesBox.Items.Contains(userNick))
                    {
                        addNick(userNick.Trim());
                    }
                    continue;
                }
                else if (message.StartsWith("!!removelist"))
                {
                    message = message.Replace("!!removelist", "");
                    string userNick = message.Trim();
                    removeNick(userNick);
                    output("Someone left the room: " + userNick);
                    continue;
                }
                else if (!namesBox.Items.Contains(message.Substring(0, message.IndexOf(":"))))
                {
                    addNick(message.Substring(0, message.IndexOf(":")).Trim()); //So they at least get added when they send a message
                }
                output(message);
            }
        }
        catch (Exception)
        {
            output("\n\nConnection to the server lost.");
            isConnected = false;
        }
    }

这是我的 addNick 函数,似乎可以解决一些问题?

    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);
        }
    }

我认为问题是某些数据包被跳过了?

可能在 Receive 之后客户端在再次调用之前有太多代码?

我是否应该为每条消息创建一个单独的线程以便接收持续运行? (哑巴)

我是否应该让我的客户端也使用异步接收和发送?

我感觉这就是答案^

通过我所做的所有检查,我设法解决了重名问题...但我似乎经常收到来自其他客户端的带有空格的消息和部分消息。

Not Receiving All The Names

最佳答案

好吧,折腾了半天,还是比较稳定的。

对于初学者,我添加了以下状态对象:

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;
}

这使得跟踪每个连接并为每个连接提供自己的缓冲区变得容易。

我做的第二件事是在每条消息中寻找一个新行。 我并没有在原始代码中寻找这个,我相信这是大多数问题的根源。

我也把处理用户名管理的责任交给了服务器;显然我从一开始就应该做的事情。

这是当前的服务器代码:

这段代码绝不是完美的,而且我越是尝试破解它,就会不断发现新的错误。我打算继续弄乱它一段时间,但目前,它似乎工作正常。

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;
}

客户端代码(进行中):

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;
    }
}

要做的事:

添加调用、更多委托(delegate)、做更多 QA 并找出问题所在。 另外,我相信由于客户端地址列表功能处于读取循环中,仍然存在丢包的可能性。我相信这就是为什么在名称填充的服务器回调中使用 Thread.Sleep(10) 的“蹩脚黑客”是一个问题。

我认为最好是在继续阅读的同时将命令传递给另一个线程,或者让客户端告诉服务器它已准备好使用另一个名称。

否则,名称更新期间可能会丢失一些数据。

另一件事是,如上面的评论所述,在更新 UI 对象(聊天框和列表框)时应该使用委托(delegate)。我为这些代码编写了代码,但最终删除了它,因为没有明显的变化,我想保持简单。

在向聊天框输出文本时,我仍然使用对象锁,但那里没有明显的区别。

代码应该被添加,因为不使用委托(delegate)可能有问题,但我确实在无限更新循环中捕获了聊天框,没有问题。

我尝试用 telnet 破解它并成功了,所以我向 StateObject 添加了一个 newConnection 属性以确保每个客户端只能发送一次“!!addlist”。

当然,还有其他方法可以通过创建一个反复加入和离开的客户端来滥用服务器,所以最终我可能最终会将 !!removelist 处理传递给服务器,而不是将其留给客户端。

关于c# - 异步聊天服务器缓冲区问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21591366/

相关文章:

c# - AggregateException.Flatten() 丢失堆栈跟踪信息。丢失的信息重要还是不重要?

c - 使 C 库线程安全

java - Spring Boot 中使用 ExecutorService 时的 "Out Of Memory"和 "GC over limit exceeded"

multithreading - 套接字服务器中每个客户端连接有新线程?

iphone - 在 iPhone 上使用套接字的广播式蓝牙?

java - Kafka SimpleConsumer 无法连接到 zookeeper : Received -1 when reading from channel

c# - 在 linq 语句中输出变量

c# - 如何在 Visual Studio Code 中为大型 C# 文件启用语法高亮显示?

c# - 使用 linq 从正则表达式匹配中获取组名

java - 将 diffrenet runnables 或消息传递到 android 中的单个预定义线程