C应用程序设计教程 第10章 多线程和Scket编程初步

上传人:ni****g 文档编号:567706170 上传时间:2024-07-22 格式:PPT 页数:144 大小:416.52KB
返回 下载 相关 举报
C应用程序设计教程 第10章 多线程和Scket编程初步_第1页
第1页 / 共144页
C应用程序设计教程 第10章 多线程和Scket编程初步_第2页
第2页 / 共144页
C应用程序设计教程 第10章 多线程和Scket编程初步_第3页
第3页 / 共144页
C应用程序设计教程 第10章 多线程和Scket编程初步_第4页
第4页 / 共144页
C应用程序设计教程 第10章 多线程和Scket编程初步_第5页
第5页 / 共144页
点击查看更多>>
资源描述

《C应用程序设计教程 第10章 多线程和Scket编程初步》由会员分享,可在线阅读,更多相关《C应用程序设计教程 第10章 多线程和Scket编程初步(144页珍藏版)》请在金锄头文库上搜索。

1、第10章 多线程和Socket编程初步 Socket编程技术广泛用于即时通信系统(如QQ、MSN等)、网络游戏、BT下载、Internet视频直播等C/S结构客户端网络程序,是一个程序员必须掌握的技术,本章介绍Socket编程初步知识。在Socket编程中,必须使用多线程技术,因此在本章首先介绍多线程,然后再介绍Socket编程。10.1 创建线程 如果在一个程序中,有多个工作要同时做,可以采用多线程。在Windows操作系统中可以运行多个程序,把一个运行的程序叫做一个进程。一个进程又可以有多个线程,所有程序的线程轮流共同占用CPU的运行时间,Windows操作系统将时间分为时间片,每个线程分

2、配一个时间片,一个线程用完一个时间片后,操作系统将此线程挂起,将另一个线程唤醒,使其使用下一个时间片,操作系统不断的把线程挂起,唤醒,再挂起,再唤醒,如此反复,由于现在CPU的速度比较快,给人的感觉象是多个线程同时执行。 Windows操作系统中有很多这样的例子,例如复制文件时,一方面在进行磁盘的读写操作,同时一张纸不停的从一个文件夹飘到另一个文件夹,这个飘的动作实际上是一段动画,两个动作是在不同线程中完成的,就像两个动作是同时进行的。又如Word程序中的拼写检查也是在另一个线程中完成的。每个进程最少有一个线程,叫主线程,是进程自动创建的,每进程可以创建多个线程。本节介绍线程类(Thread)

3、的属性和方法以及如何创建线程。 10.1.1 线程类(Thread)的属性和方法 线程类在命名空间System.Threading中定义的,因此如果要创建多线程,必须引入命名空间System.Threading。Thread类的常用属性和方法如下:属性Priority:设置线程优先级,有5种优先级类别:AboveNormal(稍高)、BelowNormal(稍低)、Normal(中等,默认值)、Highest(最高)和Lowest(最低)。例如语句myThread.Priority= ThreadPriority.Highest设置线程myThread的优先级为最高。一个线程的优先权并不是越高

