admin 管理员组

文章数量: 887021


2023年12月22日发(作者:js很难学吗)

【转】C#Socket网络编程入门第一章C#Socket编程(1)基本的术语和概念计算机程序能够相互联网,相互通讯,这使一切都成为可能,这也是当今互联网存在的基础。那么程序是如何通过网络相互通信的呢?这就是我记录这系列的笔记的原因。C#语言从一开始就是为了互联网而设计的,它为实现程序的相互通信提供了许多有用API,这类应用编程接口被称为套接字(Socket)。在开始学习C#Socket之前我们需要先来了解一下基本的术语和概念。1.1计算机网络计算机网络由一组通过通信信道(Communicationchannel)相互连接的机器组成。这些机器被称为:主机(hosts)和路由器(routers)。*通信信道——是将字节序列从一个主机传输到另一个主机的一种手段(有线、无线(WiFi)等方式)。*主机——是运行程序的计算机。*路由器——是将信息从一个通信信道传递或转发到另一个通信信道。TCP/IP网络通信流程图:1

1.2分组报文*分组报文——在网络环境中由程序创建和解释的字节序列。1.3协议协议相当于相互通信的一种约定,协议规定了分组报文的交换方式和它们包含意义。互联网所使用的协议是TCP/IP协议,TCP/IP协议族主要包括:*IP协议(InternetProtocol,互联网协议)*TCP协议(TransmissionControlProtocol,传输控制协议)*UDP协议(UserDatagramProtocol,用户数据报协议)1.3.1IP协议*IP协议——是TCP/IP协议中唯一属于网络层的协议。将数据从一台主机传输到另一台主机。*IP协议——提供了一种数据服务:每组分组报文都有网络独立处理和分发,类似于信件或包裹通过邮政系统发送一样。*IP协议——是一个"尽力而为"(best-effort)的协议:它试图分发每一个分组报文,在网络传输过程中,偶尔也会发生丢失报文或报文顺序打乱,或者重复发送报文的情况。在IP协议层之上是传输层(transportlayer),它提供了两种可选的协议:TCP协议和UDP协议,两种协议都建立在IP层所提供的服务基础上,二者也被称为"端到端传输协议(end-to-endtransportprotocol)"。根据应用程序协议2

(Applicationprotocol)的不同需求,使用了不同的方式传输数据。二者都有一个共同的功能:寻址。TCP协议和UDP协议使用的地址叫做端口号(portnumber),是用来区分同一主机不同应用程序的。1.3.2TCP协议TCP协议能够检测和恢复IP层提供的主机到主机的信道中可能发生的报文丢失、重复以及其他错误。TCP协议是一种面向连接(connectionoriented)协议:在使用它进行通信之前,两个应用程序之间首先要建立一个TCP连接,这涉及两台相互通信的主机的TCP部件间完成的握手消息(handshakemessage)的交换。1.3.3UDP协议UDP协议并不尝试对IP层产生的错误进行修复,它仅仅简单拓展了IP协议,"尽力而为"的数据服务,使它能够在应用程序之间工作,而不是在主机之间工作。使用UDP协议的应用程序需要对处理报文丢失、顺序混乱等问题做好准备。1.4网络地址1.4.1IP地址在TCP/IP协议中,有两部分信息用来定位一个指定的程序:互联网地址(Internetaddress)和端口号(portnumber,范围1-65535)。前者由IP协议使用,后者由传输协议(TCP/UDP)对其进行解析。互联网地址由二进制数字组成,有两种形式:IPv4(32位)和IPv6(1283

位)。为了方便使用,两个版本的IP协议有不同的表示方法:IPv4地址被表示为一组为4个十进制数,每两个数之间用圆点隔开,这种表示方法叫做:点分形式(dotted-quad)。IPv6地址的16个字节由几组16进制的数字表示,这些十六进制数之间有分号隔开,每组数字分别代表了地址中的两个字节。1.4.2回环地址回环地址(loopbackaddress)是被分配的一个特殊的回环接口(loopbackinterface),回环接口是一种虚拟设备,它的功能只是简单的把发送给它的报文立即返回给发送者。如IPv4中的127.0.0.11.5域名系统(DomainNameSystem,DNS)和本地配置数据库DNS是一种分布式数据库,它将像这样的域名映射到真实的互联网地址和其他信息上。DNS协议允许连接到互联网的主机通过TCP或者UDP协议从DNS数据库获取信息。本地配置数据库通常是一种与具体操作系统相关的机制,用来实现本地与互联网地址的映射。1.6客户端和服务器客户端(client)和服务器(server)这两个术语分别代表了两种角色:*客户端是通信的发起者,而服务器程序则被动等待客户端发起通信,并4

对其作出响应。*客户端和服务器组成了应用程序。1.7什么是Socket(套接字)Socket(套接字)是一种抽象层,应用程序通过它来发送和接受数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。在TCP/IP协议族中的主要Socket类型为:*流套接字(streamsocket):传输层使用TCP协议,提供了一个可信赖的字节流服务*数据报套接字(datagramsocket):传输层使用UDP协议,提供了一个"尽力而为"的数据报服务,最长一次可以发送65500个字节的数据。第二章C#Socket编程(2)识别网络主机通过前面的笔记我们可以知道:一个客户端想要发起一次通信,先决条件就是需要知道运行着服务器端程序的主机的IP地址是多少,端口号是多少。然后我们才能够通过这个地址向服务器特定的应用程序发送信息。对于网络上的两台计算机来说,用户操作的计算机称为本地主机,与该计算机通信的另一台计算机称为远程主机。识别远程主机依靠两部分组成:一是主机标识,用于识别与本地主机通信的远程主机;二是端口号,用于识别是在和远程主机中的哪个进程通信。5

2.1获取主机地址信息在C#开发中命名空间为网络上使用的多种协议提供了简单的编程接口。我们可以利用这个命名空间下的类,编写基于网络标准协议的应用程序时,不必去考虑各种不同协议的具体细节。在获取主机(local和remote)地址信息时,我们需要使用和学习这几个最基本的类(更加详细API可以参考MSDN,下面会给出对应链接),用它们来实现相关的功能。*提供网际协议的IP地址的IPAddress类*包含IP地址和端口号的IPEndPoint类*为Internet主机提供信息容器的IPHostEntry类*提供简单的域名解析功能的Dns类说千遍不如做一遍,学习编程的最好方式就是自己动手实践,下面我们通过创建一个简单的WindowsForms示例程序(下载地址在本章末尾)来学习如何获取网络主机的地址信息,下面是示例的示例代码主要代码:1//获取本地主机名2stringlocalHostName=tName();34//通过主机名获取该主机下存储所有IP地址信息的容器5IPHostEntrylocal=tEntry(HostName);67//通过IPHostEntry对象的AddressList属性获取相关联主机的所有IP地址8IPAddress[]ipList=sList;6

910//获取本机回环地址11IPAddressloopbackIP=ck;1213//通过它Parse函数构造IPAddress对象14IPAddresslocalIp=("192.168.1.101");1516//通过IPAddress对象和端口号构造IPEndPoint对象17IPEndPointiep=newIPEndPoint(localIp,80);查看运行示例程序效果:7

2.2获取网卡信息和网络检测网络适配器又被称为网卡或者网络接口卡(NIC),是连接计算机和网络的硬件设备。网卡主要的工作原理是:整理计算机发往信道上的数据,并将数据分解为适当大小的数据包之后向网络上发送。在.NET开发中我们使用kInformation命名空间获取:网络流量数据、网络地址信息和本地计算机的地址更改通知等信息。该命名空间还包含实现Ping实用工具的类。可以使用Ping和相关的类检查是否可通过网络连接到计算机。2.2.1获取网卡信息获取网卡信息、网络连接和网络速度以及网络协议版本(包括:IPv4和IPv6)的网络接口信息,我们使用下面的两个类:kInterface类:提供了访问主机所有接口的信息的功能。利用该类我们可以方便的检测本机有多少个网卡、哪些网络连接可用、并获取某个网卡的型号、Mac地址和速度等信息。rfaceProperties类:可用于访问支持IPv4或IPv6的网络接口的配置和地址信息。该类是一个抽象类,不能直接创建,使用roperties()返回实例。下面我们通过一个简单的WindowsForms示例程序来学习如何获取网络接口的配置和统计信息,主要代码如下:1//获取主机上所有的网络适配器对象数组2NetworkInterface[]adapters=NetworkInterfaces();8

