Unable to read data correctly from .Net socket in

2019-02-21 04:45发布

问题:

I have a client and server class in C# that uses socket communication. The Server looks like this:

    public class AsyncTcpServer
    {
        private Socket _server_socket;
        private Socket _client_socket;
        private byte[] _receive_buffer;
        private byte[] _send_buffer;
        private NetworkStream _ns;


        public void Start()
        {
            try
            {
                _server_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                _server_socket.Bind(new IPEndPoint(IPAddress.Any, 17999));
                _server_socket.Listen(0);
                _server_socket.BeginAccept(new AsyncCallback(BeginAccept), null);
            }
            catch(Exception e)
            {
                Debug.Print(e.Message);
            }
        }

        private void BeginAccept(IAsyncResult ar)
        {
            try
            {         
                _client_socket = _server_socket.EndAccept(ar);

                _receive_buffer = new byte[_client_socket.ReceiveBufferSize];
                _send_buffer = new byte[_client_socket.ReceiveBufferSize]; 
                _ns = new NetworkStream(_client_socket);

                _client_socket.BeginReceive(_receive_buffer, 0, _receive_buffer.Length, SocketFlags.None, new AsyncCallback(RecieveCallback), null);
            }
            catch(Exception e)
            {
                Debug.Print(e.Message);
            }
        }

        private void RecieveCallback(IAsyncResult ar)
        {
            try 
            {
                string text = Encoding.ASCII.GetString(_receive_buffer);
                Debug.Print("Server Received: " + text);                
            }
            catch (Exception e)
            {
                Debug.Print("Unexpected exception: " + e.Message);
            }
        }

        public void Send(byte [] bytes)
        {
            try
            {
                _ns.Write(bytes, 0, bytes.Length);
            }
            catch (Exception e)
            {
                Debug.Print("Unexpected exception: " + e.Message);
            }            
        }
    }

And the client looks like this:

    public class AsyncTcpClient
    {
        private Socket _client_socket;
        private byte[] _buffer;
        private const int HEADER_SIZE = sizeof(int);

        public void Start()
        {
            _client_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);         
            _client_socket.BeginConnect(new IPEndPoint(IPAddress.Loopback, 17999), new AsyncCallback(ConnectCallback), null);
        }

        private void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                _client_socket.EndConnect(ar);                
                _buffer = new byte[_client_socket.ReceiveBufferSize];
                StartReceive();
                byte[] buffer = Encoding.ASCII.GetBytes("Connected!");
                _client_socket.Send(buffer);
            }
            catch (Exception e)
            {
                Debug.Print(e.Message);
            }
        }

        private void StartReceive(int offset = 0)
        {
            try
            {
                _client_socket.BeginReceive(_buffer, offset, _buffer.Length, SocketFlags.None, new AsyncCallback(RecieveCallback), null);
            }
            catch (Exception e)
            {
                Debug.Print(e.Message);
            }
        }

        private void RecieveCallback(IAsyncResult ar)
        {
            try
            {
                int bytes_processed = 0;
                int bytes_read = _client_socket.EndReceive(ar);

                if (bytes_read > 0)
                {
                    NetworkStream ns = new NetworkStream(_client_socket);

                    while (ns.DataAvailable && (bytes_processed < bytes_read))
                    {
                        byte[] len_bytes = new byte[HEADER_SIZE];
                        ns.Read(len_bytes, 0, HEADER_SIZE);
                        int current_chunk_size = BitConverter.ToInt32(len_bytes, 0);

                        if (current_chunk_size > 0)
                        {
                            byte[] data_buff = new byte[current_chunk_size];
                            ns.Read(data_buff, 0, current_chunk_size);
                            string s = Encoding.ASCII.GetString(data_buff);
                            bytes_processed += (HEADER_SIZE + current_chunk_size);
                            Debug.WriteLine(s);
                        }
                    }                                        
                }
                StartReceive();         
            }
            catch (Exception e)
            {
                Debug.Print(e.Message);
            }

            StartReceive();
        }
    }

