文章目录

  • 一、结构体的传送
  • 二、Socket缓冲区
    • 2.1 sendto函数工作原理
    • 2.2 recv函数工作原理
  • 三、UDP丢包
  • 四、UDP数据传输中出现的分包问题的解释
  • 五、图片传送
    • 5.1 服务器端
    • 5.2 客户端
    • 5.3 结果演示
    • 5.3 问题

一、结构体的传送

UDP编程时经常需要使用sendto()和recvfrom()两个函数,其中

  • recvfrom()函数原型:

    ssize_t recvfrom(int sockfd,void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen
    );
    

    一般来说第二个变量buf和第三个变量len是难点。buf是指针就可以了,并不一定是char*类型,但是如果你想直接发送和接收结构体类型的指针,往往会出错。事实证明,还是得使用char*作为buffer来接收数据。

    例子:

    struct distance{char sender;int num_of_dests;dv_element dve[6];
    };
    distance rcdv;
    distance dv;
    想要发送的时候,使用
    sendto(sock, (char*) &dv, sizeof (dv); , 0, (struct sockaddr *) &echoServAddr, sizeof (echoServAddr));
    想要接收的时候,使用
    recvfrom(sock, (char*) &rcdv, sizeof(struct distance) , 0,(struct sockaddr *) &fromAddr, &fromSize))
    
  • 这两个函数都把想要发送(接收)的结构体的地址强制转换成了char*型,没有出错,数据都很完整。若是不完整,可以考虑发送之前自己写一段代码把要发送的结构体里的内容存入char*型里,然后发送这个char*型变量,接收时,写一段代码把这个char*型变量读到结构体里.

借鉴:https://blog.csdn.net/golzygo/article/details/8214748

二、Socket缓冲区

  • 每一个socket在被创建之后,系统都会给它分配两个缓冲区,即输入缓冲区和输出缓冲区。

  • sendto函数并不是直接将数据传输到网络中,而是负责将数据写入输出缓冲区,数据从输出缓冲区发送到目标主机是由协议完成的。数据写入到输出缓冲区之后,sendto函数就可以返回了,数据是否发送出去,是否发送成功,何时到达目标主机,都不由它负责了,而是由协议负责。

  • recv函数也是一样的,它并不是直接从网络中获取数据,而是从输入缓冲区中读取数据。

  • 输入输出缓冲区,系统会为每个socket都单独分配,并且是在socket创建的时候自动生成的。一般来说,默认的输入输出缓冲区大小为8K。套接字关闭的时候,输出缓冲区的数据不会丢失,会由协议发送到另一方;而输入缓冲区的数据则会丢失。

2.1 sendto函数工作原理

  • send函数只负责将数据提交给协议层。

  • 当调用该函数时:

       1.send先比较待发送数据的长度len和套接字s的发送缓冲区的长度如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;2.如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据;如果是就等待协议把数据发送完,3.如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len;如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完,4.如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回SOCKET_ERROR)
    