34//获取该网络适配器的配置对象5IPInterfacePropertiesadapterProperties=adapters[i].GetIPProperties();67//获取该网络适配器DNS服务器地址信息8IPAddressCollectiondnsServers=resses;运行实例程序效果:2.2.2网络流量检测我们可以使用kInformation命名空间的IPGlobalProperties类获取网络适配器接收、转发、丢弃、发送的数据包数目,该类提供有关本地计算机的网络连接的信息。检测网络流量是我们通过使用IPGlobalProperties类9

的GetIPGlobalProperties()方法获取记录本地计算机的网络连接和通信统计数据的信息的对象实例,通过实例的属性来获取相关信息,达到检测网络流量的目的:1//获取包含本机的网络连接和通信统计数据的信息的对象2IPGlobalPropertiesproperties=lobalProperties();34//获取本机IPv4统计数据5IPGlobalStatisticsipstate=4GlobalStatistics();下面我们还是通过上面的代码来创建一个小例子来学习如何检测网络流量,程序运行效果如下:2.2.3网络连接检测我们知道可以利用CMD命令行中输入的ping命令,通过调用命令行程序来检测网络连接,能够快速判断出网络故障。在.NET开发环境中我们可以通过使用kInformation命名空间下的Ping类、PingOptions10

类和PingReply类来实现类似于命令行的功能。*Ping类可以帮助应用程序确定是否可通过网络访问远程计算机*PingOptions类用于控制如何传输Ping数据包*PingReply类提供有关Send或SendAsync操作的状态及产生的数据的信息。示例程序主要代码如下:1//获取主机地址2stringhostAddress=_();3//构造Ping实例4PingpingSender=newPing();5//Ping选项设置6PingOptionsoptions=newPingOptions();agment=true;8//测试数据9stringtestData="TestData";10byte[]buffer=es(testData);11//设置超时时间12inttimeout=120;13//调用同步的Send方法发送数据,将结果保存至PingReply实例14PingReplyreply=(hostAddress,timeout,buffer,options);示例程序运行效果:11

2.3示例下载/s/1kT7UUWZ2.4参考资料mework4类库2.《C#网络应用编程2》第三章C#Socket编程(3)编码和解码在网络通信中,很多情况下:比如说QQ聊天,通讯双方直接传递的都是字符信息。但是字符信息并不能够直接通过网络传输,这些字符集必须先转换成一个字节序列后才能够在网络中传输,于是这里就产生了编码和解码的概念:*将字符序列转换为字节序列的过程称之为:编码*将编码的字节序列转换为字符序列的过程称之为:解码例如:对于Unicode字符来说,编码是指将一组Unicode字符转换为一个字12