They work together as follows:

  • Server starts
  • Client connects
  • Server sends custom packets to the client for its comsumption

I use the following 'data structure' to package my transmission data on the server side to send to the client:

{[DATA_LENGTH_IN_BYTES][PAYLOAD_BYTES]}

On the client side, I parse the first 4 bytes (sizeof(int)) to determine the payload length and then parse the payload itself. This works the first time I do it but after that the DataAvailable member of the NetworkStream is false and I can't parse the rest of the payload.

Why is DataAvailable false? I'm pretty new to doing this stuff in C# - am I approaching it the wrong way entirely?

Thanks in Advance!

回答1:

Here is the solution I settled on:

Server:

public class Listener
    {
        private TcpListener _listener;
        private TcpClient _client;

        public void Start()
        {
            _listener = new TcpListener(IPAddress.Loopback, 17999);
            _listener.Start();
            _listener.BeginAcceptTcpClient(new AsyncCallback(AcceptTcpClientCallback), _listener);
        }

        private void AcceptTcpClientCallback(IAsyncResult ar)
        {
            try
            {
                Debug.WriteLine("Accepted tcp client callback");
                _client = _listener.EndAcceptTcpClient(ar);               
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }            
        }

        public void Write(string data)
        {
            try
            {
                NetworkStream ns = _client.GetStream();
                byte[] buffer = Encoding.ASCII.GetBytes(data);
                ns.Write(buffer, 0, buffer.Length);
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }
        }

    }

And the client:

public class Client
    {
        private TcpClient _client;
        private byte[] _buffer;

        public void Start()
        {
            try
            {
                _client = new TcpClient();
                _client.BeginConnect(IPAddress.Loopback, 17999, new AsyncCallback(ConnectCallback), _client);
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }
        }

        private void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                NetworkStream ns = _client.GetStream();
                _buffer = new byte[_client.ReceiveBufferSize];
                ns.BeginRead(_buffer, 0, _buffer.Length, new AsyncCallback(ReadCallback), null);
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }
        }

        private void ReadCallback(IAsyncResult ar)
        {
            try
            {
                NetworkStream ns = _client.GetStream();
                int read = ns.EndRead(ar);
                string data = Encoding.ASCII.GetString(_buffer, 0, read);

                var res = data.Split(new [] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

                foreach (var r in res)
                {
                    Debug.WriteLine(r); // process messages
                }
                ns.BeginRead(_buffer, 0, _buffer.Length, ReadCallback, _client);
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }            
        }
    }

Where messages from the server to the client are formed as follows:

string message = "This is a message" + "\r\n";
_listener.Send(message);

I like this alternative for its simplicity. Its a lot shorter and (to me at least) much easier to manage.

HTH



回答2:

I think you forget the EndReceive in the RecieveCallback. (server code)

    private void RecieveCallback(IAsyncResult ar)
    {
        try 
        {
            int bytesReceived = _client_socket.EndReceive(ar); // <---
            string text = Encoding.ASCII.GetString(_receive_buffer);
            Debug.Print("Server Received: " + text);                
        }
        catch (Exception e)
        {
            Debug.Print("Unexpected exception: " + e.Message);
        }
    }

I advise to create one class that reads/writes to sockets (implementing the protocol). A Base class that handles reads/writes to Sockets, a client socket that is derived from the SocketConnection, but first will connect to an ipendpoint. A server that has a List of SocketConnection. This way you keep your client/server functionality separated of the message handling on socket. But both using the same code to receive/send messages. Here is an example:

Pseudo:

// base class that handle receive/sent packets
class SocketConnection
{

    // the reason for a Start method is that event can be bound before the Start is executed.
    void Start(Socket socket)
    {
        StartReceive();
    }

    void StartReceive()
    {
        socket.BeginReceive(...);
    }

    void EndReceive()
    {
        socket.EndReceive(...);
        // handle received message.
        // call the ondata event or something
        StartReceive();
    }
}

