C# 线程手册 第七章 网络和线程 创建一个客户端

现在你已经对.NET 中的网络编程有了一个初步的了解,现在我们来实际讨论下本章将要实现的示例程序。这个例子的目的是通过创建一个网络应用程序来让你熟悉线程的使用。这个程序实际上由两个小的Windows 窗体程序组成,一个作为服务端而另外一个作为客户端。我们将使用Visual Studio.NET 来设计实现这些程序。

设计目标

我们想创建两个交互程序。第一个是用来从一个数据库表中寻找股票交易数据然后将数据异步地返回给客户端的多线程/多用户股票交易服务程序。第二个是一个通过股票交易号来从服务端查询股票信息的客户端。所有这些都异步执行,客户端用户接口在服务端对请求作出响应时不会卡住。

在.NET Framework 中有很多方法可以为我们处理异步操作;通过这些方法我们从手动创建并管理线程的工作中解放出来。

下面第一个列表列举出我们创建应用程序所需要的基本信息:

  1. 将会有两个独立存在的程序可以在网络上互相通信

  2. 当向服务端查询股票交易时,客户端的用户接口不会由于网络连接问题而导致卡住或延迟

  3. 服务端应该有能力同时处理多个客户端连接和请求,能够以异步方式和客户端通信

  4. 网络设置必须与应用层隔离开来并且是易于修改的

为了帮助我们了解程序里典型的用户交互逻辑,我们来看一下UML 图形。

图 1

到目前为止,我们从一个很高的起点讨论了一下程序的基本设计指导。如果你和大多数程序员一样,你可能已经等不及要看看代码了。事不宜迟,我们现在就来实际创建这两个程序并同时检查一下我们学过的概念。

创建程序

正如之前提到的,本章的示例程序包含两个独立部分:一个客户端和一个服务端。这两个程序将通过一个特殊的TCP/IP 端口进行通信,可以通过应用程序配置文件修改端口。好了,我们现在就来开始实现程序部分。

下面的类图包含了客户端程序的所有代码:

StockClient 应用程序包含所有客户端程序的代码,比如私有成员变量和方法。我们首先创建一个StockClient Windows 窗体程序。在默认窗体上再创建三个控件;一个名为txtStock 的文本框,一个名为btnGetQuote 的按钮和一个名为lstQuotes 的列表视图控件。然后为这个程序添加一个菜单页,添加包括文件,连接和退出菜单项。最后,要保证除了菜单项意外的所有控件的属性都设置为False; 直到用户连接到服务端才启用这些控件。

代码如下:

首先是在StockClient 程序中要用到的一些私有成员变量:

private int mPort;
private string mHostName;
private const int mPacketSize = 1024;
private byte[] mReceivedData = new byte[mPacketSize];
private TcpClient mMyClient;
private StringBuilder mStrBuilder = new StringBuilder();

稍后我们再看这些变量,现在我们来修改一下ListView 控件以便于可以保留我们输入的所有股票信息。我们需要它包含六列:Symbol, Price, Change, Bid, Ask 和 Volume. 现在创建一个InitializeStockWindow() 来把这些列添加到ListView 控件中:

private void InitializeStockWindow()
{
    lstQuotes.View = View.Details;
    lstQuotes.Columns.Add("Symbol", 60, HorizontalAlignment.Left);
    lstQuotes.Columns.Add("Price", 50, HorizontalAlignment.Left);
    lstQuotes.Columns.Add("Change", 60, HorizontalAlignment.Left);
    lstQuotes.Columns.Add("Bid", 50, HorizontalAlignment.Left);
    lstQuotes.Columns.Add("Ask", 50, HorizontalAlignment.Left);
    lstQuotes.Columns.Add("Volume", 170, HorizontalAlignment.Left);
}

下面代码用来实现关闭事件。它简单地启用文件菜单的连接选项并通过一个消息提示框提示消息:

public delegate void DisconnectedHandler(object sender);
public event DisconnectedHandler Disconnected;

private void OnDisconnected(object sender)
{
    mnuConnect.Enabled = true;
    MessageBox.Show("The connection was lost!",
        "Disconnected", MessageBoxButtons.OK, MessageBoxIcon.Error);
    EnableComponents(false);
}

提到关闭事件,就不得不说一下连接事件,下面看一下连接方法:

