Winsocket入门教程一:多线程阻塞式服务器和阻塞式客户端程序(TCP)

  • 格式:doc
  • 大小:201.00 KB
  • 文档页数:14

Winsocket入门教程一:多线程阻塞式服务器和阻塞式客户端程序(TCP) 收藏我们首先来了解一下什么是Winsocket。

Winsocket是unix/linux下的berkeley socket在Windows下的实现。

unix/linux下的berkeley socket是网络通讯方面的基石,应用程序通过调用berkeley socket的API进行相互通讯,berkeley socket则利用具体的网络通讯协议和操作系统的调用来为我们完成具体的通讯工作。

Winsocket保留了berkeley socket的所有内容,并且为了其能在Win32消息机制和多线程的环境下更好的工作。

Winsocket在berkeley socket 原有的基础上对其进行了扩充。

如我们可以利用WSAAsyncSelect对Socket消息进行订阅,以及使用WSAGetLastError对多线程环境下的Winsocket错误进行捕获。

接着再让我们来了解一下服务器\客户端应用程序模型。

该模型是构建分布式系统的模型之一。

服务器程序一直处于监听的状态,等待客户端程序的连接。

客户端程序像服务器程序发送连接请求,服务器程序接受该连接请求,同时与客户端程序建立连接。

此时客户端程序就可以向服务器发送具体的请求,获取相关的数据。

服务器\客户端模型有三种连接方式,一种是面向连接的(TCP),面向连接的服务是一种可靠的服务,它通过数据流进行数据的传输,面向连接的服务实现了无差错无重复的顺寻数据发送。

一种是面向无连接的(UDP),面向无连接的服务是一种不可靠的服务,它通过数据报进行数据传输,由于数据报进行传输时的顺序是无序的,所以它是不可靠的服务。

最后一种是多播的方式,及服务器程序主动向多个客户端程序发送信息。

面向连接的服务器\客户端应用程序模型的程序流程图如下所示:在此模型的阻塞模式中,服务端程序在执行accept操作、客户端程序connect操作、以及服务端\客户端在进行read和write操作时,如果这些操作既没有成功也没有失败,应用程序会在执行这些操作的地方一直阻塞着。

所以我们应该在服务端应用程序的主线程中不停的调用accept操作,以使服务端程序能不停地接受客户端程序发送过来的连接请求。

而在接受了一个客户端的连接请求后,我们应改为每一个接受的连接请求开辟一个专门的线程来接受客户端程序发送的请求以及为具体的请求返回特定的信息。

根据以上的程序流程图以及说明,我们可以写出以下的服务端程序源代码:view plaincopy to clipboardprint?/////////////////////////////////////////////////////////////////////////////////////////////////////// \file ServerMultThread\ServerMultThread.cpp////// \brief 阻塞式多线程服务器程序。

每当客户端程序请求与服务端连接时,服务端程序开放一个线程接受客户端程序的请求/// 并且向客户端回馈请求的信息。

