一、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;
}