用多线程枝术实现Winsock编程

                                                                                                 作者:王广伟 李维钊

一、Windsock简介

  Winsock(Windows Sockets)是微软的窗口系统结构(WOSA)的一部分。它是基于UNIX上的 Ber Keley Software Distribution(BSD)版本的套接字,并为windows进行了专门的扩展。
  Internet是在UNIX 系统上发展起来的,在UNIX上有许多成熟的编程接口,其中最通用的是 一 种叫做Sockets(套接字)的接口。套接字的实质是通信端点的一种抽象,它提供一种发送和 接 收数据的机制。在1991年前后,许多网络软件商都在加紧研制Windows下的TCP/IP通迅组件 ,为了能使这些组件有一定的标准,降低开发难度,他们决定为Windows开发一套标准的 、通 用的TCP/IP编程接口,并使之类似于UNIX下的Sockets,这一接口迅速被所有的软件商所接 受,包括Microsoft与IBM。到1994年,它被正式制定成一项标准,称为Windows Sockets或 称WINSOCK,并通过C语言的动态连接库方式提供给用户及软件开发者。我们现在看到的Wind ows下的Internet软件都是在WINSOCK基础下开发的。随着Windows 95推出,WINSOCK 已经被 正式集成到了Windows系统中,同时包括了16位与32位的编程接口。
  WINSOCK的实现一般都由两部分组成:开发组件和运行组件。开发组件是供程序员开发WINSO CK应用程序使用的,它包括介绍WINSOCK实现的文档、WINSOCK应用程序接口(API)引入库和 一些头文件。运行组件是WINSOCK应用程序接口的动态连接库(DLL),文件名为 WINSOCK.DLL 应用程序在执行时通过装入它来实现网络通信功能。
  最初,WINSOCK1.1版是专门为Internet设计的,现在的2.x版已经不再限于Internet和TCP/ I P协议,它通过提供扩展的SPI编程接口,把自己的应用范围扩大到现存的和正在出现的各种 网络和协议,包括PSTN、ISDN、无线网、所有的局域网协议、异步传输模式ATM等等,并且 允许应用程序对所建立连接的可靠性、冗余度和带宽进行控制。所以掌握了WINSOCK编程, 就等于掌握了Windows环境下网络编程的一把钥匙。
  Windows开网络编程的规范-Windows Sockets。这套规范是windows下得到广泛应用的、开 放的、支持多种协议的网络编程接口。
  Windows Sockets规范定义并记录了如何使用API与Internet协议族(IPS,通常我们指的是TC P/IP)连接,尤其要指出的是所有的Windows Sockets实现都支持流套接口和数据报套接口。
  应用程序调用Windows Sockets的API实现相互之间的通讯。 Windows Sockets又利用下层的 网络通讯协议功能和操作系统调用实现实际的通讯工作。它们之间的关系如图1。

t3501.gif (4638 bytes)

图1 应用程序与Windows Sockets关系图

二、套接字的两种形式

  套接字是通信端点的一种抽象,它提供一种发送和接收数据的机制,在Windows套接字中 [1]。 它有两种形式:数据报套接字(Datagram Socket)和流式套接字(Stream Socket).流式套接 口采用的是传输控制协议TCP,它提供了双向的,有序的,无重复并且无记录过界的数据流 服务,在这种方式下,两个通讯的应用程序之间先要建立一种虚拟的连接,流方式的特点的 是 :通讯可靠,对数据有校验和重发的机制,通常用来作数据文件的传输如ftp、telnet等, 适合于大量数据的传输。
  数据报套接口采用的是用户数据报协议UDP,它建立在IP协议上,提供无连接数据报传输, 支 持双向的数据流,但并不保证是可靠,有序,无重复和。也就是说,一个从数据报套接口接 收信息的进程有可能发现信息重复了,或者和发出时的顺序不同。数据报套接口的一个重要 特点是它保留的记录边界,对于这一特点,数据报套接口采用了与现在许多包交换网络(例 如以太网)非常类似的模型,数据报文方式由于取消了重发校验机制,能够达到较高的通讯 速率,可以用作一些对数据可靠性要求不高的通讯,如实时的语音、图像转送、广播消息等 。
  是使用流式套接字还是使用数据报套接字,以通信效率影响较大。在编程中,流式套接字与 数据报套接字是有区别的。在流式套接字中,服务器首先启动,通过调用socket()建立一个 套接口。然后调用bind()将该套接口和本地网络地址联系在一起,再调用listen()使套接口 做好侦听的准备,并规定它的请求队列的长度,之后就调用accept()来接收连接,客户在建 立套接口后就可调用connect()和服务器建立连接,连接一旦建立,客户机和服务器之间就 可以通过调用read()和write()来发送和接收数据。最后,待数据传送结束后,双方调用clo se()关闭套接口。如图3所示。

