[Json]Json初探

mikel阅读(788)

JSON初探

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition – December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。

JSON建构于两种结构:

  • “名称/值”对的集合(A collection of name/value pairs)。不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组 (associative array)。
  • 值的有序列表(An ordered list of values)。在大部分语言中,它被理解为数组(array)。

这些都是常见的数据结构。事实上大部分现代计算机语言都以某种形式支持它们。这使得一种数据格式在同样基于这些结构的编程语言之间交换成为可能。

JSON具有以下这些形式:

对象是一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’ 对”之间使用“,”(逗号)分隔。

{"UserID":11, "Name":"Truly", "Email":"zhuleipro◎hotmail.com"};

 
数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。 
[
{"UserID":11, "Name":{"FirstName":"Truly","LastName":"Zhu"}, "Email":"zhuleipro◎hotmail.com"},
{"UserID":12, "Name":{"FirstName":"Jeffrey","LastName":"Richter"}, "Email":"xxx◎xxx.com"},
{"UserID":13, "Name":{"FirstName":"Scott","LastName":"Gu"}, "Email":"xxx2◎xxx2.com"}
]

值(value)可以是双引号括起来的字符串(string)、数值(number)、truefalsenull、对象(object)或者数组(array)。这些结构可以嵌套。

{UserID:001,UserName:"Majie",IsAdmin:true,Rights:{IsAdd:true,IsDelete:false,IsEdit:true},fn:null}

 

字符串(string)是由双引号包围的任意数量Unicode字符的集合,使用反斜线转义。一个字符(character)即一个单独的字符串(character string)。

字符串(string)与C或者Java的字符串非常相似。

