目录

  1. 1. 前言
  2. 2. 基础
  3. 3. 套接字
  4. 4. 数据结构
    1. 4.1. 本机字节顺序和网络字节顺序
  5. 5. 面向连接的套接字编程
    1. 5.1. C/S实例
      1. 5.1.1. 服务端
      2. 5.1.2. 客户端
    2. 5.2. 改进
  6. 6. 利用流式套接字实现文件传送
    1. 6.1. 服务端
    2. 6.2. 客户端
  7. 7. 无连接的套接字编程
    1. 7.1. C/S实例
      1. 7.1.1. 服务端
      2. 7.1.2. 客户端
  8. 8. MFC应用开发
    1. 8.1. 新建项目
    2. 8.2. 项目结构
      1. 8.2.1. 引用
      2. 8.2.2. 外部依赖项
      3. 8.2.3. 头文件
      4. 8.2.4. 源文件
      5. 8.2.5. 资源文件
    3. 8.3. 项目流程
    4. 8.4. 添加UI控件
    5. 8.5. Hello World
      1. 8.5.1. 弹出消息窗口
      2. 8.5.2. 直接设置文本
    6. 8.6. 对话框数据交换
  9. 9. 文件传输
  10. 10. 主机扫描
  11. 11. 多线程文件传输
    1. 11.1. 二开
  12. 12. CAsyncSocket 类
  13. 13. 聊天室
    1. 13.1. 服务端
      1. 13.1.1. 控件与成员变量
      2. 13.1.2. 派生类
      3. 13.1.3. 对话框监听按钮
      4. 13.1.4. 监听客户端
      5. 13.1.5. 接收/发送消息
      6. 13.1.6. 关闭连接
    2. 13.2. 操作演示
  14. 14. C#应用开发
    1. 14.1. 服务端
      1. 14.1.1. 添加控件

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

网络协议程序设计

2025/3/7
  |     |   总文章阅读量:

前言

学学协议设计,加深下理解(?


基础

三元组标识 应用层进程=(传输层协议, 主机的IP地址, 传输层的端口号)

五元组标识 (传输层协议, 本地机IP地址, 本地机传输层端口, 远地机IP地址, 远地机传输层端口)

三类网络编程:基于 TCP/IP 协议栈的网络编程;基于 WWW 应用的网络编程;基于 .NET 框架的 Web Services 网络编程


套接字

套接字是一种通信机制(通信的两方的一种约定),socket 屏蔽了各个协议的通信细节,提供了 TCP/IP 协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用 TCP/IP 协议的功能。这使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。

特性:域(domain),类型(type),和协议(protocol)


数据结构

sockaddr 结构:针对各种通信域的套接字,存储它们的地址信息

struct sockaddr {
	u_short	sa_family;	// 地址家族
	char	sa_data[14];	// 协议地址
};

sockaddr_in 结构:专门针对 Internet 通信域,存储套接字相关的网络地址信息,例如 IP 地址,传输层端口号

官方文档:https://learn.microsoft.com/zh-cn/windows/win32/api/winsock/ns-winsock-sockaddr_in

struct sockaddr_in {
	short	sin_family;	// 地址家族
	u_short	sin_port;	// 端口号
	struct in_addr	sin_addr;	// IP地址
	char	sin_zero[8];	// 全为0
};

in_addr 结构:专门用来存储 IP 地址

typedef struct in_addr {
  union {
    struct { u_char  s_b1, s_b2, s_b3, s_b4; } S_un_b;
    struct { u_short s_w1, s_w2; } S_un_w;
    u_long S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

用法:

首先定义一个 sockaddr_in 的结构实例,并将它清零;

然后再进行赋值;

函数调用时将这个结构强转为 sockaddr

struct sockaddr_in myad;
memset(&myad,0,sizeof(struct sockaddr_in));
myad.sin_family = AF_INET;
myad.sin_port = htons(8080);
myad.sin_addr.S_addr = htonl(INADDR-ANY);

accept(listenfd, (sockaddr*)(&myad), &addrlen);

本机字节顺序和网络字节顺序

本机字节顺序:在具体计算机中的多字节数据的存储顺序

网络字节顺序:多字节数据在网络协议报头中的存储顺序,套接字中必须用

所以装入套接字时要从本机字节顺序转为网络字节顺序,本机输出时要从网络字节顺序转换为本机字节顺序

  • htons():短整数本机顺序转换为网络顺序,用于端口号
  • htonl():长整数本机顺序转换为网络顺序,用于IP地址
  • ntohs():短整数网络顺序转换为本机顺序,用于端口号
  • ntohl():长整数网络顺序转换为本机顺序,用于IP地址

面向连接的套接字编程

image-20250319172427824

服务端:

  • SOCKET():初始化一个 socket

    WINSOCK_API_LINKAGE SOCKET WSAAPI socket(int af,int type,int protocol);

    af:协议簇,如 PF_INET 表示 TCP/IP

    #define AF_INET 2
    
    #define PF_INET AF_INET

    type:通信类型

    #define SOCK_STREAM 1	// 面向连接
    #define SOCK_DGRAM 2	// 无连接
    #define SOCK_RAW 3
    #define SOCK_RDM 4
    #define SOCK_SEQPACKET 5

    protocol:传输层协议,在 Internet 通信域中,一般取0,由系统决定

  • BIND():socket 绑定网络地址

    WINSOCK_API_LINKAGE int WSAAPI bind(SOCKET s,const struct sockaddr *name,int namelen);

    s:套接字描述符

    *name:指针所指结构中保存着特定的网络地址,Internet 域中,网络地址 = IP 地址 + 传输层端口号

    namelen:sockaddr 的结构长度,等于 sizeof(struct sockaddr)

  • LISTEN():监听

    WINSOCK_API_LINKAGE int WSAAPI listen(SOCKET s,int backlog);

    s:套接字描述符

    backlog:等待连接队列的最大长度,最大为20,一般为5~10;如果缓冲区队列有空,就接收一个来自客户端的连接请求,否则拒绝

  • ACCEPT():接受客户端的连接请求

    WINSOCK_API_LINKAGE SOCKET WSAAPI accept(SOCKET s,struct sockaddr *addr,int *addrlen);

    s:套接字描述符

    *addr:出口参数,调用完毕后指针放置客户端的网络地址

    *addrlen:出口参数,调用时初始设置 addr 结构的长度,调用完毕返回客户端的网络地址长度

  • READ():读取客户端发送来的请求/命令数据

  • WRITE():向客户端发送响应数据

  • CLOSE():关闭 socket

客户端:

  • SOCKET()

  • CONNECT():向服务端发送连接请求

    WINSOCK_API_LINKAGE int WSAAPI connect(SOCKET s,const struct sockaddr *name,int namelen);

    类似于bind

  • WRITE()

  • READ()

  • CLOSE()

C/S实例

stdafx.h:预编译头,是自定义的头文件,与项目的源代码文件存放在同一个文件夹,内容可以包含系统的头文件如 stdio.h、string.h 等,这样就不用重复引入了

#include <winsock2.h>
#include <Windows.h>

#include <stdio.h>
#include <string.h>

#pragma comment(lib,"xxx.lib"):引入库文件,用于调用动态链接库;这里用于引入 winsock2.h 的库 ws2_32.lib,注意编译时要添加-lws2_32选项


WSADATA:https://learn.microsoft.com/zh-cn/windows/win32/api/winsock/ns-winsock-wsadata

初始化 winsock:https://learn.microsoft.com/zh-cn/windows/win32/winsock/initializing-winsock

服务端

定义完数据和套接字后就可以填入对应参数,依次进行 bind、listen、accept 等操作

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;

    SOCKET sockServer;
    SOCKADDR_IN addrServer;

    SOCKET sockClient;
    SOCKADDR_IN addrClient;

    WSAStartup(MAKEWORD(2, 2), &wsaData);

    sockServer = socket(AF_INET, SOCK_STREAM, 0);

    addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = htons(6000);

    bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

    listen(sockServer, 5);
    printf("服务器已启动:\n监听中...\n");

    int len = sizeof(SOCKADDR);
    char recvBuf[100];

    sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);

    recv(sockClient, recvBuf, 100, 0);
    printf("%s\n", recvBuf);

    
    closesocket(sockClient);

    WSACleanup();

    return 0;
}

编译:

gcc server.c -o output/server.exe -lws2_32 -fexec-charset=GBK

image-20250310152151388

客户端

注意,c语言中为了确保字符串的终止符\0(空字符)也被发送,send 发送到服务端的长度需要 +1

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;

    SOCKET sockClient;
    SOCKADDR_IN addrServer;

    WSAStartup(MAKEWORD(2,2),&wsaData);

    sockClient=socket(AF_INET,SOCK_STREAM,0);

    addrServer.sin_family=AF_INET;
    addrServer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
    addrServer.sin_port=htons(6000);


    connect(sockClient,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));

    char message[30]="111 Hello Socket!";
    send(sockClient,message,strlen(message)+1,0);

    closesocket(sockClient);
    WSACleanup();

    return 0;
}