节序列的过程,解码就是将编码字节序列转换为一组Unicode字符。3.1字符编码基础知识字符集(Charset):是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。常见的编码方式主要有一下三种:3.1.1ASCII字符集ASCII(AmericanStandardCodeforInformationInterchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以勉强显示其他西欧语言。它是现今最通用的单字节编码系统(但是有被Unicode追上的迹象),并等同于国际标准ISO/IEC646。3.1.2非ASCII字符集由于ASCII字符集是针对英语设计的,当处理汉字等其他非拉丁语系的字符时,这种编码就不能适用了(因为适用128个字符表示英文是完全足够的,但是用于表示中文就远远不够了)。为了解决这个问题,不同的国家和地区制定了自己编码标准。中国一般适用国标码,常用的有GB2312-1980编码和GB183030-2000编码,其中GB183030-2000编码汉字更多,是中国计算机系统必须遵循的基础性标准之一。3.1.3Unicode字符集由于每个国家、语系都拥有独立的编码方式,同一个二进制数字可以被解13

释成不同的字符,因此要想打开一个文本文件,就必须知道它的编码方式,否则就可能出现乱码。为了使国际信息交流更加方便,非营利机构统一码联盟制定和标准化了Unicode字符集。使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示216(即65536)个字符。基本满足各种语言的使用。实际上当前版本的统一码并未完全使用这16位编码,而是保留了大量空间以作为特殊使用或将来扩展。3.1.4UTF(通用转换格式)的出现Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的(例如:在C#中字符默认都是Unicode码,即一个英文字符占两个字节,一个汉字也是两个字节,这对于能适应ASCII字符集来表示的字符来说比较显得浪费),对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(UnicodeTransformationFormat,简称UTF)。目前流行和UFT格式包括UTF-8、UTF-16和UTF-32。其中,UTF-8编码是互联网上使用最广泛的一种UTF格式,这是一种变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。而遇到与其他Unicode字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1进行识别。这样对以7位ASCII字符为主的西文文档就大大节省了编码长度。UTF-8是与字节顺序无关的,它的字节顺序在所有系统中都是一样的,因此这种编码可以使排序变得很容易。14

3.2C#中不同编码和Unicode之间的转换在C#语言中对于不同编码和Unicode之间的转换使用位于命名空间中的Encoding类。通过这个类我们可以为不同字符集直接进行转换以及获取各个字符集的相关信息。3.2.1获取系统所有编码信息我们通过调用Encoding类的GetEncodings()方法获取包含所有编码的数组,通过数组元素为EncodingInfo类,通过数组内的元素可以获得各种类型编码的信息。例如我们可以通过下面的代码获取主机上所有编码的信息:1//获取系统所有编码名称及其描述信息2EncodingInfo[]allEncoding=odings();3foreach(EncodingInfoencodinginallEncoding)4{ine("编码标识符:{0,-10}编码名称:{1,-12}编码说明:{2}",ge,,yName);6}运行如下:15

3.2.2获取指定的编码信息Encoding类提供了常用的字符集编码,可以直接通过调用属性获取UTF-8,ASCII等属性,也可以通过调用GetEncoding(+4重载)方法直接获取指定的字符集编码对象。例如下面的代码:1//获取指定的编码描述信息2Encodinggb18030Encoding=oding("GB18030");3EncodingasciiEncoding=;ine("编码标识符:{0,-10}编码名称:{1,-12}编码说明:{2}",ge,Name,ngName);ine("编码标识符:{0,-10}编码名称:{1,-12}编码说明:{2}",ge,Name,ngName);运行如下:3.2.3在不同编码之间进行转换我们可以通过利用t(+2重载)直接将字节数组从一种编码转换为另一种编码。下面我们同样通过一个示例代码来学习如何对不同编码的字节序列进行转换。下面的示例程序,为了清楚的演示如何使用,可能代码比16

较冗余(代码中包含解码和编码部分,在随后会给出相应示例),实际的应用中我们可以根据自己的情况进行适当的对方法抽象,重构,提升程序的可读性和效率。代码如下:1//不同编码之间的转换2stringGB18030String="你好!晴天猪";ine("需要转换的字符串:{0}",GB18030String);4#region对字符串进行GB18030格式编码56//获取编码器7Encodinggb18030Encoding=oding("GB18030");8//将字符串转换为char类型数组9char[]chars=Array();10//获取编码为字节序列后的字节数组长度11intbuffLength=eCount(chars,0,);12//根据获取的字节长度声明数组,存储编码后的字节13byte[]gb18030Buffer=newbyte[buffLength];14//获取GB18030编码的字节序列15gb18030Buffer=es(chars,0,);ine("GB18030编码的字节序列:{0}",ng(gb18030Buffer));17//将GB18030编码的字节序列转换成UTF-8编码的字节序列18byte[]unicodeBuffer=t(gb18030Encoding,8,17

gb18030Buffer);ine("转换为UTF-8编码字节序列:{0}",ng(unicodeBuffer));2021#endregion2223#region将GB18030编码转换为UTF-8编码2425//获取UTF-8解码器26Decoderutf8Decoder=oder();27//获取解码为字符后字符数组的长度28intutfChartsLength=rCount(unicodeBuffer,0,,true);293637StringBuilderstrBuilder=newStringBuilder();foreach(charcainutfChart){(ca);18//根据获取解码后的长度创建char数组char[]utfChart=newchar[utfChartsLength];//将UTF-8编码的字节序列转换为字符串rs(unicodeBuffer,0,,utfChart,0);

38}ine("UTF-8的字符序列解码:{0}",ng());运行程序:3.3C#编码和解码在C#中为我们提供了Encoder和Decoder类,分别对字符进行编码和对字节序列进行解码的两个类。通过使用它们,我们可以很方便进行对字符和字节序列进行编码和解码操作。由于它们的构造函数都是protected级别的,需要使用Encoding实现的GetEncoder方法才能获取到它们的实例对象。下面我们通过一个WindowsForms示例程序来了解和学习如何使用这两个类,编码和解码的主要代码如下:1///

