书:深入理解计算机系统(P651) 之 并发编程:用I/O多路复用的实现服务器

Section I Problem Specification

该部分内容首先是老师对我们提出的要求,我先翻译一下。
接着提出了自己的理解。

翻译老师的要求:

A. Overview

The select model is an I/O model widely available in Winsock. We call it the select model because it centers on using the select function to manage I/O. The design of this model originated on UNIX-based computers featuring Berkeley socket implementations. The select model was incorporated into Winsock 1.1 to allow applications that want 
to avoid blocking on socket calls the capability to manage multiple sockets in an organized manner. Because Winsock 1.1 is backward-compatible with Berkeley socket implementations, a Berkeley socket application that uses the select function should technically be able to run without modification.

译:选择模式是一个在winsock中广泛使用的I/O模式。我们之所以称其为选择模式是因为它主要使用选择函数去控制I/O。这种设计模式起源于实现了伯克利套接的以Unix为基础的电脑上。该模式被合并入Winsock1.1,使得那些想要避免堵塞socket请求的应用能够有能力以一种有组织的方式控制多个socket。因为Winscok1.1向后兼容了伯克利套接字,所以一个伯克利套接字的应用可以无障碍的使用选择函数。

The select function can be used to determine if there is data on a socket and if a socket can be written to. The reason for having this function is to prevent your application from blocking on an I/O bound call such as send or recv when a socket is in a blocking mode and to prevent the WSAEWOULDBLOCK error when a socket is in a non-blocking mode. The select function blocks for I/O operations until the conditions specified as parameters are met.

翻译:选择函数被用来决定是否有数据在套接字中和是否这个套接字能够被写入。拥有这个功能的原因是为了能够防止你的应用在套接字处于阻塞模式的情况下,被一些I/O密集型的调用如:send、recv阻塞了,并且能够防止当socket处于非堵塞下的WSAEWOULDBLOCK的错误。选择函数能够阻断I/O操作直到某个条件参数被满足了。

The advantage of using select is the capability to multiplex connections and I/O on many sockets from a single thread. This prevents the explosion of threads associated with blocking sockets and multiple connections. The disadvantage is the maximum number of sockets that may be added to the fd_set structures. By default, the maximum is defined as FD_SETSIZE, which is defined in WINSOCK2.H as 64. To increase this limit, an application might define FD_SETSIZE to something large. This define must appear before including WINSOCK2.H. Also, the underlying provider imposes an arbitrary maximum fd_set size, which typically is 1024 but is not guaranteed to be. Finally, for a large FD_SETSIZE, consider the performance hit of setting 1000 sockets before calling select followed by checking whether each of those 1000 sockets is set after the call returns

翻译:使用选择模式的优势是能够在一个线程中处理多重联接和I/O,防止了由于线程过多导致的堵塞套接字和多重连接。劣势就是过多的socket会被添加到fd_set这个结构体中,默认的,这个最大值为FD_SETSIZE,在WINSOCK2.H中被定义为64,。为了能够增加这个上线,应用程序也许会定义FD_SETSIZE为更大的某个数。这个定义必须出现在包含头文件WINSOCK2.H之前。另外,潜在的供应者(直译)强制使用一个fd_set,其值一般1024,但是并不保证一定是。最后,因为一个较大的FD_SETSIZE,会导致系统性能退化,因为调用select函数之前设置这个1000个socket,并且在函数返回之后,会检查是否每一个socket的值是否被设定了。(没太读懂,读懂了的大神请留言)

In this project, student should develop two simple console mode programs using the select I/O model: SelectFileServer and SelectFileClient, when the SelectFileclientsends a file request to the SelectFileServer, the server will send back the file to the client if the file exists.

在本篇报告中,学生应该开发出两个使用了I/O模式的简单的控制台程序,SelectFileServer和SelectFileClient,当SelectFileClient发送了一个请求发送文件的求情的时候,如果文件存在于服务器的话,这个服务器应该回送一个文件给客户端。