image-20250310152403062


改进

  1. 在服务器显示连接上的客户机的 IP 地址和端口

    服务端在 accept 的时候保留了客户端的信息 addrClient,只需要将其转格式输出即可

    使用 inet_ntoainet_addr的网络字节序转换为 IP 地址,ntohs对应客户端的htons

    sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
    printf("客户端 %s:%d\n",inet_ntoa(addrClient.sin_addr),ntohs(addrClient.sin_port));

    image-20250310160618580

  2. 服务器也发送一条信息(Hello! Client Socket.)给客户机并显示

    服务端:和客户端发给服务端消息一样的操作

    char message[30] = "Hello! Client Socket.";
    send(sockClient, message, strlen(message) + 1, 0);
    printf("消息已发送给客户端\n");

    客户端:和服务端接收消息一样的操作,从 sockClient 接收

    char recvBuf[100];
    recv(sockClient, recvBuf, 100, 0);
    printf("收到服务端消息:%s\n", recvBuf);

    image-20250310161531630

  3. 完成步骤 2 后,修改为:

    • 客户机键盘输入某一数字(例如 5)并将其发送给服务器
    • 服务器收到数字并显示,然后将该数字加 1(例如 5+1=6)并将结果回送给客户机
    • 客户机收到数字并显示

    先修改客户端发送逻辑:

    int input;
    printf("请输入一个整数: ");
    scanf("%d", &input);
    send(sockClient, (char*)&input, sizeof(input), 0);

    服务端接收逻辑:

    int receivedNum;
    recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0);
    printf("收到客户端数字: %d\n", receivedNum);

    接下来修改服务端发送逻辑:

    int resultNum = receivedNum + 1;
    send(sockClient, (char*)&resultNum, sizeof(resultNum), 0);

    客户端接收逻辑:

    int receivedNum;
    recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0);
    printf("收到服务端数字: %d\n", receivedNum);

    image-20250310163205389

  4. 修改程序使得以下情况能正常运行:服务器程序不关闭,客户机程序可重复运行关闭,两端正常接收和发送消息

    直接 while True 保持运行即可

    服务端需要持续运行的部分从 accept 开始,客户端则是从建立 socket 开始

    服务端完整代码:

    #include "stdafx.h"
    #pragma comment(lib, 'ws2_32.lib')
    
    int main(int argc, char const *argv[])
    {
        WSADATA wsaData;
    
        SOCKET sockServer;
        SOCKADDR_IN addrServer;
    
        SOCKET sockClient;
        SOCKADDR_IN addrClient;
    
        int len = sizeof(SOCKADDR);
    
    
        WSAStartup(MAKEWORD(2, 2), &wsaData);
    
        sockServer = socket(AF_INET, SOCK_STREAM, 0);
    
        addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
        addrServer.sin_family = AF_INET;
        addrServer.sin_port = htons(6000);
    
        bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
    
        listen(sockServer, 5);
        printf("服务器已启动...\n监听中...\n");
    
    
        while(1){
            sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
            printf("客户端 %s:%d\n", inet_ntoa(addrClient.sin_addr), ntohs(addrClient.sin_port));
        
            // char recvBuf[100];
            // recv(sockClient, recvBuf, 100, 0);
            // printf("收到客户端消息:%s\n", recvBuf);
            int receivedNum;
            recv(sockClient, (char*)&receivedNum, sizeof(receivedNum), 0);
            printf("收到客户端数字: %d\n", receivedNum);
        
            // char message[30] = "Hello! Client Socket.";
            // send(sockClient, message, strlen(message) + 1, 0);
            int resultNum = receivedNum + 1;
            send(sockClient, (char*)&resultNum, sizeof(resultNum), 0);
            printf("已返回结果\n");
    
            closesocket(sockClient);
            printf("客户端连接已关闭,等待下一个...\n");
        }
    
        closesocket(sockClient);
        WSACleanup();
        return 0;
    }

    客户端完整代码:

    #include "stdafx.h"
    #pragma comment(lib, 'ws2_32.lib')
    
    int main(int argc, char const *argv[])
    {
        WSADATA wsaData;
    
        SOCKET sockClient;
        SOCKADDR_IN addrServer;
    
        int input;
        int receivedNum;
    
        WSAStartup(MAKEWORD(2, 2), &wsaData);
    
        while (1)
        {
            sockClient = socket(AF_INET, SOCK_STREAM, 0);
    
            addrServer.sin_family = AF_INET;
            addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
            addrServer.sin_port = htons(6000);
    
            if (connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR)) == SOCKET_ERROR)
            {
                printf("连接失败");
                continue;
            }
    
            // char message[30] = "111 Hello Socket!";
            // send(sockClient, message, strlen(message) + 1, 0);
    
            printf("请输入一个整数 (输入-1退出): ");
            scanf("%d", &input);
            if (input == -1)
                break;
    
            send(sockClient, (char *)&input, sizeof(input), 0);
    
            // char recvBuf[100];
            // recv(sockClient, recvBuf, 100, 0);
            // printf("收到服务端消息:%s\n", recvBuf);
            recv(sockClient, (char *)&receivedNum, sizeof(receivedNum), 0);
            printf("收到服务端数字: %d\n", receivedNum);
        }
    
        closesocket(sockClient);
        WSACleanup();
        return 0;
    }

    image-20250310164459188

  5. 一台服务器,多台客户机的情况下,由于服务端只能同时处理一个端口上的连接,会出现其他客户端等待的情况

    image-20250310164744022

    在 UNIX 系统下,可以使用fork()创建子进程解决这个问题


利用流式套接字实现文件传送

使用 VC++ 6.0

创建 Win32 Console Application 工程

image-20250319193325558

服务端

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#define BUF_SIZE 1024
int main(){
    //先检查文件是否存在
    char *filename = "C:\\test.txt";  //文件名
    FILE *fp = fopen(filename, "rb");  //以二进制方式打开文件
    if(fp == NULL){
        printf("Cannot open file, press any key to exit!\n");
        system("pause");
        exit(0);
    }
    
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in sockAddr;
    
    memset(&sockAddr, 0, sizeof(sockAddr));
    
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);
    
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    listen(servSock, 20);
    
    SOCKADDR clntAddr;
    int nSize = sizeof(SOCKADDR);
    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
    
    //循环发送数据,直到文件结尾
    char buffer[BUF_SIZE] = {0};  //缓冲区
    int nCount;
    while( (nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0 ){
        send(clntSock, buffer, nCount, 0);
    }
    shutdown(clntSock, SD_SEND);  //文件读取完毕,断开输出流,向客户端发送FIN包
    recv(clntSock, buffer, BUF_SIZE, 0);  //阻塞,等待客户端接收完毕
    fclose(fp);
    
    closesocket(clntSock);
    closesocket(servSock);
    WSACleanup();
    system("pause");
    return 0;
}