2///获取字符串编码之后的bytes数组3///4///编码类型名称5///将被编码的字符串6///7privatebyte[]GetEncodeBeforeBuffer(stringcodeType,stringstrCode)19

8{9//根据编码类型构造该类型编码的编码器的实例10Encoderencoder=oding(codeType).GetEncoder();11char[]chars=Array();12//根据获取对字符进行编码所产生的字节数来创建一个byte数组13byte[]bytes=newbyte[eCount(chars,0,,true)];14//将字符写入到byte数组中es(chars,0,,bytes,0,true);16returnbytes;17}18///

19///获取字符串解码之后的字符串20///21///编码格式22///编码的字节数组23///24privatestringGetDecodeBeforeText(stringcodeType,byte[]byteCode)25{26//根据编码类型构造该类型编码的解码器的实例27Decoderdecoder=oding(codeType).GetDecoder();28//计算对字节序列(从指定字节数组开始)进行解码所产生的字符数29char[]chars=newchar[rCount(byteCode,0,byteCode.20

Length,true)];30//根据获取的解码所产生的字节数来创建一个char数组31intcharLen=rs(byteCode,0,,chars,0);32StringBuilderstrResult=newStringBuilder();33foreach(charcinchars)34{35strResult=(ng());36}ng();38}运行程序:21

3.4示例源码/s/1hqCsRly3.5参考资料&进一步阅读:.NETFramework中的字符编码:在旧式编码与Unicode之间转换3.博客园:字符集和字符编码4.维基百科:ASCII5.维基百科:Unicode6.维基百科:UTF-87.《C#网络应用编程2》第四章C#Socket编程(4)初识Socket和数据流经过前面基础知识作为背景,现在对Socket编程进行进一步的学习。在s命名空间提供了Socket类,利用该类我们可以直接编写Socket的客户端和服务的的程序。但是直接使用Socket类编写Socket程序会比较麻烦、而且容易出错,所以.NET为我们提供了进一步封装好的TcpListener类、TCPClient类和UdpClient类。同时,当我们希望通过网络传输数据时,首先应该将数据转换为数据流。4.1Socket的类型Socket的中文释义称为套接字,是支撑TCP/IP通信最基本的操作单元。可22

以将Socket看做不同主机之间的进程进行双向通信的端点,在一个双方都可以通信的Socket实例中,既保存了对方的IP地址和端口,也保持了双方通信采用的协议等信息。Socket有三种不同的类型:①流套接字:实现面向连接的TCP通信②数据报套接字:实现无连接的UDP通信③原始套接字:实现IP数据包的通信(这里不做讨论)三种类型的套接字的对象均可使用Socket类来构造:1///

2///Socket构造函数3///4///网络类型5///Socket类型6///Socket使用的协议7publicSocket(AddressFamilyaddressFamily,SocketTypesocketType,ProtocolTypeprotocolType)当我们编写基于TCP和UDP的应用程序时,既可以使用对套接字进行进一步封装的TcpListener类、TCPClient类和UdpClient类,也可以直接使用Socket类来实现,如果没有特殊需求应该使用进一步封装过的类,由于Socket类是他们实现的基础,所有在这里我们先从学习Socket类入手。23

4.2第一个Socket程序在C#Socket编程(1)基本的术语和概念中,我们知道:IP协议层之上是传输层(transportlayer),它提供了两种可选的协议:TCP协议和UDP协议,它们分别是面向连接和无连接的两种协议。在面向连接的Socket中,使用TCP来建立两个地址端点的会话。一旦建立这种连接,就可以在设备之间进行可靠的数据传输。在进行更深入的学习前我们先巩固一个简单的例子(TCP)来对Socket编程建立一个直观的印象。TCPSocket连接的过程可以简单的分为:①服务端监听②客户端请求③建立连接在使用面向连接的Socket进行通信之前,两个应用程序之间首先要建立一个TCP连接,这涉及两台相互通信的主机的TCP部件间完成的握手消息(handshakemessage)的交换。下面我们通过直接使用Socket类来构建一个简单的Socket应用程序(这里先从同步Socket入手,实际项目要比这复杂,有许多需要考虑的问题:如消息边界问题、端口号是否冲突、消息命令的解析等等)。在这里我们为了和每一个客户端进行通信建立两个线程:一个是接受客户端连接的线程,一个是接受客户端数据的线程,下面是分别是示例程序的服务端和客户端的代码:4.2.1服务器端代码24

