【问题标题】:receiving message using socket programming in c# doesn't work在 c# 中使用套接字编程接收消息不起作用
【发布时间】:2015-04-11 00:48:05
【问题描述】:

我正在尝试使用套接字编程编写程序。这个程序只是从客户端向服务器发送一条消息。

客户端代码是这样的:

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

            TcpClient client = new TcpClient();
            IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
            client.Connect(serverEndPoint);
            NetworkStream clientStream = client.GetStream();
            ASCIIEncoding encoder = new ASCIIEncoding();
            byte[] buffer = encoder.GetBytes("Hello Server!");
            clientStream.Write(buffer, 0, buffer.Length);
            clientStream.Flush();
        }
    }

如您所见,客户端只是向服务器发送 Hello server。服务端代码是这样的:

 class Server
    {
        private TcpListener tcpListener;
        private Thread listenThread;

    public Server()
    {
        this.tcpListener = new TcpListener(IPAddress.Any, 3000);
        this.listenThread = new Thread(new ThreadStart(ListenForClients));
        this.listenThread.Start();
    }
    private void ListenForClients()
    {
        this.tcpListener.Start();

        while (true)
        {
            //blocks until a client has connected to the server
            TcpClient client = this.tcpListener.AcceptTcpClient();

            //create a thread to handle communication 
            //with connected client
            Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
            clientThread.Start(client);
        }
    }
    private void HandleClientComm(object client)
    {
        TcpClient tcpClient = (TcpClient)client;
        NetworkStream clientStream = tcpClient.GetStream();

        byte[] message = new byte[4096];
        int bytesRead;

        while (true)
        {
            bytesRead = 0;

            try
            {
                //blocks until a client sends a message
                bytesRead = clientStream.Read(message, 0, 4096);
            }
            catch
            {
                //a socket error has occured
                break;
            }

            if (bytesRead == 0)
            {
                //the client has disconnected from the server
                break;
            }

            //message has successfully been received
            ASCIIEncoding encoder = new ASCIIEncoding();
            System.Diagnostics.Debug.WriteLine(encoder.GetString(message, 0, bytesRead));
            Console.ReadLine();
        }

        tcpClient.Close();
    }

}

在主类中,我像这样调用 SERVER 类

 class Program
    {

         Server obj=new Server();
        while (true)
        {

        }
    }

正如您在服务器代码中看到的那样,我尝试显示该消息,但它不起作用。为什么它不起作用?

最好的问候

【问题讨论】:

  • 你启动服务器程序。主要方法被调用。创建一个服务器对象。 Main 方法会立即退出,从而关闭整个程序(包括服务器线程)。解决方案:防止您的 Main 方法在 Server 线程运行时退出。
  • 那我应该改变什么?
  • 这个问题到底是什么意思?对于初学者,您可以在创建 Server 对象后立即在 main 方法中添加一个无限循环 for(;;){}...
  • 您也可以添加Console.ReadKey();,这样当您在控制台窗口中键入内容时您的程序就会关闭...
  • @Bun,不,他不能没有太多的痛苦。看看他在服务器中的 HandleClientComm 方法,他也从控制台读取...

标签: c# sockets


【解决方案1】:

问题中提供的代码存在许多问题,我决定在我的回答的几个部分中解决这些问题。 值得注意的是,代码来自大约六年前发布的blog post(一种简短的教程),并且已经展示了此答案中解决的一些问题。


1.为什么服务器程序立即退出?

服务器程序立即退出的原因比较简单:当Main方法退出时,程序的整个进程都被关闭了,包括任何属于这个进程的线程(比如启动的线程)在 Server 类的构造函数中)。要解决这个问题,需要防止 Main 方法退出。

一种常见的方法是在 Main 方法的最后一行添加Console.ReadKey()Console.ReadLine(),这不会让服务器程序退出,直到用户提供一些键盘输入。 然而,对于这里给出的特定代码,这种方法不是那么容易解决的,因为客户端连接处理程序方法 (HandleClientComm) 也会读取控制台键盘输入(这本身可能会成为一个问题,请参阅第 5 节下面)。

由于我不想从详细说明键盘输入开始我的回答,我建议一个不同的并且公认更原始的解决方案:在 Main 方法的末尾添加一个无限循环(这也已包含在已编辑的问题中):

static void Main(string[] args)
{
    Server srv = new Server();
    for (;;) {}
}

由于服务器的教程代码本质上是一个控制台应用程序,它仍然可以通过按CTRL+C来终止。


2.服务器似乎仍然没有做任何事情。为什么?

.NET 框架的类和方法中发生的大多数(如果不是全部)错误或问题是通过 .NET 的异常机制报告的。

这里的教程代码在处理客户端连接的服务器方法中犯了一个严重错误:

try
{
    //blocks until a client sends a message
    bytesRead = clientStream.Read(message, 0, 4096);
}
catch
{
    //a socket error has occured
    break;
}

try-catch 块在做什么?好吧,它唯一能做的就是捕捉任何异常——这可以回答问题为什么?—— 然后默默地毫不客气地丢弃这些有用的信息。 呃……!

当然,在客户端连接处理程序线程中捕获异常是有意义的。否则,未捕获的异常不仅会导致失败的客户端连接处理程序线程关闭,还会导致整个服务器控制台应用程序关闭。但需要以有意义的方式处理这些异常,以免丢失有助于解决问题的信息。

为了在我们的简单教程代码中提供有价值的异常信息,catch 块会将异常信息输出到控制台(并且还负责输出任何可能的“内部异常”)。

此外,using 语句被用来确保 NetworkStreamTcpClient 对象都被正确关闭/即使在异常导致客户端连接线程退出的情况下也是如此。

private void HandleClientComm(object client)
{
    using ( TcpClient tcpClient = (TcpClient) client )
    {
        EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;

        try
        {
            using (NetworkStream clientStream = tcpClient.GetStream() )
            {
                byte[] message = new byte[4096];

                for (;;)
                {
                    //blocks until a client sends a message
                    int bytesRead = clientStream.Read(message, 0, 4096);

                    if (bytesRead == 0)
                    {
                        //the client has disconnected from the server
                        Console.WriteLine("Client at IP address {0} closed connection.", remoteEndPoint);
                        break;
                    }

                    //message has successfully been received
                    Console.WriteLine(Encoding.ASCII.GetString(message, 0, bytesRead));

                    // Console.ReadLine() has been removed.
                    // See last section of the answer about why
                    // Console.ReadLine() was of little use here...
                }
            }
        }
        catch (Exception ex)
        {
            // Output exception information
            string formatString = "Client IP address {2}, {0}: {1}";
            do
            {
                Console.WriteLine(formatString, ex.GetType(), ex.Message, remoteEndPoint);
                ex = ex.InnerException;
                formatString = "\tInner {0}: {1}";
            }
            while (ex != null);
        }
    }
}

(您可能会注意到代码在 remoteEndPoint 中记住了客户端的(公共)IP 地址。原因是 tcpClient 属性中的 Socket 对象。 Client 将在 NetworkStream 被关闭时被释放 - 当相应 using 语句的范围被离开时会发生这种情况,这反过来又会使它变得不可能之后在 catch 块中访问 tcpClient.Client.RemoteEndPoint。)

通过让服务端在控制台输出异常信息,当客户端尝试发送消息时,我们将能够从服务端看到以下信息:

无法从传输连接读取数据:现有连接被远程主机强行关闭。

这是一个非常强烈的迹象,表明客户端出现问题或某些网络设备出现了一些奇怪的问题。碰巧的是,O/P 使用 IP 地址“127.0.0.1”在同一台计算机上运行客户端和服务器软件,这使得担心网络设备故障成为一个没有实际意义的论点,而是指向客户端软件的问题。


3.客户怎么了?

运行客户端不会抛出任何异常。查看源代码,可能会错误地认为代码似乎大部分都没问题:与服务器建立连接,字节缓冲区填充消息并写入 NetworkStream,然后 em>NetworkStream 被刷新。但是,NetworkStream 并未关闭——这肯定与问题有关。 但是即使 NetworkStream 没有被明确关闭,刷新流应该已经将消息发送到服务器,或者......?

上一段包含两个错误的假设。第一个错误假设与 NetworkStream 未正确关闭的问题有关。客户端代码中发生的情况是,在将消息写入 NetworkStream 之后,客户端程序直接退出。

当客户端程序退出时,会向服务器发送一个“连接终止”信号(简而言之)。如果在那个时间点,消息仍然停留在发送方的某个 TCP/IP 相关缓冲区中,则该缓冲区将被简单地丢弃并且不再发送消息。但即使消息已被服务器端 TCP/IP 堆栈接收并保存在与 TCP/IP 相关的接收缓冲区中,紧随其后的“连接终止”信号或多或少仍会使此信号无效在 TCP/IP 堆栈确认收到此消息之前接收缓冲区,因此 NetworkStream.Read() 因上述错误而失败。

另一个错误的假设(并且很容易被没有在 .NET 中进行常规网络相关编程的人忽略)是代码如何尝试利用 NetworkStream.Flush() 来尝试强制传输消息。 我们来看看MSDN documentation of NetworkStream.Flush()的“备注”部分:

Flush 方法实现了 Stream.Flush 方法;但是,由于 NetworkStream 没有缓冲,对网络流没有影响

是的,您没看错:NetworkStream.Flush() 确实... 没什么!
(...显然,因为 NetworkStream 不缓冲任何数据)

