书:深入理解计算机系统(P649) 之 并发编程:用socket实现多客户端的文件传输

来源:互联网 发布:php源码解密 编辑:程序博客网 时间:2024/04/29 01:47

Section I Problem Specification


实验要求:上一次实验是传输在客户端和服务端之间传输文字,这次是在客户端索要服务端的文件,服务端将文件传输给客户端。并且还要实现并发编程,使得这个服务器可以同时为多个客户端服务。
基本上,本次实验关键点在于:windows下的api,并发编程的实现。
另外本实验的代码也可以用C++写成面向对象的模式,但是我觉得代码实在是太少了,逻辑关系不是那么复杂,对象也很少的,没有必要为了非要用面对对象的思想而改造本身简易的实现的方式。

Section II Solution Method and Design


传输文件的Win Api:


发送文件:


实际上,发送文件和发送字节,本质上没有什么不同,发送方也只是将由组成文件的字节发送到接收方而已。

因为要读取文件的字节才能把文件的字节发送出去,
所以首先要打开文件,
使用的api是:CreateFile function
这是一个多功能的函数,可打开或创建以下对象,并返回可访问的句柄:控制台,通信资源,目录(只读打开),磁盘驱动器,文件,邮槽,管道。

HANDLE WINAPI CreateFile(  _In_      LPCTSTR lpFileName,  _In_      DWORD dwDesiredAccess,  _In_      DWORD dwShareMode,  _In_opt_  LPSECURITY_ATTRIBUTES lpSecurityAttributes,  _In_      DWORD dwCreationDisposition,  _In_      DWORD dwFlagsAndAttributes,  _In_opt_  HANDLE hTemplateFile);
lpFileName:要打开的文件的名字,在本实验中,我传入的C:\test.doc,表示我要打开C盘下一个doc文档
dwDesiredAccess :GENERIC_READ 读访问; GENERIC_WRITE 写访问(可组合使用:GENERIC_READ | GENERIC_WRITE)
dwShareMode:零表示不共享; FILE_SHARE_READ 和/或 FILE_SHARE_WRITE 表示允许对文件进行共享访问
lpSecurityAttributes:指向一个SECURITY_ATTRIBUTES结构的指针,定义了文件的安全特性。本实验对这个没有涉及这个问题。

dwCreationDisposition :有如下操作

CREATE_NEW 创建文件;如文件存在则会出错
CREATE_ALWAYS 创建文件,会改写前一个文件
OPEN_EXISTING 文件必须已经存在。由设备提出要求
OPEN_ALWAYS 如文件不存在则创建它
TRUNCATE_EXISTING 将现有文件缩短为零长度

dwFlagsAndAttributes:
FILE_ATTRIBUTE_ARCHIVE 标记归档属性
FILE_ATTRIBUTE_COMPRESSED 将文件标记为已压缩,或者标记为文件在目录中的默认压缩方式
FILE_ATTRIBUTE_NORMAL 默认属性
FILE_ATTRIBUTE_HIDDEN 隐藏文件或目录
FILE_ATTRIBUTE_READONLY 文件为只读
FILE_ATTRIBUTE_SYSTEM 文件为系统文件
FILE_FLAG_WRITE_THROUGH操作系统不得推迟对文件的写操作
FILE_FLAG_OVERLAPPED 允许对文件进行重叠操作
FILE_FLAG_NO_BUFFERING 禁止对文件进行缓冲处理。文件只能写入磁盘卷的扇区块
FILE_FLAG_RANDOM_ACCESS 针对随机访问对文件缓冲进行优化
FILE_FLAG_SEQUENTIAL_SCAN 针对连续访问对文件缓冲进行优化
FILE_FLAG_DELETE_ON_CLOSE 关闭了上一次打开的句柄后,将文件删除。特别适合临时文件

hTemplateFile :如果不为零,则指定一个文件句柄。新文件将从这个文件中复制扩展属性