B. Specification

The server should use the select I/O model to manage the incoming connections from the client.
  • The server should be a concurrent server that can provide downloading file service simultaneously for multi-clients.
  • The Server should display relevant information about the current connection and downloading status.
  • The client should use multi-thread to download a file.
  • The client should display relevant information about the current connection and downloading status (e.g. the downloading process information).

    译:
  • 这个服务端需要使用select I/O模式去处理来自客户端的连接
  • 这个服务器端是一个并发服务器,它能够处理来自多个客户端请求下载的要求。
  • 这个服务器能够显示出信息,包括当前的连接和下载的状态
  • 这个客户端应该使用多线程来下载一个文件
  • 这个客户端应该展示相关信息:包括当前的连接和下载的状态,比如下载的进度的信息。

C. Protocol Design

1. The client sends a request to the server to ask for the size of a specific file. 
2. The server sends back a reply with the size of the file if file exists.
3. The client creates a separate thread, and send request for the file data (optional implementation: the client may create 2 or more threads for downloading, each thread for a part of the file in a separate connection).
4. The server accepts the data download request, and starts a new thread to send the file to the client(if the client uses more than 1 thread for downloading, the server should start more than 1 thread for the corresponding connection request).
5. Format of Protocol Data Unit:Ageneral Protocol Data Unit format is defined as:


翻译:
  1. 这个客户端发送一个请求给服务端要一个特别的文件的大小
  2. 这个服务端发一个含该文件的大小的回复,如果文件存在的话。
  3. 这个客户端能够创建独立的线程,并且能够向服务器请求发送文件数据(也可以考虑实现:这个客户端可以创建2个或多个线程用于下载,每一个线程都下载同一个线程的一部分。)
  4. 这个服务器可以接受到下载数据的请求,并且开始一个先的线程将文件发送给客户端(如果这个客户端使用了超过一个线程去下载,这个服务器需要开启多个线程去响应响应的请求)。
  5. 协议数据单元的格式:一个普通的协议数据单元的格式定义看英文部分


The messages used in the client/server communication can be the following two types:
  • l File Size Request /Reply : Type=1
  • l File Data Request/Reply : Type=2
For a File Size Request/Reply message, the Data field is the name string for the requested file.
For a File Data Request message, the Data field also contains the name string for the requested file, and for a File Data Reply message, the Data field contains corresponding data segment (starting from the Position, and length is the Size) of the requested file.
The message header can be implemented as the following structure:
typedef struct MsgHeader{
char   Type;       //Message Type,
char Reserved;      //Reserved for future use
unsigned short TotalLength;  //Total Message Length including Header and Data
unsigned long Position;    //Identities the relative position in the downloading file for the
//1st byte of the Data field of the message
unsigned long Size;      // a) the size of the File in a File Size Request/Rely Msg
// b) the size of the total data starting from the Position field indicates in a File 
// Data Request Msg (could be used for multi-part downloading)
// c) the size of the Data field of current Msg in a File Data Reply Msg
}MessageHeader; 
Request/Reply Messages by Example:


翻译:在C/S通信过程中使用的信息可以有如下两种类型:
  • 文件的大小请求或者回复:type=1
  • 文件数据的请求或者请求:Type=2
对于一个文件大小的请求或者回复的信息,这个data部分是需要传送的文件的名字
对于一个文件数据的请求的信息,这个data部分也应该包含需要传送的文件的名字,对于一个文件数据回复的信息来说,数据部分应该文件对应的数据片段,开始于Position,长度为Size。
下列结构体可用于描述消息头(看英文)

例子看英文部分

6.  The server should send an empty data message (Data part is empty, Position and Data size should be zero) when all data of the current file are sent out. This message indicates the end of file transmission for the current thread.

6、当所有的文件数据都发送结束后,服务端应该能够发送一个空数据(数据部分为0,位置和数据大小也为0),这对线程意味着结束了文件的传输。例子看上图。

