第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\0"
END

2 TEXTINCLUDE
BEGIN
    "#include ""winres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE
BEGIN
    "\r\n"
    "\0"
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\0"
END

2 TEXTINCLUDE
BEGIN
    "#include ""winres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE
BEGIN
    "\r\n"
    "\0"
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
时间: 2024-10-14 04:58:06

第23章 尝试互联网(2)的相关文章

第23章 尝试互联网(3)

23.3.2 以非阻塞方式工作的TCP聊天室客户端 (1)WSAAsyncSelect函数——设置非阻塞模式 参数 含义 SOCKET s 套接字句柄 HWND hWnd 套接字的通知消息将被发往的hwnd的窗口过程 unsigned int wMsg 自定义通知消息的编号,如 #define WM_SOCKET WM_USER+XXX中任取一个. long lEvent 指定哪些通知码需要发送,可以是以下通知知的组合 ①FD_READ:套接字收到对端发送过来的数据,表明可以去读套接字了. ②F

第23章 尝试互联网(1)

23.1 Windows Socket接口简介 (1)TCP/IP模型 ①TCP/IP的核心协议运行于传输层和Internet层,主要包括TCP.UDP和IP协议,而TCP协议和UDP协议是以IP协议为基础而封装的.这两种协议提供了不同方式的数据通信服务. ②IP协议比喻为道路,则下一层的网络访问层上的协议相当于不同的铺路材料,上面的TCP和UPD协议相当于路上跑的不同类型的车辆,再上层应用层的协议相当于车上的丰富多彩的货物.他们都是以TCP.UDP为载体的. (2)WinSock动态库 ①早期

第23章 尝试互联网(4)【全书完】

23.4 WinInet和FTP (1)WinInet接口(含HTTP.FTP)及FTP函数层次关系 (2)Ftp函数介绍 ①InternetOpen——初始化,它告诉 Internet DLL 初始化内部数据结构并准备接收应用程序之后的其他调用. 参数 含义 LPCTSTR lpszAgent 调用WinInet函数的应用程序名字,在HTTP协议中作为用户代理项 DWORD   dwAccessType 访问要求类型: INTERNET_OPEN_TYPE_DIRECT:解析所有本地主机,使用

Spring Framework Reference Documentation 3.2.8.RELEASE 第23章中文翻译

23. JMS (Java Message Service) [中文翻译 by [email protected]] 23.1 介绍 Spring提供了一个JSM集成框架,简化了JMS API的使用.这点很像Spring对JDBC的集成. JMS大致提供生产消息和消费消息两类功能.JmsTemplate类用来生产消息和同步接收消息[译注:接收消息也就是消费消息].为了异步接收消息(异步接收消息类似于JavaEE的消息驱动Bean(Message-Driven Bean,MDB),Spring提供

4.26日第14次作业,23章项目整体绩效评估,24-32章信息安全相关知识

一.23章:项目整体绩效评估 1.三E审计是什么的合称?(记)P524 答:三E审计是经济审计.效率审计和效果审计的合称,因为三者的第一个英文字母均为E,顾称为三E审计. 2.霍尔三维结构是从哪三个方面考察系统工程的工作过程的?P527-528 答:霍尔三维结构是霍尔(A Hall)提出的关于系统方法论的结构,它从逻辑.时间.知识三方面考察系统工程的工作过程. 3.投资回收期的公式?(记,并理解)P533答:投资回收期的公式:(累计净现金流量出现正值的年份-1) + (上年累计净现金流量值的绝对

JavaScript高级程序设计(第三版)学习笔记20、21、23章

第20章,JSON JSON(JavaScript Object Notation,JavaScript对象表示法),是JavaScript的一个严格的子集. JSON可表示一下三种类型值: 简单值:字符串,数值,布尔值,null,不支持js特殊值:undefined 对象:一组无序的键值对 数组:一组有序的值的列表 不支持变量,函数或对象实例 注:JSON的字符串必须使用双引号,这是与JavaScript字符串最大的区别 对象 { "name":"Nicholas"

第23章 CSS边框图片效果

本章学习日后开发使用参考一下内容 https://www.qianduan.net/css3border-image-bian-kuang-tu-xiang-xiang-jie/ 或W3C 或者百度 未排版的PDF转WORD(不想排版了) 第 23章 CSS3边框图片效果学习要点:1.属性初探2.属性解释3.简写和版本 本章主要探讨 HTML5中 CSS3中边框图片背景的效果,通过这个新属性让边框更加的丰富多彩.一.属性解释CSS3提供了一个新的属性集合,用这几个属性可以嵌入图片形式的边框.这样

Lua_第23章 C API 纵览

第23章 C  API 纵览 Lua是一个嵌入式的语言,意味着 Lua 不仅可以是一个独立运行的程序包也可以是一个用来嵌入其他应用的程序库.你可能觉得奇怪:如果 Lua 不只是独立的程序,为什么到目前为止贯穿整本书我们都是在使用 Lua 独立程序呢? 这个问题的答案在于 Lua 解释器(可执行的 lua).Lua解释器是一个使用 Lua 标准库实现的独立的解释器,它是一 个很小的应用(总共不超过500 行的代码).解释器负责程序和使用者的接口:从使用者那里获取文件或者字符串,并传给 Lua 标准

第23章、OnFocuChangeListener焦点事件(从零开始学Android)

在Android App应用中,OnFocuChangeListener焦点事件是必不可少的,我们在上一章的基础上来学习一下如何实现. 基本知识点:OnFocuChangeListener事件 一.界面 打开“res/layout/activity_main.xml”文件. 1.分别从工具栏向activity拖出2个编辑框EditText.控件来自Form Widgets. 2.打开activity_main.xml文件. [html] view plaincopy <LinearLayout