[UI]80 年代以来的操作系统 GUI 设计进化史(上)

mikel阅读(710)

70年代,施乐公司 Xerox Palo Alto Research Center (PARC) 的 研究人员开发了第一个 GUI 图形用户界面,开启了计算机图形界面的新纪元,80年代以来,操作系统的界面设计经历了众多变迁,OS/2,Macintosh, Windows, Linux, Symbian OS ,各种操作系统将 GUI设计带进新的时代。本文介绍了80年代以来各种操作系统 GUI 界面设计的进化史。

第一个使用现代图形界面的个人电脑是 Xerox Alto,设计于1973年,该系统并未商用,主要用于研究和大学。
Source: toastytech.com
1981-1985
Xerox 8010 Star (1981年推出)
这是第一台全集成桌面电脑,包含应用程序和图形用户界面(GUI),一开始叫 The Xerox Star,后改名为 ViewPoint, 最后又改名为 GlobalView。

Xerox 8010 Star, Source: toastytech.com
Apple Lisa Office System 1 (1983)
又 称 Lisa OS,这里的 OS 是 Office System的缩写,苹果开发这款机器的初衷是作为文档处理工作站。不幸的是,这款机器的寿命并不长,很快被更便宜的 Macintosh 操作系统取代。LisaOS 几个升级包括 1983年的 Lisa OS2, 1984年的 Lisa OS 7/7 3.1。

Apple Lisa OS 1, Source: GUIdebook

Apple Lisa OS 1, Source: GUIdebook
VisiCorp Visi On (1984)
Visi On 是为 IBM PC 开发的第一款桌面 GUI,该系统面向大型企业,价格昂贵,使用基于鼠标的 GUI,它内置安装程序与帮助系统,但尚未使用图标。

VisiCoprt Visi On, Source: toastytech.com

VisiCoprt Visi On, Source: toastytech.com
Mac OS System 1.0 (1984)
System 1.0 是最早的 Mac 操作系统 GUI,已经拥有现代操作系统的几项特点,基于窗体, 使用图标。窗体可以用鼠标拖动,文件与文件夹可以通过拖放进行拷贝。

Apple Mac System 1.0, Source: toastytech.com
Amiga Workbench 1.0 (1985)
一经发布,Amiga 就领先时代。它的 GUI 包含诸如彩色图形(四色:黑,白,蓝,橙),初级多任务,立体声以及多状态图标(选中状态和未选中状态)

Amiga Workbench 1.0, Source: GUIdebook

Amiga Workbench 1.0, Source: GUIdebook
Windows 1.0x (1985)
1985年,微软终于在图形用户界面大潮中占据了一席之地,Windows 1.0 是其第一款基于 GUI 的操作系统 。使用了 32×32 像素的图标以及彩色图形,其最有趣的功能是模拟时钟动画图标。

Microsoft Windows 1.01, Source: makowski-berlin.de

Microsoft Windows 1.01, Source: makowski-berlin.de
1986 – 1990 IRIX 3 (released in 1986, first release 1984)
64为 IRIX 操作系统是为 Unix 设计的,它的一个有趣功能是支持矢量图标,这个功能远在 Max OS X 面世前就出现了。

Silicon Graphics IRIX 3.0, Source: osnews.com
Windows 2.0x (1987)
这个版本的 Windows 操作系统中对 Windows 的管理有了很大改进,Windows 可以交叠,改变大小,最大化或最小化。

Microsoft Windows 2.03, Source: guidebookgallery.org

Microsoft Windows 2.03, Source: guidebookgallery.org
OS/2 1.x (released in 1988)
OS/2 最早由 IBM 和微软合作开发,然而1991年,随着微软将这些技术放到自己的 Windows 操作系统,两家公司决裂,IBM继续开发 OS/2,OS/2 使用的 GUI 被称为 “Presentation Manager (展示管理)”,这个版本的 GUI只支持单色,以及固定图标。

Microsoft-IBM OS/2 1.1, Source: pages.prodigy.net

Microsoft-IBM OS/2 1.1, Source: pages.prodigy.net
NeXTSTEP / OPENSTEP 1.0 (1989)
Steve Jobs 心血来潮,想为大学和研究机构设计一款完美的电脑,他的这个设想后来造就了一家叫做 NeXT Computer 的公司。
第一台 NeXT 计算机于1988年发布,不过到了1989年随着 NeXTSTEP 1.0 GUI 的发布才取得显著进展,该 GUI 后来演变成 OPENSTEP。
该 GUI 的图标很大,48×48像素,包含更多颜色,一开始是单色的,从1.0开始支持彩色,下图中已经可以看到现代 GUI 的影子。

NeXTSTEP 1.0, Source: kernelthread.com
OS/2 1.20 (1989)
OS/2 的下一个小版本在很多方面都做了改进,图标看上去更好看,窗体也显得更平滑。

OS/2 1.2, Source pages.prodigy.net
Windows 3.0 (1990)
到 Windows 3.0, 微软真正认识到 GUI的威力,并对之进行大幅度改进。该操作系统已经支持标准或386增强模式,在增强模式中,可以使用640K以上的扩展内存,让更高的屏幕分辨率和更 好的图形成为可能,比如可以支持 SVGA 800×600 或 1024×768。
同时,微软聘请 Susan Kare 设计 Windows 3.0 的图标,为 GUI 注入统一的风格。

Microsoft Windows 3.0, Source: toastytech.com

Microsoft Windows 3.0, Source: toastytech.com
1991 – 1995 Amiga Workbench 2.04 (1991)
该版 GUI 包含很多改进,桌面可以垂直分割成不同分辨率和颜色深度,在现在看来似乎有些奇怪。默认的分辨率是 640×256,不过硬件支持更高的分辨率。

Commodore Amiga Workbench 2.04, Source: guidebookgallery.org
Mac OS System 7 (released in 1991)
Mac OS 7 是第一款支持彩色的 GUI,图标中加入了微妙的灰,蓝,黄阴影。

Apple Mac OS System 7.0, Source: guidebookgallery.org

Apple Mac OS System 7.0, Source: guidebookgallery.org
鸣谢

延伸阅读
苹果的设计演化史:1977-2008
本文国际来源:Operating System Interface Design Between 1981-2009
中文翻译来源:COMSHARP CMS 官方网站

[XNA]XNA基础(03) —— 动画与帧率

mikel阅读(822)

    我们要做的2D和3D游戏离不开动画,那么在XNA中如何实现动画了?

     首先,我们来看最简单的动画 —— 移动。

     要移动一个Sprite非常简单,我们只需要在Game1.Update()方法中改变Sprite的位置坐标,在下次Game1.Draw()方法被调用时,屏幕上显示的Sprite就被移动了。

     接下来,我们看复杂一点的动画,比如炸弹的爆炸效果,我们可以这样来实现,制作一系列的图片,每张图片都是爆炸过程中某一状态的表现,如下所示:

     

     上面的20个小图片表现了一个爆炸从初始到结束的所有状态,在实际制作时,我们通常将这些图片按顺序制作在一张大图中,并且保证大图中每个小图的尺寸是完全一样的。我们称这样的大图为精灵帧序列图Sprite Sheets

     有了爆炸的Sprite Sheets,我们可以通过在Game1.Update()方法中改变下一个要显示的小图片的索引(比如[2,3]),再根据索引以及小图片的尺寸计算出 该将要显示的小图片在Sprite Sheets中的相对位置,这样我们就可以在Game1.Draw()方法中将Sprite Sheets中的目标小图片的那块区域绘制出来。你应该还记得上一篇文章中讲到的Game1.Draw()方法的第三个参数 sourceRectangle,用它可以指定我们要绘制Sprite Sheets的目标区域。

 

     看来,实现一个动画并非难事,真正困难的地方在于如何控制每个动画可以有自己不同的刷新速度,如何使同一个动画在不同配置的机器上表现相同。这就涉及到帧率问题。

     所谓帧率Frame Rate,指的是一秒钟内重新绘制屏幕的次数。XNA框架为我们设置的默认帧率是60fps。为什么选择60了?因为这是在人的眼睛感觉不到闪烁的情况下显示器最低的刷新频率。我们可以通过基类Microsoft.Xna.Framework.Game的属性TargetElapsedTime来重新设置它。比如:

base.TargetElapsedTime = new TimeSpan(000010);

     这表示每隔10ms就重绘一次,即帧率为100fps。

     设置帧率为100fps,并不表示就真的会达到100fps的速度,这要看机器的配置如何。当机器的配置不够时,XNA会自动跳过某些次 绘制——即不调用Game1.Draw()方法。我们可以通过GameTime(你还记得Update和Draw方法都有一个该类型的参数)的IsRunningSlowly属性来检测实际的帧率是否比我们设定的要小。

     通过修改TargetElapsedTime属性来设置帧率,会使所有的动画都受到影响,因为它实际修改的是调用Game1.Update()和Game1.Draw()的频率。

     我们如何使一个动画以自己恒定的速度刷新了?包括这个动画的刷新速度不受主帧率(即TargetElapsedTime设定的值)和机器配置的影响,当然,前提条件是我们动画的刷新率不能大于主帧率,也不能超出机器配置允许的最大帧率。

     我们可以用类似下面的代码来控制每个动画以自己的刷新率运行:

          //成员变量          

          int timeSinceLastFrame = 0

          int millisecondsPerFrame = 50; // 20fps          

 

          //在Update方法中

          timeSinceLastFrame += gameTime.ElapsedGameTime.Milliseconds;
          