数值(number)也与C或者Java的数值非常相似。除去未曾使用的八进制与十六进制格式。除去一些编码细节。

 
  • 对象是属性、值对的集合。一个对象的开始于“{”,结束于“}”。每一个属性名和值间用“:”提示,属性间用“,”分隔。
  • 数组是有顺序的值的集合。一个数组开始于"[",结束于"]",值之间用","分隔。
  • 值可以是引号里的字符串、数字、true、false、null,也可以是对象或数组。这些结构都能嵌套。
  • 字符串和数字的定义和C或Java基本一致。
  • [Google]Google Web Toolkit(GWT) v1.5.3 - Google We

    mikel阅读(835)

    如今,编写网络应用程序是一个单调乏味且易于出错的过程。开发人员可能要花费 90% 的时间来处理浏览器行话。此外,构建、重复使用以及维护大量 JavaScript 代码库和 AJAX 组件可能困难且不可靠。Google Web 工具包 (GWT) 通过允许开发人员用 Java 编程语言快速构建和维护复杂但高性能的 JavaScript 前端应用程序来减轻该负担。

    Google Web 工具包工作原理
    有了 Google Web 工具包 (GWT),可以使用 Java 编程语言编写 AJAX 前端,然后 GWT 会交叉编译到优化的 JavaScript 中,而 JavaScript 可以自动在所有主要浏览器上运行。在开发过程中,您可以用 JavaScript 按习惯的相同“编辑 – 刷新 – 查看”循环快速反复,还有另一个好处就是能够调试和逐行单步调试 Java 代码。准备好进行部署后,GWT 会将 Java 源代码编译到优化且独立的 JavaScript 文件中。使用 Google Web 工具包可以轻松地为现有网页或整个应用程序构建一个 Widget。
    使用 Java 语言编写 AJAX 应用程序,然后编译为优化的 JavaScript
    与仅在文本级别运行的 JavaScript Minifier 不同,GWT 编译器会在整个 GWT 数据库中执行综合性静态分析和优化,通常生成的 JavaScript 加载和执行均比等效手写的 JavaScript 更快。例如,GWT 编译器可以安全地消除无用代码 — 极大的减少不使用的类别、方法、字段甚至方法参数 — 以确保您编译的脚本尽可能最小。另一个示例:GWT 编译器选择性地内联方法,消除方法调用的性能开销。
    交叉编译提供了开发所需的可维护的提取和模块性,而不会导致运行时性能损失。
    开发工作流程
    编辑 Java 代码,然后立即查看更改而无需重新编译
    在开发过程中,使用 GWT 的托管模式浏览器可以立即查看代码更改。无需汇编译为 JavaScript 或部署到服务器。只需进行更改,然后在托管模式浏览器中单击“刷新”。
    使用 Java 调试器单步调试当前 AJAX 代码
    在生产过程中,可以将代码编译为纯 JavaScript,但是在开发阶段,代码将在 Java 虚拟机作为字节码运行。这意味着,当代码执行处理鼠标事件等操作时,将获得功能完整的 Java 调试。Java 调试器可以执行的任何操作也应用于 GWT 代码,所以也可以执行断点和单步调试等自然操作。
    编译和部署优化的、跨浏览器的 JavaScript
    准备好进行部署后,GWT 会将 Java 代码编译成独立的纯 JavaScript 文件,任何网络服务器都支持该文件。此外,GWT 应用程序可自动支持 IE、Firefox、Mozilla、Safari 和 Opera,而无需在代码中进行浏览器检测或特殊封装。编写相同的代码后,GWT 会根据每个用户的特殊浏览器将其转换为最有效的 JavaScript。
    功能
    通过非常简单的 RPC 与服务器通信
    GWT 支持一组开放的传输协议,例如 JSON 和 XML,但 GWT RPC 使所有 Java 通信都特别轻松且有效。类似于传统 Java RMI,只需创建一个用于指定您要调用的远程方法的接口。从浏览器调用远程方法时,GWT RPC 将自动串行化参数,并调用服务器上的适当方法,然后反串行化客户端代码的返回值。GWT RPC 也将非常成熟,其可以处理多态类层次结构、对象图循环,甚至可以跨网抛出异常。
    根据用户个人资料优化 JavaScript 脚本下载
    延时绑定是 GWT 的一种功能,可以生成许多版本的编译代码,而在运行时自引导期间仅其中一个版本需要由特殊客户端载入。每个版本均以浏览器为基础生成,并带有应用程序定义 或使用的任何其他轴。例如,如果要使用 GWT 的国际化模块来国际化应用程序,GWT 编译器可能会根据每个浏览器环境生成各个版本的应用程序,例如“英文版 Firefox”、“法文版 Firefox”、“英文版 Internet Explorer”等,因此,部署的 JavaScript 代码非常紧凑并且下载比在 JavaScript 中编码然后声明更快。
    跨项目重复使用 UI 组件
    通过合成其他 Widget 来创建可重复使用的 Widget,然后轻松地在面板中自动对他们进行布局。GWT 展示应用程序可以提供 GWT 中各种 UI 功能的概述。要在其他项目中重复使用 Widget 吗?只需将其打包以便他人在 JAR 文件中使用。
    使用其他 JavaScript 库和本机 JavaScript 代码
    如果 GWT 的类库不能满足您的需要,则可以使用 JavaScript 本地接口 (JSNI) 在 Java 源代码中加入手写的 JavaScript。使用 GWT 1.5,现在就可以为 GWT JavaScriptObject (JSO) 类创建子类以将 Java“类覆盖”创建到任意 JavaScript 对象上。因此,可以获得将 JS 对象比拟为适当的 Java 类型(例如代码完成、重构、内联)而无需另外占用内存或速度的好处。此功能可以优化使用 JSON 结构。
    轻松支持浏览器的后退按钮和历史记录
    不,AJAX 应用程序无需破坏浏览器的后退按钮。使用 GWT,您可以通过轻松地为浏览器的后退按钮历史记录添加状态,来使您的站点更加有用。
    有效的本地化应用程序
    使用 GWT 功能强大的延时绑定技术来轻松创建有效的国际化应用程序和库。此外,从 1.5 版起,标准 GWT Widget 开始支持双向性。
    使用选择的开发工具提高生产力
    由于 GWT 使用 Java,您可以使用所有喜欢的 Java 开发工具(Eclipse、IntelliJ、JProfiler、JUnit)来进行 AJAX 开发。这使网络开发人员可以控制自动化 Java 重构和代码提示/完成的生产效率。此外,Java 语言的静态类型检查使开发人员可以在编写代码时而非运行时找出一类 JavaScript 错误(输入错误、类型不匹配),在减少错误的同时提高生产率。没有临时变量发现的更多用户。最后,则可以利用基于 Java 的 OO 设计模式和提取,由于编译器优化,模式和提取易于理解和维护而无需用户承担任何运行时性能损失。
    使用 JUnit 测试代码
    GWT 与 JUnit 直接集成,使您可以在调试器和浏览器中进行单元测试,并且您甚至可以对异步 RPC 进行单元测试。
    扩展或投稿 – Google Web 工具包是一种开源软件
    使用 Apache 2.0 许可,可获取所有 GWT 代码。如果您对投稿感兴趣,请访问使 GWT 变得更好。
    Google Web Toolkit (GWT) is an open source Java software development framework that makes writing AJAX applications like Google Maps and Gmail easy for developers who don't speak browser quirks as a second language. Writing dynamic web applications today is a tedious and error-prone process; you spend 90% of your time working around subtle incompatibilities between web browsers and platforms, and JavaScript's lack of modularity makes sharing, testing, and reusing AJAX components difficult and fragile.
    GWT lets you avoid many of these headaches while offering your users the same dynamic, standards-compliant experience. You write your front end in the Java programming language, and the GWT compiler converts your Java classes to browser-compliant JavaScript and HTML.
    Release Notes for 1.5.3
    Fixed Issues
    RPC requests no longer fail on the embedded Android web browser
    Leaf TreeItems now line up with their non-leaf siblings
    Removing the last child node from a TreeItem no longer creates extra margins on the left
    HTTPRequest no longer uses POST instead of GET on some IE installs because of incorrect XHR selection
    Compiler now uses a more reliable check to prevent methods with local variables from being inlined
    getAbsoluteTop()/Left() can no longer return non-integral values
    Time.valueOf() no longer fails to parse "08:00:00" or incorrectly accepts "0xC:0xB:0xA".
    更新:http://code.google.com/intl/zh-CN/webtoolkit/releases/release-notes-1….
    官网:http://code.google.com/webtoolkit
    官方下载:
    Windows:http://google-web-toolkit.googlecode.com/files/gwt-windows-1.5.3.zip
    Mac OS X:http://google-web-toolkit.googlecode.com/files/gwt-windows-1.5.3.zip
    Linux:http://google-web-toolkit.googlecode.com/files/gwt-linux-1.5.3.tar.bz2

    [MVC]调试、部署Oxite 开源系统

    mikel阅读(1160)

    调试、部署Oxite 开源系统
     
    Oxite 是微软近期发布了一个开源CMS或博客平台,关于Oxite 系统的基本介绍,请参考文章 – 微软发布Oxite开源CMS博客平台。
     
    这里,EntLib.com 开源论坛小组一步一步详细介绍如何编译、调试和部署 Oxite 系统。假设你的系统已经有SQL Server 2005 和 Visual Studio 2008。
     
    1. 首先下载并安装 ASP.NET MVC 开发包。
    ASP.NET MVC下载地址:
     
    如果你之前有安装 ASP.NET MVC 的前期版本,则你需要先下载早期的版本。ASP.NET MVC 的安装过程很简单,这里就不具体介绍了。
    关于ASP.NET MVC 的介绍,推荐你阅读如下文章:
    Microsoft ASP.NET MVC Preview 5 及LINQ TO SQL最新版开发示例演示(提供源码下载)
     
    2. 下载 Oxite 开源软件。
    展开压缩文件,通过 Visual Studio 2008 打开Oxite.sln 项目文件。先看看项目文件:
     
     
    接下来,设置 OxiteSite 项目为启动项目(Set as Startup Project)。编译整个Oxite 项目,整个项目编译通过。到现在,一切都很顺利。
     
    但是,当你运行OxiteSite 项目时,Visual Studio 2008 会提示你Deploy Failed-发布失败。仔细看看,这是Oxite.Database 项目产生的。
     
    最简单的解决办法是,右键单击Oxite.Database项目,选择 Unload Project 或者 Remove 菜单项,如下图所示。
     

     
    现在,你再次运行Oxite 项目,发现可以正常运行了,并且显示如下运行界面。
     
     
    Oxite.Database 项目到底有什么用呢?这里,Oxite.Database 项目用来创建Oxite 数据库,由于OxiteSite Web项目采用的是SQL Server Express进行 Oxite 数据库连接。数据库连接字符串Connection String 如下:
     
     <connectionStrings>
        <add name="ApplicationServices" connectionString="Data Source=.\SQLEXPRESS;AttachDBFileName=|DataDirectory|Oxite.Database.mdf;Integrated Security=true;User Instance=true;"/>
     </connectionStrings>
    默认,数据库文件存放在\OxiteSite\App_Data 目录下。所以,上面的Oxite.Database 项目可以直接Unload 或者 Remove。
     
    如果你想通过Oxite.Database 直接在SQL Server 2005 创建一个Oxite 的数据库,你也可以按照如下的步骤,轻松创建Oxite数据库。
    首先,需要修改Oxite.Database 项目的一些设置,如下图所示:
    (1) 设置Default collation 属性为 Chinese_PRC_CI_AS,采用中文字符集。
     
     
    (2) 正确设置Oxite.Database 项目的Target database name 属性为:Oxite。同时,设置Default location for target database files – 默认数据库文件存放位置属性为:c:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA\。这一属性根据实际情况,可以设置为其他路径。
    具体设置信息如下图所示:
     
     
    现在,编译、部署Oxite.Database 项目就可以正确通过了。此时,你可以进一步修改OxiteSite 项目中的web.config 配置文件,修改其中的数据库连接字符串 Connection String。如下所示:
     <connectionStrings>
        <addname="ApplicationServices"connectionString="Server=localhost; Database=Oxite;Integrated Security=true;"/>
     </connectionStrings>
     
    现在,重新编译、运行OxiteSite 项目,数据库连接到 SQL Server 2005 中的Oxite 数据库了,而不是先前的SQL Server express 的Oxite.Database.mdf 数据库文件。
     
    本文由http://forum.entlib.com 开源论坛小组提供,欢迎交流、讨论、分享。
     
    相关文章:
    1. 微软发布Oxite开源CMS博客平台
     
    2. Oxite 配置入门文章
     
    3. Microsoft ASP.NET MVC Preview 5 及LINQ TO SQL最新版开发示例演示(提供源码下载)

    [SQL]SQL Server2005 Sp3

    mikel阅读(896)

    Feature Pack for SQL Server 2005 December 2008

    Download the December 2008 Feature Pack for Microsoft SQL Server 2005, a collection of standalone install packages that provide additional value for SQL Server 2005.

    12/15/2008

    Microsoft SQL Server Protocol Documentation

    The Microsoft SQL Server protocol documentation provides technical specifications for Microsoft proprietary protocols that are implemented and used in Microsoft SQL Server 2008.

    12/15/2008

     

    SQL Server 2005 Express Edition with Advanced Services SP3

    Microsoft SQL Server 2005 Express Edition with Advanced Services is a free, easy-to use version of SQL Server Express that includes more features and makes it easier than ever to start developing powerful data-driven applications for web or local desktop development.

    12/15/2008

     

    SQL Server Management Studio Express SP3

    Microsoft SQL Server Management Studio Express (SSMSE) is a free, easy-to-use graphical management tool for managing SQL Server 2005 Express Edition and SQL Server 2005 Express Edition with Advanced Services.

    12/15/2008

    SQL Server 2005 Express Edition Toolkit SP3

    Microsoft SQL Server 2005 Express Edition Toolkit provides additional tools and resources for SQL Server 2005 Express Edition and SQL Server 2005 Express Edition with Advanced Services.

    12/15/2008

     

    SQL Server 2005 Express Edition SP3

    Microsoft SQL Server 2005 Express Edition is a free, easy-to-use, lightweight version of SQL Server 2005. It is fast and easy to learn, allowing you to quickly develop and deploy dynamic data-driven applications.

    12/15/2008

    SQL Server Data Mining Add-ins for Office 2007

    Download SQL Server 2005 Data Mining Add-ins for Office 2007. This package includes two add-ins for Microsoft Office Excel 2007 (Table Analysis Tools and Data Mining Client) and one add-in for Microsoft Office Visio 2007 (Data Mining Templates).

    12/15/2008

    SQL Server Reporting Services Add-in for SharePoint Technologies

    The Microsoft SQL Server 2005 Reporting Services Add-in for Microsoft SharePoint Technologies is a Web download that provides features for running a report server within a larger deployment of Windows SharePoint Services 3.0 or Microsoft Office SharePoint Server 2007.

    [C#]基于socket的聊天室实现原理

    mikel阅读(850)

    基于socket的聊天室,目前还比较少见,国内比较知名的有网易和碧海银沙聊天室。这种聊天室的特点 很明显,不象CGI聊天室那样不管有没有人发言,都会定时刷新。而是当有人发言时,屏幕上才会出现新聊天内容,而且聊天内容是不断向上滚动的,如果浏览器 状态栏在的话,可以看到进度条始终处于下载页面状态。这种聊天室可以容纳许多人而性能不会明显降低,象网易聊天室经常有数百人在一台服务器上聊天。由于这 种方式不同于CGI聊天室由客户端浏览器定时请求聊天内容,而是由聊天服务器软件向客户浏览器主动发送信息。
    Socket聊天室基本原理是,抛开cgi和www服务器,根据html规范,接收到浏览器的请求以后,模仿www服务器的响应,将聊天内容发回浏览器。 在浏览器看来就象浏览一个巨大的页面一样始终处于页面联接状态。实际上就是一个专门的聊天服务器,一个简化了的www服务器。
    这样相比CGI方式来说,Socket聊天室的优点就很明显:
    1. 不需要专门的WWW Server,在聊天服务器里完成必要的工作,避开耗时的CGI过程
    2. 如果使用单进程服务器,就不需要每次产生新进程
    3. 数据交换完全在内存进行,不用读写文件
    4. 不需要定时刷新,减少屏幕的闪烁,减少对服务器的请求次数

    在讨论具体流程之前,我们先来讨论相关的一些技术:

    http请求和应答过程
    http协议是浏览器与www服务器之间通信的标准,作为一个简化了的www服务器,socket聊天服务器应当遵守这个协议。实际上只要实现一小部分就可以了。
    http使用了客户服务器模式,其中浏览器是http客户,浏览某个页面实际上就是打开一个连接,发送一个请求到www服务器,服务器根据所请求的资源发 送应答给浏览器,然后关闭连接。客户和服务器之间的请求和应答有一定的格式要求,只要按照这个格式接收请求发送应答,就可以“欺骗”浏览器,使它以为正在 与www服务器通信。
    请求和应答具有类似的结构,包括:
    · 一个初始行
    · 0个或多个header lines
    · 一个空行
    · 可选的信息
    我们看看一个浏览器发出的请求:
    当我们浏览网页:http://www.somehost.com/path/file.html的时候,浏览器首先打开一个到主机www.somehost.com的80端口的socket,然后发送以下请求:
    GET /path/file.html HTTP/1.0
    From: someuser@somehost.com
    User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 4.0; DigExt)
    [空行]

    第一行GET /path/file.html HTTP/1.0是我们需要处理的核心。由以空格分隔的三部分组成,方法(method):GET,请求资源:/path/file.html,http版本:HTTP/1.0。

    服务器将会通过同一个socket用以下信息回应:
    HTTP/1.0 200 OK
    Date: Fri, 31 Dec 1999 23:59:59 GMT
    Content-Type: text/html
    Content-Length: 1354

    <html>
    <body>
    <h1>Hello world!</h1>
    (其他内容)
    .
    .
    .
    </body>
    </html>
    第一行同样也包括三部分:http版本,状态码,与状态码相关的描述。状态码200表示请求成功。
    发送完应答信息以后,服务器就会关闭socket。
    以上过程可以利用telnet www.somehost.com:80来模拟。

    循环服务器和并发服务器
    循环服务器是一个时刻只能处理一个请求的服务器,多个请求同时到来将会放在请求队列里。
    而并发服务器则是在每个请求到来以后分别产生一个新进程来处理。
    并发服务器由于其算法而具有与生俱来的快速响应优势,而且当某一个用户与服务器通信死锁不会影响其他进程,但由于多个进程之间需要通过进程间通信实现信息交换,而且fork新进程所带来的开销随着用户数量的增加越来越大,因此并发服务器在某些情况下不一定是最佳选择。
    循环服务器虽然表面上会产生时延,但是象聊天室这样的系统实际上处理每个请求的过程非常的短,对客户而言,可以取得与并发服务器一样的效果,而且由于是单 进程服务器,不需要进程间通信,不需要fork新进程,编程简单,系统资源消耗极少。但由于是单进程,某个客户与服务器之间死锁将导致整个系统死锁。

    POST与GET
    提交form信息一般常用的有两种:POST & GET,POST由于长度不受限制,而作为大多数form提交时使用的方法。GET方法通过URL来发送提交信息,由于URL最长只能1024字节,所以 如果发送信息很长的话,不能使用这种方法。由于聊天内容有长度限制,不会很长,而且因为普通浏览页面使用GET方法,因此使用GET方法提交form表 单,可以简化处理过程。

    使用perl模块实现Socket通信
    假定您对socket编程有一定的了解,如果您使用过C语言进行过socket编程,那么理解perl语言socket编程将是一件非常容易的事。如果不熟悉socket,请看本文所附socket参考。
    使用perl编写socket程序可以通过use Socket,也可以通过use IO::Socket。前一种方法接近于C语言,后一种则进行了对象封装,编写维护会容易许多。

    我们在通过单进程循环服务器实现并发服务的时候,基本思路是:允许多个客户打开到服务器的socket 连接,服务器通过一定的方法监测哪些socket有数据到达,并处理该连接。在这个思路中有个关键问题服务器如何触发数据处理?了解C语言socket编 程就会知道有个系统函数select可以完成这一操作,但由于使用了位操作,perl语言处理不是很清晰,但是如果使用了模块IO::Select就会变 得很简单。
    我们看一个来自IO::Select的帮助的例子:
    use IO::Select;
    use IO::Socket;

    $lsn = new IO::Socket::INET(Listen => 1, LocalPort => 8080);
    #创建socket,在端口 8080上监听,相当于使用系统函数
    #socket(),bind(),listen()

    $sel = new IO::Select( $lsn );
    #创建select对象,并把前面创建的socket对象加入

    while(@ready = $sel->can_read) {#处理每个可读的socket
    foreach $fh (@ready) {
    if($fh == $lsn) {
    #如果最初创建的socket可读,说明有新连接
    #建立一个新的socket,加入select
    $new = $lsn->accept;
    $sel->add($new);
    }
    else {
    #如果是其他socket,读取数据,处理数据
    ……
    #处理完成以后,从select中删除socket,然后关闭socket
    $sel->remove($fh);
    $fh->close;
    }
    }
    }
    IO::Socket的基本操作,
    创建socket对象:$socket=new IO::Socket::INET();
    接收客户的连接请求:$new_socket=$socket->accept;
    通过socket发送数据:$socket->send($message);
    从socket接收数据:$socket->recv($buf,LENGTH);
    关闭socket连接:$socket->close;
    判断socket是否出于打开状态:$socket->opened;

    IO::Select的基本操作
    创建select对象:$select=new IO::Select();
    添加socket到select中:$select->add($new_socket);
    从select中删除socket:$select->remove($old_socket);
    从select中查找可读的socket:@readable=$select->can_read;
    找出select中的所有socket:@sockets=$select->handles;

    Daemon实现方法
    实现一个后台进程需要完成一系列的工作,包括
    · 关闭所有的文件描述字
    · 改变当前工作目录
    · 重设文件存取屏蔽码(umask)
    · 在后台执行
    · 脱离进程组
    · 忽略终端I/O信号
    · 脱离控制终端
    这些操作可以利用perl模块来简化:
    use Proc::Daemon;
    Proc::Daemon::Init;


    pipe信号处理
    如果客户关闭了socket以后,服务器继续发送数据,将会产生PIPE Signal,如果不加处理,就会导致服务器意外中断,为避免这一情况的发生,我们必须对它进行处理,一般情况下,只需要简单地忽略这个信号即可。
    $SIG{‘PIPE’}=’IGNORE’;

    意外处理
    在Socket通信过程中很容易出现一些意外情况,如果不加处理直接发送数据,就可能导致程序意外退出。Perl语言中的eval函数可以用于意外处理。例如:
    if (!defined(eval{操作语句;})){
    错误处理;
    }
    这样当eval中的操作语句出现错误,如die的时候,只会中止eval语句,并不会中断主程序。

    用户断线判断和处理
    许多情况下,用户不是通过提交“离开”按钮离开聊天室,这时候就需要判断用户是否断线了。方法是:当用户关闭浏览器,或者点击了浏览器stop按钮,或者跳转到其他网页的时候,相对应的socket将会变成可读状态,而此时读出的数据却是空字符串。
    利用这个原理,只要在某个可读的socket读取数据时,读到的却是空数据,那么我们就可以断定,与这个socket相对应的用户断线了。

    防止用户断线
    如果浏览器在一段时间内没有接到任何数据,那么就会出现超时错误。要避免这一错误,必须在一定间隔内发送一些数据,在我们这个应用系统里,可以发送一些html注释。发送注释的工作可以由在线名单刷新过程顺带完成。

    下面我们来看看具体实现流程:
    聊天服务器实现流程
    · 服务器端
    下图是NS盒图程序流程:


    上图中的“处理用户输入”部分可以细化为下图:

     

    用户数据输入都是通过URL传送,下面是几个url实例,结合后面客户端流程,可以更好地理解系统结构:
    这是一个用户名密码均为’aaa’的聊天用户登录系统,说了一句话“hello”,然后退出所产生的一系列请求,其中密码用系统函数crypt加密过:
    /login?name=aaa&passwd=PjHIIEleipsEE
    /chat?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE
    /talk?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE
    /names?sid=ZUyPHh3TWhENKsICnjOv
    /doTalk?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE&message=hello
    /leave?sid=ZUyPHh3TWhENKsICnjOv&passwd=PjHIIEleipsEE

    以上是服务器程序流程,下面我们从客户端看看具体登录过程。
    我们先看看聊天界面:


    聊天界面由三个frame组成,其中chat帧是聊天内容显示部分;talk帧是用户输入部分,包括聊天内容输入、动作、过滤以及管理功能都在这一帧输入;names是在线名单显示部分,这一部分是定时刷新的。

    让我们从浏览器的角度来看看进入聊天室的过程。
    · 首先浏览器请求页面
    http://host:9148/login?name=NAME&passwd=PWD
    此时产生了一个连接到服务器聊天端口的socket联接,并发送了一行数据:
    GET /login?name=NAME&passwd=PWD HTTP/1.1
    · 服务器生成一个session ID,验证密码以后,发回:
    HTTP/1.1 200 OK
    <其他头信息>
    Content-TYPE: text/html
    <空行>
    <html>
    ……
    <frameset cols="*,170" rows="*" border="1" framespacing="1">
    <frameset rows="*,100,0" cols="*" border="0" framespacing="0">
    <frame src="/chat?sid=$sid&passwd=$encrypt_pass" name="u" frameborder="NO" noresize>
    <frame src="/talk?sid=$sid&passwd=$encrypt_pass" name="d" frameborder="NO" noresize>
    </frameset>
    <frame src="/names?sid=$sid" name="r" noresize>
    </frameset>
    ……
    </html>
    然后服务器关闭socket联接。

    · 浏览器收到以上html文件后,将会依次打开三个联接(其中的$sid和$encrypt_pass是变量):
    /chat?sid=$sid&passwd=$encrypt_pass /talk?sid=$sid&passwd=$encrypt_pass
    /names?sid=$sid
    这三个联接中的第一个联接chat在整个聊天过程中都是保持联接的,这样从浏览器角度来看,就是一个始终下载不完的大页面,显示效果上就是聊天内容不是靠 刷新来更新,而是不断地向上滚动。通过察看html代码可以看到,只有<html><body>,然后就是不断增加的聊天内容, 没有</body></html>。
    另外两个联接在页面发送完毕以后,socket就关闭了。
    这样一次登录聊天室实际上有四次socket联接,但登录完成以后,只有chat帧的socket是保持联接的,用于接收来自服务器的聊天信息,这是socket聊天室的关键所在。
    在服务器端储存了所有参加聊天的客户的chat socket,当有人发言时,服务器就向所有chat socket发送聊天内容。
    Talk与names帧的html实际上和普通的form是一样的。

    · 在用户登录以后,服务器端保存了一张包括用户信息的表格。
    在perl实现中,我们使用哈希结构储存信息,以session ID作为key索引。这样的存储结构便于存取数据,回收空间。每个客户信息是一个数组:
    [socket,name,passwd,privilige,filter,login_time,color]
    socket:储存chat帧socket联接
    name:用户名
    passwd:密码
    privilige:权限
    filter:某个用户的过滤列表的引用(reference)
    login_time:记录登录时间,以便以后清除一些超时联接
    color:用户聊天颜色
    以上用户数据大部分是在login阶段,用户通过密码验证以后填入的。只有chat socket要等到chat帧显示以后才得到。如果超过一定时间,socket还是没有填入,说明浏览器取得主框架以后连接中断了,这时候就需要删除该用户数据。

    以上是聊天室核心部分,其他部分,如用户注册、改密码等可以沿用CGI聊天室代码。

    需要改进的地方
    目前提供了聊天、悄悄话、动作这些基本聊天功能以及过滤用户名单这样的附加功能。管理功能完成了踢人、查IP、任命室主。今后需要改进的地方有:
    稳定性:目前聊天室还没有经过大用户量测试,稳定性还不能充分保证。由于是单进程循环服务器,某个用户通信死锁将导致所有人死锁。如果采用并发多进程服务器,可以使稳定性得到提高。但这样的系统对服务器资源消耗也会大许多。
    功能:自建聊天室等功能还没有完成,这些外围功能在稳定性有保证以后就可以比较容易地加入。

    [参考内容]
    1. 本文所述的聊天室的最初结构来自于Entropy Chat 2.0(http://missinglink.darkorb.net/pub/entropychat/),如果没有它的启示,完成这一系统会有许多 困难,非常感谢他们的努力工作,愿意共同完善这个程序的朋友们,可以到http://tucows.qz.fj.cn/chat下载源代码。

    2. http的基本交互过程请参考
    HTTP Made Really Easy(http://www.jmarshall.com/easy/http/),RFC1945:Hypertext Transfer Protocol — HTTP/1.0

    3. 本文所提到的perl模块,都可以在http://tucows.qz.fj.cn找到,请使用页面上方的搜索功能搜索。
    IO::Socket和IO::Select是perl标准模块,也可以通过安装IO-1.20.tar.gz得到。
    Proc:Daemon需要另外安装,模块为Proc-Daemon-0.02.tar.gz
    上述模块版本号可能有所不同,搜索时只要输部分关键字如:”Daemon”即可找到。

    4. 为加快开发过程,程序的界面部分参考了网易聊天室(http://chat.163.net/),程序的很多想法也来自于他们的工作。

    5. 《How to Write a Chat Server》可以作为一个很好的参考
    http://hotwired.lycos.com/webmonkey/97/18/index2a.html

    6. 需要测试聊天室功能可以到http://tucows.qz.fj.cn/chat;
    7. socket编程参考
    · Unix Socket FAQ(http://www.ntua.gr/sock-faq/)
    · Beejs Guide to Network Programming
    (http://www.ecst.csuchico.edu/~beej/guide/net/

    [Python]IronPython 2.0 发布了

    mikel阅读(798)

           DLR团队终于发布了 IronPython 2.0 ,IronPython 2.0完全基于Dynamic Language Runtime (DLR). DLR允许多个动态语言在系统类型层面实现互操作。这个版本修复大概500多个bug,有453个来自codeplex社区的反馈。热烈祝贺开发团队发布 了这一个重大的里程碑版本。可以到codeplex上去下载,下面是一些重要的链接:

    下面是IronPython 2.0一些说明:

    [C#]C#编写的聊天室程序

    mikel阅读(1006)

    源码下载:http://www.tracefact.net/SourceCode/CSharp-Chat.rar

    C#编写简单的聊天程序

    引言

    这是一篇基于Socket进行网络编程的入门文章,我对于网络编程的学习并不够深入,这篇文章是对于自己知识的一个巩固,同时希望能为初学的朋友提供一点参考。文章大体分为四个部分:程序的分析与设计、C#网络编程基础(篇外篇)、聊天程序的实现模式、程序实现。

    程序的分析与设计

    1.明确程序功能

    如 果大家现在已经参加了工作,你的经理或者老板告诉你,“小王,我需要你开发一个聊天程序”。那么接下来该怎么做呢?你是不是在脑子里有个雏形,然后就直接 打开VS2005开始设计窗体,编写代码了呢?在开始之前,我们首先需要进行软件的分析与设计。就拿本例来说,如果只有这么一句话“一个聊天程序”,恐怕 现在大家对这个“聊天程序”的概念就很模糊,它可以是像QQ那样的非常复杂的一个程序,也可以是很简单的聊天程序;它可能只有在对方在线的时候才可以进行 聊天,也可能进行留言;它可能每次将消息只能发往一个人,也可能允许发往多个人。它还可能有一些高级功能,比如向对方传送文件等。所以我们首先需要进行分 析,而不是一上手就开始做,而分析的第一步,就是搞清楚程序的功能是什么,它能够做些什么。在这一步,我们的任务是了解程序需要做什么,而不是如何去做。

    了解程序需要做什么,我们可以从两方面入手,接下来我们分别讨论。

    1.1请求客户提供更详细信息

    我 们可以做的第一件事就是请求客户提供更加详细的信息。尽管你的经理或老板是你的上司,但在这个例子中,他就是你的客户(当然通常情况下,客户是公司外部委 托公司开发软件的人或单位)。当遇到上面这种情况,我们只有少得可怜的一条信息“一个聊天程序”,首先可以做的,就是请求客户提供更加确切的信息。比如, 你问经理“对这个程序的功能能不能提供一些更具体的信息?”。他可能会像这样回答:“哦,很简单,可以登录聊天程序,登录的时候能够通知其他在线用户,然 后与在线的用户进行对话,如果不想对话了,就注销或者直接关闭,就这些吧。”

    有了上面这段话,我们就又可以得出下面几个需求:

    1. 程序可以进行登录。
    2. 登录后可以通知其他在线用户。
    3. 可以与其他用户进行对话。
    4. 可以注销或者关闭。

    1.2对于用户需求进行提问,并进行总结

    经常会有这样的情况:可能客户给出的需求仍然不够细致,或者客户自己本身对于需求就很模糊,此时我们需要做的就是针对用户上面给出的信息进行提问。接下来我就看看如何对上面的需求进行提问,我们至少可以向经理提出以下问题:

    NOTE:这里我穿插一个我在见到的一个印象比较深刻的例子:客户往往向你表达了强烈的意愿他多么多么想拥有一个属于自己的网站,但是,他却没有告诉你网站都有哪些内容、栏目,可以做什么。而作为开发者,我们显然关心的是后者。

    1. 登录时需要提供哪些内容?需不需要提供密码?
    2. 允许多少人同时在线聊天?
    3. 与在线用户聊天时,可以将一条消息发给一个用户,还是可以一次将消息发给多个用户?
    4. 聊天时发送的消息包括哪些内容?
    5. 注销和关闭有什么区别?
    6. 注销和关闭对对方需不需要给对方提示?

    由于这是一个范例程序,而我在为大家讲述,所以我只能再充当一下客户的角色,来回答上面的问题:

    1. 登录时只需要提供用户名称就可以了,不需要输入密码。
    2. 允许两个人在线聊天。(这里我们只讲述这种简单情况,允许多人聊天需要使用多线程)
    3. 因为只有两个人,那么自然是只能发给一个用户了。
    4. 聊天发送的消息包括:用户名称、发送时间还有正文。
    5. 注销并不关闭程序,只是离开了对话,可以再次进行连接。关闭则是退出整个应用程序。
    6. 注销和关闭均需要给对方提示。

    好了,有了上面这些信息我们基本上就掌握了程序需要完成的功能,那么接下来做什么?开始编码了么?上面的这些属于业务流程,除非你对它已经非常熟悉,或者程序非常的小,那么可以对它进行编码,但是实际中,我们最好再编写一些用例,这样会使程序的流程更加的清楚。

    1.3编写用例

    通常一个用例对应一个功能或者叫需求,它是程序的一个执行路径或者执行流程。编 写用例的思路是:假设你已经有了这样一个聊天程序,那么你应该如何使用它?我们的使用步骤,就是一个用例。用例的特点就每次只针对程序的一个功能编写,最 后根据用例编写代码,最终完成程序的开发。我们这里的需求只有简单的几个:登录,发送消息,接收消息,注销或关闭,上面的分析是对这几点功能的一个明确。 接下来我们首先编写第一个用例:登录。

    在开始之前,我们先明确一个概念:客户端,服务端。因为这个程序只是在两个人(机器)之间聊天,那么我们大致可以绘出这样一个图来:

    我们期望用户A和用户B进行对话,那么我们就需要在它们之间建立起连接。尽管“用户A”和“用户B”的地位是对等的,但按照约定俗称的说法:我们将发起连接请求的一方称为客户端(或叫本地),另一端称为服务端(或叫远程)。所以我们的登录过程,就是“用户A”连接到“用户B”的过程,或者说客户端(本地)连接到服务端(远程)的过程。在分析这个程序的过程中,我们总是将其分为两部分,一部分为发起连接、发送消息的一方(本地),一方为接受连接、接收消息的一方(远程)。

    登录和连接(本地)
    主路径 可选路径
    1.打开应用程序,显示登录窗口  
    2.输入用户名  
    3.点击“登录”按钮,登录成功 3.“登录”失败

    如果用户名为空,重新进入第2步。

    4.显示主窗口,显示登录的用户名称  
    5.点击“连接”,连接至远程  
    6.连接成功
    6.1提示用户,连接已经成功。
    6.连接失败
    6.1 提示用户,连接不成功
    5.在用户界面变更控件状态

    5.2连接为灰色,表示已经连接

    5.3注销为亮色,表示可以注销

    5.4发送为亮色,表示可以发消息

     

    这里我们的用例名称为登录和连接,但是后面我们又打了一个括号,写着“本地”,它的意思是说,登录和连接是客户端,也就是发起连接的一方采取的动作。同样,我们需要写下当客户端连接至服务端时,服务端采取的动作。

    登录和连接(远程)
    主路径 可选路径
    1-4 同客户端  
    5.等待连接  
    6.如果有连接,自动在用户界面显示“远程主机连接成功”  

    接下来我们来看发送消息。在发送消息时,已经是登录了的,也就是“用户A”、“用户B”已经做好了连接,所以我们现在就可以只关注发送这一过程:

    发送消息(本地)
    主路径 可选路径
    1.输入消息  
    2.点击发送按钮 2.没有输入消息,重新回到第1步
    3.在用户界面上显示发出的消息 3.服务端已经断开连接或者关闭

    3.1在客户端用户界面上显示错误消息

    然后我们看一下接收消息,此时我们只关心接收消息这一部分。

    接收消息(远程)
    主路径 可选路径
    1.侦听到客户端发来的消息,自动显示在用户界面上。  

    注意到这样一点:当远程主机向本地返回消息时,它的用例又变为了上面的用例“发送消息(本地)”。因为它们的角色已经互换了。

    最后看一下注销,我们这里研究的是当我们在本地机器点击“注销”后,双方采取的动作:

    注销(本地主动)
    主路径 可选路径
    1.点击注销按钮,断开与远程的连接  
    2.在用户界面显示已经注销  
    3.更改控件状态

    3.1注销为灰色,表示已经注销

    3.2连接为亮色,表示可以连接

    3.3发送为灰色,表示无法发送

     

    与此对应,服务端应该作出反应:

    注销(远程被动)
    主路径 可选路径
    1.自动显示远程用户已经断开连接。  

    注意到一点:当远程主动注销时,它采取的动作为上面的“本地主动”,本地采取的动作则为这里的“远程被动”。

    至 此,应用程序的功能分析和用例编写就告一段落了,通过上面这些表格,之后再继续编写程序变得容易了许多。另外还需要记得,用例只能为你提供一个操作步骤的 指导,在实现的过程中,因为技术等方面的原因,可能还会有少量的修改。如果修改量很大,可以重新修改用例;如果修改量不大,那么就可以直接编码。这是一个 迭代的过程,也没有一定的标准,总之是以高效和合适为标准。

    2.分析与设计

    我们已经很清楚地知道了程序需要做些什么,尽 管现在还不知道该如何去做。我们甚至可以编写出这个程序所需要的接口,以后编写代码的时候,我们只要去实现这些接口就可以了。这也符合面向接口编程的原 则。另外我们注意到,尽管这是一个聊天程序,但是却可以明确地划分为两部分,一部分发送消息,一部分接收消息。另外注意上面标识为自动的语句,它们暗示这个操作需要通过事件的通知机制来完成。关于委托和事件,可以参考这两篇文章:

    • C#中的委托和事件 – 委托和事件的入门文章,同时捎带讲述了Observer设计模式和.NET的事件模型
    • C#中的委托和事件(续) – 委托和事件更深入的一些问题,包括异常、超时的处理,以及使用委托来异步调用方法。

    2.1消息Message

    首先我们可以定义消息,前面我们已经明确了消息包含三个部分:用户名、时间、内容,所以我们可以定义一个结构来表示这个消息:

    public struct Message {
        private readonly string userName;
        private readonly string content;
        private readonly DateTime postDate;
        public Message(string userName, string content) {
            this.userName = userName;
            this.content = content;
            this.postDate = DateTime.Now;
        }
        public Message(string content) : this("System", content) { }
        public string UserName {
            get { return userName; }
        }
        public string Content {
            get { return content; }
        }
        public DateTime PostDate {
            get { return postDate; }
        }
        public override string ToString() {
            return String.Format("{0}[{1}]: {2} ", userName, postDate, content);
        }
    }

    2.2消息发送方IMessageSender

    从上面我们可以看出,消息发送方主要包含这样几个功能:登录连接发送消息注销。 另外在连接成功或失败时还要通知用户界面,发送消息成功或失败时也需要通知用户界面,因此,我们可以让连接和发送消息返回一个布尔类型的值,当它为真时表 示连接或发送成功,反之则为失败。因为登录没有任何的业务逻辑,仅仅是记录控件的值并进行显示,所以我不打算将它写到接口中。因此我们可以得出它的接口大 致如下:

    public interface IMessageSender {
        bool Connect(IPAddress ip, int port);       // 连接到服务端
        bool SendMessage(Message msg);              // 发送用户
        void SignOut();                                 // 注销系统
    }

    2.3消息接收方IMessageReceiver

    而对于消息接收方,从上面我们可以看出,它的操作全是被动的:客户端连接时自动提示,客户端连接丢失时显示自动提示,侦听到消息时自动提 示。注意到上面三个词都用了“自动”来修饰,在C#中,可以定义委托和事件,用于当程序中某种情况发生时,通知另外一个对象。在这里,程序即是我们的 IMessageReceiver,某种情况就是上面的三种情况,而另外一个对象则为我们的用户界面。因此,我们现在首先需要定义三个委托:

    public delegate void MessageReceivedEventHandler(string msg);  
    public delegate void ClientConnectedEventHandler(IPEndPoint endPoint);
    public delegate void ConnectionLostEventHandler(string info);

    接下来,我们注意到接收方需要侦听消息,因此我们需要在接口中定义的方法是StartListen()和StopListen()方法,这两个方法是典型的技术相关,而不是业务相关,所以从用例中是看不出来的,可能大家现在对这两个方法是做什么的还不清楚,没有关系,我们现在并不写实现,而定义接口并不需要什么成本,我们写下IMessageReceiver的接口定义:

    public interface IMessageReceiver {
        event MessageReceivedEventHandler MessageReceived; // 接收到发来的消息
        event ConnectionLostEventHandler ClientLost;            // 远程主动断开连接
        event ClientConnectedEventHandler ClientConnected;  // 远程连接到了本地
        void StartListen();         // 开始侦听端口
        void StopListen();          // 停止侦听端口
    }

    我记得曾经看过有篇文章说过,最好不要在接口中定义事件,但是我忘了他的理由了,所以本文还是将事件定义在了接口中。

    2.4主程序Talker

    而 我们的主程序是既可以发送,又可以接收,一般来说,如果一个类像获得其他类的能力,以采用两种方法:继承和复合。因为C#中没有多重继承,所以我们无法同 时继承实现了IMessageReceiver和IMessageSender的类。那么我们可以采用复合,将它们作为类成员包含在Talker内部:

    public class Talker {
        private IMessageReceiver receiver;
        private IMessageSender sender;
        public Talker(IMessageReceiver receiver, IMessageSender sender) {
            this.receiver = receiver;
            this.sender = sender;
        }
    }

    现在,我们的程序大体框架已经完成,接下来要关注的就是如何实现它,现在让我们由设计走入实现,看看实现一个网络聊天程序,我们需要掌握的技术吧。

    C#网络编程基础(篇外篇)

    这部分的内容请参考 C#网络编程 系列文章,共5个部分较为详细的讲述了基于Socket的网络编程的初步内容。

    编写程序代码

    如 果你已经看完了上面一节C#网络编程,那么本章完全没有讲解的必要了,所以我只列出代码,对个别值得注意的地方稍微地讲述一下。首先需要了解的就是,我们 采用的是三个模式中开发起来难度较大的一种,无服务器参与的模式。还有就是我们没有使用广播消息,所以需要提前知道连接到的远程主机的地址和端口号。

    1.实现IMessageSender接口

    public class MessageSender : IMessageSender {
        TcpClient client;
        Stream streamToServer;
        // 连接至远程
        public bool Connect(IPAddress ip, int port) {
            try {
                client = new TcpClient();
                client.Connect(ip, port);
                streamToServer = client.GetStream();    // 获取连接至远程的流
                return true;
            } catch {
                return false;
            }
        }
        // 发送消息
        public bool SendMessage(Message msg) {
            try {
                lock (streamToServer) {
                    byte[] buffer = Encoding.Unicode.GetBytes(msg.ToString());
                    streamToServer.Write(buffer, 0, buffer.Length);
                    return true;
                }
            } catch {
                return false;
            }
        }
        // 注销
        public void SignOut() {
            if (streamToServer != null)
                streamToServer.Dispose();
            if (client != null)
                client.Close();
        }
    }

    这段代码可以用朴实无华来形容,所以我们直接看下一段。

    2.实现IMessageReceiver接口

    public delegate void PortNumberReadyEventHandler(int portNumber);
    public class MessageReceiver : IMessageReceiver {
        public event MessageReceivedEventHandler MessageReceived;
        public event ConnectionLostEventHandler ClientLost;
        public event ClientConnectedEventHandler ClientConnected;
        // 当端口号Ok的时候调用 — 需要告诉用户界面使用了哪个端口号在侦听
        // 这里是业务上体现不出来,在实现中才能体现出来的
        public event PortNumberReadyEventHandler PortNumberReady;
        private Thread workerThread;
        private TcpListener listener;
        public MessageReceiver() {
            ((IMessageReceiver)this).StartListen();
        }
        // 开始侦听:显示实现接口
        void IMessageReceiver.StartListen() {
            ThreadStart start = new ThreadStart(ListenThreadMethod);
            workerThread = new Thread(start);
            workerThread.IsBackground = true;
            workerThread.Start();
        }
        // 线程入口方法
        private void ListenThreadMethod() {
            IPAddress localIp = IPAddress.Parse("127.0.0.1");
            listener = new TcpListener(localIp, 0);
            listener.Start();
            // 获取端口号
            IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;
            int portNumber = endPoint.Port;
            if (PortNumberReady != null) {
                PortNumberReady(portNumber);        // 端口号已经OK,通知用户界面
            }
            while (true) {
                TcpClient remoteClient;
                try {
                    remoteClient = listener.AcceptTcpClient();
                } catch {
                    break;
                }
                if (ClientConnected != null) {
                    // 连接至本机的远程端口
                    endPoint = remoteClient.Client.RemoteEndPoint as IPEndPoint;
                    ClientConnected(endPoint);      // 通知用户界面远程客户连接
                }
                Stream streamToClient = remoteClient.GetStream();
                byte[] buffer = new byte[8192];
                while (true) {
                    try {
                        int bytesRead = streamToClient.Read(buffer, 0, 8192);
                        if (bytesRead == 0) {
                            throw new Exception("客户端已断开连接");
                        }
                        string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                        if (MessageReceived != null) {
                            MessageReceived(msg);       // 已经收到消息
                        }
                    } catch (Exception ex) {
                        if (ClientLost != null) {
                            ClientLost(ex.Message);     // 客户连接丢失
                            break;                      // 退出循环
                        }
                    }
                }
            }
        }
        // 停止侦听端口
        public void StopListen() {
            try {
                listener.Stop();
                listener = null;
                workerThread.Abort();
            } catch { }
        }
    }

    这里需要注意的有这样几点:我们StartListen()为显式实现接口, 因为只能通过接口才能调用此方法,接口的实现类看不到此方法;这通常是对于一个接口采用两种实现方式时使用的,但这里我只是不希望 MessageReceiver类型的客户调用它,因为在MessageReceiver的构造函数中它已经调用了StartListen。意思是说,我 们希望这个类型一旦创建,就立即开始工作。我们使用了两个嵌套的while循环,这个它可以为多个客户端的多次请求服务,但是因为是同步操作,只要有一个 客户端连接着,我们的后台线程就会陷入第二个循环中无法自拔。所以结果是:如果有一个客户端已经连接上了,其它客户端即使连接了也无法对它应答。最后需要注意的就是四个事件的使用,为了向用户提供侦听的端口号以进行连接,我又定义了一个PortNumberReadyEventHandler委托。

    3.实现Talker类

    Talker 类是最平庸的一个类,它的全部功能就是将操作委托给实际的IMessageReceiver和IMessageSender。定义这两个接口的好处也从这 里可以看出来:如果日后想重新实现这个程序,所有Windows窗体的代码和Talker的代码都不需要修改,只需要针对这两个接口编程就可以了。

    public class Talker {
        private IMessageReceiver receiver;
        private IMessageSender sender;
        public Talker(IMessageReceiver receiver, IMessageSender sender) {
            this.receiver = receiver;
            this.sender = sender;
        }
        public Talker() {
            this.receiver = new MessageReceiver();
            this.sender = new MessageSender();
        }
        public event MessageReceivedEventHandler MessageReceived {
            add {
                receiver.MessageReceived += value;
            }
            remove {
                receiver.MessageReceived -= value;
            }
        }
        public event ClientConnectedEventHandler ClientConnected {
            add {
                receiver.ClientConnected += value;
            }
            remove {
                receiver.ClientConnected -= value;
            }
        }
        public event ConnectionLostEventHandler ClientLost {
            add {
                receiver.ClientLost += value;
            }
            remove {
                receiver.ClientLost -= value;
            }
        }
        // 注意这个事件
        public event PortNumberReadyEventHandler PortNumberReady {
            add {
                ((MessageReceiver)receiver).PortNumberReady += value;
            }
            remove {
                ((MessageReceiver)receiver).PortNumberReady -= value;
            }
        }
               
        // 连接远程 – 使用主机名
        public bool ConnectByHost(string hostName, int port) {
            IPAddress[] ips = Dns.GetHostAddresses(hostName);
            return sender.Connect(ips[0], port);
        }
        // 连接远程 – 使用IP
        public bool ConnectByIp(string ip, int port) {
            IPAddress ipAddress;
            try {
                ipAddress = IPAddress.Parse(ip);
            } catch {
                return false;
            }
            return sender.Connect(ipAddress, port);
        }
        // 发送消息
        public bool SendMessage(Message msg) {
            return sender.SendMessage(msg);
        }
        // 释放资源,停止侦听
        public void Dispose() {
            try {
                sender.SignOut();
                receiver.StopListen();
            } catch {
            }
        }
        // 注销
        public void SignOut() {
            try {
                sender.SignOut();
            } catch {
            }
        }
    }

    4.设计窗体,编写窗体事件代码

    现在我们开始设计窗体,我已经设计好了,现在可以先进行一下预览:

    这 里需要注意的就是上面的侦听端口,是程序接收消息时的侦听端口,也就是IMessageReceiver所使用的。其他的没有什么好说的,下来我们直接看 一下代码,控件的命名是自解释的,我就不多说什么了。唯一要稍微说明下的是txtMessage指的是下面发送消息的文本框,txtContent指上面 的消息记录文本框:

    public partial class PrimaryForm : Form {
        private Talker talker;
        private string userName;
        public PrimaryForm(string name) {
            InitializeComponent();
            userName = lbName.Text = name;
            this.talker = new Talker();
            this.Text = userName + " Talking …";
            talker.ClientLost +=
                new ConnectionLostEventHandler(talker_ClientLost);
            talker.ClientConnected +=
                new ClientConnectedEventHandler(talker_ClientConnected);
            talker.MessageReceived +=
                new MessageReceivedEventHandler(talker_MessageReceived);
            talker.PortNumberReady +=
                new PortNumberReadyEventHandler(PrimaryForm_PortNumberReady);
        }
        void ConnectStatus() {  }
        void DisconnectStatus() { }
        // 端口号OK
        void PrimaryForm_PortNumberReady(int portNumber) {         
            PortNumberReadyEventHandler del = delegate(int port) {
                lbPort.Text = port.ToString();
            };
            lbPort.Invoke(del, portNumber);
        }
        // 接收到消息
        void talker_MessageReceived(string msg) {
            MessageReceivedEventHandler del = delegate(string m) {
                txtContent.Text += m;
            };
            txtContent.Invoke(del, msg);
        }
        // 有客户端连接到本机
        void talker_ClientConnected(IPEndPoint endPoint) {
            ClientConnectedEventHandler del = delegate(IPEndPoint end) {
                IPHostEntry host = Dns.GetHostEntry(end.Address);
                txtContent.Text +=
                    String.Format("System[{0}]: 远程主机{1}连接至本地。 ", DateTime.Now, end);
            };
            txtContent.Invoke(del, endPoint);
        }
        // 客户端连接断开
        void talker_ClientLost(string info) {
            ConnectionLostEventHandler del = delegate(string information) {
                txtContent.Text +=
                    String.Format("System[{0}]: {1} ", DateTime.Now, information);
            };
            txtContent.Invoke(del, info);
        }
        // 发送消息
        private void btnSend_Click(object sender, EventArgs e) {
            if (String.IsNullOrEmpty(txtMessage.Text)) {
                MessageBox.Show("请输入内容!");
                txtMessage.Clear();
                txtMessage.Focus();
                return;
            }
            Message msg = new Message(userName, txtMessage.Text);
            if (talker.SendMessage(msg)) {
                txtContent.Text += msg.ToString();
                txtMessage.Clear();
            } else {
                txtContent.Text +=
                    String.Format("System[{0}]: 远程主机已断开连接 ", DateTime.Now);
                DisconnectStatus();
            }
        }
        // 点击连接
        private void btnConnect_Click(object sender, EventArgs e) {
            string host = txtHost.Text;
            string ip = txtHost.Text;
            int port;
            if (String.IsNullOrEmpty(txtHost.Text)) {
                MessageBox.Show("主机名称或地址不能为空");
            }          
            try{
                port = Convert.ToInt32(txtPort.Text);
            }catch{
                MessageBox.Show("端口号不能为空,且必须为数字");
                return;
            }
           
            if (talker.ConnectByHost(host, port)) {
                ConnectStatus();
                txtContent.Text +=
                    String.Format("System[{0}]: 已成功连接至远程 ", DateTime.Now);
                return;
            }
               
            if(talker.ConnectByIp(ip, port)){
                ConnectStatus();
                txtContent.Text +=
                    String.Format("System[{0}]: 已成功连接至远程 ", DateTime.Now);
            }else{
                MessageBox.Show("远程主机不存在,或者拒绝连接!");
            }          
            txtMessage.Focus();
        }
        // 关闭按钮点按
        private void btnClose_Click(object sender, EventArgs e) {
            try {
                talker.Dispose();
                Application.Exit();
            } catch {
            }
        }
        // 直接点击右上角的叉
        private void PrimaryForm_FormClosing(object sender, FormClosingEventArgs e) {
            try {
                talker.Dispose();
                Application.Exit();
            } catch {
            }
        }
        // 点击注销
        private void btnSignout_Click(object sender, EventArgs e) {
            talker.SignOut();
            DisconnectStatus();
            txtContent.Text +=
                String.Format("System[{0}]: 已经注销 ",DateTime.Now);
        }
        private void btnClear_Click(object sender, EventArgs e) {
            txtContent.Clear();
        }
    }

    在上面代码中,分别通过四个方法订阅了四个事件,以实现自动通知的机制。最后需要注意的就是SignOut()和Dispose()的区分。SignOut()只是断开连接,Dispose()则是离开应用程序。

    总结

    这 篇文章简单地分析、设计及实现了一个聊天程序。这个程序只是对无服务器模式实现聊天的一个尝试。我们分析了需求,随后编写了几个用例,并对本地、远程的概 念做了定义,接着编写了程序接口并最终实现了它。这个程序还有很严重的不足:它无法实现自动上线通知,而必须要事先知道端口号并进行手动连接。为了实现一 个功能强大且开发容易的程序,更好的办法是使用集中型服务器模式。

    感谢阅读,希望这篇文章能对你有所帮助。

    [C#]C#网络编程基本概念和操作5

    mikel阅读(903)

    源码下载:http://www.tracefact.net/SourceCode/Network-Part5.rar

    C#网络编程(接收文件) – Part.5

    这篇文章将完成Part.4中剩余的部分,它们本来是一篇完整的文章,但是因为上一篇比较长,合并起来页数太多,浏览起来可能会比较不方便,我就将它拆为两篇了,本文便是它的后半部分。我们继续进行上一篇没有完成的步骤:客户端接收来自服务端的文件。

    4.客户端接收文件

    4.1服务端的实现

    对 于服务端,我们只需要实现上一章遗留的sendFile()方法就可以了,它起初在handleProtocol中是注释掉的。另外,由于创建连接、获取 流等操作与receiveFile()是没有区别的,所以我们将它提出来作为一个公共方法getStreamToClient()。下面是服务端的代码, 只包含新增改过的代码,对于原有方法我只给出了签名:

    class Server {
        static void Main(string[] args) {
            Console.WriteLine("Server is running … ");
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            TcpListener listener = new TcpListener(ip, 8500);
            listener.Start();           // 开启对控制端口 8500 的侦听
            Console.WriteLine("Start Listening …");
            while (true) {
                // 获取一个连接,同步方法,在此处中断
                TcpClient client = listener.AcceptTcpClient(); 
                RemoteClient wapper = new RemoteClient(client);
                wapper.BeginRead();
            }
        }
    }
    public class RemoteClient {
        // 字段 略

        public RemoteClient(TcpClient client) {}
        // 开始进行读取
        public void BeginRead() { }
        // 再读取完成时进行回调
        private void OnReadComplete(IAsyncResult ar) { }
        // 处理protocol
        private void handleProtocol(object obj) {
            string pro = obj as string;
            ProtocolHelper helper = new ProtocolHelper(pro);
            FileProtocol protocol = helper.GetProtocol();
            if (protocol.Mode == FileRequestMode.Send) {
                // 客户端发送文件,对服务端来说则是接收文件
                receiveFile(protocol);
            } else if (protocol.Mode == FileRequestMode.Receive) {
                // 客户端接收文件,对服务端来说则是发送文件
                sendFile(protocol);
            }
        }
        // 发送文件
        private void sendFile(FileProtocol protocol) {
            TcpClient localClient;
            NetworkStream streamToClient = getStreamToClient(protocol, out localClient);
            // 获得文件的路径
            string filePath = Environment.CurrentDirectory + "/" + protocol.FileName;
            // 创建文件流
            FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            byte[] fileBuffer = new byte[1024];     // 每次传1KB
            int bytesRead;
            int totalBytes = 0;
            // 创建获取文件发送状态的类
            SendStatus status = new SendStatus(filePath);
            // 将文件流转写入网络流
            try {
                do {
                    Thread.Sleep(10);           // 为了更好的视觉效果,暂停10毫秒:-)
                    bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);
                    streamToClient.Write(fileBuffer, 0, bytesRead);
                    totalBytes += bytesRead;            // 发送了的字节数
                    status.PrintStatus(totalBytes); // 打印发送状态
                } while (bytesRead > 0);
                Console.WriteLine("Total {0} bytes sent, Done!", totalBytes);
            } catch {
                Console.WriteLine("Server has lost…");
            }
            streamToClient.Dispose();
            fs.Dispose();
            localClient.Close();
        }
        // 接收文件
        private void receiveFile(FileProtocol protocol) { }
        // 获取连接到远程的流 — 公共方法
        private NetworkStream getStreamToClient(FileProtocol protocol, out TcpClient localClient) {
            // 获取远程客户端的位置
            IPEndPoint endpoint = client.Client.RemoteEndPoint as IPEndPoint;
            IPAddress ip = endpoint.Address;
            // 使用新端口号,获得远程用于接收文件的端口
            endpoint = new IPEndPoint(ip, protocol.Port);
            // 连接到远程客户端
            try {
                localClient = new TcpClient();
                localClient.Connect(endpoint);
            } catch {
                Console.WriteLine("无法连接到客户端 –> {0}", endpoint);
                localClient = null;
                return null;
            }
            // 获取发送文件的流
            NetworkStream streamToClient = localClient.GetStream();
            return streamToClient;
        }
        // 随机获取一个图片名称
        private string generateFileName(string fileName) {}
    }

    服务端的sendFile方法和客户端的SendFile()方法完全类似,上面的代码几乎是一次编写成功的。另外注意我将客户端使用的SendStatus类也拷贝到了服务端。接下来我们看下客户端。

    4.2客户端的实现

    首先要注意的是客户端的SendFile()接收的参数是文件全路径,但是在写入到协议时只获取了路径中的文件名称。这是因为服务端不需要知道文件在客户端的路径,所以协议中只写文件名;而为了使客户端的SendFile()方法更通用,所以它接收本地文件的全路径。

    客户端的ReceiveFile()的实现也和服务端的receiveFile()方法类似,同样,由于要保存到本地,为了避免文件名重复,我将服务端的generateFileName()方法复制了过来。

    public class ServerClient :IDisposable {
        // 字段略

        public ServerClient() {}
        // 发送消息到服务端
        public void SendMessage(string msg) {}
        // 发送文件 – 异步方法
        public void BeginSendFile(string filePath) {    }
        private void SendFile(object obj) { }
       
        // 发送文件 — 同步方法
        public void SendFile(string filePath) {}
       
        // 接收文件 — 异步方法
        public void BeginReceiveFile(string fileName) {
            ParameterizedThreadStart start =
                new ParameterizedThreadStart(ReceiveFile);
            start.BeginInvoke(fileName, null, null);
        }
        public void ReceiveFile(object obj) {
            string fileName = obj as string;
            ReceiveFile(fileName);
        }
        // 接收文件 — 同步方法
        public void ReceiveFile(string fileName) {
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            TcpListener listener = new TcpListener(ip, 0);
            listener.Start();
            // 获取本地侦听的端口号
            IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;
            int listeningPort = endPoint.Port;
            // 获取发送的协议字符串
            FileProtocol protocol =
                new FileProtocol(FileRequestMode.Receive, listeningPort, fileName);
            string pro = protocol.ToString();
            SendMessage(pro);       // 发送协议到服务端

            // 中断,等待远程连接
            TcpClient localClient = listener.AcceptTcpClient();
            Console.WriteLine("Start sending file…");
            NetworkStream stream = localClient.GetStream();
            // 获取文件保存的路劲
            string filePath =
                Environment.CurrentDirectory + "/" + generateFileName(fileName);
            // 创建文件流
            FileStream fs = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write);
            byte[] fileBuffer = new byte[1024];     // 每次传1KB
            int bytesRead;
            int totalBytes = 0;
            // 从缓存buffer中读入到文件流中
            do {
                bytesRead = stream.Read(buffer, 0, BufferSize);
                fs.Write(buffer, 0, bytesRead);
                totalBytes += bytesRead;
                Console.WriteLine("Receiving {0} bytes …", totalBytes);
            } while (bytesRead > 0);
            Console.WriteLine("Total {0} bytes received, Done!", totalBytes);
            fs.Dispose();          
            stream.Dispose();
            localClient.Close();
            listener.Stop();
        }
        // 随机获取一个图片名称
        private string generateFileName(string fileName) {}
        public void Dispose() {
            if (streamToServer != null)
                streamToServer.Dispose();
            if (client != null)
                client.Close();
        }
    }

    上面关键的一句就是创建协议那句,注意到将mode由Send改为了Receive,同时传去了想要接收的服务端的文件名称。

    4.3程序测试

    现在我们已经完成了所有收发文件的步骤,可以看到服务端的所有操作都是被动的,接下来我们修改客户端的Main()程序,创建一个菜单,然后根据用户输入发送或者接收文件。

    class Program {
        static void Main(string[] args) {
            ServerClient client = new ServerClient();
            string input;
            string path = Environment.CurrentDirectory + "/";
            do {
                Console.WriteLine("Send File:    S1 – Client01.jpg, S2 – Client02.jpg, S3 – Client03.jpg");
                Console.WriteLine("Receive File: R1 – Server01.jpg, R1 – Server02.jpg, R3- Server03.jpg");
                Console.WriteLine("Press 'Q' to exit. ");
                Console.Write("Enter your choice: ");
                input = Console.ReadLine();
                switch(input.ToUpper()){
                    case "S1":
                        client.BeginSendFile(path + "Client01.jpg");
                        break;
                    case "S2":
                        client.BeginSendFile(path + "Client02.jpg");
                        break;
                    case "S3":
                        client.BeginSendFile(path + "Client02.jpg");
                        break;
                    case "R1":
                        client.BeginReceiveFile("Server01.jpg");
                        break;
                    case "R2":
                        client.BeginReceiveFile("Server01.jpg");
                        break;
                    case "R3":
                        client.BeginReceiveFile("Server01.jpg");
                        break;
                }              
            } while (input.ToUpper() != "Q");
            client.Dispose();
        }
    }

    由于这是一个控制台应用程序,并且采用了异步操作,所以这个菜单的出现顺序有点混乱。我这里描述起来比较困难,你将代码下载下来后运行一下就知道了:-)

    程序的运行结果和上一节类似,这里我就不再贴图了。接下来是本系列的最后一篇,将发送字符串与传输文件的功能结合起来,创建一个可以发送消息并能收发文件的聊天程序,至于语音聊天嘛…等我学习了再告诉你 >_<、

    [C#]C#网络编程基本概念和操作4

    mikel阅读(809)

    源码下载:http://www.tracefact.net/SourceCode/Network-Part4.rar

    C#网络编程(订立协议和发送文件) – Part.4

    文件传输

    前面两篇文章所使用的范例都是传输字符串,有的时候我们可能会想在服务端和客户端之间传递文件。比如,考虑这样一种情况,假如客户端显示了一个菜 单,当我们输入S1、S2或S3(S为Send缩写)时,分别向服务端发送文件Client01.jpg、Client02.jpg、 Client03.jpg;当我们输入R1、R2或R3时(R为Receive缩写),则分别从服务端接收文件Server01.jpg、 Server02.jpg、Server03.jpg。那么,我们该如何完成这件事呢?此时可能有这样两种做法:

    • 类似于FTP协议,服务端开辟两个端口,并持续对这两个端口侦听:一个用于接收字符串,类似于FTP的控制端口,它接收各种命令(接收或发送文件);一个用于传输数据,也就是发送和接收文件。
    • 服务端只开辟一个端口,用于接收字符串,我们称之为控制端口。当接到请求之后,根据请求内容在客户端开辟一个端口专用于文件传输,并在传输结束后关闭端口。

    现在我们只关注于上面的数据端口,回忆一下在第二篇中我们所总结的,可以得出:当我们使用上面的方法一时,服务端的数据端口可以为多个客户端的多次 请求服务;当我们使用方法二时,服务端只为一个客户端的一次请求服务,但是因为每次请求都会重新开辟端口,所以实际上还是相当于可以为多个客户端的多次请 求服务。同时,因为它只为一次请求服务,所以我们在数据端口上传输文件时无需采用异步传输方式。但在控制端口我们仍然需要使用异步方式。

    从上面看出,第一种方式要好得多,但是我们将采用第二种方式。至于原因,你可以回顾一下Part.1(基本概念和操作)中关于聊天程序模式的讲述,因为接下来一篇文章我们将创建一个聊天程序,而这个聊天程序采用第三种模式,所以本文的练习实际是对下一篇的一个铺垫。

    1.订立协议

    1.1发送文件

    我们先看一下发送文件的情况,如果我们想将文件client01.jpg由客户端发往客户端,那么流程是什么:

    1. 客户端开辟数据端口用于侦听,并获取端口号,假设为8005。
    2. 假设客户端输入了S1,则发送下面的控制字符串到服务端:[file=Client01.jpg, mode=send, port=8005]。
    3. 服务端收到以后,根据客户端ip和端口号与该客户端建立连接。
    4. 客户端侦听到服务端的连接,开始发送文件。
    5. 传送完毕后客户端、服务端分别关闭连接。

    此时,我们订立的发送文件协议为:[file=Client01.jpg, mode=send, port=8005]。但是,由于它是一个普通的字符串,在上一篇中,我们采用了正则表达式来获取其中的有效值,但这显然不是一种好办法。因此,在本文及 下一篇文章中,我们采用一种新的方式来编写协议:XML。对于上面的语句,我们可以写成这样的XML:

    <protocol><file name="client01.jpg" mode="send" port="8005" /></protocol>

    这样我们在服务端就会好处理得多,接下来我们来看一下接收文件的流程及其协议。

    NOTE:这里说发送、接收文件是站在客户端的立场说的,当客户端发送文件时,对于服务器来收,则是接收文件。

    1.2接收文件

    接收文件与发送文件实际上完全类似,区别只是由客户端向网络流写入数据,还是由服务端向网络流写入数据。

    1. 客户端开辟数据端口用于侦听,假设为8006。
    2. 假设客户端输入了R1,则发送控制字符串:<protocol><file name="Server01.jpg" mode="receive" port="8006" /></protocol>到服务端。
    3. 服务端收到以后,根据客户端ip和端口号与该客户端建立连接。
    4. 客户端建立起与服务端的连接,服务端开始网络流中写入数据。
    5. 传送完毕后服务端、客户端分别关闭连接。

    2.协议处理类的实现

    和上面一章一样,在开始编写实际的服务端客户端代码之前,我们首先要编写处理协议的类,它需要提供这样两个功能:1、方便地帮我们获取完整的协议信 息,因为前面我们说过,服务端可能将客户端的多次独立请求拆分或合并。比如,客户端连续发送了两条控制信息到服务端,而服务端将它们合并了,那么则需要先 拆开再分别处理。2、方便地获取我们所想要的属性信息,因为协议是XML格式,所以还需要一个类专门对XML进行处理,获得字符串的属性值。

    2.1 ProtocalHandler辅助类

    我们先看下ProtocalHandler,它与上一篇中的RequestHandler作用相同。需要注意的是必须将它声明为实例的,而非静态 的,这是因为每个TcpClient都需要对应一个ProtocalHandler,因为它内部维护的patialProtocal不能共享,在协议发送 不完整的情况下,这个变量用于临时保存被截断的字符串。

    public class ProtocolHandler {
        private string partialProtocal; // 保存不完整的协议
       
        public ProtocolHandler() {
            partialProtocal = "";      
        }
        public string[] GetProtocol(string input) {
            return GetProtocol(input, null);
        }
       
        // 获得协议
        private string[] GetProtocol(string input, List<string> outputList) {
            if (outputList == null)
                outputList = new List<string>();
            if (String.IsNullOrEmpty(input))
                return outputList.ToArray();
            if (!String.IsNullOrEmpty(partialProtocal))
                input = partialProtocal + input;
            string pattern = "(^<protocol>.*?</protocol>)";
            // 如果有匹配,说明已经找到了,是完整的协议
            if (Regex.IsMatch(input, pattern)) {
                // 获取匹配的值
                string match = Regex.Match(input, pattern).Groups[0].Value;
                outputList.Add(match);
                partialProtocal = "";
                // 缩短input的长度
                input = input.Substring(match.Length);
                // 递归调用
                GetProtocol(input, outputList);
            } else {
                // 如果不匹配,说明协议的长度不够,
                // 那么先缓存,然后等待下一次请求
                partialProtocal = input;
            }
            return outputList.ToArray();
        }
    }

    因为现在它已经不是本文的重点了,所以我就不演示对于它的测试了,本文所附带的代码中含有它的测试代码(我在ProtocolHandler中添加了一个静态类Test())。

    2.2 FileRequestType枚举和FileProtocol结构

    因为XML是以字符串的形式在进行传输,为了方便使用,我们最好构建一个强类型来对它们进行操作,这样会方便很多。我们首先可以定义FileRequestMode枚举,它代表是发送还是接收文件:

    public enum FileRequestMode {
        Send = 0,
        Receive
    }

    接下来我们再定义一个FileProtocol结构,用来为整个协议字符串提供强类型的访问,注意这里覆盖了基类的ToString()方法,这样在客户端我们就不需要再手工去编写XML,只要在结构值上调用ToString()就OK了,会方便很多。

    public struct FileProtocol {
        private readonly FileRequestMode mode;
        private readonly int port;
        private readonly string fileName;
        public FileProtocol
            (FileRequestMode mode, int port, string fileName) {
            this.mode = mode;
            this.port = port;
            this.fileName = fileName;
        }
        public FileRequestMode Mode {
            get { return mode; }
        }
        public int Port {
            get { return port; }
        }
        public string FileName {
            get { return fileName; }
        }
        public override string ToString() {
            return String.Format("<protocol><file name=\"{0}\" mode=\"{1}\" port=\"{2}\" /></protocol>", fileName, mode, port);
        }
    }

    2.3 ProtocolHelper辅助类

    这个类专用于将XML格式的协议映射为我们上面定义的强类型对象,这里我没有加入try/catch异常处理,因为协议对用户来说是不可见的,而且客户端应该总是发送正确的协议,我觉得这样可以让代码更加清晰:

    public class ProtocolHelper {
        private XmlNode fileNode;
        private XmlNode root;
       
        public ProtocolHelper(string protocol) {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(protocol);
            root = doc.DocumentElement;
            fileNode = root.SelectSingleNode("file");
        }
        // 此时的protocal一定为单条完整protocal
        private FileRequestMode GetFileMode() {
            string mode = fileNode.Attributes["mode"].Value;
            mode = mode.ToLower();
            if (mode == "send")
                return FileRequestMode.Send;
            else
                return FileRequestMode.Receive;
        }
        // 获取单条协议包含的信息
        public FileProtocol GetProtocol() {
            FileRequestMode mode = GetFileMode();
            string fileName = "";
            int port = 0;
            fileName = fileNode.Attributes["name"].Value;
            port = Convert.ToInt32(fileNode.Attributes["port"].Value);
            return new FileProtocol(mode, port, fileName);
        }
    }

    OK,我们又耽误了点时间,下面就让我们进入正题吧。

    3.客户端发送数据

    3.1 服务端的实现

    我们还是将一个问题分成两部分来处理,先是发送数据,然后是接收数据。我们先看发送数据部分的服务端。如果你从第一篇文章看到了现在,那么我觉得更多的不是技术上的问题而是思路,所以我们不再将重点放到代码上,这些应该很容易就看懂了。

    class Server {
        static void Main(string[] args) {
            Console.WriteLine("Server is running … ");
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            TcpListener listener = new TcpListener(ip, 8500);
            listener.Start();           // 开启对控制端口 8500 的侦听
            Console.WriteLine("Start Listening …");
            while (true) {
                // 获取一个连接,同步方法,在此处中断
                TcpClient client = listener.AcceptTcpClient();             
                RemoteClient wapper = new RemoteClient(client);
                wapper.BeginRead();
            }
        }
    }
    public class RemoteClient {
        private TcpClient client;
        private NetworkStream streamToClient;
        private const int BufferSize = 8192;
        private byte[] buffer;
        private ProtocolHandler handler;
       
        public RemoteClient(TcpClient client) {
            this.client = client;
            // 打印连接到的客户端信息
            Console.WriteLine("\nClient Connected!{0} <– {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
            // 获得流
            streamToClient = client.GetStream();
            buffer = new byte[BufferSize];
            handler = new ProtocolHandler();
        }
        // 开始进行读取
        public void BeginRead() {      
            AsyncCallback callBack = new AsyncCallback(OnReadComplete);
            streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
        }
        // 再读取完成时进行回调
        private void OnReadComplete(IAsyncResult ar) {
            int bytesRead = 0;
            try {
                lock (streamToClient) {
                    bytesRead = streamToClient.EndRead(ar);
                    Console.WriteLine("Reading data, {0} bytes …", bytesRead);
                }
                if (bytesRead == 0) throw new Exception("读取到0字节");
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Array.Clear(buffer,0,buffer.Length);        // 清空缓存,避免脏读

                // 获取protocol数组
                string[] protocolArray = handler.GetProtocol(msg);
                foreach (string pro in protocolArray) {
                    // 这里异步调用,不然这里可能会比较耗时
                    ParameterizedThreadStart start =
                        new ParameterizedThreadStart(handleProtocol);
                    start.BeginInvoke(pro, null, null);
                }
                // 再次调用BeginRead(),完成时调用自身,形成无限循环
                lock (streamToClient) {
                    AsyncCallback callBack = new AsyncCallback(OnReadComplete);
                    streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
                }
            } catch(Exception ex) {
                if(streamToClient!=null)
                    streamToClient.Dispose();
                client.Close();
                Console.WriteLine(ex.Message);      // 捕获异常时退出程序
            }
        }
        // 处理protocol
        private void handleProtocol(object obj) {
            string pro = obj as string;
            ProtocolHelper helper = new ProtocolHelper(pro);
            FileProtocol protocol = helper.GetProtocol();
            if (protocol.Mode == FileRequestMode.Send) {
                // 客户端发送文件,对服务端来说则是接收文件
                receiveFile(protocol);
            } else if (protocol.Mode == FileRequestMode.Receive) {
                // 客户端接收文件,对服务端来说则是发送文件
                // sendFile(protocol);
            }
        }
        private void receiveFile(FileProtocol protocol) {
            // 获取远程客户端的位置
            IPEndPoint endpoint = client.Client.RemoteEndPoint as IPEndPoint;
            IPAddress ip = endpoint.Address;
           
            // 使用新端口号,获得远程用于接收文件的端口
            endpoint = new IPEndPoint(ip, protocol.Port);
            // 连接到远程客户端
            TcpClient localClient;
            try {
                localClient = new TcpClient();
                localClient.Connect(endpoint);
            } catch {
                Console.WriteLine("无法连接到客户端 –> {0}", endpoint);
                return;
            }
            // 获取发送文件的流
            NetworkStream streamToClient = localClient.GetStream();
            // 随机生成一个在当前目录下的文件名称
            string path =
                Environment.CurrentDirectory + "/" + generateFileName(protocol.FileName);
            byte[] fileBuffer = new byte[1024]; // 每次收1KB
            FileStream fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write);
            // 从缓存buffer中读入到文件流中
            int bytesRead;
            int totalBytes = 0;
            do {
                bytesRead = streamToClient.Read(buffer, 0, BufferSize);            
                fs.Write(buffer, 0, bytesRead);
                totalBytes += bytesRead;
                Console.WriteLine("Receiving {0} bytes …", totalBytes);
            } while (bytesRead > 0);
            Console.WriteLine("Total {0} bytes received, Done!", totalBytes);
            streamToClient.Dispose();
            fs.Dispose();
            localClient.Close();
        }
        // 随机获取一个图片名称
        private string generateFileName(string fileName) {
            DateTime now = DateTime.Now;
            return String.Format(
                "{0}_{1}_{2}_{3}", now.Minute, now.Second, now.Millisecond, fileName
            );
        }
    }

    这里应该没有什么新知识,需要注意的地方有这么几个:

    • 在OnReadComplete()回调方法中的foreach循环,我们使用委托异步调用了handleProtocol()方法,这是因为handleProtocol即将执行的是一个读取或接收文件的操作,也就是一个相对耗时的操作。
    • 在handleProtocol()方法中,我们深切体会了定义ProtocolHelper类和FileProtocol结构的好处。如果没有定义它们,这里将是不堪入目的处理XML以及类型转换的代码。
    • handleProtocol()方法中进行了一个条件判断,注意sendFile()方法我屏蔽掉了,这个还没有实现,但是我想你已经猜到它将是后面要实现的内容。
    • receiveFile()方法是实际接收客户端发来文件的方法,这里没有什么特别之处。需要注意的是文件存储的路径,它保存在了当前程序执行的目录下,文件的名称我使用generateFileName()生成了一个与时间有关的随机名称。

    3.2客户端的实现

    我们现在先不着急实现客户端S1、R1等用户菜单,首先完成发送文件这一功能,实际上,就是为上一节SendMessage()加一个姐妹方法SendFile()。

    class Client {
        static void Main(string[] args) {
            ConsoleKey key;
            ServerClient client = new ServerClient();
            string filePath = Environment.CurrentDirectory + "/" + "Client01.jpg";
            if(File.Exists(filePath))
                client.BeginSendFile(filePath);
           
            Console.WriteLine("\n\n输入\"Q\"键退出。");
            do {
                key = Console.ReadKey(true).Key;
            } while (key != ConsoleKey.Q);
        }
    }
    public class ServerClient {
        private const int BufferSize = 8192;
        private byte[] buffer;
        private TcpClient client;
        private NetworkStream streamToServer;
        public ServerClient() {
            try {
                client = new TcpClient();
                client.Connect("localhost", 8500);      // 与服务器连接
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                return;
            }
            buffer = new byte[BufferSize];
            // 打印连接到的服务端信息
            Console.WriteLine("Server Connected!{0} –> {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
            streamToServer = client.GetStream();
        }
        // 发送消息到服务端
        public void SendMessage(string msg) {
            byte[] temp = Encoding.Unicode.GetBytes(msg);   // 获得缓存
            try {
                lock (streamToServer) {
                    streamToServer.Write(temp, 0, temp.Length); // 发往服务器
                }
                Console.WriteLine("Sent: {0}", msg);
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                return;
            }
        }
        // 发送文件 – 异步方法
        public void BeginSendFile(string filePath) {
            ParameterizedThreadStart start =
                new ParameterizedThreadStart(BeginSendFile);
            start.BeginInvoke(filePath, null, null);
        }
        private void BeginSendFile(object obj) {
            string filePath = obj as string;
            SendFile(filePath);
        }
        // 发送文件 — 同步方法
        public void SendFile(string filePath) {
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            TcpListener listener = new TcpListener(ip, 0);
            listener.Start();
            // 获取本地侦听的端口号
            IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;
            int listeningPort = endPoint.Port;
            // 获取发送的协议字符串
            string fileName = Path.GetFileName(filePath);
            FileProtocol protocol =
                new FileProtocol(FileRequestMode.Send, listeningPort, fileName);
            string pro = protocol.ToString();
            SendMessage(pro);       // 发送协议到服务端

            // 中断,等待远程连接
            TcpClient localClient = listener.AcceptTcpClient();
            Console.WriteLine("Start sending file…");
            NetworkStream stream = localClient.GetStream();
            // 创建文件流
            FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);          
            byte[] fileBuffer = new byte[1024];     // 每次传1KB
            int bytesRead;
            int totalBytes = 0;
            // 创建获取文件发送状态的类
            SendStatus status = new SendStatus(filePath);
            // 将文件流转写入网络流
            try {
                do {
                    Thread.Sleep(10);           // 为了更好的视觉效果,暂停10毫秒:-)
                    bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);                 
                    stream.Write(fileBuffer, 0, bytesRead);
                    totalBytes += bytesRead;            // 发送了的字节数
                    status.PrintStatus(totalBytes); // 打印发送状态
                } while (bytesRead > 0);
                Console.WriteLine("Total {0} bytes sent, Done!", totalBytes);
            } catch {
                Console.WriteLine("Server has lost…");
            }
           
            stream.Dispose();
            fs.Dispose();
            localClient.Close();
            listener.Stop();
        }
    }

    接下来我们来看下这段代码,有这么两点需要注意一下:

    • 在Main()方法中可以看到,图片的位置为应用程序所在的目录,如果你跟我一样处于调试模式,那么就在解决方案的Bin目录下的Debug目录中放置三张图片Client01.jpg、Client02.jpg、Client03.jpg,用来发往服务端。
    • 我在客户端提供了两个SendFile()方法,和一个BeginSendFile()方法,分别用于同步和异步传输,其中私有的SendFile()方法只是一个辅助方法。实际上对于发送文件这样的操作我们几乎总是需要使用异步操作。
    • SendMessage()方法中给streamToServer加锁很重要,因为SendFile()方法是多线程访问的,而在SendFile()方法中又调用了SendMessage()方法。
    • 我另外编写了一个SendStatus类,它用来记录和打印发送完成的状态,已经发送了多少字节,完成度是百分之多少,等等。本来这个类的内容我 是直接写入在Client类中的,后来我觉得它执行的工作已经不属于Client本身所应该执行的领域之内了,我记得这样一句话:当你觉得类中的方法与类的名称不符的时候,那么就应该考虑重新创建一个类。我觉得用在这里非常恰当。

    下面是SendStatus的内容:

    // 即时计算发送文件的状态
    public class SendStatus {
        private FileInfo info;
        private long fileBytes;
        public SendStatus(string filePath) {
            info = new FileInfo(filePath);
            fileBytes = info.Length;
        }
        public void PrintStatus(int sent) {
            string percent = GetPercent(sent);
            Console.WriteLine("Sending {0} bytes, {1}% …", sent, percent);
        }
        // 获得文件发送的百分比
        public string GetPercent(int sent){    
            decimal allBytes = Convert.ToDecimal(fileBytes);
            decimal currentSent = Convert.ToDecimal(sent);
            decimal percent = (currentSent / allBytes) * 100;
            percent = Math.Round(percent, 1);   //保留一位小数
           
            if (percent.ToString() == "100.0")
                return "100";
            else
                return percent.ToString();
        }
    }

    3.3程序测试

    接下里我们运行一下程序,来检查一下输出,首先看下服务端:

    接着是客户端,我们能够看到发送的字节数和进度,可以想到如果是图形界面,那么我们可以通过扩展SendStatus类来创建一个进度条:

    最后我们看下服务端的Bin\Debug目录,应该可以看到接收到的图片:

    本来我想这篇文章就可以完成发送和接收,不过现在看来没法实现了,因为如果继续下去这篇文章就太长了,我正尝试着尽量将文章控制在15页以内。那么我们将在下篇文章中再完成接收文件这一部分。

    [C#]C#网络编程基本概念和操作3

    mikel阅读(924)

    C#网络编程(异步传输字符串) – Part.3

    源码下载:http://www.tracefact.net/SourceCode/Network-Part3.rar

    这篇文章我们将前进一大步,使用异步的方式来对服务端编程,以使它成为一个真正意义上的服务器:可以为多个客户端的多次请求服务。但是开始之前,我们需要解决上一节中遗留的一个问题。

    消息发送时的问题

    这个问题就是:客户端分两次向流中写入数据(比如字符串)时,我们主观上将这两次写入视为两次请求;然而服务端有可能将这两次合起来视为一条请求, 这在两个请求间隔时间比较短的情况下尤其如此。同样,也有可能客户端发出一条请求,但是服务端将其视为两条请求处理。下面列出了可能的情况,假设我们在客 户端连续发送两条“Welcome to Tracefact.net!”,则数据到达服务端时可能有这样三种情况:

    NOTE:在这里我们假设采用ASCII编码方式,因为此时上面的一个方框正好代表一个字节,而字符串到达末尾后为持续的0(因为byte是值类型,且最小为0)。

    上面的第一种情况是最理想的情况,此时两条消息被视为两个独立请求由服务端完整地接收。第二种情况的示意图如下,此时一条消息被当作两条消息接收了:

    而对于第三种情况,则是两条消息被合并成了一条接收:

    如果你下载了上一篇文章所附带的源码,那么将Client2.cs进行一下修改,不通过用户输入,而是使用一个for循环连续的发送三个请求过去,这样会使请求的间隔时间更短,下面是关键代码:

    string msg = "Welcome to TraceFact.Net!";
    for (int i = 0; i <= 2; i++) {
        byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 获得缓存
        try {
            streamToServer.Write(buffer, 0, buffer.Length); // 发往服务器
            Console.WriteLine("Sent: {0}", msg);
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            break;
        }
    }

    运行服务端,然后再运行这个客户端,你可能会看到这样的结果:

    可以看到,尽管上面将消息分成了三条单独发送,但是服务端却将后两条合并成了一条。对于这些情况,我们可以这样处理:就好像HTTP协议一样,在实 际的请求和应答内容之前包含了HTTP头,其中是一些与请求相关的信息。我们也可以订立自己的协议,来解决这个问题,比如说,对于上面的情况,我们就可以 定义这样一个协议:

    [length=XXX]:其中xxx是实际发送的字符串长度(注意不是字节数组buffer的长度),那么对于上面的请求,则我们发送的数据 为:“[length=25]Welcome to TraceFact.Net!”。而服务端接收字符串之后,首先读取这个“元数据”的内容,然后再根据“元数据”内容来读取实际的数据,它可能有下面这样 两种情况:

    NOTE:我觉得这里借用“元数据”这个术语还算比较恰当,因为“元数据”就是用来描述数据的数据。

    • “[“”]”中括号是完整的,可以读取到length的字节数。然后根据这个数值与后面的字符串长度相比,如果相等,则说明发来了一条完整信息; 如果多了,那么说明接收的字节数多了,取出合适的长度,并将剩余的进行缓存;如果少了,说明接收的不够,那么将收到的进行一个缓存,等待下次请求,然后将 两条合并。
    • “[”“]”中括号本身就不完整,此时读不到length的值,因为中括号里的内容被截断了,那么将读到的数据进行缓存,等待读取下次发送来的数据,然后将两次合并之后再按上面的方式进行处理。

    接下来我们来看下如何来进行实际的操作,实际上,这个问题已经不属于C#网络编程的内容了,而完全是对字符串的处理。所以我们不再编写服务端/客户端代码,直接编写处理这几种情况的方法:

    public class RequestHandler {
        private string temp = string.Empty;
        public string[] GetActualString(string input) {
            return GetActualString(input, null);
        }
        private string[] GetActualString(string input, List<string> outputList) {
            if (outputList == null)
                outputList = new List<string>();
            if (!String.IsNullOrEmpty(temp))
                input = temp + input;
            string output = "";
            string pattern = @"(?<=^\[length=)(\d+)(?=\])";
            int length;
                       
            if (Regex.IsMatch(input, pattern)) {
                Match m = Regex.Match(input, pattern);
                // 获取消息字符串实际应有的长度
                length = Convert.ToInt32(m.Groups[0].Value);
                // 获取需要进行截取的位置
                int startIndex = input.IndexOf(']') + 1;
                // 获取从此位置开始后所有字符的长度
                output = input.Substring(startIndex);
                if (output.Length == length) {
                    // 如果output的长度与消息字符串的应有长度相等
                    // 说明刚好是完整的一条信息
                    outputList.Add(output);
                    temp = "";
                } else if (output.Length < length) {
                    // 如果之后的长度小于应有的长度,
                    // 说明没有发完整,则应将整条信息,包括元数据,全部缓存
                    // 与下一条数据合并起来再进行处理
                    temp = input;
                    // 此时程序应该退出,因为需要等待下一条数据到来才能继续处理

                } else if (output.Length > length) {
                    // 如果之后的长度大于应有的长度,
                    // 说明消息发完整了,但是有多余的数据
                    // 多余的数据可能是截断消息,也可能是多条完整消息

                    // 截取字符串
                    output = output.Substring(0, length);
                    outputList.Add(output);
                    temp = "";
                    // 缩短input的长度
                    input = input.Substring(startIndex + length);
                    // 递归调用
                    GetActualString(input, outputList);
                }
            } else {    // 说明“[”,“]”就不完整
                temp = input;
            }
            return outputList.ToArray();
        }
    }

    这个方法接收一个满足协议格式要求的输入字符串,然后返回一个数组,这是因为如果出现多次请求合并成一个发送过来的情况,那么就将它们全部返回。随 后简单起见,我在这个类中添加了一个静态的Test()方法和PrintOutput()帮助方法,进行了一个简单的测试,注意我直接输入了 length=13,这个是我提前计算好的。

    public static void Test() {
        RequestHandler handler = new RequestHandler();
        string input;
        // 第一种情况测试 – 一条消息完整发送
        input = "[length=13]明天中秋,祝大家节日快乐!";
        handler.PrintOutput(input);
        // 第二种情况测试 – 两条完整消息一次发送
        input = "明天中秋,祝大家节日快乐!";
        input = String.Format
            ("[length=13]{0}[length=13]{0}", input);
        handler.PrintOutput(input);
        // 第三种情况测试A – 两条消息不完整发送
        input = "[length=13]明天中秋,祝大家节日快乐![length=13]明天中秋";
        handler.PrintOutput(input);
        input = ",祝大家节日快乐!";
        handler.PrintOutput(input);
        // 第三种情况测试B – 两条消息不完整发送
        input = "[length=13]明天中秋,祝大家";
        handler.PrintOutput(input);
        input = "节日快乐![length=13]明天中秋,祝大家节日快乐!";
        handler.PrintOutput(input);
       
        // 第四种情况测试 – 元数据不完整
        input = "[leng";
        handler.PrintOutput(input);     // 不会有输出

        input = "th=13]明天中秋,祝大家节日快乐!";
        handler.PrintOutput(input);
    }
    // 用于测试输出
    private void PrintOutput(string input) {
        Console.WriteLine(input);
        string[] outputArray = GetActualString(input);
        foreach (string output in outputArray) {
            Console.WriteLine(output);
        }
        Console.WriteLine();
    }

    运行上面的程序,可以得到如下的输出:

    OK,从上面的输出可以看到,这个方法能够满足我们的要求。对于这篇文章最开始提出的问题,可以很轻松地通过加入这个方法来解决,这里就不再演示 了,但在本文所附带的源代码含有修改过的程序。在这里花费了很长的时间,接下来让我们回到正题,看下如何使用异步方式完成上一篇中的程序吧。

    异步传输字符串

    在上一篇中,我们由简到繁,提到了服务端的四种方式:服务一个客户端的一个请求、服务一个客户端的多个请求、服务多个客户端的一个请求、服务多个客 户端的多个请求。我们说到可以将里层的while循环交给一个新建的线程去让它来完成。除了这种方式以外,我们还可以使用一种更好的方式――使用线程池中 的线程来完成。我们可以使用BeginRead()、BeginWrite()等异步方法,同时让这BeginRead()方法和它的回调方法形成一个类 似于while的无限循环:首先在第一层循环中,接收到一个客户端后,调用BeginRead(),然后为该方法提供一个读取完成后的回调方法,然后在回 调方法中对收到的字符进行处理,随后在回调方法中接着调用BeginRead()方法,并传入回调方法本身。

    由于程序实现功能和上一篇完全相同,我就不再细述了。而关于异步调用方法更多详细内容,可以参见 C#中的委托和事件(续)

    1.服务端的实现

    当程序越来越复杂的时候,就需要越来越高的抽象,所以从现在起我们不再把所有的代码全部都扔进Main()里,这次我创建了一个RemoteClient类,它对于服务端获取到的TcpClient进行了一个包装:

    public class RemoteClient {
        private TcpClient client;
        private NetworkStream streamToClient;
        private const int BufferSize = 8192;
        private byte[] buffer;
        private RequestHandler handler;
       
        public RemoteClient(TcpClient client) {
            this.client = client;
            // 打印连接到的客户端信息
            Console.WriteLine("\nClient Connected!{0} <– {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
            // 获得流
            streamToClient = client.GetStream();
            buffer = new byte[BufferSize];
            // 设置RequestHandler
            handler = new RequestHandler();
            // 在构造函数中就开始准备读取
            AsyncCallback callBack = new AsyncCallback(ReadComplete);
            streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
        }
        // 再读取完成时进行回调
        private void ReadComplete(IAsyncResult ar) {
            int bytesRead = 0;
            try {
                lock (streamToClient) {
                    bytesRead = streamToClient.EndRead(ar);
                    Console.WriteLine("Reading data, {0} bytes …", bytesRead);
                }
                if (bytesRead == 0) throw new Exception("读取到0字节");
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Array.Clear(buffer,0,buffer.Length);        // 清空缓存,避免脏读
           
                string[] msgArray = handler.GetActualString(msg);   // 获取实际的字符串

                // 遍历获得到的字符串
                foreach (string m in msgArray) {
                    Console.WriteLine("Received: {0}", m);
                    string back = m.ToUpper();
                    // 将得到的字符串改为大写并重新发送
                    byte[] temp = Encoding.Unicode.GetBytes(back);
                    streamToClient.Write(temp, 0, temp.Length);
                    streamToClient.Flush();
                    Console.WriteLine("Sent: {0}", back);
                }              
                // 再次调用BeginRead(),完成时调用自身,形成无限循环
                lock (streamToClient) {
                    AsyncCallback callBack = new AsyncCallback(ReadComplete);
                    streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
                }
            } catch(Exception ex) {
                if(streamToClient!=null)
                    streamToClient.Dispose();
                client.Close();
                Console.WriteLine(ex.Message);      // 捕获异常时退出程序             
            }
        }
    }

    随后,我们在主程序中仅仅创建TcpListener类型实例,由于RemoteClient类在构造函数中已经完成了初始化的工作,所以我们在下面的while循环中我们甚至不需要调用任何方法:

    class Server {
        static void Main(string[] args) {
            Console.WriteLine("Server is running … ");
            IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
            TcpListener listener = new TcpListener(ip, 8500);
            listener.Start();           // 开始侦听
            Console.WriteLine("Start Listening …");
            while (true) {
                // 获取一个连接,同步方法,在此处中断
                TcpClient client = listener.AcceptTcpClient();             
                RemoteClient wapper = new RemoteClient(client);
            }
        }
    }

    好了,服务端的实现现在就完成了,接下来我们再看一下客户端的实现:

    2.客户端的实现

    与服务端类似,我们首先对TcpClient进行一个简单的包装,使它的使用更加方便一些,因为它是服务端的客户,所以我们将类的名称命名为ServerClient:

    public class ServerClient {
        private const int BufferSize = 8192;
        private byte[] buffer;
        private TcpClient client;
        private NetworkStream streamToServer;
        private string msg = "Welcome to TraceFact.Net!";
        public ServerClient() {
            try {
                client = new TcpClient();
                client.Connect("localhost", 8500);      // 与服务器连接
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                return;
            }
            buffer = new byte[BufferSize];
            // 打印连接到的服务端信息
            Console.WriteLine("Server Connected!{0} –> {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
            streamToServer = client.GetStream();
        }
        // 连续发送三条消息到服务端
        public void SendMessage(string msg) {
            msg = String.Format("[length={0}]{1}", msg.Length, msg);
            for (int i = 0; i <= 2; i++) {
                byte[] temp = Encoding.Unicode.GetBytes(msg);   // 获得缓存
                try {
                    streamToServer.Write(temp, 0, temp.Length); // 发往服务器
                    Console.WriteLine("Sent: {0}", msg);
                } catch (Exception ex) {
                    Console.WriteLine(ex.Message);
                    break;
                }
            }
            lock (streamToServer) {
                AsyncCallback callBack = new AsyncCallback(ReadComplete);
                streamToServer.BeginRead(buffer, 0, BufferSize, callBack, null);
            }
        }
        public void SendMessage() {
            SendMessage(this.msg);
        }
        // 读取完成时的回调方法
        private void ReadComplete(IAsyncResult ar) {
            int bytesRead;
            try {
                lock (streamToServer) {
                    bytesRead = streamToServer.EndRead(ar);
                }
                if (bytesRead == 0) throw new Exception("读取到0字节");
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Received: {0}", msg);
                Array.Clear(buffer, 0, buffer.Length);      // 清空缓存,避免脏读

                lock (streamToServer) {
                    AsyncCallback callBack = new AsyncCallback(ReadComplete);
                    streamToServer.BeginRead(buffer, 0, BufferSize, callBack, null);
                }
            } catch (Exception ex) {
                if(streamToServer!=null)
                    streamToServer.Dispose();
                client.Close();
                Console.WriteLine(ex.Message);
            }
        }
    }

    在上面的SendMessage()方法中,我们让它连续发送了三条同样的消息,这么仅仅是为了测试,因为异步操作同样会出现上面说过的:服务器将 客户端的请求拆开了的情况。最后我们在Main()方法中创建这个类型的实例,然后调用SendMessage()方法进行测试:

    class Client {
        static void Main(string[] args) {
            ConsoleKey key;
            ServerClient client = new ServerClient();
            client.SendMessage();
           
            Console.WriteLine("\n\n输入\"Q\"键退出。");
            do {
                key = Console.ReadKey(true).Key;
            } while (key != ConsoleKey.Q);
        }
    }

    是不是感觉很清爽?因为良好的代码重构,使得程序在复杂程度提高的情况下依然可以在一定程度上保持良好的阅读性。

    3.程序测试

    最后一步,我们先运行服务端,接着连续运行两个客户端,看看它们的输出分别是什么:

    大家可以看到,在服务端,我们可以连接多个客户端,同时为它们服务;除此以外,由接收的字节数发现,两个客户端均有两个请求被服务端合并成了一条请求,因为我们在其中加入了特殊的协议,所以在服务端可以对这种情况进行良好的处理。

    在客户端,我们没有采取类似的处理,所以当客户端收到应答时,仍然会发生请求合并的情况。对于这种情况,我想大家已经知道该如何处理了,就不再多费口舌了。

    使用这种定义协议的方式有它的优点,但缺点也很明显,如果客户知道了这个协议,有意地输入[length=xxx],但是后面的长度却不匹配,此时 程序就会出错。可选的解决办法是对“[”和“]”进行编码,当客户端有意输入这两个字符时,我们将它替换成“\[”和“\]”或者别的字符,在读取后再将 它还原。

    关于这个范例就到此结束了,剩下的两个范例都将采用异步传输的方式,并且会加入更多的协议内容。下一篇我们将介绍如何向服务端发送或接收文件。