所以,为了让客户端程序正常工作,需要做的就是正确关闭客户端连接(这也确保了在连接断开之前服务器正在发送和接收消息)。正如上面的服务器代码中已经展示的那样,我们将使用 using 语句,该语句将负责正确关闭和处置 NetworkStream任何情况下的 TcpClient 对象。此外,正在删除 NetworkStream.Flush() 的误导性和无用调用:

static void Main(string[] args)
{
    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
    using ( TcpClient client = new TcpClient() )
    {
        client.Connect(serverEndPoint);

        using ( NetworkStream clientStream = client.GetStream() )
        {
            byte[] buffer = Encoding.ASCII.GetBytes("Hello Server!");
            clientStream.Write(buffer, 0, buffer.Length);
        }
    }
}


4.关于从/向 NetworkStream 读取和写入消息的建议

始终应牢记的是,NetworkStream.Read(...) 不保证一次读取完整的消息。 根据消息的大小,NetworkStream.Read(...) 可能只读取消息的片段。其原因与客户端软件如何发送数据以及 TCP/IP 如何管理通过网络的数据传输有关。 (当然,对于像 “Hello world!”这样的短消息,您不太可能不会遇到这种情况。但是,根据您的服务器和客户端操作系统以及使用的 NIC + 驱动程序,您如果消息变得大于 500 字节,则可能会开始观察消息被碎片化。)

因此,为了使服务器更健壮且不易出错,应更改服务器代码以适应零碎的消息。

如果您坚持使用 ASCII 编码,您可能会选择 Alexander Brevig 的回答中所示的方法——只需将接收到的带有 ASCII 字节的消息片段写入 StringBuilder。但是,这种方法只能可靠地工作,因为任何 ASCII 字符都由单个字节表示。

一旦您使用另一种可以将单个字符编码为多个字节的编码(例如,任何 UTF 编码),这种方法将不再可靠地工作。字节数据的可能碎片可能会拆分多字节字符的字节序列,使得一个 NetworkStream.Read 调用仅读取此类字符的第一个字节,并且仅读取后续调用NetworkStream.Read 将获取此多字节字符的剩余字节。如果将单独解码每个消息片段,这将扰乱字符解码。因此,在进行任何文本解码之前,将 complete 消息存储在单字节缓冲区(数组)中通常更安全。

读取不同长度的消息仍然存在一个问题。服务器如何知道完整的消息何时发送?现在,在此处给出的教程中,客户端只发送一条消息然后断开连接。因此,服务器仅通过客户端断开连接就知道已收到完整的消息。

但是如果您的客户端想要发送多条消息,或者如果服务器需要向客户端发送响应怎么办?在这两种情况下,客户端都不能在发送第一条消息后简单地断开连接。那么,服务器如何知道消息何时发送完毕?

一个非常简单可靠的方法是让客户端在消息前添加一个短整数值(2字节)或整数值(4字节)来指定消息长度(以字节为单位)。因此,在接收到这 2 个字节(或 4 个字节)后,服务器将知道还需要读取多少字节才能获得完整的消息。

现在,您不需要自己实现这样的机制。好消息是 .NET 已经为类提供了为您完成所有这些工作的方法:BinaryWriterBinaryReader,这使得发送由不同数据类型组成的消息几乎与众所周知的漫步一样简单和愉快公园。

客户端使用BinaryWriter.Write(string)

static void Main(string[] args)
{
    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
    using ( TcpClient client = new TcpClient() )
    {
        client.Connect(serverEndPoint);

        using ( BinaryWriter writer = new BinaryWriter(client.GetStream(), Encoding.ASCII) )
        {
            writer.Write("Hello Server!");
        }
    }
}

服务器的客户端连接处理程序使用BinaryReader.ReadString()

private void HandleClientComm(object client)
{
    using ( TcpClient tcpClient = (TcpClient) client )
    {
        EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;

        try
        {
            using ( BinaryReader reader = new BinaryReader(tcpClient.GetStream(), Encoding.ASCII) )
            {
                for (;;)
                {
                    string message = reader.ReadString();
                    Console.WriteLine(message);
                }
            }
        }
        catch (EndOfStreamException ex)
        {
            Console.WriteLine("Client at IP address {0} closed the connection.", remoteEndPoint);
        }
        catch (Exception ex)
        {
            string formatString = "Client IP address {2}, {0}: {1}";
            do
            {
                Console.WriteLine(formatString, ex.GetType(), ex.Message, remoteEndPoint);
                ex = ex.InnerException;
                formatString = "\tInner {0}: {1}";
            }
            while (ex != null);
        }
    }
}