if (timeSinceLastFrame > millisecondsPerFrame) //满足了换帧条件
          {
                timeSinceLastFrame 
-= millisecondsPerFrame;

                //…… 此处可以转到动画的下一帧

          }

     通过上述代码,我们就可以控制目标Sprite的动画速率为20fps。     

     在实际的应用中,我们可以将上述控制帧率的代码放到Sprite的基类中,这样就可以控制不同的Sprite以各自的速率运行了。

     今天就讲到这里,下一节我们将讲述如何捕捉用户的输入事件,比如鼠标、键盘的输入。

[XNA]XNA基础(02)绘制基础

mikel阅读(826)

在所有的图形引擎中,绘制都是最基础的部分,本文将介绍在XNA框架中与绘制相关的基础知识。

在XNA中,我们使用SpriteBatch来进行绘制。首先,我们需要使用SpriteBatch来绘制什么了?是精灵Sprite,对。

那么Sprite通过什么来表现了?是纹理,比如2D纹理Texture2D。嗯,你可以把纹理想象成Sprite的外表,比如我们制作的一幅精灵图片,就是一个纹理。

我们要如何才能把一幅图片加载到我们的游戏中来作为一个Sprite的纹理了?这要通过素材管道Content Pipeline。所谓素材,包括我们游戏中要用到的图片、模型、声音等,比如一个纹理图片就是素材的一种。素材管道就很容易理解了,它可以把我们需要的素材导入到我们的游戏中,以XNA能够识别的正确的格式。而且,格式的识别与转换是在编译期完成的。

在新建的XNA项目中,会有一个默认的Content文件夹,通常,我们会把所有的素材放在这个地方,并且根据素材的种类我们会在其下创建一些子文件夹,比如Image子文件夹用来存放所有的图片素材,Audio文件夹用来存放声音素材等。

当一个物件别被识别为素材后,就会有一个唯一的资产名称AssetName来标记它,我们可以通过素材的属性页来查看素材的AssetName属性并修改它。

下面我们可以用素材管理器ContentManager将一个素材导入到游戏中:

Texture2D texture;
texture
= Content.Load<Texture2D>(@”Image\logo); //在上文提到的LoadContent方法中调用

很多时候,我们的图片需要是透明的,那么如何在SpriteBatch绘制的时候将图片绘制为透明了?有两种方法:要么图片具有透明的背景,要么在制作图片时将需要透明的部分设置为纯正的洋红色(255,0,255)。除此之外,还需要注意,SpriteBlendMode模式必须为AlphaBlend(这也是默认模式,稍后会提到),才能达到我们期望的透明效果。

在进行渲染时,我们还需要注意层深度(Layer Depth)。所谓Layer Depth,指的是你需要将目标Sprite绘制在哪一个深度的层次。默认情况下,SpriteBatch会根据你调用的顺序来决定Layer Depth的相对值,比如,你先调用SpriteBatch绘制Sprite A,然后调用SpriteBatch在同一位置绘制Sprite B,那么,Sprite B就会把Sprite A挡住。如果我们依靠调用SpriteBatch绘制的顺序来决定Sprite的深度,那就太不灵活了,为此,调用SpriteBatch绘制方法时,你 可以指定一个代表层次深度的参数(范围0~1)来指示SpriteBatch按照我们希望的顺序来绘制对象。

有了上面的这些基础之后,我们可以详细讲解一下XNA中的渲染过程。每次渲染的过程都类似下面的模式:

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
spriteBatch.Draw();
spriteBatch.End();
base.Draw(gameTime);
}

(1)GraphicsDevice.Clear()方法用于清除屏幕背景。

(2)SpriteBatch.Begin()方法用于通知显卡准备绘制场景。

(3)轮流调用SpriteBatch.Draw()绘制所有组件。

(4)SpriteBatch.End()方法用于告诉显卡绘制已经完成。

(5)最后,显卡显示绘制的结果。

SpriteBatch.Begin()方法有一个接受参数的重载:

void Begin(SpriteBlendMode blendMode, SpriteSortMode sortMode, SaveStateMode stateMode, Matrix transformMatrix) ;

SpriteBlendMode 参数决定了Sprite的颜色与背景颜色的混合模式,默认为AlphaBlend,我们刚才提到过。

SpriteSortMode参数决定了Sprite的排序模型,即与前面的Layer Depth关联。

SaveStateMode参数用于控制是否保存图形设备的状态。

TransformMatrix参数用于控制旋转、缩放等。

接下来我们看最重要的绘制Draw方法:

void Draw(Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth);

第一个Texture2D参数,表示要绘制Sprite的纹理。

第二个Vector2参数,表示要绘制的Sprite的左上角的位置在哪里 。

第三个Rectangle参数,表示要绘制源纹理的哪一个区域,如果要绘制纹理的全部部分,则传null。

第四个Color参数,表示绘制时所调制的颜色通道。

第五个参数rotation,表示要旋转的角度。

第六个参数origin,表示围绕哪个位置进行旋转。

第七个参数scale,表示绘制时要缩放的比例。

第八个参数effects,表示是否进行翻转。SpriteEffects.FlipHorizontally — 水平翻转;SpriteEffects.FlipVertically — 垂直翻转。

最后一个参数layerDepth,就是我们前面说过的要在哪一个深度层次绘制对象。

最后,要解释一下,为什么需要在每次Draw调用时,都先调用GraphicsDevice.Clear()方法清除屏幕然后再重新绘制所有物件了?这样性能不是很差吗?而且有时候我们可能只有极小的一部分需要重新绘制。

假设我们每次只重新绘制变动的那一部分,那我们就需要一个复杂的管理器来追踪每一个部分的变动,这样我们才有可能知道要重绘哪一部分。你 可以想象一下,这个追踪的过程是相当复杂的。而且还存在这样的情况,那就是当一个Sprite移动时,它后面原先被挡住的Sprite会露出来,而这个 Sprite可能又会挡住另外一个Sptrite的一部分,这样一来就更加复杂了。所以,每次重绘整个屏幕并不是一个坏的idea。

今天就讲到这里,下一节我们将讲述与FrameRate相关的知识。

[XNA]XNA基础(01) —— 游戏循环

mikel阅读(829)

     当安装好了VS 2008和XNA GameStudio 3.0后,我们就可以开始学习XNA了。

     首先,在VS 2008中新建一个XNA GameStudio 3.0项目(选择Windows Game类型),会生成一个最简单的、可运行的游戏模板。

     接下来我们将注意力转移到我们要剖析的重点 —— 从Microsoft.Xna.Framework.Game继承的Game1类,其代码如下:          

 public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        
public Game1()
        {
            graphics 
= new GraphicsDeviceManager(this);
            Content.RootDirectory 
= "Content";
        }
       
        
protected override void Initialize()
        {            
            
base.Initialize();
        }
       
        
protected override void LoadContent()
        {            
            spriteBatch 
= new SpriteBatch(GraphicsDevice);           
        }
        
        
protected override void UnloadContent()
        {           
        }
      
        
protected override void Update(GameTime gameTime)
        {           
            
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                
this.Exit();
            
base.Update(gameTime);
        }
      
        
protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            
base.Draw(gameTime);
        }
    }

 

        我们简单解释一下该类中所用到的重要类型。

GraphicsDeviceManager 图形设备管理器,用于访问图形设备的通道。

GraphicsDevice 图形设备。

Sprite 精灵,绘制在屏幕上的的2D3D图像,比如游戏场景中的一个怪兽就是一个Sprite

SpriteBatch 它使用同样的方法来渲染一组Sprite对象。

 

        而Microsoft.Xna.Framework.Game 这个基类就像是一个基础框架,它把整个游戏流程串联了起来,即,整个游戏会按如下流程运转。

             

        该图中的五个方法正好对应着Game1类中的五个方法,它们的作用解释如下。

     Initialize方法用于初始化与游戏相关的对象,比如初始化图形设备、游戏环境设置等。

     LoadContent方法在Initialize方法之后调用,它用于加载游戏所需要的图形或其它素材,比如模型、图片、声音等。

     UpdateDraw方法构成了游戏循环

     Update方法用于改变和控制游戏的状态,主导着游戏逻辑的进行。

     Draw方法用于在屏幕上绘制我们的场景、Sprite。要注意的是,我们应该尽可能少的在Draw方法中处理游戏逻辑——它们应该在Update方法中被处理。Draw方法仅仅负责绘制。

     UpdateDraw方法都接受一个GameTime类型的参数,GameTime有什么作用了?这个参数可以帮助我们依据实际的游戏时间而不是处理器的速度来决定动画或其它事件的发生时刻。

      在这个最简单的例子中,游戏将以默认的60fpsFrame/Second)运行。

     UnloadContent方法在游戏结束时被调用,该方法用于卸载在LoadContent方法中所加载的素材和那些需你要进行特别处理的善后事宜。

 

     状态轮询与事件监听

     写过Windows应用程序的朋友都知道,当用鼠标点击Form上的一个Button时,会触发一个Click事件,而我们的应用程序通过监听到事件的发生进而来处理事件。

     而在游戏开发中,我们需要将我们的这种“事件”思维切换到“轮询”思维。也就是说,游戏中,用户的鼠标、键盘操作并不会触发相关的事件。如果是这样的话,那我们该如何知道用户是否按下了鼠标了?答案是我们需要在游戏循环中(确切的说是在Update方法中)不断地检测输入设备(比如鼠标)的状态来进行判断。

       这就是轮询机制与事件机制的区别,也是游戏开发和普通windows应用开发需要转换思路的地方。

       归根到底,windows事件机制也是对轮询机制(即Windows消息循环)的一个封装

       

       今天的介绍就到这里,下一节我们将介绍与渲染相关的基础知识。   

       最后,附上XNA GameStudio 3.0的下载地址:http://creators.xna.com/en-us/xnags_islive

 

