最近一直在想实体类应不应该具有操作,还是如以往的一样是缺血模式的实体类呢,目前KiWing框架用的是缺血模式的实体类(缺血实体类指那些只有属性而没有方法的实体类),于是将现有的实体类进行了改写,代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using KiWing.CustomAttribute;
using KiWing.Helper.Attribute;
namespace KiWing.Test
{
class Program
{
[Table("Table1","Identifier","table1Insert","table1update","table1delete","table1select")]
public class Test
{
/// <summary>
/// 委托保存方法列表
/// </summary>
private SaveMethod saveList;
public int Identifier { get; set; }
public string TestName { get; set; }
/// <summary>
/// 委托保存方法
/// </summary>
/// <param name="test"></param>
/// <returns></returns>
public delegate int SaveMethod(Test test);
/// <summary>
/// 注册保存委托方法
/// </summary>
/// <param name="method"></param>
public void AddSaveMethod(SaveMethod method)
{
saveList += method;
}
/// <summary>
/// 删除已经注册的委托方法
/// </summary>
/// <param name="method"></param>
public void RemoveSaveMethod(SaveMethod method)
{
saveList -= method;
}
/// <summary>
/// 保存方法
/// </summary>
/// <returns></returns>
public int Save()
{
if (saveList != null)
return saveList(this);
return 1;
}
}
public class Area
{
}
/// <summary>
/// 业务处理类
/// </summary>
public class Business
{
public int Save<T>(T obj) where T : class
{
Console.Write("Business Save {0}!",typeof(T).Name);
return 1;
}
}
static void Main(string[] args)
{
//实例化实体类
Test test = new Test();
//属性赋值
test.Identifier = 1;
test.TestName="Test";
//创建业务逻辑对象
Business business=new Business();
//注册委托方法
test.AddSaveMethod(business.Save<Test>);
//保存数据
test.Save();
Console.Read();
}
}
}
[图书]AIR Bible (download)
Download AIR Bible
下载 AIR 圣经
RIAbook Rank:★★★★