客户端

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int main(){
    //先输入文件名,看文件是否能创建成功
    char filename[100] = {0};  //文件名
    printf("Input filename to save: ");
    gets(filename);
    FILE *fp = fopen(filename, "wb");  //以二进制方式打开(创建)文件
    if(fp == NULL){
        printf("Cannot open file, press any key to exit!\n");
        system("pause");
        exit(0);
    }
    
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sockAddr;
    
    memset(&sockAddr, 0, sizeof(sockAddr));
    
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockAddr.sin_port = htons(1234);
    
    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
    
    //循环接收数据,直到文件传输完毕
    char buffer[BUF_SIZE] = {0};  //文件缓冲区
    int nCount;
    while( (nCount = recv(sock, buffer, BUF_SIZE, 0)) > 0 ){
        fwrite(buffer, nCount, 1, fp);
    }
    puts("File transfer success!");
    //文件接收完毕后直接关闭套接字,无需调用shutdown()
    fclose(fp);
    
    closesocket(sock);
    WSACleanup();
    system("pause");
    return 0;
}

f7 编译,crtl+f5 运行

image-20250319200515060

image-20250319200553008

成功下载文件 test.txt 并保存为 save.txt


无连接的套接字编程

image-20250319172252531

特点:

  • 应用程序双方是不对等的,服务器要先行启动,处于被动的等待访问的状态,而客户机则可以随时主动地请求访问服务器
  • 服务器进程将套接字绑定到指定的端口,客户端需要知道服务端的网络地址
  • 客户端套接字使用动态分配的自由端口,不需要进行绑定
  • 客户端必须先发送数据报,在数据报中携带双方的地址,服务端收到后才能知道客户机端的地址,才能给客户机端回送数据报
  • 服务器可以接收多个客户端的数据

系统调用:

  • SENDTO():发送数据报,需要一个全相关的五元组信息(协议(UDP)、源IP地址、源端口号、目的IP地址和目的端口号)

    WINSOCK_API_LINKAGE int WSAAPI sendto(SOCKET s,const char *buf,int len,int flags,const struct sockaddr *to,int tolen);

    s:发送方的数据报套接字描述符

    buf:发送缓冲区的长度

    flags:发送方式,一般为0

    to:指针,指向的 sockaddr 结构包含接收方完整的地址

    tolen:sockaddr 结构的长度

  • RECVFROM():接收数据报

    WINSOCK_API_LINKAGE int WSAAPI recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr *from,int *fromlen);

C/S实例

在面向连接的基础上稍作修改即可

服务端

相比面向连接,这里多了一些异常处理,socket 的参数为 socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;
    WORD sockVersion = MAKEWORD(2, 2);

    SOCKET serSocket;
    SOCKADDR_IN serAddr;

    SOCKADDR_IN remoteAddr;

    int nAddrLen = sizeof(SOCKADDR);

    if (WSAStartup(sockVersion, &wsaData) != 0)
    {
        return 0;
    }

    serSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (serSocket == INVALID_SOCKET)
    {
        printf("socket error!");
        return 0;
    }

    serAddr.sin_addr.S_un.S_addr = INADDR_ANY;
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(8888);

    if (bind(serSocket, (SOCKADDR *)&serAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
    {
        printf("bind error!");
        closesocket(serSocket);
        return 0;
    }

    while (1)
    {
        char recvData[255];
        int ret = recvfrom(serSocket, recvData, 255, 0, (SOCKADDR *)&remoteAddr, &nAddrLen);
        if (ret > 0)
        {
            recvData[ret] = 0x00;
            printf("接受到一个连接:%s\r\n", inet_ntoa((remoteAddr.sin_addr)));
            printf((recvData));
        }
        char *sendData = "一个来自服务端的UDP数据包\n";
        sendto(serSocket, sendData, strlen(sendData), 0, (SOCKADDR *)&remoteAddr, nAddrLen);
    }

    closesocket(serSocket);
    WSACleanup();
    return 0;
}

客户端

#include "stdafx.h"
#pragma comment(lib, 'ws2_32.lib')

int main(int argc, char const *argv[])
{
    WSADATA wsaData;
    WORD sockVersion = MAKEWORD(2, 2);

    SOCKET sockClient;

    if (WSAStartup(sockVersion, &wsaData) != 0)
    {
        return 0;
    }

    sockClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    SOCKADDR_IN sin;

    sin.sin_family = AF_INET;
    sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    sin.sin_port = htons(8888);

    int len = sizeof(sin);

    char *sendData = "来自客户端的数据包.\n";
    sendto(sockClient, sendData, strlen(sendData), 0, (SOCKADDR *)&sin, len);

    char recvData[255];
    int ret = recvfrom(sockClient, recvData, 255, 0, (SOCKADDR *)&sin, &len);
    if (ret > 0)
    {
        recvData[ret] = 0x00;
        printf(recvData);
    }

    closesocket(sockClient);
    WSACleanup();
    return 0;
}

先启动服务端,再启动客户端

image-20250319171111646


MFC应用开发

参考:

https://www.cnblogs.com/JCpeng/p/15011896.html

https://zhuanlan.zhihu.com/p/778291182

https://www.cnblogs.com/ranjiewen/p/5414244.html

环境:Visual Studio 2022

MFC(Microsoft Foundation Classes)是微软基础类库,以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。它抽象了Windows API,提供了一个基于C++的面向对象的方式,使得程序员可以更容易地开发出图形用户界面(GUI)应用程序。

新建项目

在 VS2022 中选择 MFC应用 新建项目

image-20250418104310206

应用程序类型选择 基于对话框

  • 单个文档:像Windows记事本、Windows画图、Windows写字板这样的程序,一个程序只有一个文档处于编辑状态。
  • 多个文档:像Word,Excel这样可以在一个MDI窗口里面同时处理多个文档的类型。
  • 基于对话框:像Windows扫雷、纸牌那样直接在对话框进行操作的程序。用不着文档。

新建项目完成后界面如下:

image-20250418105536721


项目结构

引用

假设这里引用了另一个项目的 DLL1

image-20250418105804732

那么会有三个用处:

  1. 决定生成整个解决方案的项目顺序:如果不引用 Dll1,点击生成整个解决方案时,项目生成顺序会是 ConsoleApplication2.1,Dll1【按照顺序从上到下】。结果是报错。

    因为 ConsoleApplication2.1 使用了 Dll1,可 Dll1 还没有生成,所以 ConsoleApplication2.1 找不到 Dll1 的 lib 或 dll 文件。

    像这种情况很多公司常见,这时候往往会多生成几遍解决方案,错误会逐渐减少,直至最后成功生成所有项目。

  2. 用于生成单个项目:如果我清理了解决方案,然后右键生成ConsoleApplication2.1,会报错,找不到Dll1。所以这时候应该在“引用”中添加Dll1。然后右键生成ConsoleApplication2.1,这时,会先生成Dll1,再生成ConsoleApplication2.1。

  3. 用于导出:省略了__declspec(dllexport)。导出库中需要导出的所有前边可省略__declspec(dllexport)


外部依赖项

工程中显式包含的那些头文件本身所包含的头文件(非自己定义),主要包含一些外部库

image-20250418110029744


头文件

image-20250418110246182

  • framework.h:定义了项目所使用的 MFC 应用程序框架相关的头文件。通常包括预编译头文件的引用。

    image-20250418110212099

  • MFC_Test.h:这是主应用程序类的头文件,一般从 CWinApp 或其派生类继承

    image-20250418110336515

  • MFC_TestDlg.h::这是对话框类的头文件,用于定义主对话框类及其成员函数和变量。对于以对话框为基础的应用程序,这是主要的用户界面。

    image-20250418110436135

  • pch.h(Pre-compiled Header):预编译头文件,用于提高编译速度。会包含项目中常用的头文件。被预先编译过的头文件,对于比较大型的工程,往往编译时间会很久,通过使用 PCH,把那些不经常发生改动的头文件都预先编译出来,就可以大大节省实际使用时候的编译时间。实际应用中,还经常把外部调用的 API 的头文件编译为 PCH,比如调用 STL、调用 Windows 的 APIwindows.h 等等。

    image-20250418110544657

    缺点的话就是降低了文件间的关联性,只知道包含了pch,但不知道具体用到了是哪些头文件

  • Resource.h:定义了资源的ID,如对话框、控件、菜单、字符串等资源的标识符。

    image-20250418110758205

  • targetver.h:定义了目标版本的Windows平台,用于确保项目能正确地在指定的平台上进行编译。

    image-20250418111350739


源文件

  • MFC_Test.cpp:主应用程序文件,包含CWinApp派生类的实现代码,如应用程序初始化和退出例程。
  • MFC_TestDlg.cpp:对话框类的实现文件,包含主要的用户界面逻辑和事件处理函数。
  • pch.cpp:用于生成预编译头文件的源文件。

资源文件

  • MFC_Test.ico:应用程序图标文件。

  • MFCTest.rc:资源脚本文件,包含应用程序的资源定义,如图标、对话框、菜单等。这些资源在编译时被编译进应用程序的可执行文件中。

    image-20250418111252788

  • 其他的资源文件(如字符串表等):这些文件也会包含在资源文件中,有时会在目录中显示。


项目流程

  1. 框架初始化:MFC_Test.cpp 中的 CWinApp 派生类负责初始化应用程序框架。
  2. 用户界面:MFC_TestDlg.h 和 MFC_TestDlg.cpp 中的对话框类负责展示和处理用户交互。
  3. 资源管理:各资源文件(如.rc文件和.h文件中的ID定义)提供UI元素和其他资源的定义和引用。

添加UI控件

  1. 打开资源视图,找到对话框资源(通常是 IDD_[项目名]_DIALOG)

    image-20250418111926759

  2. 添加控件

    Ctrl + Alt + X 呼出工具箱

    从工具箱中拖放工具来添加按钮、标签、目录选择控件等

    这里添加一个 Button

    image-20250418113259963

  3. 添加控制变量,修改属性,添加对应函数

    右键这个 Button 添加控制变量

    image-20250418114250411

    于是 MFC_TestDlg.h 中会出现如下代码:

    public:
    	CButton Test;
    };

    修改属性

    在添加完一个控件(按钮)之后,右键点击按钮选择属性,便可以编辑它的属性

    image-20250418114856611


    添加对应函数:

    双击按钮可以自动生成按钮的点击时间处理函数(没有定义按钮点击事件处理函数之前)

    在 MFC_TestDlg.cpp 里会新增:

    void CMFCTestDlg::OnBnClickedButton1()
    {
    	// TODO: 在此添加控件通知处理程序代码
    }

    MFC_TestDlg.h 会变成:

    public:
    	CButton Test;
    	afx_msg void OnBnClickedButton1();
    };

