zoukankan      html  css  js  c++  java
  • 第23章 尝试互联网(2)

    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;
    }

    ★服务器端文件★

     //ChatService.c   ——服务器端主程序
    /*-----------------------------------------------------------------
       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""
    "
        ""
    END
    
    3 TEXTINCLUDE 
    BEGIN
        "
    "
        ""
    END
    
    #endif    // APSTUDIO_INVOKED
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Dialog
    //
    
    CHATSERVICE DIALOGEX 0, 0, 165, 36
    STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
    CAPTION "TCP聊天室服务器"
    FONT 8, "MS Shell Dlg", 400, 0, 0x1
    BEGIN
        LTEXT           "当前连线的客户端数量:",IDC_STATIC,15,15,89,8
        LTEXT           "0",IDC_COUNT,109,15,44,8
    END
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // DESIGNINFO
    //
    
    #ifdef APSTUDIO_INVOKED
    GUIDELINES DESIGNINFO
    BEGIN
        "TCPECHO", DIALOG
        BEGIN
            LEFTMARGIN, 7
            RIGHTMARGIN, 158
            TOPMARGIN, 7
            BOTTOMMARGIN, 29
        END
    END
    #endif    // APSTUDIO_INVOKED
    
    #endif    // 中文(简体,中国) resources
    /////////////////////////////////////////////////////////////////////////////
    
    
    
    #ifndef APSTUDIO_INVOKED
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 3 resource.
    //
    
    
    /////////////////////////////////////////////////////////////////////////////
    #endif    // not APSTUDIO_INVOKED
     ★客户端文件★
     //ChatClient.c
    /*--------------------------------------------------------------------
     CHATCLIENT.C —— 使用 TCP 协议的聊天室例子程序(客户端)
    ; 本例子使用阻塞模式socket    (c)浅墨浓香,2015.6.27
    --------------------------------------------------------------------*/
    #include <windows.h>
    #include "resource.h"
    #include <strsafe.h>
    #include "..\ChapService\SocketRoute.h"
    #include "..\ChapService\Message.h"
    
    #pragma  comment(lib,"WS2_32.lib")
    
    #define TCP_PORT      9999
    
    TCHAR   szAppName[] = TEXT("ChatClient");
    
    typedef struct _tagSOCKPARAMS
    {
        TCHAR   szUserName[12];
        TCHAR   szPassword[12];
        TCHAR   szText[256];
        char    szServer[16];
        HWND    hWinMain;
        SOCKET  sock;
        int     nLastTime;
    
    }SOCKPARAMS,*PSOCKPARAMS;
    
    BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
    DWORD WINAPI WorkThread(LPVOID lpParameter);
    
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int nCmdShow)
    {
        if (-1==DialogBox(hInstance, TEXT("ChatClient"), NULL, DlgProc))
        {
            MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_OK | MB_ICONEXCLAMATION);
        }
        return 0;
    }
    
    void EnableWindows(HWND hwnd, BOOL bEnable)
    {
        EnableWindow(GetDlgItem(hwnd,IDC_SERVER), bEnable);
        EnableWindow(GetDlgItem(hwnd, IDC_USER),  bEnable);
        EnableWindow(GetDlgItem(hwnd, IDC_PASS),  bEnable);
        EnableWindow(GetDlgItem(hwnd, IDC_LOGIN), bEnable);
    }
    
    DWORD WINAPI WorkThread(LPVOID lpParameter)
    {
        SOCKPARAMS* pSockParams = (PSOCKPARAMS)lpParameter;
        TCHAR szErrIP[] = TEXT("无效的服务器IP地址!");
        TCHAR szErrConnect[] = TEXT("无法连接到服务器!");
        TCHAR szErrLogin[] = TEXT("无法登录到服务器,请检查用户名密码!");
        TCHAR szSpar[] = TEXT(" : ");
    
        SOCKET sockWork;
        SOCKADDR_IN sa;
        char szBuffer[512];
        PMSGSTRUCT  pMsgStruct;
        int iRet;
    
        pMsgStruct = (PMSGSTRUCT)szBuffer;
        //将编辑框(服务器IP、用户名、密码)及登录按钮变灰色
        EnableWindows(pSockParams->hWinMain, FALSE);
        
        /*********************************************************************
          创建 socket
        *********************************************************************/
        memset(&sa, 0, sizeof(SOCKADDR_IN));
    
        if (INADDR_NONE == inet_addr(pSockParams->szServer))
        {
            MessageBox(pSockParams->hWinMain, szErrIP,szAppName,MB_OK | MB_ICONSTOP);
            EnableWindows(pSockParams->hWinMain, TRUE);
            return 0;
        }
        sa.sin_family = AF_INET;
        sa.sin_addr.S_un.S_addr = inet_addr(pSockParams->szServer);
        sa.sin_port = htons(TCP_PORT);
        
        sockWork = socket(AF_INET, SOCK_STREAM, 0);
        pSockParams->sock = sockWork;
    
        /*********************************************************************
          连接到服务器
        *********************************************************************/
        if (SOCKET_ERROR == connect(sockWork, (PSOCKADDR)&sa, sizeof(SOCKADDR_IN)))
        {
            MessageBox(pSockParams->hWinMain, szErrConnect, szAppName, MB_OK | MB_ICONSTOP);
            EnableWindows(pSockParams->hWinMain, TRUE);
            closesocket(pSockParams->sock);
            pSockParams->sock = 0;
            return 0;
        }
    
        /*********************************************************************
          登录到服务器
        *********************************************************************/
        StringCchCopy(pMsgStruct->Login.szUserName, lstrlen(pSockParams->szUserName)+1, pSockParams->szUserName);
        StringCchCopy(pMsgStruct->Login.szPassword, lstrlen(pSockParams->szPassword) + 1, pSockParams->szPassword);
        pMsgStruct->MsgHead.nCmdID = CMD_LOGIN;
        pMsgStruct->MsgHead.cbSize = sizeof(MSGHEAD)+sizeof(MSGLOGIN);
    
        //发送登录命令
        iRet = send(sockWork, szBuffer, pMsgStruct->MsgHead.cbSize, 0);
        if (SOCKET_ERROR == iRet)
        {
            MessageBox(pSockParams->hWinMain, szErrLogin, szAppName, MB_OK | MB_ICONSTOP);
            EnableWindows(pSockParams->hWinMain, TRUE);
            closesocket(sockWork);
            pSockParams->sock = 0;
            return 0;
        }
        
        //等待服务器验证结果
        iRet = RecvPacket(sockWork, szBuffer, sizeof(MSGHEAD)+sizeof(MSGLOGINRESP));
        if ((!iRet) || (pMsgStruct->LoginResp.dbResult !=1)) //验证失败
        {
            MessageBox(pSockParams->hWinMain, szErrLogin, szAppName, MB_OK | MB_ICONSTOP);
            EnableWindows(pSockParams->hWinMain, TRUE);
            closesocket(sockWork);
            pSockParams->sock = 0;
            return 0;
        }
    
        //登录成功
        EnableWindow(GetDlgItem(pSockParams->hWinMain, IDC_LOGOUT), TRUE);
        EnableWindow(GetDlgItem(pSockParams->hWinMain, IDC_TEXT), TRUE);
        pSockParams->nLastTime = GetTickCount();
    
        /*********************************************************************
         循环接收消息
        *********************************************************************/
        while (pSockParams->sock)
        {
            //服务器端每隔30秒会发送一个链路检测包过来,如果客户端超过60秒没接收到
            //表示链路己断,则退出。
            if ((GetTickCount() - pSockParams->nLastTime) >=60*1000)  //超过60秒,则退出
               break;
    
            iRet = WaitData(sockWork, 200 * 1000);//等待200ms
            if (SOCKET_ERROR == iRet)
              break;
    
            if (iRet)
            {
                if (SOCKET_ERROR == RecvPacket(sockWork, szBuffer, sizeof(MSGSTRUCT)))
                   break;
    
                if (pMsgStruct->MsgHead.nCmdID == CMD_MSG_DOWN)
                {
                    StringCbCopy((TCHAR*)szBuffer, sizeof(pMsgStruct->MsgDown.szSender), pMsgStruct->MsgDown.szSender);
                    StringCchCat((TCHAR*)szBuffer,lstrlen((TCHAR*)szBuffer)+lstrlen(szSpar)+1, szSpar);
                    StringCchCat((TCHAR*)szBuffer, lstrlen((TCHAR*)szBuffer) + lstrlen(pMsgStruct->MsgDown.szContent) + 1,
                                                      pMsgStruct->MsgDown.szContent);
                    SendDlgItemMessage(pSockParams->hWinMain, IDC_INFO, LB_INSERTSTRING, 0, (LPARAM)szBuffer);
                }
    
                pSockParams->nLastTime = GetTickCount();
    
            }
    
        }
        //启用编辑框(服务器IP、用户名、密码)及登录按钮
        EnableWindows(pSockParams->hWinMain, TRUE);
        closesocket(sockWork);
        pSockParams->sock =0;
        return 0;
    }
    BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        static SOCKPARAMS sockParam;
        MSGSTRUCT msgStruct;
        RECT rect;
        WSADATA wsa;
        BOOL  bEnable;
        HANDLE hWorkThread;
        int iRet;
    
        switch (message)
        {
        case WM_COMMAND:
            switch (LOWORD(wParam))
            {
    
            case IDC_SERVER:
            case IDC_USER:
            case IDC_PASS:
                GetDlgItemTextA(hwnd, IDC_SERVER, sockParam.szServer, sizeof(sockParam.szServer));
                GetDlgItemText(hwnd, IDC_USER,   sockParam.szUserName, sizeof(sockParam.szUserName));
                GetDlgItemText(hwnd, IDC_PASS,   sockParam.szPassword, sizeof(sockParam.szPassword));
                bEnable = sockParam.szServer[0] && sockParam.szUserName[0] && sockParam.szPassword[0] && (sockParam.sock==0);
                EnableWindow(GetDlgItem(hwnd, IDC_LOGIN), bEnable);
                return TRUE;
    
            //登录成功后,输入聊天语句后才能激活“发送”按钮
            case IDC_TEXT: 
                GetDlgItemText(hwnd, IDC_TEXT, sockParam.szText, sizeof(sockParam.szText));
                bEnable = (lstrlen(sockParam.szText) > 0) && sockParam.sock;
                EnableWindow(GetDlgItem(hwnd, IDOK), bEnable);
    
                return TRUE;
    
            case IDC_LOGIN:
                hWorkThread = CreateThread(NULL, 0, WorkThread, &sockParam, 0, 0);
                CloseHandle(hWorkThread);
                return TRUE;
    
            case IDC_LOGOUT:
                if (sockParam.sock)
                    closesocket(sockParam.sock);
                sockParam.sock = 0;
                return TRUE;
    
            case IDOK:    
                StringCchCopy((TCHAR*)&msgStruct.MsgUp.szConetent, lstrlen(sockParam.szText)+1, sockParam.szText);
                msgStruct.MsgUp.cbSizeConent = sizeof(TCHAR)*(lstrlen(sockParam.szText) + 1);
                msgStruct.MsgHead.nCmdID = CMD_MSG_UP;
                msgStruct.MsgHead.cbSize = sizeof(MSGHEAD)+sizeof(msgStruct.MsgUp.cbSizeConent) + msgStruct.MsgUp.cbSizeConent;
    
                iRet = send(sockParam.sock, (char*)&msgStruct, msgStruct.MsgHead.cbSize, 0);
                if (SOCKET_ERROR == iRet)
                {
                    if (sockParam.sock)
                        closesocket(sockParam.sock);
                    sockParam.sock = 0;
                    return TRUE;
                }
    
                sockParam.nLastTime = GetTickCount();
                SetDlgItemText(hwnd, IDC_TEXT, NULL);
                SetFocus(GetDlgItem(hwnd, IDC_TEXT));
                return TRUE;
    
            }
            break;
    
        case WM_INITDIALOG:
            sockParam.hWinMain = hwnd;
            GetWindowRect(hwnd, &rect);
            SetWindowPos(hwnd, NULL, (GetSystemMetrics(SM_CXSCREEN) - rect.right + rect.left) / 2,
                (GetSystemMetrics(SM_CYSCREEN) - rect.bottom + rect.top) / 2,
                rect.right - rect.left, rect.bottom - rect.top, SWP_SHOWWINDOW);
    
            SendDlgItemMessage(hwnd, IDC_SERVER, EM_SETLIMITTEXT, 15, 0);
            SendDlgItemMessage(hwnd, IDC_USER, EM_SETLIMITTEXT, 11, 0);
            SendDlgItemMessage(hwnd, IDC_PASS, EM_SETLIMITTEXT, 11, 0);
            SendDlgItemMessage(hwnd, IDC_TEXT, EM_SETLIMITTEXT, 250, 0);
    
            SetDlgItemText(hwnd, IDC_SERVER, TEXT("127.0.0.1"));
            SetDlgItemText(hwnd, IDC_USER, TEXT("SantaClaus"));
            SetDlgItemText(hwnd, IDC_PASS, TEXT("123456"));
    
            WSAStartup(0x0002, &wsa);
        
            return TRUE;
    
        case WM_CLOSE:
            WSACleanup();
            EndDialog(hwnd, 0);
            return TRUE;
        }
        return FALSE;
    }

    //resource.h

    //{{NO_DEPENDENCIES}}
    // Microsoft Visual C++ 生成的包含文件。
    // 供 ChapClient.rc 使用
    //
    #define IDC_SERVER                      1001
    #define IDC_USER                        1002
    #define IDC_PASS                        1003
    #define IDC_LOGIN                       1004
    #define IDC_LOGOUT                      1005
    #define IDC_INFO                        1006
    #define IDC_TEXT                        1007
    
    // 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         1009
    #define _APS_NEXT_SYMED_VALUE           101
    #endif
    #endif

    //CharClient.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""
    "
        ""
    END
    
    3 TEXTINCLUDE 
    BEGIN
        "
    "
        ""
    END
    
    #endif    // APSTUDIO_INVOKED
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Dialog
    //
    
    CHATCLIENT DIALOGEX 0, 0, 249, 176
    STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
    CAPTION "Tcp聊天—客户端"
    FONT 8, "MS Shell Dlg", 400, 0, 0x1
    BEGIN
        LTEXT           "服务器IP地址",IDC_STATIC,15,18,48,8
        EDITTEXT        IDC_SERVER,64,16,113,14
        LTEXT           "用户名",IDC_STATIC,15,40,25,8
        LTEXT           "密码",IDC_STATIC,107,40,17,8
        EDITTEXT        IDC_USER,43,37,58,14,ES_AUTOHSCROLL
        EDITTEXT        IDC_PASS,127,37,50,14,ES_AUTOHSCROLL
        PUSHBUTTON      "登录(&L)",IDC_LOGIN,185,16,50,14,WS_DISABLED
        PUSHBUTTON      "注销(&X)",IDC_LOGOUT,185,37,50,14,WS_DISABLED
        LISTBOX         IDC_INFO,14,55,220,97,LBS_SORT | WS_VSCROLL
        LTEXT           "输入",IDC_STATIC,14,154,17,8
        EDITTEXT        IDC_TEXT,33,151,138,14,ES_AUTOHSCROLL | WS_DISABLED
        DEFPUSHBUTTON   "发送(&S)",IDOK,180,151,50,14,WS_DISABLED
    END
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // DESIGNINFO
    //
    
    #ifdef APSTUDIO_INVOKED
    GUIDELINES DESIGNINFO
    BEGIN
        "CHATCLIENT", DIALOG
        BEGIN
            LEFTMARGIN, 7
            RIGHTMARGIN, 240
            TOPMARGIN, 7
            BOTTOMMARGIN, 169
        END
    END
    #endif    // APSTUDIO_INVOKED
    
    #endif    // 中文(简体,中国) resources
    /////////////////////////////////////////////////////////////////////////////
    
    
    
    #ifndef APSTUDIO_INVOKED
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 3 resource.
    //
    
    
    /////////////////////////////////////////////////////////////////////////////
    #endif    // not APSTUDIO_INVOKED

     

     

  • 相关阅读:
    SSL测试百度握手协议,下载百度CA证书
    新浪微博推荐之股神一探究竟,是大神?
    IPQ4019开发板使用 openWRT开发(第2篇)未完成!!!
    IPQ4019开发板使用硬件和启动(第一篇)
    2020-01-17 学习笔记(1)
    Kube-DNS搭建(1.4版本)
    Kubernetes持久化存储2——探究实验
    Kubernetes持久化存储1——示例
    Kubernetes部分Volume类型介绍及yaml示例
    Kubernetes外挂配置管理—ConfigMap介绍
  • 原文地址:https://www.cnblogs.com/5iedu/p/4715291.html
Copyright © 2011-2022 走看看