send函数和recv函数

一、send函数和recv函数的基本用法

send和recv是C语言中的函数,分别用于向一个已连接的socket发送和接收数据。

1.send()

	int send(
		SOCKET     s,		//一个用于标识已连接套接口的描述字
		const char *buf,	//指向包含要传输数据的缓冲区的指针
		int        len,		//buf参数指向缓冲区中的数据的长度,以字节为单位
		int        flags	//指定调用的方式,该参数一般填0
	);

返回值:

返回值 > 0,send返回发送的字节总数,它可能小于len参数中请求发送的字节数;

返回值 = 0,连接已优雅的关闭;

返回值 < 0,发生错误,返回值为SOCKET_ERROR值,可以通过调用WSAGetLastError检索特定的错误代码。

https://docs.microsoft.com/zh-cn/windows/desktop/api/winsock2/nf-winsock2-send

2.recv()

	int recv(
		SOCKET s,			//一个用于标识已连接套接口的描述字
		char   *buf,		//指向接收传入数据的缓冲区的指针
		int    len,			//buf参数指向的缓冲区的长度(以字节为单位)
		int    flags		//指定调用的方式,该参数一般填0
	);

返回值:

返回值 > 0,recv返回接收到的字节数;

返回值 = 0,连接已优雅的关闭;

返回值 < 0,发生错误,返回值为SOCKET_ERROR值,可以通过调用WSAGetLastError检索特定的错误代码。

https://docs.microsoft.com/zh-cn/windows/desktop/api/winsock/nf-winsock-recv

 

二、send函数和recv函数的包装

1.包装send函数

由于send函数返回的字节总数可能小于len参数中请求发送的字节数,也无法预计一次传输会发送多少数据。所以要对send函数进行包装后再使用。包装思路如下:

1)确保实际发送的字节数等于要发送的字节数。即必须在发送缓冲区中收集到要发送的全部数据才返回成功。

2)对数据进行切块,每次至多发送32KB。即小于32KB的数据一次发送,大于32KB的数据每次发送32KB,循环n个32KB后发送剩余数据。

结果如下:

	BOOL Send(SOCKET sock, char* buffer, int length)
	{
		int once;			//一次发送的返回值
		int total = 0;		//目前发送了多少数据
		int thislen = 0;	//本次要发送的数据长度

		do
		{
			if (length - total > 32 * 1024)		//判断剩余数据是否大于32KB,如果大于32KB
				thislen = 32 * 1024;			//则本次发送32KB,完成数据切块
			else
				thislen = length - total;		//如果小于等于32KB,则一次将剩余数据发完
			once = send(sock, buffer + total, thislen, 0);
			if (once <= 0)						//如果once < 0则表示发送失败,用break跳出循环,返回FALSE
			{
				break;
			}
			total += once;
		} while (total < length);				//如果total < length说明数据还未完全发送,继续循环

		if (total == length)					//当total == length时,说明已完全发送,返回TRUE
			return TRUE;
		return FALSE;
	}

2.包装recv函数

首先,recv函数与send函数有相似的问题,即一次接受未必能够收全数据。需要循环接受数据,再进行判断,看已接收到的数据是否等同于需要接受的数据。其次,recv函数还存在一个send函数没有的问题。

以由用户在客户端输入字符串再由客户端向服务端发送数据为例。在客户端可以使用如strlen函数计算需要发送的数据长度,在send时判断是否发送完全。而单次的收发数据中,接收端使用recv函数时无法提前得知接收多少数据才是完整的,所以无法保证接收到了完整的数据。

要解决这个问题,可以采用分两次发送接收的方式。发送端先发送一个数据包头,包头中携带的信息为接下来实际数据的长度。这样在接下来调用recv函数时,就可以在len参数中填入实际的数据长度了。而包头的长度可以事先约定好,如长度固定为4字节。

结果如下:

	BOOL Recv(SOCKET sock, char* buffer, int length)
	{
		int once = 0;
		int total = 0;
		BOOL r = FALSE;

		do
		{
			once = recv(sock, buffer + total, length - total, 0);
			if (once <= 0)
			{
				printf("error.");
				break;
			}
			total += once;
		} while (total < length);
		if (total == length)
		{
			r = TRUE;
		}
		return r;
	}

发送数据包头时将length参数设为4字节即可。

 

三、包装一次完整的发送和接收

通过上面包装好的函数,在一次完整的发送接收过程中,需要调用两次Send函数和两次Recv函数。在确定数据包头和实际数据的关系后,可以对该函数进一步包装,以达到使代码逻辑更清晰的效果。

以下为进一步包装的发送接收函数:

1.发送函数

BOOL Send(SOCKET sock, char* buffer, int length)
{
	int once;
	int total = 0;
	int thislen = 0;

	do
	{
		if (length - total > 32 * 1024)
			thislen = 32 * 1024;
		else
			thislen = length - total;
		once = send(sock, buffer + total, thislen, 0);
		if (once <= 0)
		{
			break;
		}
		total += once;
	} while (total < length);

	if (total == length)
		return TRUE;
	return FALSE;
}

BOOL SendPackage(SOCKET sock, char* buffer, int length)
{
	if (Send(sock, (char*)&length, sizeof(length)) && Send(sock, buffer, length))
	{
		return TRUE;
	}
	return FALSE;
}
/*该函数中用到了强制类型转换,以及逻辑与运算。
程序会先完整执行&&之前的表达式,如果结果为假,
则不会执行后面的表达式,直接返回FALSE。*/

2.接收函数

BOOL Recv(SOCKET sock, char* buffer, int length)
{
	int once = 0;
	int total = 0;
	BOOL r = FALSE;

	do
	{
		once = recv(sock, buffer + total, length - total, 0);
		if (once <= 0)
		{
			printf("error.");
			break;
		}
		total += once;
	} while (total < length);
	if (total == length)
	{
		r = TRUE;
	}
	return r;
}

BOOL RecvPackage(SOCKET sock, char* buffer, int *length)
{
	int buflen = *length;		//注意*length既是输入参数又是输出参数,一般可以输入buffer的长度
	int packet_length;
	BOOL r = FALSE;

	*length = 0;
	if (Recv(sock, (char*)&packet_length, sizeof(packet_length)))	//先发一个4字节的数据包头
	{
		if (packet_length <= buflen)					//判断发送的数据长度是否大于缓冲区长度
		{
			if (Recv(sock, buffer, packet_length))		//接收实际数据
			{
				*length = packet_length;				//将实际的数据长度赋值给*length
				r = TRUE;
			}
		}
	}
	return r;
}