Hello World

弹出消息窗口

要求在点击按钮时弹出消息窗口显示 Hello World

只需修改 MFC_TestDlg.cpp 中的 OnBnClickedButton1,使用 MessageBox 方法弹出消息窗口即可

详情见官方文档:https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-messagebox

注意这里的字符串都是 LPCSTR 型的,需要转换类型

void CMFCTestDlg::OnBnClickedButton1()
{
	MessageBox(L"Hello World.", L"Test", MB_ICONINFORMATION | MB_OK);
	// TODO: 在此添加控件通知处理程序代码
}

image-20250509094033458

直接设置文本

使用 SetDlgItemText 函数: https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-setdlgitemtextw

点击按钮使在文本框控件 IDC_STATIC 上设置文本

void CMFCTestDlg::OnBnClickedButton1()
{
	//MessageBox(L"Hello World.", L"Test", MB_ICONINFORMATION | MB_OK);
	SetDlgItemText(IDC_STATIC, L"Hello World");
}

image-20250509105034553


对话框数据交换

https://learn.microsoft.com/zh-cn/cpp/mfc/dialog-data-exchange?view=msvc-170

使用 ClassWizard 建立了控件和变量之间的联系时,

UpdateData(FALSE):函数会将数据成员变量中的数据读取到用户界面控件中。这种模式通常用于显示数据或预填充表单。

UpdateData(TRUE):函数会将用户界面控件中的数据写入到相应的数据成员变量中。这种模式通常用于保存用户输入的数据。


文件传输

VS2022 关闭 SDL 检查

使用 CFileDialog 类进行文件操作:https://learn.microsoft.com/zh-cn/cpp/mfc/reference/cfiledialog-class?view=msvc-170

CFileDialog dlgFile(TRUE);
dlgFile.DoModal();	// 显示 Windows 通用文件对话框,并允许用户浏览文件和目录并输入文件名,返回 IDOK 或 IDCANCEL
dlgFile.GetPathName();	// 检索输入到对话框中的文件完整路径

那么创建一个 Button1

这里涉及到 CString 和 char 之间的转换问题,参考:https://zhuanlan.zhihu.com/p/425996522

服务端代码:

#include <atlconv.h>
#define BUF_SIZE 1024

void CFileServerDlg::OnClickedButton1()
{
	CFileDialog dlgFile(TRUE);
	if (dlgFile.DoModal() == IDOK) {
		CString strPath = dlgFile.GetPathName();
		//MessageBox(strPath, L"Path", MB_ICONINFORMATION | MB_OK);

		USES_CONVERSION;
		FILE* fp = fopen(T2A(strPath), "rb");
		if (!fp) {
			MessageBox(L"文件打开失败!");
			return;
		}
		// 初始化Winsock
		WSADATA wsaData;
		WSAStartup(MAKEWORD(2, 2), &wsaData);

		// 创建套接字
		SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);

		// 绑定地址
		sockaddr_in sockAddr;
		memset(&sockAddr, 0, sizeof(sockAddr));
		sockAddr.sin_family = AF_INET;
		sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		sockAddr.sin_port = htons(1234);
		bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

		// 监听端口
		listen(servSock, 20);

		// 接受客户端连接
		SOCKADDR clntAddr;
		int nSize = sizeof(SOCKADDR);
		SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);

		// 发送文件内容
		char buffer[BUF_SIZE] = { 0 };
		int nCount;
		while ((nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0) {
			send(clntSock, buffer, nCount, 0);
		}

		// 清理资源
		shutdown(clntSock, SD_SEND);
		recv(clntSock, buffer, BUF_SIZE, 0);
		fclose(fp);
		closesocket(clntSock);
		closesocket(servSock);
		WSACleanup();

		MessageBox(L"文件发送完成!");
	}
}

客户端:

void CFileClientDlg::OnBnClickedButton1() {
	TCHAR szFilters[] = _T("All Files (*.*)|*.*||");
	CFileDialog dlgFile(FALSE, NULL, NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, szFilters);

	if (dlgFile.DoModal() == IDOK) {
		CString strFilePath = dlgFile.GetPathName();

		USES_CONVERSION;
		// 打开文件
		FILE* fp = fopen(T2A(strFilePath), "wb");
		if (!fp) {
			MessageBox(L"文件创建失败!");
			return;
		}

		// 初始化Winsock
		WSADATA wsaData;
		WSAStartup(MAKEWORD(2, 2), &wsaData);

		// 创建套接字
		SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);

		// 配置地址
		sockaddr_in sockAddr;
		memset(&sockAddr, 0, sizeof(sockAddr));
		sockAddr.sin_family = AF_INET;
		sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		sockAddr.sin_port = htons(1234);

		// 连接服务器
		if (connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
			MessageBox(L"连接服务器失败!");
			closesocket(sock);
			return;
		}

		// 接收数据
		char buffer[BUF_SIZE];
		int totalBytes = 0;
		int nCount;
		while ((nCount = recv(sock, buffer, BUF_SIZE, 0)) > 0) {
			fwrite(buffer, 1, nCount, fp);
			totalBytes += nCount;
		}

		// 清理资源
		fclose(fp);
		closesocket(sock);
		WSACleanup();
		MessageBox(L"文件接收完成!");
	}
}