自己的理解

首先认为老师定义的协议设计为何如此复杂,感觉十分难用,而且没有必要。总感觉其中REserved和TotalLength、position都没什么用。
所以我决定设计一个更多简单实用的,如下图所示:

首先,我确定了每次就传输最多1024个字节,如果不足后面用0来填满,那就省去了Total Length那一行。
其次,当Type为-1时,表示,客户端向服务器传输需要的文件名,那么data里面存放就是文件名。\
接着,当Type大于0时,那么data后面就是向客户端回传的文件的数据,type具体的值就是这个文件的大小。
最后,如果因为type里面有一共需要传多少,那么客户端也知道要接受多少,所以,当接受到那么多之后,客户端不接了,实际上服务器也不会再传了。那么就算文件传完了。
总之,这个地方是把在程序写好之后再写的这个总结,所以我没发现我这样设计有什么大的问题,至少两个客户端连接服务器的时候,同时传输数据的时候,没有发生乱子。
那么具体在代码中设计的结构体,一下就简单多了。如下所示:
typedef struct MsgHeader{  
	long Type;       //大于0表示长度,等于-1表示数据后面跟的是请求文件的文件名
	char data[DEFAULT_BUFLEN-4];//反正每次这样该协议就规定了每次发送就发送1024个字节。
}MessageHeader;

Section II Solution Method and Design

I/O Multiplexing

作为并发编程的一个方式,老师称为select Model,我喜欢的那本深入理解计算机系统称为I/O多路复用。上次就是说了这个玩意很理解,想不到马上就用它了,通过几次使用之后,我发现其实貌似原理还是很简单的,不知道书上和老师讲的很复杂的样子。

通俗的理解

至少目前的我是这样理解的,
这个是在模拟多线程,如何模拟的呢?它把所有的I/O都装到一个集合里了,然后,到某一个I/O操作准备好了,就将它在集合里对应的标示符,改变一下,比如改为1,当然这唯一的一个线程执行的它的时候,查看一下是不是1,如果是1,线程就去执行响应的代码。
相当于每一个轮回,都要执行:看看谁的I/O操作准备好了。然后把准备好的I/O操作,依次执行一遍。
我认为就是模拟了多线程,本来多线程的调度是由内核来执行的,相当于本来每个线程里面的代码就是执行了一会就停下来了,然后别的线程启动,执行别的线程的代码。这个I/O多路复用,也就是用一个线程去执行不同的I/O的代码。

书上的讲解

再多的理论有点空白,还是举书上那个例子吧
试想有这样一个服务器,
如果有个客户端连接它,并且发送文字给他时,他必须立马和接通这个客户端。
如果我用户在键盘上输入文字的时候,他必须立马把输入的文字显示出来。
所以现在我做一个集合,
暂时就叫read_set(这是为了和书上的图保持一直,此外,因为这两个操作都是“I”的操作,也就I/O中的I,进入数据的意思,相当于数据进来了,计算机就要去读)
读集合里有当然还有别的I/O操作,我们假设有四个(其实每个都有含义,而且前3个是固定的,但是我给忘了是什么含义了)
如下图所示:

之后,我们来循环执行一段代码,这段代码会执行一个叫select的函数,如果这个select函数会堵塞,堵塞到什么时候呢?直到上面那个集合内的某个0变成1,它就会返回,那变1表示什么呢?表示有I/O的操作了,比如说我们在键盘上输入了字符,并且按了空格,那么stdin就会变为1,因为现在有数据进来了,我们要读取它,如果有一个客户端要来接着这个服务器了,那么listenfd的标志就会变成1,表示有链接来了,需要处理一下。
如下图所示:

现在读集合里有了0和3,是吧,然后我们就要在代码里写好,当有0的时候,应该执行那段代码,当有3的时候该执行的那段代码。这些都是我们认为控制的。
比如执行完了处理链接的函数(也就是有客户端来连接,listenfd变成了1)之后,又回到了这样的状态:

