昨天花了点时间,写了个小程序,做了个局域网内的文件传输工具。当然了,关键在于学习,局域网内的文件用共享就好了,还有什么好传的。但里面用到的知识是可以用在INTERNET上的,只不过如果用在公网上的话一般还需要一个服务器的中转,多一步NAT穿透技术的支持。就是在做这个小程序的时候也出现过一些问题。先把关键部分代码贴出,再细细解释。

struct FILE_INFO
{
	int FileLength;
	char FileName[50];
	char* buf;
};

这是我用于传输文件信息的结构体,本来我将buf定义为CString类型,在传送阶段没有出现错误,很正常,但是在接收端却出现了异常,接收端接收到的buf显示的是乱码,至今不知道为什么。

void CXYFTDlg::OnBtnSend() 
{
	// TODO: Add your control notification handler code here
	DWORD dwIP;
	((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
	
	SOCKADDR_IN addrTo;
	addrTo.sin_addr.S_un.S_addr = htonl(dwIP);
	addrTo.sin_family = AF_INET;
	addrTo.sin_port = htons(8000);

	CFileDialog fdlg(TRUE,NULL,NULL,OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT,
		"ALL FILE(*.*)|*.*||",NULL);
	fdlg.DoModal();
	FILE_INFO finfo;
	SEND_STRUCT* pSendStruct = new SEND_STRUCT;
	strcpy(finfo.FileName,fdlg.GetFileName());
	finfo.buf = new char[20];
	memset(finfo.buf,0,20);
	strcpy(finfo.buf,"IWANTTOSENDFILETOYOU!");
	finfo.FileLength = 0;
	strcpy(pSendStruct->FileName,fdlg.GetFileName());
	strcpy(pSendStruct->FilePath,fdlg.GetPathName());
	pSendStruct->hwnd = m_hWnd;
	pSendStruct->sock = m_socket;
	pSendStruct->addrTo = addrTo;
	if(SOCKET_ERROR == sendto(m_socket,(const char*)&finfo,
		sizeof(finfo),0,(SOCKADDR*)&addrTo,sizeof(SOCKADDR)))
	{
		MessageBox("发送失败!");
		return;
	}

	HANDLE hThread = CreateThread(NULL,0,SendProc,(LPVOID)pSendStruct,0,NULL);
	CloseHandle(hThread);
	return;
}

这是发送端代码,因为要询问对方是否同意接收,所以要多了一个线程用于等待对方消息。如果没有多增加一个线程的话,等待将是痛苦的,而如果是在本机上进行测试的话,这样的等待更将是没有结果的,因为你根本没有表达的机会,就在那“死循环”。

DWORD WINAPI CXYFTDlg::SendProc(LPVOID lpParameter)
{
	FILE_INFO finfo;
    strcpy(finfo.FileName,((SEND_STRUCT*)lpParameter)->FileName);
	SOCKET sock = ((SEND_STRUCT*)lpParameter)->sock;
	HWND hWnd = ((SEND_STRUCT*)lpParameter)->hwnd;
	char FilePath[100];
	strcpy(FilePath,((SEND_STRUCT*)lpParameter)->FilePath);
	SOCKADDR_IN addrTo = ((SEND_STRUCT*)lpParameter)->addrTo;

	while(TRUE)
	{
		if(m_SendAck == 0)
		{
			AfxMessageBox("对方已拒绝接收");
			m_SendAck = 2;
			return 0 ;
		}else
			if(m_SendAck == 1)
			{
				AfxMessageBox("对方已经同意接收");
				CFile file(FilePath,CFile::modeRead|CFile::typeBinary);
				char* pBuf = new char[file.GetLength()];
				file.ReadHuge(pBuf,file.GetLength());
				finfo.buf = new char[file.GetLength()];
				memcpy(finfo.buf,pBuf,file.GetLength());
				finfo.FileLength = file.GetLength();
				AfxMessageBox(finfo.FileName);
				
				if(SOCKET_ERROR == sendto(sock,(const char*)&finfo,
					sizeof(finfo),0,(SOCKADDR*)&addrTo,sizeof(SOCKADDR)))
				{
					int fail = WSAGetLastError();
					CString szfail;
					szfail.Format("%d",fail);
					AfxMessageBox(szfail);
					return 0;
				}
				file.Close();
				m_SendAck = 2;
				return 1;
			}
	}
	return 0;
}

这就是那个专门用于等待对方应答和发送文件正文的线程。做这个的过程中不得不提的就是通过int  WSAGetLastError (void);函数得到的发送错误报警10038,在CSDN当中,查到的10038提示的错误是:


WSAENOTSOCK

(10038)


Socket operation on nonsocket.

An operation was attempted on something that is not a socket. Either the socket handle parameter did not reference a valid socket, or for select, a member of an

fd_set was not valid.


说的大概意思就是在一个无效的SOCKET上进行操作。我出现这个错误的原因并不是因为SOCKET关闭或者SOCKET的什么问题,而是因为当中的一个变量——用于表示文件路径名的FilePath定义得过小,这可能就导致了该说明所提到的第二个错误 for select, a member of an fd_set was not valid。

void CXYFTDlg::OnRecv(WPARAM wParam,LPARAM lParam)
{
	switch(LOWORD(lParam))
	{
	case FD_READ:

		SOCKADDR_IN addrFrom;
		char recvBuf[300];
		int len = sizeof(SOCKADDR);
		
		if(SOCKET_ERROR == recvfrom(m_socket,recvBuf,300,0,
			(SOCKADDR*)&addrFrom,&len))
		{
			int fail = WSAGetLastError();
			CString sfail;
			sfail.Format("%d",fail);
			MessageBox(sfail);
			return;
		}
		
		if(strcmp(((FILE_INFO*)recvBuf)->buf,"IWANTTOSENDFILETOYOU!") == 0)
		{
			FILE_INFO finfo;
			finfo.buf = new char[20];
			memset(finfo.buf,0,20);
			memset(finfo.FileName,0,20);
			CString strTemp = inet_ntoa(addrFrom.sin_addr);
			strTemp += "想传送文件";
			strTemp += ((FILE_INFO*)recvBuf)->FileName;
			strTemp += "给你,是否接收?";
			SetDlgItemText(IDC_EDIT_SHOW,strTemp);
			RECV_STRUCT* pRecvStruct = new RECV_STRUCT;
			pRecvStruct->hwnd = m_hWnd;
			pRecvStruct->sock = m_socket;
			pRecvStruct->addrFrom = addrFrom;
			HANDLE hThread = CreateThread(NULL,0,RecvProc,pRecvStruct,
							0,NULL);
			CloseHandle(hThread);
			
		}else 
			if(strcmp(((FILE_INFO*)recvBuf)->FileName,"ACKINFO") == 0)
			{
				if(strcmp(((FILE_INFO*)recvBuf)->buf,"RECEIVE")==0)
				{
					m_SendAck = 1;
					return;
				}else
				{
					m_SendAck = 0;
					return;
				}
			}else
			{
				CString szAddress;
				MessageBox(((FILE_INFO*)recvBuf)->FileName);
				szAddress += "D://新建文件夹//XYFT//Debug//";
				szAddress += ((FILE_INFO*)recvBuf)->FileName;
				MessageBox(szAddress);
				CFile file(szAddress,
					CFile::modeCreate|CFile::modeWrite|CFile::typeBinary);
				file.WriteHuge(((FILE_INFO*)recvBuf)->buf,
					((FILE_INFO*)recvBuf)->FileLength);
			}
	}
}

这是接收端的代码之一,我使用的是异步套接字进行接收,感觉挺方便的。当然,接收端跟发送端一样,也需要在建立一个线程,用于回答发送端的询问。如果没有多一个线程的话,等待也将是痛苦的。

DWORD WINAPI CXYFTDlg::RecvProc(LPVOID lpParameter)
{
	HWND hwnd = ((RECV_STRUCT*)lpParameter)->hwnd;
	SOCKET sock = ((RECV_STRUCT*)lpParameter)->sock;
	SOCKADDR_IN addrFrom = ((RECV_STRUCT*)lpParameter)->addrFrom;
	FILE_INFO finfo;
	finfo.buf = new char[20];
	memset(finfo.buf,0,20);

	while(TRUE)
	{
		if(m_ACKReceive == 1)
		{
			strcpy(finfo.FileName,"ACKINFO");
			strcpy(finfo.buf,"RECEIVE");
			finfo.FileLength = 0;
			if(SOCKET_ERROR == sendto(sock,(const char*)&finfo,
				sizeof(finfo),0,(SOCKADDR*)&addrFrom,sizeof(SOCKADDR)))
			{
				AfxMessageBox("发送失败!");
				return 0;
			}
			CString strShow = "您已经同意接收来自";
			strShow += inet_ntoa(addrFrom.sin_addr);
			strShow += "的文件";
			::PostMessage(hwnd,UM_SHOW,0,
				(LPARAM)strShow.GetBuffer(strShow.GetLength()));
			m_ACKReceive = 2;
			return 1;
		}
		if(m_ACKReceive == 0)
		{
			strcpy(finfo.FileName,"ACKINFO");
			strcpy(finfo.buf,"NORECEIVE");
			finfo.FileLength = 0;
			if(SOCKET_ERROR == sendto(sock,(const char*)&finfo,
				sizeof(finfo),0,(SOCKADDR*)&addrFrom,sizeof(SOCKADDR)))
			{
				AfxMessageBox("发送失败!");
				return 0;
			}
			CString strShow = "您已经拒绝接收来自";
			strShow += inet_ntoa(addrFrom.sin_addr);
			strShow += "的文件";
			::PostMessage(hwnd,UM_SHOW,0,
				(LPARAM)strShow.GetBuffer(strShow.GetLength()));
			m_ACKReceive = 2;
			return 1;				
		}
	}
}

这就是接收端新增加的线程。

 

当中还定义了几个自定义消息和静态成员变量。之所以定义静态成员变量是因为CreateThread函数的响应函数是一个静态成员函数,要在外部对工作在它内部的一个变量进行修改,方法之一就是定义一个静态成员变量。但是需要记住的一点就是静态成员变量的初始化必须在类的外部。

int CXYFTDlg::m_SendAck = 2;
int CXYFTDlg::m_ACKReceive = 2;

大概的程序就是如此,不足还有很多,希望路过者多提宝贵意见,共同学习,共同进步。