image-20250509114544488

image-20250509114515252


主机扫描


多线程文件传输

窗口:

BOOL CTransFileDlg::OnInitDialog()
{
	CDialog::OnInitDialog();

	// Add "About..." menu item to system menu.

	// IDM_ABOUTBOX must be in the system command range.
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != NULL)
	{
		CString strAboutMenu;
		strAboutMenu.LoadString(IDS_ABOUTBOX);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// Set the icon for this dialog.  The framework does this automatically
	//  when the application's main window is not a dialog
	SetIcon(m_hIcon, TRUE);			// Set big icon
	SetIcon(m_hIcon, FALSE);		// Set small icon

	(GetDlgItem(IDC_EDITRECV))->SetWindowText(L"尚未准备好接收");
	(GetDlgItem(IDC_EDITSEND))->SetWindowText(L"尚未准备好发送");

	// TODO: Add extra initialization here

	return TRUE;  // return TRUE unless you set the focus to a control
}

接收部分:

DWORD WINAPI CTransFileDlg::SocketRecv(LPVOID lpParameter)
{
	HWND hwnd = ((sockrecv*)lpParameter)->hwnd;//将参数传递到本地变量
	CString recvfname;

	SOCKET socketrecv = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
	if (INVALID_SOCKET == socketrecv)
	{
		closesocket(socketrecv);
		::MessageBox(hwnd, L"套接字创建失败!", L"警告", MB_OK);
		return FALSE;
	}

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(5555);

	int ret1;
	ret1 = bind(socketrecv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //绑定套接字
	if (SOCKET_ERROR == ret1)
	{
		closesocket(socketrecv);
		::MessageBox(hwnd, L"绑定失败!", L"警告", MB_OK);

		return FALSE;
	}

	int ret2;


	ret2 = listen(socketrecv, 5);					//开始监听
	if (SOCKET_ERROR == ret2)
	{
		closesocket(socketrecv);
		::MessageBox(hwnd, L"监听失败!", L"警告", MB_OK);
		return FALSE;
	}

	SOCKADDR_IN addrconn;
	int len = sizeof(SOCKADDR);



	::PostMessage(hwnd, UM_PRGRRDY, 0, 0);        //向主窗口发送消息


	SOCKET sockConn = accept(socketrecv, (SOCKADDR*)&addrconn, &len);   //开始接收
	if (INVALID_SOCKET == sockConn)
	{
		closesocket(socketrecv);
		::MessageBox(hwnd, L"接收失败!", L"警告", MB_OK);
		return FALSE;
	}


	::PostMessage(hwnd, UM_PRGRBGN, 0, 0);			//向主窗口发送消息

	BOOL bRet = TRUE;

	int dataLength, cbBytesRet, cbLeftToReceive;

	BYTE* recdData = NULL;

	CFile destFile;
	CFileException fe;
	BOOL bFileIsOpen = FALSE;



	//当接收到向本地发送文件的请求时弹出文件保存对话框
	CFileDialog fileDlg(FALSE);
	fileDlg.m_ofn.lpstrTitle = L"保存将接收到的文件";
	fileDlg.m_ofn.lpstrFilter = L"All Files(*.*)\0*.*\0\0";
	if (IDOK == fileDlg.DoModal())
	{
		recvfname = fileDlg.GetPathName();
	}


	//用从文件保存对话框中得到的文件名在指定位置创建文件
	if (!(bFileIsOpen = destFile.Open(recvfname, CFile::modeCreate |
		CFile::modeWrite | CFile::typeBinary, &fe)))
	{
		TCHAR strCause[256];
		fe.GetErrorMessage(strCause, 255);
		TRACE("GetFileFromRemoteSender encountered an error while opening the local file\n"
			"\tFile name = %s\n\tCause = %s\n\tm_cause = %d\n\tm_IOsError = %d\n",
			fe.m_strFileName, strCause, fe.m_cause, fe.m_lOsError);



		bRet = FALSE;
		goto PreReturnCleanup;
	}

	//首先接收文件的长度信息
	cbLeftToReceive = sizeof(dataLength);

	do
	{
		BYTE* bp = (BYTE*)(&dataLength) + sizeof(dataLength) - cbLeftToReceive;
		cbBytesRet = recv(sockConn, (char*)bp, cbLeftToReceive, 0);


		if (cbBytesRet == SOCKET_ERROR || cbBytesRet == 0)
		{
			int iErr = ::GetLastError();
			TRACE("GetFileFromRemoteSite returned a socket error while getting file length\n"
				"\tNumber of Bytes received (zero means connection was closed) = %d\n"
				"\tGetLastError = %d\n", cbBytesRet, iErr);



			bRet = FALSE;
			goto PreReturnCleanup;
		}



		cbLeftToReceive -= cbBytesRet;

	} while (cbLeftToReceive > 0);



	dataLength = ntohl(dataLength);  //将文件的长度信息转化为本地字节序


	//准备开始接收文件
	recdData = new byte[RECV_BUFFER_SIZE];
	cbLeftToReceive = dataLength;

	do
	{
		//向主窗口发送消息主要用于控制进度条
		::PostMessage(hwnd, UM_PRGRECV, 0, (LPARAM)cbLeftToReceive);
		int iiGet, iiRecd;

		iiGet = (cbLeftToReceive < RECV_BUFFER_SIZE) ?
			cbLeftToReceive : RECV_BUFFER_SIZE;
		iiRecd = recv(sockConn, (char*)recdData, iiGet, 0);


		if (iiRecd == SOCKET_ERROR || iiRecd == 0)
		{
			int iErr = ::GetLastError();
			TRACE("GetFileFromRemoteSite returned a socket error while getting chunked file data\n"
				"\tNumber of Bytes received (zero means connection was closed) = %d\n"
				"\tGetLastError = %d\n", iiRecd, iErr);



			bRet = FALSE;
			goto PreReturnCleanup;
		}


		destFile.Write(recdData, iiRecd);
		cbLeftToReceive -= iiRecd;

	} while (cbLeftToReceive > 0);

	::PostMessage(hwnd, UM_PRGROVER, 0, 0);   //向主窗口发送消息

PreReturnCleanup:						   //文件接收结束标签

	delete[] recdData;					   //释放内存

	if (bFileIsOpen)
		destFile.Close();


	closesocket(sockConn);
	closesocket(socketrecv);

	return bRet;

}

发送部分:

DWORD WINAPI CTransFileDlg::SocketSend(LPVOID lpParameter)
{
	//将参数传递给本地变量
	HWND hwnd = ((socksend*)lpParameter)->hwnd;
	CString sendfname = CA2W(((socksend*)lpParameter)->sendfname);
	DWORD dwip = ((socksend*)lpParameter)->dwip;

	SOCKET socketsend = socket(AF_INET, SOCK_STREAM, 0);//创建Socket

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(dwip);
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(5555);

	int ret1;
	ret1 = connect(socketsend, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//连接指定地址

	if (SOCKET_ERROR == ret1)
	{
		closesocket(socketsend);
		::MessageBox(hwnd, L"连接失败!", L"警告", MB_OK);
		return FALSE;
	}
	::PostMessage(hwnd, UM_PRGSBGN, 0, 0);				//向主窗口发送消息
    
	//声明用于文件发送的本地变量
	BOOL bRet = TRUE;
	int fileLength, cbLeftToSend;
	BYTE* sendData = NULL;

	CFile sourceFile;
	CFileException fe;
	BOOL bFileIsOpen = FALSE;


	//打开参数传递进来的文件用于发送
	if (!(bFileIsOpen = sourceFile.Open(sendfname,
		CFile::modeRead | CFile::typeBinary, &fe)))
	{
		TCHAR strCause[256];
		fe.GetErrorMessage(strCause, 255);
		TRACE("SendFileToRemoteRecipient encountered an error while opening the local file\n"
			"\tFile name = %s\n\tCause = %s\n\tm_cause = %d\n\tm_IOsError = %d\n",
			fe.m_strFileName, strCause, fe.m_cause, fe.m_lOsError);
		bRet = FALSE;
		goto PreReturnCleanup;
	}

	//首先传递将要被发送文件的长度给接收端
	fileLength = sourceFile.GetLength();
	fileLength = htonl(fileLength);
	cbLeftToSend = sizeof(fileLength);

	do
	{
		int cbBytesSent;
		BYTE* bp = (BYTE*)(&fileLength) + sizeof(fileLength) - cbLeftToSend;
		cbBytesSent = send(socketsend, (const char*)bp, cbLeftToSend, 0);
		if (cbBytesSent == SOCKET_ERROR)
		{
			int iErr = ::GetLastError();
			TRACE("SendFileToRemoteRecipient returned a socket error while sending file length\n"
				"\tNumber of Bytes sent = %d\n"
				"\tGetLastError = %d\n", cbBytesSent, iErr);
			bRet = FALSE;
			goto PreReturnCleanup;
		}


		cbLeftToSend -= cbBytesSent;
	} while (cbLeftToSend > 0);

	//开始发送文件
	sendData = new BYTE[SEND_BUFFER_SIZE];
	cbLeftToSend = sourceFile.GetLength();
	do
	{
		::PostMessage(hwnd, UM_PRGSEND, 0, (LPARAM)cbLeftToSend);
		int sendThisTime, doneSoFar, buffOffset;
		sendThisTime = sourceFile.Read(sendData, SEND_BUFFER_SIZE);
		buffOffset = 0;
		do
		{
			doneSoFar = send(socketsend, (const char*)(sendData + buffOffset), sendThisTime, 0);
			if (doneSoFar == SOCKET_ERROR)
			{
				int iErr = ::GetLastError();
				TRACE("SendFileToRemoteRecipient returned a socket error while sending chunked file data\n"
					"\tNumber of Bytes sent = %d\n"
					"\tGetLastError = %d\n", doneSoFar, iErr);
				bRet = FALSE;
				goto PreReturnCleanup;
			}
			buffOffset += doneSoFar;
			sendThisTime -= doneSoFar;
			cbLeftToSend -= doneSoFar;
		} while (sendThisTime > 0);

	} while (cbLeftToSend > 0);
	::PostMessage(hwnd, UM_PRGSOVER, 0, 0);//向主窗口发送消息发送完毕
    
PreReturnCleanup:					//结束标签后释放内存
	delete[] sendData;
	if (bFileIsOpen)
		sourceFile.Close();
	closesocket(socketsend);
	return bRet;
}

image-20250511170623779


二开

需求:

  1. 用户A向用户B提出发送文件请求(而不是原程序那样用户B得先点击“准备接收文件”)。

  2. 用户B同意接收文件或者拒绝。

  3. 如果用户B同意接收,则文件传送开始并保存在默认的位置(界面上应该有这样的设置)。

数据结构:

struct sockrecv {
	HWND hwnd;                // 主窗口句柄
	SOCKET sockConn;          // 连接套接字
	CString defaultPath;      // 默认保存路径
	HANDLE hResponseEvent;    // 用户响应事件
	BOOL bUserResponse;       // 用户是否同意
	CString fileName;         // 文件名
};
struct socksend {
	HWND hwnd;
	DWORD dwip;
	CString fileName;     // 纯文件名
	CString fullFilePath; // 文件完整路径
};

接收端:

DWORD WINAPI CTransFileDlg::SocketRecv(LPVOID lpParameter)
{
	sockrecv* sr = (sockrecv*)lpParameter;  // 接收线程参数
	HWND hwnd = sr->hwnd;                   // 主窗口句柄
	CString defaultPath = sr->defaultPath;   // 默认保存路径
	SOCKET socketrecv = INVALID_SOCKET;

	// 创建监听套接字
	socketrecv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (socketrecv == INVALID_SOCKET)
	{
		CString errMsg;
		errMsg.Format(L"创建套接字失败 (错误码: %d)", WSAGetLastError());
		::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(errMsg));
		delete sr;
		return 1;
	}

	// 绑定地址
	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_addr.s_addr = htonl(INADDR_ANY);
	addrSrv.sin_port = htons(5555);
	if (bind(socketrecv, (SOCKADDR*)&addrSrv, sizeof(addrSrv)) == SOCKET_ERROR)
	{
		CString errMsg;
		errMsg.Format(L"绑定端口失败 (错误码: %d)", WSAGetLastError());
		::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(errMsg));
		closesocket(socketrecv);
		delete sr;
		return 2;
	}

	// 开始监听
	if (listen(socketrecv, 5) == SOCKET_ERROR)
	{
		CString errMsg;
		errMsg.Format(L"监听失败 (错误码: %d)", WSAGetLastError());
		::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(errMsg));
		closesocket(socketrecv);
		delete sr;
		return 3;
	}

	// 持续监听连接
	while (TRUE)
	{
		SOCKADDR_IN addrClient;
		int addrLen = sizeof(addrClient);
		SOCKET sockConn = accept(socketrecv, (SOCKADDR*)&addrClient, &addrLen);
		if (sockConn == INVALID_SOCKET)
		{
			::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(L"接受连接失败"));
			continue;
		}

		// 接收文件名长度(网络字节序)
		int fileNameLength = 0;
		if (recv(sockConn, (char*)&fileNameLength, sizeof(int), 0) <= 0)
		{
			closesocket(sockConn);
			continue;
		}
		fileNameLength = ntohl(fileNameLength);  // 转换为本地字节序

		// 接收文件名(UTF-16格式)
		wchar_t* fileNameBuffer = new wchar_t[fileNameLength + 1];
		int received = 0;
		while (received < fileNameLength * sizeof(wchar_t))
		{
			int ret = recv(sockConn,
				(char*)fileNameBuffer + received,
				fileNameLength * sizeof(wchar_t) - received,
				0);
			if (ret <= 0)
			{
				delete[] fileNameBuffer;
				closesocket(sockConn);
				break;
			}
			received += ret;
		}
		fileNameBuffer[fileNameLength] = L'\0';
		CString fileName = fileNameBuffer;
		delete[] fileNameBuffer;

		// 弹出确认对话框
		sr->fileName = fileName;
		sr->hResponseEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
		::PostMessage(hwnd, UM_FILE_REQUEST, (WPARAM)sr, 0);

		// 等待用户响应(最多等待30秒)
		DWORD waitResult = WaitForSingleObject(sr->hResponseEvent, 30000);
		if (waitResult != WAIT_OBJECT_0 || !sr->bUserResponse)
		{
			send(sockConn, "0", 1, 0);  // 发送拒绝
			closesocket(sockConn);
			CloseHandle(sr->hResponseEvent);
			continue;
		}

		send(sockConn, "1", 1, 0);  // 发送同意

		// 创建目标文件
		CString fullPath = defaultPath + L"\\" + fileName;
		CFile destFile;
		CFileException fe;
		if (!destFile.Open(fullPath, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary, &fe))
		{
			TCHAR errorMsg[256];
			fe.GetErrorMessage(errorMsg, 255);
			::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(errorMsg));
			closesocket(sockConn);
			continue;
		}

		// 接收文件数据
		const int BUFFER_SIZE = 8192;
		BYTE* buffer = new BYTE[BUFFER_SIZE];
		DWORD totalReceived = 0;

		// 先接收文件总大小
		__int64 fileSize = 0;
		recv(sockConn, (char*)&fileSize, sizeof(__int64), 0);
		fileSize = _byteswap_uint64(fileSize);  // 处理字节序

		// 更新进度条
		::PostMessage(hwnd, UM_PRGRBGN, 0, (LPARAM)fileSize);

		// 接收文件内容
		while (totalReceived < fileSize)
		{
			int ret = recv(sockConn, (char*)buffer, BUFFER_SIZE, 0);
			if (ret <= 0) break;

			destFile.Write(buffer, ret);
			totalReceived += ret;

			// 更新进度
			::PostMessage(hwnd, UM_PRGRECV, 0, (LPARAM)(fileSize - totalReceived));
		}

		// 收尾工作
		delete[] buffer;
		destFile.Close();
		closesocket(sockConn);
		::PostMessage(hwnd, UM_PRGROVER, 0, 0);
		CloseHandle(sr->hResponseEvent);
	}

	closesocket(socketrecv);
	delete sr;
	return 0;
}