您可能会注意到 EndOfStreamException 异常的特殊处理。 BinaryReader 使用此异常来指示已到达流的末尾;即,连接已被客户端关闭。 不是像任何其他异常一样将其打印出来(因此可能会被误解为错误 - 它确实可能在不同的应用场景中),而是在控制台上打印出一条特定消息以使连接被关闭的事实非常清除。

(旁注:如果您打算让您的客户端软件连接并与第 3 方服务器软件交换数据,BinaryWriter.Write(string) 可能是也可能不是一个可行的选项,因为它使用ULEB128 对字符串的字节长度进行编码。 在 BinaryWriter.Write(string) 不可行的情况下,您很可能仍然可以充分利用 BinaryWriter.Write(short)/BinaryWriter 的组合。 Write(int)BinaryWriter.Write(byte[]) 一起使用 messageLength 值添加消息字节数据。)


5.控制台键盘输入

问题代码中的客户端连接处理程序方法 HandleClientComm 在收到来自客户端的消息后等待键盘输入,然后它将继续等待并读取下一条消息。 这是毫无意义的,因为 HandleClientComm 可以继续等待下一条消息而无需显式键盘输入。

但也许您的意图是使用控制台键盘输入作为将发送回客户端的响应——我不知道。只要你只是玩弄一个简单的客户端,我想这种方法就可以了。

但是,一旦您有两个或多个客户端同时访问服务器,即使是一些简单的测试/玩具场景也可能需要您采取措施确保多个客户端连接处理程序线程不会交叉其控制台输出和访问以易于理解的方式管理这些线程的控制台键盘输入。也就是说,这真的取决于你想详细做什么——这很可能也只是一个非问题......

【讨论】:

  • “一个字节正好等于一个 ASCII 字符”:实际上只有少数字符集编码适用于所有可能的字节值。 CP437 就是其中之一。 ASCII 和 Windows-1252 不是。
  • @TomBlodget,同意 - 我的措辞有点落后和错误。抱歉 :) 编辑了我的答案以使预期的论点更清楚:每个 ASCII 字符都由一个字节表示。
  • @eFloh,NetworkStream 使用 Socket 类发送数据。 Socket 类使用 Winsock2 的 send 函数。看看它的online MSDN documentation。但是,在线文档有些模棱两可。尤其要证明其备注部分中的一句话:“如果传输系统中没有可用的缓冲区空间来保存要传输的数据,除非套接字已置于非阻塞模式,否则发送将阻塞[ ...]"(续)
  • @eFloh,这句话强烈表明文档理解短语“发送数据”,只是将数据移交给 TCP/IP 堆栈。此外,请阅读一位名为 Jeris 的用户的评论; i quote: "有可能是数据没有发送。例如,发送者有64k的发送缓冲区,而接收者有32k的接收缓冲区。发送者发送32k数据后,有可能收到一个零接收窗口。此时send调用成功,而数据只在send buffer中,还没有发送出去。”
  • @eFloh,哎呀,我忘了你问题的第一部分。当进程退出时,不再有对象被 GC(它们只是与进程的堆一起消失)。因此,在问题的原始代码中,不会调用 Socket.Dispose()Socket.Dispose() 将确保发生正确的 TCP 连接关闭对于仍然打开的连接)。然后,当进程退出时,内部本机 Winsock2 套接字将被销毁,无需进行正常的 TCP 连接关闭握手,而仅发送“RESET”数据包(我猜;我目前无法验证...)
【解决方案2】:

尝试更改此设置

try
{
    //blocks until a client sends a message
    bytesRead = clientStream.Read(message, 0, 4096);
}

到这里

StringBuilder myCompleteMessage = new StringBuilder();
try {
    do{
        bytesRead = clientStream.Read(message, 0, message.Length);
        myCompleteMessage.AppendFormat("{0}", Encoding.ASCII.GetString(message, 0, bytesRead );                             
    } while(clientStream.DataAvailable);
}
///catch and such....
System.Diagnostics.Debug.WriteLine(myCompleteMessage);

【讨论】:

  • 是的,在排除了过早的 Main-exit 和 TcpClient 设置期间可能引发的异常之后,或多或少是这样。
  • @EA,我认为是时候启动调试器并仔细查看您的服务器在做什么——检查是否发生任何可能导致服务器方法出错的异常。如果方法真的像您期望的那样处理数据和值,还要检查调试器...
  • @EA,请注意try {...} catch { break; }(如在您的 HandleClientComm 方法中)非常非常糟糕,因为它会对您隐藏任何异常,从而使您无法诊断和修复任何问题。 .
  • @elgonzo 好的,我再做一次
  • @elgonzo 我现在又做了一次,你知道我的程序只调用服务器构造函数并保持在 while 循环中!!!没有调用服务器类中的任何其他函数
猜你喜欢
  • 1970-01-01
  • 2019-02-26
  • 1970-01-01
  • 1970-01-01
  • 2017-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多