打开文件后,我们依照字节的次序读取文件,使用函数:

BOOL ReadFile(    HANDLE hFile, //文件的句柄    LPVOID lpBuffer, //用于保存读入数据的一个缓冲区    DWORD nNumberOfBytesToRead, //要读入的字节数    LPDWORD lpNumberOfBytesRead, //指向实际读取字节数的指针    LPOVERLAPPED lpOverlapped     //如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。    //该结构定义了一次异步读取操作。否则,应将这个参数设为NULL);
上面已经将这个函数解释的参数解释的很清楚了,这里有一个问题记录在了conclusion。

我们把数据读到lpBuffer之后,再调用winsock的函数send,把字节发送发送出去就可以了。

接收文件:

有两个函数关键的I\O函数完成这个功能。
第一个函数CreateFile,当然对文件的操作都需要这个函数,并且需要将dwDesiredAccess设置为GENERIC_WRITE,这是为了,我们接收到字节后,是要把文件写入到文件中的。
当然我们调用了winsock的函数rec(),接受到数据之后,
就调用第二个函数writeFile,一切看起来那么顺理成章,没什么好解释的。
BOOL WriteFile(HANDLE hFile, // 文件句柄LPCVOID lpBuffer, // 数据缓存区指针DWORD nNumberOfBytesToWrite, // 你要写的字节数LPDWORD lpNumberOfBytesWritten, // 用于保存实际写入字节数的存储区域的指针LPOVERLAPPED lpOverlapped // OVERLAPPED结构体指针);


面对对象编程

实际上,本次实验并不是那么需用利用面向对象来编程,因为我认为面对对象编程一定程序体系非常庞大才好。
但是本次,我依然采用了面向对象的思想来写了代码,请下图:是server端的类图:


并发编程:

我先打算总结一下思想,暂时不涉及win api,也许会涉及unix的api,但是关键还是思维。

什么是并发

并发是指:多个逻辑控制流在时间上重叠,那么就是并发的。硬件异常处理程序、进程和unix信号处理程序都是并发的例子。
一个并发程序是在同一个时间内,由一组逻辑流组成的。

并发的用处

访问慢速设备:当一个应用程序在等待慢速I/O设备的数据带来的时候,那么内核会去执行其他的程序,使得CPU保持繁忙,这样提高了CPU的利用率
与人机交互:比如我们在打印文档的时候,想要调整一个窗口的大小。这是并发的体现,在windows系统中,每点击一次一下鼠标按键,一个独立的并发逻辑流就会被创建出来执行这个操作。
服务多个网络客户端:这就是本次实验要求的。并发服务器使得,服务器会为每一个客户端创建一个单独的逻辑流,这就允许服务器同时为多个客户端服务,并且也避免了慢速客户端独占服务器的情况。

并发编程的方法

基本多进程:

每一个逻辑控制流都是一个进程,并且由内核来调度和维护。因为进程有着独立的虚拟内存地址,想要和其他流进程通信,控制流必须使用某种显示的进程间的通信。
用下图可以清晰的看出一个基于多进程的并发服务器是如何处理多个客户端的连接问题的。



此类服务器最大的问题就是不能进程间如何通信的问题,IPC的开销往往很高,当使用这个方式时,一定要考虑到这个问题。

I/O多路复用:

这个是我没怎么看懂的。在这种形式的并发编程中,应用程序在一个进程的上下文中显示地调度它们自己的逻辑流,逻辑流被模型化为状态机,数据到达文件描述符后,主程序显示第从一个状态转换到另一个状态,因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。

基于多线程:

这是本实验使用的方法。线程是一个运行在单一进程上下文中的逻辑流,由内核进行调度。你可以把线程看成是上两种方式的结合,想进程流一样由内核进程调度,但是I/O多路复用流一样共享同一个虚拟地址空间。
并发线程在执行的过程当中,会执行上下文的切换,如下图所示:
在unix下多有一些线程处理的标准接口,称之为Posix线程,出现于1995年,在大多数unix系统上都可以通用,pthreads定义了大约60个标准函数,允许创建、杀死和回收现场,与对等线程的安全地共享数据,还可以通知对等线程系统状态的变化。




Section III Test Cases and Results Analysis

我开启了一个服务端和两个客户端

服务端:


二个客户端:


Section IV Conclusion

readfile函数怎么知道读到哪了

我还以为会有一个指针专门指着文件里面的字节序列,一直指向当前读到的位置,下次读的时候再从这里开始读,但是看对readfile的解释,我并没有看到有这样一个功能的指针。
BOOL ReadFile(    HANDLE hFile, //文件的句柄    LPVOID lpBuffer, //用于保存读入数据的一个缓冲区    DWORD nNumberOfBytesToRead, //要读入的字节数    LPDWORD lpNumberOfBytesRead, //指向实际读取字节数的指针    LPOVERLAPPED lpOverlapped     //如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。    //该结构定义了一次异步读取操作。否则,应将这个参数设为NULL);

如下所示,readfile函数是在一个循环内的,那么每次循环读的时候都要往后读,但是我却没有看到一个标志目前读在哪里的指针。这个到底是怎么把文件的字节持续的读下去的。
//开始发送文件//sendData = new BYTE[DEFAULT_BUFLEN];cbLeftToSend = GetFileSize(hFile, NULL);do { //循环发送//int sendThisTime, doneSoFar, buffOffset;  DWORD dwLen;sendThisTime = ReadFile(hFile, sendData, DEFAULT_BUFLEN, &dwLen, NULL);sendThisTime = dwLen;buffOffset = 0;  do {doneSoFar = send(ClientSocket, (const char*)(sendData + buffOffset), sendThisTime, 0);if (doneSoFar < 0) {printf("thread ID: %d ,ConnFD: %d  disconnected and shutdown.\n", GetCurrentThreadId(),ClientSocket);closesocket(ClientSocket);return 1;}buffOffset += doneSoFar;sendThisTime -= doneSoFar;cbLeftToSend -= doneSoFar;} while (sendThisTime > 0);} while (cbLeftToSend > 0);}

Section V References

深入理解计算机系统(原书第2版) 作者: (美)Randal E.Bryant / David O'Hallaron 译者: 龚奕利 / 雷迎春 出版年: 2010年

Section VI Appendix

server端

#include "server.h"int __cdecl main(int argc, char **argv) {server server;server.Accept(server.Listen());return 0;}


#undef UNICODE#define WIN32_LEAN_AND_MEAN#include <windows.h>#include <winsock2.h>#include <ws2tcpip.h>#include <stdlib.h>#include <stdio.h>#include <strsafe.h> #pragma comment (lib, "Ws2_32.lib")#define PORT "8888" //默认端口//#define DEFAULT_BUFLEN 1024typedef struct _MyData { SOCKET ClientSocket;} MYDATA, *PMYDATA;#pragma onceclass server{public:server(void);SOCKET ListenSocket;bool Accept(SOCKET ListenSocket);SOCKET Listen();~server(void);};

#include "server.h"DWORD WINAPI ThreadProc(LPVOID lpParameter){PMYDATA pData;int iResult,iSendResult;char recvbuf[DEFAULT_BUFLEN];int recvbuflen = DEFAULT_BUFLEN;pData = (PMYDATA)lpParameter;SOCKET ClientSocket = pData->ClientSocket;do {printf("thread ID: %d  is working for Connfd:%d  \n", GetCurrentThreadId(),ClientSocket);iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);if (iResult > 0) {recvbuf[iResult]='\0';//因为是从0开始的,所以如果这里是14,那么第14个为\0printf("thread ID: %d request file: %s   \n", GetCurrentThreadId(),recvbuf);char filePath[DEFAULT_BUFLEN];memcpy(filePath,recvbuf,sizeof(recvbuf));int fileLength, cbLeftToSend;BYTE* sendData = NULL;HANDLE hFile;hFile = CreateFile(filePath, //打开要传送的文件//GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,0, NULL);fileLength = GetFileSize(hFile, NULL);printf("Sending......\n");fileLength = htonl(fileLength); //转化为网络字序//cbLeftToSend = sizeof(fileLength);do {int cbBytesSend;BYTE* bp = (BYTE*)(&fileLength) + sizeof(fileLength) - cbLeftToSend;cbBytesSend = send(ClientSocket, (const char*)bp, cbLeftToSend, 0);if (cbBytesSend < 0) {printf("thread ID: %d ,ConnFD: %d  disconnected and shutdown.\n", GetCurrentThreadId(),ClientSocket);closesocket(ClientSocket);return 1;}cbLeftToSend -= cbBytesSend;} while (cbLeftToSend > 0);//开始发送文件//sendData = new BYTE[DEFAULT_BUFLEN];cbLeftToSend = GetFileSize(hFile, NULL);do { //循环发送//int sendThisTime, doneSoFar, buffOffset;  DWORD dwLen;sendThisTime = ReadFile(hFile, sendData, DEFAULT_BUFLEN, &dwLen, NULL);sendThisTime = dwLen;buffOffset = 0;  do {doneSoFar = send(ClientSocket, (const char*)(sendData + buffOffset), sendThisTime, 0);if (doneSoFar < 0) {printf("thread ID: %d ,ConnFD: %d  disconnected and shutdown.\n", GetCurrentThreadId(),ClientSocket);closesocket(ClientSocket);return 1;}buffOffset += doneSoFar;sendThisTime -= doneSoFar;cbLeftToSend -= doneSoFar;} while (sendThisTime > 0);} while (cbLeftToSend > 0);}else if (iResult == 0)printf("Receive Completed...\n");else  {//printf("recv failed with error: %d\n", WSAGetLastError());//closesocket(ClientSocket);}} while (iResult > 0);// shutdown the connection since we're doneiResult = shutdown(ClientSocket, SD_SEND);printf("thread ID: %d ,ConnFD: %d  disconnected and shutdown.\n", GetCurrentThreadId(),ClientSocket);if (iResult == SOCKET_ERROR) {printf("shutdown failed with error: %d\n", WSAGetLastError());closesocket(ClientSocket);return 1;}closesocket(ClientSocket);}bool server::Accept(SOCKET ListenSocket){PMYDATA pData;DWORD dwThreadId;HANDLE hThread;while(1){SOCKADDR_IN addrCli;memset(&addrCli, '\0', sizeof(addrCli));int len = sizeof(SOCKADDR);SOCKET ClientSocket = accept(ListenSocket,(SOCKADDR*)&addrCli, &len);printf("connect with %s,Connfd:%d\n", inet_ntoa(addrCli.sin_addr),ClientSocket); //显示连接端信息//if (ClientSocket == INVALID_SOCKET) {printf("accept failed with error: %d\n", WSAGetLastError());closesocket(ListenSocket);WSACleanup();}pData = (PMYDATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,sizeof(MYDATA));pData->ClientSocket=ClientSocket;hThread = CreateThread(NULL, // default security attributes0, // use default stack sizeThreadProc, // thread functionpData, // argument to thread function0, // use default creation flags&dwThreadId); // returns the thread identifier// No longer need server socket//closesocket(ListenSocket);}WSACleanup();}SOCKET server::Listen(){WSADATA wsaData;int iResult;char hostName[255];PHOSTENT hostinfo; LPCSTR hostIp;SOCKET ListenSocket = INVALID_SOCKET;struct addrinfo *result = NULL;struct addrinfo hints;int iSendResult;char recvbuf[DEFAULT_BUFLEN];int recvbuflen = DEFAULT_BUFLEN;// Initialize WinsockiResult = WSAStartup(MAKEWORD(2,2), &wsaData);if (iResult != 0) {printf("WSAStartup failed with error: %d\n", iResult);}ZeroMemory(&hints, sizeof(hints));hints.ai_family = AF_INET;hints.ai_socktype = SOCK_STREAM;hints.ai_protocol = IPPROTO_TCP;hints.ai_flags = AI_PASSIVE;// Resolve the server address and portiResult = getaddrinfo(NULL, PORT, &hints, &result);if ( iResult != 0 ) {printf("getaddrinfo failed with error: %d\n", iResult);WSACleanup();}// Create a SOCKET for connecting to serverListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);if (ListenSocket == INVALID_SOCKET) {printf("socket failed with error: %ld\n", WSAGetLastError());freeaddrinfo(result);WSACleanup();}// Setup the TCP listening socketiResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);if (iResult == SOCKET_ERROR) {printf("bind failed with error: %d\n", WSAGetLastError());freeaddrinfo(result);closesocket(ListenSocket);WSACleanup();}freeaddrinfo(result);iResult = listen(ListenSocket, SOMAXCONN);if (iResult == SOCKET_ERROR) {printf("listen failed with error: %d\n", WSAGetLastError());closesocket(ListenSocket);WSACleanup();}if( gethostname ( hostName, sizeof(hostName)) == 0) { //如果成功地将本地主机名存放入由name参数指定的缓冲区中 if((hostinfo = gethostbyname(hostName)) != NULL) { //这是获取主机名,如果获得主机名成功的话,将返回一个指针,指向hostinfo,hostinfo为PHOSTENT型的变量,下面即将用到这个结构体 hostIp = inet_ntoa (*(struct in_addr *)*hostinfo->h_addr_list); }}// Accept a client socketprintf("Ip: %s Listening on Port: %s \n",hostIp,PORT);return ListenSocket;};server::server(void){}server::~server(void){}

客户端:

#include "client.h"int __cdecl main(int argc, char **argv) {if (argc != 2) {  printf("usage: %s server-ip\n", argv[0]);  return 1;  }  client client(argv[1]);return 0;}

#define WIN32_LEAN_AND_MEAN#include <windows.h>#include <winsock2.h>#include <ws2tcpip.h>#include <stdlib.h>#include <stdio.h>#define PORT "8888"#define CHARLENGTH 80 //字符串长//// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib#pragma comment (lib, "Ws2_32.lib")#pragma comment (lib, "Mswsock.lib")#pragma comment (lib, "AdvApi32.lib")#pragma once#define DEFAULT_BUFLEN 1024class client{public:client(PCSTR IP);~client(void);};

#include "client.h"client::client(PCSTR IP){WSADATA wsaData;SOCKET ConnectSocket = INVALID_SOCKET;struct addrinfo *result = NULL,*ptr = NULL,hints;char sendbuf[DEFAULT_BUFLEN];char recvbuf[DEFAULT_BUFLEN];int iResult;int recvbuflen = DEFAULT_BUFLEN;// Initialize WinsockiResult = WSAStartup(MAKEWORD(2,2), &wsaData);if (iResult != 0) {printf("WSAStartup failed with error: %d\n", iResult);}ZeroMemory( &hints, sizeof(hints) );hints.ai_family = AF_UNSPEC;hints.ai_socktype = SOCK_STREAM;hints.ai_protocol = IPPROTO_TCP;// Resolve the server address and portiResult = getaddrinfo(IP, PORT, &hints, &result);if ( iResult != 0 ) {printf("getaddrinfo failed with error: %d\n", iResult);WSACleanup();}// Attempt to connect to an address until one succeedsfor(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {// Create a SOCKET for connecting to serverConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);if (ConnectSocket == INVALID_SOCKET) {printf("socket failed with error: %ld\n", WSAGetLastError());WSACleanup();}// Connect to server.iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);if (iResult == SOCKET_ERROR) {closesocket(ConnectSocket);ConnectSocket = INVALID_SOCKET;continue;}break;}freeaddrinfo(result);if (ConnectSocket == INVALID_SOCKET) {printf("Unable to connect to server!\n");WSACleanup();goto PreReturn;}while (1){// Send an initial bufferprintf("what file do you want?\n");memset(sendbuf,'\0',sizeof(sendbuf));scanf("%s",&sendbuf);iResult = send( ConnectSocket, sendbuf, (int)strlen(sendbuf), 0 );if (iResult == SOCKET_ERROR) {printf("send failed with error: %d\n", WSAGetLastError());closesocket(ConnectSocket);WSACleanup();}char filePath[CHARLENGTH];printf("Save at? :\n"); //获取文件保存路径//memset(&filePath, '\0', sizeof(filePath));scanf("\n%s", filePath);int fileLength, cbBytesRet, cbLeftToRecv;BYTE* recvData = NULL;//创建文件//HANDLE hFile;hFile = CreateFile( filePath, //string 文件名或者绝对路径GENERIC_WRITE, //long 读写权限,如果为零,表示只允许获取与一个设备有关的信息FILE_SHARE_WRITE, //long 零表示不共享,FILE_SHARE_READ 或 FILE_SHARE_WRITE 表示允许对文件进行共享访问NULL, //指向SECURITY_ATTRIBUTES结构的指针,判定返回的句柄是否可以被子进程继承,定义了文件的安全特性,用null表示不被继承。OPEN_ALWAYS, //long 创建文件,如文件存在则会出错0, //long 文件默认属性NULL); //long 如果不为零,则指定一个文件句柄。新文件将从这个文件中复制扩展属性printf("reciving......\n");//首先接收文件长度信息//cbLeftToRecv = sizeof(fileLength);do {BYTE* bp = (BYTE*)(&fileLength) + sizeof(fileLength) - cbLeftToRecv;cbBytesRet = recv(ConnectSocket, (char*)bp, cbLeftToRecv, 0); //流型数据的接收处理//if (cbBytesRet < 0 || cbBytesRet == 0 ) {perror("recv");printf("警告: 接收文件长度失败!\n");goto PreReturn;   }  cbLeftToRecv -= cbBytesRet;} while (cbLeftToRecv > 0);fileLength = ntohl(fileLength); //将文件的长度信息转化为本地字节序////开始接收文件recvData = new BYTE[DEFAULT_BUFLEN];cbLeftToRecv = fileLength;do {   int iiGet, iiRecv;        iiGet = (cbLeftToRecv < DEFAULT_BUFLEN) ? cbLeftToRecv : DEFAULT_BUFLEN;iiRecv = recv(ConnectSocket, (char*)recvData, iiGet, 0); //流型数据的接收处理//if (iiRecv < 0 || iiRecv == 0) {perror("recv");printf("警告: 接收文件失败!\n");goto PreReturn;   }DWORD dwLen;int ret3 = WriteFile(hFile, //Long,一个文件的句柄 recvData, //Any,要写入的一个数据缓冲区 iiRecv, //Long,要写入数据的字节数量。如写入零字节,表示什么都不写入,针对位于远程系统的命名管道,限制在65535个字节以内 &dwLen, //Long,实际写入文件的字节数量 NULL); //OVERLAPPED,倘若在指定FILE_FLAG_OVERLAPPED的前提下打开文件,这个参数就必须引用一个特殊的结构。//那个结构定义了一次异步写操作。否则,该参数应置为空(将声明变为ByVal As Long,并传递零值)cbLeftToRecv -= iiRecv;} while (cbLeftToRecv > 0);printf("Receviced Completed \n");//接收结束,释放内存,关闭连接//PreReturn :delete recvData;CloseHandle(hFile);// cleanup}closesocket(ConnectSocket);WSACleanup();}client::~client(void){}


原创粉丝点击