发送端:

DWORD WINAPI CTransFileDlg::SocketSend(LPVOID lpParameter)
{
	socksend* ss = (socksend*)lpParameter;
	HWND hwnd = ss->hwnd;
	SOCKET socketsend = INVALID_SOCKET;
	CFile sourceFile;
	CFileException fe;
	BOOL bSuccess = TRUE;

	// 1. 创建套接字
	socketsend = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (socketsend == INVALID_SOCKET)
	{
		CString errMsg;
		errMsg.Format(L"创建套接字失败 (错误码: %d)", WSAGetLastError());
		::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(errMsg));
		delete ss;
		return 1;
	}

	// 2. 连接接收方
	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_addr.s_addr = htonl(ss->dwip);
	addrSrv.sin_port = htons(5555);

	if (connect(socketsend, (SOCKADDR*)&addrSrv, sizeof(addrSrv)) == SOCKET_ERROR)
	{
		CString errMsg;
		errMsg.Format(L"连接失败 (错误码: %d)", WSAGetLastError());
		::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(errMsg));
		closesocket(socketsend);
		delete ss;
		return 2;
	}

	// 3. 发送文件名
	try {
		// 3.1 发送文件名长度(网络字节序)
		int fileNameLength = ss->fileName.GetLength();
		int netFileNameLength = htonl(fileNameLength);
		if (send(socketsend, (char*)&netFileNameLength, sizeof(int), 0) != sizeof(int))
			throw L"发送文件名长度失败";

		// 3.2 发送文件名内容(Unicode)
		if (send(socketsend, (const char*)ss->fileName.GetString(),
			fileNameLength * sizeof(wchar_t), 0) != fileNameLength * sizeof(wchar_t))
			throw L"发送文件名内容失败";

		// 4. 等待接收方确认
		char response;
		if (recv(socketsend, &response, 1, 0) != 1 || response != '1')
			throw L"对方拒绝接收文件";

		// 5. 打开本地文件
		if (!sourceFile.Open(ss->fullFilePath, CFile::modeRead | CFile::typeBinary, &fe))
		{
			TCHAR errorMsg[256];
			fe.GetErrorMessage(errorMsg, 255);
			throw errorMsg;
		}

		// 6. 发送文件大小(网络字节序)
		__int64 fileSize = sourceFile.GetLength();
		__int64 netFileSize = _byteswap_uint64(fileSize);
		if (send(socketsend, (char*)&netFileSize, sizeof(__int64), 0) != sizeof(__int64))
			throw L"发送文件大小失败";

		// 7. 发送文件内容
		const DWORD BUFFER_SIZE = 8192;
		BYTE* buffer = new BYTE[BUFFER_SIZE];
		__int64 totalSent = 0;

		// 初始化进度条
		::PostMessage(hwnd, UM_PRGSBGN, 0, (LPARAM)fileSize);

		while (totalSent < fileSize)
		{
			// 读取文件块
			UINT bytesRead = sourceFile.Read(buffer, BUFFER_SIZE);
			if (bytesRead == 0) break;

			// 发送文件块
			DWORD bytesSent = 0;
			while (bytesSent < bytesRead)
			{
				int ret = send(socketsend, (char*)(buffer + bytesSent),
					bytesRead - bytesSent, 0);
				if (ret == SOCKET_ERROR)
					throw L"发送数据时发生网络错误";

				bytesSent += ret;
				totalSent += ret;

				// 更新进度
				::PostMessage(hwnd, UM_PRGSEND, 0, (LPARAM)(fileSize - totalSent));
			}
		}

		delete[] buffer;
		::PostMessage(hwnd, UM_PRGSOVER, 0, 0);
	}
	catch (LPCTSTR errorMsg)
	{
		::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(errorMsg));
		bSuccess = FALSE;
	}
	catch (...)
	{
		::PostMessage(hwnd, UM_LOG_MSG, 0, (LPARAM)new CString(L"未知错误"));
		bSuccess = FALSE;
	}

	// 8. 清理资源
	if (sourceFile.m_hFile != CFile::hFileNull)
		sourceFile.Close();

	if (socketsend != INVALID_SOCKET)
	{
		shutdown(socketsend, SD_BOTH);
		closesocket(socketsend);
	}

	delete ss;
	return bSuccess ? 0 : 3;
}