1privatestaticbyte[]m_DataBuffer=newbyte[1024];2//设置端口号3privatestaticintm_Port=8099;4staticSocketserverSocket;5staticvoidMain(string[]args)6{7//为了方便在本机上同时运行Client和server,使用回环地址为服务的监听地址8IPAddressip=ck;9//实例化一个Socket对象,确定网络类型、Socket类型、协议类型10serverSocket=newSocket(etwork,,);11//Socket对象绑定IP和端口号(newIPEndPoint(ip,m_Port));13//挂起连接队列的最大长度为15,启动监听(15);ine("启动监听{0}成功",ng());17//一个客户端连接服务器时创建一个新的线程18ThreadmyThread=newThread(ListenClientConnect);();25

20}2122///

23///接收连接24///25privatestaticvoidListenClientConnect()26{27while(true)28{29//运行到Accept()方法是会阻塞程序(同步Socket),30//收到客户端请求创建一个新的SocketClient对象继续执行31SocketclientSocket=();(es("Server说:Client你好!"));33//创建一个接受客户端发送消息的线程34ThreadreciveThread=newThread(ReciveMessage);(clientSocket);36}37}3839///40///接收信息41///26

42///包含客户端信息的套接字43privatestaticvoidReciveMessage(ObjectclientSocket)44{45if(clientSocket!=null)46{4748495051525354Socketm_ClientSocket=clientSocketasSocket;while(true){try{//通过clientSocket接收数据intreciverNumber=m_e(m_DataBuffer);ine("接收客户端:{0}消息:{1}",m_ng(),ing(m_DataBuffer,0,reciverNumber));55565758596061}catch(Exceptionex){ine(e);m_wn();m_();break;27

6263646566}}}}}4.2.2客户端代码:1//创建一个数据缓冲区2privatestaticbyte[]m_DataBuffer=newbyte[1024];3staticvoidMain(string[]args)4{5IPAddressip=ck;6SocketclientSocket=newSocket(etwork,,);78try9{t(newIPEndPoint(ip,8099));ine("连接服务器成功");12}13catch(Exceptionex)14{28

15161718}ine("连接服务器失败,按回车键退出");ine(e);return;19//通过clientSocket接收数据20intreceiveLength=e(m_DataBuffer);ine("接受服务器消息:{0}",ing(m_DataBuffer,0,receiveLength));22232425262728//通过clientSocket发送数据for(inti=0;i<10;i++){try{(1000);stringsendMessage=("{0}{1}","Server你好!",ng());(es(sendMessage));ine("向服务器发送消息:{0}",sendMessage);}catch{wn();29

3536373839404142}}}();break;}ine("发送完毕,按回车键退出");();4.2.3运行示例程序首先运行服务端程序:接着运行客户端程序,向服务端发送消息后这时候我们可以看到服务端已经收到了客户端发送的消息30

4.3网络流和内存流通过网络传输数据,或者对文件数据进行操作的时候都需要先将数据转换为数据流。典型的数据流是和某个外部数据源相关,数据源可以是文件、外部设备、内存、网络套接字等。.NET提供多个从Stream类派生的子类来对不同的数据源提供支持,每个类都代表了一种具体的数据流类型。例如和磁盘文件相关的文件流FileStream和Socket相关的NetworkStream,和内存相关的MemoryStream等,在Socket编程中我们只需了解NetworkStream和MemoryStream(具体文件IO可以参考博文:.NETI/O学习笔记:文件的读和写),一个用于网络数据的传输,另一个用作数据缓冲区。4.3.1网络流(NetworkStream)数据在网络的各个位置之间是以连续的字节形式传输的,我们使用NetworkStream类来发送和接收网络数据。和其他的的流类型不同NetworkStream类是在s命名空间中的,该类实现专门用于网络资源的Stream类。NetworkStream选件类和其他流之间的主要差异在于31

