掌握Streams、Readers和Writers

发表于:2007-05-25来源:作者:点击数: 标签:StreamsReadersWriters掌握
你可以用.NET Streams、Readers和Writers来访问系统文件,并通过 网络 传输数据以及高效地处理XML文档。 by Enrico Sabbadin 技术工具箱:VB.NET, ASP.NET, XML .NET Framework Base Class Library (BCL) 从各方面增加了对VB 程序员 的支持。(一些MSDN文档
你可以用.NET Streams、Readers和Writers来访问系统文件,并通过网络传输数据以及高效地处理XML文档。
by Enrico Sabbadin
技术工具箱:VB.NET, ASP.NET, XML

.NET Framework Base Class Library (BCL) 从各方面增加了对VB程序员的支持。(一些MSDN文档引用该Library作为Framework Class Library [FCL])。你可以通过它编写出更加简洁的代码,用于从数据访问和Web编程方面的问题 –- 然而它并不是随随便便地拿来就能用的。你必须掌握几种新的概念并将其运用到你的编程技巧中,同时学习用新的方法来处理普遍的任务,比如读写文件或是处理XML文档。用于执行这些任务的Stream、Reader和Writer类及其子类的运用在.NET Framework中是相当普遍的。

Stream、Reader和Writer的应用在VB开发中并不少见。或许你在用ADO库时用到过Stream类,或者你在开发基于SAX的XML应用程序时用到过Reader和Writer类。而如果你要研究一些更深的课题(比如TCP socket)则需要对它们有更详细的了解。我将讲解Stream、Reader和Writer是如何相互关连的、它们不同的用法,以及如何运用它们来更高效地完成一些基本和高级的任务。

图1.
图1. javascript:openWindowRes('DotNetMagazine/2003_01/xml2html.asp?xmlfile=StreamReaderWriter/Figure1.xml&xslfile=../../include/xsl/Figure.xsl');">看箭头读图
这三个类的关系是相当对称的(见图1)。Stream是.NET Framework中的抽象类,它提供一个通用的接口用来实现用统一编程访问指定输入/输出(I/O)设备(如文件或TCP sockets)。这样就使你和操作系统及内部储备的细节分隔开了。

Stream通过一个字节数组(byte array)执行对读和写的操作。Read和Write方法的字面意思是基于指针(cursor)模式的。Stream类中包含一个内置的pointer,用来指向最后读取或书写的字节。之后对Read或Write的调用以指针的当前位置+1开始。如果调用成功,则在Stream中的指针会按书写或读取字节的个数将当前位置向后移动。

除了一个字节数组,你还需要将一个offset和count参数传入Read和Write方法中来控制数据,这些数据必须是从该数组中被复制到底层设备(Write)的,或者从底层设备被复制到数组中(Read)的。offset参数允许你指定字节数组的起始位置并以此作为起点开始读取或写入;count参数用来指定读取或写入Stream的最多字节的个数。Read方法会返回复制到字节数组中的字节个数,其最终结果会少于count参数允许的字节个数(即使你需要更多的字节也不会被抛出异常):

clearcase/" target="_blank" >cc>
Dim s As New FileStream("c:\file.txt", _
   FileMode.OpenOrCreate)
Dim b(15) As byte
'read 3 bytes and put them in byte array's
'position 5, 6 and 7
Dim byteread As Integer = s.Read(b, 5, 3)
'now the stream cursor is pointing to the 4th 
'position in the stream
'write the 6th and 7th bytes to the Stream at
'Stream position 4 and 5
s.Write(b, 6, 2)

五种可选的Streams类
BCL 提供五种Stream类的具体实现 (concrete implementation)(见表1)。FileStream、NetworkStream和CryptoStream类分别用来访问系统文件(local file system)、TCP 设计和加密服务(cryptographic service)。MemoryStream和BufferedStream类是基于内存的(memory-based)的Stream,它们在执行一些特定程序任务(application-specific tasks)时会相当方便,并且它们还会通过NetworkStreams优化你的读写过程。

NetworkStream和CryptoStream类提供了forward-only功能,而FileStream类和MemoryStream类却能使pointer来回运作。BufferedStream类的指针行为(cursor behavior)是不固定的,因为它是由在构造器中和BufferedStream对象相关的底层Stream来决定的。

你可以用CanSeek属性来测定该底层设备是不是forward-only的,如果其返回值为true,那么你就可以用Position属性或者Seek方法将指针定位到一个特定的位置了。Length属性会告诉你Stream中包含多少个字节。

FileStream类用来提供文件的只读功能。只要你输入文件的路径,你就可以访问该文件的内容了:

'read the whole file content into a byte array
Dim fsstream As New FileStream("c:\x.bin", _
   FileMode.Open)
Dim readbytes(fsstream.Length - 1) As byte
Dim readbytes As Integer = _
   fsstream.Read(readbytes, 0, fsstream.Length)

你还能够用File类的Create、Open、OpenRead和OpenWrite等静态方法来获得一个FileStream类(File类中所有的方法都是静态的)。

FileInfo类使用了和File类获得FileStream对象相同的方法,只不过在FileInfo类中的Create、OpenRead和OpenWrite都是基于实例(instance-based)的方法。根据Microsoft的文档说明,这两个类是可以互换的。但出于安全方面的原因,该文档建议当你需要在相同对象中进行不同的操作时最好使用FileInfo类 。

FileInfo类还提供其他一些用于操作系统文件结构的方法,比如文件的复制和目录的重命名。这使该类继承了在VB6 中我们所喜爱的FileSystemObject类的一些特性。

当你要访问包含二元数据的文件时,FileStream类就会派上用场,但在处理基于文本的文件时不要使用它。我下面要介绍的StreamReader类和StreamWriter类是专门用来处理此类问题的。

FileStream类不会立即将每个单一字节写入底层设备中,它有一个用来缓存数据的内部缓冲器(buffer)。数据只有在缓冲器被填满的情况下才会进入系统文件中。如果你没有在FileStream中指定缓冲器的大小(size),则它的缺省大小是4K(这在通常情况下足够了)。你最好用其中一种重载构造器来定制该缓冲器的大小,当你需要执行大的I/O操作时,构造器会将缓冲器的大小它作为参数传入。优化缓冲器的大小意味着你要在频繁地访问底层系统文件时找到一个平衡点,以使内存的使用量和性能都不受影响。

设置缓冲器的大小
一般来说,缓冲器大小的值应该是读取或写入Stream数据大小的二十分之一。这只是一个粗略的算法,你应该在程序中自己测试这个值:

fs = New FileStream("c:\aaa.txt", FileMode.Open, _
   FileAccess.Write, FileShare.Read, _
   inputsize/20)

通过调用Flush方法,你可以在缓冲区接近最大容量之前强制写入底层Stream。

遗憾的是,你不能在NetworkStream类中对缓冲器的大小进行设置。每一个单一的被写入的字节会被立即发送出去。在某些情况下,这会是影响网络应用程序的一个主要因素。 BufferedStream类可以将Stream对象串连起来解决这个问题。新建一个NetworkStream对象并将它作为创建BufferedStream对象的一个参数。你可以在BufferedStream构造器中指定缓冲器的大小。

这样,你就可以用BufferedStream对象来写数据了,当内部缓冲器接近最大容量时它只会将数据堆存到NetworkStream中去。以下代码教你如何使用这个简单却有效的技巧:

Dim clientsocket As New TcpClient("localhost", _
   8080)
Dim ns As networkstream = clientsocket.GetStream()
Dim bfstream As New BufferedStream(ns, 2048)
Dim i As Integer
'writes a single byte on each call
For i = 0 To 100
   bfstream.Write(New byte(0) {i}, 0, 1)
Next
'since the internal buffer (2048 size) has not 
'been filled up yet, data is sent to the network
'stream only when flush is called
bfstream.Flush()
bfstream.Close()

MemoryStream提供了一种方法来控制何时发送数据以及有多少数据被发送到NetworkStream。你可以建立一个MemoryStream对象并将数据写进去,然后在合适的时间调用WriteTo方法,并将NetworkStream以一个参数形式传入。MemoryStream对象会将内部所有的数据堆存到已有的Stream当中:

Dim clientsocket As New TcpClient("localhost", _
   8080)
Dim ns As NetworkStream = clientsocket.GetStream()
Dim memstr As New MemoryStream()
Dim i As Integer
'writes a single byte on each call
For i = 0 To 100
   memstr.Write(New byte(0) {i}, 0, 1)
Next
'write all data in the network Stream
memstr.WriteTo(ns)

使用Streams的另一个好处是你可以用它来优化ASP.NET页面的性能。ASP.NET 的Response对象允许通过使用底层的NetworkStream的OutputStream属性来将页面响应发送回浏览器。在购置一个页面响应时,你可以利用它来减少内存分配(避免产生临时的字符串)。

可以考虑使用这个普通的任务,即通过XSLT将XML转换为HTML页面发送回去。在你试过了不同XslTranform类的Transform方法之后,你就可以以此作为结束:

Dim x As New Xsl.XslTransform()
x.Load(<xslfilepath>)
Dim a As New XPath.XPathDocument(<xmlfilepath>)
'The StringWriter is just a wrapper on a string 
'object
Dim s As New StringWriter()
x.Transform(a.CreateNavigator, Nothing, s)
Response.Write(s.ToString())

这样做的效果不是很好,因为它会建一个毫无必要的临时文件。你可以通过将底层Response对象的NetworkStream传入Transform方法来更好地执行这个任务:

Dim x As New Xsl.XslTransform()
x.Load(<xslfilepath>)
Dim a As New XPath.XPathDocument(<xmlfilepath>)
x.Transform(a.CreateNavigator, Nothing, _
   Response.OutputStream)

当你需要将二元的、不基于文本的数据发送到浏览器中时,使用Response.OutputStream也是一个好办法:

Dim f As New FileStream(Server.MapPath("") + _
   "\help.gif", FileMode.Open)
Dim bt(f.Length - 1) As byte
f.Read(bt, 0, fs.Length)
Response.ContentType = "image/gif"
Response.OutputStream.Write(bt, 0, f.Length)

游弋在字节和字符串之间
在很多种情况下,你需要将基于文本的数据写入一个Stream或者从Stream中读取数据。此时如果在TextReaders 和TextWriters中发现一个简短的、封装好的Stream就再好不过了。然而你并不一定非得使用这些类,因为.NET Framework中的System.Text.Encoding类就能够使你轻易地将字节数组编译到字符串中去,或是将字符串反编译到字节数组中来。GetBytes方法能够将一个字符串看作是一个输入和输出的字节数组;而GetString方法则正好相反。无论使用何种方法,你都必须提供一个编码(ASCII、UTF8、Unicode,等等),用来定义何种特性会被映射到数字型字节的值中(见列表1)。

除了可以用字节数组和字符串来写Stream之外,.NET BCL还支持formatter类。你可以通过Serialize和Deserialize方法来调用formatter:

Dim a As New Class1()
a.DateOfBirth = New Date(2000, 12, 12)
a.Name = "John"
Dim fm As New Formatters.Binary.BinaryFormatter()
fm.Serialize(bsw, a)

BCL目前支持两种formatter:Binary formatter和 SOAP formatter。如前段代码所显示的,在使用formatter时你可以不受基类型(base type)的限制。

你可以将任意对象序列化(Serialize)或反序列化(Deserialize)到一个Stream中去,只要你以<Serializable>属性注明该类(见列表2)。当你要开发一个分布式的、松散耦合(Loosely-coupled)的程序时,用FileStream类或通过NetworkStream类来持续一个对象状态是一个很棒的方法,使用SOAP formatter,你甚至可以将一个对象实例以一个邮件附件的形式发送出去。

正如你在前面的例子中看到的,Stream编程是一个针对低层次的任务。你的代码通常处理的是一些比字节数组更高层次的实体:如字符串、整数、对象等等。因而Formatter提供的帮助是不可估量的。但是在你每次需要将所有基类型转换为字节数组时,formatter要求使用大量能够重用的、error-prone的代码。 这时便可以用Readers和Writers来解决了(但你还是得用formatter来解决对象的序列化)。

我先来讲解一下BinaryReader和BinaryWriter类。这两个类都在构造器中使用了Stream,而且能够使你分别从相关的Stream中读取基本数据(尽管还没有被写入文档,但这两个类中很可能会使用BinaryFormatter)。这两个类并非完全对应的,用于不同类型的Write操作被用在一组Write方法的重载中;BinaryReader 类中用到的Read操作被用于实现不同的方法,每一种方法对应一种数据类型:

Dim fsstream As FileStream
fsstream = File.Create("c:\formatter.bin")
Dim bw As New BinaryWriter(l_fsstream)
Dim varin As Long = 1
bw.Write(varin)
fsstream.Close()
fsstream = File.OpenRead("c:\formatter.bin")
Dim br As New BinaryReader(fsstream)
Dim varout As Long = br.ReadInt64()

TextReaders类和TextWriters类都是专门用于解决字符和字符串读写操作的抽象类。BCL 提供一个TextReader的两种具体实现类:StringReader类和StreamReader类。由于StringReader类并不是特别有用,所以在这里我就不详细介绍它了。

指定StreamReader参数
StreamReader构造器接受一个Stream对象或者一个文件路径(你可以使用Universal Naming Convention [UNC] 路径,但不能用URL)。你还可以指定这些参数:比如编码类型(encoding type)(如果没有特别指定,系统会默认使用UTF8编码);作为缓存的内置缓冲器大小(很可能通过BufferedStream对象来实现);还有一个布尔值,它用来指示是否应通过该Stream的第一个字节来判断编码类型。

可以看到,StreamReader是个很有用的封装(wrapper)类,其中所包含的功能你也可以用前面提及的Stream和Formattter类来完成。

StreamReader类使用不同的Read操作。你可以读取单行字符串(用ReadLine方法)或是底层Stream的所有内容(ReadtoEnd方法);你可以指定字符的个数,通过Read或ReadBlock方法来读取(此时会返回一个char的数组);你还可以用Peek方法来检测该Stream是否被读完。以下代码显示如何实现一个简单的客户端应用程序,该程序运用了StreamReader和NetworkStream类在Web服务器上执行荷载应力测试

Dim tcpClient As New _
   Net.Sockets.TcpClient( _
   "www.sitetostress.com", 80)
Dim networkStream As _
   System.Net.Sockets.NetworkStream = _
   tcpClient.GetStream()
Dim strmwrt As New _
   StreamReader("c:\mycommands.txt")
Do While Not strmwrt.Peek = -1
Dim sbytes As [byte]() = _
   System.Text.Encoding.ASCII.Getbytes( _
   strmwrt.ReadLine())
networkStream.Write(s字节s, 0, sbytes.Length)
Loop
strmwrt.Close()

一组HTTP命令是通过StreamReader对象从一个文件中读取的,它被Encoding类转换到一个字节数组中,然后被发送到Web服务器上。

现在来看看这些将书写功能封装到Stream中的类。它们的基本抽象类(base abstract class)是TextWriter类,BCL提供了它的五种继承类(见表2)。这里我将主要介绍StreamWriter类和StringWriter类。

StreamWriter类采用Stream或文件名以及相同的参数名作为StreamReader类的构造器。 StreamWriter类使用了两个重载方法来写入底层Stream: Write和WriteLine(WriteLine方法在最后加了一个回车键)。两种方法都提供很多的重载形式来接受所有.NET基本类型,这看起来很象BinaryWriter类的Write方法;区别在于,数字类型以字符串形式(与区域设置相关)被写入底层设备。

如果你需要同时读取和写入文本文件,那么你可以使用TextReader和TextWriter这些方便的性能,而不要用FileStream对象来处理字节数组。用FileStream对象来打开文件,将它以TextReader和TextWriter的构造器参数来传入并运行它。记住Reader和Writer使用相同的Stream指针,并在每次写入TextWriter时调用Flush以使TextReader读取你写入的内容:

Dim fsstream As New FileStream("c:\x.txt", _
   FileMode.Create)
'Associate the same stream to a StreamReader and 
'a StreamWriter
Dim sr As New StreamReader(fsstream)
Dim sw As New StreamWriter(fsstream)
sw.WriteLine("mytext")
'flush data to the Stream
sw.Flush()
'move cursor to the beginning 
fsstream.Position = 0
Dim s As String = sr.ReadLine()
'This will dump "mytext" to the debugger
Debug.WriteLine(s)

字符串连接
StringWriter类提供一个类似于Stream的方法连接字符串。字符串连接是一个开销很大的操作,因为字符串本身是不变的(immutable)类型。来看看这行代码:

mystring = mystring + "my string"

这里mystring被赋予了一个新值,并申请了新的空间来保存连接结果字符串。

用StringWriter来连接字符串会提供更好的性能。StringWriter类只是另一个对象 -- StringBuilder 的封装,StringBuilder才是真正用来执行字符串连接的类。你可以将StringBuilder作为一个参数构造器传入StringWriter (否则会在其内部生成一个实例),并用GetStringBuilder方法返回StringBuilder。然而StringBuilder 类会通过Remove、Insert和Replace方法来提供一些额外的性能(见资源中关于StringBuilder对象其他功能的介绍)。

.NET中的XML类也使用了Streams、Readers和Writers。XmlReaders和XmlWriters抽象类类似于TextReaders和TextWriters。XmlWriter是包含在XmlTextWriter中的具体实现。你可以将一个Stream、TextWriter或XML文档文件路径传入构造器。以下代码用 XmlTextWriter将XML document写入NetworkStream:

s = New TcpListener(8080)
s.Start()
Dim tcpClient As TcpClient = s.AcceptTcpClient()
Dim ns As NetworkStream = tcpClient.GetStream()
Dim xmltextwr As New Xml.XmlTextWriter(ns, _
   Encoding.UTF8)
xmltextwr.WriteStartDocument()
xmltextwr.WriteStartElement("myroot")
xmltextwr.WriteStartElement("child")
xmltextwr.WriteAttributeString("myattr", _
   "my attr value")
xmltextwr.WriteString("My value")
xmltextwr.WriteEndElement()
xmltextwr.WriteEndElement()
xmltextwr.WriteEndDocument()
xmltextwr.Flush()
xmltextwr.Close()

network Stream用一个XmlTextWriter连接引入客户端的连接请求。调用XmlTextWriter对象中的WriteXXX将相应的XML文本写入TCP连接。

XmlReader抽象类有三种具体实现,和上下文相关的是XmlTextReader类,它从构造器中读取文件路径、Stream或是TextReader。当你将XmlTextReader实例初始化之后,你便可以用forward-only或read-only等指针模式来操作XML文档元素了。XmlDocument类(该类代表了W3C XML Document Object Model [DOM] 在.NET Framework中的实现)也采用Stream、TextReader或是XmlReader类作为重载load方法的输入参数(见列表3)。

关于在.NET Framework BCL当中对I/O对象的简要介绍差不多就这么多了。你可能会发现我跳过了VB.NET部分 –- 在Microsoft Visual Basic .NET runtime assembly中用到的特定对象。这些assembly reference会被自动加入到所有新的VB项目中。assembly提供一套可供选择的方法来访问和处理I/O 操作。我的想法是,如果你正开始一个新的.NET项目,那么你应该研究一下BCL类,而不仅仅是I/O操作。这会对你将来更轻松地应用其他.NET语言打好基础。


图1.看箭头读图

从图中可以看到这个类的整体结构布局、各种关系,以及所有包含在.NET Framework中的写入文档的Stream、Reader和Writer对象。箭头指向的类表示该类的构造器认可箭头起始方向的类。你还可以看到构造器何时认可一个文件路径。

VB.NET / 游弋在字节和字符串之间

列表1.Encoding类的GetBytes方法预置了一个HTTP request,然后用GetString方法将response从一个字节数组转换到一个字符串里。

Dim tcpClient As New _
   Net.Sockets.TcpClient("www.microsoft.com", 80)
Dim networkStream As _
   System.Net.Sockets.NetworkStream = _
   tcpClient.GetStream()
Dim sendBytes As [Byte]() = _
   System.Text.Encoding.ASCII.GetBytes( _
   "GET /default.htm" + ControlChars.CrLf)
networkStream.Write(sendBytes, 0, _
   sendBytes.Length)
' Reads the NetworkStream into a byte buffer.
Dim bytes(tcpClient.ReceiveBufferSize) As Byte
networkStream.Read(bytes, 0, _
   CInt(tcpClient.ReceiveBufferSize))
' Returns the data received from the host to the 
' console.
Dim returndata As String = _
   Encoding.ASCII.GetString(bytes)

 

VB.NET / 在Snap中进行序列化和反序列化

列表2.以下代码在客户端和服务器间建立了一个TCP连接。该服务器建立一个类,初始化它的state,对其进行序列化,然后通过网络发送回客户端。客户端对它进行反序列化,并将该类的state堆存到调试器中。

' Server
Dim s As TcpListener
s = New TcpListener(8080)
s.Start()
Dim tcpClient As TcpClient = s.AcceptTcpClient()
Dim ns As NetworkStream = tcpClient.GetStream()
Dim bsw As New BufferedStream(ns, 2048)
Dim a As New Class1()
a.DateOfBirth = New Date(2000, 12, 12)
a.Name = "john"
Dim fm As New _
   System.Runtime.Serialization.Formatters. _
   Binary.BinaryFormatter()
fm.Serialize(bsw, a)
bsw.Flush()
bsw.Close()

' Client
Dim clientsocket As New TcpClient("localhost", _
   8080)
Dim ns As NetworkStream = clientsocket.GetStream()
Dim bfstreamr As New BufferedStream(ns, 2048)
System.Threading.Thread.CurrentThread.Sleep(2000)
Dim fm As New _
   System.Runtime.Serialization.Formatters. _
   Binary.BinaryFormatter()
Dim a As Class1 = CType( _
   fm.Deserialize(bfstreamr), Class1)
Debug.WriteLine(a.Name)
Debug.WriteLine(a.DateOfBirth)

描述
System.IO.StreamWriter 将特定书写操作执行到底层设备中。
System.IO.StringWriter 提供针对字符串的类似于stream的操作。
System.Web.HttpWriter 建立一个作为储备库的streams。
System.Net.Sockets.NetworkStream 由Response.Output返回。将字符写入HTTP响应中。
System.Web.UI.HtmlTextWriter 用于方便写入HTML文档。
System.CodeDom.Compiler.IndentedTextWriter 提供插入一个标签字符串和跟踪当前缩进层(indention level)的方法。

表2. BCL提供的五种TextWriters实现。

.NET Framework包含五种Stream抽象类的实现。StreamWriter类是目前最有用和最常见的。HttpWriter和HtmlTextWriter类是特别用于封装NetworkStream的实现。

VB.NET / 有效加载XML文档

列表3.在第一段代码中,NetworkStream对象被传入XmlTextReader。在第二段中,NetworkStream被传入Xmldocument的Load方法中。这两段代码都是简洁而有效的,因为它们都没有生成不必要的临时字符串。

'Load an XmlTextReader with the received data
Dim tcpClient As New _
   Net.Sockets.TcpClient("localhost", 80)
Dim ns As System.Net.Sockets.NetworkStream = _
   tcpClient.GetStream()
Dim xmltextreder As System.Xml.XmlTextReader = _
   New System.Xml.XmlTextReader(ns)
While xmltextreder.Read()
Debug.WriteLine(xmltextreder.Name)
End While
'Load an XmlDocument with the received data
Dim tcpClient As New _
   Net.Sockets.TcpClient("localhost", 80)
Dim ns As System.Net.Sockets.NetworkStream = _
   tcpClient.GetStream()
Dim l_dom As New Xml.XmlDocument()
l_dom.Load(ns)


关于作者:
Enrico Sabbadin是一名软件架构师和开发者。它是VB2theMax小组的成员,并为Francesco Balena's Code Architects提供培训和咨询。他在自己的网站中(www.sabbasoft.com)有一些对MTS、COM+、VB/COM,以及.NET Enterprise Services等常见问题的解答。你可以通过esabbadin@vb2themax.com和他联系。

原文转自:http://www.ltesting.net

评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)