c#socket网络编程.docx
《c#socket网络编程.docx》由会员分享,可在线阅读,更多相关《c#socket网络编程.docx(56页珍藏版)》请在冰豆网上搜索。
![c#socket网络编程.docx](https://file1.bdocx.com/fileroot1/2023-2/3/b98b5b04-50c6-4aab-90ae-f115ab54af11/b98b5b04-50c6-4aab-90ae-f115ab54af111.gif)
c#socket网络编程
1.1 套接字编程介绍
套接字(socket)是网络计算机与应用程序之间发送和接收数据的方式的一种抽象描述。
它描述了(可能在不同的计算机上,也可能在同一台计算机内的)两个通信点之间的连接。
在实际操作中,套接字编程往往与TCP/IP和UDP/IP通信相结合(关于TCP/IP和UDP/IP的更多信息参见下面的“理解IP、TCP和UDP”)。
论及套接字编程时,以下3类信息是很重要的:
协议(如TCP/IP或UDP/IP)。
IP地址(例如127.0.0.1)。
端口号(例如端口80)。
举例来说,对于http//这样的地址,你应该比较熟悉,这个地址用来指示Web浏览器加载位于的主页。
http指定了使用的应用协议(HTTP使用TCP/IP传输数据),指定了地址(名称将会被DNS服务器解析成一个IP地址)。
由于HTTP使用端口80进行通信,端口号80被隐式地指明,故没有出现在地址里。
如图1-1所示,通信的双方都必须拥有IP地址。
(点击查看大图)图1-1 Web浏览器和网络服务器之间的通信
尽管有TCP/IP这样的协议负责把数据从一个点传输到另一个点,但所传输数据的内容则需由诸如HTTP这样的应用协议来指定。
在.NETFramework里,套接字通信由Socket类来实现(该类位于System.Net.Sockets命名空间)。
理解IP、TCP和UDP
对于网络编程来说,深入地理解当下正在使用的一些常见网络协议是很重要的。
首先是网际协议(InternetProtocol,IP)。
IP指定了从一个点传送到另一个点的数据分组(如数据报datagrams)的格式和寻址方案。
假设IP是一种邮递系统,你可以通过它把邮件从一个地方寄到另一个地方。
你只需写上收件者地址并把邮件丢进邮箱里。
随后邮局会试图把邮件投递给收件人。
但是,你不能确定你的邮件肯定会到达目的地,也不会知道它究竟何时到达。
为了确保邮件被正确地投递,你必须使用额外的服务,比如挂号信。
与上述情形类似,我们需要将其他协议与IP联合使用,以保证数据分组传送无误。
传输控制协议(TransmissionControlProtocol,TCP)正是这样一种协议。
TCP是一种面向连接的网络协议,它(通过应答机制)保证数据分组可靠并有序地传送。
作为流行的网络协议,与IP协同工作的TCP已被Web浏览器和电子邮件客户这样的应用程序广泛采用。
TCP确保了传送的正确性,但它也有不便的地方。
正如要花更多的钱来寄送的挂号信一样,TCP给被发送的数据分组加上了额外的报头,增大了分组的尺寸。
因此,开发人员有时会将用户数据报协议(UserDatagramProtocol,UDP)与IP联用。
UDP是一种无连接的网络协议,同样把数据分组从一点发送到另一个点,只有一个例外——它并不提供可靠的、有保障的传送。
由于UDP不对传送提供保障,数据分组将包含更多的有效信息并能更快地传送。
使用UDP的开发人员必须建立自己的逻辑以确保数据分组的正确传送。
这也与邮寄的例子很相似:
你可以自己给收件人打电话,看他们是否已经收到你寄的邮件。
如果他们没有收到,你可能需要重新寄。
对于那些传送小数据分组且不需要数据精确组装的应用程序来说,UDP是非常有用的。
这类程序包括简单文件传输协议(TrivialFileTransferProtocol,TFTP)、域名系统(DomainNameSystem,DNS)以及语音IP(VoiceoverIP,VoIP)。
1.2 创建自己的多用户聊天应用程序
1.2 创建自己的多用户聊天应用程序
在本章的这一部分,我们将首先建立一个简单的聊天程序,它允许连接到中央服务器的任何人互相进行通信。
这样可以让你探索套接字通信的基本原理,并学习如何向所有已连接的用户广播消息。
图1-2展示了本章的这一部分将要的建立的应用程序。
(点击查看大图)图1-2 即将创建的聊天程序
1.2.1 为网络通信使用TcpClient和TcpListener类
创建聊天程序通常涉及套接字编程——创建一个客户和服务器之间的连接,使客户和服务器都能发送和接收消息。
System.Net.Sockets命名空间提供套接字编程所需的功能。
在这个项目我们将使用System.Net.Sockets命名空间里的2个类:
TcpClient和TcpListener。
TcpClient类实现了使用TCP发送和接收数据的套接字。
因为与远程设备的连接被表示为流,数据可以使用.NETFramework的流处理技术来读取和写入。
TcpListener类以阻塞同步模式提供用于监听和接受外来连接请求的简单方法。
下面的示例代码实现了一个简单的等待外来连接的服务器(一个控制台应用程序)。
要连接到服务器并向它发送一个字符串,客户代码(一个控制台应用程序)将是下面这样的:
注意,NetworkStream对象操作字节数组,因而需要使用来自System.Text命名空间的Encoding.ASCII.GetString()和Encoding.ASCII.GetBytes()方法来将字节数组转换成字符串,反之亦然。
上面的例子是比较简单的——它包含了服务器代码和客户代码。
服务器在127.0.0.1使用端口500打开一个套接字并监听外来TCP连接。
当连接建立起来以后,由一个NetworkStream对象读取客户发来的数据。
到达的数据随后显示在控制台上。
另一方面,客户在127.0.0.1打开一个连接,然后使用NetworkStream对象向服务器发送一个字符串。
但是,当服务器需要同时与多个客户通信并能同时发送和接收消息时,问题就会变得复杂得多。
为了实现这些,必须满足以下几条。
服务器必须能够与多个客户建立连接。
服务器必须能够从客户异步读取数据并能在任何时刻向客户发送消息。
客户必须能够从服务器异步读取数据并能在任何时刻向服务器发送消息。
接下来的几节将解决这3个问题。
1.2.2 构建服务器
(1)
聊天程序有两个部件——服务器和客户,我们来首先构建服务器。
用VistualStudio2005创建一个控制台程序项目,将该项目命名为Server。
在默认的Module1.vb/Program.cs文件里,首先导入System.Net.Socket命名空间,它包含这个项目将要用到的所有相关的类。
接下来,声明一个常量来存储这个应用程序使用的端口号。
对这个程序,我们使用端口号500。
提示 如果你在服务器(或者客户)上安装了防火墙,请确保打开端口500,以便这个应用程序运行。
我们还需要定义所要监听的本地地址,然后创建一个TcpListener类的实例,用来监听来自TCP客户的连接。
在Main()函数里,使用来自类TcpListener的Strart()方法来开始监听外来连接请求。
AcceptTcpClient()方法是一个阻塞式的调用,直到连接建立起来以后程序才会继续执行。
因为这个示例里的服务器需要同时为多个客户提供服务,我们将为每一个用户创建一个ChatClient类(稍后将定义)的实例。
服务器将会无限地循环检查,若有客户请求连接,则接受。
完整的Module1.vb源文件如下所示。
接下来定义ChatClient类。
我们使用ChatClient类来表示连接到该服务器的每个客户的信息。
在VisualStudio2005中为项目添加一个新的类,并将其命名为ChatClient.vb/ChatClient.cs。
照例,第一步是导入System.Net.Sockets命名空间(对于代码的C#版本,还需要导入System.Collections命名空间)。
1.2.2 构建服务器
(2)
在ChatClient类中,首先定义各种私有成员(它们的用途在代码注释中会有说明)。
同时声明一个HashTable对象(AllClients),用来保存所有连接到该服务器的用户的列表。
将它声明为共享成员的原因是,确保ChatClient类的所有实例都能够获得当前连接到该服务器的所有客户的列表。
当一个客户连接到服务器时,服务器将创建一个ChatClient类的实例,并将变量TcpClient(client)传递给类的构造函数。
同时,我们取得客户的IP地址并将它用作索引来标识HashTable对象中的客户。
BeginRead()方法将在独立的线程中启动一个从NetworkStream对象(client.GetStream)的异步读取。
这样可以使服务器保持响应能力并继续接受来自其他客户的新连接。
读取完成以后,控制将被转移到ReceiveMessage()函数(此函数稍后将会定义)。
在ReceiveMessage()函数中,首先调用EndRead()方法来处理异步读取的结尾。
在这里,我们将检查读取的字节数是否小于1。
如果是,则意味着客户已经断开连接,那么我们需要从HashTable对象中(使用客户的IP地址作为表中的索引)删除该客户,同时使用Broadcast()函数(此函数稍后定义)向所有客户广播消息,告知这一特定客户已经离开了聊天。
为了简单起见,假设客户会在第一次连接到服务器时发送他的昵称。
此后,只需向所有人广播该客户发出的所有信息。
一旦完成,服务器将再次执行从该客户的异步读取。
上面的代码里需要注意的一个问题是,需要使用SyncLock(C#中的lock)语句来阻止多个线程同时使用NetworkStream对象。
当服务器连接到多个客户而且它们全都在同一时刻试图访问NetworkStream对象时,这一情况就可能会出现。
SendMessage()函数允许服务器向客户发送消息。
最后,Broadcast()函数向存储在AllClientsHashTable对象中的所有客户发送消息。
1.2.3 构建客户
(1)
现在服务器已经构建起来,接下来构建客户。
使用VisualStudio2005创建一个新的Windows应用程序(将它命名为WinClient),并在默认窗体上放置如图1-3所示的控件。
设置txtMessageHistory控件的MultiLine和ReadOnly属性为True,并设置其ScrollBars属性为Vertical。
同时,设置btnSend控件的Enabled属性为False。
(点击查看大图)图1-3 在Windows窗体上放置控件
客户应用程序的逻辑与服务器类似,不过更加直接。
双击表单切换到编辑窗口,导入以下命名空间。
在类Form1里定义下列常量和变量。
当用户登录时,程序会首先连接到服务器并使用SendMessage()子程序发送用户的昵称,然后开始从服务器异步读取数据并将按钮的名称改为SignOut。
当用户从聊天应用程序退出时,调用Disconnect()子程序。
1.2.3 构建客户
(2)
用户点击Send按钮时,程序向服务器发送消息。
加入SendMessage()子程序,它允许客户向服务器发送消息。
ReceiveMessage()子程序在一个独立的线程里异步读取从服务器发来的数据。
收到数据时,它把数据显示在txtMessageHistory控件里。
因为Windows控件不是线程安全的,你需要使用一个代理(delUpdateHistory())来更新该控件。
线程安全
默认情况下,Windows应用程序使用单个的执行线程。
而当你有多个执行线程(正如使用ReceiveMessage()子程序在这里所做的一样)并试图更新来自不同线程的UI时,情况会变得稍稍复杂一些。
应该记住的是,我们无法直接访问独立线程(相对于所在的主线程而言)里Windows控件的属性,因为Windows控件不是线程安全的。
试图这么做将会引发运行时错误,这是VisualStudio2005新加入的一个有用的特性。
相反,我们应该使用代理,并且使用要更新的控件/窗体的Invoke()/BeginInvoke()方法来调用它。
使用代理delUpdateHistory()来调用主线程里的UpdateHistory()函数。
最后,Disconnect()子程序断开客户与服务器之间的连接。
窗体关闭时,调用Disconnect()子程序来断开客户与服务器之间的连接。
对于代码的C#版本,需要将以下代码中的黑体部分添加到Form1.Designer.cs(在SolutionExplorer中,调用ShowAllFiles按钮,你将在Form1.cs下面找到这个文件)中,为窗体的FormClosing事件绑定处理程序。
1.2.4 测试聊天应用程序
要测试这个应用程序,首先在VisualStudio2005里按下F5以运行服务器。
需要装载客户的多份副本以测试服务器的多线程能力,可以在WinClient项目的\bin目录下找到客户的.exe文件。
运行WinClient.exe的多个副本,登录并同时聊天(见图1-4)。
注解 简单起见,假设数据在同一个块里是通过TCP流发送和接收的。
但是,这并不总是正确的。
通过TCP流发送的数据并不保证立即到达,你可能在当前的读取循环里收到消息的一部分而在下一个循环里收到另一部分,或者几个消息会在同一时候被读到。
下一节的项目将会告诉你怎样处理这一问题。
(点击查看大图)图1-4 测试多用户聊天应用程序
1.3 构建高级的多用户聊天应用程序
在上一节你已经看到了怎样构建一个多用户聊天程序,实现多个用户同时聊天。
尽管该程序很有意思,但它不太灵活,因为你不能选择用户与他私下沟通,所有的信息都广播给聊天中的每个人。
在接下来的几节里,在前面几节所建立的基础上,我们将增强这个应用程序,以允许与选定的用户私聊。
我们还将在程序中加入FTP支持,实现用户之间的文件传输。
1.3.1 定义自己的通信协议
开始增强聊天程序时,你会意识到必须为种种不同的功能定义自己的应用程序协议。
比如,当你想要与某人聊天时,你需要向服务器指出用户名,以便只有发往该用户的消息才能到达他那里。
类似地,当你需要完成文件传输时,必须有几个握手环节以确保接收者明确地接受了文件传输,只有那时才能开始发送文件。
程序将使用下面几节中定义和描述的协议。
1.3 构建高级的多用户聊天应用程序
在上一节你已经看到了怎样构建一个多用户聊天程序,实现多个用户同时聊天。
尽管该程序很有意思,但它不太灵活,因为你不能选择用户与他私下沟通,所有的信息都广播给聊天中的每个人。
在接下来的几节里,在前面几节所建立的基础上,我们将增强这个应用程序,以允许与选定的用户私聊。
我们还将在程序中加入FTP支持,实现用户之间的文件传输。
1.3.1 定义自己的通信协议
开始增强聊天程序时,你会意识到必须为种种不同的功能定义自己的应用程序协议。
比如,当你想要与某人聊天时,你需要向服务器指出用户名,以便只有发往该用户的消息才能到达他那里。
类似地,当你需要完成文件传输时,必须有几个握手环节以确保接收者明确地接受了文件传输,只有那时才能开始发送文件。
程序将使用下面几节中定义和描述的协议。
1.3.3 功能一览
在开始学习怎样编写聊天应用程序之前,我们先来看看本章的这一部分将要构建的应用程序是什么样的。
登录到服务器时,在线用户的列表将会出现在ListBox控件里(见图1-5的左侧)。
要与一个用户聊天,简单地选择你想要交谈的用户,单击Send按钮以发送消息(见图1-6)。
要与多个用户聊天,按下Ctrl键并单击ListBox控件里的用户名(见图1-7)。
要向另一个用户发送文件,选择接收者的名称,点击SendFile按钮,然后选择想要传输的文件并点击Open(见图1-8)。
在接收者那端,他将会得到一个请求下载文件的提示框。
如果他点击了Yes,文件即被下载(见图1-9)。
当文件下载时,状态栏将显示当前已收到的字节数(见图1-10)。
(点击查看大图)图1-10 显示下载进度
1.3.4 构建服务器
(1)
这个聊天应用程序有两个组件:
服务器和客户。
我们首先构建服务器。
使用VisualStudio2005创建一个控制台程序项目,将它命名为Server。
在默认的Module1.vb文件里,输入以下代码。
使用真实的IP地址
如果仔细看过代码,你会发现这次使用了一个真实的IP地址(并且不是本地服务器地址127.0.0.1)。
在这里使用的IP地址——10.0.1.4——是由路由器分配的,你的计算机通常会有一个不同的IP地址。
之所以使用真实IP地址,是因为要测试这个项目的FTP功能需要至少两台计算机。
一台计算机上同时承载服务器和客户,而另一台则专门承载客户。
因为FTP功能会直接使用每个客户的IP地址,用本地服务器地址将会发生错误。
同时,如果服务器与客户不在同一台计算机上,你也需要使用服务器的真实IP地址。
你自己测试时,请务必使用服务器计算机的真实IP地址。
下一步是定义ChatClient类。
我们使用ChatClient类来表示每个连接到服务器的客户的信息。
在VisualStudio2005中向项目中加入一个新的类,并将其命名为ChatClient.vb。
首先导入以下命名空间。
在ChatClient类中,首先定义各种私有成员(它们的用途在代码的注释中说明)。
同时声明一个HashTable对象(AllClients),用来保存所有连接到该服务器的用户的列表。
将它声明为共享成员的原因是,确保ChatClient类的所有实例都能够获得当前连接到该服务器的所有客户的列表。
当一个客户连接到服务器时,服务器将创建一个ChatClient类的实例,并将变量TcpClient(client)传递给类的构造函数。
同时,我们取得客户的IP地址并将它用作索引来标识HashTable对象中的客户。
BeginRead()方法将在独立的线程中启动一个从NetworkStream对象(client.GetStream)的异步读取。
这样可以使服务器保持响应能力并继续接受来自其他客户的新连接。
读取完成以后,控制将被转移到ReceiveMessage()函数(此函数稍后将会定义)。
1.3.4 构建服务器
(2)
SendMessage()函数允许服务器向客户发送消息。
Broadcast()函数向存储在AllClientsHashTable对象中的所有客户发送消息。
1.3.4 构建服务器(3)
注解 发送给客户的所有消息都以换行符(VisualBasic里的vbLf,C#里的\n)结束。
在ReceiveMessage()函数中,首先调用EndRead()方法来处理异步读取的结尾。
在这里,我们将检查读取的字节数是否小于1。
如果是,则意味着客户已经断开连接,我们需要从HashTable对象中(使用客户的IP地址作为表中的索引)删除该客户,同时使用Broadcast()函数(此函数稍后定义)向所有客户广播消息,告知这一特定客户已经离开了聊天。
在这个ReceiveMessage()函数中,我们检查发送自客户的不同的消息格式并采取相应的动作。
比方说,客户发起一个FTP请求,则需要将消息重新打包(如前面1.3.2节所述)并将它发给文件的接收者。
必须注意,发送过来的数据可能并不是在一次全部到达——请求可能会被打破并被单独收到,或者多个请求会同时到达。
下面的“接收传入的数据”讨论了3种可能的情况。
接收传入的数据
有3种可能的情景。
情景1
第一种情景是理想的。
在这里,客户发来的字符串被完整地接收到。
下面的插图展示了由User1向User2和User3发起的一个交谈请求。
该请求以LF字符结束,而字节数组的剩余部分包含空字符(0)。
情景2
第二种情景在一个请求被打破并分别被收到时发生。
在下面的图解里,由User1发往User2和User3的请求被打破成两个部分。
只有请求的第二部分以LF字符结束。
情景3
第三种情景在两个单独的请求一起被收到时发生,如下图所示。
在这里,第一个请求与第二个请求以一个LF字符分隔。
1.3.5 构建客户
(1)
现在服务器已经构建起来,接下来构建客户。
使用VisualStudio2005创建一个新的Windows应用程序(将它命名为WinClient),并在默认窗体上放置如图1-11所示的控件。
设置txtMessageHistory控件的MultiLine和ReadOnly属性为True,并设置其ScrollBars属性为Vertical。
另外,设置lstUsers的SelectionMode属性为MultiExtended。
(点击查看大图)图1-11 在Windows窗体上放置各种控件
双击窗体以切换到代码视图,导入以下命名空间。
在类Form1里定义以下变量和常量。
当用户登录时,客户首先连接到服务器并使用SendMessage()子程序(稍后定义)发送用户的昵称。
然后开始从服务器异步读取数据并将SignIn按钮的文字改为SignOut。
它将请求当前在线的用户的列表。
当用户从聊天应用程序退出时,调用Disconnect()子程序(稍后定义)。
1.3.5 构建客户
(2)
Send按钮向服务器发送消息。
注意,在左边选择用户以后,你需要在ListBox控件里选择用户,才能发送消息。
前面代码使用的SendMessage()子程序允许客户向服务器发送消息。
1.3.5 构建客户(3)
ReceiveMessage()子程序在一个独立的线程里异步读取从服务器发来的消息。
收到数据时,它把数据显示在txtMessageHistory控件里。
因为Windows控件不是线程安全的,你需要使用一个代理delUpdateHistory()来更新该控件。
像前面一样,需要特别注意到请求可能不是整体到达的。
使用代理delUpdateHistory()调用主线程里的UpdateHistory()函数。
1.3.5 构建客户(4)
在UpdateHistory()子程序里,我们检查消息格式并执行适当的操作。
比如说,如果用户结束了聊天(通过[Left]消息),我们必须将它的用户名从ListBox里删除