t3602.gif (6751 bytes)

图2 无连接套接口应用程序时序图

  与流式套接字不同的是,在数据报套接字中,服务器不调用accept(),客户机不调用connec t()。在发送数据之前,客户机和服务器之间尚未建立完整相关,无连接服务器通过socket( ) 和bind()建立了本地半相关。在传输数据之前,无连接的两个端点已建立起来,分别以一个 本地Socket号和信宿端 Socket地址。于是,一个完整的相关在数据收发过程中动态地建立 起来,实现无连接客户和服务器彼此识别。如图2所示。

t3601.gif (5437 bytes)

图3 面向喧哗接套接口应用程序时序图

三、通信的选择方式

  在进行网络开发时,必须对是使用阻塞方式(blocking mode)还是非阻塞方式(non-blocking mode)进行选择。
  在网络通讯中,由于网络拥挤或一次发送的数据量过大等原因,经常会发生交换的数据在 短 时间内不能传送完,收发数据的函数因此不能返回,这种现象叫作阻塞。WINSOCK对有可能 阻塞的函数提供了两种处理方式;阻塞和非阻塞方式。
  在阻塞方式(也称同步方式)下,收发数据的函数在被调用后一直要到传送完毕或者出错才能 返回。由于操作可能需要任意长的时间才能完成。于是问题就出现了。最明显的一个例子就 是recv(),这个函数会一直处于阻塞状态直到收到对方系统发送的数据。
  对于非阻塞方式,函数被调用后立即返回,当传送完成后WINSOCK给程序发一个事先约定好 的消息。
  在Berkeley套接口模型中,一个套接口的操作的缺省行为是阻塞方式的,除非程序员 显式地请求该操作为非阻塞方式。微软强烈推荐程序员在尽可能的情况下使用非阻塞方式( 异步方式)的操作。因此非阻塞方式的操作能够更好地在非占先的Windows环境下工作,程序 员应该在绝对必要的时候才采用阻塞方式。
  如果一个正在运行某一阻塞操作的进程收到一个Windows消息,那么应用程序有可能试图发 出另一个Windows Sockets调用,由于很难安全地处理这种情形,Windows Sockets规范不支 持这种应用程序的工作方式。在这种情况下,有两个函数可以帮助程序员。WSAIsBlocking( )可以用来确定在该进程上是否有阻塞的Windows Sockets 调用。WSACancelBlookingCall() 可以用来取消在线的阻塞调用,如果有的话。任何其他的Windows Sockets函数如果在这种 情况下被调用,则会失败并返回错误代码WSAEINPROGRESS。要强调的是,这一限制适用于所 有阻塞和非阻塞的操作。为了避免阻塞操作的缺点,我们可以使用多线程编程来实现。
  WINSOCK通过异步选择函数WSAAsyncSelect()来实现非阻塞通讯。方法是由该函数指定某种 网络事件(如:有数据到达,可以发送数据,有程序请求连接等)当指定的网络事件发生时, 由WINSOCK对程序发送由程序事先约定的消息。程序中就可以根据这些消息做相应的处理。 格式如下:
  int WSAAsyncSelect(SOCKET s,HWND hwnd,unsigned int wMsg,long lEvent)sockets在这 个函数调用中被自动设成非阻塞方式,hWnd是接收WINSOCK消息的窗口句柄,非阻塞方式下 ,wMsg是向窗口发出消息名称,用户可以任意定义。lEvent是被指定的网络事件,有下面几 种选择:
  FD-READ 希望在Socket收到数据时收到消息;
  FD-WRITE在可以发送数据时收到消息;
  FD-ACCEPT在有连接请求到达时收到消息;
  FD-CONNECT在连接成功时收到消息;
  FD-CLOSE在连结关闭时收到的消息。
  当被指定的事件发生时,程序将收到消息,消息的MSG结构体中,Message项就是被规定的消 息名称wMsg,lParam项中的内容就是与上面完全一样的网线络事件名称。有一点要注意的是 ,在非阻塞状态下,connect()函数的返回值都SOCKET-ERROR,表示有可能无法与远端的服 务器 建立连接。为了把这种情况与其他的错误区分开,可以调用一个专门的函数WSAGetLastErro r()来检查产生SOCKET-ERROR的原因,如该函数的返回值是WSAEWOULDBLOCK就表示上述情况 。这时,程序可以给用户一个提示,表明程序正试图与远方服务器建立连接,如果等待时 间过长,用户可以将其停止。