CAsyncSocket 类

Server Client
1. 构造套接字 CAsyncSocket sockServer CAsyncSocket sockClient
2. 创建句柄 sockServer.Create(nPort) sockClient.Create()
3. 监听 sockServer.Listen()
4. 请求连接 sockClient.Connect(strAddr,nport)
5. 接收连接 CAsyncSocket sockRecv; sockServer.Accept(sockRecv)
6. 接收/发送数据 sockRecv.Receive(pBuf, nLen) sockClient.Send(pBuf, nLen)
7. 发送/接收数据 sockRecv.Send(pBuf, nLen) sockClient.Receive(pBuf, nLen)
8. 关闭套接字 sockRecv.Close() sockClient.Close()

聊天室

服务端

image-20250529210422234

控件与成员变量

控件布局:

image-20250528005406952

注意 listbox 需要关闭排序

.rc文件里的属性如下:

BEGIN
    LTEXT           "监听端口号",IDC_STATIC_PORT,24,25,37,8
    EDITTEXT        IDC_EDIT_PORT,68,22,70,14,ES_AUTOHSCROLL
    PUSHBUTTON      "监听",IDC_BUTTON_LISTEN,146,22,50,14
    PUSHBUTTON      "停止服务",IDOK,200,22,50,14
    LISTBOX         IDC_LIST_MSG,24,44,225,120,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
    LTEXT           "聊天室在线人数: 0",IDC_STATIC_NUM,24,174,67,8
END

类向导添加成员变量:

image-20250523115631653

生成对应的代码:

// TSDlg.c
void CTSDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_BUTTON_LISTEN, m_btnListen);
	DDX_Text(pDX, IDC_EDIT_PORT, m_nPort);
	DDX_Control(pDX, IDC_LIST_MSG, m_listMsg);
	DDX_Control(pDX, IDC_STATIC_NUM, m_staNum);
	DDX_Control(pDX, IDOK, m_btnClose);
}

// TSDlg.h
public:
	CButton m_btnListen;
	UINT m_nPort;
	CListBox m_listMsg;
	CStatic m_staNum;
	CButton m_btnClose;
};

类向导添加事件响应函数:

image-20250524095629123

生成对应代码

// TSDlg.c
void CTSDlg::OnClose()
{
	// TODO: 在此添加控件通知处理程序代码
	CDialogEx::OnOK();
}

void CTSDlg::OnButtonListen()
{
	// TODO: 在此添加控件通知处理程序代码
}

// TSDlg.h
	afx_msg void OnClose();
	afx_msg void OnButtonListen();

派生类

从 CSocket 类派生两个套接字类:

  • CLSocket,监听客户机端的连接请求
  • CCSocket,与客户机端建立连接并交换数据

这两个类都需要添加一个指向对话框类的指针变量

头文件引入 #include <afxsock.h>

再创建一个专用于数据传输序列化处理的类 Msg

类向导里添加 MFC 类

image-20250524172915900


对话框监听按钮

初始化