客户端请求的信息输出到控制台中.////////////////////////////////////////////////////////////////////////////////////////////////////#include <iostream>#include <cassert>#include <WinSock2.h>#include <process.h>#pragma comment(lib, "ws2_32.lib" )#define ASSERT assert#define THREAD HANDLE#define EVENT HANDLE#define CloseThread CloseHandle#define CloseEvent CloseHandleusing std::cin;using std::cout;using std::endl;/////////////////////////////////////////////////////////////////////////////////////////////////////// \struct tagServerRecv////// \brief 线程函数参数结构体,其中包含已建立连接的socket.////// \author Shining100/// \date 2010-05-18////////////////////////////////////////////////////////////////////////////////////////////////////typedef struct tagServerRecv{SOCKET skAccept; // 已建立连接的socketCRITICAL_SECTION *pcs; // 同步控制台输出的临界区EVENT e; // 保证结构体各个字段在结构体字段改变之前将其拷贝到线程中的信号量THREAD t; // 当前线程的内核对象DWORD dwThreadID; // 当前线程的ID}SERVER_RECV, *PSERVER_RECV;/////////////////////////////////////////////////////////////////////////////////////////////////////// \fn static int ServerRecv(LPVOID lParam)////// \brief 服务器与建立连接的客户端进行通讯.////// \author Shining100/// \date 2010-05-18////// \param lParam 线程函数参数, 详细信息见上面说明.////// \return 总是返回0.////////////////////////////////////////////////////////////////////////////////////////////////////static int ServerRecv(LPVOID lParam);static const int c_iPort = 10001;int main(){int iRet = SOCKET_ERROR;// 初始化Winsocket,所有Winsocket程序必须先使用WSAStartup进行初始化WSADATA data;ZeroMemory(&data, sizeof(WSADATA));iRet = WSAStartup(MAKEWORD(2, 0), &data);ASSERT(SOCKET_ERROR != iRet);// 建立服务端程序的监听套接字SOCKET skListen = INV ALID_SOCKET;skListen = socket(AF_INET, SOCK_STREAM, 0);ASSERT(INV ALID_SOCKET != skListen);// 初始化监听套接字地址信息sockaddr_in adrServ; // 表示网络地址ZeroMemory(&adrServ, sizeof(sockaddr_in));adrServ.sin_family = AF_INET; // 初始化地址格式,只能为AF_INETadrServ.sin_port = htons(c_iPort); // 初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序adrServ.sin_addr.s_addr = INADDR_ANY; // 初始化IP,由于是服务器程序,所以可以将INADDR_ANY赋给该字段,表示任意的IP// 绑定监听套接字到本地iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in));ASSERT(SOCKET_ERROR != iRet);// 使用监听套接字进行监听iRet = listen(skListen, SOMAXCONN); // SOMAXCONN表示可以连接到该程序的最大连接数ASSERT(SOCKET_ERROR != iRet);// 输出控制台缓冲区,由于可能有多个客户端程序可能同时向缓冲区发送请求信息// 为了保证输出时能够一次性完整的输出完一个客户端的请求信息,所以在输出客// 户程序的信息到控制台时,必须使用临界区阻塞其它线程CRITICAL_SECTION cs;InitializeCriticalSection(&cs);// 保证结构体各个字段在结构体字段改变之前将其拷贝到线程中的信号量// 因为当该结构体拷贝到线程中之前, 有可能有新的连接到来并改变了结构体的值// 所以我们必须先保证值拷贝过后再接受连接EVENT e = NULL;e = CreateEvent(NULL, FALSE, FALSE, NULL);ASSERT(NULL != e);for(;;){// 客户端向服务器端发送连接请求,服务器端接受客户端的连接SOCKET skAccept = INV ALID_SOCKET;sockaddr_in adrClit;ZeroMemory(&adrClit, sizeof(sockaddr_in));int iLen = sizeof(sockaddr_in);skAccept = accept(skListen, (sockaddr*)&adrClit, &iLen); // 如果没有客户端程序请求连接,服务端程序会一直阻塞在这里等待连接ASSERT(INV ALID_SOCKET != skAccept);SERVER_RECV sr;// 成功创建连接后创建一个独立的线程应答客户请求,以防止应用程序因为阻塞无法应答新的客户请求// 我们应该先将线程挂起,以便我们能够在线程执行之前初始化线程所需要的结构体变量中的各个字段THREAD hThread = NULL;DWORD dwThreadID = 0;hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ServerRecv,&sr, CREATE_SUSPENDED, &dwThreadID);ASSERT(NULL != hThread);// 初始化结构体字段sr.skAccept = skAccept;sr.pcs = &cs;sr.e = e;sr.t = hThread;sr.dwThreadID = dwThreadID;// 启动线程DWORD dwRet = ResumeThread(hThread);ASSERT(-1 != dwRet);// 保证结构体被拷贝到线程中后再应答新的连接 dwRet = WaitForSingleObject (e, INFINITE);ASSERT(W AIT_FAILED != dwRet);}// 清理线程同步资源DeleteCriticalSection(&cs);BOOL bRet = FALSE;bRet = CloseEvent(e);ASSERT(bRet);// 关闭该套接字的连接iRet = shutdown(skListen, SD_SEND);ASSERT(SOCKET_ERROR != iRet);// 清理该套接字的资源iRet = closesocket(skListen);ASSERT(SOCKET_ERROR != iRet);// 清理Winsocket资源iRet = WSACleanup();ASSERT(SOCKET_ERROR != iRet);cin.get();return 0;}int ServerRecv(LPVOID lParam){// 拷贝结构体各个字段到线程中PSERVER_RECV psr = (PSERVER_RECV)lParam;SERVER_RECV sr = {psr->skAccept,psr->pcs,psr->e,psr->t,psr->dwThreadID};// 设置信号量,使主线程能够接受新的连接BOOL bRet = FALSE;bRet = SetEvent(sr.e);ASSERT(bRet);const int c_iBufLen = 512;char szBuf[c_iBufLen + 1] = {'\0'};const char c_szPrefix[] = "Server recv:";const int c_iPrefLen = strlen(c_szPrefix);char szRely[c_iBufLen + 16 + 1] = {'\0'};strcpy(szRely, c_szPrefix);int iRet = SOCKET_ERROR;for(;;){iRet = recv(sr.skAccept, szBuf, c_iBufLen, 0); // 接收客户端发送的信息,如果客户端不发送信息,则线程会阻塞到此处if(0 == iRet) // 客户端优雅的关闭了此连接{cout << "Connection " << sr.dwThreadID << " shutdown." << endl;break;}else if(SOCKET_ERROR == iRet) // 客户端粗鲁的关闭了此连接或者接受信息出错{cout << "Connection " << sr.dwThreadID << " recv error." << endl;break;}szBuf[iRet] = '\0';EnterCriticalSection(sr.pcs);cout << "Connection " << sr.dwThreadID << " says:" << szBuf << endl; // 输出接收到的信息LeaveCriticalSection(sr.pcs);// 向客户端发送信息strcpy(szRely + c_iPrefLen, szBuf);iRet = send(sr.skAccept, szRely, strlen(szRely), 0); // 客户端如果没有足够的缓冲区接受信息,则线程会阻塞到此处if(SOCKET_ERROR == iRet){cout << "Connection " << sr.dwThreadID << " send error." << endl;break;}}// 关闭该套接口iRet = shutdown(sr.skAccept, SD_SEND);while(recv(sr.skAccept, szBuf, c_iBufLen, 0) > 0);ASSERT(SOCKET_ERROR != iRet);// 清理该套接口的资源在用Winsocket编写程序时,我们首先必须要进行如下的操作,以为该进程初始化Winsocket和Ws2_32.dll,而使后面的函数调用有效。