我们在使用 ActionScript3.0 进行 Socket 编程的时候需要关注下面的问题 , 我们将在今后的学习中逐个对下面的问题进行讨论 , 并尽量逐渐的改进我们的程序 .
1. 与 Socket 服务器建立连接 .
2. 向 Socket 服务器发送数据 .
3. 从 Socket 服务器读数据 .
4. 同 Socket 服务器进行握手 , 并确定收到了什么样的数据和如何处理这些数据 .
5. 与 Socket 服务器断开 , 或者当服务器想与你断开的时候发消息给你 .
6. 处理使用 Sockets 时候引发的错误 .
ActionScript3.0 Socket 编程 (1)
与 Socket 服务器建立连接 .
解决方法 :
我们通过调用 Socket.connect( ) 或者 XMLSocket.connect( ) 方法并监听网络连接的事件消息 .
讨论 :
连接一台 Socket 服务器你需要确定两个信息 , 一个是 Socket 服务器的域名或者 IP 地址 , 另一个是服务器监听的端口号 .
无论你使用的是 Socket 还是 XMLSocket 类的实例 , 连接请求都是完全的一样的 , 两个类都是使用一个名叫 connect() 的方法 , 该方法有两个参数 :
host :
该参数为字符串类型 , 可以是一个域名 , 例如 "www.example.com", 也可以是一个 IP 地址 , 例如 "192.168.1.101". 如果 Socket 服务器与你该 Flash 影片发布的 Web 服务器是同一个 , 该参数为 Null.
port :
该参数为一个表示 Socket 服务器监听端口的 int 值 . 该值最小为 1024. 除非在服务器中有一个 policy 文件 , 用于指定允许端口号小于 1024.
因为 Flash Socket 编程是一个异步的过程 ,connect() 方法不会等到一个连接完成后再执行下一行代码的执行 . 如果你想在一个连接完全执行完之前与一个 Socket 完全绑定 , 那么你将会得到一个意想不到的结果 , 并且你当前的代码将不能工作 .
在尝试一个新的 Socket 连接的时候我们最好先添加一个连接事件监听器 . 当一个连接建立成功 ,Socket 或者 XMLSocket 会发出一个连接事件 , 这就可以让你知道交互已经准备好了 .
下面举了一个 Socket 实例与本地 Socket 服务器的 2900 端口建立连接的例子 :
package {
import flash.display.Sprite;
import flash.events.*;
import flash.net.Socket;
public class SocketExample extends Sprite {
private var socket:Socket;
public function SocketExample( ) {
socket = new Socket( );
// Add an event listener to be notified when the connection
// is made
socket.addEventListener( Event.CONNECT, onConnect );
// Connect to the server
socket.connect( "localhost", 2900 );
}
private function onConnect( event:Event ):void {
trace( "The socket is now connected..." );
}
}
}
如果你想通过 XMLSocket 与服务器建立连接代码也是基本一样的 . 首先你创建了一个连接事件监听器 , 然后调用 connect() 方法 . 所不同的是 Socket 实例改为了 XMLSocket:
package {
import flash.display.Sprite;
import flash.events.*;
import flash.net.XMLSocket;
public class SocketExample extends Sprite {
private var socket:XMLSocket;
public function SocketExample( ) {
socket = new XMLSocket( );
// Add an event listener to be notified when the connection is made
socket.addEventListener( Event.CONNECT, onConnect );
// Connect to the server
socket.connect( "localhost", 2900 );
}
private function onConnect( event:Event ):void {
trace( "The xml socket is now connected..." );
}
}
}
如果连接失败 , 可那是下面两种原因的一种 : 一种是连接立即失败和运行时错误 , 另一种是如果无法完成连接从而产生一个 ioError 或者 securityError 事件 . 关于错误事件处理信息的描述 , 我们打算改日讨论 .
请牢记 , 当与一个主机建立一个 Socket 连接时 ,Flash Player 要遵守如下安全沙箱规则 .
1.Flash 的 .swf 文件和主机必须严格的在同一个域名 , 只有这样才可以成功建立连接 .
2. 一个从网上发布的 .swf 文件是不可以访问本地服务器的 .
3. 本地未通过认证的 .swf 文件是不可以访问任何网络资源的 .
4. 你想跨域访问或者连接低于 1024 的端口 , 必须使用一个跨域策略文件 .
如果尝试连接未认证的域或者低端口服务 , 这样就违反了安全沙箱策略 , 同时会产生一个 securityError 事件 . 这些情况都可以通过使用一个跨域策略 文件解决 . 无论是 Socket 对象还是 XMLSocket 对象的策略文件 , 都必须在连接之前通过使用 flash.system.Security.loadPolicyFile() 方法载入策略文件 . 具体如下 :
Security.loadPolicyFile("http://www.rightactionscript.com/crossdomain.xml");
获得的改策略文件不仅定义了允许的域名 , 还定义了端口号 . 如果你不设置端口号 , 那么 Flash Player 默认为 80 端口 (HTTP 协议默认端口 ). 在 <allow-access-from> 标签中可以使用逗号隔开设置多个端口号 . 下 面这个例子就是允许访问 80 和 110 端口 .
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*" to-ports="80,110" />
</cross-domain-policy>
ActionScript3.0 Socket 编程 (2)
2. 向 Socket 服务器发送数据 .
解决方法 :
对于 Socket 对象来说 , 通过是用 write 方法 (writeByte(),writeUTFBytes( ) 等方法 .) 先向缓存区写入数据 , 然后使用 flush() 方法发送数据 . 对于 XMLSocket 对象 , 使用 send() 方法 .
讨论 :
Socket 和 XMLSocket 类向 Socket 服务器发送数据的方法是不相同的 . 让我们首先看一下 Socket 类的方法 .
当你使用 Socket 对象向服务器发送数据的时候 , 你首先要将数据写入到一个缓冲区中 .Socket 类设置了一系列的方法来写数据 . 每一个方法都用于写不 同的数据类型的数据 ( 或者不同的数据 ). 这些方法分别是 : writeBoolean( ), writeByte( ), writeBytes( ), writeDouble( ), writeFloat( ), writeInt( ), writeMultiByte( ), writeObject( ), writeShort( ), write- UnsignedInt( ), writeUTF(), 和 writeUTFBytes( ). 这些方法大多数都只接受一个参数 , 该参数的类型同方法的名字相匹配 . 例如 ,writeBoolean() 方法接受一个布尔值作为参数 , 而 writeByte( ), writeDouble( ), writeFloat( ), writeInt( ), writeShort( ), writeUnsignedInt( ) 方法接受一个数字型参数 .writeObject() 方法接受一个对象类型作为参数 , 但该对象必须序列化成为 AMF 格式 .writeBytes( ) 方法允许你传一个 ByteArray 参数 , 并带有偏移量和长度两个参数 . 例如 , 下面这段代码 , 调用了一个 writeBytes( ) 方法 , 该方法将 ByteArray 对象中的所有 byt 值都传出去了 ( 偏移量为 0, 长度和 ByteArray 数组长度等长 ):
socket.writeBytes(byteArray, 0, byteArray.length);
writeUTF( ) 和 writeUTFBytes( ) 方法允许你的发送字符串类型的参数 . 每个一个方法只接受一个字符串作为参数 .writeUTFBytes( ) 方法简单的将字符串作为 Bytes 发送 .writeUTF( ) 方法在写入真正数据之前 , 先写入 byts 的数量 .
writeMultiByte( ) 方法也允许字符串类型的参数 , 但是使用的为非默认字符集 . 该方法需要两个参数 : 字符串和字符集名称 . 在 Flash 和 Flex 的帮助文档中有一个自持所有 字符集的列表 , 该列表中的标签和描述符是一一对应的 . 使用标签值作为 writeMultiByte( ) 作为字符集 . 例如下面的代码发送了一个编码为 Unicode 的字符串 :
socket.writeMultiByte("example", "unicode");
相一个 Socket 对象传数值的方法完全依赖于你所有数据的类型和服务所接受数据的类型 . 使用一个 Socket 对象 , 你完全可以使用 ActionScript 写一个 Telnet 和 POP mail 客户端 . 这两种协议都支持 ASCII 字符指令 . 例如 , 在连接一个 POP 服务器之后 , 你可以通过使用 USER 指令指定一个用户 . 下面代码向一个 Socket 对象发一条指令 :
// POP servers expect a newline (/n) to execute the preceding command.
socket.writeUTFBytes("USER exampleUsername/n");
向一个 Socket 对象写入数据其实并没有将数据发送到 Socket 服务器 . 每调用一个 write 方法都向 Socket 对象添加一个数据 . 例如 , 下面代码向一个 Socket 对象添加了四个 byte 的数据 , 但是没有一个发出了 .
socket.writeByte(1);
socket.writeByte(5);
socket.writeByte(4);
socket.writeByte(8);
当你想将这些累积的数据发送到 Socket 服务器需要调用 flush() 方法 .flush() 方法调用之后将把所有已经写入的数据发送出去 , 并清空缓冲区 :
socket.flush( );
XMLSocket 类是一个非常简单用于发送数据的 API. 写于发数据都是由 send() 这一个方法来完成的 .send() 方法可以接受任何数据类型的参 数 . 它可以将所有的参数都转换为一个字符串类型并发送到服务器 . 通常参数为一个 XML 对象或者一个包含数据结构类似 XML 数据的字符串 :
xmlSocket.send(xml);
然而 , 准确的格式完全依赖于服务器所能够接受的格式 . 如果服务器接受 XML 格式的数据 , 你必须发送 XML 格式的数据 . 如果服务器只接受 URL 编码的数据 , 你也必须发送 URL 编码的数据 .
ActionScript3.0 Socket 编程 (3)
3. 从 Socket 服务器读数据
解决方法 :
对于 Socket 实例 , 先收到 socketData 事件 , 然后调用如下两个方法的一个 , 比如 ,readByte() 或者 readInt(), 在事件控制器中确定不会去读过去的 bytesAvailable.
对于 XMLSocket 实例 , 先收到 data 事件 , 然后解析从事件控制器内部装载的 XML 数据 .
讨论 :
从一个 socket 连接接收的数据依赖于你使用的 Socket 的类型 .socket 和 XMLSocket 都可以从服务器接受到数据 , 但是它们处于不同重量级的技术 . 让我们在讨论 XMLSocket 之前先关注下 Socket 类 .
我都知道 socket 在 Flash 中是一个异步的行为 . 因此 , 它就不能简单的创建一个 Socket 连接 , 然后就立刻尝试去读取数据 .read 方法不能等到 从服务器传过来数据之后在返回 . 换句话说 , 你只能在客户端从服务器载入所有数据之后才可以读取数据 . 在数据可用之前读数据会产生一个错误 .
通过 socketData 事件广播到 Socket 实例 , 这样我们就可以知道什么时候数据可以被读取 . 那么我们要为 socketData 事件添加一个事件监 听器 , 任何时候只要有新的数据从一个 socket 服务器发送过来 , 都会触发事件控制器 . 在事件处理器的内部我们写入我们要执行的代码去读取和处理收到的数 据 .
从一个前端服务器读取数据 ,Socket 类为我们提供了许多不同的方法 , 这些方法依赖于你所读得数据类型 . 例如 , 你可以通过 readByte() 方法读一 个 byte 数据 , 或者通过一个使用 readUnsignedInt() 方法去读一个无符号整数 . 下面这个表列出来能够从服务器读取的数据类型 , 返回值 , 和 read 方法每次读入的字节数 .
Table:Socket read methods for various datatypes
方法 : 返回值类型 描述 字节数
readBoolean( ):Boolean 从 Socket 读取一个 Boolean 值 . 1
readByte( ):int 从 Socket 读取一个 byte 值 . 1
readDouble( ):Number 从 Socket 读取一个 IEEE 754 双精度浮点数 . 8
readFloat( ):Number 从 Socket 读取一个 IEEE 754 单精度浮点数 . 4
readInt( ):int 从 Socket 读取一个有符号 32-bit 整数值 . 4
readObject( ):* 从 Socket 读取一个 AMF-encoded 对象 . n
readShort( ):int 从 Socket 读取一个有符号 16-bit 整数值 . 2
readUnsignedByte( ):uint 从 Socket 读取一个无符号字节 . 1
readUnsignedInt( ):uint 从 Socket 读取一个无符号 32-bit 整数 4
readUnsignedShort( ):uint 从 Socket 读取一个无符号 16-bit 整数 . 2
readUTF( ):String 从 Socket 读取一个一个 UTF8 字符串 . n
有两个额外的方法没有在上面这个表中描述 . 它们分别是 readBytes() 和 readUTFBytes().readBytes() 方法只可以让 socket 读数据但不能返回一个值 , 并且该方法需要 3 个参数 :
bytes:
一个 flash.util.ByteArray 实例读取从 socket 中收到的数据 .
offset:
一个 uint 值 , 指定从什么位置开始读取 socket 中收到数据的偏移量 . 默认值为 0.
length:
一个 uint 值 , 用于指定读取 bytes 的数量 . 默认值为 0, 意思就是说将所有的可用的数据都放入 ByteArray 中 .
另一个 readUTFBytes() 方法 , 只需要一个长度参数用于指定 UTF-8 字节的读入数量 , 并且该方法会将所有读入的字节码转换成为字符串类型 .
注意 : 在从一个 Socket 读数据之前 , 首先要判断 bytesAvailable 的属性 . 如果你不知道要读入的数据类型是什么就去读数据的话 , 将会产生一个错误 (flash.errors.EOFError).
下面的例子代码连接了一个 socket 服务器 , 读取并显示每次从服务器发来的数据 .
package {
import flash.display.Sprite;
import flash.events.ProgressEvent;
import flash.net.Socket;
public class SocketExample extends Sprite {
private var socket:Socket;
public function SocketExample( ) {
socket = new Socket( );
// Listen for when data is received from the socket server
socket.addEventListener( ProgressEvent.SOCKET_DATA, onSocketData );
// Connect to the server
socket.connect( "localhost", 2900 );
}
private function onSocketData( eventrogressEvent ):void {
trace( "Socket received " + socket.bytesAvailable + " byte(s) of data:" );
// Loop over all of the received data, and only read a byte if there
// is one available
while ( socket.bytesAvailable ) {
// Read a byte from the socket and display it
var data:int = socket.readByte( );
trace( data );
}
}
}
}
在上面的这个例子中 , 如果一个 socket 服务器发送回一个消息 ( 例如 "hello"), 当一个客户段连入服务器就会返回并输出下面类似的文字 :
Socket received 5 byte(s) of data:
72
101
108
108
111
注意 : 一旦数据从 socket 读出 , 它就不能再次被读 . 例如 , 读一个字节之后 , 这个字节就不能再 " 放回来 ", 只能读后边的字节 .
当收到的数据为 ASCII 编码 , 你可以通过 readUTFBytes() 方法重新构建一个字符串 .readUTFBytes() 方法需要知道多少个字节需要转换为字符串 . 你可以使用 bytesAvailable 去读所有的字节数据 :
var string:String = socket.readUTFBytes(socket.bytesAvailable);
XMLSocket 类的动作和 Socket 类相比在从服务器接受数据的风格相似 . 两者都是通过事件监听器来监听数据接收通知的 , 这主要取决于 Flash 异步的 Socket 实现 . 然而 , 在处理实际数据的时候有很大的不同 .
有个 XMLSocket 实例在从服务器下载完数据后分发数据事件 . 通过 flash.events.DataEvent.DATA 常量定义的数据事件包含一个 data 属性 , 该属性包含了从服务器收到的信息 .
注意 : 使用 XMLSocket 从服务器返回的数据总是认为是一个字符串类型的数据 . 这样不用为任何数据类型的数据指定读取方法 .
这些从服务器返回的数据是没有经过任何处理的原始数据 . 因此 , 你不能通过 XMLSocket 连接立即使用 XML, 你发送和接收的都是纯字符串数据 . 如果你期望 XML, 在你处理数据之前 , 你必须首先将这些数据转换为一个 XML 的实例 .
下面的这段代码在初始化的时候通过 XMLSocket 连接到了本地服务器的 2900 端口 . 在连接成功之后 , 一个 <test> 消息会发送到服务 器 .onData 事件监听者控制从服务器返回的响应 . 在本例中返回字符串 <response><test success='true'/></response>. 你可以通过事件的 data 属性发现为字符串数据 , 然后 XML 类的构造函数将 字符串转换成为了 XML 实例 . 最后 , 通过使用 E4X 语法的 XML 实例的一部分信息 .( 关于通过使用 E4X 处理 XML 的更多详细信息 , 我们需要另外讨论 .)
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.DataEvent;
import flash.net.XMLSocket;
public class SocketExample extends Sprite {
private var xmlSocket:XMLSocket;
public function SocketExample( ) {
xmlSocket = new XMLSocket( );
// Connect listener to send a message to the server
// after we make a successful connection
xmlSocket.addEventListener( Event.CONNECT, onConnect );
// Listen for when data is received from the socket server
xmlSocket.addEventListener( DataEvent.DATA, onData );
// Connect to the server
xmlSocket.connect( "localhost", 2900 );
}
private function onConnect( event:Event ):void {
xmlSocket.send( "<test/>" );
}
private function onData( eventataEvent ):void {
// The raw string returned from the server.
// It might look something like this:
// <response><test success='true'/></response>
trace( event.data );
// Convert the string into XML
var response:XML = new XML( event.data );
// Using E4X, access the success attribute of the "test"
// element node in the response.
// Output: true
trace( response.test.@success );
}
}
}
注意 : 在 data 事件分发数据之前 ,XMLSocket 实例必须从服务器收到一个表示为空的 byte('//0'). 也就是说 , 从服务器仅仅只发送所需要的字符串是不够的 , 必须在结尾处加入一个表示为空的 byte.
ActionScript3.0 Socket 编程 (4)
4. 同 Socket 服务器进行握手 , 并确定收到了什么样的数据和如何处理这些数据 .
解决方法 :
创建不同的常量来声明协议的状态 . 使用这些常量将指定的处理函数映射到相应的状态 . 在一个 socketData 事件控制器中 , 通过状态映射调用这些函数的 .
讨论 :
建立 Socket 连接通常要处理握手这个环节 . 尤其是在服务器初始化需要向客户端发送数据 . 然后客户端通过一种特殊的方式相应这些数据 , 接着服务器因此再次响应 . 整个处理过程直到握手完成并且建立起一个 " 正常的 " 连接为止 .
处理服务器的不同响应是非难的 , 主要的原因是 socketData 事件控制器不能保存上下文的顺序 . 也就是说 , 服务器的响应不会告诉你 " 为什么 " 响应 , 也不告诉你这些响应数据被那个处理程序来处理 . 要想知道如何处理这些从服务器返回的响应不能从响应的本身来获得 , 尤其在响应变化的时候 . 或许一个响应返回 了两个字节码 , 另一个返回了一个整数值还跟了一个双精度浮点数 . 这样看来让响应本身处理自己是一大难题 .
我们通过创建一个状态量来标注不同的上下文 , 服务器通过这些上下文将数据发送到客户端 . 与这些状态量都有一个相关联的函数来处理该数据 , 这样你就可以很轻松的按照当前的协议状态去调用正确的处理函数 .
当你要与一个 Socket 服务器建立连接需要考虑如下几个步骤 :
1. 当与服务器连接的时候 , 服务器立刻返回一个标志服务器可以支持的最高协议版本号的整数值 .
2. 客户端在响应的时候会返回一个实际使用协议的版本号 .
3. 服务器返回一个 8byte 的鉴定码 .
4. 然后客户端将这鉴定码返回到服务器 .
5. 如果客户端的响应不是服务器端所期望的 , 或者 , 就在这个时候该协议变成了一个常规操作模式 , 于是握手结束 .
实际上在第四步可以在鉴定码中包含更多的安全响应 . 你可以通过发送各种加密方法的密匙来代替逐个发送的鉴定码 . 这通常使用在客户端向用户索要密码的时候 , 然后密码成为了加密过的 8byte 鉴定码 . 该加密过的鉴定码接着返回到服务器 . 如果响应的鉴定码匙服务器所期望的 , 客户端就知道该密码是正确的 , 然后同意 建立连接 .
实现握手框架 , 你首先要为处理从服务器返回的不同类型的数据分别创建常量 . 首先 , 你要从步骤 1 确定版本号 . 然后从步骤 3 收取鉴定码 . 最后就是步骤 5 的常规操作模式 . 我们可以声明
如下常量 :
public const DETERMINE_VERSION:int = 0;
public const RECEIVE_CHALLENGE:int = 1;
public const NORMAL:int = 2;
常量的值并不重要 , 重要的是这些值要是不同的值 , 两两之间不能有相同的整数值 .