[SQL]SQL Server 2005 中使用正则表达式匹配

mikel阅读(1117)

SQL Server 2005 中使用正则表达式匹配

CLR 用户定义函数只是在 .NET 程序集中定义的静态方法。Create FUNCTION 语句已扩展为支持创建 CLR 用户定义函数。

1、创建数据库项目

  

2、添加用户定义函数

  

以下是演示代码:

Code
using System;
using System.Data;
using System.Data.SQLClient;
using System.Data.SqlTypes;
using Microsoft.SQLServer.Server;
using System.Text.RegularExpressions;
// 示意代码
public partial class UserDefinedFunctions
{
    
public static readonly RegexOptions Options = RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline;
    [Microsoft.SQLServer.Server.SqlFunction]
    
public static string RegexValue(SqlChars input, SqlString pattern)
    {
        Regex regex 
= new Regex(pattern.Value, Options);
        
return  regex.Match(new string(input.Value)).Value;
    }
}

 

3、将自定义函数关联到数据库

  

4、Sql 查询分析器

    

为了确保SQL可以执行托管代码,执行下面的语句:

EXEC sp_configure 'clr enabled', 1

sql 如下:
select dbo.RegexValue('2008-09-02',N'\d{4}') from Table

=============================================

[C#]C#4.0的dynamic代替反射的第一个应用实例——日志跟踪器

mikel阅读(1051)

在平时做框架架构设计的时候,头疼之一的是处处得采用反射,但有了C#4.0,发现dynamic完全可以取代反射,这个功能让我有些激动,立马在VS2010将日志跟踪器框架里的第一个反射的代码升级到C#4.0,结果一点都不令人失望,代码简化了很多。
先看看用dynamic替换反射后的代码吧: 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Reflection;
 6 using System.IO;
 7 /********************************
 8  * Updated by Lihua at 03/13/2009
 9  * 
10  * 更新功能:
11  * 1. 升级到C#4.0,加入dynamic代替反射
12  * 2. 如果Zivsoft.Log.dll不存在,日志不输出
13  * 3. 项目编译不依赖于Zivsoft.Log.dll
14  ****************************************/
15 namespace Zivsoft.Data
16 {
17     /// <summary>
18     /// 只提供持久数据层框架里的类使用
19     /// </summary>
20     internal class Logger
21     {
22         private static Assembly _assemblyFile;
23         private static dynamic _logger;
24         static Logger()
25         {
26             var strDllFile = AppDomain.CurrentDomain.BaseDirectory + "Zivsoft.Log.dll";
27             if (File.Exists(strDllFile))
28             {
29                 _assemblyFile = Assembly.LoadFile(strDllFile);
30                 try
31                 {
32                     _logger = _assemblyFile.CreateInstance("Zivsoft.Log.Logger"false, BindingFlags.CreateInstance, nullnullnullnull);
33                 }
34                 catch {
35                     _logger = null;
36                 }
37             }
38         }
39 
40         public static void LogInfo(string message, params object[] args)
41         {
42             if (null != _logger)
43             {
44                 _logger.LogInfo(message, args);
45             }
46         }
47 
48         public static void LogWarning(string message, params object[] args)
49         {
50             if (null != _logger)
51             {
52                 _logger.LogWarning(message, args);
53             }
54         }
55 
56         public static void LogError(string message, params object[] args)
57         {
58             if (null != _logger)
59             {
60                 _logger.LogError(message, args);
61             }
62         }
63 
64         public static void LogDebug(string message, params object[] args)
65         {
66             if (null != _logger)
67             {
68                 _logger.LogDebug(message, args);
69             }
70         }
71 
72         public static void LogError(Exception e)
73         {
74             LogError("{0}", e);
75         }
76     }
77 }
78 

 

以上是持久数据层调用日志跟踪器的入口代码,以前采用反射,刚被我用dynamic改了过来,经调试一点问题都没有,所以这让我欣喜,因为接下来的 我的很多框架采用反射的机制将都可能被dynamic替换,比如《持久数据层框架》中的被反射的SQLServer, MySQL, Access等等数据库。

不多写了,大家仔细体会吧。

[汇编]深入系统底层--之--教你用0101写程序

mikel阅读(804)

准备你的行囊—-建立环境

为了让大家更为轻松,除非迫不得已,我们尽量使用系统上已经安装的工具,在这一章里,下面两个外部工具是必须的

同时,读者应该稍微具备的汇编知识,不用太多,知道下面这些指令的意义和用法即可

MOV 数据传送指令
ADD 加法指令
PUSH,POP 堆栈指令
CMP 比较指令
LEA 取地址指令
XOR 异或指令

所有的转移指令:JMP,JZ,JE

如果你还想进一步了解机器码的规范,可以下载 http://download.csdn.net/source/1103630,里面有Intel的文档,以及本文用到的操作码查询表

用0和1写程序

曾经有人发给我一张图片,说世界上"最牛程序员"的键盘,键盘上一共三个键,01,当时年少无知,崇拜到抓狂,今天就让我们当回"顶尖高手",用01直接写程序

请打开一个十六进制编辑器比如UltraEdit

把下面的二进制代码化为16进制输入进去(主要无法直接输入二进制代码)

1011 1000 0000 0001 0000 0000 0000 0101 0000 0001 0000 0000

十六进制为B8 01 00 05 01 00

将文件保存为test.com文件,恭喜你,你刚刚完成了一个伟大"壮举",你 成功的让CPU计算出了1+1等于几,如果你兴匆匆的运行它,什么结果都看不到,那是因为为了保证代码简单,还没有告诉CPU输出结果的缘故,你愿意的 话,可以运行cmd,切换到保存test.com的目录,通过执行Debug test.com,来看看我们到底输入了什么

image

1011 1000 代表 MOV ax
0000 0001 0000 0000 代表1
0000 0101 代表 ADD ax
0000 0001 0000 0000 代表 0001h

全文加起来表示
MOV ax,01h
ADD ax,01h

可以看出,我们的代码对应了两条机器指令,每个指令分成两个部分,比如MOV ax,1的二进制代码,1011 1000 代表 MOV ax他指定了本条指令的操作,叫做指令操作码(Opcode),0000 0001 0000 0000 代表1,指定了操作的操作数,可以看出机器码是有自己固定的格式,只要掌握了这个格式,查询对应的操作码,应该就可以掌握机器语言了

当然,事情也有复杂的一面,同一条汇编指令其操作码可能根据寻址方式或寄存器或操作数的位数的变化发生变化,比如同样是MOV指令,MOV al,1 和MOV ax,1中Mov的操作码分别为B0(1011 0000)和B8(1011 1000),而MOV ax,[ES:100]操作码会变成26 A1(前面26是段超越前缀,现在不用仔细追究),Intel8086规定的MOV指令就有12种之多,而且操作码的长度还有可能不同,这些操作码都可以 在表<x86操作码查询表>中对应的查到,不需要记忆,下面我们就来了解机器语言指令的格式

自己设计机器语言指令格式

在阅读Intel公司的实现前,为了不让您陷入一堆的解释和说明中迷惘无助,我们先来热热身,做点有趣的事情—思考一下如果让你自己来设计机器语言指令的格式,那么你会做出怎样的设计,下面是我的设计思路

首先汇编代码和机器代码是对应的,所以让我们来看看一条典型x86汇编指令:

MOV ax,1

这条指令由三个部分组成:指令,目的操作数,源操作数

指令为Mov,目的操作数ax,源操作数1,

ADD bx,2

指令为Add,目的操作数bx,源操作数2

相对应的我们可以考虑把机器指令格式也分成三个部分:指令码,目的操作数,原操作数

由于寄存器的数目是有限的,我们可以列个寄存器机器码指令表,这样代码中的寄存器就可以被替换为如下的机器代码,比如

  • 000   AX
  • 001  CX
  • 010  DX 
  • 011  BX 
  • 100  SP 
  • 101  BP 
  • 110  SI 
  • 111  DI

     

    然后我们再列一个指令码表,比如

    MOV=00000000
    ADD=00000001
    AND=00000010
    .
    .
    .

    则MOV ax,1就可以变成 00000000 00000000 00000001(ax是000)

    但是这样简单清晰的三个部分会出现一些问题mov bx,0,和mov bx,ax就有可能混淆了,因为ax的代码是000,和立即数0相同

    所以我们需要一个标志位来确定是那种操作数,操作数有下面5种可能

    目的操作数和原操作数的大小就比较难了,因为操作数可能是

    1)一个立即数 比如1

    2)一个寄存器 ax,bx,cx,dx

    3)一个内存地址 [StringLable]

    4)一个由一个或多个寄存器组成的内存地址

    [ebx],[ebx+esi],[es:ebx+esi]

    5)一个由一个或多个寄存器再加上一个偏移量组成的内存地址

    [ebx+esi]

    显然我们需要两个标志字段,每个5个值,(每个操作数一个)来标定自己是哪种操作数,每个标志字段只要3位就够了,我把这两个标志字段放到一个字节里,放在两个操作数前面

    格式一:

      指令码 保留2位|标志1|标志2| 操作数1 操作数2
    Mov ax,1 00000000 00|001|000 00000000 00000001

     

    标志的意义

    000:立即数
    001:寄存器
    010:内存地址
    011:多个寄存器
    100:多个寄存器加偏移量

    问题又出来了,当标志位为100,这时,操作数应该是多个寄存器+偏移量,假设每个寄存器占3位,两个就是6位,留给我们的偏移量的空间只有两位, 也就是说偏移量最大只有3,这显然是不够的,所以我们必须加上一个字节表示偏移量,而当不需要偏移量的时候,这两个字段可以不存在,也就是说表格变成了

    格式二:

      指令码 00|标志1|标志2 操作数1 偏移量 00|操作数2
    bbb|iii
    偏移量
    Mov ax,[bp+si+5] 00000000 00|001|100 00000000   00|101|110 00000110

    怎么样,有点像样子了吧,固定长度8位的指令码可能有256种指令,我想最基本的操作,AND,OR,XOR,ADD,SHR,SHL等等不会太多,而其他的操作都可以由这些操作组合而成,比如减法是补码的加法,乘法是重复相加等

    似乎大部分问题都已经解决了,但是稍微熟悉x86汇编的朋友就会知道,不可能有任何指令的两个操作数都是内存,也就是永远不会出现
    MOV [dx+di],[ex+si]这样的语句,要想实现这样的移动我们必须要把源操作数移动到一个寄存器里,然后再从寄存器里移动到目的地

    反应在我们的设计上,我们就会发现两个偏移量是多余的,任何情况下最多会有一个被使用到,所以表格可以修改成这样

    格式三:

      指令码 00|标志1|标志2 偏移量 操作数1 操作数2
    00|bbb|iii
    MOV ax,[bp+si+5] 00000000 00|001|100 00000110 00000000 00|101|110
    MOV ax,bx 00000000 00|001|001 00000000 00000011

     

    其实看看上表的第二条语句,我们就会发现一个很重大的问题,那就是空间浪费,第二行中所有黑体的部分都是被浪费掉的空间,浪费了12位,总共才32 位的代码,居然就浪费了12位,心疼啊,而且看看标志字段,占了三位,总共可以表示8个标志,确只用了5个,我们能不能想办法把这些空间利用起来呢?

    我们重新仔细考虑第二个字节,也就是标志字节,把最高位的两位利用起来,称作寄存器标志,他的值如下表

    00:操作数中没有寄存器

    01:操作数的后一个为寄存器

    10:操作数的前一个为寄存器

    11:两个操作数都是寄存器

    如果此位指明某操作数为寄存器,则后面的标志位直接为寄存器值,如果为00,则后面的操作数只可能为 (内存,立即数) 形式,这样MOV ax,bx的机器码就变成了下面的样子

    格式四:

      指令码 寄存器标志|标志1|标志2 偏移量 操作数1 操作数2
    00|bbb|iii
    MOV ax,bx 00000000 11|000|011

     

    好了,指令系统的雏形已经出来了,虽然和Intel的实现有很多不同,并且本身还有各种问题,比如依然有浪费空间的情况,功能也不太健全,不过基本体现了指令格式的特点:

    • 分成几个字段表示不同意义
    • 尽量短小精干
    • 不能浪费任何一位

    下面让我们来看看Intel公司的实现方法

     

    让书写机器码像填表一样简单

    从上面的叙述,我们已经大概能看出点门道,每条指令分为几个部分,表示不同的含义.Intel规定,机器指令都可以被表示成六个部 分,Prefix,Opcode,ModR/M,SIB,Displacement,Immediate,除了Opcode部分是必须的外,其他部分都有 可能不存在

    好像有点复杂不是?不要着急,我们稍作解释就可以把书写机器指令变得像填写表格一样简单

    下面我们把几条命令按照六个部分进行分割,填写到这张表里,后面会解释六个部分的含义

      Prefix
    前缀
    0-4个前缀,每个1字节
    可选
    Opcode
    操作码
    1-2字节
    一定存在
    ModR/M
    寻址与寄存器
    1个字节
    可选
    SIB
    内存寻址模式
    一个字节
    可选
    Displayment
    偏移量
    1,2或4个字节
    可选
    Immeidate
    立即数
    1,2或4个字节
    可选
          oo|rrr|mmm cc|iii|bbb    
    MOV ax,1 1011 1000 0001 0000
    ADD ax,1 0000 0101 0001 0000
    MOV ax,[ES:0100h] 0010 0110(26h代表es的段超越前缀) 1010 0001 0000 0000
    0001 0000
    mov ax,[ebx+esi*2+1] 0110 0111
    (67h,代表使用了32位
    1000 1011 01 000 100 01 110 011 0000 0001
    mov [ebx+esi*2+1],01h 67 1100 0111 01 000 100 01 110 011 0000 0001 0000 00001

     
    只要会填这个表,我们就可以写出所有的机器代码.

    可以看到,Intel的格式中并没有明确的标出两个操作数,而是把偏移量和立即数单独拿了出来,而且同一条指令的操作码会根据寻址方式的不同而变化,不像我们的设计,MOV就是MOV,所有的MOV指令都对应同样的操作码,Prefix部分也是我们的设计所没有的

    下面简单的解释下这六个部分,每个部分的具体含义和使用,后面的例子里会逐步阐述

    prefix:

    指令前缀,为了一些特殊的定义或者操作而存在,只有10个可能的值,可以在下表里面查到,我们大致了解下就是了
    • 锁(Lock)和重复前缀:
    锁前缀用于多CPU环境中对共享存储的排他访问。重复前缀用于字符串的重复操作,他可以获得比软件循环方法更快的速度。
    — F0H—LOCK 前缀.
    — F2H—REPNE/REPNZ 前缀.
    — F3H—REP 前缀
    — F3H—REPE/REPZ prefix (used only with string instructions).
    • Segment override:
    根据指令的定义和程序的上下文,一条指令所使用的段寄存器名称可以不出现在指令格式中,这称为段缺省规则。当要求一条指令不按缺省规则使用某个段寄存器时,必须以段取代前缀明确指明此段寄存器。
    — 2EH—CS  段前缀
    — 36H—SS 段前缀.
    — 3EH—DS 段前缀.
    — 26H—ES 段前缀.
    — 64H—FS 段前缀.
    — 65H—GS 段前缀.
    • 操作大小前缀 66H 和 地址长度前缀 67H

    Opcode:

    操作码,这个操作码指定了具体的操作,他的值可以在下表查到,注意查表时候要根据操作类型,操作数类型和寻址方式来查询,比如Mov指令有12种操 作操作码,我们需要根据操作数的类型,比如Mov bx,1,的两个操作数一个是寄存器,一个是立即数,即Reg,Imm,查下表,应为1011wrrr

        MemOfs,Acc     1010001w    
          Acc,MemOfs     1010000w    
          Reg,Imm     1011wrrr    
          Mem,Imm     1100011woo000mmm    
          Reg,Reg     1000101woorrrmmm    
          Reg,Mem     1000101woorrrmmm    
          Mem,Reg     1000100woorrrmmm    
          Reg16,Seg     10001100oosssmmm    
          Seg,Reg16     10001110oosssmmm    
          Mem16,Seg     10001100oosssmmm    
          Seg,Mem16     10001110oosssmmm    
          Reg32,CRn     000011110010000011sssrrr    
          CRn,Reg32     000011110010001011sssrrr    
          Reg32,DRn     000011110010000111sssrrr    
          DRn,Reg32     000011110010001111sssrrr    
          Reg32,TRn     000011110010010011sssrrr    
          TRn,Reg32     000011110010011011sssrrr

    表中rrr,w,mmm,oo都可以看做几个变量,会根据寄存器,和寻址方式的变化而变化,如果使用4位寄存器,比如al,ah,bl,bh等,则其值为0,否则为1,表<x86操作码查询表>可以查到,注意所查的结果中已经包含了后面的ModR/M字节

    ModR/M和SIB:

    这两个字节共同决定了寻址方式,ModR/M包含三个部分oo|rrr|mmm:这三个部分联合表示了寻址方式,oo指示了寻址模式,rrr:指明所用寄存器,注意使用<x86操作码查询表>查询得到的结果里已经包含ModR/M字节,而SIB是辅助的寻址方式确定位,也包含三个部分

    • ss:放大倍数
    • iii:变址寄存器
    • bbb:基址寄存器

    比如如果用到这样的地址[ebp+5*esi],则ebp为基址寄存器,esi为变址寄存器,5为放大倍数

    Displayment偏移量位:寻址方式中的偏移量,如[ebp+5]中的5

    Immediate:立即数,操作数中的立即数

     

    一起练练手:人肉翻译汇编代码

    一) mov bx,cx 

    查询其操作码为1000 100w,由于使用16位寄存器,则w=1 得到100010001即16进制的89H

    ModR/M包含三个部分oo|rrr|mmm:这三个部分联合表示了寻址方式,这里由于没有内存寻址,查表得,oo=11,rrr和mmm各表示 一个寄存器,那么问题来了:哪个表示目的寄存器bx,哪个表示源寄存器cx呢?翻文档太累了,不如用nasm汇编一下这条指令瞧瞧.得到的ModR/M字 节为对应寄存器代码可以看出来,rrr表示的是源寄存器bx,则这一个字节为:11 001 011,即16进制CBH

    由于这条语句没有内存寻址,SIB列为空,也没有偏移量列Displayment,这条语句也没有立即数作为操作数,所以Immediate列为空

    至于Prefix列,我们稍微看下Prefix的说明和他的值表就能知道,Prefix列只有少数的几种情况才能出现,比如段超越啊,16位/32位切换啊,锁定啊,像mov bx,cx这样普通的语句自然也没有Prefix列

    所以我们可以得到mov bx,cx的最终代码为

      Prefix Opcode ModR/M
    oo|rrr|mmm
    SIB
    ss|iii|bbb
    Displayment Immeidate
    mov bx,cx   100010001 11 001 011      
    mov cx,bx            
    mov cl,bl            

    既然已经掌握了mov bx,cx,那么mov cx,bx呢? mov cl,bl呢?大家自己想想

    如果觉得上面例子还是太简单了,毕竟6列只用了2列,那么我们就来挑战一个有点难度的怎么样

    二) mov [ebx+esi+1],dword 00h

    word是nasm的关键字,表明存入内存的操作数是一个双字,在内存中占32位,即4个字节

    查询Opcode,得1100011w,w=1,即C7

    现在来看ModR/M,这里会有些变化了,我们要仔细分析我们的内存寻址方式ebx+esi+1,有一个8位的偏移量1,所以oo=01,后面的 rrr和mmm该指明用于寻址的两个寄存器,ebp和esi,查询rrr表,应该分别是011,110,则rrr=011,mmm=110,但是我偏偏不 这样作,我设置rrr为000(EAX),mmm为100(ESP),于是代码变为了01000100,44h

    奇怪?明明是ebx+esi,怎么偏偏让你给变成了eax+esp了?

    其实在查询mmm的时候,我们不应该查询rrr表,应该查询iii表,iii表是专门查询变址寄存器号码的,rrr表和iii表基本上完全相同,只 是rrr表中100代表ESP,而iii表中呢…..no index….,这不是表示没有变址寄存器,而是表示设置两个寄存器的工作交给后面的SIB来做,44h可以看做是个特殊的数字,这个数字就表明寻址 方式所用的寄存器会让SIB位来完成.

    上面的做法不是我别出心裁,其实如果你用nasm编译这句话,也会得到这个结果,让SIB来设置内存寻址,我想至少有两个好处,

    一是可以更加灵活一些,毕竟人家SIB有整整一个字节专门来作这件事情,比如如果寻址模式位改为ebx+esi*2+1,SIB里专门有两位ss,表示这个倍数,而ModR/M里呢,对不起,没地方放了

    二是可以让汇编编译器简单一些:统一成一种格式方便处理

    ok,那么如果我们严格按照寄存器查表的结果(ebx=011,esi=110)能不能运行呢,大家自己去试试吧
    SIB
    ss:没有倍数,ss=00
    iii:刚才查过了esi=110
    bbb:ebx=011
    合起来是00110011即33

    后面是8位的偏移量,01h,最后是立即数00h,注意这里是个双字,所以占4个字节

    填在表里

      Prefix Opcode ModR/M
    oo|rrr|mmm
    SIB
    ss|iii|bbb
    Displayment Immeidate
    mov [ebx+esi+1],dword 00h 67,66 C7 44 33 01 0000
                 
                 

    你可能用nasm汇编了一下这条语句,发现前面多了个67,66,恭喜你,67和66正是Prefix,由于你是在16位环境下汇编的,所以如果某 条指令使用到32位的数据和地址,指令前面就会出现前缀,67表示使用了32位地址,66表示使用了32位数据.消除的方法是在文件头上加上[BITS 32]

    推荐一个好的机器码入门<老罗的OPCODE教程:http://www.luocong.com/learningopcode.htm>,x86 OPCODE规范下载<>

    让人迷惑的倒置 -LittleEndian

    参见上面的代码,MOV到ax的操作数为16位二进制的一,即0001h(h表示16进制)可是从这里看上去,是0100h,这是为什么呢?

    其实这是著名的Little Endian存储格式捣的鬼,Little Endian的意思是高位在高地址,低位在低地址,比如0100 0011 0010 0001这个二进制数(十六进制为4321h),在内存里类似

    位置 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
    1 0 0 0 0 1 0 0 1 1 0 0 0 0 1 0

     

    显示的时候,显示程序一般都以一个字节为整体显示这个数,即先解析处0-7位,为数字21h,显示在前面,然后解析8-16位,为数据43h,显示 在后面,则变为了21h 43h,如果显示程序能按照字为整体解析并显示,就能没有这个倒装了,但是显示是不会知道你到底需要怎么显示的,比如你可以定义一个32位数据,也可能定 义64位数据,即使是按照16位,也仍然会有倒装发生,所以现在一般显示程序都简单按照字节显示

    除了LittleEndian反过来当然也有BigEndian,这种存储格式就和咱平时的数字理解习惯没有冲突了

    LittleEndian是Intel x86(8086/8088,80286,80×86,PentiumX)系列CPU所采用的格式,而BigEndian是Motorola的 PowerPC系列CPU所采用的标准,网络传输也采用BigEndian,二者各有优缺点,有兴趣的读者可以参考1980年的著名论文<On Holy Wars and a Plea for Peace>

    别看LittleEndian这个是个细节,却绊倒了不少初学者的腿,比如你刚打开Windbg,想尝试利用调试工具修改某个游戏角色的体力值,从 157110修改为100000000,157110的16进制为265B6,而你在内存里怎么都找不到02 65 B6这个序列,那就是LittleEndian搞的鬼

  • 据Jargon File记载,endian这个词来源于Jonathan Swift在1726年写的讽刺小说 "Gulliver's Travels"(《格利佛游记》)。该小说在描述Gulliver畅游小人国时碰到了如下的一个场景。在小人国里的小人因为非常小(身高6英寸)所以总 是碰到一些意想不到的问题。有一次因为对水煮蛋该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开的争论而引发了一场战争,并 形成了两支截然对立的队伍:支持从Big- End剥开的人Swift就称作Big-Endians而支持从Little-End剥开的人就称作Little-Endians……(后缀ian表明的 就是支持某种观点的人:-)。Endian这个词由此而来。
    1980年,Danny Cohen在其著名的论文"On Holy Wars and a Plea for Peace"中为了平息一场关于在消息中字节该以什么样的顺序进行传送的争论而引用了该词。该文中,Cohen非常形象贴切地把支持从一个消息序列的 MSB开始传送的那伙人叫做Big-Endians,支持从LSB开始传送的相对应地叫做Little-Endians。此后Endian这个词便随着这 篇论文而被广为采用。

    思考:指令的起止

    既然每条指令都可能不一样常,我们的CPU怎么知道每条指令从哪里开始,到哪里结束?

    要知道变长指令的起止,系统就必须自己知道各个指令的长度,可以说系统内部有个登记簿,登记了每个指令的长度.

    程序执行的时候,系统会把eip指向的指令加载到cpu,cpu会尝试翻译指令,这样系统会知道这条指令的长度,比如长 度为6,则将eip增加6,指向下一条语句.如何正确计算指令长度本身是采用CISC(复杂指令集)计算机特有的问题,因为使用RISC(精简指令集)的 cpu,他的指令长度是固定的,让指令变长的优势在于可以节省空间,也方便以后的扩展,缺点是cpu实现会比较复杂

  • 输出结果

    也许你觉得虽然cpu已经执行了我们的工作,但是由于看不到结果,不能满足我们小小的虚荣心,那么下面我们就告诉系统,让他把结果展示在屏幕上

    打开刚才建立的test.com,在刚才的程序后面附加上下面这段

    04 30 88 C2 B4 02 CD 21 E9 FD FF

    程序变为:

    B8 01 00 05 01 00 04 30 88 C2 B4 02 CD 21 E9 FD FF

    保存运行一下看看是不是输出了结果

    感觉好多了吧,至少看见了自己劳动的结晶,后面附加的那段机器码是调用了Dos的int 21中断输出了一个字符,我们直接给出他对应的汇编代码

    mov ax,1
    add ax,1
    add al,'0'    ;数字到ascii的粗糙转换
    mov dl,al    ;—–|
    mov ah,02h;—–|–调用中断
    int 21h      ;—–|
    jmp $        ;保证程序不会立即退出,好让我们看到结果

    image

    从上面的图上我们可以清晰的看到机器码和汇编指令的对应关系,不再赘述

    add al,'0',是把结果转化成ascii,'0'的值为30h,2+30h=32h,是'2'这个字符的ascii值,当然这是个非常粗糙的转换,一旦数字大过9,就会输出奇怪的结果,这样作是为了机器码尽量简单,方便大家输入

    通过上面的二进制编码与汇编代码的对比,我们大概能知道汇编和机器指令是一一对应的,但是由于机器指令实在是太不方便人类记忆,写起来也非常繁琐,所以需要汇编语言,也就是说汇编语言实际上是机器语言的助记符号

     

    总结

     

  • 我们会算1+1了

  • [C#]C#.net同步异步SOCKET通讯和多线程总结

    mikel阅读(736)

    同步套接字通信

    Socket支持下的网上点对点的通信

    服务端实现监听连接,客户端实现发送连接请求,建立连接后进行发送和接收数据的功能

    服务器端建立一个socket,设置好本机的ip和监听的端口与socket进行绑定,开始监听连接请求,当接收到连接请求后,发送确认,同客户端建立连接,开始与客户端进行通信。

    客户端建立一个socket,设置好服务器端的IP和提供服务的端口,发出连接请求,接收到服务的确认后,尽力连接,开始与服务器进行通信。

    服务器端和客户端的连接及它们之间的数据传送均采用同步方式。

     

    Socket

    Socket是tcp\ip网络协议接口。内部定义了许多的函数和例程。可以看成是网络通信的一个端点。在网络通信中需要两个主机或两个进程。通过网络传递数据,程序在网络对话的每一端需要一个socket。

           Tcp/IP传输层使用协议端口将数据传送给一个主机的特定应用程序,协议端口是一个应用程序的进程地址。传输层模块的网络软件模块要于另一个程序通信, 它将使用协议端口,socket是运行在传输层的api,使用socket建立连接发送数据要指定一个端口给它。

    Socket:

    Stream Socket流套接字 Socket提供双向、有序、无重复的数据流服务,出溜大量的网络数据。

    Dgram socket数据包套接字 支持双向数据流,不保证传输的可靠性、有序、无重复。

    Row socket 原始套接字 访问底层协议

    建立socket 用C#

    命名空间:using System.Net;using System.Net.Socket;

    构造新的socket对象:socket原型:

    Public socket (AddressFamily addressFamily,SocketType sockettype,ProtocolType protocolType)

    AddressFamily 用来指定socket解析地址的寻址方案。Inte.Network标示需要ip版本4的地址,Inte.NetworkV6需要ip版本6的地址

    SocketType参数指定socket类型Raw支持基础传输协议访问,Stream支持可靠,双向,基于连接的数据流。

    ProtocolType表示socket支持的网络协议

     

    定义主机对象:

    IPEndPoint类:IPEndPoint构造方法  位置:System.Net

    原型:1)   public IPEndPoint(IPAddress address,int port)     2)public IPEndPoint(long address,int port) 参数1整型int64如123456,参数2端口int32

    主机解析:

    利用DNS服务器解析主机,使用Dns.Resolve方法

    原型:public static IPHostEntry Resolve(string hostname) 参数:待解析的主机名称,返回IPHostEntry类值,IPHostEntry为Inte.Net主机地址信息提供容器,该容器提供存有IP地址列 表,主机名称等。

    Dns.GetHostByName获取本地主机名称

    原型:public static IPHostEntry GetHostByName(string hostname)

    GetHostByAddress

    原型:1)public static IPHostEntry GetHostByAddress(IPAddress address) 参数:IP地址 2)public static IPHostEntry GetHostByAddress(string address) IP地址格式化字符串

     

    端口绑定和监听:

    同步套接字服务器主机的绑定和端口监听

    Socket类的Bind(绑定主机),Listen(监听端口),Accept(接收客户端的连接请求)

    Bind:原型:public void Bind(EndPoint LocalEP)参数为主机对象 IPEndPoint

    Listen:原型:public void Listen(int backlog) 参数整型数值,挂起队列最大值

    accept:原型:public socket accept() 返回为套接字对象

    演示程序:

    IPAddress myip=IPAddress.Parse(“127.0.0.1”);

    IPEndPoint myserver=new IPEndPoint(myip,2020);

    Socket sock=new Socket(AddressFamily.Inte.Network,SocketType.Stream,ProtocolType.Tcp);

    Sock.Bind(myserver);

    Sock.Listen(50);

    Socket bbb=sock.Accept();

    发送数据:方法1:socket类的send方法二.NetworkStream类Write

    send原型:public int Send(byte[] buffer) 字节数组 

    public int Send(byte[],SocketFlags)原型2说明,SocketFlags成员列表:DontRoute(不使用路由表发 送),MaxIOVectorLength(为发送和接收数据的wsabuf结构数量提供标准值)None 不对次调用使用标志) OutOfBand(消息的部分发送或接收)Partial(消息的部分发送或接收) Peek(查看传入的消息)

    原型三:public int Send(byte[],int,SocketFlags) 参数二要发送的字节数

    原型四:public int Send(byte[],int,int,SocketFlags) 参数二为Byte[]中开始发送的位置

    演示:

    Socket bbb=sock.Accept();

    Byte[] bytes=new Byte[64];

    string send="aaaaaaaaaaaa";

    bytes=System.Text.Encoding.BigEndianUnicode.GetBytes(send.ToCharArray());

    bbb.Send(bytes,bytes.length,0);//将byte数组全部发送

    .NetWordStream类的Write方法发送数据

    原型:public override void write(byte[] buffer,int offset,int size) 字节数组,开始字节位置,总字节数

     

    Socket bbb=sock.Accept();

    .NetWorkStream stre=new NewWorkStream(bbb);

    Byte[] ccc=new Byte[512];

    string sendmessage="aaaaaaaaaaaaaa";

    ccc=System.Text.Encoding.BigEndianUnicode.GetBytes(sendmessage);

    stre.Write(ccc,0,ccc.length);

    接收数据:Socket类Receive.NetworkStream类Read

    Socket类Receive方法

    原型:public int Receive(byte[] buffer)   

    2)public int Receive(byte[],SocketFlags)

    3)public int Receive(byte[],int,SocketFlags)   

    4)public int Receive(byte[],int,int,SocketFlags)

    …..

    Socket bbb=sock.Accept();

    ……..

    Byte[] ccc=new Byte[512];

    bbb.Receive(ccc,ccc.Length,0);

    string rece=System.Text.Encoding.BigEndianUnicode.GetString(ccc);

    richTextBox1.AppendText(rece+"\r\n");

     

    .NetworkStream类的Read方法接收数据

    public override int Read(int byte[] buffer,int offset,int size)

     

    演示:bbb=sock.Accept();

    …….

    .NetworkStream stre=new.NetworkStream(bbb);

    Byte[] ccc=new Byte[512];

    stre.Read(ccc,0,ccc.Length);

    string readMessage=System.Text.Encoding.BigEndianUnicode.GetString(ccc);

    线程

    线程创建:System.Threading空间下的Thread类的构造方法:

    原型:public Thread(ThreadStart start) ThreadStart类型值     

           Thread thread=new Thread(new ThreadStart(accp));

           Private void accp(){}//使用线程操作

    线程启动

    Thread thread=new Thread(new ThreadStart(accp));

    线程暂停与重新启动

    启动线程使用Thread.Sleep是当前线程阻塞一段时间Thread.Sleep(Timeout.Infinite)是线程休眠,直到被调用Thread.Interrrupt的另一个线程中断或被Thread.Abort中止。

    一个线程不能对另一个调用Sleep,可以使用Thread.Suspend来暂停线程,当线程对自身 调用Thread.Suspend将阻塞,直到该线程被另一个线程继续,当一个线程对另一个调用,该调用就成为使另一个线程暂停的非阻塞调用。调用 Thread.Resume使另一个线程跳出挂起状态并使该线程继续执行,而与调用Thread.Suspend的次数无关

    线程休眠:Thread.Sleep(10000);

    线程挂起:Thread thread=new Thread(new ThreadStart(accp));

                    Thread.start();

                    Thread.Suspend();

    重新启动:Thread thread=new Thread(new ThreadStart(accp));

                   Thread.start();

                   Thread.Suspend();

                   Thread.Resume();

    阻塞线程的方法:thread.Join使用一个线程等待另一个线程停止

    Thread.Join

    Public void Join();

    Public void Join(int millisecondsTimeout);毫秒

    Public bool Join(TimeSpan timeout);时间间隔类型值

    实例:Thread thread=new Thread(new ThreadStart(accp));

                  Thread.start();

                  Thread.Join(10000);

    线程销毁:

    Thread.Abort,Thread.Interrupt

    Abort方法引发ThreadAbortException,开始中止此线程的过程,是一个可以由应 用程序代码捕获的特殊异常,ResetAbort可以取消Abort请求,可以组织ThreadAbortException终止此线程,线程不一定会立 即终止,根本不终止。

    对尚未启动的线程调用Abort,则当调用Start时该线程将终止。对已经挂起的线程调用Abort,则该线程将继续,然后终止。对阻塞或正在休眠的线程调用Abort,则该线程被中断,然后终止。

    Thread类的Abort方法:

    Public void Abort()

    Public void Abort(object stateinfo);

    演示:

    Thread thread=new Thread(new ThreadStart(accp));

    Thread.Start();

    Thread.Abort();

    Thread.Join(10000);

     

    Socket编程原理:

    Unix的i/o命令集,模式为开-读/写-关 open write/read close

    用户进程进行i/o操作

    用户进程调用打开命令,获取文件或设备的使用权,并返回描述文件或设备的整数,以描述用户打开的进程,该进程进行读写操作,传输数据,操作完成,进程关闭,通知os对哪个对象进行了使用。

    Unix网络应用编程:BSD的套接字socket,unix的System V 的TLI。

    套接字编程的基本概念:

    网间进程通信:源于单机系统,每个进程在自己的地址范围内进行运行,保证互相不干扰且协调工作。操作系统为进程之间的通信提供设施:

    Unix BSD 管道pipe,命名管道named pipe软中断信号signal

    Unix System V 消息message 共享存储区 shared memory 信号量semaphore

    以上仅限于本机进程之间通信。

    端口:网络上可以被命名和寻址的通信端口,是操作系统可以分配的一种资源,网络通信的最终地址不是主机地址,是可以描述进程的摸中标识符。TCP/IP提出协议端口porotocol port端口,表示通信进程。

           进程通过os调用绑定连接端口,而在传输层传输给该端口的数据传入进程中处理,同样在进程的数据需要传给传输层也是通过绑定端口实现。进程对端口的操作相 当于对os中的i/o文件进行操作,每一个端口也对应着一个端口号,tcp/ip协议分为tcp和udp,虽然有相同port number的端口,但是互相也不冲突。 端口号的分配有全局分配,本地分配(动态分配),当进程需要访问传输层,os分配给进程一个端口号。全局分配,就是os固定分配的端口,标准的服务器都有 固定的全局公认的端口号提供给服务。小于256的可以作为保留端口。

           地址:网络通信中的两台机器,可以不再同一个网络,可能间隔(网关,网桥,路由器等),所以可以分为三层寻址

    机器在不同的网络则有该网络的特定id

    同一个网络中的机器应该有唯一的机器id

    一台机器内的进程应该有自己的唯一id

    通常主机地址=网络ID+主机ID  tcp/ip中使用16位端口号来表示进程。

    网络字节顺序,高价先存,tcp和udp都使用16或32整数位的高价存储,在协议的头文件中。

    半相关:在网络中一个进程为协议+本地地址+端口号=三元组,也叫半相关,表示半部分。

    全相关:两台机器之间通信需要使用相同协议

                  协议+本地地址+本地端口号+远程地址+远程端口号 五元组 全相关。

    顺序:两个连续的报文在网络中可能不会通过相同的路径到达,所以接收的顺序会和发送的顺序不一致。顺序是接收顺序与发送顺序一致。Tcp/ip提供该功能。

    差错控制:检查数据差错:检查和CheckSum机制 检查连接差错:双方确认应答机制。

    流控制:双方传输数据过程中,保证数据传输速率的机制,保证数据不丢失。

    字节流:把传输中的报文当作一个字节序列,不提供任何数据边界。

    全双工/半双工:两个方向发送或一个方向发送

    缓存/带外数据:字节流服务中,没有报文边界,可以同一时刻读取任意长度的数据。为保证传输正确或流协议控制,需要使用缓存,交互型的应用程序禁用缓存。

    数据传送中,希望不通过常规传输方式传送给用户以便及时处理的某一类信息(unix系统的中断键delete,Control-c)、终端流控制符Control-s、Control-q)为带外数据。

    客户/服务器模式主动请求方式:

    1.       打开通信通道,通知本地主机,在某一个公认地址上接收客户请求

    2.       等待客户请求到达端口

    3.       接收到重复服务请求,处理请求发送应答信号。接收到并发服务请求。要激活一个新进程处理客户请求,unix系统fork、exec,新进程处理客户请求,不需要对其他请求作出应答,服务完成后,关闭此进程与客户的通信链路。终止

    4.       返回第二步,等待另一个客户请求。

    5.       关闭服务端

    客户方:

    1.       打开一通信通道,并连接到服务器所在主机的特定端口。

    2.       向服务器发送服务请求报文,等待并接收应答;继续提出请求…….

    3.       请求结束以后关闭通信通道并终止。

    1.       客户与服务器进程的作用非对称,编码不同

    2.       服务进程先于客户请求而启动,系统运行,服务进程一致存在,直到正常退出或强迫退出

    套接字类型:

    TCP/IP的socket

    Sock_stream可靠的面对连接数据传输,无差错、无重复发送,安照顺序发送接收,内设流量控制,避免数据流超限,数据为字节流,无长度限制,ftp流套接字。

    Sock_DGRAM 无连接的服务,数据包以独立包的形式发送,不提供无措保证,数据可能丢失重复,发送接收的顺序混乱,网络文件系统nfs使用数据报式套接字。

    Sock_Ram 接口允许较底层协议,IP,ICMP直接访问,检查新的协议实现或访问现有服务中配置的新设备。

    服务端:

    using System.Net;

    using System.Net.Sockets;

    using System.Text;

    using System.Threading;

    Thread mythread ;

    Socket socket;

    // 清理所有正在使用的资源。

            protected override void Dispose( bool disposing )

             {

                  try

                 {

                socket.Close();//释放资源

                mythread.Abort ( ) ;//中止线程

                 }

                 catch{ }

                 if( disposing )

                  {

                       if (components != null)

                       {

                           components.Dispose();

                       }

                  }

                  base.Dispose( disposing );

             }       

             public static IPAddress GetServerIP()

             {

                  IPHostEntry ieh=Dns.GetHostByName(Dns.GetHostName());

                  return ieh.AddressList[0];

             }

             private void BeginListen()

             {

                  IPAddress ServerIp=GetServerIP();

                  IPEndPoint iep=new IPEndPoint(ServerIp,8000);

                  socket=new

                           Socket(AddressFamily.Inte.Network,SocketType.Stream,ProtocolType.Tcp);

                  byte[] byteMessage=new byte[100]; 

                  this.label1.Text=iep.ToString();

                  socket.Bind(iep); 

    //            do

                  while(true)

                  {

                       try

                       {

                           socket.Listen(5);

                           Socket newSocket=socket.Accept();

                           newSocket.Receive(byteMessage);

                           string sTime = DateTime.Now.ToShortTimeString ( ) ;

    string msg=sTime+":"+"Message from:";

    msg+=newSocket.RemoteEndPoint.ToString()+Encoding.Default.GetString(byteMessage);

                           this.listBox1.Items.Add(msg);

                       }

                       catch(SocketException ex)

                       {

                           this.label1.Text+=ex.ToString();

                       }

                  }

    //            while(byteMessage!=null);

             }

             //开始监听

             private void button1_Click(object sender, System.EventArgs e)

             {

                  try

                  {

                       mythread = new Thread(new ThreadStart(BeginListen));

                       mythread.Start();

                  }

                  catch(System.Exception er)

                  {

                       MessageBox.Show(er.Message,"完成",MessageBoxButtons.OK,MessageBoxIcon.Stop);

                  }

             }

    客户端:

    using System.Net;

    using System.Net.Sockets;

    using System.Text;

             private void button1_Click(object sender, System.EventArgs e)

             {

                  BeginSend();      

             }

             private void BeginSend()

             {            

                  string ip=this.txtip.Text;

                  string port=this.txtport.Text;

                  IPAddress serverIp=IPAddress.Parse(ip);           

                  int serverPort=Convert.ToInt32(port);

                  IPEndPoint iep=new IPEndPoint(serverIp,serverPort); 

                  byte[] byteMessage; 

    //            do

    //            {

                       Socket socket=new Socket(AddressFamily.Inte.Network,SocketType.Stream,ProtocolType.Tcp);

                       socket.Connect(iep);

                       byteMessage=Encoding.ASCII.GetBytes(textBox1.Text);

                       socket.Send(byteMessage);

                       socket.Shutdown(SocketShutdown.Both);

                       socket.Close();

    //            }

    //            while(byteMessage!=null);

             }

    基于TCP协议的发送和接收端

     

                                                                                                                                                张海

                                                                                                                                            2009/03/13

    [Flex]FMS3系列(三):创建基于FMS的流媒体播放程序,看山寨帮的山寨传奇

    mikel阅读(813)

       本文主要介绍怎么去创建基于FMS的流媒体播放程序,Flash客户端通过网络加载FMS服务器上的视频流文件(.flv,.mp4等),实现视频流的播放。

         要实现媒体流文件的播放是非常简单的,只要在FMS服务器上提供好流媒体文件,Flash客户端通过NetConnection连接到 FMS服务器,然后通过NetStream加载就OK。关于怎么连接FMS在本系列的前两篇已有详细介绍,首先得在fms上建立好服务器应用并部署好媒体 文件,如下图示:

               

     

         下面是在Flash中开发的流媒体文件播放示例程序:

     1 import flash.display.*;
     2 import flash.events.*;
     3 import flash.net.*;
     4 
     5 var nc:NetConnection = new NetConnection();
     6 var ns:NetStream;
     7 var video:Video;
     8 
     9 nc.connect("rtmp://localhost/PlayStreams");
    10 nc.addEventListener(NetStatusEvent.NET_STATUS,onStatusHandler);
    11 
    12 function onStatusHandler(evt:NetStatusEvent):void
    13 {
    14     trace(evt.info.code);
    15     if(evt.info.code=="NetConnection.Connect.Success")
    16     {
    17         ns=new NetStream(nc);
    18         ns.addEventListener(NetStatusEvent.NET_STATUS,onStatusHandler);
    19         ns.client=new CustomClient();
    20         video=new Video();
    21         video.attachNetStream(ns);
    22         ns.play("2009031301",0);
    23         addChild(video);
    24     }
    25 }

         看看上面的程序代码是不是非常简单,现在我对上面的代码进行详细的分析。程序从上到下思路很清晰,首先将程序中需要的相关包导入,然后定义了连接对象(NetConnection),流对象(NetStream)和视频对象(Video)。

         通过NetConnection的connect方法连接到fms服务器(rtmp://localhost/PlayStreams),并添加网络连接的事件处理函数,在此函数内判断网络连接状态,如果连接成功(连接状态:NetConnection.Connect.Success)则通过NetStream建立视频流,调用NetStream的play方法播放指定的流媒体文件,然后将流附加到视频对象并显示在flash界面上。如下图示:

              

         OK,我们已经实现了流媒体文件的播放,下面我们来扩展程序的功能,为前面的视频播放程序加上播放、暂停、停止以及重新播放等功能。这时可以在界面上放置几个按扭来驱动这些功能,添加按扭代码如下(当然也可以直接拖拽Botton组件):

     1 var btnPlay:Button=new Button();
     2 btnPlay.x=10;
     3 btnPlay.y=250;
     4 btnPlay.width=50;
     5 btnPlay.label="播放";
     6 btnPlay.addEventListener(MouseEvent.CLICK,onPlayHandler);
     7 addChild(btnPlay);
     8 
     9 var btnPause:Button=new Button();
    10 btnPause.x=80;
    11 btnPause.y=250;
    12 btnPause.width=50;
    13 btnPause.label="暂停";
    14 btnPause.addEventListener(MouseEvent.CLICK,onPauseHandler);
    15 addChild(btnPause);
    16 
    17 var btnStop:Button=new Button();
    18 btnStop.x=150;
    19 btnStop.y=250;
    20 btnStop.width=50;
    21 btnStop.label="停止";
    22 btnStop.addEventListener(MouseEvent.CLICK,onStopHandler);
    23 addChild(btnStop);
    24 
    25 var btnReplay:Button=new Button();
    26 btnReplay.x=220;
    27 btnReplay.y=250;
    28 btnReplay.width=80;
    29 btnReplay.label="重新播放";
    30 btnReplay.addEventListener(MouseEvent.CLICK,onReplayHandler);
    31 addChild(btnReplay);
    32 
    33 function onPlayHandler(evt:MouseEvent):void
    34 {}
    35 
    36 function onPauseHandler(evt:MouseEvent):void
    37 {}
    38 
    39 function onStopHandler(evt:MouseEvent):void
    40 {}
    41 
    42 function onReplayHandler(evt:MouseEvent):void
    43 {}

     

         这里我们需要对上面的代码进行一下重构,将流和控制视频播放的代码重构为方法,以便在重新播放的时候直接调用。

     

     1 function playStream():void
     2 {
     3     ns=new NetStream(nc);
     4     ns.addEventListener(NetStatusEvent.NET_STATUS,onStatusHandler);
     5     ns.client=new CustomClient();
     6     video=new Video();
     7     video.attachNetStream(ns);
     8     ns.play("2009031302",0);
     9     addChild(video);
    10 }

     

         上面我们已经将控制视频播放、暂停、停止和重新播放的按扭构造在了flash界面上,现在只需要完成这些按扭的功能就是,要实现视频的播放、暂停、停止和重新播放同样是非常简单的,NetStream为我们提供了可直接调用的API。详细如下:

     1 function onPlayHandler(evt:MouseEvent):void
     2 {
     3     ns.resume();
     4 }
     5 
     6 function onPauseHandler(evt:MouseEvent):void
     7 {
     8     ns.pause();
     9 }
    10 
    11 function onStopHandler(evt:MouseEvent):void
    12 {
    13     ns.close();
    14 }
    15 
    16 function onReplayHandler(evt:MouseEvent):void
    17 {
    18     ns.close();
    19     playStream();
    20 }

     

         一切搞定 ,可以按下Ctrl+Enter测试了,看到了flash已经成功的加载到了fms上的视频文件(.flv)。打开FMS管理控制台就会看到,在应 用"PlayStreams"下有一个NetStream的连接,代表当前应用有一个网络流正在传输,如下图:

     

    完整示例代码

     

         如果在Flex环境下开发,更方便实现,详细本文就不做介绍了,核心代码和Flash里开发是一样的。

    版权说明

      本文属原创文章,欢迎转载,其版权归作者和博客园共有。  

      作      者:Beniao

     文章出处:http://beniao.cnblogs.com/  或  http://www.cnblogs.com/

    [PHP]PHP动态生成flash动画

    mikel阅读(748)

    了解 Flash

    字串6

    Flash Player 是集成到运行 Microsoft® Windows®、Mac OS X 和 Linux® 的计算机的 Web 浏览器中的一个插件。截至本文完稿时,最新版本的 Flash Player 是 V8。它是可以免费获得的,大多数浏览器都附带安装了此插件。它十分流行并且具有优秀的客户机渗透力 —— 而这种渗透力随着 YouTube 和 Google Video 这类服务的出现得到了提高,这些服务都使用 Flash 显示视频流。

    字串9

    Flash Player 只是天平的一端。要发挥作用,Flash Player 还需要使用一个 Flash 动画。此类动画通常是使用一种 Flash 的开发工具编译的文件,其文件扩展名为 .swf。但正如您将在本文中看到的那样,还可以使用 Ming 库用几乎与动态创建图片相同的方法来动态构建 .swf 文件,并在 Web 服务器上绘制图形。Ming 库利用由 PHP 代码构建的对象和方法在新的 .swf 文件中构建操作代码。 字串6

    您可以通过两种方法中的任意一种方法来查看 Web 站点中的 .swf 文件。第一种方法只需导航到 .swf 文件的 URL。这样做将把 Web 服务器的整个内容区域替换为 Flash 动画。此方法便于进行调试,但主要的用法还是将动画嵌入到 HTML Web 页面的 标记中。该 标记然后再通过 URL 引用 SWF 动画。方法的优点在于您可以把动画放在页面的任意位置,并可通过JavaScript 代码进行动态控制,就像处理页面中的任何其他元素一样。

    字串1

    清单 1 显示的是一个引用 SWF 动画的 <object> 标记的示例。 字串6

    清单 1. 嵌入式 Flash 动画 字串8

    <OBJECT classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
    codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#
            version=6,0,40,0"
    WIDTH="550" HEIGHT="400">
    <PARAM NAME="movie" VALUE="lines.swf">
    <EMBED src="lines.swf" WIDTH="550" HEIGHT="400"
    TYPE="application/x-shockwave-flash"
    PLUGINSPAGE="http://www.macromedia.com/go/getflashplayer">
    </EMBED>
    </OBJECT> 字串1

    这组标记将引用一个名为 lines.swf 的动画。内部的 <embed> 标记用于确保 Flash 动画可以在安装了插件的各种浏览器中播放。 字串5

    标记还把 Flash Player 的高度和宽度分别指定为 550 像素和 400 像素。非常值得注意的是,Flash 动画中的图形都是基于矢量的,这意味着当您使用 Flash 命令绘制线条和文本时,那些元素都被存储为坐标并且按照匹配显示区域的比例进行缩放。如您所见,Flash 动画有自己的坐标系统,您可以按照适合自己的方法使代码尽可能整洁。 字串7

    Ming

    字串2

    本文中提供的使用 Flash 动画的第一种方法是使用 Ming 库动态生成它们。Ming 库是一个 PHP 库,其中有一组映射到 SWF 动画中的数据类型的对象:子图形、图形、文本、位图等等。我将不讨论如何构建和安装 Ming,因为其操作是特定于平台的而且并不特别简单(请参阅 参考资料)。在本文中,我使用了预编译的扩展 php_ming.dll 库用于 Windows 版本的 PHP。

    字串6

    必须指出的是,Ming 仍处于开发阶段。截至本文完稿时,库的版本是 V0.4,并且较老版本中的一些命令在最新版本中不能使用。我使用了 V0.4 撰写本文,因此,要使用这段代码,您需要使用这个版本。 字串9

    清单 2 显示了使用 Ming 库实现的 HelloWorld 示例。

    字串3

    清单 2. Hello.php

    字串9

    <?php
    $f = new SWFFont( '_sans' );
    $t = new SWFTextField();
    $t->setFont( $f );
    $t->setColor( 0, 0, 0 );
    $t->setHeight( 400 );
    $t->addString( 'Hello World' );
    $m = new SWFMovie();
    $m->setDimension( 2500, 800 );
    $m->add( $t );
    $m->save( 'hello.swf' );
    ?>

    字串2

    在命令行中运行这段代码将生成文件 hello.swf。当我在 Web 浏览器中打开该文件时, 字串8

      字串4

    回过头来查看这段代码,我做的第一件事是创建指向一个内置字体(_sans)的指针,然后创建文本字段,设定字体、颜色和大小,最后为其提供一些文 本内容(“Hello World”)。再接下来创建了一个 SWFMovie 对象并设定其尺寸。最后,向动画中添加了文本元素并将动画保存到文件中。

    字串6

    作为直接构建文件的替代性方法,也可以使用下面的代码,使 SWF 动画像页面那样输出,而无需使用 save 方法: 字串7

    header( 'Content-type: application/x-shockwave-flash' );
    $m->output( );

    字串9

    此过程类似于使用 PHP 中的 ImageMagick 库来构建位图。对于所有 Ming 示例,我都将使用 save 方法,但您可以根据喜好来选择是否使用 save 方法。

    字串7

    让文本动起来

    字串5

    只是将一些文本放入 Flash 动画中是没有多大意义的,除非您能让它动起来。因此我整合了清单 2 中的示例,它包括两段文本:一部分开始很小后来变得越来越大,而另一部分保持静态。

    字串9

    清单 3. Text.php 字串9

    <?php
    $f = new SWFFont( '_sans' );
    $pt = new SWFTextField();
    $pt->setFont( $f );
    $pt->setColor( 0, 0, 0 );
    $pt->setHeight( 400 );
    $pt->addString( '1000' );
    $tt = new SWFTextField();
    $tt->setFont( $f );
    $tt->setColor( 192, 192, 192, 90 );
    $tt->setHeight( 350 );
    $tt->addString( 'Points' );
    $m = new SWFMovie();
    $m->setDimension( 2500, 800 );
    $pts = $m->add( $pt );
    $pts->moveTo( 0, 0 );
    $tts = $m->add( $tt );
    $tts->moveTo( 1300, 200 );
    for( $i = 0; $i < 10; $i++ ) {
      $m->nextframe();
      $pts->scaleTo( 1.0 + ( $i / 10.0 ), 1.0 + ( $i / 10.0 ) );
    }
    $m->save( 'text.swf' );
    ?> 字串2