class ClientSocket : SocketConnection
{
    void Connect()
    {
        Socket socket = new Socket(...);
        socket.Connect(..);

        // start receiving from the client socket.
        base.Start(socket);
    }
}

class Server
{
    List<SocketConnection> _clients;

    void Start()
    {
        // Create server socket + listening etc..

        StartAccept();
    }

    void StartAccept()
    {
        serverSocket.BeginAccept(...);
    }

    void EndAccept()
    {
        Socket serverClientSocket = EndAccept(...);

        // create a base socket handler.....
        SocketConnection clientSocket = new SocketConnection();

        _clients.Add(clientSocket);
        // bind the ondata event of the client and pass it to the clientondata event of the server.
        // Start receiving from the socket.
        clientSocket.Start(serverClientSocket);

        // accept new clients.
        StartAccept();
    }
}

UPDATE:

"Regarding how to handle buffers, what would you suggest as the best way to not miss data in separate packets?"

I would send a size first:

// sending part.
string message = "This is a message";
byte[] buffer = Encoding.ASCII.GetBytes(message);

_client_socket.Send(BitConverter.GetBytes(buffer.Length)); // sends a int as 4 bytes.
_client_socket.Send(data);



// receiving part.
// try to receive at least 4 bytes. (no more/ no less)

int length = BitConverter.ToInt32(buffer, 0); 

// try to receive data with `length` size,  (no more/ no less)

This will separate different packets.


Asynchronous example:


You still need to add some exception handling code.

public static class SocketReader
{
    public static void ReadFromSocket(Socket socket, int count, Action<byte[]> endRead)
    {
        // read from socket, construct a new buffer.
        DoReadFromSocket(socket, 0, count, new byte[count], endRead);
    }

    public static void ReadFromSocket(Socket socket, int count, ref byte[] buffer, Action<byte[]> endRead)
    {
        // if you do have a buffer available, you can pass that one. (this way you do not construct new buffers for receiving.
        // the ref is because if the buffer is too small, it will return the newly created buffer.

        // if the buffer is too small, create a new one.
        if (buffer.Length < count)
            buffer = new byte[count];

        DoReadFromSocket(socket, 0, count, buffer, endRead);
    }

    // This method will continues read until count bytes are read. (or socket is closed)
    private static void DoReadFromSocket(Socket socket, int bytesRead, int count, byte[] buffer, Action<byte[]> endRead)
    {
        // Start a BeginReceive.
        socket.BeginReceive(buffer, bytesRead, count - bytesRead, SocketFlags.None, (result) =>
        {
            // Get the bytes read.
            int read = socket.EndReceive(result);

            // if zero bytes received, the socket isn't available anymore.
            if (read == 0)
            {
                endRead(new byte[0]);
                return;
            }

            // increase the bytesRead, (index point for the buffer)
            bytesRead += read;

            // if all bytes are read, call the endRead with the buffer.
            if (bytesRead == count)
                endRead(buffer);
            else
                // if not all bytes received, start another BeginReceive.
                DoReadFromSocket(socket, bytesRead, count, buffer, endRead);

        }, null);
    }

}

Example how to use it:

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


// read header. (length of data) (4 bytes)
SocketReader.ReadFromSocket(socket, 4, (headerBuffer) =>
    {
        if (headerBuffer.Length == 0)
        {
            // disconnected;
            return;
        }


        int length = BitConverter.ToInt32(headerBuffer, 0);

        // read bytes specified in length.
        SocketReader.ReadFromSocket(socket, length, (dataBuffer) =>
            {
                if (dataBuffer.Length == 0)
                {
                    // disconnected;
                    return;
                }

                // if you want this in a stream, you can do: This way the stream is readonly and only wraps arround the bytearray.
                using (MemoryStream stream = new MemoryStream(dataBuffer, 0, length))
                using (StreamReader reader = new StreamReader(stream))
                    while (!reader.EndOfStream)
                        Debug.WriteLine(reader.ReadLine());

            });

    });