四、多线程技术简介

  当在网络中传输的数据是较大的数据块进(如图象),常常会出现阻塞现象。为了避免出现这 种情况,并且提高传输的效率,我们可以使用多线程来进行网络编程。
  32位Windows环境下的Win32 API提供了开发多线程应用程序的接口函数,并且VC++6.0提供 的标准C库也可以开发多线程应用程序,但是更为经常的是使用MFC类库进行多线程编程。MF C执行两种类型的线程,一种是工作线程(Worker Thread);另一种是用户界面线程(User-Int erface Thread)。这两种类型都使用同一Win32 API调用机制,而且Visual C++6.0的 MFC库 提供的CWinThread类是进行多线程编程的基础。实际上,CWinApp类就是一个用户界面线程 ,它由CWinThread类派生,用于应用程序与用户的交互。
  有两种方法可以创建线程。MFC提供如下全局函数创建线程:AfxBeginThread(),用AfxBeg i nThread()可以创建一个线程,MFC提供两个版本的AfxBeginThread()分别用于创工作线程和 用户界面线程。该函数创建一个线程对象,并返回对象的指针(利用这个指针可以完成一些 用户想做的事情)。另外,你也可以显式地创建线程对象,这需要首先声明线程对象,然后 调用CWinThread的成员函数CWinThread::Create Therad()函数创建线程,由于MFC区分两种 线程, 因此创建线程的参数也是不同的,也正是不同的参数区分了不同的线程,这将在下面结合So ckets编程做详细说明。

五、在VC6.0中用多线程开发套接字程序

  在实际编程中,我们一般采用面向对象技术,特别采用消息驱动机制实现多任务的Windows 编程思想,VC由于它的强大的功能而被广泛采用。
  1.用VC6.0开发套接字程序的基本步骤
  在VC++6.0中,我们可以用Windows Sockets API来编写网络程序,更为普遍的是,我们使用 MFC封装的CAsyncSocket和CSocket两个类来进行网络编程,它把与套接字有关的Windows消 息换为回调函数,CAsyncSocket类比CSocket更加面向低层,使用比较灵活,但它对编程 人员的要求也高,需要对网络了解的更多。CSocket是CAsyncSocket的导出类,通过MFC中的 CArchive类的对象提供了更高层次的抽象,它封装了Socket实现中的许多细节,并将Socke t与 Archive相结合,使用它与使用MFC中的文档串行化协议相类似,使用便利。一般我们使 用简便的CSocket编程,其步骤如下:
  (1)构造套接字对象。(2)使用该对象构造基本的套接字,对于CSocket客户端对象,使用缺 省参数Create;对于CSocket服务器对象,应指明一个端口号作为Greate的一个参数,用于监 听。(3)建立客户端CSocket,调用CAsyncSocket::Connect建立与服务器端的连接, 服务器 端套接字调用CAsyncSocket::Listen监听,并在收到客户端请求后调用CAsyncSocket::Acce pt。(4)构造CSocketFile对象,并使CSocket对象与之关联。(5)构造CArchive对象,用于接 收或 发送数据。(6)使用CArchive对象来进行客户端与服务器端的套接字通信。(7)删除CArchive ,C SocketFile,CSocket对象。流程图如图4所示:

t3801.gif (8281 bytes)   t3802.gif (6771 bytes)

图4 利用VC在客户端和服务器之间建立套接字通信的流程

  2.回调函数的使用
  为了使网络通信更加方便,CAsyncSocket和CSocket提供了一些回调函数。主窗口通过调用 这些回调函数来套接字的一些重要事件的来临。这些回调函数有OnReceive,OnSend,onConne ct,OnAccept,OnClose,它们可以通过在两个类中重载得到。这两个类仅仅是通过回调函数将 消息转化为通知,具体的如何响应这些通知,还须我们自己来实现。
  void CReceSocket::OnReceive(int nErrorCode)

  CAsyncSocket::OnReceive(nErrorCode);
  Receive(lpBuf,int nBufLen,int nFlage=0)

  如果自己的类继承CAsyncSocket,为了使通信更加便利,必须重载这些回调函数,如果自己 的类继承CSocket,将由你根据情况自己决定是否重载它们。必须注意的是,CSocket对象从 不调用OnSend OnConnect这两个通知函数,而只能调用Send函数来发送数据,直到发送完成 有数据Send对返回,同样只能调用Connect函数来进行连接,当完成连接时(成功或失败)Con nect将返回。
  3.利用多线程来开发网络通信
  CSocket类的缺省方式阻塞方式,为了避免阻塞的种种缺点,可以使用多线程,可以在一个 工作线程中处理数据的接收和发送,该工作线程可以在后台运行,套接字在工作线程中的阻 塞不会影响主线程中的其他活动,这样主线程可以处理主窗口的消息映射,下面结合一个实 例说明如何创建、执行一个套接字工作线程的过程,该线程创建一个套接字,然后与服务器 (IP为202.194.20.250)进行连接,连接成功后向服务器发送一些数据。
