Winsocket入门教程一:多线程阻塞式服务器和阻塞式客户端程序(TCP)收藏最近因为工作需要学习了Winsocket客户端服务器模型程序的设计。在学习的过程中,我发现学习Winsocket的资料不多并且十分的零散。我一直没有找到一本学习Winsocket方面的经典国外著作。而且这些资料中并没有提供源代码文件,所以我只有将这些源代码在自己敲一遍。在敲代码的过程中,我发现了这些源代码中的一些错误的地方和一些已经过时的Windows程序的输写方法(Win16?)。现将学习经验和通过阅读各种资料总结出来的模型以及代码分享出来。希望对学习Winsocket的初学者有一定的帮助。我们首先来了解一下什么是Winsocket。Winsocket是unix/linux下的berkeleysocket在Windows下的实现。unix/linux下的berkeleysocket是网络通讯方面的基石,应用程序通过调用berkeleysocket的API进行相互通讯,berkeleysocket则利用具体的网络通讯协议和操作系统的调用来为我们完成具体的通讯工作。Winsocket保留了berkeleysocket的所有内容,并且为了其能在Win32消息机制和多线程的环境下更好的工作。Winsocket在berkeleysocket原有的基础上对其进行了扩充。如我们可以利用WSAAsyncSelect对Socket消息进行订阅,以及使用WSAGetLastError对多线程环境下的Winsocket错误进行捕获。接着再让我们来了解一下服务器\客户端应用程序模型。该模型是构建分布式系统的模型之一。服务器程序一直处于监听的状态,等待客户端程序的连接。客户端程序像服务器程序发送连接请求,服务器程序接受该连接请求,同时与客户端程序建立连接。此时客户端程序就可以向服务器发送具体的请求,获取相关的数据。服务器\客户端模型有三种连接方式,一种是面向连接的(TCP),面向连接的服务是一种可靠的服务,它通过数据流进行数据的传输,面向连接的服务实现了无差错无重复的顺寻数据发送。一种是面向无连接的(UDP),面向无连接的服务是一种不可靠的服务,它通过数据报进行数据传输,由于数据报进行传输时的顺序是无序的,所以它是不可靠的服务。最后一种是多播的方式,及服务器程序主动向多个客户端程序发送信息。面向连接的服务器\客户端应用程序模型的程序流程图如下所示:在此模型的阻塞模式中,服务端程序在执行accept操作、客户端程序connect操作、以及服务端\客户端在进行read和write操作时,如果这些操作既没有成功也没有失败,应用程序会在执行这些操作的地方一直阻塞着。所以我们应该在服务端应用程序的主线程中不停的调用accept操作,以使服务端程序能不停地接受客户端程序发送过来的连接请求。而在接受了一个客户端的连接请求后,我们应改为每一个接受的连接请求开辟一个专门的线程来接受客户端程序发送的请求以及为具体的请求返回特定的信息。根据以上的程序流程图以及说明,我们可以写出以下的服务端程序源代码:viewplaincopytoclipboardprint?1.////////////////////////////////////////////////////////////////////////////////////////////////////2.///\fileServerMultThread\ServerMultThread.cpp3.///4.///\brief阻塞式多线程服务器程序。每当客户端程序请求与服务端连接时,服务端程序开放一个线程接受客户端程序的请求5.///并且向客户端回馈请求的信息。客户端请求的信息输出到控制台中.6.////////////////////////////////////////////////////////////////////////////////////////////////////7.#includeiostream8.#includecassert9.#includeWinSock2.h10.#includeprocess.h11.#pragmacomment(lib,ws2_32.lib)12.#defineASSERTassert13.#defineTHREADHANDLE14.#defineEVENTHANDLE15.#defineCloseThreadCloseHandle16.#defineCloseEventCloseHandle17.usingstd::cin;18.usingstd::cout;19.usingstd::endl;20.////////////////////////////////////////////////////////////////////////////////////////////////////21.///\structtagServerRecv22.///23.///\brief线程函数参数结构体,其中包含已建立连接的socket.24.///25.///\authorShining10026.///\date2010-05-1827.////////////////////////////////////////////////////////////////////////////////////////////////////28.typedefstructtagServerRecv29.{30.SOCKETskAccept;//已建立连接的socket31.CRITICAL_SECTION*pcs;//同步控制台输出的临界区32.EVENTe;//保证结构体各个字段在结构体字段改变之前将其拷贝到线程中的信号量33.THREADt;//当前线程的内核对象34.DWORDdwThreadID;//当前线程的ID35.}SERVER_RECV,*PSERVER_RECV;36.////////////////////////////////////////////////////////////////////////////////////////////////////37.///\fnstaticintServerRecv(LPVOIDlParam)38.///39.///\brief服务器与建立连接的客户端进行通讯.40.///41.///\authorShining10042.///\date2010-05-1843.///44.///\paramlParam线程函数参数,详细信息见上面说明.45.///46.///\return总是返回0.47.////////////////////////////////////////////////////////////////////////////////////////////////////48.staticintServerRecv(LPVOIDlParam);49.staticconstintc_iPort=10001;50.intmain()51.{52.intiRet=SOCKET_ERROR;53.//初始化Winsocket,所有Winsocket程序必须先使用WSAStartup进行初始化54.WSADATAdata;55.ZeroMemory(&data,sizeof(WSADATA));56.iRet=WSAStartup(MAKEWORD(2,0),&data);57.ASSERT(SOCKET_ERROR!=iRet);58.//建立服务端程序的监听套接字59.SOCKETskListen=INVALID_SOCKET;60.skListen=socket(AF_INET,SOCK_STREAM,0);61.ASSERT(INVALID_SOCKET!=skListen);62.//初始化监听套接字地址信息63.sockaddr_inadrServ;//表示网络地址64.ZeroMemory(&adrServ,sizeof(sockaddr_in));65.adrServ.sin_family=AF_INET;//初始化地址格式,只能为AF_INET66.adrServ.sin_port=htons(c_iPort);//初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序67.adrServ.sin_addr.s_addr=INADDR_ANY;//初始化IP,由于是服务器程序,所以可以将INADDR_ANY赋给该字段,表示任意的IP68.//绑定监听套接字到本地69.iRet=bind(skListen,(sockaddr*)&adrServ,sizeof(sockaddr_in));70.ASSERT(SOCKET_ERROR!=iRet);71.//使用监听套接字进行监听72.iRet=listen(skListen,SOMAXCONN);//SOMAXCONN表示可以连接到该程序的最大连接数73.ASSERT(SOCKET_ERROR!=iRet);74.//输出控制台缓冲区,由于可能有多个客户端程序可能同时向缓冲区发送请求信息75.//为了保证输出时能够一次性完整的输出完一个客户端的请求信息,所以在输出客76.//户程序的信息到控制台时,必须使用临界区阻塞其它线程77.CRITICAL_SECTIONcs;78.InitializeCriticalSection(&cs);79.//保证结构体各个字段在结构体字段改变之前将其拷贝到线程中的信号量80.//因为当该结构体拷贝到线程中之前,有可能有新的连接到来并改变了结构体的值81.//所以我们必须先保证值拷贝过后再接受连接82.EVENTe=NULL;83.e=CreateEvent(NULL,FALSE,FALSE,NULL);84.ASSERT(NULL!=e);85.for(;;)86.{87.//客户端向服务器端发送连接请求,服务器端接受客户端的连接88.SOCKETskAccept=INVALID_SOCKET;89.sockaddr_inadrClit;90.ZeroMemory(&adrClit,sizeof(sockaddr_in));91.intiLen=sizeof(sockaddr_in);92.skAccept=accept(skListen,(sockaddr*)&adrClit,&iLen);//如果没有客户端程序请求连接,服务端程序会一直阻塞在这里等待连接93.ASSERT(INVALID_SOCKET!=skAccept);94.SERVER_RECVsr;95.//成功创建连接后创建一个独立的线程应答客户请求,以防止应用程序因为阻塞无法应答新的客户请求96.//我们应该先将线程挂起,以便我们能够在线程执行之前初始化线程所需要的结构体变量中的各个字段97.THREADhThread=NULL;98.DWORDdwThreadID=0;99.hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ServerRecv,100.&sr,CREATE_SUSPENDED,&dwThreadID);101.ASSERT(NULL!=hThread);102.//初始化结构体字段103.sr.skAccept=skAccept;104.sr.pcs=&cs;105.sr.e=e;106.sr.t=hThread;107.sr.dwThreadID=dwThreadID;108.//启动线程109.DWORDdwRet=ResumeThread(hThread);110.ASSERT(-1!=dwRet);111.112.//保证结构体被拷贝到线程中后再应答新的连接113.