第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

 

 

原文地址:https://www.cnblogs.com/5iedu/p/4715291.html