4、越好,应考虑到整个进程中所有线程以及其他进程的情况做出最优选择。优先级相同的线程按照时间片轮流运行。优先级高的线程先运行,只有优先级高的线程停止、休眠或暂停时,低优先级的线程才能运行。构造函数:New(new ThreadStart(线程中要执行的无参数方法名),参数中指定的方法需要程序员自己定义,这个方法完成线程所要完成的任务,退出该方法,线程结束。该方法必须为公有void类型的方法,无参数。如果希望有参数,可使用VB.Net2.0中新构造函数:New(new ParameterizedThreadStart(线程中要执行的只能有一个参数的方法名)。方法Start():建立线程类对象后,线程

5、处于未启动状态,这个方法使线程改变为就绪状态,如果能获的CPU运行时间,线程变为运行状态。方法IsAlive():判断线程对象是否存在,=true,线程存在。方法Abort():撤销线程对象。不能撤销一个已不存在的线程对象,因此在撤销一个线程对象前,必须用方法IsAlive()判断线程对象是否存在。静态方法Sleep():线程休眠参数设定的时间,单位为毫秒,此时线程处于休眠状态。线程休眠后,允许其他就绪线程运行。休眠指定时间后,线程变为就绪状态。方法Suspend()和Resume():Suspend()方法使线程变为挂起状态。Resume方法使挂起线程变为就绪状态,如能获的CPU的运行时间,

6、线程变为运行状态。如线程多次被挂起,调用一次Resume()方法就可以把线程唤醒。由于不安全建议不使用这两个函数。10.1.2 创建线程例子【例10.1】本例使用线程类Thread创建一个新的线程,在标签控件中显示该线程运行的时间。在窗体放置2个按钮,单击按钮完成新建和停止线程的功能。(1)新建项目。在窗体中放置2个按钮和1个标签控件(label1)。button1的属性Text=新线程, Enabled= true。button2的属性Text=撤销,Enabled=false。(2)在Form1.cs头部增加语句: using System.Threading (3)为Form1类中声明一

7、个委托类dFun、定义一个类dFun的变量和线程类变量:/dFun类可代表无返回值有一个string参数方法delegate void dFun(string text);/dFun类变量dFun dFun1; /线程类变量private Thread thread; (4)为标题为“新线程”的按钮(button1)增加单击事件处理函数如下:private void button1_Click(object sender, EventArgs e) /生成线程类对象,fun为自定义方法名称 thread=new Thread(new ThreadStart(fun); Label1.Text

8、= 0“ 运行时间从0开始 /线程变为就绪状态,如能获的CPU运行时间, thread.Start() /线程变为运行状态 /标题为“新线程”的按钮,创建线程后, Button1.Enabled = False /不允许再创建线程 /标题为“撤销”的按钮,允许对运行状态的线程撤销 /Button2.Enabled = True(5)为标题为“撤销”的按钮(button2)增加单击事件处理函数如下:private void button2_Click(object sender, EventArgs e) if(thread.IsAlive) thread.Abort();/撤销线程对象 but

9、ton1.Enabled=true; button2.Enabled=false; (6)C#线程模型允许将任何一个公有过程(静态或非静态)作为线程过程,因此允许在任何一个类(不要求这个类是某个类的子类)中定义线程过程,而且同一个类中可以定义多个线程过程。 C#不允许在此过程中直接修改线程外控件属性,这是防止多个线程同时修改同一控件的同一属性发生错误,必须使用控件的Invoke方法修改线程外控件属性,Invoke方法有两个参数,参数1是修改控件属性的方法的委托, 参数2是object数组,是传递给参数1代表的方法的参数。为Form1类定义一个线程方法如下:/C#1.x中在线程中执行的方法,退出

10、该方法,线程结束public void fun() /必须为公有void类型方法,无参数 while(true)/这里是死循环,线程将一直运行 /允许得到线程外控件属性值 int x=Convert.ToInt32(label1.Text); x+; string s=Convert.ToString(x); /dFun1代表修改label1.Text的方法 label1.Invoke(dFun1,new objects); /线程休眠1秒钟,休眠一次,线程运行了1秒钟 Thread.Sleep(1000);(7)为Form1类定义一个修改label1.Text的方法如下: private v

11、oid SetText(string text) label1.Text = text; (8)在Form1类的Load事件函数的最后增加如下语句: dFun1=new dFun(SetText);(9)在关闭程序之前,必须撤销线程对象。为主窗体的FormClosing事件增加事件处理函数如下:private void Form1_FormClosing(object sender, FormClosingEventArgs e) if(thread.IsAlive) thread.Abort(); (10)编译运行,单击标题为新线程的按钮,新线程开始,计数器从0开始计数。单击标题为撤销的按钮

12、,线程对象被撤销,计数器停止计数。【例10.2】本例重做例9.19,查找文件在另一个线程中进行,当单击“停止搜索”按钮后,停止搜索线程,以便停止查找文件。本例修改例9.19。请同学课后自己完成。10.2 多个线程互斥多个线程同时修改共享数据可能发生错误。假设2个线程分别监视2个入口进入的人数,每当有人通过入口,线程用C#语句对总人数变量执行加1操作。一条C#语句可能包含若干机器语言语句,假设C#语句加1操作包含的机器语言语句是:取总人数,加1,再存回。操作系统可以在一条机器语言语句结束后,挂起运行的线程。如当前总人数为5,线程1运行,监视到有人通过入口,取出总人数(=5)后,线程1时间用完挂起

13、。线程2唤醒,也监视到有人通过入口,并完成了总人数加1并送回的操作,总人数为6,线程2挂起。线程1唤醒,对已取出的总人数(此时为5)加1,存回去,总人数应为7,实为6,少算一个。 为了防止此类错误,在一个线程修改共享资源(例如上例的总人数变量)时,不允许其他线程对同一共享资源进行修改,这叫线程的互斥。这样的实例很多,例如计算机中的许多外设,网络中的打印机等都是共享资源,只允许一个进程或线程使用。10.2.1 多个线程同时修改共享数据可能发生错误【例10.3】下边的例子模拟2个线程同时修改同一个共享数据时可能发生的错误。(1)新建项目。在Form1.cs头部增加语句:using System.T

14、hreading;(2)为Form1类定义2个Thread线程类变量:thread1,thread2。定义整形变量:num=0。(3)在窗体中放置一个标签和按钮控件,按钮的事件处理函数如下:private void button1_Click(object sender, EventArgs e) label1.Text = num.ToString(); (4)为Form1类构造函数增加语句如下: thread1= new Thread(new ThreadStart(Fun1);thread2= new Thread(new ThreadStart(Fun2);thread1.Start(

15、);thread2.Start();(5)为Form1类中定义Fun1() 方法如下:public void Fun1() int k,n; for(k=0;k4;k+) n=num; /取出num,可以把把num想象为总人数 n+;/加1 Thread.Sleep(20); /模拟复杂的费时运算 num=n;/存回num Thread.Sleep(50); /退出该方法,线程结束public void Fun2() int k,n; for(k=0;k4;k+) n=num; n+; Thread.Sleep(10); num=n; Thread.Sleep(100); (6)编译运行,单击

16、按钮,标签控件应显示8,实际运行多次,显示的数要小于8。10.2.2 用Lock语句实现互斥Lock语句的形式如下:lock(e)访问共享资源的代码。其中e指定要锁定的对象,锁定该对象内所有临界区,必须是引用类型,一般为this。Lock语句将访问共享资源的代码标记为临界区。临界区的意义是:假设线程1正在执行e对象的临界区中的代码时,如其他线程也要求执行这个e对象的任何临界区中代码,将被阻塞,一直到线程1退出临界区。 【例10.4】用C#语句Lock实现互斥。修改例10.2中的Fun1()和Fun2()方法如下: public void Fun1() int k,n; for(k=0;k4;k

17、+) lock(this)/这里的this是Form1类的对象 n=num; /这对大括号中代码为this的临界区 /this的临界区包含两部分, n+; /函数Fun1和Fun2中的临界区 Thread.Sleep(10); num=n; Thread.Sleep(50); /退出该方法,线程结束public void Fun2() int k,n; for(k=0;k4;k+) lock(this)/如有线程进入此临界区, n=num; /其他线程就不能进入这个临界区 /this的临界区包含两部分, n+; /函数Fun1和Fun2中的临界区 Thread.Sleep(10); num=n

18、; Thread.Sleep(100); /退出该方法,线程结束编译运行,单击按钮标签控件应显示8。 10.3 TCP/IP协议和Socket本节首先介绍TCP/IP协议的基础知识,然后介绍Socket类的基本概念。 10.3.1 TCP/IP协议协议 把分布在不同地理区域的计算机和网络设备利用通信设备互连,使各个计算机之间能够相互通信,实现信息和资源共享,就组成了计算机网络。网络的目的是为了通信,共享资源。通信即传输数据,为了传输数据各个网络系统应遵守一定规则,这个规则叫网络传输协议。当前广泛采用的网络协议是TCP/IP协议。 网络中有成千上万台计算机,应允许任何两台计算机之间进行通信,为了

19、区分不同的计算机,必须给每一台连网计算机一个唯一的编号,这个编号在TCP/IP协议中叫计算机的IP地址,它是一个32位二进制数,用四个十进制数表示,中间用点隔开,每个十进制数允许值为0-255(一个字节),例如,202.112.10.105,这种记录方法叫点数记法。一个计算机要和网络中其他计算机连接,必须有自己的IP地址。C#语言使用IPAddress类表示IP地址,用静态方法Parse可将IP地址字符串转换为IPAddress实例。例如:/127.0.0.1表示本机IP地址IPAddress ip = IPAddress.Parse(“127.0.0.1”); IPAddress类提供了几个

20、静态只读字段,其中字段Any表示本地系统所有可用的IP地址,字段Broadcast表示本地网络广播地址。 Dns类提供了一系列静态的方法,其中GetHostAddresses方法获取指定主机的IP地址,返回一个IPAddress类型的数组(一台计算机可能有多个IP地址)。 例如获得CCTV网站的所有IP地址:IPAddress ip=Dns.GetHostAddresses(); Dns类GetHostName方法,获取本机主机名。 string hostname = Dns.GetHostName(); IPAddress ip=Dns.GetHostAddresses(hostname);

21、一台计算机上可能运行多个网络通信软件,它们的IP地址是相同的。为了访问IP地址相同的不同网络通信软件,可为运行的每个网络通信软件编号,这个编号叫端口号。 IPEndPoint类包含了IP地址和端口信息,IPEndPoint类常用的构造函数如下,第一个参数指定IP地址,第二个参数指定端口号 public IPEndPoint(IPAddress, int); 10.3.2 套接字(Socket)套接字可以理解为编写网络通信软件的函数库,在套接字中封装了为进行网络通信而设计的一组公共函数,网络通信软件通过调用这些公共函数,完成和在网络其他计算机中运行的指定网络通信软件间的双向通信。在.Net中,S

22、ystem.Net.Sockets 命名空间为开发人员提供了开发基于Socket套接字的网络通信程序的一些类,包括Socket类、TcpClient类、TcpListener类和UdpClient类,如果开发基于TCP/IP网络协议网络通信程序,可以使用TcpClient类、TcpListener类和UdpClient类,使用上比较简单,本书所有例子基本上都是使用这三个类。如果为了提高效率或者采用其他网络通信协议,可采用Socket类。 套接字有两种不同的类型:一种是流套接字,又称面向连接的协议,如 TCP;另一种是数据报套接字,又称无连接协议,例如 UDP。基于流套接字的网络通信采用连接方式

23、,通信前要进行网络连接,一旦建立了这种连接,就可以在设备之间可靠的传输数据,建立连接后数据以流的形式在被连接的两个计算机中运行程序间进行流动。这有些像打电话。基于流套接字的网络通信一般采用客户机/服务器模式。基于数据报套接字,采用不连接方式,两个计算机中运行程序间使用单个信息包进行数据传输,这种方式类似邮局,不保证数据包按照发送顺序传送,也可能丢失。以下简单介绍Socket类的用法,后续章节将详细介绍TcpClient类、TcpListener类和UdpClient类的使用。Socket类的构造方法定义如下,其中,addressFamily 参数指定 Socket 使用的寻址方案,socket

24、Type 参数指定 Socket 的类型,protocolType 参数指定 Socket 使用的协议。 public Socket(AddressFamily addressFamily,SocketType socketType, ProtocolType protocolType);生成基于 TCP协议的Socket类对象的例子如下: Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 一旦创建 基于 TCP协议连接的Socket类对象,在客户端将通过Connect

25、方法连接到指定的服务器,通过Send/SendTo方法向远程服务器发送数据,通过Receive/ReceiveFrom从服务端接收数据;而在服务器端,需要使用Bind方法将Socket对象绑定到本地指定的IP地址和端口号,并通过Listen方法侦听该接口上的请求,当侦听到用户端的连接时,调用Accept完成连接的操作,创建新的Socket以处理传入的连接请求。使用完 Socket 后,使用 Shutdown 方法禁用 Socket,并使用 Close 方法关闭 Socket。 生成基于 UDP协议的Socket类对象的例子如下: Socket s = new Socket(AddressFam

26、ily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); 由于UDP不存在固定连接,所以可直接使用SendTo方法发送数据,用ReceiveFrom方法接收数据,如不再使用 Socket对象,用 Shutdown 方法禁用Socket对象,用 Close 方法关闭 Socket对象。10.4 基于TCP协议的Socket编程本节详细介绍编写基于基于TCP协议的Socket程序方法和步骤。在System.Net.Sockets命名空间下,TcpClient类与TcpListener类是两个专门用于TCP协议编程的类。这两个类封装了底层的套接字,

27、并分别提供了对Socket进行封装后的同步和异步操作的方法,降低了TCP应用编程的难度。TcpClient类用于连接、发送和接收数据。TcpListener类则用于监听是否有传入的连接请求。基于TCP协议的网络通信一般采用客户机/服务器模式,因此必须分别建立客户机和服务器程序。10.4.1 TcpClient类 利用TcpClient类提供的方法,可以通过网络进行连接、发送和接收网络数据流。该类的构造函数有四种重载形式,常用属性方法如下:无参数构造函数:创建一个TcpClient类对象,该对象自动选择客户端IP地址和尚未使用的端口号。创建该对象后,即可用Connect方法与服务器端进行连接。例

28、如:TcpClient tcpClient=new TcpClient();tcpClient.Connect(, 51888);构造函数New(AddressFamily family):创建的TcpClient类对象也能自动选择客户端IP地址和尚未使用的端口号,但是使用AddressFamily枚举指定了使用哪种网络协议。创建该对象后,即可用Connect方法与服务器端进行连接。例如:TcpClient tcpClient = new TcpClient(AddressFamily.InterNetwork);tcpClient.Connect(, 51888);构造函数New(IPEnd

29、Point iep):iep是IPEndPoint类的对象,iep指定了客户端的IP地址与端口号。当客户端的主机有一个以上的IP地址时,可使用此构造函数选择要使用的客户端主机IP地址。例如:IPAddress address = Dns.GetHostAddresses(Dns.GetHostName();IPEndPoint iep = new IPEndPoint(address0, 51888);TcpClient tcpClient = new TcpClient(iep);tcpClient.Connect(, 51888);构造函数New(string hostname,int p

30、ort):这是使用最方便的一种构造函数。该构造函数可直接指定服务器端域名和端口号,而且不需使用connect方法。客户端主机的IP地址和端口号则自动选择。例如:TcpClient tcpClient=new TcpClient(, 51888); 方法Connect():和服务器进行连接,参数分别是服务器IP地址和端口号。方法Close():释放此 TcpClient 实例,而不关闭基础连接。方法GetStream():返回用于发送和接收数据的 NetworkStream。见后边例子。属性SendTimeout和ReceiveTimeout:等待发送和接收成功完成时间,超过这个时间,将产生So

31、cketException异常。属性SendBufferSize和ReceiveBufferSize:发送和接收缓冲区大小。属性Connected:是否已和服务器连接。属性Client:TcpClient类对象使用的Socket类对象。10.4.2 TcpListener类TcpListener类用于监听和接收传入的连接请求。该类的构造函数及常用函数如下:TcpListener(IPEndPoint iep):该构造函数通过IPEndPoint类型的对象在指定的IP地址与端口监听客户端连接请求。TcpListener(IPAddress localAddr, int port):建立TcpLi

32、stener对象,在参数中直接指定本机IP地址和端口,并通过指定的本机IP地址和端口号监听传入的连接请求。AcceptTcpClient():等待连接,直到有新的连接,获取并返回一个用来接收和发送数据的套接字对象后,才执行后续语句。这种方式称作同步阻塞方式。AcceptSocket:在同步阻塞方式下获取并返回一个可以用来接收和发送数据的封装了Socket的TcpClient对象。Start():启动监听,其构造函数为:Start(int backlog):整型参数backlog为请求队列的最大长度,即最多允许的客户端连接个数。Stop():停止监听请求。10.4.3 服务器程序 使用TCP和流

33、套接字建立服务器,服务器将等待来自客户机的连接请求。在接到请求后,服务器建立和客户机的连接,利用这个连接,服务器和客户机实现通信。IE浏览器(客户机)和Web服务器就是一个典型的客户机/服务器模式,IE浏览器向Web服务器请求网页,Web服务器接到请求,发送请求的网页到IE浏览器。VB.Net语言使用TCP和流套接字建立服务器需要五步。具体步骤如下:(1)System.Net.Sockets命名空间的TcpListener类对象用来等待来自客户机的连接请求,TcpListener类采用TCP协议。创建TcpListener类对象例子如下: /采用本机IP地址,端口号为1300 TcpListe

34、ner server = new TcpListener(1300); 客户端程序必须知道服务器的IP地址和端口号,才能和服务器建立连接。使用如下方法获得IP地址和端口号,IPEndPoint和IPAddress在System.Net命名空间。IPEndPoint iPEndPoint = server.LocalEndpoint;IPAddress iPAddress = iPEndPoint.Address;int port = iPEndPoint.Port;(2)使用TcpListener类方法Start()开始等待来自客户机的连接请求,代码如下:Server.Start() 或者采用

35、下条语句Server.Start(200)参数是允许的最大的连接客户机数(3)使用TcpListener类方法AcceptSocket()等待来自客户机的连接请求,如果没有客户机的连接请求,程序将被阻塞,既不能执行这条语句的后续语句。如果有一个客户机的连接请求,将返回一个Socket或TcpClient类对象,将继续执行后续语句。代码如下: /返回Socket类对象,然后执行后续语句 Socket socket = server.AcceptSocket(); /或采用本语句返回TcpClient类对象TcpClient tcpClient = server.AcceptTcpClient()

36、; 得到Socket或TcpClient类对象,已经和客户机建立了连接,就可以和客户机进行通信。在通信时,将不再侦听其他客户机的连接要求。很多服务器是不允许这种情况发生的,例如Web服务器必须随时等待众多的浏览器的访问。解决的方法是建立一个线程用来和这个客户机进行通信,而TcpListener类对象server将继续侦听其他客户机的连接要求。(4)如果使用server.AcceptSocket方法建立连接,返回的Socket类对象,就可以使用Socket类的Send方法发送数据(返回TcpClient用法见下节)。代码如下:byte msg=Encoding.UTF8.GetBytes(Thi

37、s is a test);int i=socket.Send(msg); /i为发送数据的字节数 可以使用Socket类的方法Receive接收数据(TcpClient用法见下节),代码如下:byte bytes = new byte256;i=socket.Receive(bytes);/i为发送数据的字节数(5)最后,如果不再通信,使用Socket类的Close方法终止连接,代码如下:Socket.Close()10.4.4 客户机程序 网络中的计算机可以运行客户机端网络程序访问服务器,例如,通过IE浏览器(客户机)可以访问Internet中的Web服务器,浏览网页。编写运行于客户机端的网

38、络程序程序需要四个步骤。具体步骤如下:(1)创建System.Net.Sockets命名空间的TcpClient类对象用来和服务器建立连接,代码如下:/自动选择最合适的本地 IP 地址和端口号 TcpClient tcpClient = new TcpClient (); /和本机的服务器连接tcpClient.Connect (localhost,1300); Connect方法的第一个参数也可以是远程服务器的域名,例如,“”。如果知道远程服务器的IP地址,可以采用如下代码: TcpClient tcpClient = new TcpClient ();/参数为远程服务器的IP地址IPAdd

39、ress ServerIP=IPAddress.Parse(202.206.96.204); tcpClient.Connect (ServerIP,1300)(2)使用TcpClient类的GetStream方法得到一个NetworkStream类对象,用来对服务器进行读写。NetworkStream netStream = tcpClient.GetStream(); (3)使用NetworkStream类对象读写服务器数据代码如下:if (netStream.CanWrite) Byte sendBytes = Encoding.UTF8.GetBytes (Is anybody the

40、re?);if (netStream.CanRead)byte bytes = new bytetcpClient.ReceiveBufferSize; netStream.Read (bytes, 0, (int)tcpClient.ReceiveBufferSize); string returndata = Encoding.UTF8.GetString (bytes);(4)关闭NetworkStream类对象后,关闭和服务器的连接。netStream.Close()tcpClient.Close()10.4.5 TCP协议Socket实例 本节首先实现一个时间服务器,客户端访问这个时

41、间服务器系统,可以得到时间服务器系统所在地点的时间,在例子中时间服务器直接使用侦听线程和客户机通信,因此本例仅支持客户机顺序访问和多次访问,但由于服务器发送时间的代码很少,很快能够完成,所以客户机程序感觉没有延迟很快就能得到时间。这是一个最简单的基于TCP协议的Socket程序实例,通过这个例子读者可以清楚地理解Socket编程的基本步骤。实际服务器要比这个时间服务器复杂的多,一般情况下,服务器和客户机通信也许需要较多的时间,例如客户机访问文件下载服务器下载文件,服务器直接使用侦听线程和客户机通信显然不能实现多客户机同时访问服务器功能。例10.7和例10.8实现了一个文件下载系统,该系统实现了

42、多客户机同时访问服务器功能。【例10.5】本例实现一个时间服务器,客户端访问这个时间服务器系统,可以得到时间服务器系统所在地点的时间。这是一个最简单的Scoket编程实例。具体实现步骤如下: (1)建立一个新的Windows应用项目 。在Form1.cs头部增加命名空间引用: using System.Net; using System.Net.Sockets; using System.Threading;(2)为Form1类增加变量: Thread thread; /线程类变量 bool ifStop = true; /是否停止时间服务器 /负责侦听是否有客户机访问服务器 TcpListe

43、ner server;/服务器端和客户机连接的Socket类对象Socket socket; (3)修改构造函数如下:public Form1() InitializeComponent();/建立侦听线程,TimeThread是线程执行的方 /法名称,退出该方法,线程结束 thread = new Thread (new ThreadStart(TimeThread); thread.Start();/线程启动 ifStop=false;/变量表示是否退出线程,false不退出Text= 时间服务器; /Form1窗体的标题栏内容(4)侦听工作不能在主线程中进行,否则当侦听工作被阻塞后,将不

44、能执行其他任何语句,程序看起来就像死了一样,不能执行任何动作。因此侦听工作必须在另一线程中进行。在线程为Form1类定义一个侦听线程方法如下,采用本机IP地址,端口号为1300。public void TimeThread() try server = new TcpListener(1300); server.Start();/开始侦听是否有客户机连接服务器 catch MessageBox.Show(不能建立服务器, 提示, MessageBoxButtons.OK); Return; /原因可能是端口号1300被占用 /或网络不可用,退出线程 while (!ifStop) /如退出wh

45、ile语句,线程结束 Try /下句等待客户端的连接 aSocket = server.AcceptSocket() /阻塞 /得到用字符串表示的时间 string s = DateTime.Now.ToString(); /将时间字符串转换为字节数组 byte msg = Encoding.UTF8.GetBytes(s); /本例发送时间方法Send和侦听方法/AcceptSocket()在同一线程。在发送时间时不能/继续侦听是否有客户机连接服务器。本例发送/数据较少,发送后很快开始侦听,基本不影响 /其他客户机的连接。本方法支持客户机顺序访/问和多次访问。如果发送数据较多占用较多时/间或

46、者客户机要长时间和服务器连接,必须建/立新线程用来发送数据使侦听可以继续,见后/续例子。发送时间到客户机,完成之前被阻塞/完成后执行后续语句。 aSocket.Send(msg) aSocket.Close() 送出时间后关闭和客户机的连接/退出前,要使ifStop=true,关闭socket和server,/如果这两个对象正在使用必定产生异常,执行catch中/语句,继续while循环,由于ifStop=true,将退出while/循环语句,即退出TimeThread方法结束线程。/如果仅仅是在程序运行时,socket = /server.AcceptSocket()或socket.Send

47、(msg)语句发生/异常,由于ifStop=false,仅仅重新开始侦听。 catch if (socket != null) socket.Close();/关闭关闭和客户机的连接 if (socket != null)/运行到此,线程将结束 socket.Close();/关闭关闭和客户机的连接 server.Stop(); /关闭TcpListener类对象取消侦听/运行到此,线程将结束,要关闭所有建立的对象 (5)在关闭程序之前,必须撤销线程对象。为主窗体的Closing事件增加事件处理函数如下:private void Form1_FormClosing(object sender,

48、 FormClosingEventArgs e) ifStop = true; if (socket != null) socket.Close(); if (server != null) server.Stop(); if (thread != null & thread.IsAlive) thread.Abort();(6)编译得到可执行文件。请注意,所建立的时间服务器必须在另一个线程中运行,而不能在主线程中,否则主线程将不会响应用户的任何动作,包括关闭程序。这是由于函数TimeThread()中包括一个死循环,如在主线程中运行,将占用主线程的所有时间,没有时间去运行其他代码。读者可以试

49、验一下,修改上例,首先去掉构造函数中自己增加的语句,然后增加一个按钮,为按钮增加单击事件处理函数,在函数中,调用函数TimeThread(),编译运行后,单击按钮,程序可以得到时间,但是将不能使用关闭按钮关闭程序。【例10.6】本例实现客户机从例10.5的时间服务器得到时间并显示。具体步骤如下:(1)建立一个新的Windows应用项目。在Form1.cs头部增加命名空间引用:using System.Net; using System.Net.Sockets; (2)为Form1类增加变量:TcpClient tcpClient; /客户机类对象/网络流对象,流的概念参见第9章NetworkS

50、tream netStream;(3)在窗体Form1中放置1个Label控件用来显示时间,增加1个Button控件,标题为“得到时间”,按钮的单击事件函数如下。接受应在另一线程,为了简单接受也在主线程(按钮的单击事件函数中),为防止无限等待,设定超时时间,超时发异常,5秒未接到时间数据,引发异常。下句自动选择本地 IP 地址和端口号。private void button1_Click(object sender, EventArgs e) aTcpClient = New TcpClient() aTcpClient.ReceiveTimeout=5000/设定超时时间5秒/localho

51、st表示程序所在计算机的服务器,这样设置服务/器和客户机在同一台计算机,连接成功之前被阻塞,/成功后执行下条语句,5秒后仍未连接成功,抛出异常。/1300为时间服务器端口号 try tcpClient.Connect(localhost,1300); netStream = tcpClient.GetStream(); if (netStream.CanRead)/判断数据否支持读取 /发来的是字节数组,定义字节数组保存接收的数据 byte bytes = new bytetcpClient.ReceiveBufferSize; /开始读服务器发回的时间,接受成功前被阻塞, /成功后执行下条语

52、句,5秒未读出数据抛出异常 netStream.Read (bytes, 0, (int)tcpClient.ReceiveBufferSize); label1.Text = Encoding.UTF8.GetString (bytes); catch label1.Text = 连接超时,连接不成功; Finally if(netStream!=null) netStream.Close();tcpClient.Close(); (4)首先运行时间服务器程序,再运行客户机程序,单击客户机程序的标题为“得到时间”按钮,显示当前时间。关闭时间服务器程序,再一次单击客户机程序的标题为“得到时间”

53、按钮,显示连接超时,连接不成功。 在网络应用程序中,经常传送文件。从上边的例子可以看到,在网路中使用字节数组进行传送,因此传送文件,首先要把文件变为字节数组,接收文件,则必须把字节数组变为文件。文件变为字节数组的具体步骤如下:FileStream fs = 参数1是要传输的文件 New FileStream(d:/g1.bin,FileMode.Open)byte data=new bytefs.Length; /将文件读到字节数组data中,n为所读字节数long n=fs.Read(data,0,(int)fs.Length); fs.Close();字节数组变为文件具体步骤如下:File

54、Stream fs= /参数1是保存文件全路径 new FileStream(d:/g1.bin,FileMode.Create)/写data字节数组中的所有数据到文件fs.Write(data,0,data.Length)fs.Close() 如果创建一个文件下载服务器,客户机就可以访问文件下载服务器下载文件。下载文件的时间一般比较长,因此当客户机和文件下载服务器建立连接后,下载文件的工作必须在另一个线程中进行,以便文件下载服务器可以继续侦听工作,等待其他客户机的连接,使文件下载服务器允许多个客户机同时下载文件。 在客户机中,接收文件下载服务器传输的文件也必须在另一线程中,否则,接收过程将占

55、用主线程的所有时间,主线程不能响应其他任何事件,包括关闭窗体。Socket类读写缓冲区的大小是一定的,而文件大小可能超过读写缓冲区的大小,如使用Socket类Send方法发送文件,可能要写多次才能完成,很不方便。可以使用NetworkStream的Write方法写文件,调用一次Write方法就完成文件文件传送。【例10.7】本例实现文件下载服务器,客户机可以访问文件下载服务器下载文件。为了简单,客户机和下载文件服务器建立连接后,立刻传递一个指定文件到客户端。步骤如下:(1)建立一个新的Windows应用项目 。在Form1.vb头部增加命名空间引用:using System.Net; usin

56、g System.Net.Sockets; using System.Threading;using System.IO; /读写文件必须引用的命名空间(2)为Form1类增加变量:/线程类变量,分别引用侦听线程和下载线程private Thread Listenerthread,DownLoadthread; bool ifStop = true; /是否停止下载服务器/负责侦听是否有客户机访问服务器TcpListener server; /服务器端和客户机连接的TcpClient类对象TcpClient socket; /服务器端和客户机连接后,得到TcpClient类对/象,将创建下载线

57、程和流对象程序关闭前,必/须关闭TcpClient类对象、下载线程和流对象,/本结构用来记录这些信息public struct DownLoadthreadObject public Thread thread;/下载线程public TcpClient tcpClient;/TcpClient类对象public NetworkStream networkStream; /流对象 /记录所有服务器端和客户机连接信息List downLoadthreadObjectS; (3)修改构造函数如下:public Form1() /建立侦听线程,ListenerthreadMethod是线程执 /行的

58、方法名称,退出该方法,线程结束 Listenerthread = new Thread(new ThreadStart(ListenerthreadMethod); Listenerthread.Start(); /侦听线程启动 ifStop=false; /为false表示不退出侦听线程 Text= 文件下载服务器“; /窗体的标题栏内容 downLoadthreadObjectS=new List();(4)为Form1类定义一个线程方法如下:public void ListenerthreadMethod() /线程执行的方法 Try /下句采用本机IP地址,端口号为1300 serve

59、r = New TcpListener(1300) server.Start()/开始侦听是否有客户机连接服务器 catch MessageBox.Show(不能建立服务器, 提示, MessageBoxButtons.OK); Return; while (!ifStop) try /未侦听到客户机前被阻塞,成功后执行后续语句 aSocket=server.AcceptTcpClient(); DownLoadthread = New Thread( 下载线程 New ThreadStart(AddressOf ClientThreadF); DownLoadthread.Start();

60、Thread.Sleep(100)/等待ClientThreadF正常工作 catch /不处理异常,退出线程方法 if (socket != null) socket.Close(); server.Stop();/关闭TcpListener类对象取消侦听 (5)为Form1类定义ClientThreadF如下,每连接一个客户,建立一个下载线程。下载线程负责发送文件。public void ClientThreadF() FileStream fs = null;DownLoadthreadObject downLoadthreadObject=new DownLoadthreadObjec

61、t();downLoadthreadObject.thread = DownLoadthread;downLoadthreadObject.tcpClient = socket;try/下条语句得到网路流对象 NetworkStream netStream = downLoadthreadObject.tcpClient.GetStream();downLoadthreadOworkStream = netStream; lock(this) downLoadthreadObjectS.Add(downLoadthreadObject); /下句参数1是要传输的文件 fs=new FileSt

62、ream(d:/g1.txt,FileMode.Open); byte data=new bytefs.Length; fs.Read(data,0,(int)fs.Length);/读文件到字节数组 fs.Close(); if (netStream.CanWrite) netStream.Write(data, 0, data.Length);catch/不处理异常,退出线程方法 finally if(fs != null) fs.Close();if(downLoadthreadOworkStream!=null) downLoadthreadOworkStream.Close(); i

63、f (downLoadthreadObject.tcpClient!= null) downLoadthreadObject.tcpClient.Close();lock(this) downLoadthreadObjectS.Remove( downLoadthreadObject); (6)在关闭程序之前,必须撤销线程对象。为主窗体的Closing事件增加事件处理函数如下: private void Form1_FormClosing(object sender, FormClosingEventArgs e) ifStop = true; foreach(DownLoadthreadOb

64、ject DLO in downLoadthreadObjectS) if(DLO.networkStream != null)DLO.networkStream.Close(); if(DLO.tcpClient!= null)DLO. tcpClient.Close(); if(DLO.thread != null & DLO.thread.IsAlive)DLO.thread.Abort(); if (server != null) server.Stop(); if(Listenerthread!=null&Listenerthread.IsAlive) Listenerthread.

65、Abort();(7)编译得到可执行文件。【例10.8】本例实现客户机,从例10.7的文件下载服务器下载文件。具体步骤如下:(1)建立一个新的Windows应用项目 。在Form1.cs头部增加命名空间引用:using System.Net; using System.Net.Sockets; using System.Threading;using System.IO;(2)为Form1类增加变量:TcpClient tcpClient;/客户机类对象NetworkStream netStream;/网络流对象Thread DownLoadthread;/下载线程类变量(3)在窗体Form1

66、中放置1个Button控件,标题为“下载文件”,事件函数如下:private void button1_Click(object sender, EventArgs e) tcpClient = new TcpClient(); DownLoadthread = new Thread(new ThreadStart(ClientThreadF); /下载线程DownLoadthread.Start();(4)为Form1类定义ClientThreadF如下,负责接收文件。public void ClientThreadF()/字节数组保存接收的数据 byte bytes=new ytetcpC

67、lient.ReceiveBufferSize; FileStream fs = null; List data = new List(); int n = 0; try /localhost表示程序所在计算机的服务器,这样设 /置服务器和客户机在同一台计算机,连接成功之前 /被阻塞,成功后执行后续语句。 /1300为时间服务器端口号 aTcpClient.Connect(localhost,1300) netStream=aTcpClient.GetStream() if (netStream.CanRead) /判断数据是否可读 do/不知文件大小,多次读入数据,直到读完所有数据 n =

68、netStream.Read(bytes, 0, (int)tcpClient.ReceiveBufferSize); if (n != 0) /如果读入的字节数不为0 data.AddRange(bytes);/保存这次读入的数据 while (netStream.DataAvailable); /是否还有数据 fs = new FileStream(d:/g2.txt, FileMode.Create); byte bytes1 = data.ToArray();/转换为数组 fs.Write(bytes1, 0, bytes1.Length);/写入文件 catch MessageBox

69、.Show(下载失败, 提示, MessageBoxButtons.OK); finally if (netStream != null) netStream.Close(); if (fs != null) fs.Close(); tcpClient.Close(); (5)在关闭程序之前,必须撤销线程对象。为主窗体的Closing事件增加事件处理函数如下:private void Form1_FormClosing(object sender, FormClosingEventArgs e) if(netStream!=null) netStream.Close();if(tcpClien

70、t!=null)tcpClient.Close(); if (DownLoadthread!=null&ownLoadthread.IsAlive) thread.Abort();(6)编译运行,运行例10.7的文件下载服务器。在D盘根目录下使用记事本创建g1.txt文件。单击例10.8程序的按钮,可以看到盘根目录中下载的g2.txt文件。10.4.6 异步TCP编程 例10.7实现了一个文件下载服务器,允许多个客户机同时下载文件。实现方法是每当一个客户和文件下载服务器建立连接后,文件下载服务器将建立一个新线程,在这个线程中将文件传送给客户机,而文件下载服务器继续侦听新的客户机的连接,数据传送

71、完成后创建的线程撤销。用这种方法建立的多个线程是相互独立的,线程反复建立撤销,占用较多资源。 使用NetworkStream类的传送数据方法BeginWrite也可以实现类似的功能,该方法在线程池中申请一个线程,在线程中使用建立的连接开始向客户机传送数据后,立刻退出该方法继续后续语句,使文件下载服务器能继续侦听新客户机的连接。传送数据完成后,将自动调用一个指定方法,完成传送数据后的善后工作。采用方法BeginWrite的优点是传送数据的线程在线程池中,由系统统一管理,安全可靠,线程池中的线程是预先定义的,不必反复建立和撤销,节约资源。一般把这种方式称为异步方式,前面的那种方式称为同步方式。具体

72、使用方法见下例。【例10.9】本例建立文件下载服务器,使用异步TCP发送数据,请读者和例10.7相比较,查看两个程序的异同。具体步骤如下:(1)建立一个新的Windows应用项目 。在Form1.cs头部增加命名空间引用: using System.Net; using System.Net.Sockets; using System.Threading;using System.IO;(2)为Form1类增加变量:private Thread Listenerthread; /变量引用侦听线程bool ifStop = true; /是否停止下载服务器TcpListener server;/

73、负责侦听是否有客户机访问服务器TcpClient socket; /服务器端和客户机连接的对象public struct DownLoadthreadObject public TcpClient tcpClient; public NetworkStream networkStream;List downLoadthreadObjectS;(3)修改构造函数如下: public Form1() InitializeComponent(); Listenerthread = new Thread(new ThreadStart(ListenerthreadMethod); Listenerth

74、read.Start();/线程启动 ifStop = false;/表示是否退出线程,false不退出线程 Text= 异步文件下载服务器;downLoadthreadObjectS= new List();(4)为Form1类定义一个侦听线程执行的方法如下:public void ListenerthreadMethod try server = new TcpListener(1300); server.Start();/开始侦听是否有客户机连接服务器 catch MessageBox.Show(不能建立服务器, 提示, MessageBoxButtons.OK); Return; by

75、te data=null; FileStream fs=null; NetworkStream netStream=null; while (!ifStop) try /未侦听到客户机前被阻塞,成功后执行后续语句 socket = server.AcceptTcpClient DownLoadthreadObject downLoadthreadObject =new DownLoadthreadObject(); netStream = socket.GetStream(); downLoadthreadOworkStream = netStream; downLoadthreadObjec

76、t.tcpClient = socket; lock (this) downLoadthreadObjectS.Add( downLoadthreadObject); fs = new FileStream(d:/g1.txt, FileMode.Open); data = new bytefs.Length; fs.Read(data, 0, (int)fs.Length); fs.Close(); catch if(fs!=null) fs.Close(); if (socket != null) socket.Close(); if (netStream != null) netStre

77、am.Close(); break; /这条是必须的,否则退出时可能有异常 if (socket != null) socket.Close(); if (server != null) /关闭TcpListener类对象取消侦听 server.Stop();BeginWrite方法使用线程池中线程,传送数组data数据,从data0号开始传送,传送长度为data.Length,AsyncCallback为委托类型,参见3.12节,传送所有数据后,调用ClientThreadF方法,完成传送数据后的善后工作。最后一个参数的数据将传递到ClientThreadF方法。该方法不会阻塞,线程池中线程

78、开始传送数据后,该方法立刻退出,程序继续监听(5)为Form1类定义传送所有数据后调用的方法。public void ClientThreadF(IAsyncResult ar) DownLoadthreadObject downLoadthreadObject =(DownLoadthreadObject)ar.AsyncState; TcpClient tcpClient= downLoadthreadObject.tcpClient; NetworkStream netStream= downLoadthreadOworkStream; netStream.EndWrite(ar); /

79、结束线程 tcpClient.Close(); lock (this) downLoadthreadObjectS.Remove( downLoadthreadObject); (6)在关闭程序之前,必须撤销线程对象。为主窗体的Closing事件增加事件处理函数如下:private void Form1_FormClosing(object sender, FormClosingEventArgs e) ifStop = true; foreach (DownLoadthreadObject DLO in downLoadthreadObjectS) DLO.networkStream.End

80、Write(null); if (DLO.networkStream != null) DLO.networkStream.Close(); if (DLO.tcpClient != null) DLO.tcpClient.Close(); if (server != null) server.Stop(); if (Listenerthread != null & Listenerthread.IsAlive) Listenerthread.Abort(); (7)编译运行本程序后,再次运行例10.7的文件下载服务器的客户机程序。在D盘根目录下使用记事本创建g1.txt文件。单击例10.7程

81、序的按钮,可以看到盘根目录中下载的g2.txt文件。System.Net.Sockets命名空间的所有阻塞函数都有异步方法,见下表,具体的使用方法请参见帮助文档。类提供的方法说明TcpListenerBeginAcceptTcpClient开始一个异步操作来接受一个传入的连接尝试BeginAcceptSocket开始一个异步操作来接受一个传入的连接尝试TcpClientBeginConnect开始一个对远程主机连接的异步请求EndConnect异步接受传入的连接尝试SocketBeginReceive开始从连接的 Socket 中异步接收数据EndReceive结束挂起的异步读取BeginSe

82、nd将数据异步发送到连接的 SocketEndSend结束挂起的异步发送NetworkStreamBeginRead从 NetworkStream 开始异步读取BeginWrite开始向流异步写入10.4.7 基于TCP协议的P2P技术 P2P技术也叫对等互联或点对点技术,与TCP和UDP不同,P2P不是一种新的网络协议,而是利用现有网络协议完成网络数据或资源共享的一种技术,实现网路上任意一台计算机和其它计算机直接通信,P2P技术不区分谁是客户机,谁是服务器,所有的客户机都是对等的,既是客户机,又是服务器。实现P2P技术可以采用TCP协议,也可以采用UDP协议。由于HTTP协议是基于TCP协议

83、的,一般防火墙都允许访问Web网站,也就是说允许TCP协议数据通过。但有些防火墙不允许UCP数据通过。本节例10.10采用TCP协议实现了一个P2P客终端,后边章节将采用UDP协议实现了P2P终端。【例10.10】本例采用TCP协议实现多人聊天室。TCP协议多人聊天室至少有如下功能:可以通过IP地址和端口号向其他计算机发送信息,一般从好友列表中选择其他计算机的IP地址和端口号。实现方法是使用TcpClient类对象和其他计算机程序的TcpListener类对象建立连接,并传送信息。这个功能实现方法和例10.6和例10.8类似。本例发送格式为: 本机IP地址:本程序侦听程序所使用的端口号说:发送

84、的信息 例如:127.0.0.1:1500说:我上线了。随时侦听是否有其他计算机发来的信息,这可以使用TcpListener类对象用来等待其他计算机的连接请求,并接受信息。这个功能实现方法和例10.5和例10.7类似。因此一个P2P终端可以认为同时具有服务器和客户机功能的通信软件。当成功地接受到其他计算机P2P终端的信息,要将该P2P终端的IP地址和端口号增加到好友列表中。允许使用者手工增加好友P2P终端的IP地址和端口号。设计界面如图。具体的步骤如下:(1)建立一个新的Windows应用项目。在Form1.cs头部增加命名空间引用: using System.Net; using Syste

85、m.Net.Sockets;using System.Threading;using System.IO;(2)首先得到侦听类TcpListener对象使用的IP地址和端口号,可能有多个程序运行,要占用一些端口号,必须试验找到一个可以使用的端口号。为Form1类增加变量:/用于发送信息的TcpClient类对象TcpClient tcpClient; /本程序侦听准备使用的端口号,为静态变量static int MyPort=1499; /本程序侦听使用的IP地址IPAddress myIPAddress = null; /接受信息的侦听类对象,检查是否有信息TcpListener serve

86、r = null; (3)放置1个Label控件,属性Name=label4,用来显示侦听IP地址和端口号。(4)为Form1窗体的Load事件增加事件函数Form1_Load如下:private void Form1_Load(object sender, EventArgs e) Text= P2P终端;/下句得到IP地址myIPAddress= (IPAddress)Dns.GetHostAddresses (Dns.GetHostName().GetValue(0);MyPort+;for(int i=0;i51;i+)/检查端口号是否被使用 try server= new TcpLi

87、stener(myIPAddress, MyPort); server.Start(); label1.Text= 本机IP地址和端口号:+ myIPAddress.ToString() + : + MyPort.ToString(); break; catch MyPort+;/已被使用,端口号加1 if(i=50)/试验50次,可能计算机网络有问题 MessageBox.Show( 不能建立服务器,可能计算机网络有问题); this.Close();/退出 server.Stop();(5)编译运行,将在label1处显示本程序使用的IP地址和端口号。请注意,显示的实际上是侦听类TcpLi

88、stener对象使用的IP地址和端口号,其他计算机必须向这个IP地址和端口号发送数据,本程序才能接受到发送来的数据。(6)首先实现第一个功能,参考例10.6。在窗体Form1中放置1个Label控件,其Text属性为“好友列表”。在Label控件下侧放置ListView控件到窗体,属性Name为FriendListView,属性View=Details,MultiSelect为true,允许多选,即可把一条消息同时发给多个好友。单击属性Column右侧标题为“”的按钮,在弹出的“ColumnHeader集合编辑器”对话框中添加2个列头,属性Name分别为:FriendIP、FriendPort

89、,属性Text分别为:好友IP、好友端口号。参见上图。(7)在窗体Form1中放置1个Label控件,其Text属性为发送内容。在Label控件下侧放置TextBox控件到窗体,属性Name为SendMessageTextBox,其Text属性为空。(8)在窗体Form1中放置1个Button控件,属性Name为SendMessageButton,标题为“发送信息”,按钮的单击事件函数如下:private void SendMessageButton_Click(object sender, EventArgs e) if (SendMessageTextBox.Text = ) Messag

90、eBox.Show(this, 发送信息不能为空, 提示, MessageBoxButtons.OK); return; ListView.SelectedListViewItemCollection I =FriendListView.SelectedItems; if (I.Count = 0) MessageBox.Show(this, 请选择好友IP和端口号, 提示, MessageBoxButtons.OK); return; tcpClient = new TcpClient(); tcpClient.ReceiveTimeout = 10000; NetworkStream ne

91、tStream=null; string ip,stringPort; int port;for (int i=0; i I.Count; i+)/给选定所有好友发信息 ip = Ii.SubItems0.Text;/IP地址字符串 stringPort = Ii.SubItems1.Text;/端口号字符串 port = Convert.ToInt32(stringPort);/转换为端口号 try tcpClient.Connect(ip, port);/连接其他计算机 netStream = tcpClient.GetStream(); if (netStream.CanWrite) s

92、tring s1 = myIPAddress.ToString()+:+ MyPort.ToString()+“ 说:+SendMessageTextBox.Text; Byte sendBytes = Encoding.UTF8.GetBytes(s1); netStream.Write(sendBytes, 0, sendBytes.Length); catch string s=ip+:+stringPort+发送失败, 可能好友不在线或自己输入的IP和端口号有误; MessageBox.Show(this, s, 提示, MessageBoxButtons.OK); break; fi

93、nally if (netStream != null) netStream.Close(); tcpClient.Close();(9)然后实现第二、三个功能,参考例10.5和例10.7。在窗体Form1中放置Label控件,其Text属性为“收到的信息”。在Label控件下侧放置ListBox控件到窗体,属性Name为FriendListBox。参见上图。(10)为Form1类增加变量:private Thread thread; /接受信息的侦听线程类变量bool ifStop = true; /是否停止接受信息的侦听 /和发送方连接的TcpClient类对象 TcpClient soc

94、ket; /dFun类可代表无返回值有一个string参数方法delegate void dFun(string text); dFun dFun1; /代表增加FriendListBox内容的方法 delegate void dFun2(ListViewItem lvi); dFun2 dFun3;(11)为Form1类定义一个修改FriendListBox的方法如下:private void SetFriendListBox(string text) FriendListBox.Items.Add(text); (12)为Form1类定义修改FriendListView的方法如下:priv

95、ate void SetFriendListView(ListViewItem lvi) FriendListView.Items.Add(lvi); (13)在Form1窗体的Load事件函数Form1_Load函数最后增加语句如下:ifStop=False /表示是否退出线程,false不退出线程/令dFun1代表修改的方法SetFriendListBoxdFun1=new dFun(SetFriendListBox);/令dFun3代表修改的方法SetFriendListViewdFun3 = new dFun2(SetFriendListView);thread = new Threa

96、d(new ThreadStart(ReadThread);thread.Start();(14)为Form1类定义一个线程方法如下:public void ReadThread()/线程执行的方法 server.Start(); while (!ifStop) try socket = server.AcceptTcpClient(); NetworkStream netStream = socket.GetStream(); StreamReader sr = new StreamReader(netStream); string receiveMessage = sr.ReadLine(

97、); FriendListBox.Invoke(dFun1, new object receiveMessage ); int i1 = receiveMessage.IndexOf(:); int i2 = receiveMessage.IndexOf(:, i1 + 1); string ipString = receiveMessage.Substring(0, i1); string portString = receiveMessage.Substring( i1 + 1, i2 - i1 - 2); ListViewItem item1 = FriendListView.FindI

98、temWithText(ipString); ListViewItem item2 = FriendListView.FindItemWithText(portString); if (item1=null & item2=null)|(item1!=item2) ListViewItem lvi; string s = new String ipString, portString ; lvi = new ListViewItem(s); FriendListView.Invoke( dFun3, new object lvi ); socket.Close(); catch if (soc

99、ket != null) socket.Close(); if (socket != null)/运行到此,线程将结束, socket.Close();/关闭关闭和客户机的连接 if (server != null)/关闭TcpListener类对象取消侦听 server.Stop();(15) 最后实现第四个功能。在窗体Form1中放置1个Label控件,其Text属性为“增加的好友IP”。在Label控件右侧放置TextBox控件到窗体,属性Name为addFriendIPTextBox。在窗体Form1中放置1个Label控件,其Text属性为“增加的好友端口号”。在Label控件右侧放

100、置TextBox控件到窗体,属性Name为addFriendPortTextBox。在窗体Form1中放置1个Button控件,属性Name为AddFriendButton,标题为“增加好友IP和端口号到好友列表”,参见上图,按钮的单击事件函数如下:private void AddFriendButton_Click(object sender, EventArgs e) IPAddress myFriendIpAddress; if(IPAddress.TryParse(addFriendIPTextBox.Text, out myFriendIpAddress) = false) Mess

101、ageBox.Show(IP地址格式不正确!); return; int myFriendPort; if(int.TryParse(addFriendPortTextBox.Text, out myFriendPort) = false) MessageBox.Show(端口号格式不正确!); return; else if (myFriendPort 65535) MessageBox.Show(端口号范围不正确!,必须在1000-65535之间); return; ListViewItem item1=FriendListView.FindItemWithText(addFriendIP

102、TextBox.Text); ListViewItem item2=FriendListView.FindItemWithText (addFriendPortTextBox.Text); if (item1!=null&item2!=null&(item1= item2) MessageBox.Show(该好友已经在列表中); else ListViewItem lvi;/不在好友列表,增加到好友列表 string s = new String ddFriendIPTextBox.Text, addFriendPortTextBox.Text ; lvi = new ListViewItem

103、(s); FriendListView.Items.Add(lvi); (16)在关闭程序之前,必须撤销线程对象。为主窗体的Closing事件增加事件处理函数如下:private void Form1_FormClosing(object sender, FormClosingEventArgs e) ifStop = true; if (socket != null) socket.Close(); if (server != null) server.Stop(); if (thread != null & thread.IsAlive) thread.Abort(); (17)编译后,双

104、击“Form1.cs文件所在文件夹binDebug可执行文件”两次,生成两个P2P终端。在一个P2P终端好友列表中添加对方为好友,键入发送内容,在好友列表中选中发送好友,单击“发送信息”按钮,在对方“收到的信息”列表中,可以看到发送的信息,同时对方好友列表中自动添加发送信息P2P终端的IP和端口号。运行效果如上图。即时通信系统,例如QQ、MSN等,采用的架构基本相同。这些系统都有登录注册服务器,需要即时通信的客户机首先登录(需要先注册),将自己侦听IP地址和端口号传到登录注册服务器,登录注册服务器将已经登录的该客户机好友侦听IP地址和端口号传回来,同时登录注册服务器还将这个登录的客户机的侦听I

105、P地址和端口号传送给该客户机的所有在线好友。有了这些信息,一个客户机就可以使用P2P技术和在线的所有好友直接通信,而不必通过登录注册服务器。网络游戏软件、BT下载软件、Internet视频直播软件等都有类似的结构,他们工作过程和即时通信系统工作过程基本类似。由于Internet中各个客户机IP的不确定性,例如广泛使用的ADSL连接Internet的客户机的IP地址是动态分配的,一个客户机可能无法预先知道另一台客户机的IP地址和端口号,因此在这些系统中,登录注册服务器是必须的。登录注册服务器的设计方法和本节的例子中的服务器设计方法基本相同,只是要包括一个数据库记录所有注册客户机的信息,包括本人信

106、息、好友信息以及所有客户在线信息,另外登录注册服务器要更加安全、可靠,要允许更多的客户机同时登录。本节所设计的P2P终端可以作为即时通信系统中的P2P终端,只是第四个功能有所不同,P2P终端首先要登录注册服务器,将自己侦听IP地址和端口号传到登录注册服务器,登录注册服务器将已经登录的该客户机好友侦听IP地址和端口号传回来,添加到好友列表中。同时登录注册服务器还将这个登录的客户机的侦听IP地址和端口号传送给该客户机的所有在线好友。10.5 基于UDP协议Socket编程本节详细介绍编写基于基于UDP协议的Socket程序方法和步骤。10.5.1 基于UDP协议编程 UDP是一个简单的、面向数据报

107、的无连接协议,提供了快速但不一定可靠的传输服务。UDP与TCP的比较,其优缺点如下:优点:无连接(速度快)、可用于广播(组播)、消耗网络带宽小、有消息边界。缺点:不可靠、安全性差、不保证报文顺序交付。 编写UDP应用程序时,可以直接使用Socket类,也可以使用UdpClient类。其中 UdpClient类对基础Socket进行了封装,发送和接收数据时不必考虑底层套接字收发时必须处理的一些细节问题,从而简化了UDP应用编程的难度,提高了编程效率。本书只使用UdpClient类编写应用程序。UdpClient类常用的构造函数如下:public UdpClient():创建一个新的UdpClie

108、nt对象,并自动分配合适的本地IP地址和端口号。例子如下:UdpClient udpClient = new UdpClient(); /连接指定IP和端口号的远程主机udpClient.Connect(, 51666);Byte sendBytes = System.Text.Encoding.Unicode.GetBytes(你好!);/发送给Connect方法指定的远程主机udpClient.Send(sendBytes, sendBytes.Length);public UdpClient(int port):创建一个与指定的端口绑定的新的UdpClient实例,并自动分配合适的本地I

109、P地址。例子如下:UdpClient udpClient = new UdpClient(51666);public public UdpClient(IPEndPoint localEp):创建一个新的UdpClient实例,该实例与包含本地IP地址和端口号的IPEndPoint实例绑定。例子如下:IPAddress address = IPAddress.Parse(127.0.0.1);IPEndPoint iep = new IPEndPoint(address, 51666);UdpClient udpClient =new UdpClient(iep);public UdpClie

110、nt(string remoteHost,int port):创建一个新的UdpClient实例,自动分配合适的本地IP地址和端口号,并连接指定IP和端口号的远程主机。使用这种构造函数,不必再调用Connect方法。例子如下:UdpClient udpClient =new UdpClient(,8080); 发送数据使用Send方法,有多个重载Send方法,具体使用哪种方式取决于UdpClient是如何连接到远程主机的,以及UdpClient实例是如何创建的。Send方法定义如下:public int Send(byte data, int length, string remoteHost

111、Name, int port):这种重载形式用于知道了远程主机IP地址和端口的情况下,它有三个参数:记录数据的字节数组、数组长度、记录远程主机IP地址和端口号的IPEndPoint对象。返回值为已发送的字节数。public int Send(byte data, int length, string remoteHostName, int port):这种重载形式用于知道了远程主机名和端口号的情况下,利用Send方法直接把UDP数据报发送到远程主机。public int Send(byte data, int length):这种重载形式假定UDP客户端已经通过Connect方法指定了默认的远

112、程主机,因此,只要用Send方法指定发送的数据和数据长度即可。 接收远程主机发来的数据使用Receive方法,在从远程主机接受到数据报之前,该方法将一直阻塞。如果接受数据可用,则 Receive 方法将读取入队的第一个数据报,并将数据部分作为字节数组返回。该方法将发送数据的远程主机IPAddress 和端口号填充到IPEndPoint类型参数中。该方法能够接受任何远程主机发来的数据。如果在 Connect 方法中指定了默认远程主机,则 Receive 方法将只接受来自该主机的数据报,其他所有数据报将被丢弃。接收数据一般要在另一个线程中进行。 使用UdpClient对象的Receive方法的优点

113、是当本机接收的数据报容量超过分配给它的缓冲区大小时,该方法能够自动调整缓冲区大小。而使用Socket对象遇到这种情况时,将会产生SocketException异常。例子如下。下句接受任何远程主机发来的数据,远程主机的 IP地址和端口号赋值给RemoteIpEndPoint。IPEndPoint RemoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0);/接受任何远程主机发来的数据,远程主机的 IP地址/和端口号赋值给RemoteIpEndPointByte receiveBytes = udpClient.Receive(ref RemoteIpE

114、ndPoint);string receiveData= System.Text.Encoding.Unicode.GetString(receiveBytes);【例例10.11】本例使用UdpClient实现多人聊天室,界面和使用方法和例10.10完全相同,只是采用UdpClient类实现。本例修改例10.10。修改具体步骤如下:(1)在Form1类定义了一些变量(例10.10第2步),修改这些变量中的一些变量: /替换TcpClient tcpClient;用于发送 UdpClient myUdpClient; private Thread thread; /线程类变量 bool ifS

115、top = true; /是否停止时间服务器 /TcpListener server;/删除这个变量 /替换TcpClient socket;用于接受UdpClient udpClient;(2)修改例10.10的Load事件函数Form1_Load如下:private void Form1_Load(object sender, EventArgs e) Text= P2P终端;/下句得到IP地址myIPAddress= (IPAddress)Dns.GetHostAddresses (Dns.GetHostName().GetValue(0);MyPort+; IPEndPoint iep

116、; for(int i=0;i51;i+)/检查端口号是否被使用 try iep = new IPEndPoint(myIPAddress, MyPort); udpClient = new UdpClient(iep); label1.Text= 本机IP地址和端口号:+ myIPAddress.ToString() + : + MyPort.ToString(); break; catch MyPort+;/已被使用,端口号加1 if(i=50)/试验50次,可能计算机网络有问题 MessageBox.Show( 不能建立服务器,可能计算机网络有问题); this.Close();/退出

117、server.Stop();(3)修改标题为“发送信息”按钮的单击事件函数如下:private void SendMessageButton_Click(object sender, EventArgs e) if (SendMessageTextBox.Text = ) MessageBox.Show(this, 发送信息不能为空, 提示, MessageBoxButtons.OK); return; ListView.SelectedListViewItemCollection I =FriendListView.SelectedItems; if (I.Count = 0) Messag

118、eBox.Show(this, 请选择好友IP和端口号, 提示, MessageBoxButtons.OK); return; myUdpClient = new UdpClient(); string ip,stringPort; int port; for (int i=0; i I.Count; i+)/给选定所有好友发信息 ip = Ii.SubItems0.Text;/IP地址字符串 IPAddress remoteIP = IPAddress.Parse(ip); stringPort = Ii.SubItems1.Text; port = Convert.ToInt32(stri

119、ngPort); IPEndPoint iep=new IPEndPoint(remoteIP, port); string s1=myIPAddress.ToString()+:+ MyPort.ToString()+ 说:+ SendMessageTextBox.Text; Byte sendBytes = Encoding.UTF8.GetBytes(s1); try myUdpClient.Send (sendBytes,endBytes.Length,iep); catch string s = ip + : + stringPort + 发送失败; MessageBox.Show(

120、s); myUdpClient.Close();(4)实现接受数据。修改Form1类ReadThread方法如下:public void ReadThread()/线程执行的方法 IPEndPoint remote = null; while (!ifStop) /如退出while语句,线程结束 try byte bytes = udpClient.Receive(ref remote); string receiveMessage=Encoding.UTF8. GetString(bytes,0,bytes.Length); FriendListBox.Invoke(dFun1, new o

121、bject receiveMessage ); int i1 = receiveMessage.IndexOf(:); int i2 = receiveMessage.IndexOf(:, i1 + 1); string ipString=receiveMessage.Substring(0, i1); string portString=receiveMessage.Substring (i1 + 1, i2 - i1 - 2); ListViewItem item1 = FriendListView.FindItemWithText(ipString); ListViewItem item

122、2 = FriendListView.FindItemWithText(portString); if(item1=null&item2=null)|(item1!= item2) ListViewItem lvi; string s = new String ipString, portString ; lvi = new ListViewItem(s); FriendListView.Invoke(dFun3, new object lvi ); catch/不处理异常,退出线程方法 if (udpClient != null)/运行到此,线程将结束, udpClient.Close();

123、/关闭关闭和客户机的连接(5)修改Closing事件事件处理函数如下: private void Form1_FormClosing(object sender, FormClosingEventArgs e) ifStop = true; if (myUdpClient!= null) myUdpClient.Close(); if (udpClient != null) udpClient. Close(); if (thread != null & thread.IsAlive) thread.Abort();(6)编译后,双击“Form1.cs文件所在文件夹binDebug可执行文件”

124、两次,生成两个P2P终端。在一个P2P终端好友列表中添加对方为好友,键入发送内容,在好友列表中选中发送好友,单击“发送信息”按钮,在对方“收到的信息”列表中,可以看到发送的信息,同时对方好友列表中自动添加发送信息P2P终端的IP和端口号。运行效果如例10.10图。10.5.2 用UDP实现广播和组播UDP协议的另外一个重要用途是可以通过广播和组播实现一对多的通信模式,即可以把数据发送到多个远程主机,从而完成发送网络会议通知、广告、网络信息公告等群发功能。通过Internet实现群发功能的形式有两种,一种是利用广播向子网中的所有客户发送消息,比如各类通知、单位公告、集体活动日程安排等;另外一种是

125、利用组播向Internet网上特定的子网发送消息,比如集团向其所属的公司或用户子网发布信息公告等。利用广播实现群发功能: 所谓广播,就是指同时向子网中的多台计算机发送消息,并且所有子网中的计算机都可以接收到发送方发来的消息。每个广播消息包含一个特殊的IP地址。广播消息地址分为两种类型:本地广播模式和全球广播模式。使用本地广播模式可向子网中所有计算机发送广播消息,其他网络不会受到本地广播的影响。一个IP地址分为两部分,网络地址和主机地址,主机地址全部为1的IP地址是本地广播地址。例如,本地区域网采用C类地址,192.168.100.X,则192.168.100.255是广播地址。全球广播地址为:

126、255.255.255.255。由于路由器会过滤掉这个地址,广域网中并不能使用这个IP地址将信息发送到广域网上所有计算机。 使用本地广播地址发送数据的代码如下:UdpClient myUdpClient = new UdpClient(); /IPAddress.Broadcast是子网中的IP广播地址,/参数1也可为实际广播地址,参数2为端口号IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, 8001);/允许发送和接收广播数据myUdpClient.EnableBroadcast=trueByte sendBytes = /s1是要发

127、送的字符串 Encoding.UTF8.GetBytes(s1)myUdpClient.Send(sendBytes,sendBytes.Length,iep); 请读者修改例10.10,使该程序能够向子网中的所有计算机发送广播消息。可以增加一个RadiioButton控件,控制程序是否允许广播,修改Button控件的单击事件函数,用语句判断RadiioButton控件状态,如果允许广播,使用上边语句发送信息。其他代码不用修改。利用组播实现群发功能: 广播通信模式虽然能够实现一对多的通信需要,但是广播是向子网中的所有计算机用户发送消息,没有目的性,这不但增加了网络传输负担,而且资源消耗较高。组

128、播较好地解决了这个问题。组播也叫多路广播。所谓组播是将消息从一台计算机发送到本网或全网内指定的计算机子集上,即发送到那些加入指定组播组的计算机上。组播组是开放的,每台计算机都可以通过程序随时加入到组播组中,也可以随时离开。 组播组是分享一个组播地址的一组设备。与IP广播类似,IP组播使用特殊的IP地址范围来表示不同的组播组。组播地址是范围在224.0.0.0到239.255.255.255的D类IP地址。任何发送到组播地址的消息都会被发送到组内的所有成员设备上。组可以是永久的,也可以是临时的。大多数组播组是临时的,仅在有成员的时候才存在。用户创建一个新的组播组时只需从地址范围内选出一个地址,然

129、后为这个地址构造一个对象,就可以开始发送消息了。使用组播时,应注意的是TTL(生存周期Time To Live)值的设置。TTL值是允许路由器转发的最大数目,当达到这个最大值时,数据包就会被丢弃。如果使用默认值(默认值为1),则只能在子网中发送。可以通过UdpClient对象的Ttl属性直接设置TTL值,例如:UdpClient myUdpClient = new UdpClient(); /设置TTL值为50,即最多允许50次路由器转发。myUdpClient.Ttl=50在UdpClient类中,使用JoinMulticastGroup方法将UdpClient对象和TTL一起加入组播组,使

130、DropMulticastGroup退出组播组。例如:UdpClient udpClient=new UdpClient(8001);udpClient.JoinMulticastGroup(IPAddress.Parse(224.100.0.1)或者( 其中50为TTL值 ):UdpClient udpClient=new UdpClient(8001);udpClient.JoinMulticastGroup(IPAddress.Parse(“224.100.0.1”), 50);然后,可以接受来自224.100.0.1组的信息。IPEndPoint iep=null;byte bytes

131、 = udpClient.Receive(ref iep);为了实现组播,在发送数据时,使用如下代码进行设置:UdpClient myUdpClient = new UdpClient();IPEndPoint iep = new 224.0.0.1是组播地址 IPEndPoint(IPAddress.Parse(224.0.0.1),8001)/允许发送和接收广播数据myUdpClient.EnableBroadcast = trueByte sendBytes= /s1是要发送的字符串Encoding.UTF8.GetBytes(s1)myUdpClient.Send(sendBytes,sendBytes.Length,iep)为了实现组播,在接受数据时,使用如下代码进行设置:UdpClient udpClient = new UdpClient();udpClient.JoinMulticastGroup(IPAddress.Parse(224.0.0.1);udpClient.Ttl = 50;IPEndPoint remote = null;byte bytes = udpClient.Receive(ref remote);请读者修改例10.10,使该程序能够向子网中的所有计算机发送组播消息。

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 办公文档 > 工作计划

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号