UINT SocketProc(LPVOID pParam)

  CSendView*pView=(CSendView*)pParam;
  CSocket*pSocket=new CSocket();
  pSocket->Create(); //构造CSocket对象
  //与服务器连接
  if(!pSocket->Connect("202.194.20.250"5050))
  AfxMessageBox("can'tconnect to the distines!")
  //构造CSocketFile对象,并使 CSocket对象与之关联。
  CSocketFile* m-pFile=new CSocketFile(pSocket);
  //构造CArchive对象,用于发送数据。
  CArchive* m-pArchiveOut=new CArchive(m-pFile,CArchive::STORE)
  if(m-pArchiveOut!=NULL)
  }
   //发送数据,其中m-sockdata是在视窗类中定义的一个结构,
   //pMsg是该结构中的一个指针变量
   pView->m-sockdata->pMsg->Serialize(*m-pArchiveOut)
   m-pArchiveout->Flush()
  }
  else
   return O;

  在程序适当的地方,用AfxBeginThread(SocketProc,pView)来执行即可。
  除了使用工作线程外,还可以使用用户界面线程来实现,用户界面线程增加了消息映射,在 下面的例子中将会用到。
  而CAsyncSocket类可以为了处理阻塞问题,不应该设为阻塞方式,而应使用异步方式。在异 步方式中,调用会立即返回,用GetLastError函数会获取相应的错误代码为WSAEWOULDBLOCK表示无连接可以接受,举例说明,在异步方式中,在调用Receive函数后,会得到WSAEWOULD BLOCK的错误信息,直到OnReceive回调函数被调用以通知我们可以再次接收数据了。
  4.使用多线程进行套接字编程时应注意同步问题
  在使用多线程技术进行网络编程时,必须注意套接字对象的同步问题,可以使用线程同步机 制来协调套接字对象的存取。
  如果在线程中创建了一个MFC窗口对象,这个MFC对象不能在其他线程中使用,即对于套接字 编程来说,一个套接字对象应该仅仅用于单个线程,在两个线程之间不能传递套接字对象。 例如;服务器一般可以接受多个连接,它每接受一个连接,就创建一个线程用来处理连接。 为达到这个目的,仅仅在这两个线程中传递套接字对象的不够的。
  虽然在两个线程之间不能传递套接字对象,但我们可以在线程之间传递线程句柄。因此我们 可以:
  (1)把附加接受连接的线程的套接字对象上的套接字句柄分离出来。
  (2)在两个线程中传递套接字句柄。
  (3)在处理套接字连接的线程中,把这个套接字句柄附加到套接字对象上。
  为了更好的说明以上方法,现将程序的部分列出:
//在主线程中创建监听线程的OnAccept函数
void CListenSocket::OnAccept(int nErrorCode)

  CAsyncSocket soc;//用于接受连接请求而建的临时对象
  Accept(soc);//接受请求
   //创建新线程并挂起(该线程是用户界面线程)
  CSockThread*pThread=(CSockThread*)AfxBeginThread(
   RUNTIME-CLASS(CSockThread),THREAD-PRIORITY-NORMAL.
   O,CREATE-SUSPENDED);
   //将套接字句柄从套接字对象中分离出来,并保存
  pThread->m-hSocket=(SOCKET)SOC.Detach();
   //开始执行新线程
  pThread->ResumeThread();
  CAsyncSocket::OnAccept(nErrorCode);

  //处理连接线程(即新线程)的InitInstance函数,
BOOL CSockThread::InitInstance()
   //把这个套接字句柄附加到新的套接字对象上,
   //这样主线程的通知函数就能发送到新的线程中,在新的线程中处理
  m-socket,Attach(m-hSocket);
  return TRUE;

  通过以上步骤,我们就可以用多线程处理网络通信了。

六、总结

  在开发电子教室系统过程中,我们利用网络编程实现了教师机和学生机之间的连续图像的传 输,传输效果相当令人满意。我们利用CAsyncSocket和CSocket两个类分别实现了图象在网 络上传输。两个类实现的效果相差无几,不过由于CSocket类可以使用CArchive对象之间传递数据,并且引入了多线程技术进行网络通信以避免阻塞,CSocket类完全可以满足图像通 信的要求。
  由于客户机和服务器程序的通信模块都是用套接字开发的网络通信程序,因此屏蔽了网络层 的复杂的结构和协议,使应用软件能在各种网上运行,而不必关心是何种网络以及服务器 或客户机在网上的具体位置和数据传送细节。

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值