记一个网络传输功能的实现过程

写在前面的话:功能是基于C/S模型的网络传输实现,要求是服务器端可以在局域网中任何机子上运行,客户端启动后自动寻找服务器端进行连接,之后,服务器端向已经连接的客户端发送命令,客户端根据命令执行相应的操作(即发送某个约定文件夹下的所有文件),并且客户端不需要用户操作。

1、思路

首先,对于这个功能的实现思路如下,因为服务器不确定在哪个机子上,所以为了寻找到服务器端,客户端需要发送广播消息,并且为了维护客户端在线,广播消息需要实现成心跳包(即定时发送广播消息)。服务器监听心跳包,如果是新加入的客户端,则更新用户列表,否则不做处理。这是维护在线,离线的实现还在考虑中。

第二,当服务器端发送命令给客户端时,客户端根据命令来决定发送哪个文件夹中的文件给服务器端,由于文件夹下可能有多个文件,所以需要有个循环,循环发送每个文件。我的处理时在发送前先发送即将发送的文件个数,之后循环发送每个文件,在每个文件的发送过程中,首先发送文件名,之后发送文件长度,最后循环发送文件内容。当然服务器端接受的时候也需要按这个顺序来接收。

2、实现

客户端:

开启一个线程来发送心跳包,每发送一个心跳包,就睡眠3S,接着发送下一个,如此循环,心跳包采用UDP套接字来发送。

当接受命令时,客户端转换为C/S模型中的服务器端,因此客户端需要开启一个线程来监听固定端口,接受服务器端发送来的命令,之后根据命令执行相应的操作。

服务器端:

开启一个线程监听UDP心跳包。根据心跳包的IP地址判断是否做处理。

当点击发送命令按钮时,开启一个线程连接选中的客户端,发送命令并接收数据。

3、碰到的问题

(1)客户端的监听是使用WSAAsyncSelect()函数注册网络事件来实现的,注册了FD_ACCEPT、FD_READ、FD_CLOSE三个网络事件,代码如下:

// TCP套接字
    if(InitTcpSock())
    {
        if(BindTcpSock())
        {
            if(SOCKET_ERROR == WSAAsyncSelect(m_tcpSocket, m_hWnd, WM_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE))
            {
                DWORD dwError = 0;
                dwError = GetLastError();
                ShowError(_T("注册网络事件失败"), dwError);
                return FALSE;
            }
            listen(m_tcpSocket, 2);
        }
    }

在FD_READ网络事件发生时做处理,按接收到的命令执行相应的操作,出问题的地方就是当接受到发送文件的命令,执行相应的操作,发送指定文件夹下所有文件时,代码如下:


1
// 发送指定文件夹所有文件 2 UINT CClientDlg::SendFolderThread(LPVOID lpParameter) 3 { 4 SendParam *pSendParam = (SendParam*)lpParameter; 5 BOOL bIsNull(TRUE); 6 DWORD dwError(0); 7 8 SOCKET socket = pSendParam->socket; 9 10 CString strFolderPath = pSendParam->szFilePath; 11 12 int iFileSum = FileSum(strFolderPath, 0); 13 if(iFileSum == 0) 14 { 15 return dwError; 16 } 17 else 18 { 19 // 发送文件总数 20 char szFileSum[10] = {0}; 21 _itoa_s(iFileSum, szFileSum, 10); 22 if(SOCKET_ERROR == send(socket, szFileSum, 10, NULL)) 23 { 24 dwError = GetLastError(); 25 ShowError(_T("发送文件总数失败"), dwError); 26 delete pSendParam; 27 pSendParam = NULL; 28 return dwError; 29 } 30 31 strFolderPath.Append(_T("\*")); 32 CFileFind finder; 33 BOOL isFind = finder.FindFile(strFolderPath); 34 CString strFilePath; 35 CString strFileName; 36 37 while(isFind) 38 { 39 isFind = finder.FindNextFile(); 40 if(!finder.IsDots()) 41 { 42 strFilePath = finder.GetFilePath(); 43 strFileName = finder.GetFileName(); 44 CFile file; 45 if (!file.Open(strFilePath, CFile::modeRead)) 46 { 47 dwError = GetLastError(); 48 ShowError(_T("打开文件失败"), dwError); 49 delete pSendParam; 50 pSendParam = NULL; 51 return dwError; 52 } 53 54 char strBuf[512] = {0}; 55 char strEnd[512] = {0}; 56 57 UINT len = 0; 58 // 发送文件名 59 WideCharToMultiByte(CP_ACP, 0, 60 strFileName.GetBuffer(strFileName.GetLength() - 1), 61 strFileName.GetLength(), strBuf, 511, 0, 0); 62 strFileName.ReleaseBuffer(); 63 strBuf[strFileName.GetLength()] = 0; 64 if(SOCKET_ERROR == send(socket, strBuf, 512, NULL)) 65 { 66 dwError = GetLastError(); 67 ShowError(_T("发送文件名失败"), dwError); 68 return dwError; 69 } 70 // 发送文件内容 71 memset(strBuf, 0, 512); 72 while(len = file.Read(strBuf, 511)) 73 { 74 if(SOCKET_ERROR == send(socket, strBuf, 512, NULL)) 75 { 76 dwError = GetLastError(); 77 ShowError(_T("发送文件失败"), dwError); 78 return dwError; 79 } 80 memset(strBuf, 0, 512); 81 if(len < 511) 82 { 83 send(socket, strEnd, 512, NULL); 84 break; 85 } 86 } 87 // 如果文件长度刚好是511的整数倍 88 if(0 == len) 89 { 90 send(socket, strEnd, 512, NULL); 91 } 92 file.Close(); 93 } 94 } 95 //AfxMessageBox(_T("文件传送完毕")); 96 } 97 delete pSendParam; 98 pSendParam = NULL; 99 return dwError; 100 }

 问题就出在这段代码的74行,程序运行的时候总是提示10035错误代码,根据错误代码大全,这个错误是无法立即完成一个非阻挡性套接字操作,原因就是说缓冲区已满,无法立即完成发送。在网上搜了一下,感觉这个帖子解释的挺好的,摘抄几个个人认为解释的不错的回复:

  1. 10035错误的原因是无法立即完成一个非阻挡性套接字操作。
    原因就说缓冲区满了 所以无法立即
  2. TCP情况下:
    负责监听和接收的socket:可以采用非阻塞;
    负责发送的发送的socket要采用阻塞模式;

    因为TCP协议在网络上传输的两端是阻塞传输的,你在代码中设置非阻塞只是要socket把数据放到本地缓冲区上就不要管了,其实数据还在本地缓冲区,在上次发出去的数据没有得到回应以前,数据将一直不会发出去,但这时你的socket又把下一批数据放入缓冲区,

    在本机上,把发送端的包大小设置为10240,接收的包设置为5120,会立即出错。

    解决办法:
    1.TCP情况下,把负责发送的socket采用阻塞模式;
    2.采用UDP
  3. 10035出错的原因是这样的
    因为你采用了WSAEventSelect,这个函数会自动把socket设成了非阻塞模式。
    非阻塞模式的socket在send前一定要检查socket的当前状态是否为可send状态,因为你没有合理的检查机制,导至在socket不可send的状态你send了。
    阻塞模式的socket在send的时候不用检查状态的,可以直接send.

    阻塞模式 写法简单,适合单连接。
    用阻塞模式写多连接也可以,不过有几个连接,就得最少开几个线程,为方便控制,书上推荐开连接数*2个线程。

    非阻塞模式,相当难控制,对于现在的我来说,是个很大的工程,正在学习中...
    如果有多个连接,书上建议用非阻塞模式,因为不管有多个连接,只要1-2个线程就可以搞定。

    非阻塞模式有5种控制方法如下(抄书):
    1.select模型
    2.WSAAsyncSelect
    3.WSAEventSelect
    4.重叠模型
    5.完成端口模型
  4. 楼上说的其实都没说到关键
    楼主这样发送大文件的方法根本不是正确的方法,因为这样根本没有考虑发送缓存是否已经满了或网络的异常状况,
    这样一直发一直发,完全不考虑结果,只要有一个send没发送成功,那么本次文件发送就会失败,在实际应用中根本不可取;
    楼主的做发是一种理想的做法;

    如果非要这样循环读循环send的话,可以在每次循环的时候Sleep(10)一下,这样基本就没有问题,但不推荐这样做,这样会
    使程序效率极其低效;

    要做好大文件的发送,要考虑以下几个方面:
    1, 发送缓存的选取
    2, 文件的读取,可以考虑使用内存文件,如果能够一次性读取的话,就不要读那么多次,总之就是要减少读取的次数
    *  由于Tcp/ip协议能够自己进行流量控制,所以即使你在发送的时候一次将整个文件一次发送,也不会有问题
    *  send的时候没必要每次只发送一点点数据,太影响效率了,一个文件要发送很久才能传完
  5. 楼上的,
    可以在每次循环的时候Sleep(10)一下,这样基本就没有问题
    //Sleep(1000)该出问题还是会出问题,但通常加上Sleep(1),这样为防止CPU 100%,而不是防出错

    我搞不明白,把文件拆开(只要不拆成1bit),有什么不妥 1040-10240之间都可以运行的很好
    发整个文件,一个文件4M的话,就是4M内存,机器只运行你一个程序?
  6. 楼上的,你没明白我的意思
    姑且不论Sleep(1)是为了防止CPU 100%,还是防出错,这无关紧要,
    一个合理高效的服务器是不会这样发送数据的;

    大文件100%是要拆的,毫无疑问
    一般的小文件,又能占多少内存呢!为什么不能一次读完?
    我想说的是,每次"尽量"多从文件读些数据进来,避免频繁读写磁盘,象这样的代码是没有任何问题的:
    int ret = fread( bug, sizeof(char) , 65536, pFile);
    int nsend = send(mSock,(char *)bug, ret, 0);
  7. 完成端口不存在这个问题是因为操作系统内部对待发送的数据进行了缓存,也就是说如果TCP的发送缓冲区已满则,操作系统会将你要发送的数据加入待发送队列当检测到发送缓存区中有空闲的时候在进行发送。这也就是为什么在进行完成端口的发送时数据往往需要从堆上分配而不是栈上分配的具体原因。所以单纯从吞吐量的角度来看完成端口不一定比 select + 非阻塞套接字的方式更高。

我认为第三个说的不错,非阻塞模式的socket在send前一定要检查socket的当前状态是否为可send状态,因为自己是初学网络编程,对于select模型的使用还不太熟练,所以不会使用他说的用select来检测套接字缓冲区是否已满。最后不得以换成了开线程的方法来实现。其次第五个说使用sleep这种方法我试了,不行。还是会出现同样的错误代码。

原文地址:https://www.cnblogs.com/lit10050528/p/3700395.html