NetworkStream没有当前位置的概念,因此不支持查找功能,并且NetworkStream仅支持面向连接(TCP)的Socket。对于NetworkStream来说,写入操作是指将数据源内存缓冲区到网络上的数据传输;读取操作是从网络上到接收端内存缓冲区的数据传输。*创建NetworkStream对象我们可以通过TcpClient对象的GetStream()方法获取该对象发送和接收数据的NetworkStream对象:1TcpClientclient=newTcpClient();t("",8099);3NetworkStreamnStream=eam();也可以通过使用Socket来获取NetworkStream对象:1NetworkStreammyNetworkStream=newNetworkStream(mySocket);*通过NetworkStream对象获取数据接收数据端通过调用Read方法将数据从接收缓冲区中读取到进程缓冲区中,完成读取操作。可以通过调用DataAvailable属性来确定是否还有数据可供读取,如下:1TcpClientclient=newTcpClient();t("",8099);3NetworkStreamnStream=eam();4//是否有数据可读5if(d)6{32

789//接受数据的缓冲区byte[]myReadBuffer=newbyte[1024];StringBuildercompleteMessage=newStringBuilder();10intnumberOfBytesRead=0;11//准备接收的信息也有可能大于1024所以使用循环12do1314{numberOfBytesRead=(myReadBuffer,0,);Format("{0}",ing(myReadBuffer,0,numberOfBytesRead);122}}while(ailable);ine("接受的信息为:"+completeMessage);}else{ine("当前没有可供读取的数据。");33

4.3.2内存流(MemoryStream)MemoryStream表示保存在内存中的数据流,有该类封装的数据可以直接在内存中访问。内存流一般用于暂时缓存数据以降低应用程序对临时缓冲区和临时文件的需要。内存流相对于字节数组容量可以自动增长,并且在需要对数据进行加密以及对数据长度不定的数据进行缓存时,使用内存流比较方便。MemoryStream支持对数据流的查找和随机访问,当该类对象的CanSeek属性值为True时,程序可以通过范围Position属性获取内存流当前的位置。下面我们通过一个简单的小示例学习如何具体使用内存流:1staticvoidMain(string[]args)23456789//将待写入数据从字符串转换为字节数组UnicodeEncodingencoder=newUnicodeEncoding();{//构造MemoryStream实例MemoryStreamm_Stream=newMemoryStream();ine("初始化分配容量:{0}",m_ty);ine("初始使用量:{0}",m_);10byte[]bytes=es("新增数据");1112//向内存流中写入数据13for(inti=0;i<4;i++)34

14{ine("第{0}写入新数据",i);16m_(bytes,0,);17}1819//写入数据后MemoryStream实例的容量和使用量的大小ine("当前分配容量:{0}",m_ty);ine("当前使用量:{0}",m_);();24}4.4示例程序/s/1mgLErG04.5参考资料&进一步阅读:套接字:在网络上使用流3.维基百科:Sokcet4.《C#网络应用编程2》35

第五章C#Socket编程(5)使用TCPSocketTCP协议(TransmissionControlProtocol,传输控制协议)是TCP/IP体系中面向连接(connectionoriented)的传输层(transportlayer),TCP协议能够检测和恢复IP层提供的主机到主机的信道中可能发生的报文丢失、重复以及其他错误。由于TCP协议是一种面向连接协议:在使用它进行通信之前,两个应用程序之间首先要建立一个TCP连接。TCP能够在网络中提供双工和可靠的的服务。5.1TCP概述通信双方建立了TCP连接后,双方就可以相互发送数据了。TCP负责把用户数据(字节流)按照一定格式和长度组成多个数据报进行发送,然后在接到数据报之后分解按顺序重新组装和恢复用户数据。利用TCP传输数据时,数据是以字节的形式进行传输的。客户端和服务端建立连接后,发送数据方需要先将数据转换为字节流,然后将字节流发送到对方。TCP协议主要有以下特点:1.是面向连接的传输层协议2.每个TCP连接只能有两个端点,且只能一对一通信3.通过TCP连接传送数据能够保证报文的完整和准确性4.数据只能够以字节流的形式传输5.传输的数据无消息边界36

