在c#中使用套接字编程接收消息不起作用

Ehs*_*bar 1 c# sockets

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

客户端代码是这样的:

  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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

您可以看到客户端只是向服务器发送Hello服务器.服务器代码是这样的:

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

}
Run Code Online (Sandbox Code Playgroud)

在主类中,我调用SERVER类,如下所示:

 class Program
    {

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

        }
    }
Run Code Online (Sandbox Code Playgroud)

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

最好的祝福

小智 12

问题中提供的代码有很多问题,我决定在我的答案的几个部分中提出.值得注意的是,该代码来自六年前发布的博客文章(一种简短的教程),并且已经展示了本答案中提到的一些问题.


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

服务器程序立即退出的原因很简单:当Main方法退出时,程序的整个过程都会关闭,包括属于此进程的任何线程(例如在Server类的构造函数中启动的线程).要解决此问题,需要阻止Main方法退出.

一种常见的方法是在Main方法中添加Console.ReadKey()Console.ReadLine()作为最后一行,在用户提供某些键盘输入之前不会让服务器程序退出.但是,对于此处给出的特定代码,此方法不会是一个如此简单的修复,因为客户端连接处理程序方法(HandleClientComm)也读取控制台键盘输入(它本身可能会成为一个问题,请参阅下面的第5节).

由于我不想通过详细说明键盘输入来开始我的答案,我建议一个不同的,可以肯定的更原始的解决方案:在Main方法的末尾添加一个无限循环(它也包含在已编辑的问题中):

static void Main(string[] args)
{
    Server srv = new Server();
    for (;;) {}
}
Run Code Online (Sandbox Code Playgroud)

由于服务器的教程代码本质上是一个控制台应用程序,因此仍然可以通过按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;
}
Run Code Online (Sandbox Code Playgroud)

那个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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

(您可能会注意到代码正在记住remoteEndPoint中客户端的(公共)IP地址.原因是当关闭NetworkStream时,属性tcpClient.Client中的Socket对象将被释放 - 当相应的使用范围发生时声明被遗忘,这反过来将导致无法访问tcpClient.Client.RemoteEndPoint捕捉块之后.)

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

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

这是一个非常强烈的迹象表明客户端出现问题或者某些网络设备存在一些奇怪的问题.事实上,O/P使用IP地址"127.0.0.1"在同一台计算机上运行客户端和服务器软件,这使得担心网络设备故障成为一个有争议的争论,而是指出客户端软件存在问题.


3.客户有什么问题?

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

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

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

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

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

是的,你确实读过这个:NetworkStream.Flush()完全正确...... 没什么!
(......显然,因为NetworkStream不缓冲任何数据)

因此,为了使客户端程序正常工作,所有需要做的就是正确关闭客户端连接(这也确保在连接断开之前服务器发送和接收消息).正如上面的服务器代码中已经证明的那样,我们将使用using语句,它将负责在任何情况下正确关闭和处理NetworkStreamTcpClient对象.此外,正在删除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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


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

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

因此,为了使服务器更健壮且更不容易出错,应更改服务器代码以满足碎片消息的需要.

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

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

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

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

一种非常简单可靠的方法是让客户端在消息前面加上一个短整数值(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!");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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


5.控制台键盘输入

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

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

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