BOOL CTSDlg::OnInitDialog(){
    // ...
    
    // TODO: 在此添加额外的初始化代码
	m_nPort = 8000;
	UpdateData(FALSE);
	GetDlgItem(IDOK)->EnableWindow(FALSE);
    
	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

通过 TSDlg::OnButtonListen 方法实现

BEGIN_MESSAGE_MAP(CTSDlg, CDialogEx)
	// ...
	ON_BN_CLICKED(IDC_BUTTON_LISTEN, &CTSDlg::OnButtonListen)
END_MESSAGE_MAP()
// ...

void CTSDlg::OnButtonListen()
{
	UpdateData(TRUE);
   	// 初始化Winsock库
	if (!AfxSocketInit())
	{
		AfxMessageBox(_T("Winsock初始化失败,程序无法启动!"));
		return;
	}
    
	m_pLSocket = new CLSocket(this);	// 创建监听套接字
	if (!m_pLSocket->Create(m_nPort)) {
		DWORD dwError = GetLastError();
		CString strError;
		strError.Format(_T("创建套接字失败!错误码: %d"), dwError);

		delete m_pLSocket;
		m_pLSocket = NULL;
		AfxMessageBox(strError);
		return;
	}
	if (!m_pLSocket->Listen()) {
		delete m_pLSocket;
		m_pLSocket = NULL;
		AfxMessageBox(_T("启动监听错误"));
		return;
	}
	GetDlgItem(IDC_EDIT_PORT)->EnableWindow(FALSE);
	GetDlgItem(IDC_BUTTON_LISTEN)->EnableWindow(FALSE);
	GetDlgItem(IDOK)->EnableWindow(TRUE);
}

监听客户端

LSocket 类,监听客户机端的连接请求

#pragma once
class CTSDlg;
// CLSocket 命令目标

class CLSocket : public CSocket
{
	DECLARE_DYNAMIC(CLSocket);	// 动态类声明,用于动态创建对象
public:
	CLSocket(CTSDlg* pDlg);
	virtual ~CLSocket();
public:
	CTSDlg* m_pDlg;

protected:
	virtual void OnAccept(int nErrorCode);
};
// LSocket.cpp: 实现文件
//

#include "pch.h"
#include "TSDlg.h"
#include "LSocket.h"

// CLSocket

CLSocket::CLSocket(CTSDlg* pDlg)
{
	m_pDlg = pDlg;
}

CLSocket::~CLSocket()
{
	m_pDlg = NULL;
}

// CLSocket 成员函数

void CLSocket::OnAccept(int nErrorCode) {
	CSocket::OnAccept(nErrorCode);
	m_pDlg->OnAccept();
}

IMPLEMENT_DYNAMIC(CLSocket, CSocket)

对话框类实现 OnAccept()

// Dlg.h
CPtrList m_connList;
void OnAccept();

// Dlg.cpp
void CTSDlg::OnAccept()
{
	CCSocket* pSocket = new CCSocket(this);	// 创建客户机连接套接字
	if (m_pLSocket->Accept(*pSocket)) {
		pSocket->Initialize();
		m_connList.AddTail(pSocket);	// 将新连接添加至链表尾部
		CString strTemp;
		strTemp.Format(_T("聊天室在线人数: %d"), m_connList.GetCount());
		m_staNum.SetWindowText(strTemp);	// 更新在线人数
	}
	else {
		delete pSocket;
	}
}

CSocket::Initialize() 初始化数据结构

// .h
	CSocketFile* m_pFile;
	CArchive* m_pArchiveIn;
	CArchive* m_pArchiveOut;
	void Initialize();

// .cpp
CCSocket::CCSocket(CTSDlg* pDlg)
{
	m_pDlg = pDlg;
	m_pFile = NULL;
	m_pArchiveIn = NULL;
	m_pArchiveOut = NULL;
}

void CCSocket::Initialize() {
	m_pFile = new CSocketFile(this, TRUE);
	m_pArchiveIn = new CArchive(m_pFile, CArchive::load);
	m_pArchiveOut = new CArchive(m_pFile, CArchive::store);
}

接收/发送消息

CSocket::OnReceive 接收对话框事件

void CCSocket::OnReceive(int nErrorCode) {
	CSocket::OnReceive(nErrorCode);
	m_pDlg->OnReceive(this);
}

对话框中接收消息,并显示在 listbox 组件上

void CTSDlg::DoDataExchange(CDataExchange* pDX)
{
    // ...
	DDX_Control(pDX, IDC_LIST_MSG, m_listMsg);
}

void CTSDlg::OnReceive(CCSocket* pSocket)
{
	static CMsg msg;
	do {
		pSocket->ReceiveMessage(&msg);	// 从套接字中接收消息
		//AfxMessageBox(msg.m_strText);
		//TRACE(_T("Received: %s\n"), msg.m_strText);
		m_listMsg.AddString(msg.m_strText);
		backClients(&msg);
		if (msg.m_bClose) {
			pSocket->Close();
			POSITION pos, temp;
			for (pos = m_connList.GetHeadPosition(); pos != NULL;) {
				temp = pos;
				CCSocket* pSock = (CCSocket*)m_connList.GetNext(pos);
				if (pSock == pSocket) {
					m_connList.RemoveAt(temp);
					CString strTemp;
					strTemp.Format(_T("在线人数: %d"), m_connList.GetCount());
					m_staNum.SetWindowText(strTemp);
					break;
				}
			}
		}
	} while (!pSocket->m_pArchiveIn->IsBufferEmpty());
}

CSocket::ReceiveMessage 接收客户端传来的消息并序列化

void CCSocket::ReceiveMessage(CMsg* pMsg) {
	pMsg->Serialize(*m_pArchiveIn);
}

Msg 类的数据实现的序列化数据结构:

// .h
    CString m_strText;
    BOOL m_bClose;

// .cpp
CMsg::CMsg()
{
	m_strText = _T("");
	m_bClose = FALSE;
}

CMsg::~CMsg()
{
}

void CMsg::Serialize(CArchive& ar) {
	if (ar.IsStoring()) {
		ar << (WORD)m_bClose;
		ar << m_strText;
	}
	else {
		WORD wd;
		ar >> wd;
		m_bClose = (BOOL)wd;
		ar >> m_strText;
	}
}

然后把消息通过 backClients 类转发到其余连接的客户端上

void CTSDlg::backClients(CMsg* pMsg)
{
	for (POSITION pos = m_connList.GetHeadPosition(); pos != NULL;) {
		CCSocket* pSocket = (CCSocket*)m_connList.GetNext(pos);
		pSocket->SendMessage(pMsg);
	}
}

接下来实现 CSocket::SendMessage 发送序列化的消息

void CCSocket::SendMessage(CMsg* pMsg) {
	if (m_pArchiveOut != NULL) {
		pMsg->Serialize(*m_pArchiveOut);
		m_pArchiveOut->Flush();	// 使用 Flush 方法进行传输
	}
}

关闭连接

对话框实现 OnClose 停止监听并发送消息给客户端

void CTSDlg::OnClose()
{
	CMsg msg;
	msg.m_strText = "服务器终止服务!";
	delete m_pLSocket;
	m_pLSocket = NULL;
	while (!m_connList.IsEmpty()) {
		CCSocket* pSocket = (CCSocket*)m_connList.RemoveHead();
		pSocket->SendMessage(&msg);
		delete pSocket;
	}
	while (m_listMsg.GetCount() != 0) {
		m_listMsg.DeleteString(0);
	}
	GetDlgItem(IDC_EDIT_PORT)->EnableWindow(TRUE);
	GetDlgItem(IDC_BUTTON_LISTEN)->EnableWindow(TRUE);
	GetDlgItem(IDOK)->EnableWindow(FALSE);
}

操作演示

启动客户端,服务端

服务端启动监听,客户端连接后发送消息

image-20250528012319478

可以看到客户发送的消息均出现在服务器上,且后面发送的消息出现在了客户端消息框中


C#应用开发

https://learn.microsoft.com/zh-cn/visualstudio/ide/create-csharp-winform-visual-studio?view=vs-2022

image-20250530102152063

框架采用 .NET 8.0

ctrl + alt + x 查看工具箱,获取控件

服务端

以 《C#网络通信程序设计》 4.3 同步套接字编程技术 为例

添加控件