那么就算处理完了一个I/O事件,接着处理stdin的,处理完之后就会到第一个图的状态
那样,就只用了一个线程,完成了多个线程的工作。具体代码如下:
当然,涉及到函数时,我们就会发现有很多重要的操作已经有了相应的api了,我们调用即可
1 #include "csapp.h"
2 void echo(int connfd);
3 void command(void);
4
5 int main(int argc, char **argv)
6 {
7 int listenfd, connfd, port;
8 socklen_t clientlen = sizeof(struct sockaddr_in);
9 struct sockaddr_in clientaddr;
10 fd_set read_set, ready_set;
11
12 if (argc != 2) {
13 fprintf(stderr, "usage: %s <port>\n", argv[0]);
14 exit(0);
15 }
16 port = atoi(argv[1]);
17 listenfd = Open_listenfd(port);
18
19 FD_ZERO(&read_set); /* Clear read set */
20 FD_SET(STDIN_FILENO, &read_set);/* Add stdin to read set */
21 FD_SET(listenfd, &read_set); /* Add listenfd to read set */
22
23 while (1) {
24 ready_set = read_set;
25 Select(listenfd+1, &ready_set, NULL, NULL, NULL);//如果有这个集合里面的哪个变成了1,就会返回,然后执行下面的代码
26 if (FD_ISSET(STDIN_FILENO, &ready_set))//查看STDIN_FILENO是不是变为了1,如果是,则执行相应的代码
27 command();/* Read command line from stdin */
28 if (FD_ISSET(listenfd, &ready_set)) {//查看listenfd是不是变为了1,如果是,则执行相应的代码
29 connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
30 echo(connfd);/* Echo client input until EOF */
31 Close(connfd);
32 }
33 }
34 }
35
36 void command(void) {
37 char buf[MAXLINE];
38 if (!Fgets(buf, MAXLINE, stdin))
39 exit(0);/* EOF */
40 printf("%s", buf);/* Process the input command */
41 }




Section III Test Cases and Results Analysis

两个客户端可以同时连上服务端


服务器向两个客户端同时发送数据


Section IV Conclusion

1、关于I/O多路复用的问题,更多、更好的参考还是来自
W. R. Stevens, B. Fenner, and A. M. Rudoff.Unix Network Programming: The SocketsNetworking API, Third Edition, volume 1.Prentice Hall, 2003.
W. R. Stevens and S. A. Rago. AdvancedProgramming in the Unix Environment, SecondEdition. Addison-Wesley, 2008

2、为什么要将协议设计那么复杂,不设计的那么复杂行不行?是不是因为客户端多线程下载的关系才把协议设计的如此复杂?

3、I/O多路复用的优劣势
I/O多路复用始终都是在一个线程中进行的,所以给数据的共享提供了方便的方式,此时我们可以使用GDB来调试
显然,劣势就是代码十分复杂啊,其实我现在也不是看的特别特别明白,而且,我们还不能很好的利用多核处理器。


Section V References


MSDN

Section VI Appendix

服务器的代码

#include <winsock2.h> 
#include <windows.h> 
#include <mswsock.h> 
#include <stdio.h> 

// 将ws2_32.lib连接到工程,也可以在工程选项中的连接中设置
#pragma comment(lib, "ws2_32.lib") 

#define PORT 5150 
#define DATA_BUFSIZE 8192 
#define DEFAULT_BUFLEN 1024  