private void mnuConnect_Click(object sender, EventArgs e)
{
    IDictionary hostSettings;
    try
    {
        hostSettings = (IDictionary)ConfigurationManager.GetSection("HostInfo");
        mHostName = (string)hostSettings["hostname"];
        mPort = (int)hostSettings["port"];

        mMyClient = new TcpClient(mHostName, mPort);
        mMyClient.GetStream().BeginRead(
            mReceivedData, 0, mPacketSize,
            new AsyncCallback(ReceiveStream), null);
        EnableComponents(true);
        InitializeStockWindow();
        mnuConnect.Enabled = false;
        Disconnected += new DisconnectedHandler(OnDisconnected);
    }
    catch (System.Exception ex)
    {
        MessageBox.Show("Error: Unable to establish a connection!",
            "Disconnected", MessageBoxButtons.OK, MessageBoxIcon.Error);
        mMyClient.Close();
    }
}

如果你正确地实现了上面所有步骤而且有一个服务器在指定主机名和端口处监听,那么就会创建一个新连接。为了保持住连接,我们必须生成一个后台线程来异步地从服务端获取数据并将数据显示给用户。这部分开始变得有趣了。

如之前提到的,我们需要我们程序的接收方法是异步的。这是客户端能够工作且不会延迟任何用户请求的唯一方式。让客户端程序在等待数据从服务端返回过程中卡死是无法让人接受的。由于有了.NET Framework, 解决方案相对来说简单并易于实现了。我们首先定义一个TcpClient 的NetworkStream 对象。我们可以调用TcpClinet.GetStream() 方法来返回NetworkStream 对象,并通过它来发送和接收数据。NetworkStream 继承自Stream 类,提供了一系列方法用来进行网络通信。一旦我们有了一个底层数据流,我们可以用它来在网络上发送和接收数据。与其兄弟类FileStream 和 TextStream 类似,NetworkStream 类暴露读写方法用来以同步方式发送和接收数据。BeginRead() 和 BeginWrite() 是这些方法的异步版本。实际上,.NET Framework 中的大部分以Begin开始的方法,比如BeginRead() 和 BeginGetResponse(),当作为委托时不需要程序员提供任何额外代码就能实现异步调用。因此,没有必要生成新线程,由于有一个后台线程来处理数据读取,程序的主线程仍然可以相应UI 请求。让我们来看一下BeginRead() 方法签名:

public override IAsyncResult BeginRead(
    byte[] buffer,
    int offset,
    int size,
    AsyncCallback callback,
    object state);

下表解释了这个方法的每个参数。

在我们继续之前,让我们来了解下异步调用,因为这是一个非常重要的概念。正如之前提到的那样,同步操作的问题在于直到调用结束之后工作线程才能继续工作。异步调用可以运行在一个后台线程中并允许调用线程继续正常执行。.NET 允许使用委托对任何类/方法进行异步调用。然而,特定的类,比如NetworkStream中的BeginRead() 方法已经包含内建的异步能力。委托作为需要进行异步调用的占位符。委托事实上是一个类型安全的函数指针。

正如我们看到的,BeginRead() 方法需要一个字节数组而不是字符串或者文本流,因此处理起来会稍微有点复杂。我们已经定义了一个名为ReceiveData的变量和另外一个整型变量PacketSize. 现在我们需要传递实际接收数据的方法名-当数据到达以后这个方法将要被委托调用。记住这个方法将要运行在一个后台线程中,所以如果我们希望和UI交互的话那就得小心了。我们通过一行代码生成一个后台线程来接收通过网络从服务端发过来的数据:

mMyClient.GetStream().BeginRead(
    mReceivedData, 0, mPacketSize,
    new AsyncCallback(ReceiveStream), null);

我们创建了一个ReceivedStream() 方法来处理接收到的数据:

private void ReceiveStream(IAsyncResult ar)
{
    int bytesCount;
    try
    {
        bytesCount = mMyClient.GetStream().EndRead(ar);
        if (bytesCount < 1)
        {
            Disconnected(this);
            return;
        }
        MesssageAssembler(mReceivedData, 0, bytesCount);
        mMyClient.GetStream().BeginRead(mReceivedData, 0,
            mPacketSize, new AsyncCallback(ReceiveStream), null);
    }
    catch (System.Exception ex)
    {
        //Display error message
        object[] paramObjs = {("An error has occurred " + ex.ToString()).ToString()};
        Invoke(new InvokeDisplay(DisplayData), paramObjs);
    }
}

首先,我们需要检查下缓存中是否有数据。通常情况下应该一直有数据。你可以把网络连接想象成一段连续的脉冲;只要客户端连着服务端,在到来的数据包中就会有数据,不管多小。我们使用Stream 对象的EndRead() 方法来检查当前字节数组的大小。我们给EndRead() 方法传递一个IAsyncResult 的实例。GetStream().BeginRead() 方法初始化一个异步调用来调用ReceiveStream()方法,为了保证异步调用完成编译器还会在后台做一些额外工作。ReceiveStream() 方法在一个线程池线程上执行。如果委托方法ReceiveStream() 抛出一个异常,那么新创建的异步线程就会被终止,并在调用线程中再产生一个异常。下表深入描述了这种情况:

如果EndRead() 方法返回的数字小于1,我们就会知道连接已经丢失,接下来可以引发Disconnected 事件来进行适当的工作处理这种情况。然而,如果接收到字节数目大于0,我们可以开始接受数据。在这个时候,我们需要一个帮助类类帮助我们把从服务端接收到的数据构造成一个字符串。

事实上,.NET 中你可以按照与BeginRead()方法同样行为来异步调用几乎任何方法。你仅需要定义一个委托并使用BeginInvoke()和EndInvoke() 来调用委托。后者在定义委托时会自动添加。异步架构复杂的细节已经抽象出来,你不需要担心后台线程和同步问题。需要注意的是在VS.NET IDE 的智能感知中不能找到这两个方法。它们仅在运行时才会被添加。

好的,我们再来看看MessageAssembler() 方法。由于BeginRead() 方法的异步特性,我们实际上并不知道何时以及何种数据将会从服务端到达。数据可能一次全部接收到,或者可能以几百个小数据块形式到达,每块数据仅是一到两个字母大小。在这种情况下,我们将在消息的后面加上一个字符”#”,这将告诉MessageAssembler() 方法何时到达消息末尾,并停止接受数据以便进一步处理数据。我们将使用StringBuilder – 这个类用来高性能字符串连接操作。我们来看一下MessageAssembler() 方法:

private void MesssageAssembler(byte[] bytes, int offset, int count)
{
    for (int bytesCount = 0; bytesCount < count - 1;bytesCount++ )
    {
        if (bytes[bytesCount] == 35) //Check for "#" to signal the end
        {
            object[] paramObjs = new object[]{mStrBuilder.ToString()};
            Invoke(new InvokeDisplay(DisplayData), paramObjs);
            mStrBuilder = new StringBuilder();
        }
        else
        {
            mStrBuilder.Append((char)bytes[bytesCount]);
        }
    }
}

我们可以看到MessageAssembler() 方法循环遍历字节数组并把数据作为一个字符串附加到StringBuilder 实例上知道遇到”#”字符。一旦遇到这个字符,意味着数据流到达末尾。我们不需要担心字节到字符串的转换因为StringBuilder 类替我们做了这些。接下来它会调用DisplayData() 方法来处理数据:

private void DisplayData(string stockInfo)
{
    if (stockInfo == "-1")
    {
        MessageBox.Show("Symbol not found!", "Invalid Symbol",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    else
    {
        AddStock(stockInfo);
    }
}

这是我们第二次看到类似的代码,你可能想知道它是用来干嘛的。这个方法运行在后台工作线程里且与UI表单是同一个线程。尽管我们可以在程序中任何地方调用这个方法,但是这样做并不是一个好主意,因为它是非线程安全的。Windows 窗体程序基于Win32 单线程单元并且是非线程安全的,这意味着一个窗体在初始化以后不能安全地与操作线程(包括异步操作创建的后台线程)之间来回切换。你必须在窗体程序内部调用方法。为了解决这个问题,CLR 支持Invoke() 方法,它负责包装不同线程间的调用。

如果你怀疑上面说的,你可以自己调试一下代码并通过线程窗口看代码执行的当前线程ID。通过窗体的Invoke()方法调用创建的委托,实际上是运行在窗体线程中的,因此可以与窗体控件进行交互。如果不使用包装,通常情况下代码执行也没有问题,但是以后可能出问题并导致程序执行不稳定。程序生成的线程越多情况就会变得越差。因此,如果没有包装线程的话不要调用GUI。额外的,委托签名必须与Invoke()方法匹配,我们需要创建一个对象数组来保存字符串;这是使用Invoke()的唯一方式。我们可以调用DisplayData()方法来显示数据:

object[] paramObjs = new object[]{mStrBuilder.ToString()};
      Invoke(new InvokeDisplay(DisplayData), paramObjs);

当要发送的数据传递给Send() 方法,Send() 方法创建一个SteamWriter 类的实例并传递TcpClient 流作为其参数然后调用Write()方法,这回将数据以数据流形式在网络上发送。我们也调用Flush()方法来保证所有数据立即发送出去而不会被放到缓存中:

private void Send(string sendData)
{
    StreamWriter writer = new StreamWriter(mMyClient.GetStream());
    writer.Write(sendData);
    writer.Flush();
}

我们将要完成了,现在做一些清理工作。大多数据情况,Windows窗体类会调用它自己的Dispose()方法来清理资源,但是由于.NET 有不确定的垃圾回收期,所以我们最好自己手动关闭TcpClient 连接。我们写一个小函数来实现这个:

private void CloseConnection()
{
    if (mMyClient != null)
    {
        mMyClient.Close();
        mMyClient = null;
    }
}

至此,整个程序的客户端部分已经完成。下一篇我们将介绍如何创建服务端部分…

Original: https://www.cnblogs.com/danielWise/archive/2012/05/26/2519056.html
Author: DanielWise
Title: C# 线程手册 第七章 网络和线程 创建一个客户端

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/539902/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • IDEA清空控制台以及Java中运行cmd命令实现清屏操作

    在网上有看到各种的实现方法,比如: Runtime.getRuntime().exec("cls"); &#x6216;&#x8005;&amp…

    Java 2023年6月15日
    082
  • HikariCP连接池参数解释

    HikariCP连接池参数解释 ## 数据库配置 spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring….

    Java 2023年6月5日
    086
  • 使用对象流传输Student类

    上一篇博客已经介绍了如何通过定义原始的字节流来传输Student类,我们接下来继续练习如何通过对象流传输Student类。因为通过对象流传输的对象以及所包含的变量都必须要实现Ser…

    Java 2023年6月5日
    088
  • Ubuntu 制作 系统的启动盘

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月8日
    069
  • 妄撮小游戏的开发思想-Android开发资料-《妄撮(撕开美女衣服)》游戏源代码外传

    激动!想必大家一定听说过《妄撮》又名《撕开美女衣服》这个手机游戏,体验非常棒,很H很BL啊,现在很难下载到。不过今天哥在一个论坛竟然发现了这个游戏的源代码被外传,赶紧收藏,并慷慨与…

    Java 2023年5月29日
    071
  • 并发Map类删除数据的问题java.lang.UnsupportedOperationException: null

    笔者最近在开发Websocket相关的消息推送服务,使用了JSR356规范,由于需要维持会话。于是分别使用了以下类 笔者在维护失效的代码写了以下代码 最终在调用clients.re…

    Java 2023年5月29日
    070
  • Java面试整理(精简版)

    特征(OOP) 解释说明 通俗理解 关系联系 作用 封装 隐藏内部细节,只对外暴露访问方法 属性/方法封装,便于使用,限制不合理操作 类-类 低耦合,高内聚,增强代码可维护性;**…

    Java 2023年6月5日
    053
  • 【Oracle初学者】ORA-01034: ORACLE not available

    系统报错代码 ORA-01034: ORACLE not available 出现原因 //&#x5728;&#x542F;&#x52A8;&#x5…

    Java 2023年6月13日
    062
  • Aop踩坑!记一次模板类调用注入属性为空的问题

    问题起因 在做一个需求的时候,发现原来的代码逻辑都是基于模板+泛型的设计模式,模板用于规整逻辑处理流程,泛型用来转换参数和选取实现类。听上去是不是很nice! 但是在方法调用的时候…

    Java 2023年6月5日
    083
  • 1.音乐类

    bash;gutter:true;dos命令ffmpeg -i G:\准备上传的歌曲\MV\111.mp4 -acodec copy -vn G:\准备上传的歌曲\MV\111.a…

    Java 2023年6月9日
    060
  • Sword nginx slab源码解析二(初始化)

    /* 设计说明: 当page划分的slot块&#x5C…

    Java 2023年5月30日
    065
  • 排序算法-冒泡排序

    一、基本思想 冒泡排序是一种简单的排序算法,它也是一种稳定排序算法。其实现原理是重复扫描待排序序列,并比较每一对相邻的元素,当该对元素顺序不正确时进行交换。一直重复这个过程,直到没…

    Java 2023年6月5日
    0123
  • SpringMVC 五种注解参数绑定

    SpringMVC参数绑定,简单来说就是将客户端请求的key/value数据绑定到controller方法的形参上,然后就可以在controller中使用该参数了 下面通过5个常用…

    Java 2023年5月30日
    085
  • Java—-零钱通项目

    Java—-零钱通项目 项目要求:使用 Java 开发 零钱通项目 , 可以完成收益入账,消费,查看明细,退出系统等功能。 1. 面向过程 1)首先是这样一个菜单界面 …

    Java 2023年6月8日
    070
  • 系统的性能瓶颈,排查该从哪些方面入手,如何定位?

    如何排查系统的性能瓶颈点? 梳理系统的性能瓶颈点这件事应该不是一件简单的事情,需要针对不同设计的系统来进行单独分析。 首先一套完整可用的系统应该是有ui界面的(这里强调的是一套完整…

    Java 2023年5月29日
    0111
  • Leetcode刷题笔记(双指针)

    双指针主要用来遍历数组,两个指针指向不同的元素,从而协同完成任务。我们也可以类比这个概念,推广到多个数组的多个指针。 若两个指针指向同一数组,遍历方向相同且不会相交,可以称之为 滑…

    Java 2023年6月7日
    069
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球