socket-vs-udp-图片传送

结构体的传送

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。套接字关闭的时候,输出缓冲区的数据不会丢失,会由协议发送到另一方;而输入缓冲区的数据则会丢失。

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) 
    

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。

    扫描二维码关注公众号,回复: 3891252 查看本文章
  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个字节,数据长度小于此值,则不容易出现分包情况。

图片传送

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

// server2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include"pch.h"
#include <iostream>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

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 << "2s后退出控制台!" << endl;
		Sleep(2000);
		exit(0);
	}


	char data[200];                        //接收短数据的缓冲区
	memset(data, 0, sizeof(data));         //初始化缓冲区
	//char dir[50] = { 0 };                  //存放目录的缓冲区
	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;
		memset(data, 0, sizeof(data));

		//发送回馈消息给客户端
		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;

		//接收图片名字
		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;
		char *path = piturePath(data);
		cout << "path= " << path << endl;
		memset(data, 0, sizeof(data));

		//打开图片
		openPicture(p, path, sSocket);

		cout << "····接收中····" << endl;
		//int count = 1;
		picture.flag = 0;

		while (!picture.flag) {
			//cout << count << endl;
			//count++;
			//cout << "1";
			memset(picture.buffer, 0, sizeof(picture.buffer));
			//cout << "2";
			iRcv = recvfrom(sSocket, (char*)&picture, sizeof(struct Picture), 0, (SOCKADDR*)&clientaddr, &clen);
			//cout << "iRcv==" << iRcv << endl;
			if (iRcv == SOCKET_ERROR) {
				cout << "接受图片失败!" << endl;
				cout << "2s后退出控制台!" << endl;
				closesocket(sSocket);
				WSACleanup();
				Sleep(2000);
				return -8;
			}
			if (iRcv == 0) //客户端已经关闭连接
			{
				printf("Client has closed the connection\n");
				return 0;
			}

			fwrite(picture.buffer, picture.length, 1, p);
			//cout << "4" << endl;

			//接受一次包就确认一次
			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;
			}
			//cout << "Server: " <<success << endl;
			memset(success, 0, sizeof(success));

			//fseek(p, 0, 2);
			//cout << "内容" << picture.buffer << endl;
		}
		cout << "····接收中····" << endl;
		cout << "····接收完成····" << endl;
		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;
		//cout << strcmp(data, "byebye") << endl;
		if (!(strcmp(data, "byebye"))) {
			cout << "2m后关闭服务器连接!";
			Sleep(200000);
			break;
		}
		memset(data, 0, sizeof(data));
		fclose(p);
		p = NULL;
		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);
	char path[100] = { 0 };
	for (int i = 0; i < dlen; i++) {
		path[k] = dir[i];
		k += 1;
	}
	for (int j = 0; j < nlen; j++) {
		path[k] = name[j];
		k += 1;
	}
	return (char*)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 << "存放图片的绝对路径: " << path << endl;
	cout << endl;

}

// client2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

using namespace std;

//创建一个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;           //标记图片是否发送完成
	//int success;      
}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" << 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;  //输入图片的绝对路径
		// 以读 / 写方式打开一个二进制文件,只允许读 / 写数据。若图片无法打开,则路径有问题,关闭连接
		if (!(p = fopen(data, "rb+"))) {
			memset(data, 0, sizeof(data));
			cout << "图片路径出错,请重新尝试!" << endl;
		}
		else {
			break;
		}
	}
}

char* getPictureName(char path[]) {
	char name[20] = { 0 };
	int len = strlen(path);
	int count = 0;
	for (int i = len; i > 0; i--) {
		if (path[i] != '\\') {
			count++;
		}
		else {
			break;
		}
	}
	int j = 0;
	int pos = len - count + 1;
	for (int i = pos; i < len; i++) {
		name[j++] = path[i];
	}
	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;
	}
}

结果:

在这里插入图片描述

在这里插入图片描述

问题

  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);
    
```c
推荐几个文章: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

猜你喜欢

转载自blog.csdn.net/qq_41498261/article/details/83476319