typedef struct MsgHeader{  
	long Type;       //大于0表示长度,等于-1表示数据后面跟的是请求文件的文件名,小于-2则为该文件不存在
	char data[DEFAULT_BUFLEN-4];//反正每次这样该协议就规定了每次发送就发送1024个字节。
}MessageHeader;
typedef struct _SOCKET_INFORMATION { 
	CHAR Buffer[DATA_BUFSIZE]; 
	WSABUF DataBuf; 
	SOCKET Socket; 
	OVERLAPPED Overlapped; 
	DWORD BytesSEND; 
	DWORD BytesRECV; 
	HANDLE hFile; 
	MsgHeader Msg;
	DWORD NeedToSEND;//表示某个需要被发送的文件的大小
	//char fileName[25];
} SOCKET_INFORMATION, * LPSOCKET_INFORMATION; 



BOOL CreateSocketInformation(SOCKET s); 
void FreeSocketInformation(DWORD Index); 

DWORD TotalSockets = 0; 
LPSOCKET_INFORMATION SocketArray[FD_SETSIZE]; 

void main(void) 
{ 
	SOCKET ListenSocket; 
	SOCKET AcceptSocket; 
	SOCKADDR_IN InternetAddr; 
	WSADATA wsaData; 
	INT Ret; 
	FD_SET WriteSet; 
	FD_SET ReadSet; 
	DWORD i; 
	DWORD Total; 
	ULONG NonBlock; 
	DWORD Flags; 
	DWORD SendBytes; 
	DWORD RecvBytes;     

	// WSAStartup即WSA(Windows SocKNDs Asynchronous,
	// Windows套接字异步)的启动命令
	// 该函数的第一个参数指明程序请求使用的Socket版本
	// 操作系统利用第二个参数返回请求的Socket的版本信息
	if ((Ret = WSAStartup(0x0202,&wsaData)) != 0) 
	{ 
		printf("WSAStartup() failed with error %d\n", Ret); 
		WSACleanup(); 
		return; 
	} 

	// Prepare a socket to listen for connections.     
	// 创建一个与指定传送服务提供者捆绑的套接口
	if (INVALID_SOCKET == (ListenSocket = WSASocket(AF_INET, // int af 地址族描述。
		SOCK_STREAM,    // int type 新套接口的类型描述
		0,              // int protocol 套接口使用的特定协议
		NULL,           // LPPROTOCOL_INFO lpProtocolInfo 一个指向PROTOCOL_INFO结构的指针
		0,              // Group g 套接口组的描述字
		WSA_FLAG_OVERLAPPED)))   // int iFlags 套接口属性描述
	{ 
		printf("WSASocket() failed with error %d\n", WSAGetLastError()); 
		return; 
	} 

	InternetAddr.sin_family = AF_INET; 
	InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); 
	InternetAddr.sin_port = htons(PORT); 

	// 将socket绑定到一个端口
	if (bind(ListenSocket, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) 
		== SOCKET_ERROR) 
	{ 
		printf("bind() failed with error %d\n", WSAGetLastError()); 
		return; 
	} 

	// 监听这个端口,这个程序可同时处理5个客户端接入的请求
	if (listen(ListenSocket, 5)) 
	{ 
		printf("listen() failed with error %d\n", WSAGetLastError()); 
		return; 
	} 

	// Change the socket mode on the listening socket from blocking to 
	// non-block so the application will not block waiting for requests.     
	NonBlock = 1; 
	// 本函数可用于任一状态的任一套接口。它用于获取与套接口相关的操作参数,
	// 而与具体协议或通讯子系统无关。FIONBIO:允许或禁止套接口s的非阻塞模式。
	if (ioctlsocket(ListenSocket, FIONBIO, &NonBlock) == SOCKET_ERROR) 
	{ 
		printf("ioctlsocket() failed with error %d\n", WSAGetLastError()); 
		return; 
	} 
	printf("开始监听,端口号为5150\n");
	while(TRUE) 
	{ 
		// Prepare the Read and Write socket sets for network I/O notification. 
		FD_ZERO(&ReadSet); // 将set初始化为空集NULL。

		FD_ZERO(&WriteSet); // 

		// Always look for connection attempts.         
		FD_SET(ListenSocket, &ReadSet); // 向集合添加描述字。

		// Set Read and Write notification for each socket based on the 
		// current state the buffer. If there is data remaining in the 
		// buffer then set the Write set otherwise the Read set.         
		for (i = 0; i < TotalSockets; i++) 
		{// FD_SET 向集合添加描述字。
			//if (SocketArray[i]->BytesRECV > SocketArray[i]->BytesSEND)//因为接受到的大于了发送的,也就是说收到的内容还没有发送完 
			if (SocketArray[i]->NeedToSEND > 0)//我将其改为如果还有需要发送的资源	
				FD_SET(SocketArray[i]->Socket, &WriteSet); 
			else 
				FD_SET(SocketArray[i]->Socket, &ReadSet); 
		}

		if ((Total = select(0, &ReadSet, &WriteSet, NULL, NULL)) == SOCKET_ERROR)  
		{ 
			printf("select() returned with error %d\n", WSAGetLastError()); 
			return; 
		} 
		
		// Check for arriving connections on the listening socket. 
		// FD_ISSET(int fd, fd_set *set);用来测试描述词组set中相关fd的位是否为真
		if (FD_ISSET(ListenSocket, &ReadSet)) 
		{ 
			Total--; 
			if ((AcceptSocket = accept(ListenSocket, NULL, NULL)) != INVALID_SOCKET)  
			{                     
				// Set the accepted socket to non-blocking mode so the server will 
				// not get caught in a blocked condition on WSASends                     
				NonBlock = 1; 
				if (ioctlsocket(AcceptSocket, FIONBIO, &NonBlock) == SOCKET_ERROR) 
				{ 
					printf("ioctlsocket() failed with error %d\n", WSAGetLastError()); 
					return; 
				} 

				if (CreateSocketInformation(AcceptSocket) == FALSE) 
					return;                     
			}
			else 
			{	 
				if (WSAGetLastError() != WSAEWOULDBLOCK) 
				{ 
					printf("accept() failed with error %d\n", WSAGetLastError()); 
					return; 
				} 
			} 
		} 

		// Check each socket for Read and Write notification until the number 
		// of sockets in Total is satisfied.             
		for (i = 0; Total > 0 && i < TotalSockets; i++) 
		{ 
			LPSOCKET_INFORMATION SocketInfo = SocketArray[i]; 

			// If the ReadSet is marked for this socket then this means data 
			// is available to be read on the socket.                 
			if (FD_ISSET(SocketInfo->Socket, &ReadSet)) 
			{
				Total--; 

				SocketInfo->DataBuf.buf = SocketInfo->Buffer; 
				SocketInfo->DataBuf.len = DATA_BUFSIZE; 

				Flags = 0; 
				//第二个参数是接受的内容,第四个参数是接受内容的字节数
				if (WSARecv(SocketInfo->Socket, &(SocketInfo->DataBuf), 1, &RecvBytes, &Flags, NULL, NULL) == SOCKET_ERROR) 
				{ 
					if (WSAGetLastError() != WSAEWOULDBLOCK) 
					{ 
						printf("WSARecv() failed with error %d\n", WSAGetLastError());                             
						FreeSocketInformation(i); 
					}                         
					continue; 
				}else {
					//注意当上面那个WSARecv函数成功了才会执行下面这段函数

					SocketInfo->BytesRECV = RecvBytes;
					int fileLength, cbLeftToSend;  
					MessageHeader *msg;
					msg=(MessageHeader *)SocketInfo->DataBuf.buf;
					if (msg->Type==-1)
					{
						SocketInfo->hFile = CreateFile( msg->data, //打开要传送的文件//  
							GENERIC_READ,  
							FILE_SHARE_READ,  
							NULL,  
							OPEN_EXISTING,  
							0,   
							NULL);  
					//	memset(SocketInfo->fileName,'\0',sizeof(SocketInfo->fileName));
						//memcpy(SocketInfo->fileName,&msg->data,sizeof(msg->data));
						SocketInfo->NeedToSEND = GetFileSize(SocketInfo->hFile, NULL);
						SocketInfo->Msg.Type=SocketInfo->NeedToSEND;
					}


					// If zero bytes are received, this indicates the peer closed the 
					// connection. 
					if (RecvBytes == 0) 
					{ 
						FreeSocketInformation(i); 
						continue; 
					} 
				} 
			} 


			// If the WriteSet is marked on this socket then this means the internal 
			// data buffers are available for more data.                 
			if (FD_ISSET(SocketInfo->Socket, &WriteSet)) 
			{ 
				Total--; 
				
				//SocketInfo->DataBuf.buf = SocketInfo->Buffer + SocketInfo->BytesSEND; 
				//SocketInfo->DataBuf.len = SocketInfo->BytesRECV - SocketInfo->BytesSEND; 
				//应该根据收到的内容打开文件,并且读取文件,然后发送文件
				/*char string[25];
				memset(string,'\0',sizeof(string));
				itoa(SocketInfo->NeedToSEND, string, 10);*/

				WSABUF SendData;
				DWORD dwLen,hasSent;  
				if (ReadFile(SocketInfo->hFile, SocketInfo->Msg.data, (DEFAULT_BUFLEN-4), &dwLen, NULL)==0)
				{
					//printf("读完了");                             
				}else{
					//printf("\n读了字节:%d\n", dwLen);                             
				}
				CHAR* sendData = NULL;
				sendData = new CHAR[DEFAULT_BUFLEN];
				//sendData = (char *)SocketInfo->Msg;
				memcpy(sendData,&(SocketInfo->Msg.Type),1024);
				SendData.buf = sendData;
				SendData.len = (dwLen+4);
				if (WSASend(SocketInfo->Socket, &SendData, 1, &hasSent, 0, NULL, NULL) == SOCKET_ERROR) 
				{ 
					if (WSAGetLastError() != WSAEWOULDBLOCK) 
					{ 
						printf("WSASend() failed with error %d\n", WSAGetLastError());                             
						FreeSocketInformation(i); 
					}                         
					continue; 
				} 
				else 
				{ 
					SocketInfo->NeedToSEND -= (hasSent-4);   
					float remained=(((float)SocketInfo->NeedToSEND)/((float)SocketInfo->Msg.Type));
					if (rand()%10==0||remained==0)
					{
						printf("连接号:%d, 剩余:%2.2f%%",SocketInfo->Socket,remained*100);                             
						if (remained!=0)
						{
							printf("\n");
						}else{
							printf("    传输完成!\n");
						}
					}


				} 
			} 
		} 
	} 
} 

