一、客户端的实现
(1)需要一个发包的函数——udp_client_fun_packetloss
函数实现:
int udp_client_fun_packetloss(int times, SOCKET s, struct sockaddr* server, int serverlen)
{
int iResult = 0;
int i = 0;
char sendline[DEFAULT_BUFLEN];//应用程序的发送缓冲区
memset(sendline, 1, DEFAULT_BUFLEN);//设置测试数据报的内容
printf("\r\n客户端发送 %d 个数据报\n", times);
while (i < times)//根据用户输入发送的数据报个数循环发送相同数量的数据报
{
iResult = sendto(s, sendline, strlen(sendline), 0, server, serverlen);//发送!
if (iResult == SOCKET_ERROR)
{
printf("sendto failed with error: %d\n", WSAGetLastError());
return -1;
}
i++;
}
return iResult;
};
/*这里对该函数中每个参数进行讲解:
times是指一次发多少个包;
s:这是发送数据的套接字的文件描述符;
server:这是指向服务器地址的指针,类型通常是 struct sockaddr
。这个结构包含了服务器的网络地址信息,比如IP地址和端口号;
serverlen:这是服务器地址结构的大小;
*/
(2)计算丢包率的函数——udp_client_fun_reclve
函数实现:
int udp_client_fun_reclve(int times, SOCKET s, struct sockaddr* server, socklen_t* serverlen) {
char recvline[DEFAULT_BUFLEN]; // 应用程序接收缓冲区长度
int iResult = 0;
iResult = recvfrom(s, recvline, DEFAULT_BUFLEN, 0, server, serverlen);
if (iResult > 0) {
// 确保接收到的数据以 null 结尾
recvline[iResult] = '\0';
printf("Received data: %d\n", recvline[0]);//该数组的第一个元素便是服务端传回接收到的数据报个数
int a = recvline[0];//用整数a来存储字符型的数据
double m =(1- (double)a/(double)times)*100;//计算丢包率
printf("丢包率 =%.2f%%", m);
}
else if (iResult == 0) {
printf("Connection closing...\n");
}
else {
perror("recvfrom");
return -1;
}
return iResult;
}
(3)客户端的主体代码:
int main(int argc, char* argv[])//第一个参数为ipv4地址,第二个参数为发送数据报数目
{
WSADATA wsaData;
SOCKET ClientSocket = INVALID_SOCKET;//客户端套接字
sockaddr_in servaddr;//存放客户端地址信息
int times;//发送次数
int iResult;
//验证参数合法性
if (argc != 3)
{
printf("usage: <IPaddress> <times>\n");
return -1;
}
//初始化套接字
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
//声明IPV4地址簇、数据报套接字、UDP协议
servaddr.sin_family = AF_INET;//ipv4地址族
servaddr.sin_addr.s_addr = inet_addr(argv[1]);//s_addr 是 in_addr 结构体中的一个无符号整数,表示网络字节序的 IP 地址
//inet_addr 函数用于将点分十进制的 IP 地址字符串转换为网络字节序的无符号整数
servaddr.sin_port = htons(atoi(DEFAULT_PORT));//htons 函数用于将主机字节序的端口号转换为网络字节序
//atoi 函数用于将字符串转换为整数
//创建数据报套接字
ClientSocket = socket(AF_INET, SOCK_DGRAM, 0);//ipv4地址族,UDP协议
if (ClientSocket == INVALID_SOCKET)
{
printf("socket failed with error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
//循环发送缓冲区中的测试数据
for (int q = 0; q < 10;q++)
{
times = atoi(argv[2]);//将第二个命令参数类型字符串转化为整型
iResult = udp_client_fun_packetloss(times, ClientSocket, (struct sockaddr*)&servaddr, sizeof(servaddr));//测试启动!
if (iResult == -1)
{
printf("test failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
//测试结束,释放空间
printf("Packets sent: %d\n", times);
struct sockaddr_in cliaddr;//sockaddr_in结构体,存放客户端地址与端口号
int serverlen = sizeof(sockaddr_in);
iResult = udp_client_fun_reclve(times, ClientSocket, (struct sockaddr*)&servaddr, &serverlen);
}
closesocket(ClientSocket);
WSACleanup();
return 0;
}
(4)客户端的完整代码:
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#pragma comment (lib,"Ws2_32.lib")
#pragma comment (lib,"Mswsock.lib")
#pragma comment (lib,"AdvApi32.lib")
#define DEFAULT_BUFLEN 2000
#define DEFAULT_PORT "3000"
int udp_client_fun_packetloss(int times, SOCKET s, struct sockaddr* server, int serverlen)
{
int iResult = 0;
int i = 0;
char sendline[DEFAULT_BUFLEN];//应用程序的发送缓冲区
memset(sendline, 1, DEFAULT_BUFLEN);//设置测试数据报的内容
printf("\r\n客户端发送 %d 个数据报\n", times);
while (i < times)//根据用户输入发送的数据报个数循环发送相同数量的数据报
{
iResult = sendto(s, sendline, strlen(sendline), 0, server, serverlen);//发送!
if (iResult == SOCKET_ERROR)
{
printf("sendto failed with error: %d\n", WSAGetLastError());
return -1;
}
i++;
}
return iResult;
};
int udp_client_fun_reclve(int times, SOCKET s, struct sockaddr* server, socklen_t* serverlen) {
char recvline[DEFAULT_BUFLEN]; // 应用程序接收缓冲区长度
int iResult = 0;
int a;
iResult = recvfrom(s, recvline, DEFAULT_BUFLEN, 0, server, serverlen);
if (iResult > 0) {
// 确保接收到的数据以 null 结尾
recvline[iResult] = '\0';
printf("Received data: %s\n", recvline);
printf("Received data: %d\n", recvline[0]);
a = recvline[0];
double m =(1- (double)a/(double)times)*100;
printf("丢包率 =%.2f%%", m);
}
else if (iResult == 0) {
printf("Connection closing...\n");
}
else {
perror("recvfrom");
return -1;
}
return iResult;
}
int main(int argc, char* argv[])//第一个参数为ipv4地址,第二个参数为发送数据报数目
{
WSADATA wsaData;
SOCKET ClientSocket = INVALID_SOCKET;//客户端套接字
sockaddr_in servaddr;//存放客户端地址信息
int times;//发送次数
int iResult;
//验证参数合法性
if (argc != 3)
{
printf("usage: <IPaddress> <times>\n");
return -1;
}
//初始化套接字
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
//声明IPV4地址簇、数据报套接字、UDP协议
servaddr.sin_family = AF_INET;//ipv4地址族
servaddr.sin_addr.s_addr = inet_addr(argv[1]);//s_addr 是 in_addr 结构体中的一个无符号整数,表示网络字节序的 IP 地址
//inet_addr 函数用于将点分十进制的 IP 地址字符串转换为网络字节序的无符号整数
servaddr.sin_port = htons(atoi(DEFAULT_PORT));//htons 函数用于将主机字节序的端口号转换为网络字节序
//atoi 函数用于将字符串转换为整数
//创建数据报套接字
ClientSocket = socket(AF_INET, SOCK_DGRAM, 0);//ipv4地址族,UDP协议
if (ClientSocket == INVALID_SOCKET)
{
printf("socket failed with error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
//循环发送缓冲区中的测试数据
for (int q = 0; q < 10;q++)
{
times = atoi(argv[2]);//将第二个命令参数类型字符串转化为整型
iResult = udp_client_fun_packetloss(times, ClientSocket, (struct sockaddr*)&servaddr, sizeof(servaddr));//测试启动!
if (iResult == -1)
{
printf("test failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
//测试结束,释放空间
printf("Packets sent: %d\n", times);
struct sockaddr_in cliaddr;//sockaddr_in结构体,存放客户端地址与端口号
int serverlen = sizeof(sockaddr_in);
iResult = udp_client_fun_reclve(times, ClientSocket, (struct sockaddr*)&servaddr, &serverlen);
}
closesocket(ClientSocket);
WSACleanup();
return 0;
}
二、服务端的实现
(1)需要一个统计接收到多少个包的函数并将数据传回给客户端——udp_server_fun_packetloss
int udp_server_fun_packetloss(SOCKET s)
{
int iResult = 0;
int count = 0;//接收计数
struct sockaddr_in clint_addr;//sockaddr_in结构体,存放客户端地址与端口号
int addrlen = sizeof(sockaddr_in);//结构体长度
char recvline[DEFAULT_BUFLEN];//应用程序接收缓冲区长度
do//循环接收
{
memset(recvline, 0, DEFAULT_BUFLEN);//清除缓冲区
//接收数据
iResult = recvfrom(s, recvline, DEFAULT_BUFLEN, 0, (SOCKADDR*)&clint_addr, &addrlen);//接收来自客户端的数据报
if (iResult > 0)
{//接收成功
count++;//计数器+1
}
else
{//接收失败
printf("recvfrom failed with error: %d\n", WSAGetLastError());//返回错误码
}
} while (iResult > 0);
if (count >= 0)
{
printf("服务器总共收到 %d 个数据报\n", count);//打印结果
}
if (count > 0)
{
char buffer[sizeof(count)];
memcpy(buffer, &count, sizeof(count));
iResult = sendto(s, buffer, sizeof(buffer), 0, (struct sockaddr*)&clint_addr, sizeof(clint_addr));
}
return iResult;
};
(2)服务端的主体代码:
int main(int argc, char* argv[])//argv[1]:设置的缓冲区大小
{
if (argc != 2)//检测传参是否正确
{
printf("usage: <bufferlen>\n");
}
WSADATA wsaData;
int iResult;
SOCKET ServerSocket = INVALID_SOCKET;//服务器套接字
struct addrinfo* result = NULL;//用于存放getaddrinfo解析出的服务器的地址信息
struct addrinfo hints;//用于限制getaddrinfo函数得到的结果
int iSendResult;
int recvbuflen;//应用程序缓冲区
int len = sizeof(recvbuflen);//存储缓冲区长度的字节长度
//初始化WinSock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
//声明限制为:IPV4地址簇、数据报套接字、UDP协议
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;
//解析服务器地址和端口号
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);//应用hints的限制,结果传给result
if (iResult != 0)
{
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
//为无连接的服务器创建套接字
ServerSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);//应用getaddrinfo函数得到的result
if (ServerSocket == INVALID_SOCKET)
{
printf("socket failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
//为套接字绑定地址和端口号
iResult = bind(ServerSocket, result->ai_addr, (int)result->ai_addrlen);//将套接字与result的地址信息绑定
if (iResult == SOCKET_ERROR)
{
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ServerSocket);
return 1;
}
freeaddrinfo(result);//result不再被使用,可以释放掉
//查询服务器套接字的系统默认接收缓冲区大小
if (getsockopt(ServerSocket, SOL_SOCKET, SO_RCVBUF, (char*)&recvbuflen, &len) < 0)//查询服务器套接字的接收缓冲区大小
//并将这个值存储在 recvbuflen 变量中
{
printf("getsockopt BUFLEN failed with error: %d\n", WSAGetLastError());
return -1;
}
printf("系统默认接收缓冲区大小为:%d\n", recvbuflen);
//获得用户输入的系统缓冲区大小并设置
recvbuflen = atoi(argv[1]);//将命令参数行上第一个参数转换为一个整数(表示设置的缓冲区大小)
//并将其赋值给 recvbuflen 变量
if (setsockopt(ServerSocket, SOL_SOCKET, SO_RCVBUF, (const char*)&recvbuflen, len) < 0)//把recvbuflen的值设置为缓冲区大小
{
printf("setsockopt BUFLEN failed with error: %d\n", WSAGetLastError());
return -1;
}
printf("系统缓冲区大小被设置为:%d\n", recvbuflen);
//设置套接字的接收超时时间
int nTimeover = TIMEOVER;
if (setsockopt(ServerSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&nTimeover, sizeof(nTimeover)) < 0)//设置接收的超时时间
{
printf("setsockopt TIMEOVER failed with error: %d\n", WSAGetLastError());
return -1;
}
printf("系统接收超时时限被设置为:%d ms\n", nTimeover);
printf("UDP server starting...\n");
for (;;)//循环接收并统计收到的数据报数量
{
iResult = udp_server_fun_packetloss(ServerSocket);//接收并统计
if (iResult == -1)
{
printf("udp_server_fun_packetloss failed with error: %d\n", WSAGetLastError());
}
else if (iResult == 0)
{
printf("未接收到数据报。\n");
}
}
//关闭套接字,释放资源
closesocket(ServerSocket);
WSACleanup();
return 0;
}
(3)服务端的完整代码:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#pragma comment (lib,"Ws2_32.lib")
#define DEFAULT_BUFLEN 1024
#define DEFAULT_PORT "3000"
#define TIMEOVER 10000 //10秒判超时
int udp_server_fun_packetloss(SOCKET s)
{
int iResult = 0;
int count = 0;//接收计数
struct sockaddr_in clint_addr;//sockaddr_in结构体,存放客户端地址与端口号
int addrlen = sizeof(sockaddr_in);//结构体长度
char recvline[DEFAULT_BUFLEN];//应用程序接收缓冲区长度
do//循环接收
{
memset(recvline, 0, DEFAULT_BUFLEN);//清除缓冲区
//接收数据
iResult = recvfrom(s, recvline, DEFAULT_BUFLEN, 0, (SOCKADDR*)&clint_addr, &addrlen);//接收来自客户端的数据报
if (iResult > 0)
{//接收成功
count++;//计数器+1
}
else
{//接收失败
printf("recvfrom failed with error: %d\n", WSAGetLastError());//返回错误码
}
} while (iResult > 0);
if (count >= 0)
{
printf("服务器总共收到 %d 个数据报\n", count);//打印结果
}
if (count > 0)
{
char buffer[sizeof(count)];
memcpy(buffer, &count, sizeof(count));
iResult = sendto(s, buffer, sizeof(buffer), 0, (struct sockaddr*)&clint_addr, sizeof(clint_addr));
}
return iResult;
};
int main(int argc, char* argv[])//argv[1]:设置的缓冲区大小
{
if (argc != 2)//检测传参是否正确
{
printf("usage: <bufferlen>\n");
}
WSADATA wsaData;
int iResult;
SOCKET ServerSocket = INVALID_SOCKET;//服务器套接字
struct addrinfo* result = NULL;//用于存放getaddrinfo解析出的服务器的地址信息
struct addrinfo hints;//用于限制getaddrinfo函数得到的结果
int iSendResult;
int recvbuflen;//应用程序缓冲区
int len = sizeof(recvbuflen);//存储缓冲区长度的字节长度
//初始化WinSock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
//声明限制为:IPV4地址簇、数据报套接字、UDP协议
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;
//解析服务器地址和端口号
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);//应用hints的限制,结果传给result
if (iResult != 0)
{
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
//为无连接的服务器创建套接字
ServerSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);//应用getaddrinfo函数得到的result
if (ServerSocket == INVALID_SOCKET)
{
printf("socket failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
//为套接字绑定地址和端口号
iResult = bind(ServerSocket, result->ai_addr, (int)result->ai_addrlen);//将套接字与result的地址信息绑定
if (iResult == SOCKET_ERROR)
{
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ServerSocket);
return 1;
}
freeaddrinfo(result);//result不再被使用,可以释放掉
//查询服务器套接字的系统默认接收缓冲区大小
if (getsockopt(ServerSocket, SOL_SOCKET, SO_RCVBUF, (char*)&recvbuflen, &len) < 0)//查询服务器套接字的接收缓冲区大小
//并将这个值存储在 recvbuflen 变量中
{
printf("getsockopt BUFLEN failed with error: %d\n", WSAGetLastError());
return -1;
}
printf("系统默认接收缓冲区大小为:%d\n", recvbuflen);
//获得用户输入的系统缓冲区大小并设置
recvbuflen = atoi(argv[1]);//将命令参数行上第一个参数转换为一个整数(表示设置的缓冲区大小)
//并将其赋值给 recvbuflen 变量
if (setsockopt(ServerSocket, SOL_SOCKET, SO_RCVBUF, (const char*)&recvbuflen, len) < 0)//把recvbuflen的值设置为缓冲区大小
{
printf("setsockopt BUFLEN failed with error: %d\n", WSAGetLastError());
return -1;
}
printf("系统缓冲区大小被设置为:%d\n", recvbuflen);
//设置套接字的接收超时时间
int nTimeover = TIMEOVER;
if (setsockopt(ServerSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&nTimeover, sizeof(nTimeover)) < 0)//设置接收的超时时间
{
printf("setsockopt TIMEOVER failed with error: %d\n", WSAGetLastError());
return -1;
}
printf("系统接收超时时限被设置为:%d ms\n", nTimeover);
printf("UDP server starting...\n");
for (;;)//循环接收并统计收到的数据报数量
{
iResult = udp_server_fun_packetloss(ServerSocket);//接收并统计
if (iResult == -1)
{
printf("udp_server_fun_packetloss failed with error: %d\n", WSAGetLastError());
}
else if (iResult == 0)
{
printf("未接收到数据报。\n");
}
}
//关闭套接字,释放资源
closesocket(ServerSocket);
WSACleanup();
return 0;
}
三、运行结果
附:
参数的添加: