23.3 TCP应用程序设计
23.3.1 通信协议的工作线程的设计——阻塞模式
(1)设计TCP链路的通信协议
①数据包的设计:数据包头和数据包体(可参考代码中的消息定义部分)——TLV(Type-Length-Value)
组成 |
说明 |
数据包头 |
包含命令代码字段和整个数据包大小的字段(这个字段长度是固定的),即使通信双方己约定好各种命令数据包的长度,可以直接从命令代码中间接地判断出该数据包的长度,但仍建议设计该结构头时,保留数据包长度这个字段。 命令代码如:登录命令、消息上传、下载命令、退出 |
数据包体 |
各种数据包定义的集合 |
②如此设计的好处——便于接收方从数据流中解出正确的数据包。首先接收一个完整的数据包头,并对数据包进行检验。检验合法时再从包头中读出数据包总长度,就可以接收数据包剩余部分。不管通信协议的设计如何改变。请记住一点:数据包的结构必须有一个固定长度的包头,便于接收方首先接收,其次包头中必须包含整个数据包长度的信息,这样接收方才能从数据流中正确分解出完整的数据包。
③RecvPacket函数:设计用来接收一个完整的数据包(注意,每个数据包的长度是不一的)。该函数调用RecvData函数来接收数据包头,然后校验 。通过后检收剩余的部分。
④RecvData函数:设计用来循环接收指定字节数的数据。为防止recv函数的阻塞,在每次recv之前,都先利用select函数检测是否有数据到达。同时该函数还设计了一个超时检测。
(2)链路异常检测
①TCP连接正常断开的时候,主动断开的一方会向另一方发送含有断开连接标志的数据包(在链路层里实现的,对应用程序来说是透明的)。接收方收到数据包会,将连接的状态更新为断开。这里任何对套接字的操作都会返回SOCKET_ERROR,包含正在阻塞等待操作(如recv)都会立即返回。这个SOCKET_ERROR可以被工作线程检测到。
②当网络故障或一方的计算机崩溃时,另一方是收不到含有断开连接标志的数据包的。系统中会认为对方一直没有数据过来。这种情况会延续到程序需要主动发送数据包为止,如果发送的数据包得不到对方的应答,在经过几次尝试全部超时以后,就会意识到连接己经断开(注意:为什么要发送多次,因为send时要将数据送入“发送缓冲区”,因为发送缓冲区未满,WinSock接口并没有真正在网络发送数据包。所以第1次send会返回成功。在经过几次的尝试,如果数据真正发送出去,却得不到对方的回复,WinSock才将连接置为断开。这时对套接字的操作才会全部失败)。
③发现链路异常的唯一办法是主动发送数据!实现中可记录链路最后一次活动时间,一旦空闲的时间一到(即距最后一次的活动时间秒数一到),就主动向另一方发送一个数据包以检测链路状态。考虑到网络传输的问题,发送方如果空闲30秒后发送检测包,接收方可以在链路空闲60秒会才认为是连接异常的。
④链路检测包由服务端还是客户端发送是没有规定的。可根据实际情况自行决定。
(3)多线程下的数据收发
①线程设计
接收数据 |
单独创建一个线程,循环调用select和recv函数来接收数据 |
发送数据 |
在主线程中调用,因为经常在用户界面上操作send函数。如点击“发送”按钮。 |
②sock的排队锁定机制
WinSock对“发送缓冲区”和“接收缓冲区”进行排队锁定机制,当多个线程同时调用send函数操作同一套接字时,只有一个线程锁定发送缓冲区,其余线程处于等待。所以多个线程调用send函数发送的数据,每一份数据仍是被串行化的结果。同理,recv接收时也会被锁定,同一份数据不会被多个线程重复接收(如线程A和线程B都收到这份数据)。
③多线程操作同一socket进行收发数据包时的问题
函数 |
阻塞模式 |
非阻塞模式 |
send |
总是指定大小的数据发送完才返回。 1、如果每次发送一个完整数据包时:因排队锁定机制,数据包之间不会互相交织(即线程A的数据包内部不会出现线程B数据包的一部分数据)。 2、如果每次发送的是部分数据包,如发送线程A数据包头完毕,接着发送线程B的数据包头,然后轮到线程A的数据包体,这样接方收的数据是错误的。 |
即使一次发送一个完整的数据包时,也只会发送部分的数据出去,要通过多次send将整个数据包发完。这样循环调用send的过程中可能被其他线程的send操作插入,造成数据包的混乱。 |
recv |
不管是阻塞还是非阻塞下,recv总不能保证一次收全一个数据包。有时必须多次调用recv函数(如一次接收数据包头、一个接收数据包体),多次收取的过程,中间部分数据可能被其他线程收走,造成数据错误。 |
|
备注 |
如果要进行一个数据包分多次发送或分多次分取,一般都需要加临界区对象,以保证在发送或接收一个完整的数据包期间,不会其他线程打断而出现数据包收发错误的问题。 |
④网络应用程序的常见通信方式(注意,这是应用层的,不是传输层的)
【同一线程中用下面的代码结构来处理应答式通信】
/*---------------------------------------------------------- 处理接收的命令并回复对方的模块,输入参数为接收到的命令数据包 -----------------------------------------------------------*/ void ProcRecvCmd(MSGSTRUCT* pMsg) { switch (pMsg->MsgHead.nCmdID) { case C1: //命令码 处理并用send发送C1_RESP数据包 return; case C2: 处理并用send发送C1_RESP数据包 return; } } /*---------------------------------------------------------- 主动发送命令并接收对方回复(对方处理结果) -----------------------------------------------------------*/ void SendCmd() { if (命令队列中没有命令需要发送) return; 从队列中取需要发送的命令; switch (命令码) { case S1: 用send发送S1数据包;
do { 用RecvPacket接收回复的数据包; if (回复的数据 != S1_RESP) ProcRecvCmd(数据包); //处理接收到的数据包 else //回复的数据包,则结束本轮发送 break; } while (TRUE);
return 0; case S2: //同处理S1代码类似,这里省略...... return 0; ...... //其他命令代码的发送 } } /*---------------------------------------------------------- 工作线程 -----------------------------------------------------------*/ ...... //前面的省略 while (TRUE) { SendCmd(); CheckLine; //发送链路检测包(具体程序省略)
调用select函数等待100ms,查看是否有数据到达;
if (有数据到达) { 调用RecvPacket接收整个数据包; ProcRecvCmd(数据包); //处理这个数据包 } }
【阻塞模式的TCP聊天室程序】
效果图
★客户端和服务器端公共文件★
//Message.h文件 ——通信协议的定义的文件,同时供服务器端和客户端使用
#pragma once #include <windows.h> //取结构体某个字段的偏移量 //思路:将址址0x00000000开始的地址看作是TYPE结构体对象 //然后再取出指定的字段的地址,即是偏移量 #define OFFSET(TYPE, MEMB) ((size_t) &((TYPE *)0)->MEMB) //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // 使用 TCP 协议的聊天室例子程序 // 通讯链路传输的数据结构定义 //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> //******************************************************************** #define CMD_LOGIN 0x10 //客户端 ->服务器端,登录 #define CMD_LOGIN_RESP 0x81 //服务器端 -> 客户端,登录回应 #define CMD_MSG_UP 0x02 //客户端 -> 服务器端,聊天语句 #define CMD_MSG_DOWN 0x82 //服务器端 -> 客户端,聊天语句 #define CMD_CHECK_LINK 0x83 //服务器端 -> 客户端,链路检测 //******************************************************************** //******************************************************************** // 数据包头部,所有的数据包都以 MSGHEAD 开头 //******************************************************************** typedef struct _tagMsgHead { int nCmdID; //命令ID int cbSize; //整个数据包长度 = 数据包头部 + 数据包体 }MSGHEAD,*PMSGHEAD; //******************************************************************** // 登录数据包(客户端->服务器端) //******************************************************************** typedef struct _tagMsgLogin { TCHAR szUserName[12]; //用户登录ID TCHAR szPassword[12]; //登录密码 }MSGLOGIN, *PMSGLOGIN; //******************************************************************** // 登录回应数据包(服务器端->客户端) //******************************************************************** typedef struct _tagMsgLoginResp { char dbResult; //登录结构:1=成功,0=用户名或密码错误 }MSGLOGINRESP, *PMSGLOGINRESP; //******************************************************************** // 聊天语句(客户端->服务器端):不等长数据包 //******************************************************************** typedef struct _tagMsgUp { int cbSizeConent; //后面内容字段的长度 char szConetent[256]; //内容,不等长,长度由cbSizeConent指定 }MSGUP, *PMSGUP; //******************************************************************** // 聊天语句(服务器端->客户端):不等长数据包 //******************************************************************** typedef struct _tagMsgDown { int cbSizeConent; //后面内容字段的长度(单位字节) TCHAR szSender[12]; //消息发送者 TCHAR szContent[256]; //内容,不等长,长度由nLength指定,要求这是最后一个字段 }MSGDOWN, *PMSGDOWN; //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // 数据包定义方式 // 每个数据包以MSGHEAD + MSGXXX组成,整个长度填入MSGHEAD.dwLength //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> typedef struct _tagMsgStruct { MSGHEAD MsgHead; union { MSGLOGIN Login; MSGLOGINRESP LoginResp; MSGUP MsgUp; MSGDOWN MsgDown; };//Body }MSGSTRUCT, *PMSGSTRUCT;
//MsgQueue.h——消息队列函数定义
#pragma once #include <windows.h> #include <strsafe.h> //使用到StringcbCopy等函数 extern CRITICAL_SECTION cs; extern int nMsgCount; //队列中当前消息的数量 extern int nSequence; //消息的序号,从1开始 typedef struct _tagMsgQueueItem //队列中单条消息的格式定义 { int nMessageId; //消息编号 TCHAR szSender[12]; //发送者 TCHAR szContent[256]; //聊天内容 }MSGQUEUEITEM,*PMSGQUEUEITEM; void InsertMsgQueue(TCHAR* pszSender, TCHAR* pszContent); int GetMsgFromQueue(int nMessageId,TCHAR* pszSender,TCHAR* pszContent);
//MsgQueue.c文件——实现消息队列函数
/*----------------------------------------------------------------------- MSGQUEUE.C ——先进先出消息队列的实现(First in, first out) (c)浅墨浓香,2015.6.27 -----------------------------------------------------------------------*/ #include "MsgQueue.h" #define QUEUE_SIZE 100 //消息队列的长度 CRITICAL_SECTION cs; int nMsgCount = 0; //队列中当前消息的数量 int nSequence = 0; //消息的序号,从1开始 MSGQUEUEITEM MsgQueue[QUEUE_SIZE]; //在队列中加入一条消息 //——如果队列己满,则将整个队列前移一个位置,相当于最早的消息被覆盖 // 然后在队列尾部空出的位置加入新消息 //——如果队列未满,则在队列的最后加入新消息 //——消息编号从1开始递增,这样保证队列中的各消息的编号是连续的 //pszSender指两只发送者字符串的指针,pszContent指向聊天语句内容的字符串指针 void InsertMsgQueue(TCHAR* pszSender, TCHAR* pszContent) { //static int nSequence = 0; MSGQUEUEITEM* pMsgItem=&MsgQueue[0]; EnterCriticalSection(&cs); //如果队列己满,则移动队列,并在队列尾部添加新消息 if (nMsgCount>=QUEUE_SIZE) CopyMemory(&MsgQueue[0], &MsgQueue[1], (QUEUE_SIZE - 1)*sizeof(MSGQUEUEITEM)); else ++nMsgCount; //将消息添加到队列尾部 pMsgItem += (nMsgCount-1); //CopyMemory(&pMsgItem->szSender, pszSender, (lstrlen(pszSender) + 1)*sizeof(TCHAR)); //注意,这里的pszSender是个指针 //CopyMemory(&pMsgItem->szContent, pszContent, (lstrlen(pszContent) + 1)*sizeof(TCHAR)); StringCchCopy((TCHAR*)&pMsgItem->szSender,lstrlen(pszSender) + 1,pszSender); StringCchCopy((TCHAR*)&pMsgItem->szContent, lstrlen(pszContent) + 1, pszContent); pMsgItem->nMessageId = ++nSequence; //消息的序号,从1开始 LeaveCriticalSection(&cs); } /*>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 从队列获取指定编号的消息 -- 如果指定编号的消息已经被清除出消息队列,则返回编号最小的一条消息 当向连接速度过慢的客户端发消息的速度比不上消息被清除的速度,则中间 的消息等于被忽略,这样可以保证慢速链路不会影响快速链路 -- 如果队列中的所有消息的编号都比指定编号小(意味着这些消息以前都被获取过) 那么不返回任何消息 参数: nMessageId = 需要获取的消息编号 pszSender = 用于返回消息中发送者字符串的缓冲区指针 pszSender = 用于返回消息中聊天内容字符串的缓冲区指针 返回: 0 (队列为空,或者队列中没有小于等于指定编号的消息) 为不0(已经获取指定消息号) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/ int GetMsgFromQueue(int nMessageId, TCHAR* pszSender, TCHAR* pszContent) { MSGQUEUEITEM* pMsgItem; int nMaxID, nMinID; if (nMsgCount <= 0) return 0; EnterCriticalSection(&cs); pMsgItem = NULL; nMinID = MsgQueue[0].nMessageId; nMaxID =nMinID+ nMsgCount - 1; //获取指定编号的消息 if (nMessageId < nMinID) pMsgItem = &MsgQueue[0]; else if (nMessageId <= nMaxID) pMsgItem = &MsgQueue[nMessageId - nMinID]; if (NULL != pMsgItem) { //CopyMemory(&pszSender, pMsgItem->szSender, sizeof(pMsgItem->szSender));//注意这里pMsgItem->szSender是个数组 //CopyMemory(&pszContent, pMsgItem->szContent, sizeof(pMsgItem->szContent)); StringCbCopy(pszSender, sizeof(pMsgItem->szSender), pMsgItem->szSender); StringCbCopy(pszContent, sizeof(pMsgItem->szContent), pMsgItem->szContent); } LeaveCriticalSection(&cs); return (pMsgItem ==NULL) ? 0:pMsgItem->nMessageId; }
//SocketRoute.h文件 ——阻塞模式下通用的函数声明
#pragma once #include <windows.h> int WaitData(SOCKET sock, DWORD dwTime); int RecvData(SOCKET sock, char* pBuffer, int nBytes); BOOL RecvPacket(SOCKET sock, char* pBuffer, int nBytes);
//SocketRoute.c ——阻塞模式下通用的函数实现
/*------------------------------------------------------------------- SOCKETROUTE.C——阻塞模式下使用的常用子程序 (c)by 浅墨浓香,2015.6.25 ---------------------------------------------------------------------*/ #include <windows.h> #include "Message.h" #include "SocketRoute.h" #pragma comment(lib,"Ws2_32.lib") //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // 在规定的时间内等待数据到达 // 输入:dwTime = 需要等待的时间(微秒) // 返回值:0 ——超时而返回 // SOCKET_ERROR ——出错而返回 // X(x>0) ——就绪的套接字数量 //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> int WaitData(SOCKET sock, DWORD dwTime) { FD_SET fds; TIMEVAL tv; fds.fd_count = 1; fds.fd_array[0] = sock; tv.tv_sec = 0; tv.tv_usec = dwTime; return select(0, &fds, NULL, NULL, &tv); } //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // 接收规定字节的数据,如果缓冲区中的数据不够则等待 // 返回:FALSE,连接中断或发生错误 // TRUE,成功 //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BOOL RecvData(SOCKET sock, char* pBuffer, int nBytes) { int nStartTime; int nRet,nRecv; nStartTime = GetTickCount(); nRecv = 0; while ((GetTickCount()-nStartTime)<10*1000) //查看是否超时 { nRet = WaitData(sock, 100 * 1000); //等待数据100ms if (SOCKET_ERROR == nRet) //连接错误 return FALSE; if (0 == nRet) //超时 break; do { //接收数据,直至收完指定的字节数 nRecv += recv(sock, pBuffer + nRecv, nBytes - nRecv, 0); if (nRecv == SOCKET_ERROR || nRecv == 0) return FALSE; if (nRecv == nBytes) return TRUE; } while (nRecv < nBytes); } return TRUE; } //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // 接收一个符合规范的数据包 // 参数: pBuffer用来接收数据的缓冲区 // nBytes 数据区最大的空间 // 返回: FALSE——失败 // TRUE ——成功 //注意:这里的nBytes不要指要接收的字节数,只是用来判断缓冲区是否只够大 //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BOOL RecvPacket(SOCKET sock, char* pBuffer, int nBytes) { MSGSTRUCT* pMsgStruct; int iRet; pMsgStruct = (MSGSTRUCT*)pBuffer; //接收数据包头部并检测数据是否正常 iRet = RecvData(sock, pBuffer, sizeof(MSGHEAD)); if (iRet) //如果成功接收数据 { if (pMsgStruct->MsgHead.cbSize <= sizeof(MSGHEAD) || pMsgStruct->MsgHead.cbSize > nBytes) return FALSE; //接收余下的数据 iRet = RecvData(sock, pBuffer + sizeof(MSGHEAD), pMsgStruct->MsgHead.cbSize - sizeof(MSGHEAD)); } return iRet; }
★服务器端文件★
/*----------------------------------------------------------------- TCPECHO.C —— 使用 TCP 协议的聊天室例子程序(服务器端) (c)浅墨浓香,2015.6.27 -----------------------------------------------------------------*/ #include <Windows.h> #include "resource.h" #include "Message.h" #include "MsgQueue.h" #include "SocketRoute.h" #pragma comment(lib,"Ws2_32.lib") //客户端会话信息 typedef struct _tagSession { TCHAR szUserName[12]; //用户名 int nMessageID; //己经下发的消息编号 DWORD dwLastTime; //链路最近一次活动的时间 }SESSION,*PSESSION; #define TCP_PORT 9999 //监听端口 #define F_STOP 1 extern int nSequence; extern CRITICAL_SECTION cs; TCHAR szAppName[] = TEXT("Tcp聊天室服务器"); TCHAR szSysInfo[] = TEXT("系统消息"); TCHAR szUserLogin[] = TEXT(" 进入聊天室!"); TCHAR szUserLogout[] = TEXT(" 退出了聊天室!"); int g_iThreadCount = 0; HWND g_hwnd = NULL; //对话框句柄 int g_dwFlag=0; //退出标志 int CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { DialogBox(hInstance, TEXT("ChatService"), NULL, DlgProc); return 0; } //检测链路的最后一次活动时间 //pBuffer ——指向要发送的链路检测的数据包 //pSession——指向上次的会话信息 //返回值:——TRUE(链路畅通) // ——FALSE(链路断开) // BOOL LinkCheck(SOCKET sock, char* pBuffer, SESSION* pSession) { DWORD dwTime; BOOL iRet = FALSE; PMSGSTRUCT pMsgStruct=(PMSGSTRUCT)pBuffer; //查看是否需要检测链路(30秒内没有数据通信,则发送链路检测包) dwTime = GetTickCount(); if ((dwTime - pSession->dwLastTime) < 30 * 1000) return TRUE; pSession->dwLastTime = dwTime; pMsgStruct->MsgHead.nCmdID = CMD_CHECK_LINK; pMsgStruct->MsgHead.cbSize = sizeof(MSGHEAD); //发送检测链路的数据包(只需发送数据包头部就可以) return (SOCKET_ERROR != send(sock, pBuffer, pMsgStruct->MsgHead.cbSize, 0)); } //循环取消息队列中的聊天语句并发送到客户端,直到全部消息发送完毕 //pBuffer ——指向从消息队列中取出的消息的缓冲区,该消息将被发送到客户端 //pSession——指向上次的会话信息 //返回值:TRUE ——正常 // FALSE——出现错误 BOOL SendMsgQueue(SOCKET sock, char* pBuffer, PSESSION pSession) { int iRet; int nMsgID; PMSGSTRUCT pMsgStruct = (PMSGSTRUCT)pBuffer; nMsgID = pSession->nMessageID+1; //mMessageID为会话最后一次得到的消条,取它的下一条消条 while (!(g_dwFlag & F_STOP)) { iRet = GetMsgFromQueue(nMsgID++,pMsgStruct->MsgDown.szSender, pMsgStruct->MsgDown.szContent); if (iRet == 0) break; pSession->nMessageID = iRet; pMsgStruct->MsgDown.cbSizeConent = (lstrlen(pMsgStruct->MsgDown.szContent) + 1)*sizeof(TCHAR); pMsgStruct->MsgHead.cbSize = sizeof(MSGHEAD)+OFFSET(MSGDOWN,szContent) + pMsgStruct->MsgDown.cbSizeConent; pMsgStruct->MsgHead.nCmdID = CMD_MSG_DOWN; iRet = send(sock, (char*)pMsgStruct, pMsgStruct->MsgHead.cbSize, 0); if (SOCKET_ERROR == iRet) return FALSE; pSession->dwLastTime = GetTickCount(); //当多人聊天时,队列里的消息会急剧增加,为了防止发送速度较慢 //队列里的消息会越积越多,从而导致没有机会退出循环去接收来自本SOCKET的 //(即本线程所服务的客户端)消息,所以在每次发送数据后,通过WaitData去 //一下,是否有数据到达,如果有,则退出发送消息过程,优先去处理要接收的数据 iRet = WaitData(sock,0); if (SOCKET_ERROR == iRet) //如果链路断了 return FALSE; if (iRet>0) //如果有要接收的数据,则退出,优先去处理 break; } return TRUE; } void CloseSocket(SOCKET sock) { closesocket(sock); sock = 0; SetDlgItemInt(g_hwnd, IDC_COUNT, --g_iThreadCount, FALSE); } //通信服务线程,每个客户端登录的连接将产生一个线程 DWORD WINAPI ServiceThread(PVOID pVoid) { SOCKET SrvSocket = (SOCKET)pVoid; PMSGSTRUCT pMsgStruct; SESSION session; char szBuffer[512]; int iRet; pMsgStruct = (PMSGSTRUCT)szBuffer;//让pMsgStruct指向缓冲区 //连接的客户数量加1,并显示出来 ++g_iThreadCount; SetDlgItemInt(g_hwnd, IDC_COUNT, g_iThreadCount, FALSE); memset(&session, 0, sizeof(SESSION)); session.nMessageID = nSequence; /********************************************************************* 用户名和密码检测,为了简化程序,现在可以使用任意用户名和密码 *********************************************************************/ //接收用户输入的用户名和密码。 //客户端会发送一个MSGLOGIN数据包,命令代码为CMD_LOGIN,这是服务 //器接受到客户端的第一个数据包。如果不是,即关闭连接。 if (!RecvPacket(SrvSocket, szBuffer, sizeof(MSGHEAD)+sizeof(MSGLOGIN))) //接收失败 { CloseSocket(SrvSocket); return FALSE; } //判断是否是登录数据包 if (pMsgStruct->MsgHead.nCmdID != CMD_LOGIN) { CloseSocket(SrvSocket); return FALSE; } StringCchCopy(session.szUserName, lstrlen(pMsgStruct->Login.szUserName) + 1, pMsgStruct->Login.szUserName); pMsgStruct->LoginResp.dbResult = 1; //省略了验证用户名和密码,任何的用户名和密码都是可以通过的 //此处为1,说明验证通过 pMsgStruct->MsgHead.nCmdID = CMD_LOGIN_RESP; pMsgStruct->MsgHead.cbSize = sizeof(MSGHEAD)+sizeof(MSGLOGINRESP); iRet = send(SrvSocket, szBuffer, pMsgStruct->MsgHead.cbSize,0); if (SOCKET_ERROR == iRet) { CloseSocket(SrvSocket); return FALSE; } /********************************************************************* 广播:xxx 进入了聊天室 *********************************************************************/ StringCchCopy((TCHAR*)szBuffer, lstrlen(session.szUserName) + 1, session.szUserName); StringCchCat((TCHAR*)szBuffer, (lstrlen((TCHAR*)szBuffer) + lstrlen(szUserLogin) + 1), szUserLogin); InsertMsgQueue(szSysInfo,(TCHAR*)szBuffer); session.dwLastTime = GetTickCount(); //循环处理消息 while (!(g_dwFlag & F_STOP)) { //将消息队列中的聊天记录发送给客户端 if (!SendMsgQueue(SrvSocket, szBuffer, &session)) break; //注意检测链路放在接收之前,而不是SendMsgQueue之前,为什么? //因为检测链路是通过发送数据包来实现的,而在SendMsgQueue本身就可以 //发送数据包,返回SOCKET_ERROR就说明链路己断。但接收数据不同,如果 //在接收之前,网络异常中断,这时系统并没设置socket的状态没为断开,会以 //为对方一直没发数据过来,而处于等待.所以这时调用recv或select并不会返回 //SOCKET_ERROR,只有通过主动发送数据检测探测,当多次send得不到回应时 //系统才会将socket置为断开,以后的全部操作才会失败。 pMsgStruct->MsgHead.nCmdID = CMD_CHECK_LINK; pMsgStruct->MsgHead.cbSize = sizeof(MSGHEAD); if ((SOCKET_ERROR == LinkCheck(SrvSocket, (char*)pMsgStruct, &session)) || (g_dwFlag & F_STOP)) break; //等待200ms,如果没有接收到数据,则循环 iRet = WaitData(SrvSocket, 200 * 1000); if (SOCKET_ERROR == iRet) break; if (0==iRet) continue; //注意,这里接收的数据只表明是个完整的数据包。可能是聊天语句的数据包,也可能是 //是退出命令的数据包(本例没有实现这个,因为客户端退出里,链路会断开,会被LinkCheck检测到) iRet = RecvPacket(SrvSocket, szBuffer, sizeof(szBuffer)); if (!iRet) break; session.dwLastTime = GetTickCount(); pMsgStruct = (PMSGSTRUCT)szBuffer; if (pMsgStruct->MsgHead.nCmdID == CMD_MSG_UP) { InsertMsgQueue(session.szUserName, (TCHAR*)pMsgStruct->MsgUp.szConetent); } } /********************************************************************* 广播:xxx 退出了聊天室 *********************************************************************/ StringCchCopy((TCHAR*)szBuffer, lstrlen(session.szUserName) + 1, session.szUserName); StringCchCat((TCHAR*)szBuffer, (lstrlen((TCHAR*)szBuffer) + lstrlen(szUserLogout) + 1), szUserLogout); InsertMsgQueue(szSysInfo, (TCHAR*)szBuffer); /********************************************************************* 关闭socket *********************************************************************/ CloseSocket(SrvSocket); return TRUE; } //监听线程 DWORD WINAPI ListenThread(PVOID pVoid) { SOCKET ServiceSocket,ListenSocket; SOCKADDR_IN sa; HANDLE hThread; TCHAR szErrorBind[] = TEXT("无法绑定到TCP端口9999,请检查是否有其它程序在使用!"); //创建socket ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); *(SOCKET*)pVoid = ListenSocket; //绑定socket memset(&sa, 0, sizeof(SOCKADDR_IN)); sa.sin_port = htons(TCP_PORT); sa.sin_family = AF_INET; sa.sin_addr.S_un.S_addr = INADDR_ANY; if (bind(ListenSocket, (PSOCKADDR)&sa, sizeof(SOCKADDR_IN))) //返回0表示无错误,是成功的。 { MessageBox(g_hwnd, szErrorBind, szAppName, MB_OK | MB_ICONSTOP); closesocket(ListenSocket); return FALSE; //ExitProcess(0); } //开始监听,等待连接并为每个连接创建一个新的服务线程 listen(ListenSocket, 5); while (TRUE) { ServiceSocket = accept(ListenSocket, NULL, 0); if (ServiceSocket == INVALID_SOCKET) break; hThread = CreateThread(NULL, 0, ServiceThread, (LPVOID)ServiceSocket, 0, 0); CloseHandle(hThread);//线程是内核对象,关闭表示不需用操作了(如唤醒、挂机)。 } closesocket(ListenSocket); return TRUE; } int CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { WSADATA WSAData; static SOCKET ListenSocket; static HANDLE hListenThread; switch (message) { case WM_INITDIALOG: g_hwnd = hwnd; //初始化临界区对象; InitializeCriticalSection(&cs); //载入WS2_32.DLL动态链0x0002:MAKEWORD(2,0) WSAStartup(MAKEWORD(2, 0), &WSAData); //动态库的信息返回到WSAdata变量中 //创建监听线程 hListenThread = CreateThread(NULL, 0, ListenThread, (LPVOID)&ListenSocket, 0, 0); CloseHandle(hListenThread); //只是关闭了一个线程句柄对象,表示我不再使用该句柄,即不对这个句柄对 //应的线程做任何干预了(如挂起或唤醒)。并没有结束线程。 return TRUE; case WM_CLOSE: closesocket(ListenSocket); //当未有客户端连接时,该socket在线程中创建,且未退出线程。 //所以要在这里监听socket,此时会将accept返回失败,监听线程退出。 g_dwFlag |= F_STOP; //设置退出标志,以便让服务线程中止 while (g_iThreadCount > 0); //等待服务线程关闭 WSACleanup(); DeleteCriticalSection(&cs); EndDialog(hwnd, 0); return TRUE; } return FALSE; }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 ChatService.rc 使用 // #define IDC_COUNT 1001 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1002 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//ChatService.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h " END 2 TEXTINCLUDE BEGIN "#include ""winres.h"" " "