简介 Book Description:
Adobe Integrated Runtime, or AIR, enables developers to create desktop applications using HTML,JavaScript, and ActionScript. These applications are able to run on Windows, Mac OS X, and Linux systems, meaning that Web developers will be able to use familiar languages and tools to easily create desktop software.
A Web application can look the same to every user, on any computer, because the same code is being executed to create the interface. The browser application itself handles the differences between operating systems, which allows code to execute in the same way on a wide variety of machines. A desktop application, on the other hand, starts up quickly because it is run directly from the user’s computer, accesses data quickly because it can store data locally, does not require an Internet connection to run, and is not constrained by the browser window.
Consider the current market of e-mail applications. If you use a Web application for your e-mail,you will be able to access the interface from any computer, and possibly even some mobile devices.
These applications have become very popular as a result, but there are still drawbacks to using a Web application over a desktop application. For example, if you want to find an e-mail you received last week or last month, you often need to page through old messages to find the right place in a Web application. This is a necessity because the data is stored remotely, so the amount of data passed to the browser must be constrained. In a desktop application, messages can be stored locally, and you can easily scroll down to find an older message.
Clearly there are uses for both Web applications and desktop applications. With AIR, there is now a way to use the same interface in both environments. While there may need to be some differences between a Web implementation and a desktop implementation in order to take full advantage of those environments, there is a lot to be gained from not having to create an entirely new application for each environment. AIR, along with other recent developments that enable Web applications to run on the desktop, blurs the line between Web and desktop applications, and it will raise user expectations on both.
One of the most powerful features of Web development languages is that they are high-level scripting languages designed for developing presentation layers. HTML isn’t able to manage memory or access low-level operating system APIs; it was never intended to do such things. Instead, a browser interprets HTML, and the developers of that browser’s engine focus on those things. This allows developers of the higher-level language to focus their efforts on the user experience and the business logic of an application.
In the world of desktop applications, C, C++, Cocoa, Java, and .NET are considered high-level languages.
There is no question that scripting languages like ActionScript and JavaScript are less powerful than these traditional desktop languages. At the same time, Web-scripting languages are also much more focused on user experience and allow for much quicker development. AIR opens up the door to this world and allows the Web development community to prove that user experience is the key to a great application.
Put simply, this is the direction that application development is heading. Users and businesses should not have to wait for developers of lower-level programming languages to reinvent the wheel for every application when the same application could be developed far more quickly using a scripting language interpreted by a low-level framework. Add to that the fact that these same scripting languages can be interpreted by Web browsers and mobile devices, and it’s clear that this is going to create a real shift in the world of application development. AIR is the future of application development, and this is the book to get you on your feet.
目录 Summary of Contents
Part I: Introduction to AIR
Chapter 1: Clearing the AIR
Chapter 2: Setting Up Your Development Environment
Chapter 3: Building Your First AIR Application
Part II: Programming for AIR Essentials
Chapter 4: Crash Course in AIR Programming
Chapter 5: Development Essentials
Chapter 6: Debugging and Profiling
Part III: AIR API
Chapter 7: Communicating with the Local Machine
Chapter 8: Using the Filesystem
Chapter 9: Using the Clipboard
Chapter 10: Dragging and Dropping
Chapter 11: SQLite Databases
Chapter 12: Using Native Operating System Windows
Chapter 13: HTML Content
Part IV: Building an Application
Chapter 14: Preparing to Build a Large-Scale Application
Chapter 15: Building a Reusable Config Class
Chapter 16: Application Design Best Practices
Chapter 17: SDK Development
Chapter 18: Sample Application: LogReader
Chapter 19: Polishing a Finished Application
Part V: Testing and Deploying
Chapter 20: Deployment Workflow
Chapter 21: Leveraging Ant to Automate the Build Process
Chapter 22: Installation and Distribution
Index
关于作者 About the Author
Benjamin Gorton has been developing software for the desktop and the Web for over 10 years. For the
past seven years, he has been working in Flash and ActionScript, doing projects for such companies as
Disney, MTV, Neopets, and Sandisk. He currently resides in Los Angeles, where he works as a Senior
Software Developer for Schematic.
Ryan Taylor is an award-winning artist and programmer specializing in object-oriented architecture,
CGI mathematics/programming, as well as both static and motion design. Ryan, 25, has already landed
his name in the credits of the #1 and #5 all-time best selling video game titles, written for multiple
books, and established himself as an all-around leader in the digital arts community. Currently, Ryan
serves as a senior developer on the Multimedia Platforms Group at Schematic. He also works as an independent
contractor, offering his expertise to top companies and agencies all over the world.
Jeff Yamada lives with his wife AmyLynn and son Jackson in Salt Lake City, Utah, where he is currently
a Senior Interactive Developer at the award-winning RED Interactive Agency. Jeff specializes in the
architecture and development of immersive branded Flash experiences, rich Internet applications, and of
course, AIR applications. As both a designer and developer, Jeff has spent the last ten years freelancing,
consulting, and working for the University of Washington, Microsoft, Avenue A | Razorfish, Schematic,
and Nintendo. Jeff contributes to the open-source community and shares his thoughts and ideas with
the world at http://blog.jeffyamada.com.
http download
From rapidshare : AIR Bible
[Json]Json初探
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)、
true、false、null、对象(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的数值非常相似。除去未曾使用的八进制与十六进制格式。除去一些编码细节。
[Google]Google Web Toolkit(GWT) v1.5.3 - Google We
如今,编写网络应用程序是一个单调乏味且易于出错的过程。开发人员可能要花费 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 开源系统
Oxite 是微软近期发布了一个开源CMS或博客平台,关于Oxite 系统的基本介绍,请参考文章 – 微软发布Oxite开源CMS博客平台。



具体设置信息如下图所示:

[SQL]SQL Server2005 Sp3
|
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的聊天室实现原理
基于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 发布了
DLR团队终于发布了 IronPython 2.0 ,IronPython 2.0完全基于Dynamic Language Runtime (DLR). DLR允许多个动态语言在系统类型层面实现互操作。这个版本修复大概500多个bug,有453个来自codeplex社区的反馈。热烈祝贺开发团队发布 了这一个重大的里程碑版本。可以到codeplex上去下载,下面是一些重要的链接:
- 项目codeplex站点http://www.codeplex.com/IronPython
- 下载地址 http://www.codeplex.com/IronPython/Release/ProjectReleases.aspx?ReleaseId=8365
- 发布说明:http://www.codeplex.com/IronPython/Wiki/View.aspx?title=v2.0.0%20Release%20Notes&referringTitle=Home
下面是IronPython 2.0一些说明:
- 要求安装.NET 2.0 SP1
- 和CPython 2.5 兼容,有几个不同的地方,参看Differences between IronPython 2.0.x and CPython 2.5.2
- 性能良好: 比Python 2.5 快1.8 倍 ,使用 PyStone 基准测试的
- 可以调用大部分的标准 Python 库和所有.NET 库
[C#]C#编写的聊天室程序
源码下载:http://www.tracefact.net/SourceCode/CSharp-Chat.rar
C#编写简单的聊天程序
引言
这是一篇基于Socket进行网络编程的入门文章,我对于网络编程的学习并不够深入,这篇文章是对于自己知识的一个巩固,同时希望能为初学的朋友提供一点参考。文章大体分为四个部分:程序的分析与设计、C#网络编程基础(篇外篇)、聊天程序的实现模式、程序实现。
程序的分析与设计
1.明确程序功能
如 果大家现在已经参加了工作,你的经理或者老板告诉你,“小王,我需要你开发一个聊天程序”。那么接下来该怎么做呢?你是不是在脑子里有个雏形,然后就直接 打开VS2005开始设计窗体,编写代码了呢?在开始之前,我们首先需要进行软件的分析与设计。就拿本例来说,如果只有这么一句话“一个聊天程序”,恐怕 现在大家对这个“聊天程序”的概念就很模糊,它可以是像QQ那样的非常复杂的一个程序,也可以是很简单的聊天程序;它可能只有在对方在线的时候才可以进行 聊天,也可能进行留言;它可能每次将消息只能发往一个人,也可能允许发往多个人。它还可能有一些高级功能,比如向对方传送文件等。所以我们首先需要进行分 析,而不是一上手就开始做,而分析的第一步,就是搞清楚程序的功能是什么,它能够做些什么。在这一步,我们的任务是了解程序需要做什么,而不是如何去做。
了解程序需要做什么,我们可以从两方面入手,接下来我们分别讨论。
1.1请求客户提供更详细信息
我 们可以做的第一件事就是请求客户提供更加详细的信息。尽管你的经理或老板是你的上司,但在这个例子中,他就是你的客户(当然通常情况下,客户是公司外部委 托公司开发软件的人或单位)。当遇到上面这种情况,我们只有少得可怜的一条信息“一个聊天程序”,首先可以做的,就是请求客户提供更加确切的信息。比如, 你问经理“对这个程序的功能能不能提供一些更具体的信息?”。他可能会像这样回答:“哦,很简单,可以登录聊天程序,登录的时候能够通知其他在线用户,然 后与在线的用户进行对话,如果不想对话了,就注销或者直接关闭,就这些吧。”
有了上面这段话,我们就又可以得出下面几个需求:
- 程序可以进行登录。
- 登录后可以通知其他在线用户。
- 可以与其他用户进行对话。
- 可以注销或者关闭。
1.2对于用户需求进行提问,并进行总结
经常会有这样的情况:可能客户给出的需求仍然不够细致,或者客户自己本身对于需求就很模糊,此时我们需要做的就是针对用户上面给出的信息进行提问。接下来我就看看如何对上面的需求进行提问,我们至少可以向经理提出以下问题:
NOTE:这里我穿插一个我在见到的一个印象比较深刻的例子:客户往往向你表达了强烈的意愿他多么多么想拥有一个属于自己的网站,但是,他却没有告诉你网站都有哪些内容、栏目,可以做什么。而作为开发者,我们显然关心的是后者。
- 登录时需要提供哪些内容?需不需要提供密码?
- 允许多少人同时在线聊天?
- 与在线用户聊天时,可以将一条消息发给一个用户,还是可以一次将消息发给多个用户?
- 聊天时发送的消息包括哪些内容?
- 注销和关闭有什么区别?
- 注销和关闭对对方需不需要给对方提示?
由于这是一个范例程序,而我在为大家讲述,所以我只能再充当一下客户的角色,来回答上面的问题:
- 登录时只需要提供用户名称就可以了,不需要输入密码。
- 允许两个人在线聊天。(这里我们只讲述这种简单情况,允许多人聊天需要使用多线程)
- 因为只有两个人,那么自然是只能发给一个用户了。
- 聊天发送的消息包括:用户名称、发送时间还有正文。
- 注销并不关闭程序,只是离开了对话,可以再次进行连接。关闭则是退出整个应用程序。
- 注销和关闭均需要给对方提示。
好了,有了上面这些信息我们基本上就掌握了程序需要完成的功能,那么接下来做什么?开始编码了么?上面的这些属于业务流程,除非你对它已经非常熟悉,或者程序非常的小,那么可以对它进行编码,但是实际中,我们最好再编写一些用例,这样会使程序的流程更加的清楚。
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(
}
// 接收到消息
void talker_MessageReceived(string msg) {
MessageReceivedEventHandler del = delegate(string m) {
txtContent.Text += m;
};
txtContent.Invoke(
}
// 有客户端连接到本机
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(
}
// 客户端连接断开
void talker_ClientLost(string info) {
ConnectionLostEventHandler del = delegate(string information) {
txtContent.Text +=
String.Format("System[{0}]: {1} ", DateTime.Now, information);
};
txtContent.Invoke(
}
// 发送消息
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
源码下载:http://www.tracefact.net/SourceCode/Network-Part5.rar
C#网络编程(接收文件) – Part.5
这篇文章将完成Part.4中剩余的部分,它们本来是一篇完整的文章,但是因为上一篇比较长,合并起来页数太多,浏览起来可能会比较不方便,我就将它拆为两篇了,本文便是它的后半部分。我们继续进行上一篇没有完成的步骤:客户端接收来自服务端的文件。
4.客户端接收文件
4.1服务端的实现
对 于服务端,我们只需要实现上一章遗留的sendFile()方法就可以了,它起初在handleProtocol中是注释掉的。另外,由于创建连接、获取 流等操作与receiveFile()是没有区别的,所以我们将它提出来作为一个公共方法getStreamToClient()。下面是服务端的代码, 只包含新增改过的代码,对于原有方法我只给出了签名:
class Server {
static void
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
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();
}
}
由于这是一个控制台应用程序,并且采用了异步操作,所以这个菜单的出现顺序有点混乱。我这里描述起来比较困难,你将代码下载下来后运行一下就知道了:-)
程序的运行结果和上一节类似,这里我就不再贴图了。接下来是本系列的最后一篇,将发送字符串与传输文件的功能结合起来,创建一个可以发送消息并能收发文件的聊天程序,至于语音聊天嘛…等我学习了再告诉你 >_<、
Mikel



