网络编程------从服务器中转下载服务
// FileDownloadThroughServer.cpp : Defines the entry point for the console application. // /*---------------------------------------------------------------------------------------------------------- 功能: 建立C/S结构的文件下载系统。每个客户端可以提供不定数目的文件用于共享,也可以下载别人共享出来的文件。 文件存在于各个客户端上,而不在服务器上。 要允许一个客户端同时下载多个文件,也要允许同时被多个别的客户端下载。 思路: 1.服务器至少应该开放3个TCP端口供客户端连接。 (1)第一个用来接收客户端发来的共享文件名以及向客户端发送别人想要下载他的文件的通知,不妨把这个端口叫做通知端口。 (2)第二个用来向客户端每隔10秒发送最新共享文件列表,把它叫做刷新端口。 (3)第三个用来提供文件传输的中转服务,叫做传输端口。 2.本系统是围绕着共享文件而工作的,为了方便起见,应该把一个共享文件的全部信息封装成“共享文件结构体”,成员变量 应该包括文件名、文件所在客户端的地址、服务器对应于这个客户端的通知套接字。 3.通知端口和传输端口应该设计各自的长度固定的头部(可设计成结构体),叫做“通知头部”和“传输头部”。 其中“传输头部”又有下载和上传两种。这样这两个端口的recv()函数就可以指定固定的接收长度了。 4.服务器和客户端都要大量使用多线程。 5.服务器可以用c++stl的multimap来保存共享文件列表,multimap的第一个字段是服务器上面对应于文件所在客户端的通知套接字, 第二字段是共享文件结构体。这样设计一来是为了在客户端退出的时候能最方便地从multimap中删除这个客户端的所有共享文件信息, 二来是为了在用户指定要下载哪个文件时不用通过搜索就知道该向哪个客户端发送通知。由于这个数据结构在客户端登录和 退出时都要被修改,所以必须有并发控制。 6.客户端的数据结构可以简单地用c++stl的vector来保存服务器发来的最新共享文件列表。vector中每个元素都是 共享文件结构体。为防止用户选择文件时正好在刷新共享文件列表,也需要有并发控制。但应知道,这里的并发控制可以 防止程序的运行出错,但并不能杜绝下载到非指定文件的可能性。 7.系统的工作流程大致如下:服务器在3个端口上等待,客户端首先连接到通知端口,在这条连接上发送自己的共享文件名, 然后同样在这条连接上准备接收“通知头部”。接着客户端每隔10秒连接到刷新端口并接收最新共享文件列表。 客户端还需要用一个线程来准备读入用户的键盘输入,当用户有输入时,客户端连接到传输端口并发送一个表示下载的 “传输头部”(其中最重要的就是一个共享文件结构体),然后在这条连接上准备接收文件数据。服务器收到这个 “传输头部”后从中得到文件所在客户端的套接字,并构造一个“通知头部”(包括想下载文件的客户端的套接字和想下载的 文件名)发送到这个套接字上。被下载方在通知端口上收到“通知头部”后,也连接到传输端口,先发送一个表示上传 的“传输头部”(其中最重要的就是下载方的套接字),以便让服务器知道该把文件转发给谁,接着就可以开始上传文件了。 考查: 1.结合源代码和注释,搞懂这个系统的结构和流程,掌握套接字编程和多线程编程的相关技术,准备回答问题。 2.已知:getsockname()函数可以获得套接字所使用的地址。自行查阅这个函数的原型,并在源程序基础上添加代码, 使用户试图下载自己的文件时能阻止并提示“这个文件是你自己的”。 3.按照本源程序的方案,分析在大负荷的情况下系统会有什么表现?应怎么改进?动手改源程序或者准备回答问题。 4.本源程序除了未进行差错处理和用户指定下载一个前10秒之内退出系统的客户端上的文件时会得不到正确提示 这些小问题外,故意留有一个真正的bug,此bug如果被激发,不仅会导致系统在一切正常并且被下载方没有退出时 也会下载不成功,而且服务器有不确定的动作。试进行debug。 ----------------------------------------------------------------------------------------------------------*/ #include "stdio.h" #include <map> #include <vector> #include "Winsock2.h" #pragma warning(disable:4996) #pragma comment(lib, "ws2_32.lib") //宏定义 //--------------------------------------------------------------------------------------------------------- #define SERVER_NOTIFY_PORT 1025 //通知端口 #define SERVER_REFRESH_PORT 1026 //刷新端口 #define SERVER_TRANSFER_PORT 1027 //传输端口 #define COMMAND_DOWNLOAD 0 //“传输头部”中表示下载的命令 #define COMMAND_UPLOAD 1 //“传输头部”中表示上传的命令 //--------------------------------------------------------------------------------------------------------- //结构体定义 //--------------------------------------------------------------------------------------------------------- #pragma pack(4) //设置结构体按照4字节对齐 struct SHARED_FILE //共享文件结构体,封装一个共享文件的所有信息。服务器端和客户端都要用到 { char filename[100]; //文件名 struct sockaddr_in client_addr; //文件所在的客户端的网络地址 SOCKET notify_sock; //服务器上面对应于这个客户端的通知套接字 }; struct TRANSFER_HEADER //“传输头部”,由客户端发往服务器,准备开始一次下载或上传 { int cmd; //命令,指明本次传输是为了下载还是上传,0-下载,1-上传 union { SHARED_FILE shared_file; //如果是下载,需要告诉服务器想下载的共享文件结构体 SOCKET sock; //如果是上传,需要告诉服务器想下载这个文件的客户端的套接字 }; }; struct NOTIFY_HEADER //“通知头部”,由服务器发往客户端,通知他有人想下载他的某个文件 { SOCKET sock; //告诉客户端是谁想下载他的文件 char filename[100]; //告诉客户端想下载他的哪个文件 }; #pragma pack() //取消结构体的字节对齐 //--------------------------------------------------------------------------------------------------------- //全局变量 //--------------------------------------------------------------------------------------------------------- //服务器端的核心数据结构(多映射),用来保存所有共享文件的信息。映射的第一个字段是服务器上面对应于客户端的通知套接字,第二个字段是客户端的一个共享文件结构体 std::multimap<SOCKET, SHARED_FILE> g_shared_files; //客户端的两个核心数据结构(数组),第一个用来暂存服务器发来的最新共享文件列表,第二个是第一个的拷贝 std::vector<SHARED_FILE> g_files_list; std::vector<SHARED_FILE> g_files_list2; unsigned int g_serverip; //服务器的IP,网络字节序表示 HANDLE g_server_semaphore; //服务器使用的信号量,用于控制对g_shared_files的并发访问 HANDLE g_client_semaphore; //客户端使用的信号量,用于控制对g_files_list2的并发访问 //--------------------------------------------------------------------------------------------------------- int mysend(SOCKET sock, char* buf, int len, int flags) { int sent = 0, remain = len; while (remain > 0) { int n = send(sock, buf + sent, remain, flags); if (n == -1) //出错的最大可能是对方关闭了套接字 break; remain -= n; sent += n; } return sent; } int myrecv(SOCKET sock, char* buf, int len, int flags) { int received = 0, remain = len; while (remain > 0) { int n = recv(sock, buf + received, remain, flags); if (n == 0 || n == -1) //0是对方调用closesocket(),-1是对方直接退出 break; remain -= n; received += n; } return received; } //------------------------------------------------------------------------------------------------------- //功能:服务器用于判断某个客户端是否下线 //参数:服务器上对应于某个客户端的通知套接字 //原理:由于通知端口的TCP连接是一直保持的,所以服务器在这个端口上调用recv()将会一直阻塞,直到客户端下线 //创建者:server_notify_thread() DWORD WINAPI server_quit_thread(LPVOID lpParam) { SOCKET comm_sock = (SOCKET)lpParam; while (1) { char c; int ret = recv(comm_sock, &c, 1, 0); if (ret == 0 || ret == -1) break; } printf("有一个客户端退出了\n"); //删除g_shared_files中的信息 WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 g_shared_files.erase(comm_sock); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器把最新共享文件列表发给客户端 //参数:服务器在刷新端口上的监听套接字 //原理:服务器在刷新端口上等待连接,建立连接后通过通信套接字把最新共享文件列表发给客户端 //创建者:main() DWORD WINAPI server_refresh_thread(LPVOID lpParam) { SOCKET server_refresh_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_refresh_sock, (struct sockaddr*)&client_addr, &size); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_refresh_thread, (void*)server_refresh_sock, 0, NULL); //向客户端发送共享文件列表。为防止遍历的过程中有客户端登录或退出,要进行并发控制 WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 std::multimap<SOCKET, SHARED_FILE>::const_iterator it; for (it = g_shared_files.begin(); it != g_shared_files.end(); it++) mysend(comm_sock, (char*)&(it->second), sizeof(SHARED_FILE), 0); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 //最后发一个空文件名告诉客户端已发完 SHARED_FILE sf; sf.filename[0] = '\0'; mysend(comm_sock, (char*)&sf, sizeof(SHARED_FILE), 0); closesocket(comm_sock); return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器接受一个客户端上线 //参数:服务器在通知端口上的监听套接字 //原理:服务器在通知端口上等待连接,建立连接后接收客户端发来的共享文件名,并构造结构体存入多映射 //创建者:main() DWORD WINAPI server_notify_thread(LPVOID lpParam) { SOCKET server_notify_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_notify_sock, (struct sockaddr*)&client_addr, &size); if (comm_sock == INVALID_SOCKET) { printf("accept() error!\n"); exit(0); } printf("有一个客户端上线了\n"); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_notify_thread, (void*)server_notify_sock, 0, NULL); //接收客户端发来的共享文件名,构造成共享文件结构体并加入g_shared_files保存 char buf[100]; SHARED_FILE sf; sf.client_addr = client_addr; sf.notify_sock = comm_sock; while (1) { myrecv(comm_sock, buf, 100, 0); if (buf[0] == '\0') //空文件名,说明客户端已发完所有共享文件名 break; strcpy(sf.filename, buf); WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 g_shared_files.insert(std::make_pair(comm_sock, sf)); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 } //创建线程,用来接收这个客户端退出的通知,以便及时从g_shared_files中删除信息 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_quit_thread, (void*)comm_sock, 0, NULL); //注意:这个线程虽然退出但是这条连接并未关闭,它将一直维持,直到客户端下线 return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器为文件传输做中转服务 //参数:服务器在传输端口上的监听套接字 //原理:服务器在传输端口上等待连接,建立连接后接收一个“传输头部”,并根据下载还是上传做相应中转 //创建者:main() DWORD WINAPI server_transfer_thread(LPVOID lpParam) { SOCKET server_transfer_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_transfer_sock, (struct sockaddr*)&client_addr, &size); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_transfer_thread, (void*)server_transfer_sock, 0, NULL); //接收一个“传输头部” TRANSFER_HEADER th; myrecv(comm_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); if (th.cmd == COMMAND_DOWNLOAD) //如果这个连接是为了下载文件 { //构造一个“通知头部”准备发给被下载方 NOTIFY_HEADER nh; nh.sock = comm_sock; //将下载方的套接字告诉被下载方,以便将来他上传文件时回传这个套接字 strcpy(nh.filename, th.shared_file.filename); //将文件名告诉被下载方 //从“传输头部”的共享文件结构体中取出被下载方对应的通知套接字 SOCKET client_notify_sock = th.shared_file.notify_sock; //向这个通知套接字发送“通知头部” int ret = mysend(client_notify_sock, (char*)&nh, sizeof(NOTIFY_HEADER), 0); if (ret != sizeof(NOTIFY_HEADER)) //说明被下载方已退出 closesocket(comm_sock); } else if (th.cmd == COMMAND_UPLOAD) //如果这个连接是为了上传文件 { //从“传输头部”中取出下载方对应的套接字 SOCKET download_client_sock = th.sock; //循环接收数据并转发给下载方 char buf[1024]; while (1) { int i = recv(comm_sock, buf, 1024, 0); //不需要调用myrecv(),因为不强求收满1024个字节 if (i > 0) mysend(download_client_sock, buf, i, 0); //但转发一定要保证发完i个字节 else { closesocket(download_client_sock); break; } } } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端每隔10秒向服务器请求最新共享文件列表 //参数:NULL //原理:客户端每隔10秒创建一个套接字,连接到服务器的刷新端口,然后循环接收数据,每次接收一个共享文件结构体,接收完成后断开连接并进行屏幕显示 //创建者:main() DWORD WINAPI client_refresh_thread(LPVOID lpParam) { sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_REFRESH_PORT); while (1) { //创建流套接字并连接到服务器的刷新端口 SOCKET refresh_sock = socket(AF_INET, SOCK_STREAM, 0); connect(refresh_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //接收最新共享文件列表 while (1) { SHARED_FILE sf; myrecv(refresh_sock, (char*)&sf, sizeof(SHARED_FILE), 0); //接收一个SHARED_FILE结构体 if (sf.filename[0] != '\0') //如果不是空文件名 g_files_list.push_back(sf); //加入g_files_list保存 else //空文件名,表示服务器本次发送共享文件列表已经完毕 break; } closesocket(refresh_sock); //为防止此时用户正在选择g_files_list2中的某个文件,进行并发控制 WaitForSingleObject(g_client_semaphore, INFINITE); //获取信号量 g_files_list2.clear(); g_files_list2 = g_files_list; ReleaseSemaphore(g_client_semaphore, 1, NULL); //释放信号量 g_files_list.clear(); //清空g_files_list,准备下一次接收共享文件列表 printf("最新共享文件列表:\n"); std::vector<SHARED_FILE>::const_iterator it; int i = 1; for (it = g_files_list2.begin(); it != g_files_list2.end(); it++, i++) { printf("%d - %s:%d上的%s", i, inet_ntoa(it->client_addr.sin_addr), ntohs(it->client_addr.sin_port), it->filename); printf("\n"); } printf("请输入文件的序号进行下载(0-退出):\n"); Sleep(10000); } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端进行文件上传 //参数:指向“通知头部”的指针 //原理:客户端从“通知头部”中得知是谁想下载他的文件以及想下载哪个文件,然后构造出表示上传的“传输头部”,连接到服务器的传输端口,首先发送“传输头部”然后发送文件 //创建者:client_notify_thread() DWORD WINAPI client_upload_thread(LPVOID lpParam) { NOTIFY_HEADER* pnh = (NOTIFY_HEADER*)lpParam; //创建套接字并连接到服务器的SERVER_TRANSFER_PORT端口 SOCKET client_upload_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_TRANSFER_PORT); connect(client_upload_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //构造一个表示上传的“传输头部”并发送给服务器 TRANSFER_HEADER th; th.cmd = COMMAND_UPLOAD; //指明这个连接是为了上传 th.sock = pnh->sock; //指明想下载这个文件的客户端套接字 mysend(client_upload_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); FILE* fp = fopen(pnh->filename, "rb"); if (fp != NULL) printf("开始上传文件%s\n", pnh->filename); else { printf("找不到文件%s,上传失败\n", pnh->filename); closesocket(client_upload_sock); delete pnh; return 0; } //循环读取文件数据并上传 char buf[1024]; while (1) { int i = fread(buf, 1, 1024, fp); mysend(client_upload_sock, buf, i, 0); if (i < 1024) //fread()没读满1024个字节表示是最后一次读文件了 break; } fclose(fp); closesocket(client_upload_sock); printf("文件%s上传完成\n", pnh->filename); delete pnh; return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端响应服务器发来的通知 //参数:客户端已经同服务器的通知端口建立连接的通知套接字 //原理:客户端在已经同服务器的通知端口建立了连接的通知套接字上等待接收“通知头部”,收到后以这个“通知头部”为参数启动client_upload_thread线程进行文件的上传 //创建者:main() DWORD WINAPI client_notify_thread(LPVOID lpParam) { SOCKET client_notify_sock = (SOCKET)lpParam; while (1) { //接收一个“通知头部” NOTIFY_HEADER nh; myrecv(client_notify_sock, (char*)&nh, sizeof(NOTIFY_HEADER), 0); printf("收到一个下载%s的通知\n", nh.filename); NOTIFY_HEADER* pnh = new NOTIFY_HEADER; //不能直接把nh的指针传给新线程,因为nh在本线程的栈中,随时可能被覆盖 *pnh = nh; //创建线程,用来上传文件 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)client_upload_thread, (void*)pnh, 0, NULL); } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端进行指定文件的下载 //参数:指向想下载的共享文件结构体的指针 //原理:客户端根据想下载的共享文件结构体构造出表示下载的“传输头部”,连接到服务器的传输端口并发送这个“传输头部”,然后循环等待接收文件数据并存盘 //创建者:client_userinput_thread() DWORD WINAPI client_download_thread(LPVOID lpParam) { SHARED_FILE* psf = (SHARED_FILE*)lpParam; //创建套接字并连接到服务器的SERVER_TRANSFER_PORT端口 SOCKET client_download_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_TRANSFER_PORT); connect(client_download_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //构造一个表示下载的“传输头部”并发送给服务器 TRANSFER_HEADER th; th.cmd = COMMAND_DOWNLOAD; //指明这个连接是为了下载 th.shared_file = *psf; //指明想下载哪一个文件 delete psf; mysend(client_download_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); printf("开始下载%s:%d的%s\n", inet_ntoa(th.shared_file.client_addr.sin_addr), ntohs(th.shared_file.client_addr.sin_port), th.shared_file.filename); FILE* fp = fopen(th.shared_file.filename, "wb"); //循环接收数据并写入文件 char buf[1024]; while (1) { int i = recv(client_download_sock, buf, 1024, 0); //不需要调用myrecv() if (i > 0) fwrite(buf, 1, i, fp); else break; } fclose(fp); closesocket(client_download_sock); printf("%s:%d的%s下载完毕\n", inet_ntoa(th.shared_file.client_addr.sin_addr), ntohs(th.shared_file.client_addr.sin_port), th.shared_file.filename); return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端接收并处理用户的键盘输入 //参数:NULL //原理:客户端等待用户的键盘输入,收到输入后从共享文件结构体数组中取出相应的结构体,并以此为参数启动client_download_thread线程进行文件的下载 //创建者:main() DWORD WINAPI client_userinput_thread(LPVOID lpParam) { int number; while (1) { scanf("%d", &number); //客户端退出的方式是用户按下0 if (number == 0) { CloseHandle(g_client_semaphore); exit(0); } //为防止此时正在刷新g_files_list2,进行并发控制 WaitForSingleObject(g_client_semaphore, INFINITE); //获取信号量 if (number > g_files_list2.size()) printf("请输入正确的序号\n"); else { SHARED_FILE* psf = new SHARED_FILE; *psf = g_files_list2[number - 1]; //创建线程,用来完成下载任务 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)client_download_thread, (void*)psf, 0, NULL); } ReleaseSemaphore(g_client_semaphore, 1, NULL); //释放信号量 } return 0; } //------------------------------------------------------------------------------------------------------- int main(int argc, char* argv[]) { if (argc == 1) { printf("使用方法 : \nfilename -server\n或\nfilename -client 服务器IP [共享文件名1] ...\n"); return 0; } //下面4行进行Windows网络环境的初始化。Linux中不需要 WORD wVersionRequested; WSADATA wsaData; wVersionRequested = MAKEWORD(2, 2); WSAStartup(wVersionRequested, &wsaData); if (argc == 2 && strcmp(argv[1], "-server") == 0) { //创建信号量,初值为1,用来控制对g_shared_files的并发修改 g_server_semaphore = CreateSemaphore(NULL, 1, 1, NULL); //创建通知端口的监听套接字 SOCKET server_notify_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_notify_sock == INVALID_SOCKET) //Linux下失败返回-1 { printf("socket() error!\n"); exit(0); } sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SERVER_NOTIFY_PORT); if (-1 == bind(server_notify_sock, (struct sockaddr*)&server_addr, sizeof(server_addr))) { printf("bind() error!\n"); exit(0); } if (-1 == listen(server_notify_sock, 5)) { printf("listen() error!\n"); exit(0); } //创建线程,用来在通知端口上接受连接请求 CreateThread(NULL, 0, server_notify_thread, (void*)server_notify_sock, 0, NULL); //创建刷新端口的监听套接字 SOCKET server_refresh_sock = socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_port = htons(SERVER_REFRESH_PORT); bind(server_refresh_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(server_refresh_sock, 5); //创建线程,用来在刷新端口上接受连接请求 CreateThread(NULL, 0, server_refresh_thread, (void*)server_refresh_sock, 0, NULL); //创建传输端口的监听套接字 SOCKET server_transfer_sock = socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_port = htons(SERVER_TRANSFER_PORT); bind(server_transfer_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(server_transfer_sock, 5); //创建线程,用来在传输端口上接受连接请求 CreateThread(NULL, 0, server_transfer_thread, (void*)server_transfer_sock, 0, NULL); printf("服务器在3个端口开始监听连接请求\n"); //消息循环,目的是不让主线程退出。Linux中换成while(1) pause(); MSG msg; while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } } else if (argc >= 3 && strcmp(argv[1], "-client") == 0) { //创建信号量,初值为1,用来控制对g_files_list2的并发读写 g_client_semaphore = CreateSemaphore(NULL, 1, 1, NULL); g_serverip = inet_addr(argv[2]); //创建用于连接到服务器通知端口的套接字 SOCKET client_notify_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_NOTIFY_PORT); connect(client_notify_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); printf("客户端请求连接成功\n"); //发送共享文件名,文件名长度与服务器约定为100字节 char buf[100]; int i; for (i = 3; i < argc; i++) { strcpy(buf, argv[i]); mysend(client_notify_sock, buf, 100, 0); } //最后发送一个空文件名告诉服务器共享文件名已发完 buf[0] = '\0'; mysend(client_notify_sock, buf, 100, 0); printf("客户端发送共享文件名成功\n"); //创建线程,用来接收服务器发来的“通知头部” CreateThread(NULL, 0, client_notify_thread, (void*)client_notify_sock, 0, NULL); //创建线程,每隔10秒向服务器请求最新的共享文件列表 CreateThread(NULL, 0, client_refresh_thread, NULL, 0, NULL); //创建线程,用来接收用户的键盘输入 CreateThread(NULL, 0, client_userinput_thread, NULL, 0, NULL); //消息循环,目的是不让主线程退出。Linux中换成while(1) pause(); MSG msg; while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } } else printf("启动参数不正确\n"); return 0; } // FileDownloadThroughServer.cpp : Defines the entry point for the console application. // /*---------------------------------------------------------------------------------------------------------- 功能: 建立C/S结构的文件下载系统。每个客户端可以提供不定数目的文件用于共享,也可以下载别人共享出来的文件。 文件存在于各个客户端上,而不在服务器上。 要允许一个客户端同时下载多个文件,也要允许同时被多个别的客户端下载。 思路: 1.服务器至少应该开放3个TCP端口供客户端连接。 (1)第一个用来接收客户端发来的共享文件名以及向客户端发送别人想要下载他的文件的通知,不妨把这个端口叫做通知端口。 (2)第二个用来向客户端每隔10秒发送最新共享文件列表,把它叫做刷新端口。 (3)第三个用来提供文件传输的中转服务,叫做传输端口。 2.本系统是围绕着共享文件而工作的,为了方便起见,应该把一个共享文件的全部信息封装成“共享文件结构体”,成员变量 应该包括文件名、文件所在客户端的地址、服务器对应于这个客户端的通知套接字。 3.通知端口和传输端口应该设计各自的长度固定的头部(可设计成结构体),叫做“通知头部”和“传输头部”。 其中“传输头部”又有下载和上传两种。这样这两个端口的recv()函数就可以指定固定的接收长度了。 4.服务器和客户端都要大量使用多线程。 5.服务器可以用c++stl的multimap来保存共享文件列表,multimap的第一个字段是服务器上面对应于文件所在客户端的通知套接字, 第二字段是共享文件结构体。这样设计一来是为了在客户端退出的时候能最方便地从multimap中删除这个客户端的所有共享文件信息, 二来是为了在用户指定要下载哪个文件时不用通过搜索就知道该向哪个客户端发送通知。由于这个数据结构在客户端登录和 退出时都要被修改,所以必须有并发控制。 6.客户端的数据结构可以简单地用c++stl的vector来保存服务器发来的最新共享文件列表。vector中每个元素都是 共享文件结构体。为防止用户选择文件时正好在刷新共享文件列表,也需要有并发控制。但应知道,这里的并发控制可以 防止程序的运行出错,但并不能杜绝下载到非指定文件的可能性。 7.系统的工作流程大致如下:服务器在3个端口上等待,客户端首先连接到通知端口,在这条连接上发送自己的共享文件名, 然后同样在这条连接上准备接收“通知头部”。接着客户端每隔10秒连接到刷新端口并接收最新共享文件列表。 客户端还需要用一个线程来准备读入用户的键盘输入,当用户有输入时,客户端连接到传输端口并发送一个表示下载的 “传输头部”(其中最重要的就是一个共享文件结构体),然后在这条连接上准备接收文件数据。服务器收到这个 “传输头部”后从中得到文件所在客户端的套接字,并构造一个“通知头部”(包括想下载文件的客户端的套接字和想下载的 文件名)发送到这个套接字上。被下载方在通知端口上收到“通知头部”后,也连接到传输端口,先发送一个表示上传 的“传输头部”(其中最重要的就是下载方的套接字),以便让服务器知道该把文件转发给谁,接着就可以开始上传文件了。 考查: 1.结合源代码和注释,搞懂这个系统的结构和流程,掌握套接字编程和多线程编程的相关技术,准备回答问题。 2.已知:getsockname()函数可以获得套接字所使用的地址。自行查阅这个函数的原型,并在源程序基础上添加代码, 使用户试图下载自己的文件时能阻止并提示“这个文件是你自己的”。 3.按照本源程序的方案,分析在大负荷的情况下系统会有什么表现?应怎么改进?动手改源程序或者准备回答问题。 4.本源程序除了未进行差错处理和用户指定下载一个前10秒之内退出系统的客户端上的文件时会得不到正确提示 这些小问题外,故意留有一个真正的bug,此bug如果被激发,不仅会导致系统在一切正常并且被下载方没有退出时 也会下载不成功,而且服务器有不确定的动作。试进行debug。 ----------------------------------------------------------------------------------------------------------*/ #include "stdio.h" #include <map> #include <vector> #include "Winsock2.h" #pragma warning(disable:4996) #pragma comment(lib, "ws2_32.lib") //宏定义 //--------------------------------------------------------------------------------------------------------- #define SERVER_NOTIFY_PORT 1025 //通知端口 #define SERVER_REFRESH_PORT 1026 //刷新端口 #define SERVER_TRANSFER_PORT 1027 //传输端口 #define COMMAND_DOWNLOAD 0 //“传输头部”中表示下载的命令 #define COMMAND_UPLOAD 1 //“传输头部”中表示上传的命令 //--------------------------------------------------------------------------------------------------------- //结构体定义 //--------------------------------------------------------------------------------------------------------- #pragma pack(4) //设置结构体按照4字节对齐 struct SHARED_FILE //共享文件结构体,封装一个共享文件的所有信息。服务器端和客户端都要用到 { char filename[100]; //文件名 struct sockaddr_in client_addr; //文件所在的客户端的网络地址 SOCKET notify_sock; //服务器上面对应于这个客户端的通知套接字 }; struct TRANSFER_HEADER //“传输头部”,由客户端发往服务器,准备开始一次下载或上传 { int cmd; //命令,指明本次传输是为了下载还是上传,0-下载,1-上传 union { SHARED_FILE shared_file; //如果是下载,需要告诉服务器想下载的共享文件结构体 SOCKET sock; //如果是上传,需要告诉服务器想下载这个文件的客户端的套接字 }; }; struct NOTIFY_HEADER //“通知头部”,由服务器发往客户端,通知他有人想下载他的某个文件 { SOCKET sock; //告诉客户端是谁想下载他的文件 char filename[100]; //告诉客户端想下载他的哪个文件 }; #pragma pack() //取消结构体的字节对齐 //--------------------------------------------------------------------------------------------------------- //全局变量 //--------------------------------------------------------------------------------------------------------- //服务器端的核心数据结构(多映射),用来保存所有共享文件的信息。映射的第一个字段是服务器上面对应于客户端的通知套接字,第二个字段是客户端的一个共享文件结构体 std::multimap<SOCKET, SHARED_FILE> g_shared_files; //客户端的两个核心数据结构(数组),第一个用来暂存服务器发来的最新共享文件列表,第二个是第一个的拷贝 std::vector<SHARED_FILE> g_files_list; std::vector<SHARED_FILE> g_files_list2; unsigned int g_serverip; //服务器的IP,网络字节序表示 HANDLE g_server_semaphore; //服务器使用的信号量,用于控制对g_shared_files的并发访问 HANDLE g_client_semaphore; //客户端使用的信号量,用于控制对g_files_list2的并发访问 //--------------------------------------------------------------------------------------------------------- int mysend(SOCKET sock, char* buf, int len, int flags) { int sent = 0, remain = len; while (remain > 0) { int n = send(sock, buf + sent, remain, flags); if (n == -1) //出错的最大可能是对方关闭了套接字 break; remain -= n; sent += n; } return sent; } int myrecv(SOCKET sock, char* buf, int len, int flags) { int received = 0, remain = len; while (remain > 0) { int n = recv(sock, buf + received, remain, flags); if (n == 0 || n == -1) //0是对方调用closesocket(),-1是对方直接退出 break; remain -= n; received += n; } return received; } //------------------------------------------------------------------------------------------------------- //功能:服务器用于判断某个客户端是否下线 //参数:服务器上对应于某个客户端的通知套接字 //原理:由于通知端口的TCP连接是一直保持的,所以服务器在这个端口上调用recv()将会一直阻塞,直到客户端下线 //创建者:server_notify_thread() DWORD WINAPI server_quit_thread(LPVOID lpParam) { SOCKET comm_sock = (SOCKET)lpParam; while (1) { char c; int ret = recv(comm_sock, &c, 1, 0); if (ret == 0 || ret == -1) break; } printf("有一个客户端退出了\n"); //删除g_shared_files中的信息 WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 g_shared_files.erase(comm_sock); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器把最新共享文件列表发给客户端 //参数:服务器在刷新端口上的监听套接字 //原理:服务器在刷新端口上等待连接,建立连接后通过通信套接字把最新共享文件列表发给客户端 //创建者:main() DWORD WINAPI server_refresh_thread(LPVOID lpParam) { SOCKET server_refresh_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_refresh_sock, (struct sockaddr*)&client_addr, &size); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_refresh_thread, (void*)server_refresh_sock, 0, NULL); //向客户端发送共享文件列表。为防止遍历的过程中有客户端登录或退出,要进行并发控制 WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 std::multimap<SOCKET, SHARED_FILE>::const_iterator it; for (it = g_shared_files.begin(); it != g_shared_files.end(); it++) mysend(comm_sock, (char*)&(it->second), sizeof(SHARED_FILE), 0); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 //最后发一个空文件名告诉客户端已发完 SHARED_FILE sf; sf.filename[0] = '\0'; mysend(comm_sock, (char*)&sf, sizeof(SHARED_FILE), 0); closesocket(comm_sock); return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器接受一个客户端上线 //参数:服务器在通知端口上的监听套接字 //原理:服务器在通知端口上等待连接,建立连接后接收客户端发来的共享文件名,并构造结构体存入多映射 //创建者:main() DWORD WINAPI server_notify_thread(LPVOID lpParam) { SOCKET server_notify_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_notify_sock, (struct sockaddr*)&client_addr, &size); if (comm_sock == INVALID_SOCKET) { printf("accept() error!\n"); exit(0); } printf("有一个客户端上线了\n"); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_notify_thread, (void*)server_notify_sock, 0, NULL); //接收客户端发来的共享文件名,构造成共享文件结构体并加入g_shared_files保存 char buf[100]; SHARED_FILE sf; sf.client_addr = client_addr; sf.notify_sock = comm_sock; while (1) { myrecv(comm_sock, buf, 100, 0); if (buf[0] == '\0') //空文件名,说明客户端已发完所有共享文件名 break; strcpy(sf.filename, buf); WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 g_shared_files.insert(std::make_pair(comm_sock, sf)); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 } //创建线程,用来接收这个客户端退出的通知,以便及时从g_shared_files中删除信息 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_quit_thread, (void*)comm_sock, 0, NULL); //注意:这个线程虽然退出但是这条连接并未关闭,它将一直维持,直到客户端下线 return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器为文件传输做中转服务 //参数:服务器在传输端口上的监听套接字 //原理:服务器在传输端口上等待连接,建立连接后接收一个“传输头部”,并根据下载还是上传做相应中转 //创建者:main() DWORD WINAPI server_transfer_thread(LPVOID lpParam) { SOCKET server_transfer_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_transfer_sock, (struct sockaddr*)&client_addr, &size); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_transfer_thread, (void*)server_transfer_sock, 0, NULL); //接收一个“传输头部” TRANSFER_HEADER th; myrecv(comm_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); if (th.cmd == COMMAND_DOWNLOAD) //如果这个连接是为了下载文件 { //构造一个“通知头部”准备发给被下载方 NOTIFY_HEADER nh; nh.sock = comm_sock; //将下载方的套接字告诉被下载方,以便将来他上传文件时回传这个套接字 strcpy(nh.filename, th.shared_file.filename); //将文件名告诉被下载方 //从“传输头部”的共享文件结构体中取出被下载方对应的通知套接字 SOCKET client_notify_sock = th.shared_file.notify_sock; //向这个通知套接字发送“通知头部” int ret = mysend(client_notify_sock, (char*)&nh, sizeof(NOTIFY_HEADER), 0); if (ret != sizeof(NOTIFY_HEADER)) //说明被下载方已退出 closesocket(comm_sock); } else if (th.cmd == COMMAND_UPLOAD) //如果这个连接是为了上传文件 { //从“传输头部”中取出下载方对应的套接字 SOCKET download_client_sock = th.sock; //循环接收数据并转发给下载方 char buf[1024]; while (1) { int i = recv(comm_sock, buf, 1024, 0); //不需要调用myrecv(),因为不强求收满1024个字节 if (i > 0) mysend(download_client_sock, buf, i, 0); //但转发一定要保证发完i个字节 else { closesocket(download_client_sock); break; } } } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端每隔10秒向服务器请求最新共享文件列表 //参数:NULL //原理:客户端每隔10秒创建一个套接字,连接到服务器的刷新端口,然后循环接收数据,每次接收一个共享文件结构体,接收完成后断开连接并进行屏幕显示 //创建者:main() DWORD WINAPI client_refresh_thread(LPVOID lpParam) { sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_REFRESH_PORT); while (1) { //创建流套接字并连接到服务器的刷新端口 SOCKET refresh_sock = socket(AF_INET, SOCK_STREAM, 0); connect(refresh_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //接收最新共享文件列表 while (1) { SHARED_FILE sf; myrecv(refresh_sock, (char*)&sf, sizeof(SHARED_FILE), 0); //接收一个SHARED_FILE结构体 if (sf.filename[0] != '\0') //如果不是空文件名 g_files_list.push_back(sf); //加入g_files_list保存 else //空文件名,表示服务器本次发送共享文件列表已经完毕 break; } closesocket(refresh_sock); //为防止此时用户正在选择g_files_list2中的某个文件,进行并发控制 WaitForSingleObject(g_client_semaphore, INFINITE); //获取信号量 g_files_list2.clear(); g_files_list2 = g_files_list; ReleaseSemaphore(g_client_semaphore, 1, NULL); //释放信号量 g_files_list.clear(); //清空g_files_list,准备下一次接收共享文件列表 printf("最新共享文件列表:\n"); std::vector<SHARED_FILE>::const_iterator it; int i = 1; for (it = g_files_list2.begin(); it != g_files_list2.end(); it++, i++) { printf("%d - %s:%d上的%s", i, inet_ntoa(it->client_addr.sin_addr), ntohs(it->client_addr.sin_port), it->filename); printf("\n"); } printf("请输入文件的序号进行下载(0-退出):\n"); Sleep(10000); } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端进行文件上传 //参数:指向“通知头部”的指针 //原理:客户端从“通知头部”中得知是谁想下载他的文件以及想下载哪个文件,然后构造出表示上传的“传输头部”,连接到服务器的传输端口,首先发送“传输头部”然后发送文件 //创建者:client_notify_thread() DWORD WINAPI client_upload_thread(LPVOID lpParam) { NOTIFY_HEADER* pnh = (NOTIFY_HEADER*)lpParam; //创建套接字并连接到服务器的SERVER_TRANSFER_PORT端口 SOCKET client_upload_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_TRANSFER_PORT); connect(client_upload_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //构造一个表示上传的“传输头部”并发送给服务器 TRANSFER_HEADER th; th.cmd = COMMAND_UPLOAD; //指明这个连接是为了上传 th.sock = pnh->sock; //指明想下载这个文件的客户端套接字 mysend(client_upload_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); FILE* fp = fopen(pnh->filename, "rb"); if (fp != NULL) printf("开始上传文件%s\n", pnh->filename); else { printf("找不到文件%s,上传失败\n", pnh->filename); closesocket(client_upload_sock); delete pnh; return 0; } //循环读取文件数据并上传 char buf[1024]; while (1) { int i = fread(buf, 1, 1024, fp); mysend(client_upload_sock, buf, i, 0); if (i < 1024) //fread()没读满1024个字节表示是最后一次读文件了 break; } fclose(fp); closesocket(client_upload_sock); printf("文件%s上传完成\n", pnh->filename); delete pnh; return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端响应服务器发来的通知 //参数:客户端已经同服务器的通知端口建立连接的通知套接字 //原理:客户端在已经同服务器的通知端口建立了连接的通知套接字上等待接收“通知头部”,收到后以这个“通知头部”为参数启动client_upload_thread线程进行文件的上传 //创建者:main() DWORD WINAPI client_notify_thread(LPVOID lpParam) { SOCKET client_notify_sock = (SOCKET)lpParam; while (1) { //接收一个“通知头部” NOTIFY_HEADER nh; myrecv(client_notify_sock, (char*)&nh, sizeof(NOTIFY_HEADER), 0); printf("收到一个下载%s的通知\n", nh.filename); NOTIFY_HEADER* pnh = new NOTIFY_HEADER; //不能直接把nh的指针传给新线程,因为nh在本线程的栈中,随时可能被覆盖 *pnh = nh; //创建线程,用来上传文件 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)client_upload_thread, (void*)pnh, 0, NULL); } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端进行指定文件的下载 //参数:指向想下载的共享文件结构体的指针 //原理:客户端根据想下载的共享文件结构体构造出表示下载的“传输头部”,连接到服务器的传输端口并发送这个“传输头部”,然后循环等待接收文件数据并存盘 //创建者:client_userinput_thread() DWORD WINAPI client_download_thread(LPVOID lpParam) { SHARED_FILE* psf = (SHARED_FILE*)lpParam; //创建套接字并连接到服务器的SERVER_TRANSFER_PORT端口 SOCKET client_download_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_TRANSFER_PORT); connect(client_download_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //构造一个表示下载的“传输头部”并发送给服务器 TRANSFER_HEADER th; th.cmd = COMMAND_DOWNLOAD; //指明这个连接是为了下载 th.shared_file = *psf; //指明想下载哪一个文件 delete psf; mysend(client_download_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); printf("开始下载%s:%d的%s\n", inet_ntoa(th.shared_file.client_addr.sin_addr), ntohs(th.shared_file.client_addr.sin_port), th.shared_file.filename); FILE* fp = fopen(th.shared_file.filename, "wb"); //循环接收数据并写入文件 char buf[1024]; while (1) { int i = recv(client_download_sock, buf, 1024, 0); //不需要调用myrecv() if (i > 0) fwrite(buf, 1, i, fp); else break; } fclose(fp); closesocket(client_download_sock); printf("%s:%d的%s下载完毕\n", inet_ntoa(th.shared_file.client_addr.sin_addr), ntohs(th.shared_file.client_addr.sin_port), th.shared_file.filename); return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端接收并处理用户的键盘输入 //参数:NULL //原理:客户端等待用户的键盘输入,收到输入后从共享文件结构体数组中取出相应的结构体,并以此为参数启动client_download_thread线程进行文件的下载 //创建者:main() DWORD WINAPI client_userinput_thread(LPVOID lpParam) { int number; while (1) { scanf("%d", &number); //客户端退出的方式是用户按下0 if (number == 0) { CloseHandle(g_client_semaphore); exit(0); } //为防止此时正在刷新g_files_list2,进行并发控制 WaitForSingleObject(g_client_semaphore, INFINITE); //获取信号量 if (number > g_files_list2.size()) printf("请输入正确的序号\n"); else { SHARED_FILE* psf = new SHARED_FILE; *psf = g_files_list2[number - 1]; //创建线程,用来完成下载任务 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)client_download_thread, (void*)psf, 0, NULL); } ReleaseSemaphore(g_client_semaphore, 1, NULL); //释放信号量 } return 0; } //------------------------------------------------------------------------------------------------------- int main(int argc, char* argv[]) { if (argc == 1) { printf("使用方法 : \nfilename -server\n或\nfilename -client 服务器IP [共享文件名1] ...\n"); return 0; } //下面4行进行Windows网络环境的初始化。Linux中不需要 WORD wVersionRequested; WSADATA wsaData; wVersionRequested = MAKEWORD(2, 2); WSAStartup(wVersionRequested, &wsaData); if (argc == 2 && strcmp(argv[1], "-server") == 0) { //创建信号量,初值为1,用来控制对g_shared_files的并发修改 g_server_semaphore = CreateSemaphore(NULL, 1, 1, NULL); //创建通知端口的监听套接字 SOCKET server_notify_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_notify_sock == INVALID_SOCKET) //Linux下失败返回-1 { printf("socket() error!\n"); exit(0); } sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SERVER_NOTIFY_PORT); if (-1 == bind(server_notify_sock, (struct sockaddr*)&server_addr, sizeof(server_addr))) { printf("bind() error!\n"); exit(0); } if (-1 == listen(server_notify_sock, 5)) { printf("listen() error!\n"); exit(0); } //创建线程,用来在通知端口上接受连接请求 CreateThread(NULL, 0, server_notify_thread, (void*)server_notify_sock, 0, NULL); //创建刷新端口的监听套接字 SOCKET server_refresh_sock = socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_port = htons(SERVER_REFRESH_PORT); bind(server_refresh_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(server_refresh_sock, 5); //创建线程,用来在刷新端口上接受连接请求 CreateThread(NULL, 0, server_refresh_thread, (void*)server_refresh_sock, 0, NULL); //创建传输端口的监听套接字 SOCKET server_transfer_sock = socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_port = htons(SERVER_TRANSFER_PORT); bind(server_transfer_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(server_transfer_sock, 5); //创建线程,用来在传输端口上接受连接请求 CreateThread(NULL, 0, server_transfer_thread, (void*)server_transfer_sock, 0, NULL); printf("服务器在3个端口开始监听连接请求\n"); //消息循环,目的是不让主线程退出。Linux中换成while(1) pause(); MSG msg; while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } } else if (argc >= 3 && strcmp(argv[1], "-client") == 0) { //创建信号量,初值为1,用来控制对g_files_list2的并发读写 g_client_semaphore = CreateSemaphore(NULL, 1, 1, NULL); g_serverip = inet_addr(argv[2]); //创建用于连接到服务器通知端口的套接字 SOCKET client_notify_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_NOTIFY_PORT); connect(client_notify_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); printf("客户端请求连接成功\n"); //发送共享文件名,文件名长度与服务器约定为100字节 char buf[100]; int i; for (i = 3; i < argc; i++) { strcpy(buf, argv[i]); mysend(client_notify_sock, buf, 100, 0); } //最后发送一个空文件名告诉服务器共享文件名已发完 buf[0] = '\0'; mysend(client_notify_sock, buf, 100, 0); printf("客户端发送共享文件名成功\n"); //创建线程,用来接收服务器发来的“通知头部” CreateThread(NULL, 0, client_notify_thread, (void*)client_notify_sock, 0, NULL); //创建线程,每隔10秒向服务器请求最新的共享文件列表 CreateThread(NULL, 0, client_refresh_thread, NULL, 0, NULL); //创建线程,用来接收用户的键盘输入 CreateThread(NULL, 0, client_userinput_thread, NULL, 0, NULL); //消息循环,目的是不让主线程退出。Linux中换成while(1) pause(); MSG msg; while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } } else printf("启动参数不正确\n"); return 0; }
先来理理这代码在说些啥:
服务器端:创建notify、refresh、transfer三个端口。
notify:长连接,用于接收第一次客户端发来的共享文件名和向客户端发送有人要下载他的文件的通知。
refresh:短连接,每隔10秒建立一次连接,用于向客户端发送最新共享文件列表
transfer:短连接,用于在服务器和客户端间传送数据。
客户端:创建notify、refresh、transfer三个端口
notify:长连接,用于向服务器发送共享文件名和接收服务器发来的下载通知
refresh:短连接,接收服务器发来的最新共享文件列表
transfer:短连接,用于在服务器和客户端间传送数据。
一般来说,一个端口只会传送一种数据结构的数据,而在notify端口中却用了两个,这是因为,共享文件名只在第一次时候发送。
各个线程函数我就不说了,接下来说说整个应用工作流程:
服务器:1.创建三个端口套接字,并处于监听状态后,创建server_notify_thread、server_refresh_thread、server_transfer_thread三个子线程,子线 程中等待接收连接请求。 3.服务器server_notify_thread接受连接请求后,创建接收下一个客户端连接的线程后,接收共享文件结构体, 并将其并发控制下插入到共享文件映射里,接着创建一个下线线程,监听客户端的下线状态。 4.服务器server_refresh_thread接受连接 请求后,并发控制下向客户端发送最新文件列表 7.服务器server_transfer_thread接收客户端连接请求后,接收到发来的下载头部,从中 取出共享结构体里的被下载方对应的服务器套接字,并向被下载方(客户端2)发送一个下载通知。 9.接收到被下载方发来的头部后, 接收被下载方发来的数据,并中转给下载方。
客户端:2.创建通知套接字连接上服务器后,向服务器发送共享文件结构体,紧接着启动client_notify_thread、client_refresh_thread、 client_userinput_thread三个子线程 5.客户端client_refresh_thread接收到了服务器发来的最新共享文件列表后,并发控制下拷贝给共享 文件列表的拷贝用于输出到屏幕上,睡眠10秒后再次请求连接。 6.客户端键盘输入想要下载的文件序号,从共享文件列表拷贝里获得共 享文件结构体,启动下载线程,下载线程里向服务器请求连接,连接建立后向服务器发送下载头部,并等待接收数据。
客户端2:8.客户端client_notify_thread接收到服务器发来的通知后,启动client_upload_thread,向服务器请求建立传输连接,并发送一个上传头部, 连接建立后,向服务器发送文件数据。
小细节:(1)当客户端正常或非正常退出时,myrecv会返回一个0或-1,通过此可以判别该客户是否下线
(2)有时候不是必须使用myrecv,例如在server_transfer_thread里接收被下载方发来的数据时,因为是在循环结构里,不须保证收到确切数量的 数据,当mysend发完数据即可代表接收完毕
程序的精妙之处:(1)共享文件结构体、通知头部、传输头部三个数据结构的妙处,从共享文件结构体里可以直接取出被下载方的服务端套接字,从通知头 部里可以将下载方的下载套接字附在里面,以便回传,从传输头部结构体里的共用体能使上传和下载通过一个结构体明显的区分开 来。
(2)对线程函数安排位置,整个流程的设计,富有章法。
第二个问题解决方案:
如何判断当前下载文件是自己的?
起初我试验了很多位置的,发现一点,每次连接服务器通知端口、传输端口后,客户端使用的端口号是由操作系统自动分配的,每个都并不相同,并不会出现两个套接字共用一个端口的情况。而经过思索分析,我们需要判别的是下载方的长连接是否和自己的长连接相同,如果相同拒绝下载,不相同则允许下载。那么我们又怎么才能知道自己的长连接呢?要知道在源程序里我们并没有自己长连接的套接字,根本无从通过getsockname获取地址,这时候我们只需通过传参将自己的长连接套接字传入输入线程,再通过getsockname获取本地地址后进行比对,这样就行了。
第三个问题解决方案:
在大负荷情况下,程序的表现情况,以及解决方案?
在大负荷情况下,由于信号量的并发控制,使得多用户多线程都会争抢该信号量资源,并且由于该资源唯一,同一时间仅开放一个线程访问,所以会出现某些线程会一直阻塞的情况。对于我们来说,主要关注的是客户端的表现,如若客户体验不好,那么程序就不会被使用,在这种情况下,客户端的表现会是自己所要请求的最新共享文件列表一直接收不到,导致client_refresh_thread阻塞住,用户界面上将会一直停住,没有刷新。这时候我们能够想到的就是让他别组塞住,在服务器向客户发送最新共享文件列表里给它取消并发控制,但取消并发控制后,又会得不到正确的共享文件列表,那么我又想到定义一个共享文件映射的拷贝,向客户端发送最新共享文件列表就从这个拷贝里获取,这时候客户就不会组塞住,并且拷贝也能保证不会出错。但这时候又来了个难点,这个拷贝时机是什么时候?
在多次试验下,发现,如果是每个插入和更新操作后就拷贝,那么在刷新工程中也会引发并发导致数据错误的问题,再加一个信号量?这又将回到不加拷贝的时候。最后想到,只需每次发送共享文件信息列表前拷贝就能很好的解决,并且关键一步是将这个拷贝定义为局部变量,不能是全局变量,全局变量仍然会引发拷贝被修改的问题。
以上第二个问题和第三个问题,由于是期末,时间紧张,老师并未确认我的解决方案,但依运行效果来看,十有八九是正确的,如有错误,还望指正!!!
代码如下:
// FileDownloadThroughServer.cpp : Defines the entry point for the console application. // /*---------------------------------------------------------------------------------------------------------- 功能: 建立C/S结构的文件下载系统。每个客户端可以提供不定数目的文件用于共享,也可以下载别人共享出来的文件。 文件存在于各个客户端上,而不在服务器上。 要允许一个客户端同时下载多个文件,也要允许同时被多个别的客户端下载。 思路: 1.服务器至少应该开放3个TCP端口供客户端连接。 (1)第一个用来接收客户端发来的共享文件名以及向客户端发送别人想要下载他的文件的通知,不妨把这个端口叫做通知端口。 (2)第二个用来向客户端每隔10秒发送最新共享文件列表,把它叫做刷新端口。 (3)第三个用来提供文件传输的中转服务,叫做传输端口。 2.本系统是围绕着共享文件而工作的,为了方便起见,应该把一个共享文件的全部信息封装成“共享文件结构体”,成员变量 应该包括文件名、文件所在客户端的地址、服务器对应于这个客户端的通知套接字。 3.通知端口和传输端口应该设计各自的长度固定的头部(可设计成结构体),叫做“通知头部”和“传输头部”。 其中“传输头部”又有下载和上传两种。这样这两个端口的recv()函数就可以指定固定的接收长度了。 4.服务器和客户端都要大量使用多线程。 5.服务器可以用c++stl的multimap来保存共享文件列表,multimap的第一个字段是服务器上面对应于文件所在客户端的通知套接字, 第二字段是共享文件结构体。这样设计一来是为了在客户端退出的时候能最方便地从multimap中删除这个客户端的所有共享文件信息, 二来是为了在用户指定要下载哪个文件时不用通过搜索就知道该向哪个客户端发送通知。由于这个数据结构在客户端登录和 退出时都要被修改,所以必须有并发控制。 6.客户端的数据结构可以简单地用c++stl的vector来保存服务器发来的最新共享文件列表。vector中每个元素都是 共享文件结构体。为防止用户选择文件时正好在刷新共享文件列表,也需要有并发控制。但应知道,这里的并发控制可以 防止程序的运行出错,但并不能杜绝下载到非指定文件的可能性。 7.系统的工作流程大致如下:服务器在3个端口上等待,客户端首先连接到通知端口,在这条连接上发送自己的共享文件名, 然后同样在这条连接上准备接收“通知头部”。接着客户端每隔10秒连接到刷新端口并接收最新共享文件列表。 客户端还需要用一个线程来准备读入用户的键盘输入,当用户有输入时,客户端连接到传输端口并发送一个表示下载的 “传输头部”(其中最重要的就是一个共享文件结构体),然后在这条连接上准备接收文件数据。服务器收到这个 “传输头部”后从中得到文件所在客户端的套接字,并构造一个“通知头部”(包括想下载文件的客户端的套接字和想下载的 文件名)发送到这个套接字上。被下载方在通知端口上收到“通知头部”后,也连接到传输端口,先发送一个表示上传 的“传输头部”(其中最重要的就是下载方的套接字),以便让服务器知道该把文件转发给谁,接着就可以开始上传文件了。 考查: 1.结合源代码和注释,搞懂这个系统的结构和流程,掌握套接字编程和多线程编程的相关技术,准备回答问题。 2.已知:getsockname()函数可以获得套接字所使用的地址。自行查阅这个函数的原型,并在源程序基础上添加代码, 使用户试图下载自己的文件时能阻止并提示“这个文件是你自己的”。 3.按照本源程序的方案,分析在大负荷的情况下系统会有什么表现?应怎么改进?动手改源程序或者准备回答问题。 4.本源程序除了未进行差错处理和用户指定下载一个前10秒之内退出系统的客户端上的文件时会得不到正确提示 这些小问题外,故意留有一个真正的bug,此bug如果被激发,不仅会导致系统在一切正常并且被下载方没有退出时 也会下载不成功,而且服务器有不确定的动作。试进行debug。 ----------------------------------------------------------------------------------------------------------*/ #include "stdio.h" #include <map> #include <vector> #include "Winsock2.h" #pragma comment(lib, "ws2_32.lib") #pragma warning(disable:4996) //宏定义 //--------------------------------------------------------------------------------------------------------- #define SERVER_NOTIFY_PORT 1025 //通知端口 #define SERVER_REFRESH_PORT 1026 //刷新端口 #define SERVER_TRANSFER_PORT 1027 //传输端口 #define COMMAND_DOWNLOAD 0 //“传输头部”中表示下载的命令 #define COMMAND_UPLOAD 1 //“传输头部”中表示上传的命令 //--------------------------------------------------------------------------------------------------------- //结构体定义 //--------------------------------------------------------------------------------------------------------- #pragma pack(4) //设置结构体按照4字节对齐 struct SHARED_FILE //共享文件结构体,封装一个共享文件的所有信息。服务器端和客户端都要用到 { char filename[100]; //文件名 struct sockaddr_in client_addr; //文件所在的客户端的网络地址 SOCKET notify_sock; //服务器上面对应于这个客户端的通知套接字 }; struct TRANSFER_HEADER //“传输头部”,由客户端发往服务器,准备开始一次下载或上传 { int cmd; //命令,指明本次传输是为了下载还是上传,0-下载,1-上传 union { SHARED_FILE shared_file; //如果是下载,需要告诉服务器想下载的共享文件结构体 SOCKET sock; //如果是上传,需要告诉服务器想下载这个文件的客户端的套接字 }; }; struct NOTIFY_HEADER //“通知头部”,由服务器发往客户端,通知他有人想下载他的某个文件 { SOCKET sock; //告诉客户端是谁想下载他的文件 char filename[100]; //告诉客户端想下载他的哪个文件 }; #pragma pack() //取消结构体的字节对齐 //--------------------------------------------------------------------------------------------------------- //全局变量 //--------------------------------------------------------------------------------------------------------- //服务器端的核心数据结构(多映射),用来保存所有共享文件的信息。映射的第一个字段是服务器上面对应于客户端的通知套接字,第二个字段是客户端的一个共享文件结构体 std::multimap<SOCKET, SHARED_FILE> g_shared_files; std::multimap<SOCKET, SHARED_FILE> g_shared_files2; //客户端的两个核心数据结构(数组),第一个用来暂存服务器发来的最新共享文件列表,第二个是第一个的拷贝 std::vector<SHARED_FILE> g_files_list; std::vector<SHARED_FILE> g_files_list2; unsigned int g_serverip; //服务器的IP,网络字节序表示 HANDLE g_server_semaphore; //服务器使用的信号量,用于控制对g_shared_files的并发访问 HANDLE g_client_semaphore; //客户端使用的信号量,用于控制对g_files_list2的并发访问 HANDLE g_quit_semaphore; //--------------------------------------------------------------------------------------------------------- int mysend(SOCKET sock, char* buf, int len, int flags) { int sent = 0, remain = len; while (remain > 0) { int n = send(sock, buf + sent, remain, flags); if (n == -1) //出错的最大可能是对方关闭了套接字 break; remain -= n; sent += n; } return sent; } int myrecv(SOCKET sock, char* buf, int len, int flags) { int received = 0, remain = len; while (remain > 0) { int n = recv(sock, buf + received, remain, flags); if (n == 0 || n == -1) //0是对方调用closesocket(),-1是对方直接退出 break; remain -= n; received += n; } return received; } //------------------------------------------------------------------------------------------------------- //功能:服务器用于判断某个客户端是否下线 //参数:服务器上对应于某个客户端的通知套接字 //原理:由于通知端口的TCP连接是一直保持的,所以服务器在这个端口上调用recv()将会一直阻塞,直到客户端下线 //创建者:server_notify_thread() DWORD WINAPI server_quit_thread(LPVOID lpParam) { SOCKET comm_sock = (SOCKET)lpParam; while (1) { char c; int ret = recv(comm_sock, &c, 1, 0); if (ret == 0 || ret == -1) break; } printf("有一个客户端退出了\n"); //删除g_shared_files中的信息 WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 g_shared_files.erase(comm_sock); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器把最新共享文件列表发给客户端 //参数:服务器在刷新端口上的监听套接字 //原理:服务器在刷新端口上等待连接,建立连接后通过通信套接字把最新共享文件列表发给客户端 //创建者:main() DWORD WINAPI server_refresh_thread(LPVOID lpParam) { SOCKET server_refresh_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_refresh_sock, (struct sockaddr*)&client_addr, &size); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_refresh_thread, (void*)server_refresh_sock, 0, NULL); //向客户端发送共享文件列表。为防止遍历的过程中有客户端登录或退出,要进行并发控制 std::multimap<SOCKET, SHARED_FILE> g_shared_files2 = g_shared_files; std::multimap<SOCKET, SHARED_FILE>::const_iterator it; for (it = g_shared_files2.begin(); it != g_shared_files2.end();it++) { mysend(comm_sock, (char*)&(it->second), sizeof(SHARED_FILE), 0); } //最后发一个空文件名告诉客户端已发完 SHARED_FILE sf; sf.filename[0] = '\0'; mysend(comm_sock, (char*)&sf, sizeof(SHARED_FILE), 0); closesocket(comm_sock); return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器接受一个客户端上线 //参数:服务器在通知端口上的监听套接字 //原理:服务器在通知端口上等待连接,建立连接后接收客户端发来的共享文件名,并构造结构体存入多映射 //创建者:main() DWORD WINAPI server_notify_thread(LPVOID lpParam) { SOCKET server_notify_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_notify_sock, (struct sockaddr*)&client_addr, &size); if (comm_sock == INVALID_SOCKET) { printf("accept() error!\n"); exit(0); } printf("有一个客户端上线了\n"); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_notify_thread, (void*)server_notify_sock, 0, NULL); //接收客户端发来的共享文件名,构造成共享文件结构体并加入g_shared_files保存 char buf[100]; SHARED_FILE sf; sf.client_addr = client_addr; sf.notify_sock = comm_sock; while (1) { myrecv(comm_sock, buf, 100, 0); if (buf[0] == '\0') //空文件名,说明客户端已发完所有共享文件名 break; strcpy(sf.filename, buf); WaitForSingleObject(g_server_semaphore, INFINITE); //获取信号量 g_shared_files.insert(std::make_pair(comm_sock, sf)); ReleaseSemaphore(g_server_semaphore, 1, NULL); //释放信号量 } //创建线程,用来接收这个客户端退出的通知,以便及时从g_shared_files中删除信息 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_quit_thread, (void*)comm_sock, 0, NULL); //注意:这个线程虽然退出但是这条连接并未关闭,它将一直维持,直到客户端下线 return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:服务器为文件传输做中转服务 //参数:服务器在传输端口上的监听套接字 //原理:服务器在传输端口上等待连接,建立连接后接收一个“传输头部”,并根据下载还是上传做相应中转 //创建者:main() DWORD WINAPI server_transfer_thread(LPVOID lpParam) { SOCKET server_transfer_sock = (SOCKET)lpParam; sockaddr_in client_addr; int size = sizeof(client_addr); SOCKET comm_sock = accept(server_transfer_sock, (struct sockaddr*)&client_addr, &size); //创建线程,用来等待下一个连接请求 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)server_transfer_thread, (void*)server_transfer_sock, 0, NULL); //接收一个“传输头部” TRANSFER_HEADER th; myrecv(comm_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); if (th.cmd == COMMAND_DOWNLOAD) //如果这个连接是为了下载文件 { //构造一个“通知头部”准备发给被下载方 NOTIFY_HEADER nh; nh.sock = comm_sock; //将下载方的套接字告诉被下载方,以便将来他上传文件时回传这个套接字 strcpy(nh.filename, th.shared_file.filename); //将文件名告诉被下载方 //从“传输头部”的共享文件结构体中取出被下载方对应的通知套接字 SOCKET client_notify_sock = th.shared_file.notify_sock; //向这个通知套接字发送“通知头部” int ret = mysend(client_notify_sock, (char*)&nh, sizeof(NOTIFY_HEADER), 0); if (ret != sizeof(NOTIFY_HEADER)) //说明被下载方已退出 closesocket(comm_sock); } else if (th.cmd == COMMAND_UPLOAD) //如果这个连接是为了上传文件 { //从“传输头部”中取出下载方对应的套接字 SOCKET download_client_sock = th.sock; //循环接收数据并转发给下载方 char buf[1024]; while (1) { int i = recv(comm_sock, buf, 1024, 0); //不需要调用myrecv(),因为不强求收满1024个字节 if (i > 0) mysend(download_client_sock, buf, i, 0); //但转发一定要保证发完i个字节 else { closesocket(download_client_sock); break; } } } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端每隔10秒向服务器请求最新共享文件列表 //参数:NULL //原理:客户端每隔10秒创建一个套接字,连接到服务器的刷新端口,然后循环接收数据,每次接收一个共享文件结构体,接收完成后断开连接并进行屏幕显示 //创建者:main() DWORD WINAPI client_refresh_thread(LPVOID lpParam) { sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_REFRESH_PORT); while (1) { //创建流套接字并连接到服务器的刷新端口 SOCKET refresh_sock = socket(AF_INET, SOCK_STREAM, 0); connect(refresh_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //接收最新共享文件列表 while (1) { SHARED_FILE sf; myrecv(refresh_sock, (char*)&sf, sizeof(SHARED_FILE), 0); //接收一个SHARED_FILE结构体 if (sf.filename[0] != '\0') //如果不是空文件名 g_files_list.push_back(sf); //加入g_files_list保存 else //空文件名,表示服务器本次发送共享文件列表已经完毕 break; } closesocket(refresh_sock); //为防止此时用户正在选择g_files_list2中的某个文件,进行并发控制 WaitForSingleObject(g_client_semaphore, INFINITE); //获取信号量 g_files_list2.clear(); g_files_list2 = g_files_list; ReleaseSemaphore(g_client_semaphore, 1, NULL); //释放信号量 g_files_list.clear(); //清空g_files_list,准备下一次接收共享文件列表 printf("最新共享文件列表:\n"); std::vector<SHARED_FILE>::const_iterator it; int i = 1; for (it = g_files_list2.begin(); it != g_files_list2.end(); it++, i++) { printf("%d - %s:%d上的%s", i, inet_ntoa(it->client_addr.sin_addr), ntohs(it->client_addr.sin_port), it->filename); printf("\n"); } printf("请输入文件的序号进行下载(0-退出):\n"); Sleep(10000); } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端进行文件上传 //参数:指向“通知头部”的指针 //原理:客户端从“通知头部”中得知是谁想下载他的文件以及想下载哪个文件,然后构造出表示上传的“传输头部”,连接到服务器的传输端口,首先发送“传输头部”然后发送文件 //创建者:client_notify_thread() DWORD WINAPI client_upload_thread(LPVOID lpParam) { NOTIFY_HEADER* pnh = (NOTIFY_HEADER*)lpParam; //创建套接字并连接到服务器的SERVER_TRANSFER_PORT端口 SOCKET client_upload_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_TRANSFER_PORT); connect(client_upload_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //构造一个表示上传的“传输头部”并发送给服务器 TRANSFER_HEADER th; th.cmd = COMMAND_UPLOAD; //指明这个连接是为了上传 th.sock = pnh->sock; //指明想下载这个文件的客户端套接字 mysend(client_upload_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); FILE* fp = fopen(pnh->filename, "rb"); if (fp != NULL) printf("开始上传文件%s\n", pnh->filename); else { printf("找不到文件%s,上传失败\n", pnh->filename); closesocket(client_upload_sock); delete pnh; return 0; } //循环读取文件数据并上传 char buf[1024]; while (1) { int i = fread(buf, 1, 1024, fp); mysend(client_upload_sock, buf, i, 0); if (i < 1024) //fread()没读满1024个字节表示是最后一次读文件了 break; } fclose(fp); closesocket(client_upload_sock); printf("文件%s上传完成\n", pnh->filename); delete pnh; return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端响应服务器发来的通知 //参数:客户端已经同服务器的通知端口建立连接的通知套接字 //原理:客户端在已经同服务器的通知端口建立了连接的通知套接字上等待接收“通知头部”,收到后以这个“通知头部”为参数启动client_upload_thread线程进行文件的上传 //创建者:main() DWORD WINAPI client_notify_thread(LPVOID lpParam) { SOCKET client_notify_sock = (SOCKET)lpParam; while (1) { //接收一个“通知头部” NOTIFY_HEADER nh; myrecv(client_notify_sock, (char*)&nh, sizeof(NOTIFY_HEADER), 0); printf("收到一个下载%s的通知\n", nh.filename); NOTIFY_HEADER* pnh = new NOTIFY_HEADER; //不能直接把nh的指针传给新线程,因为nh在本线程的栈中,随时可能被覆盖 *pnh = nh; //创建线程,用来上传文件 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)client_upload_thread, (void*)pnh, 0, NULL); } return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端进行指定文件的下载 //参数:指向想下载的共享文件结构体的指针 //原理:客户端根据想下载的共享文件结构体构造出表示下载的“传输头部”,连接到服务器的传输端口并发送这个“传输头部”,然后循环等待接收文件数据并存盘 //创建者:client_userinput_thread() DWORD WINAPI client_download_thread(LPVOID lpParam) { SHARED_FILE* psf = (SHARED_FILE*)lpParam; //创建套接字并连接到服务器的SERVER_TRANSFER_PORT端口 SOCKET client_download_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_TRANSFER_PORT); connect(client_download_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); //构造一个表示下载的“传输头部”并发送给服务器 TRANSFER_HEADER th; th.cmd = COMMAND_DOWNLOAD; //指明这个连接是为了下载 th.shared_file = *psf; //指明想下载哪一个文件 delete psf; mysend(client_download_sock, (char*)&th, sizeof(TRANSFER_HEADER), 0); printf("开始下载%s:%d的%s\n", inet_ntoa(th.shared_file.client_addr.sin_addr), ntohs(th.shared_file.client_addr.sin_port), th.shared_file.filename); FILE* fp = fopen(th.shared_file.filename, "wb"); //循环接收数据并写入文件 char buf[1024]; while (1) { int i = recv(client_download_sock, buf, 1024, 0); //不需要调用myrecv() if (i > 0) fwrite(buf, 1, i, fp); else break; } fclose(fp); closesocket(client_download_sock); printf("%s:%d的%s下载完毕\n", inet_ntoa(th.shared_file.client_addr.sin_addr), ntohs(th.shared_file.client_addr.sin_port), th.shared_file.filename); return 0; } //------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------- //功能:客户端接收并处理用户的键盘输入 //参数:NULL //原理:客户端等待用户的键盘输入,收到输入后从共享文件结构体数组中取出相应的结构体,并以此为参数启动client_download_thread线程进行文件的下载 //创建者:main() DWORD WINAPI client_userinput_thread(LPVOID lpParam) { SOCKET local = (SOCKET)lpParam; sockaddr_in localAddr, oppsiteAddr; int localAddrLen = sizeof(localAddr); getsockname(local, (struct sockaddr*)&localAddr, &localAddrLen); unsigned int local_ip = htonl(localAddr.sin_addr.s_addr); unsigned int local_port = htons(localAddr.sin_port); int number; while (1) { scanf("%d", &number); //客户端退出的方式是用户按下0 if (number == 0) { CloseHandle(g_client_semaphore); exit(0); } //为防止此时正在刷新g_files_list2,进行并发控制 WaitForSingleObject(g_client_semaphore, INFINITE); //获取信号量 if (number > g_files_list2.size()) printf("请输入正确的序号\n"); else { oppsiteAddr = g_files_list2[number - 1].client_addr; unsigned int oppsite_ip = htonl(g_files_list2[number - 1].client_addr.sin_addr.s_addr); unsigned int oppsite_port = htons(g_files_list2[number - 1].client_addr.sin_port); if (local_ip == oppsite_ip && local_port == oppsite_port) { printf("can not download your own files!!!\n\n"); continue; } SHARED_FILE* psf = new SHARED_FILE; *psf = g_files_list2[number - 1]; //创建线程,用来完成下载任务 CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)client_download_thread, (void*)psf, 0, NULL); } ReleaseSemaphore(g_client_semaphore, 1, NULL); //释放信号量 } return 0; } //------------------------------------------------------------------------------------------------------- int main(int argc, char* argv[]) { if (argc == 1) { printf("使用方法 : \nfilename -server\n或\nfilename -client 服务器IP [共享文件名1] ...\n"); return 0; } //下面4行进行Windows网络环境的初始化。Linux中不需要 WORD wVersionRequested; WSADATA wsaData; wVersionRequested = MAKEWORD(2, 2); WSAStartup(wVersionRequested, &wsaData); if (argc == 2 && strcmp(argv[1], "-server") == 0) { //创建信号量,初值为1,用来控制对g_shared_files的并发修改 g_server_semaphore = CreateSemaphore(NULL, 1, 1, NULL); //创建通知端口的监听套接字 SOCKET server_notify_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_notify_sock == INVALID_SOCKET) //Linux下失败返回-1 { printf("socket() error!\n"); exit(0); } sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SERVER_NOTIFY_PORT); if (-1 == bind(server_notify_sock, (struct sockaddr*)&server_addr, sizeof(server_addr))) { printf("bind() error!\n"); exit(0); } if (-1 == listen(server_notify_sock, 5)) { printf("listen() error!\n"); exit(0); } //创建线程,用来在通知端口上接受连接请求 CreateThread(NULL, 0, server_notify_thread, (void*)server_notify_sock, 0, NULL); //创建刷新端口的监听套接字 SOCKET server_refresh_sock = socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_port = htons(SERVER_REFRESH_PORT); bind(server_refresh_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(server_refresh_sock, 5); //创建线程,用来在刷新端口上接受连接请求 CreateThread(NULL, 0, server_refresh_thread, (void*)server_refresh_sock, 0, NULL); //创建传输端口的监听套接字 SOCKET server_transfer_sock = socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_port = htons(SERVER_TRANSFER_PORT); bind(server_transfer_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(server_transfer_sock, 5); //创建线程,用来在传输端口上接受连接请求 CreateThread(NULL, 0, server_transfer_thread, (void*)server_transfer_sock, 0, NULL); printf("服务器在3个端口开始监听连接请求\n"); //消息循环,目的是不让主线程退出。Linux中换成while(1) pause(); MSG msg; while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } } else if (argc >= 3 && strcmp(argv[1], "-client") == 0) { //创建信号量,初值为1,用来控制对g_files_list2的并发读写 g_client_semaphore = CreateSemaphore(NULL, 1, 1, NULL); g_serverip = inet_addr(argv[2]); //创建用于连接到服务器通知端口的套接字 SOCKET client_notify_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = g_serverip; server_addr.sin_port = htons(SERVER_NOTIFY_PORT); connect(client_notify_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); printf("客户端请求连接成功\n"); //发送共享文件名,文件名长度与服务器约定为100字节 char buf[100]; int i; for (i = 3; i < argc; i++) { strcpy(buf, argv[i]); mysend(client_notify_sock, buf, 100, 0); } //最后发送一个空文件名告诉服务器共享文件名已发完 buf[0] = '\0'; mysend(client_notify_sock, buf, 100, 0); printf("客户端发送共享文件名成功\n"); //创建线程,用来接收服务器发来的“通知头部” CreateThread(NULL, 0, client_notify_thread, (void*)client_notify_sock, 0, NULL); //创建线程,每隔10秒向服务器请求最新的共享文件列表 CreateThread(NULL, 0, client_refresh_thread, NULL, 0, NULL); //创建线程,用来接收用户的键盘输入 CreateThread(NULL, 0, client_userinput_thread, (void*)client_notify_sock, 0, NULL); //消息循环,目的是不让主线程退出。Linux中换成while(1) pause(); MSG msg; while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } } else printf("启动参数不正确\n"); return 0; }
第四个问题,自己才能不足,也不愿花大量的时间去钻,若有朋友发现了,可以评论或私信我,我将与你一起探讨!