BOOL CreateSocketInformation(SOCKET s) 
{ 
	LPSOCKET_INFORMATION SI; 

	printf("Accepted socket number %d\n", s); 

	if ((SI = (LPSOCKET_INFORMATION) GlobalAlloc(GPTR,sizeof(SOCKET_INFORMATION))) == NULL) 
	{ 
		printf("GlobalAlloc() failed with error %d\n", GetLastError()); 
		return FALSE; 
	} 

	// Prepare SocketInfo structure for use. 
	SI->Socket = s; 
	SI->BytesSEND = 0; 
	SI->BytesRECV = 0; 

	SocketArray[TotalSockets] = SI; 

	TotalSockets++; 

	return(TRUE); 
}   

void FreeSocketInformation(DWORD Index) 
{ 
	LPSOCKET_INFORMATION SI = SocketArray[Index]; 
	DWORD i; 

	closesocket(SI->Socket); 

	printf("Closing socket number %d\n", SI->Socket); 

	GlobalFree(SI); 

	// Squash the socket array 
	// 调整SocketArray的位置,填补队列中的空缺
	for (i = Index; i < TotalSockets; i++) 
	{ 
		SocketArray[i] = SocketArray[i + 1]; 
	} 

	TotalSockets--; 
} 

客户端的代码:

#define WIN32_LEAN_AND_MEAN

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


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

#define WIN32_LEAN_AND_MEAN  

#include <windows.h>  
#include <winsock2.h>  
#include <ws2tcpip.h>  
#include <stdlib.h>  
#include <stdio.h>  
#define PORT "5150"  
#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 1024 

typedef struct MsgHeader{  
	long Type;       //大于0表示长度,等于0表示数据后面跟的是请求文件的文件名,小于0(-1)则为该文件不存在
	char data[DEFAULT_BUFLEN-4];//反正每次这样该协议就规定了每次发送就发送1024个字节。
}MessageHeader;

int __cdecl main(int argc, char **argv) 
{	
	if (argc!=3)
	{
		printf("使用方法有误,请输入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 Winsock  
	iResult = 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 port  
	iResult = getaddrinfo(argv[1], argv[2], &hints, &result);  
	if ( iResult != 0 ) {  
		printf("getaddrinfo failed with error: %d\n", iResult);  
		WSACleanup();  
	}  

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

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

		// 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 buffer  
		printf("what file do you want?\n");  
		MessageHeader  msg;
		msg.Type=-1;
		memset(msg.data,'\0',sizeof(msg.data));  
		scanf("%s",&(msg.data));  
		memcpy(sendbuf,&(msg.Type),1024);
		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;  
		BYTE* recvTypeAndData = 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");  
	
		//开始接收文件  
		recvTypeAndData = new BYTE[DEFAULT_BUFLEN];  
		recvData = new BYTE[DEFAULT_BUFLEN-4];  
		MsgHeader *Msg;
		cbLeftToRecv = 1024;  
		bool FirstToGetLength=true;
		do {  
			int iiGet, iiRecv,putIntoFile;  

			iiGet = (cbLeftToRecv < DEFAULT_BUFLEN) ? cbLeftToRecv : DEFAULT_BUFLEN;  
			iiRecv = recv(ConnectSocket, (char*)recvTypeAndData, iiGet, 0); //流型数据的接收处理//
			Msg=(MsgHeader *)recvTypeAndData;
			if (FirstToGetLength)
			{
				cbLeftToRecv=Msg->Type;
				FirstToGetLength=false;
			}
			recvData=(BYTE *)Msg->data;
			putIntoFile=(cbLeftToRecv < (DEFAULT_BUFLEN-4)) ? cbLeftToRecv : (DEFAULT_BUFLEN-4);
			if (iiRecv < 0 || iiRecv == 0) {  
				perror("recv");  
				printf("警告: 接收文件失败!\n");  
				goto PreReturn;  
			}  
			DWORD dwLen;  
			int ret3 = WriteFile(hFile, //Long,一个文件的句柄  
				recvData, //Any,要写入的一个数据缓冲区  
				putIntoFile, //Long,要写入数据的字节数量。如写入零字节,表示什么都不写入,针对位于远程系统的命名管道,限制在65535个字节以内  
				&dwLen, //Long,实际写入文件的字节数量  
				NULL); //OVERLAPPED,倘若在指定FILE_FLAG_OVERLAPPED的前提下打开文件,这个参数就必须引用一个特殊的结构。  
			//那个结构定义了一次异步写操作。否则,该参数应置为空(将声明变为ByVal As Long,并传递零值)  
			cbLeftToRecv -= putIntoFile;  
			float remained=(((float)cbLeftToRecv)/((float)Msg->Type))*100;
			if ((int)remained%10==0){
				printf("剩余:%2.2f%%\n",remained);                             
			}
		} while (cbLeftToRecv > 0);  

		printf("Receviced Completed \n");  


		//接收结束,释放内存,关闭连接//  
PreReturn :  
		//delete recvData;  
		CloseHandle(hFile);  

		// cleanup  
	}  

	closesocket(ConnectSocket);  
	WSACleanup();  
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值