上一篇 Winsocket 一:单线程阻塞server&client程序(tcp) 在结尾处提到有一个缺点,即一次只能与一个客户端通信。本篇在服务端应用程序的主线程中不停的调用accept操作,以使服务端程序能不停地接受客户端程序发送过来的连接请求。而在接受了一个客户端的连接请求后,为每一个接受的连接请求开辟一个专门的线程来接受客户端程序发送的请求以及为具体的请求返回特定的信息。
代码如下:
#include "stdafx.h" #include <iostream> #include <cassert> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define ASSERT assert static const int c_iPort = 10001; using std::cout; using std::endl; //线程参数结构体 typedef struct tagServerRecv { SOCKET skAccept; //已建立连接的socket CRITICAL_SECTION *pcs;//关键段,保证每个连接的信息能每一次完整的输出到控制台 HANDLE e; //事件,保证结构体个字段改变之前将其拷贝到线程中的信号量 HANDLE t; //当前线程的内核对象 DWORD dwThreadID;//当前线程ID }SERVER_RECV, *PSERVER_RECV; //线程入口函数,用于server&client通信 static int ServerRecv(LPVOID LParam); int _tmain(int argc, _TCHAR* argv[]) { int iRet = SOCKET_ERROR; //初始化Winsocket WSADATA data; ZeroMemory(&data, sizeof(WSADATA)); iRet = WSAStartup(MAKEWORD(2, 0), &data); ASSERT(SOCKET_ERROR != iRet); //建立服务器端程序的监听套接字 SOCKET skListen = INVALID_SOCKET; skListen = socket(AF_INET, SOCK_STREAM, 0); //tcp ASSERT(INVALID_SOCKET != skListen); //初始化监听套接字地址信息,网络格式 sockaddr_in adrServ; ZeroMemory(&adrServ, sizeof(sockaddr_in)); adrServ.sin_family = AF_INET; adrServ.sin_port = htons(c_iPort); adrServ.sin_addr.s_addr = INADDR_ANY; //绑定监听套接字到本地 iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in)); ASSERT(SOCKET_ERROR != iRet); //使用套接字进行监听 iRet = listen(skListen, SOMAXCONN); ASSERT(SOCKET_ERROR != iRet); CRITICAL_SECTION cs; HANDLE e; InitializeCriticalSection(&cs); e = CreateEvent(NULL, FALSE, FALSE, NULL);//自动置位, 初始无触发的匿名事件 ASSERT(NULL != e); int count = 0; for (;;) { //if (count++ == 5) //{ // break; //} SOCKET skAccept = INVALID_SOCKET; sockaddr_in adrClit; ZeroMemory(&adrClit, sizeof(sockaddr_in)); int iLen = sizeof(sockaddr_in); skAccept = accept(skListen, (sockaddr*)&adrClit, &iLen); ASSERT(INVALID_SOCKET != skAccept); cout << "New connection " << skAccept << ", c_ip: " << inet_ntoa(adrClit.sin_addr) << ", c_port: " << ntohs(adrClit.sin_port) << endl; // 成功创建连接后创建一个独立的线程应答客户请求,以防止应用程序因为阻塞无法应答新的客户请求 // 应先将线程挂起,以便能够在线程执行之前初始化线程所需要的结构体变量中的各个字段 SERVER_RECV sr; HANDLE hThread = NULL; DWORD dwThreadID = 0; hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ServerRecv, &sr, CREATE_SUSPENDED, &dwThreadID); //初始化结构体字段 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(WAIT_FAILED != dwRet); } //销毁事件和关键段 BOOL bRet = CloseHandle(e); ASSERT(bRet); DeleteCriticalSection(&cs); // 关闭该套接字的连接 iRet = shutdown(skListen, SD_SEND); ASSERT(SOCKET_ERROR != iRet); // 清理该套接字的资源 iRet = closesocket(skListen); ASSERT(SOCKET_ERROR != iRet); // 清理Winsocket资源 iRet = WSACleanup(); ASSERT(SOCKET_ERROR != iRet); system("pause"); return 0; } int ServerRecv(LPVOID LParam) { PSERVER_RECV psr = (PSERVER_RECV)LParam; //触发事件,使主线程能够接受新的连接 BOOL bRet = SetEvent(psr->e); ASSERT(bRet); const int c_iBufLen = 512; char recvBuf[c_iBufLen + 1] = {'\0'}; const char c_prefix[] = "Server recv:"; char sendBuf[c_iBufLen + 16 + 1] = {'\0'} ; strcpy(sendBuf, c_prefix); int iRet = SOCKET_ERROR; for (;;) { //接收客户端消息 iRet = recv(psr->skAccept, recvBuf, c_iBufLen, 0);// 接收客户端发送的信息, 如果客户端不发送信息,则线程会阻塞到此处 if(iRet == 0)//客户端优雅的关闭了此连接 { EnterCriticalSection(psr->pcs); cout << "Connection " << psr->skAccept << " , " << psr->dwThreadID << " shutdown." << endl; LeaveCriticalSection(psr->pcs); break; } else if(SOCKET_ERROR == iRet) //粗鲁地关闭 { EnterCriticalSection(psr->pcs); cout << "Connection " << psr->skAccept << " , " << psr->dwThreadID << " recv error." << endl; LeaveCriticalSection(psr->pcs); break; } recvBuf[iRet] = '\0'; EnterCriticalSection(psr->pcs); cout << "Connection " << psr->skAccept << " , " << psr->dwThreadID << " recv: " << recvBuf << endl; LeaveCriticalSection(psr->pcs); //向客户端发送消息 strcpy(sendBuf + strlen(c_prefix), recvBuf); iRet = send(psr->skAccept, sendBuf, strlen(sendBuf), 0); // 客户端如果没有足够的缓冲区接受信息,则线程会阻塞到此处 if(SOCKET_ERROR == iRet) { EnterCriticalSection(psr->pcs); cout << "Connection " << psr->skAccept << " , " << psr->dwThreadID << " send error." << endl; LeaveCriticalSection(psr->pcs); break; } } //关闭该套接字端口 iRet = shutdown(psr->skAccept, SD_SEND); while (recv(psr->skAccept, recvBuf, c_iBufLen, 0) > 0); ASSERT(SOCKET_ERROR != iRet); //清理该套接字的资源 iRet = closesocket(psr->skAccept); ASSERT(SOCKET_ERROR != iRet); //关闭线程 bRet = CloseHandle(psr->t); ASSERT(bRet); cout << "Connection " << psr->dwThreadID << " exit." << endl; return 0; }
几点注意:
1、线程参数结构的内核对象主要用来在线程函数内部关闭线程,在许多的程序中,可以由主线程来完成子线程的结束,但这里放在线程函数内部更合适(一个连接断开后很自然应该就关闭线程了);
2、多线程下的互斥与同步。
阻塞模式下即使采用了多线程,因为服务端程序在执行accept操作、客户端程序在执行connect操作、以及服务端\客户端在进行read和write操作时,如果这些操作既没有成功也没有失败,应用程序会在执行这些操作的地方一直阻塞着。因此下一篇继续介绍非阻塞模式:Winsocket 三:非阻塞server&client程序(tcp)。
参考资料:
http://www.doc88.com/p-905997530710.html
(线程创建)http://blog.csdn.net/morewindows/article/details/7421759
(关键段)http://blog.csdn.net/morewindows/article/details/7442639
(事件)http://blog.csdn.net/morewindows/article/details/7445233