前言
学学协议设计,加深下理解(?
基础
三元组标识 应用层进程=(传输层协议, 主机的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地址
面向连接的套接字编程
服务端:
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
客户端
注意,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;
}
改进
在服务器显示连接上的客户机的 IP 地址和端口
服务端在 accept 的时候保留了客户端的信息 addrClient,只需要将其转格式输出即可
使用
inet_ntoa
把inet_addr
的网络字节序转换为 IP 地址,ntohs
对应客户端的htons
sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len); printf("客户端 %s:%d\n",inet_ntoa(addrClient.sin_addr),ntohs(addrClient.sin_port));
服务器也发送一条信息(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);
完成步骤 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);
修改程序使得以下情况能正常运行:服务器程序不关闭,客户机程序可重复运行关闭,两端正常接收和发送消息
直接
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; }
一台服务器,多台客户机的情况下,由于服务端只能同时处理一个端口上的连接,会出现其他客户端等待的情况
在 UNIX 系统下,可以使用
fork()
创建子进程解决这个问题
利用流式套接字实现文件传送
使用 VC++ 6.0
创建 Win32 Console Application 工程
服务端
#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 运行
成功下载文件 test.txt 并保存为 save.txt
无连接的套接字编程
特点:
- 应用程序双方是不对等的,服务器要先行启动,处于被动的等待访问的状态,而客户机则可以随时主动地请求访问服务器
- 服务器进程将套接字绑定到指定的端口,客户端需要知道服务端的网络地址
- 客户端套接字使用动态分配的自由端口,不需要进行绑定
- 客户端必须先发送数据报,在数据报中携带双方的地址,服务端收到后才能知道客户机端的地址,才能给客户机端回送数据报
- 服务器可以接收多个客户端的数据
系统调用:
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;
}
先启动服务端,再启动客户端
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应用 新建项目
应用程序类型选择 基于对话框
- 单个文档:像Windows记事本、Windows画图、Windows写字板这样的程序,一个程序只有一个文档处于编辑状态。
- 多个文档:像Word,Excel这样可以在一个MDI窗口里面同时处理多个文档的类型。
- 基于对话框:像Windows扫雷、纸牌那样直接在对话框进行操作的程序。用不着文档。
新建项目完成后界面如下:
项目结构
引用
假设这里引用了另一个项目的 DLL1
那么会有三个用处:
决定生成整个解决方案的项目顺序:如果不引用 Dll1,点击生成整个解决方案时,项目生成顺序会是 ConsoleApplication2.1,Dll1【按照顺序从上到下】。结果是报错。
因为 ConsoleApplication2.1 使用了 Dll1,可 Dll1 还没有生成,所以 ConsoleApplication2.1 找不到 Dll1 的 lib 或 dll 文件。
像这种情况很多公司常见,这时候往往会多生成几遍解决方案,错误会逐渐减少,直至最后成功生成所有项目。
用于生成单个项目:如果我清理了解决方案,然后右键生成ConsoleApplication2.1,会报错,找不到Dll1。所以这时候应该在“引用”中添加Dll1。然后右键生成ConsoleApplication2.1,这时,会先生成Dll1,再生成ConsoleApplication2.1。
用于导出:省略了
__declspec(dllexport)
。导出库中需要导出的所有前边可省略__declspec(dllexport)
。
外部依赖项
工程中显式包含的那些头文件本身所包含的头文件(非自己定义),主要包含一些外部库
头文件
framework.h:定义了项目所使用的 MFC 应用程序框架相关的头文件。通常包括预编译头文件的引用。
MFC_Test.h:这是主应用程序类的头文件,一般从 CWinApp 或其派生类继承
MFC_TestDlg.h::这是对话框类的头文件,用于定义主对话框类及其成员函数和变量。对于以对话框为基础的应用程序,这是主要的用户界面。
pch.h(Pre-compiled Header):预编译头文件,用于提高编译速度。会包含项目中常用的头文件。被预先编译过的头文件,对于比较大型的工程,往往编译时间会很久,通过使用 PCH,把那些不经常发生改动的头文件都预先编译出来,就可以大大节省实际使用时候的编译时间。实际应用中,还经常把外部调用的 API 的头文件编译为 PCH,比如调用 STL、调用 Windows 的 APIwindows.h 等等。
缺点的话就是降低了文件间的关联性,只知道包含了pch,但不知道具体用到了是哪些头文件
Resource.h:定义了资源的ID,如对话框、控件、菜单、字符串等资源的标识符。
targetver.h:定义了目标版本的Windows平台,用于确保项目能正确地在指定的平台上进行编译。
源文件
- MFC_Test.cpp:主应用程序文件,包含CWinApp派生类的实现代码,如应用程序初始化和退出例程。
- MFC_TestDlg.cpp:对话框类的实现文件,包含主要的用户界面逻辑和事件处理函数。
- pch.cpp:用于生成预编译头文件的源文件。
资源文件
MFC_Test.ico:应用程序图标文件。
MFCTest.rc:资源脚本文件,包含应用程序的资源定义,如图标、对话框、菜单等。这些资源在编译时被编译进应用程序的可执行文件中。
其他的资源文件(如字符串表等):这些文件也会包含在资源文件中,有时会在目录中显示。
项目流程
- 框架初始化:MFC_Test.cpp 中的 CWinApp 派生类负责初始化应用程序框架。
- 用户界面:MFC_TestDlg.h 和 MFC_TestDlg.cpp 中的对话框类负责展示和处理用户交互。
- 资源管理:各资源文件(如.rc文件和.h文件中的ID定义)提供UI元素和其他资源的定义和引用。
添加UI控件
打开资源视图,找到对话框资源(通常是 IDD_[项目名]_DIALOG)
添加控件
Ctrl + Alt + X 呼出工具箱
从工具箱中拖放工具来添加按钮、标签、目录选择控件等
这里添加一个 Button
添加控制变量,修改属性,添加对应函数
右键这个 Button 添加控制变量
于是 MFC_TestDlg.h 中会出现如下代码:
public: CButton Test; };
修改属性
在添加完一个控件(按钮)之后,右键点击按钮选择属性,便可以编辑它的属性
添加对应函数:
双击按钮可以自动生成按钮的点击时间处理函数(没有定义按钮点击事件处理函数之前)
在 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: 在此添加控件通知处理程序代码
}
直接设置文本
使用 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");
}
对话框数据交换
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"文件接收完成!");
}
}
主机扫描
多线程文件传输
窗口:
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;
}
二开
需求:
用户A向用户B提出发送文件请求(而不是原程序那样用户B得先点击“准备接收文件”)。
用户B同意接收文件或者拒绝。
如果用户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() |
聊天室
服务端
控件与成员变量
控件布局:
注意 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
类向导添加成员变量:
生成对应的代码:
// 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;
};
类向导添加事件响应函数:
生成对应代码
// 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 类
对话框监听按钮
初始化
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);
}
操作演示
启动客户端,服务端
服务端启动监听,客户端连接后发送消息
可以看到客户发送的消息均出现在服务器上,且后面发送的消息出现在了客户端消息框中
C#应用开发
https://learn.microsoft.com/zh-cn/visualstudio/ide/create-csharp-winform-visual-studio?view=vs-2022
框架采用 .NET 8.0
ctrl + alt + x 查看工具箱,获取控件
服务端
以 《C#网络通信程序设计》 4.3 同步套接字编程技术 为例