2.2 recv函数工作原理

  • 当调用该函数时:

     recv先检查套接字s的接收缓冲区如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中.(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0 。对方优雅的关闭socket并不影响本地recv的正常接收数据;如果协议缓冲区内没有数据,recv返回0,指示对方关闭;如果协议缓冲区有数据,则返回对应数据(可能需要多次recv),在最后一次recv时,返回0,指示对方关闭。学习:https://blog.csdn.net/rankun1/article/details/50488989
    推荐: https://blog.csdn.net/u010270148/article/details/53605339?utm_source=blogxgwz12
    

三、UDP丢包

UDP主要丢包原因及具体问题分析:

  • 主要丢包原因
  1. 接收端处理时间过长导致丢包:调用recv方法接收端收到数据后,处理数据花了一些时间,处理完后再次调用recv方法,在这二次调用间隔里,发过来的包可能丢失。对于这种情况可以修改接收端,将包接收后存入一个缓冲区,然后迅速返回继续recv。

  2. 发送的包巨大丢包:虽然send方法会帮你做大包切割成小包发送的事情,但包太大也不行。例如超过50K的一个udp包,不切割直接通过send方法发送也会导致这个包丢失。这种情况需要切割成小包再逐个send。

  3. 发送的包较大,超过接受者缓存导致丢包:包超过mtu size数倍,几个大的udp包可能会超过接收者的缓冲,导致丢包。这种情况可以设置socket接收缓冲。以前遇到过这种问题,我把接收缓冲设置成64K就解决了。

    int nRecvBuf=32*1024;//设置为32K
    setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
    
  4. 发送的包频率太快:虽然每个包的大小都小于mtu size 但是频率太快,例如40多个mut size的包连续发送中间不sleep,也有可能导致丢包。这种情况也有时可以通过设置socket接收缓冲解决,但有时解决不了。所以在发送频率过快的时候还是考虑sleep一下吧。

  5. 局域网内不丢包,公网上丢包。这个问题我也是通过切割小包并sleep发送解决的。如果流量太大,这个办法也不灵了。总之udp丢包总是会有的,如果出现了用我的方法解决不了,还有这个几个方法: 要么减小流量,要么换tcp协议传输,要么做丢包重传的工作。

四、UDP数据传输中出现的分包问题的解释

如果我们定义的TCP和UDP包小于1452,1464,那么我们的包在IP层就不用分包了,这样传输过程中就避免了在IP层组包发生的错误。如果使用UDP协议,如果IP层组包发生错误,那么包就会被丢弃,UDP不保证可靠传输。但是TCP发生组包错误时,该包会被重传,保证可靠传输。所以,我们在用Socket编程时,包的大小设定不一定非要小于1400,UDP协议要求包小于64K,TCP没有限定 。

  • udp本身就是不可靠,乱序的。
    首先 sendto 发送端,当socket发送缓冲占满,会有阶段性丢包出现。
    其次 recvfrom 接收端 当socket接收缓冲满,由于TCP/IP协议栈是运行在内核态,recvfrom并不会在用户态被立即调用,而且由于TCP/IP协议栈的内部锁的存在,这个时候是线性执行,而不是并发执行,那么后续包会被丢弃,也就是出现你说的 抓包可以抓到,但是你的程序接收不到的现象,因为用户态无法及时响应,导致内核态的缓冲长时间处于高负载状态,结局就是,随着时间的推移丢包越来越多。
    简单的临时化解,可以采用的方法:
    [1] 增加接收端socket的接收缓冲大小例如到 510241024, 调用setsocketopl函数
    [2] 检测发送端sendto 的返回值,当出现SOCKET=ERROR,使用select 等SOCKET缓冲被清除

  • UDP与TCP一样,属于传输层协议,而链路层有个MTU(最大传输单元)

      因特网协议允许IP分片,这样就可以将数据包分成足够小的片段以通过那些最大传输单元小于该数据包原始大小的链路了。这一分片过程发生在网络层,传输层是 OSI 模型中最重要的一层,这里是根据窗口控制传输,而非MTU。传输协议同时进行流量控制或是基于接收方可接收数据的快慢程度规定适当的发送速率。除此之外,传输层按照网络能处理的最大尺寸将较长的数据包进行强制分割。例如,以太网无法接收大于1500字节的数据包。发送方节点的传输层将数据分割成较小的数据片,同时对每一数据片安排一序列号,以便数据到达接收方节点的传输层时,能以正确的顺序重组,该过程即被称为排序。它使用的是将分组发送到链路上的网络接口的最大传输单元的值。原始分组的分片都被加上了标记,这样目的主机的TCP层就能将分组重组成原始的数据包了。
  • 以太网的数据帧长度不能大于1500字节(以太网物理特性决定),这个1500字节也就是网络层(IP层)数据报文的最大长度。链路层 首部和尾部共有18个字节,数据区即为MTU往上看网络层,网络层有IP数据报文的头,占了20个字节,所以实际数据长度为1480个字节。这个1480长度的数据就是TCP或UDP传来的传来的数据。再往上看,由于在传输层UDP协议头占了8个字节,故实际的数据长度为1472个字节(此数据即为我们可存放封装的最大数据长度)故在局域网环境下,采用UDP协议传输数据,最大封装的数据长度为1472个字节。而Internet上标准的MTU值为576,故数据封装的最大长度576-20-8=548个字节,数据长度小于此值,则不容易出现分包情况。

五、图片传送

图片是二进制文件,通过文件流进行读取和发送:

5.1 服务器端

// server2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。#include <iostream>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#pragma warning(disable : 4996)
using namespace std;//创建套接字
SOCKET createSocket();
//判断文件是否存在
BOOL IsDirExist(char cdir[]);
//获取文件存储的根路径
char* piturePath(char name[]);
//打开图片
void openPicture(FILE*& p, char path[], SOCKET sSocket);struct Picture
{long length;char buffer[4096];int flag;//string success;
}picture;int main() {//创建服务器套接字SOCKET sSocket = createSocket();//创建结构地址变量sockaddr_in serveraddr;//服务器端地址sockaddr_in clientaddr;//客户端地址int slen = sizeof(serveraddr);int clen = sizeof(clientaddr);//设置服务器的地址serveraddr.sin_family = AF_INET;   //设置家族协议serveraddr.sin_port = htons(8999); //设置端口号serveraddr.sin_addr.S_un.S_addr = INADDR_ANY;//把地址绑定到服务器int ret = bind(sSocket, (SOCKADDR*)&serveraddr, slen); if (ret == SOCKET_ERROR){cout << "bind failed " << endl;closesocket(sSocket);WSACleanup();cout << "20s后退出控制台!" << endl;Sleep(20000);exit(0);}char data[200];                        //接收短数据的缓冲区memset(data, 0, sizeof(data));         //初始化缓冲区char begin[] = "好的,准备接受图片。"; //开始标志消息char end[] = "接收图片完成。";         //结束标志消息int iRcv;                              //接受状态int iSend;                             //发送状态FILE* p;                               //文件指针/* 发送的包较大,超过接受者缓存导致丢包:包超过mtu size数倍,几个大的udp包可能会超过接收者的缓冲,导致丢包。这种情况可以设置socket接收缓冲。 *///int nRecvBuf = 128 * 1024;//设置为128K//setsockopt(sSocket, SOL_SOCKET, SO_RCVBUF, (const char*)&nRecvBuf, sizeof(int));//循环接收图片while (1) {cout << "=================================================Server===============================================" << endl;//首先接收客户端的开始消息iRcv = recvfrom(sSocket, data, sizeof(data), 0, (SOCKADDR*)&clientaddr, &clen);if (iRcv == SOCKET_ERROR) {cout << "接受信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(2000);exit(0);}cout << "Client: " << data << endl;//发送开始回馈消息给客户端iSend = sendto(sSocket, begin, strlen(begin), 0, (SOCKADDR*)&clientaddr, clen);if (iSend == SOCKET_ERROR) {cout << "发送信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(2000);exit(0);}cout << "Server: " << begin << endl;//接收图片名字memset(data, 0, sizeof(data));iRcv = recvfrom(sSocket, data, sizeof(data), 0, (SOCKADDR*)&clientaddr, &clen);if (iRcv == SOCKET_ERROR) {cout << "接受信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(2000);exit(0);}//获取图片存放的根路径cout << "接收的图片名字: " << data << endl;//设置图像存放的目录,目录必须存在,输入的形式://  D:\\test\\  或者D:/test/char* path = piturePath(data);cout << "图像存放的目录:" << path << endl;memset(data, 0, sizeof(data));//打开一个存放图像数据的文件openPicture(p, path, sSocket);cout << "····接收中····" << endl;picture.flag = 0;//接收完成的标志//循环接收客服端发送的数据包while (!picture.flag) {memset(picture.buffer, 0, sizeof(picture.buffer));iRcv = recvfrom(sSocket, (char*)&picture, sizeof(struct Picture), 0, (SOCKADDR*)&clientaddr, &clen);if (iRcv == SOCKET_ERROR) {cout << "接受图片失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(20000000);return -8;}fwrite(picture.buffer, picture.length, 1, p);if (iRcv == 0) //客户端已经关闭连接{printf("Client has closed the connection\n");return 0;}//接受一次包就确认一次char success[] = "success";iSend = sendto(sSocket, success, strlen(success), 0, (SOCKADDR*)&clientaddr, clen);if (iSend == SOCKET_ERROR) {cout << "发送信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(2000);return -10;}memset(success, 0, sizeof(success));}cout << "····接收中····" << endl;cout << "····接收完成····" << endl;//关闭文件fclose(p);p = NULL;cout << endl;//接收客服端的信息——我的图片发送完成。iRcv = recvfrom(sSocket, data, sizeof(data), 0, (SOCKADDR*)&clientaddr, &clen);if (iRcv == SOCKET_ERROR) {cout << "接受信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(2000);return -9;}cout << "Client: " << data << endl;memset(data, 0, sizeof(data));//发送接收图片完成。iSend = sendto(sSocket, end, strlen(end), 0, (SOCKADDR*)&clientaddr, clen);if (iSend == SOCKET_ERROR) {cout << "发送信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(2000);return -10;}cout << "Server: " << end << endl;//接收客户端是拜拜还是继续发图片的标志iRcv = recvfrom(sSocket, data, sizeof(data), 0, (SOCKADDR*)&clientaddr, &clen);if (iRcv == SOCKET_ERROR) {cout << "接受信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(2000);return -11;}cout << "Client: " << data << endl;//若接收的信息等于拜拜,就关闭服务器连接if (!(strcmp(data, "byebye"))) {cout << "2m后关闭服务器连接!";Sleep(200000);break;}memset(data, 0, sizeof(data));cout << endl;cout << endl;}//关闭,清理closesocket(sSocket);WSACleanup();return 0;}
//创建socket
SOCKET createSocket() {WORD version = MAKEWORD(2, 2);WSADATA wsadata;if (WSAStartup(version, &wsadata)){cout << "WSAStartup failed " << endl;cout << "2s后控制台将会关闭!" << endl;Sleep(2000);exit(0);}//判断版本if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 2){cout << "wVersion not 2.2" << endl;cout << "2s后控制台将会关闭!" << endl;Sleep(2000);exit(0);}SOCKET sSocket;sSocket = socket(AF_INET, SOCK_DGRAM, 0);if (SOCKET_ERROR == sSocket){cout << "socket failed" << endl;cout << "2s后控制台将会关闭!" << endl;Sleep(2000);exit(0);}else {return sSocket;}
}
// 判断文件夹是否存在
BOOL IsDirExist(char cdir[])
{string dir(cdir);size_t origsize = dir.length() + 1;const size_t newsize = 100;size_t convertedChars = 0;wchar_t* wcstring = (wchar_t*)malloc(sizeof(wchar_t) * (dir.length() - 1));mbstowcs_s(&convertedChars, wcstring, origsize, dir.c_str(), _TRUNCATE);DWORD dwAttrib = GetFileAttributes(wcstring);return INVALID_FILE_ATTRIBUTES != dwAttrib && 0 != (dwAttrib & FILE_ATTRIBUTE_DIRECTORY);
}
//图片存储的路径
char* piturePath(char name[]) {char dir[50] = { 0 };//循环输入存放的目录,若电脑中不存在输入的目录,将要求重新输入while (true){cout << "请输入图片存放路径: " << endl;cin >> dir;if (IsDirExist(dir)) {break;}else {cout << "文件目录不存在!" << endl;}}int k = 0;int nlen = strlen(name);int dlen = strlen(dir);static char path[100];//存放指定的图像存放地址,此处变量必须是static型memset(path, 0, sizeof(path));//必须每一次进行初始化//将目录和文件名拼接在一起for (int i = 0; i < dlen; i++) {path[k++] = dir[i];}for (int j = 0; j < nlen; j++) {path[k++] = name[j];}//返回文件名return path;
}
//打开图片存放文件
void openPicture(FILE*& p, char path[], SOCKET sSocket) {// 以读 / 写方式打开或建立一个二进制文件,允许读和写。if (!(p = fopen(path, "wb+"))) {cout << "图片存放路径出错!" << endl;cout << "2s后退出控制台!" << endl;closesocket(sSocket);WSACleanup();Sleep(20000);exit(0);}cout << endl;}

5.2 客户端

// client2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//#include <iostream>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
#pragma warning(disable : 4996)//创建一个socket
SOCKET createSocket();
//打开图片
void openPicture(char data[], FILE*& p);
//用于获取图片的存储路径
char* getPictureName(char path[]);
bool show(int iSend, SOCKET client, char discon[], sockaddr_in saddr, int slen);
//定义一个结构体,用于图片的传递
struct Picture {int length;         //统计每次真正发送的数据量char buffer[4096];  //用于发送数据缓冲区int flag;           //标记图片是否发送完成
}picture;               //声明了一个结构体变量picture;int main(void)
{//创建客户端UDP套接字SOCKET client = createSocket();//创建地址结构体变量sockaddr_in saddr;sockaddr_in caddr;int slen = sizeof(saddr);int clen = sizeof(caddr);//设置服务器地址saddr.sin_family = AF_INET;saddr.sin_port = htons(8999);saddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");char data[100] = {0};                    //接受一些短字节的数据缓冲区char begin[] = "我要准备发图片了。";       //发送图片前的确认信息char end[] = "我的图片发送完成。";         //完成图片发送的通知信息char discon[] = "byebye";                  //关闭客户端的通知信息int iSend = 0;                             //发送函数的状态int iRecv = 0;                             //接收函数的状态FILE* p;                                   //创建一个文件指针static int count = 1;                      //用于统计发送图片的个数//循环向服务器发送图片while (1){cout << "************************************第" << count << "次传输图片*******************************" << endl;//发送图片前先和服务器打个招呼,欲准备状态,//判断信息发送是否成功,若不成功,则服务器处于关闭状态iSend = sendto(client, begin, strlen(begin), 0, (SOCKADDR*)&saddr, slen);if (iSend == SOCKET_ERROR) {cout << "服务器处于关闭状态,请稍后重试!" << endl;cout << "2m后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(20000);return -4;}cout << "Client: " << begin << endl;//接受服务器的确认信息,判断信息接收是否成功,接收到信息后开始发送,否则关闭链接iRecv = recvfrom(client, data, sizeof(data), 0, (SOCKADDR*)&saddr, &slen);if (iRecv == SOCKET_ERROR) {cout << "接受确认信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(2000);return -5;}cout << "Server: " << data << endl;memset(data, 0, sizeof(data));  //重新初始化data接收数据缓冲区//开始加载图片openPicture(data, p);//获取发送图片的名字char* name=getPictureName(data);cout << "要发送的图像地址:" << name << endl;//发送图片名字iSend = sendto(client, name, strlen(name), 0, (SOCKADDR*)&saddr, slen);if (iSend == SOCKET_ERROR){cout << "发送图片内容出错" << endl;cout << "2s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(2000);return -7;}memset(data, 0, sizeof(data));//计算图片的总长度fseek(p, 0, SEEK_END);  //指针移动到图片的最后一个字节;int length = ftell(p);  //获取图片总长度fseek(p, 0, SEEK_SET);  //指针还原到开始位置//分包发送图片,图片长度大于0,循环发送,否则发送完毕,停止发送cout << endl;cout << "····发送准备中····" << endl;//int i = 1;while (length > 0) {//cout << i << endl;//i++;//cout << "1";memset(picture.buffer, 0, sizeof(picture.buffer));     //初始化接受缓冲区//cout << "2";fread(picture.buffer, sizeof(picture.buffer), 1, p);   //读取图片到缓冲区int len = sizeof(picture.buffer);                      //获取读取的长度/*若读取的长度大于当前图片剩余总长度,将结构体的图片长度赋值为图片剩余长度,并标记图片读取结束;否则图片长度为读取缓冲区长度,图片标记状态为未完成 */if (length >= len) {//cout << "3";picture.flag = 0;picture.length = len;}else {//cout << "succ";picture.length = length;picture.flag = 1;}//发送图片的一部分,发送成功,则图片总长度减去当前发送的图片长度iSend = sendto(client, (char*)&picture, sizeof(struct Picture), 0, (SOCKADDR*)&saddr, slen);if (iSend == SOCKET_ERROR) {cout << "发送图片出错" << endl;cout << "2s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(2000);return -8;}else {length -= len;}//接受服务器的确认信息iRecv = recvfrom(client, data, sizeof(data), 0, (SOCKADDR*)&saddr, &slen);if (iRecv == SOCKET_ERROR) {cout << "接受确认信息失败!" << endl;cout << "10s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(10000);return -10;}//若收到确认消息为success则继续下一轮的传输;否则退出控制台if (strcmp(data, "success") != 0) {cout << "图片部分发送失败!" << endl;cout << "10s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(10000);return -10;}memset(data, 0, sizeof(data));}cout << "····发送中····" << endl;cout << "····发送完成····" << endl;cout << endl;//发送消息给服务器,告诉他已经发送完毕iSend = sendto(client, end, strlen(end), 0, (SOCKADDR*)&saddr, slen);if (iSend == SOCKET_ERROR){cout << "发送消息出错!" << endl;cout << "2s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(2000);return -9;}cout << "Client: " << end << endl;//接受服务器的接收完成的确认信息iRecv = recvfrom(client, data, sizeof(data), 0, (SOCKADDR*)&saddr, &slen);if (iRecv == SOCKET_ERROR) {cout << "接受确认信息失败!" << endl;cout << "2s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(2000);return -10;}cout << "Server: " << data << endl;memset(data, 0, sizeof(data));//是否继续发送图片bool f = show(iSend, client, discon, saddr, slen);if (f) {count++;}else{break;}fclose(p);p = NULL;cout << endl;cout << endl;}//关闭,清理closesocket(client);WSACleanup();return 0;
}SOCKET createSocket() {//声明调用不同的Winsock版本。WORD version = MAKEWORD(2, 2);//一种数据结构。这个结构被用来存储被WSAStartup函数调用后返回的Windows Sockets数据。WSADATA wsadata;/*WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数。它允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。*/if (WSAStartup(version, &wsadata)){cout << "WSAStartup failed " << endl;cout << "2s后控制台将会关闭!" << endl;Sleep(2000);exit(0);}//判断版本if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 2){cout << "wVersion not 2.2" << endl;cout << "2s后控制台将会关闭!" << endl;Sleep(2000);exit(0);}//创建客户端UDP套接字SOCKET client;client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);if (SOCKET_ERROR == client){cout << "socket failed" << endl;cout << "2s后控制台将会关闭!" << endl;Sleep(2000);exit(0);}else {return client;}
}//打开文件函数声明,特别注意是传递引用,不然后续操作会出错
void openPicture(char data[], FILE*& p) {while (1) {cout << "请输入要发送图片的路径: " << endl;cin >> data;  //输入图片的绝对路径// 以读 / 写方式打开一个二进制文件,只允许读 / 写数据。若图片无法打开,则路径有问题,关闭连接cout << "输入的图像名:" << data << endl;if (!(p = fopen(data, "rb+"))) {memset(data, 0, sizeof(data));cout << "图片路径出错,请重新尝试!" << endl;}else {break;}}
}//获取图片的名称,去掉父目录
char* getPictureName(char path[]) {static char name[20];memset(name, 0, sizeof(name));int len = strlen(path);int count = 0;//输入的目录形如:D:\\test\\1.jpg 或者D:/test/1.jpg     名称为1.jpg//从最后一个字母开始查找,查找第一个出现的"/"或者"\"for (int i = len-1; i > 0; i--) {if (path[i] != '\\'&& path[i]!='/') {count++;}else {break;}}int j = 0;int pos = len - count ;for (int i = pos; i < len; i++) {name[j++] = path[i];}cout << "name:" << name << endl;return name;
}bool show(int iSend, SOCKET client, char discon[], sockaddr_in saddr, int slen) {int j;cout << endl;cout << "请选择是否继续发送图片:(1或者2)" << endl;cout << "   1: YES   2: NO " << endl;cout << "我的选择: ";cin >> j;if (j == 2) {iSend = sendto(client, discon, strlen(discon), 0, (SOCKADDR*)&saddr, slen);if (iSend == SOCKET_ERROR){cout << "发送消息出错!" << endl;cout << "2s后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(2000);exit(0);}cout << "Clent: " << discon << endl;cout << "30s后关闭服务器连接!";Sleep(30000);return  FALSE;}else {iSend = sendto(client, "请接受下一张图片。", strlen("请接受下一张图片。"), 0, (SOCKADDR*)&saddr, slen);if (iSend == SOCKET_ERROR){cout << "发送消息出错!" << endl;cout << "2m后退出控制台!" << endl;closesocket(client);WSACleanup();Sleep(20000);exit(0);}cout << "Client: 请接受下一张图片。" << endl;return TRUE;}
}

5.3 结果演示

  1. 必须先开启服务器端程序,否则客户端自动退出。

  2. 客服端发送一张图片

    服务器端接收图片:

  3. 一张图像发送后,客户端给出操作选择:


  4. 客户端选择2:

5.3 问题

  1. 打开文件时的打开模式需要注意是二进制文件

  2. 接收数据缓冲区使用后要进行初始化,否则下次使用会出错。

  3. 接收缓存区,系统默认最大64k,若发送文件大于64看,要进行分包发送,否则接收方会丢包,而发送方不会对包。或者改变接收缓存区的大小。但由于每次数据大小不确定,这种做法不好。

    我采用的是确认后在继续发送的机制,服务器接收后,返给客户端一个确认消息,客户端在继续发送信息。(也是一点点摸索的)

  4. 打开文件函数需要传递的是指针的引用,否则会出错,生命周期在函数内,调用完成就会清空。

    在主函数中就不能再使用这个打开的文件。(这点困扰了我很久,才焕然大悟,太菜了。)(╥╯^╰╥))

    指针是用来指向某个变量,而引用是给变量取个别名,其作用就如同typedef一样。 用引用作形参时在调用函数里就像操作实参一样,不需要考虑实参的地址问题 用指针做形参时,由于指针的值是变量的地址,所以要通过对地址解引用来操作其所指的变量。

  5. 从string转换为LPCWSTR:
    在网上找了很久

    LPCWSTR stringToLPCWSTR(std::string orig)
    {size_t origsize = orig.length() + 1;const size_t newsize = 100;size_t convertedChars = 0;
    wchar_t *wcstring = (wchar_t *)malloc(sizeof(wchar_t)*(orig.length()-1));
    mbstowcs_s(&convertedChars, wcstring, origsize, orig.c_str(), _TRUNCATE);
    return wcstring;
    }char * 的话,可以先把char * 转为 std::string,就是用string的构造函数 string(char*)
    比如
    char * charArray = "abcd";
    std::string str(charArray);
    

推荐几个文章:
https://blog.csdn.net/yangkunqiankun/article/details/75808401
https://blog.csdn.net/linfenliang/article/details/39374765

丢包:
https://blog.csdn.net/libaineu2004/article/details/48039599
https://www.2cto.com/net/201311/254835.html

文件夹是否存在:
https://www.cnblogs.com/MakeView660/p/6101251.html
https://blog.csdn.net/u012494876/article/details/51204615

接受缓冲区问:
https://www.cnblogs.com/x_wukong/p/5761676.html

项目路径:UDP通信——客户端循环向服务器端发送图像.rar

基于vs实现的socket—udp图片传送实例详解相关推荐

  1. python画二维散点图-基于python 二维数组及画图的实例详解

    1.二维数组取值 注:不管是二维数组,还是一维数组,数组里的数据类型要一模一样,即若是数值型,全为数值型 #二维数组 import numpy as np list1=[[1.73,1.68,1.71 ...

  2. python字库转文字图片_对Python生成汉字字库文字,以及转换为文字图片的实例详解...

    对Python生成汉字字库文字,以及转换为文字图片的实例详解 发布时间:2020-10-05 17:20:03

  3. python画二维数组散点图_基于python二维数组及画图的实例详解

    基于python二维数组及画图的实例详解 下面小编就为大家分享一篇基于python 二维数组及画图的实例详解,具有很好的参考价值,希望对大家有所帮助.一起跟随小编过来看看吧 1.二维数组取值 注:不管 ...

  4. CSS图片廊实例详解

    效果: <!DOCTYPE html> <html> <head> <style> div.img {margin: 2px;border: 1px s ...

  5. php jcrop,PHP结合JQueryJcrop实现图片裁切实例详解

    我们经常可以看到一些网站上有图片剪切的功能,或许你会觉得这一功能炫目华丽,神秘莫测!但是今天介绍的一款专用于图片裁切的插件jquery.Jcrop.min.js就将揭开图片剪切的神秘面纱.使用这个插件 ...

  6. python 二维数组心得_基于python 二维数组及画图的实例详解

    python中如何使用二维数组 在Python中,一个像这样的多维表格可以通过"序列的序列"实现.一个表格是行的序列.每一行又是独立单元格的序列.这类似于小编们使用的数学记号,在数 ...

  7. tcp网络通信教程 java_基于java TCP网络通信的实例详解

    JAVA中设计网络编程模式的主要有TCP和UDP两种,TCP是属于即时通信,UDP是通过数据包来进行通信,UDP当中就会牵扯到数据的解析和传送.在安全性能方面,TCP要略胜一筹,通信过程中不容易出现数 ...

  8. c语言 recv_sin,C++_C语言中经socket接收数据的相关函数详解,recv()函数: 头文件:#incl - phpStudy...

    C语言中经socket接收数据的相关函数详解 recv()函数:头文件: #include #include 定义函数: int recv(int s, void *buf, int len, uns ...

  9. Socket 套接字原理详解

    Socket 套接字原理详解 socket 编程介绍 Socket编程封装了常见的TCP.UDP操作,可以实现非常方便的网络编程. socket() 函数介绍 # socket.socket(fami ...

最新文章

  1. 财务人员学python有用吗-python在财务里面有用吗
  2. python连接es数据库_Python Elasticsearch API操作ES集群
  3. Ubuntu下一个好用的终端
  4. 大脑构造图与功能解析_解析地轨、隐藏轨推拉门及折叠门的构造做法,收藏学习...
  5. 当代开发者的六大真实现状,你被哪一个场景“戳中”了?
  6. package.json的进阶
  7. 【AI面试题】回归问题常用的性能度量指标(评价指标)
  8. 【算法导论】贪心算法,递归算法,动态规划算法总结
  9. kibana集成内部账号_揭开 Elasticsearch 中身份验证和授权的神秘面纱
  10. 自己动手——实现台达PLC远程监控数据采集
  11. 配置JDK环境变量详细步骤
  12. PS将可见图层创建为一个新的图层,保留原来的图层,Photoshop 导出可见图层
  13. 第二周学习前端总结与感悟(一)
  14. 初创企业选择阿里云服务器与传统自建服务器的对比与选择
  15. html网页随机一言,PHP简单实现一言 / 随机语录功能
  16. 关于日历点击跳转以及短信点击跳转到指定联系人界面的一些记录
  17. python 处理.xlsx文件
  18. 手机网上订货下单软件|移讯云订货通企业订货管理系统介绍
  19. 处女作,,QT+arduino的物联网小项目
  20. MySQL修改用户密码及配置远程访问

热门文章

  1. 数据库课程设计矿大_管理信息系统课程设计样本.pdf
  2. JUnit:求求你了,别再用 main 方法测试了,好吗?
  3. GMM-HMM孤立词识别
  4. Flash Switcher(二) - 极致书签管理
  5. android之图片选择器ImageSelector的使用
  6. struts2标签的使用(一)
  7. 【第93期】谁是元宇宙的“基础设施”?
  8. 2019杭电多校第七场 HDU - 6656 Kejin Player 期望
  9. 解决印象笔记无法搜索到对应标题或内容笔记的问题
  10. ubuntu 下蓝牙无法连接