5.2在.NET平台TCP应用的工作模式5.2.1同步与异步工作方式在.NET平台下开发TCP应用程序,框架提供两种工作方式:①同步工作方式,②异步工作方式。这里所说的同步工作方式和异步工作方式和线程间的同步并不是一个概念。线程间的同步指的是不同线程或其他共享资源具有先后关联的关系;而同步TCP和异步TCP指的是TCP编程中采用的两种不同的工作方式,即从执行到第一个连接得到响应时,程序是否继续往下执行,继续执行的就是异步TCP,如果程序阻塞那就是同步TCP。与同步工作方式和异步工作方式相对应,利用Socket类开发应用.NET框架也提供了相应的编程方式:分别是同步Socket编程和异步Socket编程。为了简化编程的复杂度,.NET将Socket类进行进一步的封装,提供了两个类:TcpClient类和TcpListener类,这两个类也分别提供同步和异步工作方式的API。5.2.2了解TcpListener和TcpClient通过前面我们知道:TcpClient类和TcpListener类简化了Socket编程的复杂度,但是要注意:TcpClient和TcpListener这两个类只支持标准协议编程,如果需要编写非标准协议的应用程序,只能使用Socket来实现。TcpClient类用于提供本机主机和远程主机的连接信息,而TcpListener类则用于监听客户端的请求(这两个类的更多信息可以参考MSDN类库,文中已经给出连接,这里就不在赘述了)。当Socket通信双方建立了连接后,创建了TcpClient对象,就可以使用该对象的GetStream()方法得到NetworkStream对象,37

然后再利用网络流对象向远程主机发送或接收流数据。5.3解决TCP的无消息边界问题我们知道网络数据传输是基于流的,在采用TCP通信保证了我们接收和发送数据顺序和完整性,但是在实际的网络传输过程可能会出现发送方和接收方消息不一致的情况。例如:第一次发送的数据为“123456”,第二次发送的数据为“ABCDEF”,有时候可能会出现这种情况:“123456ABCDEF”同时接收了;或者是先接收“123456ABC”,然后在接收“DEF”等情况。之所以出现这种情况是因为:TCP是一种以字节流形式传输的、无消息边界的协议,由于网络中不确定因数的影响,因此不能够保证每个Send方法发送的数据被对应的Receive读取。所以在实际的Socket应用程序开发是必须要考虑消息边界的问题否则就有可能出现数据错误等问题。解决TCP消息边界一般使用下面的三种方式,我们可以根据场景的不同选用不同的方式。5.3.1发送固定长度的消息这种方式适用于消息长度固定的场景。具体实现时可以使用BinaryReader/BinaryWriter对象每次向网络流发送/读取一个固定长度的数据即可。例如每次发送一个int类型的32位整数。1TcpClientclient=newTcpClient("",5968);2NetworkStreamm_NetStream=eam();3BinaryWriterbw=newBinaryWriter(m_NetStream,8);(99);38

5.3.2将消息长度与消息一起发送这种方式一般在每次发送消息的前面用4个字节表明本次消息的长度,然后将包含消息长度的消息发送给对方;对方收到消息后,首先从消息的前四个字节读取消息长度,然后根据消息长度值接收发送方发送的数据。这种方法适用于任何场合,在这里我们可以利用BinaryReader和BinaryWriter对象来对NetworkStream进行进一步的封装,当我们使用BinaryWriter对象调用Write(+18重载)方法向网络流写入数据时,该方法会自动计算发出送数据占用的字节数,并使用4(根据发送数据类型)个字节附加到字符串前面;然后另一方使用BinaryReader对象的对应于BinaryWriter对象的Write()方法读取数据时,它会首先读取数据的长度,并自动根据数据前缀读取指定长度的数据。5.3.3使用特殊标记分隔消息这种方式适用于消息中不包含特殊标记的场合。例如:在每个命令后面添加回车换行(rn)符号作为分隔符的场合。如果对于字符串处理,实现这种方法最简便的途径是使用StreamWriter对象和StreamReader对象。发送时使用StreamWriter对象的WriteLine()方法将发送的字符串写入网络流,接收方只需需要调用StreamReader对象的ReadLine()方法将以回车换行符作为分隔符的字符串从网络流中读取即可。39

5.4同步TCPSocket示例程序经过前面知识的积淀,现在我们直接通过创建一个简单的聊天程序来基于同步TCPSocket网络聊天程序,程序的实现比较简单,服务器接收多个客户端连接,客户端和客户端直接通过服务器中转消息达到相互通信的目的。客户端和服务器直接的消息交互使用JSON来进行传递,在这里使用了第三方的JSON库(关于的使用细节参考:使用小结)。下面是程序运行的效果:运行服务端:打开多个客户端,在在线列表中选择聊天对象,发送聊天信息:40

5.5示例源码/redir?resid=17DF440127EF27A0!14585.6参考资料&进一步阅读1.维基百科:传输控制协议(TCP)2.《C#网络应用编程2》作者:晴天猪41


本文标签: 编码 使用 数据 协议 信息