C语言:Winsock网络编程—ping命令的简单实现

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/qq_29542611/article/details/84452298

Winsock网络编程—ping命令的简单实现

前言

先声明 博主实现的是Windows平台的ping命令的简单实现,没有做域名解析,只能直接ping ip。我们要实现ping 肯定得先知道ping的实现原理,ping 发送的 ICMP报文。实际上的落脚点 就是对 ICMP协议和IP协议 结构的学习 以及 如何使用Winsock API 来实现ICMP报文的组包和解包。需要使用wireshark 抓包软件 配合学习,这样可以验证你分析的对不对。

网络协议基础知识

ip协议结构图:

  1. ping 中有显示 TTL 值,这个就是从ip头部中取得。
  2. ip头部的长度是从 IHL 中的值。
    在这里插入图片描述

icmp协议结构图:

在这里插入图片描述

类型和代码字段 所有情况如下,我们主要用的是 请求回显 和 回显应答
在这里插入图片描述

字节序问题

只有当 数据类型长度超过1个字节是 才会出现字节序问题。

网络传输的字节序和本地存储的字节序 有可能是一样 也有可能不一样。

网络字节序 是大端对齐模式(低地址 放高字节,高地址 放低字节)。

本地字节序 就要分CPU架构了,一般小型计算机 都是小端对齐模式(低地址 放低字节,高地址 放高字节)。

Winsock api 函数

完成 icmp报文的发送和接受 使用的api 函数有:

  1. setsockopt

https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-setsockopt

int WSAAPI setsockopt(
SOCKET s,
int level, // 选项级别
int optname, // 选项名
const char *optval, // 选项值
int optlen // 选项的长度
);

  1. sendto

https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-sendto

int WSAAPI sendto(
SOCKET s,
const char *buf,
int len,
int flags, // 调用模式flag
const sockaddr *to,
int tolen
);

  1. recvfrom

https://docs.microsoft.com/en-us/windows/desktop/api/winsock/nf-winsock-recvfrom

int recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from, // 写出参数,为源地址
int *fromlen
);

  1. u_short ntohs(u_short hostshort); 网络字节序 转本地字节序

实现效果

先看下效果 再说
在这里插入图片描述

和 wireshark 抓包数据对比
在这里插入图片描述

实现代码

一次ICMP 报文请求的核心代码

