首先本文默认读者了解基本的c++语法

winsockt

首先从最简单的通信程序开始,下面参考Windows手册编写一个基础程序

开始

按照手册的介绍,通信应用程序一般有两种,分别是服务端和客户端,各自负责不同的功能。一般来说,服务端负责接收并处理来自多个客户端的消息,所以服务端的代码会相对复杂。客户端则只需要向服务端发送消息,并能够接受来自服务端的消息即可。
下面是创建服务端和客户端的常规模型
服务端

  1. 初始化 Winsock。
  2. 创建套接字。
  3. 绑定套接字。
  4. 侦听客户端的套接字。
  5. 接受来自客户端的连接。
  6. 接收和发送数据。
  7. 断开连接。
    客户端
  8. 初始化 Winsock。
  9. 创建套接字。
  10. 连接到该服务器。
  11. 发送和接收数据。
  12. 断开连接。

了解了架子怎么搭我们接下来就可以开始编写程序了

这两种程序都属于socket程序,所以我们需要包含的头文件和lib库都是相同的。
这里我们使用的socket版本是winsocket2

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

int main() {
  return 0;
}

而同样,创建服务端和客户端的第一步都需要初始化winsock,初始化的流程也很简单,创建一个winsock对象然后调用相应的初始化函数即可

//定义一个wsadata对象用于存储套接字的相关信息
WSADATA wsaData;
int iResult;
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);、
//根据返回结果判断初始化错误原因
if (iResult != 0) {
    printf("WSAStartup failed: %d\n", iResult);
    return 1;
}

这部分不理解也没有关系,只是做了一些初始化操作,后面的部分客户端和服务端略有不同,下面分别列出

客户端

初始化完成之后,就要进入我们的正戏了,建立一个套接字对象,后面我们就用它来完成我们的通信过程了
创建套接字对象时首先我们需要一个 addrinfo 对象。顾名思义,这里提到的addrinfo结构是一个用于存储ip地址等信息的结构,用于建立网络连接。这个结构定义如下

typedef struct addrinfo {
  int             ai_flags;
  int             ai_family;
  int             ai_socktype;
  int             ai_protocol;
  size_t          ai_addrlen;
  char            *ai_canonname;
  struct sockaddr *ai_addr;
  struct addrinfo *ai_next;
} ADDRINFOA, *PADDRINFOA;

我们这里关注其中的三个参数 ai_familyai_socktype 、和ai_protocol

ai_family 是一个 int型的参数,表示地址系列。通常用于设定使用IPv4还是IPv6的网路地址格式,相关宏定义已经在winsock2.h文件中定义,我们可以直接使用AF_INET表示IPv4格式,AF_INET6表示IPv6格式,AF_UNSPEC表示不指定格式。

ai_socktype 也是一个int型的参数,表示套接字类型,这里直接将其值设定为SOCK_STREAM即可

ai_protocol int类型的参数,表示协议类型,可以通过这个参数设定通信使用TCP还是UDP协议,在这个程序中我们使用TCP协议所以将其设定为ai_protocol

struct addrinfo *result = NULL,
                *ptr = NULL,
                hints;

ZeroMemory( &hints, sizeof(hints) );
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;

然后我们使用getaddrinfo函数继续完成初始化操作,这里通过命令行第一个参数获取服务器的ip地址,并定义了默认端口为27015,当然也可以直接在这两个参数的位置放字符串变量来定义需要链接的服务器地址和端口号

#define DEFAULT_PORT "27015"

// 解析服务器地址和端口
iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
    printf("getaddrinfo failed: %d\n", iResult);
    WSACleanup();
    return 1;
}
//创建socket对象
SOCKET ConnectSocket = INVALID_SOCKET;

// 尝试连接到调用getaddrinfo返回的第一个地址
ptr=result;

// 创建用于连接到服务器的套接字
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
    ptr->ai_protocol);

最初的地址信息等已经初始化完毕,现在就可以调用connect函数和服务器进行链接了

iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
    closesocket(ConnectSocket);
    ConnectSocket = INVALID_SOCKET;
}

freeaddrinfo(result);

if (ConnectSocket == INVALID_SOCKET) {
    printf("Unable to connect to server!\n");
    WSACleanup();
    return 1;
}

连接成功后即可和服务器进行通信,通信过程主要使用两个函数,send和recv。注意,这两个函数在阻塞模式下会一直等待缓冲区中出现数据,否则程序会一直暂停。

send和recv函数需要的参数也十分好理解

int WSAAPI send(
  [in] SOCKET     s,
  [in] const char *buf,
  [in] int        len,
  [in] int        flags
);

第一个参数是我们和服务器建立的socket,第二个参数是需要发送的字符串,第三个则是字符串长度,最后一个参数直接传递0即可。recv和send基本相同。

#define DEFAULT_BUFLEN 512

int recvbuflen = DEFAULT_BUFLEN;

const char *sendbuf = "this is a test";
char recvbuf[DEFAULT_BUFLEN];

int iResult;

// 发送数据
iResult = send(ConnectSocket, sendbuf, (int) strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
    printf("send failed: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}

printf("Bytes Sent: %ld\n", iResult);

//停止发送数据,执行此段后程序只能接受来自服务端的数据,不再可以进行发送
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
    printf("shutdown failed: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}

// 从服务端接收信息
do {
    iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
    if (iResult > 0)
        printf("Bytes received: %d\n", iResult);
    else if (iResult == 0)
        printf("Connection closed\n");
    else
        printf("recv failed: %d\n", WSAGetLastError());
} while (iResult > 0);

到这里客户端就基本完成了,最后关闭连接即可

closesocket(ConnectSocket);
WSACleanup();
return 0;

下面是完整的代码:

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(int argc, char **argv) 
{
    WSADATA wsaData;
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo *result = NULL,
                    *ptr = NULL,
                    hints;
    const char *sendbuf = "this is a test";
    char recvbuf[DEFAULT_BUFLEN];
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;

    // Validate the parameters
    if (argc != 2) {
        printf("usage: %s server-name\n", argv[0]);
        return 1;
    }

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // Resolve the server address and port
    iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Attempt to connect to an address until one succeeds
    for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            return 1;
        }

        // Connect to server.
        iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 1;
    }

    // Send an initial buffer
    iResult = send( ConnectSocket, sendbuf, (int)strlen(sendbuf), 0 );
    if (iResult == SOCKET_ERROR) {
        printf("send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    printf("Bytes Sent: %ld\n", iResult);

    // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    // Receive until the peer closes the connection
    do {

        iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
        if ( iResult > 0 )
            printf("Bytes received: %d\n", iResult);
        else if ( iResult == 0 )
            printf("Connection closed\n");
        else
            printf("recv failed with error: %d\n", WSAGetLastError());

    } while( iResult > 0 );

    // cleanup
    closesocket(ConnectSocket);
    WSACleanup();

    return 0;
}

服务端

服务端大体上和客户端的编写相同,首先也是初始化

#define DEFAULT_PORT "27015"

struct addrinfo *result = NULL, *ptr = NULL, hints;

ZeroMemory(&hints, sizeof (hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;

iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
    printf("getaddrinfo failed: %d\n", iResult);
    WSACleanup();
    return 1;
}

创建socket对象,用于监听来自客户端的连接

SOCKET ListenSocket = INVALID_SOCKET;

ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);

服务端接收到来自客户端的连接后还需要执行绑定,将客户端绑定到系统中的网络地址

// Setup the TCP listening socket
    iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

然后我们就可以使用监听函数开始等待客户端的连接

if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) {
    printf( "Listen failed with error: %ld\n", WSAGetLastError() );
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

通常,服务器会连接多个客户端,会使用一些特定的模型来进行处理。例如select、异步I/O模型、事件选择模型,重叠I/O模型和完全端口模型

现在,我们不涉及这些技术,只连接一个客户端,来实现最简单的网络通信,使用accept接收来自客户端的通信请求

ClientSocket = INVALID_SOCKET;

// Accept a client socket
ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket == INVALID_SOCKET) {
    printf("accept failed: %d\n", WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

后面的内容就和客户端基本没什么差别了,也是使用send和recv来接受和发送信息

#define DEFAULT_BUFLEN 512

char recvbuf[DEFAULT_BUFLEN];
int iResult, iSendResult;
int recvbuflen = DEFAULT_BUFLEN;

// Receive until the peer shuts down the connection
do {

    iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", iResult);

        // Echo the buffer back to the sender
        iSendResult = send(ClientSocket, recvbuf, iResult, 0);
        if (iSendResult == SOCKET_ERROR) {
            printf("send failed: %d\n", WSAGetLastError());
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }
        printf("Bytes sent: %d\n", iSendResult);
    } else if (iResult == 0)
        printf("Connection closing...\n");
    else {
        printf("recv failed: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }

} while (iResult > 0);

然后断开连接

// cleanup
closesocket(ClientSocket);
WSACleanup();

return 0;

完整代码:

#undef UNICODE

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// Need to link with Ws2_32.lib
#pragma comment (lib, "Ws2_32.lib")
// #pragma comment (lib, "Mswsock.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(void) 
{
    WSADATA wsaData;
    int iResult;

    SOCKET ListenSocket = INVALID_SOCKET;
    SOCKET ClientSocket = INVALID_SOCKET;

    struct addrinfo *result = NULL;
    struct addrinfo hints;

    int iSendResult;
    char recvbuf[DEFAULT_BUFLEN];
    int recvbuflen = DEFAULT_BUFLEN;

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;

    // Resolve the server address and port
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Create a SOCKET for the server to listen for client connections.
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }

    // Setup the TCP listening socket
    iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    freeaddrinfo(result);

    iResult = listen(ListenSocket, SOMAXCONN);
    if (iResult == SOCKET_ERROR) {
        printf("listen failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // Accept a client socket
    ClientSocket = accept(ListenSocket, NULL, NULL);
    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // No longer need server socket
    closesocket(ListenSocket);

    // Receive until the peer shuts down the connection
    do {

        iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0) {
            printf("Bytes received: %d\n", iResult);

        // Echo the buffer back to the sender
            iSendResult = send( ClientSocket, recvbuf, iResult, 0 );
            if (iSendResult == SOCKET_ERROR) {
                printf("send failed with error: %d\n", WSAGetLastError());
                closesocket(ClientSocket);
                WSACleanup();
                return 1;
            }
            printf("Bytes sent: %d\n", iSendResult);
        }
        else if (iResult == 0)
            printf("Connection closing...\n");
        else  {
            printf("recv failed with error: %d\n", WSAGetLastError());
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }

    } while (iResult > 0);

    // shutdown the connection since we're done
    iResult = shutdown(ClientSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }

    // cleanup
    closesocket(ClientSocket);
    WSACleanup();

    return 0;
}

现在,我们已经完成了最基础的网络通信应用,实现了一个客户端和服务端,并使他们能够互相通信,下面我们使用完全端口模型实现多客户端和服务端的连接

完全端口模型

完全端口模型是五个模型中性能最好的模型,可以同时并发的处理成百上千的客户端。还有一个很大的优点是完全端口的主线程是空闲的,和客户端的通信全部交给了工作者线程,于是可以在主线程做一些别的操作,比如使用MFC创建GUI界面。

完全端口模型实际上和网络通信中的端口这个概念没有什么关系,只是叫这个名字而已,不要过多的联系端口进行理解。

完全端口模型,简单来说,就是维护一个公共的消息队列,每当有来自客户端的消息时,就将这个请求加入到这个公共的消息队列中。然后创建很多工作者线程,具体多少取决于机器的CPU数量,让这些线程排队处理同这个公共的消息队列中的消息。当然,实际上这个模型的实现是非常复杂的,但好就好在Windows已经为我们将这些复杂的内核操作全部封装成了函数,于是,我们只需要非常简单的几个函数调用就可以完成这些复杂的操作 。

服务端

使用完全端口模型构建服务端也有固定的框架格式,如果只是想使用这个模型,是十分简单的。

1、调用CreateIoCompletionPort函数,创建一个完成端口,指定第四个参数为0,设置每个处理器一次允许执行一个工作线程。
2、判断系统内安装了多少处理器,以判断将要创建的线程数量
3、创建工作者线程,在完成端口上为已完成的I/O请求提供服务
4、准备好一个监听套接字,在指定端口上监听进入的连接请求。
5、使用accetp函数,接受进入的连接请求。
6、创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接收的套接字句柄。
7、再次调用CreateIoCompletionPort函数,将自accept返回的新套接字句柄同完成端口关联,通过完成键(CompletionKey)参数,将单句柄数据结构传递给函数。
8、开始在已接受的连接上进行I/O操作。通过重叠I/O机制,在新建的套接字上传递一个或多个异步WSARecv或WSASend请求。这些IO请求完成后,一个工作者线程会为IO请求提供服务,同事处理未来的IO请求。
9、重复步骤5~8,直到服务器终止。

和不使用模型的服务端相同,首先需要一些初始化操作

int nPort = 6000;
DWORD dwRet = 0;
WSADATA wsaData;
dwRet = WSAStartup(MAKEWORD(2, 2), &wsaData);

然后我们开始进入完全端口模型的创建流程,使用CreateIoCompletionPort函数建立一个完全端口对象

HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);

获取处理器数量

SYSTEM_INFO mySysInfo; GetSystemInfo(&mySysInfo);

创建工作者线程,具体的工作者线程需要执行的操作暂时不管,先给架子搭好

// 基于处理器的核心数量创建线程
    for (DWORD i = 0; i < (mySysInfo.dwNumberOfProcessors * 2); ++i)
    {
        // 创建服务器工作器线程,并将完成端口传递到该线程
        HANDLE ThreadHandle = CreateThread(NULL, 0, ServerWorkThread, hCompletion, 0, NULL);
        if (NULL == ThreadHandle) {
            printf("Create Thread Handle failed. Error:%d", GetLastError());
            return -1;
        }
        CloseHandle(ThreadHandle);
    }

下面就是大差不差的创建监听套接字,进行绑定,等待连接请求

// 创建监听套接字
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    SOCKADDR_IN si;
    si.sin_family = AF_INET;
    si.sin_port = ::htons(nPort);
    si.sin_addr.s_addr = INADDR_ANY;
    ::bind(sListen, (sockaddr*)&si, sizeof(si));
    ::listen(sListen, 10);

    SOCKADDR_IN saRemote;
        int nRemoteLen = sizeof(saRemote);
        SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen);
        if (sNew == INVALID_SOCKET)
        {
            continue;
        }

连接成功后依然使用CreateIoCompletionPort将连接关联到完全端口对象中。看到这里是不是觉得很有趣,这里和创建完全端口时使用的是同一个函数,只不过传递了不同的参数,达到了将socket绑定到完全端口对象的效果。

PPER_HANDLE_DATA pPerHandle = (PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
pPerHandle->s = sNew;
memcpy(&pPerHandle->addr, &saRemote, nRemoteLen);
pPerHandle->nOperationType = OP_READ;
//注意这里的pPerHandle,类似线程参数,是一个我们自定义的结构体
//在这里传入后,我们在工作者线程中就也可以使用这个参数了。
::CreateIoCompletionPort((HANDLE)pPerHandle->s, hCompletion, (ULONG_PTR)pPerHandle, 0);

现在,我们的消息的监听就被完全端口接管了,后续传递来的消息都会被放入内个公共的消息队列之中。然后我们就可以通过工作者线程对收到的消息进行愉快地处理了。

接下来我们来看最重要的工作者线程
工作者线程是一个循环操作,在线程未终止前会一直执行,这个循环判断我们使用while (!bStopThread)实现

然后就是一个很关键的函数GetQueuedCompletionStatus,这个函数会等待IO操作完成,如果IO操作未完成,则会一直等待,使线程进入不占用CPU资源的睡眠状态。

直到有IO操作完成后,这个函数会返回,同时将消息的参数保存在绑定的自定义结构体中,然后我们可以根据当初绑定时候用的自定义结构体中传递来的数据,来判断我们需要做什么操作。那么首先,来看看我们自定义的结构体是啥样的。

typedef struct _PER_HANDLE_DATA
{
    SOCKET s;               //建立的socket连接
    sockaddr_in addr;      // 客户端地址
    char buf[BUFFER_SIZE];  //传递的消息
    int nOperationType;     //执行的操作码
}PER_HANDLE_DATA, * PPER_HANDLE_DATA;

这个结构体中,我们使用nOperationType存储需要工作者线程处理的方式,这个参数我使用宏定义了三个值

#define OP_READ    10
#define OP_WRITE   20
#define OP_TRANCE  30

根据这三个值我们可以使用switch结构进行选择需要执行的操作。

当执行OP_RADE操作时,表明我们完成的IO操作是接收消息,那么我们需要做的就是读取,并打印接收到的内容,然后将其转发给其他客户端。然后再向客户端回复一条消息,表示消息已经由服务端接收了。

case OP_READ:
        {
            printf("begin read\n");
            MSG_BODY msgBody = { 0 };
            memcpy(&msgBody, pPerHandle->buf , sizeof(pPerHandle->buf));
            //如果只是需要读的消息,那么读出并打印
            if (msgBody.iOpType == OP_READ)
            {
                ZeroMemory(recviedBuff, 1024);
                memcpy(recviedBuff, msgBody.szBuffer, strlen(msgBody.szBuffer));
                printf("msgBody.szBuffer = %s\n", msgBody.szBuffer);
                printf("recviedBuff = %s\n", recviedBuff);
            }
            //如果是需要转发的消息,那么在服务端打印后再转发给其他所有客户端
            else if (msgBody.iOpType == OP_TRANCE)
            {
                ZeroMemory(recviedBuff, 1024);
                memcpy(recviedBuff, msgBody.szBuffer, strlen(msgBody.szBuffer));
                printf("msgBody.szBuffer = %s\n", msgBody.szBuffer);
                printf("recviedBuff = %s\n", recviedBuff);
                printf("begin trance\n");
                for (int i = 0; i < g_ClientList.size(); i++)
                {
                    if (g_ClientList[i] == pPerHandle) continue;
                    //转发出去的消息不需要再进行二次转发,直接使用WRITE操作即可
                    g_ClientList[i]->nOperationType = OP_WRITE;
                    MsgBody msgBody;
                    msgBody.iOpType = OP_WRITE;
                    memcpy(msgBody.szBuffer, recviedBuff, strlen(recviedBuff));
                    msgBody.iBodySize = strlen(msgBody.szBuffer);

                    WSABUF buf;
                    buf.buf = (char*)&msgBody.szBuffer;
                    buf.len = sizeof(msgBody);

                    OVERLAPPED* pol = (OVERLAPPED*)::GlobalAlloc(GPTR, sizeof(OVERLAPPED));

                    DWORD dwFlags = 0, dwSend = 0;
                    ::WSASend(g_ClientList[i]->s, &buf, 1, &dwSend, dwFlags, pol, NULL);
                }
            }
    // 继续投递发送I/O请求
            msgBody = { 0 };
            memcpy(msgBody.szBuffer, "server recived", strlen("server recived"));
            msgBody.iBodySize = sizeof(msgBody);
            pPerHandle->nOperationType = OP_WRITE;
            WSABUF buf;
            buf.buf = (char*)&msgBody.szBuffer;
            buf.len = sizeof(msgBody);

            OVERLAPPED* pol = (OVERLAPPED*)::GlobalAlloc(GPTR, sizeof(OVERLAPPED));

            DWORD dwFlags = 0, dwSend = 0;
            ::WSASend(pPerHandle->s, &buf, 1, &dwSend, dwFlags, pol, NULL);

当执行OP_WRITE操作时,说明我们已经发送出去了一条信息,接下来等待接收新的消息即可,所以我们投递一个接收信息的IO操作。

case OP_WRITE:
        {
            printf("begin wirte\n");
                pPerHandle->nOperationType = OP_READ;
                OVERLAPPED* pol = (OVERLAPPED*)::GlobalAlloc(GPTR, sizeof(OVERLAPPED));
                WSABUF buf;
                buf.buf = pPerHandle->buf;
                buf.len = BUFFER_SIZE;
                DWORD dwRecv = 0;
                DWORD dwFlags = 0;
                ::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, pol, NULL);
                printf("Transfer successfully\n");
        }

然后可能会有同学有疑问为什么OP_READ操作后不需要再投递一个接收IO操作来等待下一条消息到来,这里因为先投递了一条发送IO操作,执行OP_WRITE,而执行完后,OP_WRITE操作又投递了一个接收IO操作,所以最终还是会投递接收操作。

客户端

客户端就没必要使用完全端口了,直接连接即可,不过我们需要增加一个MFC的操作页面,还是稍微有点麻烦的。

初始化操作什么的还是都一样的,没什么变化

 // 初始化Winsock库
    result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0) {
        WSACleanup();
    }

    // 创建客户端套接字
    clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket == INVALID_SOCKET) {
        WSACleanup();
    }

需要注意的是MFC的string类型是双字节的CString,需要做一些类型转换操作

// 获取用户输入的地址和端口号
    CString cIp;
    u_short port;
    GetDlgItem(IDC_EDIT1)->GetWindowText(cIp);
    port = GetDlgItemInt(IDC_EDIT2);
    USES_CONVERSION;
    char* pIp = T2A(cIp);
    // 填写服务器地址和端口号
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = inet_addr(pIp);
    serverAddress.sin_port = htons(port);

然后连接到服务器

// 连接服务器
    result = connect(clientSocket, (sockaddr*)&serverAddress, sizeof(serverAddress));
running = TRUE;
m_pThread = AfxBeginThread((AFX_THREADPROC)ThreadProc, this);

然后绑定一个点击按钮事件,当点击后发送消息,这里比较繁琐的是变量的类型转换

 // 发送到服务端
    MSG_BODY msgBody = {0};
    msgBody = { 0 };
    msgBody.iBodySize = sizeof(MSG_BODY);
    msgBody.iOpType = OP_TRANCE;
    char buf[1024]{0};
    CString cBuf , cMsg, cUserName;
    cBuf = "";
    GetDlgItem(IDC_EDIT3)->GetWindowText(cUserName);
    GetDlgItem(IDC_EDIT4)->GetWindowText(cMsg);
    cBuf += cMsg;
    const size_t size = (cBuf.GetLength() + 10) * 2;
    size_t convertedCharsw = 0;
    char* pBuf = new char[size]{0};
    wcstombs_s(&convertedCharsw, pBuf, size, cBuf, _TRUNCATE);
    strcpy_s(msgBody.szBuffer, pBuf);
    send(clientSocket, (char*)&msgBody, sizeof(msgBody), 0);
    delete []pBuf;

最后就是接收信息的线程了

UINT CmfcclientDlg::ThreadProc(LPVOID pParam) {

    //接收线程函数
    CmfcclientDlg *pDlg = (CmfcclientDlg*)pParam;
    char receiveBuffer[1024]{0};
    int result;
    while (pDlg->running) {
        ZeroMemory(receiveBuffer, 1024);
        result = recv(pDlg->clientSocket, receiveBuffer, sizeof(receiveBuffer), 0);
        if (result == 0) {
            std::cout << "Server lost" << std::endl;
            pDlg->running = false;
        }
        else {
            MSG_BODY msgBody = { 0 };
            memcpy(&msgBody, receiveBuffer, sizeof(MSG_BODY));
            receiveBuffer[result] = '\0';
            CStringW cReceiveBuffer(receiveBuffer);
        }
    }
}