int ping(char *szDestIp)
{
	//printf("destIp = %s\n",szDestIp);
    int bRet = 1;
    
    WSADATA wsaData;
    int nTimeOut = 1000;//1s  
    char szBuff[ICMP_HEADER_SIZE + 32] = { 0 };
    icmp_header *pIcmp = (icmp_header *)szBuff;
    char icmp_data[32] = { 0 };

    WSAStartup(MAKEWORD(2, 2), &wsaData);
    // 创建原始套接字
    SOCKET s = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
    
    // 设置接收超时
    setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char const*)&nTimeOut, sizeof(nTimeOut));

    // 设置目的地址
    sockaddr_in dest_addr;
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_addr.S_un.S_addr = inet_addr(szDestIp);
    dest_addr.sin_port = htons(0);

    // 构造ICMP封包
    pIcmp->icmp_type = ICMP_ECHO_REQUEST;
    pIcmp->icmp_code = 0;
    pIcmp->icmp_id = (USHORT)::GetCurrentProcessId();
    pIcmp->icmp_sequence = 0;
    pIcmp->icmp_checksum = 0;

    // 填充数据,可以任意 
    memcpy((szBuff + ICMP_HEADER_SIZE), "abcdefghijklmnopqrstuvwabcdefghi", 32);
    
    // 计算校验和
    pIcmp->icmp_checksum = chsum((struct icmp_header *)szBuff, sizeof(szBuff));

    sockaddr_in from_addr;
    char szRecvBuff[1024];
    int nLen = sizeof(from_addr);
    int ret,flag = 0; 

	DWORD  start = GetTickCount();
    ret = sendto(s, szBuff, sizeof(szBuff), 0, (SOCKADDR *)&dest_addr, sizeof(SOCKADDR));
    //printf("ret = %d ,errorCode:%d\n",ret ,WSAGetLastError() ); 
   
	int i = 0; 
	//这里一定要用while循环,因为recvfrom 会接受到很多报文,包括 发送出去的报文也会被收到! 不信你可以用 wireshark 抓包查看,这个问题纠结来了一晚上 才猜想出来! 
    while(1){
    	if(i++ > 5){// icmp报文 如果到不了目标主机,是不会返回报文,多尝试几次接受数据,如果都没收到 即请求失败 
    		flag = 1;
    		break;
		}
    	memset(szRecvBuff,0,1024);
    	//printf("errorCode1:%d\n",WSAGetLastError() ); 
    	int ret = recvfrom(s, szRecvBuff, MAXBYTE, 0, (SOCKADDR *)&from_addr, &nLen);
    	//printf("errorCode2:%d\n",WSAGetLastError() ); 
    	//printf("ret=%d,%s\n",ret,inet_ntoa(from_addr.sin_addr)) ; 
    	//接受到 目标ip的 报文 
    	if( strcmp(inet_ntoa(from_addr.sin_addr),szDestIp) == 0)  {
    		respNum++;
    		break;
		}
	} 
	
    
    	
	DWORD  end = GetTickCount();
	DWORD time = end -start; 
	
	if(flag){
		printf("请求超时。\n");
		return bRet;
	}
	sumTime += time;
	if( minTime > time){
		minTime = time;
	}
	if( maxTime < time){
		maxTime = time;
	}
	
	
	
	// Windows的原始套接字 开发,系统没有去掉IP协议头,需要程序自己处理。
	// ip头部的第一个字节(只有1个字节不涉及大小端问题),前4位 表示 ip协议版本号,后4位 表示IP 头部长度(单位为4字节)
	char ipInfo = szRecvBuff[0];
	// ipv4头部的第9个字节为TTL的值
	char ttl = szRecvBuff[8];
	//printf("ipInfo = %x\n",ipInfo);
	
	
	int ipVer = ipInfo >> 4;
	int ipHeadLen = ((char)( ipInfo << 4) >> 4) * 4;
	if( ipVer  == 4) {
		//ipv4 
		//printf("ipv4 len = %d\n",ipHeadLen);
		// 跨过ip协议头,得到ICMP协议头的位置,不过是网络字节序。 
		// 网络字节序 是大端模式 低地址 高位字节 高地址 低位字节。-> 转换为 本地字节序 小端模式 高地址高字节 低地址低字节   
		icmp_header* icmp_rep = (icmp_header*)(szRecvBuff + ipHeadLen);
		//由于校验和是 2个字节 涉及大小端问题,需要转换字节序 
		unsigned short checksum_host = ntohs(icmp_rep->icmp_checksum);// 转主机字节序 和wireshark 抓取的报文做比较 
			
		//printf("type = %d ,checksum_host = %x\n",icmp_rep,checksum_host);

		if(icmp_rep->icmp_type == 0){ //回显应答报文 
			//来自 61.135.169.121 的回复: 字节=32 时间=1ms TTL=57
			printf("来自 %s 的回复:字节=32 时间=%2dms TTL=%d checksum=0x%x \n", szDestIp, time, ttl, checksum_host);
		} else{
			bRet = 0;
			printf("请求超时。type = %d\n",icmp_rep->icmp_type);
		}	
	}else{
		// ipv6 icmpv6 和 icmpv4 不一样,要做对应的处理 
		//printf("ipv6 len = %d\n",ipLen); 
	} 
	
    return bRet;
}

代码下载

就一个C文件 可以用IDE打开运行,需要链接 ws2_32.lib 库。博主用的devc++,完整工程代码下载,或者Github最新代码

猜你喜欢

转载自blog.csdn.net/qq_29542611/article/details/84452298