Linux 原始套接字--myping的实现

一、套接字的类型

A.流套接字(SOCK_STREAM)

用于提供面向连接、可靠的数据传输服务,其使用传输层的TCP协议

B.数据报套接字(SOCK_DGRAM)

用于提供一个无连接、不可靠的服务,其使用传输层上的UDP协议

C.原始套接字(SOCK_RAM)

原始套接字是相对表中套接字(即前面两种套接字)而言的。它与标准套接字的区别是原始套接字可以读写内核没有处理的IP数据包,流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。

所以要访问其他协议的数据必须使用原始套接字。

二、ping命令

ping命令是用来查看网络上另一个主机系统的网络连接是否正常的一个工具。

ping命令的工作原理:向网络上的另一个主机系统发送ICMP报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者。

来看看在linux下使用ping命令的效果:

从上面可以看到,ping命令执行后显示出被系统主机名(或域名)和相应 IP地址、返回给当前主机的ICMP报文顺序号、ttl生存时间和往返时间rtt(单位是豪秒,即千分之一秒)。这些信息对我们后面要实现的myping有提示作用。

三、ICMP的介绍

ICMP(Internet Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告发给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。

ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,在添加IP头形成IP数据报。

注意:IP头不需要我们实现,由内核协议栈自动添加,我们只需要实现ICMP报文。

A.在Linux环境下,IP头定义如下:

我们要实现ping,需要关注一下数据:

<1>IP报头长度IHL(Internet Header Length)

其以4字节为一个单位来记录IP报头的长度,由上述IP数据结构的ip_hl变量。所以实际IP报头的长度是ip_hl << 2。

<2>生存时间TTL(Time To Live),是以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。

B. ICMP报文

IPCMP报文分为两种:一是错误报告报文,二是查询报文。

注意:每个ICMP报头均包含类型、编码、校验和这三项内容,长度为:8位、8位、16位。其余选项则随ICMP的功能不同而不同。

ping命令只使用众多ICMP报文中的两种:"请求(ICMP_ECHO)"和"回应(ICMP_ECHOREPLY)"。在linux中定义如下:


这两种报文格式如下:

通过wirshark抓包格式如下:

ICMP报头在linux定义如下:

struct icmp

{

  u_int8_t  icmp_type; /* type of message, see below */

  u_int8_t  icmp_code; /* type sub code */

  u_int16_t icmp_cksum; /* ones complement checksum of struct */

  union

  {

    u_char ih_pptr; /* ICMP_PARAMPROB */

    struct in_addr ih_gwaddr; /* gateway address */

    struct ih_idseq /* echo datagram */

    {

      u_int16_t icd_id;

      u_int16_t icd_seq;

    } ih_idseq;

    u_int32_t ih_void;


    /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */

    struct ih_pmtu

    {

      u_int16_t ipm_void;

      u_int16_t ipm_nextmtu;

    } ih_pmtu;


    struct ih_rtradv

    {

      u_int8_t irt_num_addrs;

      u_int8_t irt_wpa;

      u_int16_t irt_lifetime;

    } ih_rtradv;

  } icmp_hun;

#define icmp_pptr icmp_hun.ih_pptr

#define icmp_gwaddr icmp_hun.ih_gwaddr

#define icmp_id icmp_hun.ih_idseq.icd_id(标识一个ICMP报文,一般我们用PID标识)

#define icmp_seq icmp_hun.ih_idseq.icd_seq(发送报文的序号)

#define icmp_void icmp_hun.ih_void

#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void

#define icmp_nextmtu icmp_hun.ih_pmtu.ipm_nextmtu

#define icmp_num_addrs icmp_hun.ih_rtradv.irt_num_addrs

#define icmp_wpa icmp_hun.ih_rtradv.irt_wpa

#define icmp_lifetime icmp_hun.ih_rtradv.irt_lifetime

  union

  {

    struct

    {

      u_int32_t its_otime;

      u_int32_t its_rtime;

      u_int32_t its_ttime;

    } id_ts;

    struct

    {

      struct ip idi_ip;

      /* options and then 64 bits of data */

    } id_ip;

    struct icmp_ra_addr id_radv;

    u_int32_t   id_mask;

    u_int8_t    id_data[1];

  } icmp_dun;

#define icmp_otime icmp_dun.id_ts.its_otime

#define icmp_rtime icmp_dun.id_ts.its_rtime

#define icmp_ttime icmp_dun.id_ts.its_ttime

#define icmp_ip icmp_dun.id_ip.idi_ip

#define icmp_radv icmp_dun.id_radv

#define icmp_mask icmp_dun.id_mask

#define icmp_data icmp_dun.id_data(可以看到id_data是含有一个元素的数组名,为什么这样干呀?思考...)

};


以上红色部分使我们实现ping需要填充的部分。

规定:ICMP报头为8字节

<1>协议头校验和算法

unsigned short chksum(addr,len)   

    unsigned short *addr;  // 校验数据开始地址(注意是以2字节为单位)    

    int len;                // 校验数据的长度大小,以字节为单位

{  

    int sum = 0;        // 校验和 

    int nleft = len;    // 未累加的数据长度    

    unsigned short *p;  // 走动的临时指针,2字节为单位     

    unsigned short tmp = 0; // 奇数字节长度时用到     

  
   while( nleft > 1)   

   {       

       sum += *p++;    // 累加         

       nleft -= 2;   

    }

// 奇数字节长度    

   if(nleft == 1)   

   {      
      
// 将最后字节压如2字节的高位        

       *(unsigned char *)&tmp = *(unsigned char *)p;

       sum += tmp;   

   } 
    
//高位低位相加  

    sum = (sum >> 16) + (sum & 0xffff);    

  
   // 上一步溢出时(十六进制相加进位),将溢出位也加到sum中      
   sum += sum >> 16;
  

   // 注意类型转换,现在的校验和为16位

    tmp = ~sum;         

  
   return tmp;

}


网际校验和算法,把被校验的数据16位进行累加,然后取反码,若数据字节长度为奇数,则数据尾部补一个字节的0以凑成偶数。此算法适用于IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校验和,更详细的信息请参考RFC1071。


<2>rtt往返时间

为了实现这一功能,可利用ICMP数据报携带一个时间戳。使用以下函数生成时间戳:

获取系统时间,放在struct timeval的变量中,第二个参数tzp指针表示时区,一般都是NULL,大多数代码都是这样,我也没关注过。

其中tv_sec为秒数,tv_usec微秒数。在发送和接收报文时由gettimeofday分别生成两个timeval结构,两者之差即为往返时间,即ICMP报文发送与接收的时间差。

<3>数据统计

系统自带的ping命令当它发送完所有ICMP报文后,会对所有发送和所有接收的ICMP报文进行统计,从而计算ICMP报文丢失的比率。

注意:为达到此目标,我们在编写代码时,定义两个全局变量:接收计数器和发送计数器,用于记录ICMP报文接收和发送数目。丢失数目 =  发送总数 - 接收总数,丢失比率 = 丢失数目 / 发送总数。

四、myping的实现

<1>补充知识

判断一个字符串是否是  string :"192.168.1.45" 这样的字符串

if(   inet_addr(string) == INADDR_NONE  )

{

        .........

}


<2>补充知识

通过协议名如"icmp"获取对应的协议编号

解释如下:


注意:


创建原始套接字的时候,就需要指定其协议编号.


struct  protoent  *protocol;

int sockfd_ram;


if((protocol = getprotobyname("icmp")) == NULL)

{

    perror("Fail to getprotobyname");

    exit(EXIT_FAILURE);

}


//我们一般在创建,流套接字和数据包套接字时指定的是0,代表让系统自己自动去识别

if((sockfd_ram = socket(AF_INET,SOCK_RAM,protocol->p_proto)) < 0)

{

    perror("Fail to socket");

    exit(EXIT_FAILURE);    

}


<3>补充知识

通过主机名或域名获取其对应的ip地址

这个函数的传入值是域名或者主机名,例如"www.google.cn"等等。传出值,是一个hostent的结构。如果函数调用失败,将返回NULL。

  hostent->h_name
    表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。
    hostent->h_aliases
    表示的是主机的别名.www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。
    hostent->h_addrtype     
    表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是pv6(AF_INET6)
    hostent->h_length       
    表示的是主机ip地址的长度
    hostent->h_addr_lisst 
    表示的是主机的ip地址,注意,这个是以网络字节序存储的。千万不要直接用printf带%s参数来打这个东西,会有问题的哇。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。

这个函数,是将类型为af的网络地址结构src,转换成主机序的字符串形式,存放在长度为cnt的字符串中。返回指向dst的一个指针。如果函数调用错误,返回值是NULL。

#include <netdb.h>
#include <sys/socket.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    char   *ptr, **pptr;
    struct hostent *hptr;
    char   str[32];
    ptr = argv[1];

if((hptr = gethostbyname(ptr)) == NULL)
    {
        printf(" gethostbyname error for host:%s\n", ptr);
        return 0; 
    }

printf("official hostname:%s\n",hptr->h_name);
    for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)
        printf(" alias:%s\n",*pptr);

switch(hptr->h_addrtype)
    {
        case AF_INET:
        case AF_INET6:
            pptr=hptr->h_addr_list;
            for(; *pptr!=NULL; pptr++)
                printf(" address:%s\n", 
                       inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
            printf(" first address: %s\n", 
                       inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
        break;
        default:
            printf("unknown address type\n");
        break;
    }

return 0;
}

编译运行
-----------------------------
# gcc test.c
# ./a.out www.baidu.com
official hostname:www.a.shifen.com
alias:www.baidu.com
address:121.14.88.11
address:121.14.89.11
first address: 121.14.88.11

<4>myping源码

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <errno.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <netinet/in.h>
  8. #include <arpa/inet.h>
  9. #include <netdb.h>
  10. #include <sys/time.h>
  11. #include <netinet/ip_icmp.h>
  12. #include <unistd.h>
  13. #include <signal.h>
  14. #define MAX_SIZE 1024
  15. char send_buf[MAX_SIZE];
  16. char recv_buf[MAX_SIZE];
  17. int nsend = 0,nrecv = 0;
  18. int datalen = 56;
  19. //统计结果
  20. void statistics(int signum)
  21. {
  22. printf("\n----------------PING statistics---------------\n");
  23. printf("%d packets transmitted,%d recevid,%%%d lost\n",nsend,nrecv,(nsend - nrecv)/nsend * 100);
  24. exit(EXIT_SUCCESS);
  25. }
  26. //校验和算法
  27. int calc_chsum(unsigned short *addr,int len)
  28. {
  29. int sum = 0,n = len;
  30. unsigned short answer = 0;
  31. unsigned short *p = addr;
  32. //每两个字节相加
  33. while(n > 1)
  34. {
  35. sum += *p ++;
  36. n -= 2;
  37. }
  38. //处理数据大小是奇数,在最后一个字节后面补0
  39. if(n == 1)
  40. {
  41. *((unsigned char *)&answer) = *(unsigned char *)p;
  42. sum += answer;
  43. }
  44. //将得到的sum值的高2字节和低2字节相加
  45. sum = (sum >> 16) + (sum & 0xffff);
  46. //处理溢出的情况
  47. sum += sum >> 16;
  48. answer = ~sum;
  49. return answer;
  50. }
  51. int pack(int pack_num)
  52. {
  53. int packsize;
  54. struct icmp *icmp;
  55. struct timeval *tv;
  56. icmp = (struct icmp *)send_buf;
  57. icmp->icmp_type = ICMP_ECHO;
  58. icmp->icmp_code = 0;
  59. icmp->icmp_cksum = 0;
  60. icmp->icmp_id = htons(getpid());
  61. icmp->icmp_seq = htons(pack_num);
  62. tv = (struct timeval *)icmp->icmp_data;
  63. //记录发送时间
  64. if(gettimeofday(tv,NULL) < 0)
  65. {
  66. perror("Fail to gettimeofday");
  67. return -1;
  68. }
  69. packsize = 8 + datalen;
  70. icmp->icmp_cksum = calc_chsum((unsigned short *)icmp,packsize);
  71. return packsize;
  72. }
  73. int send_packet(int sockfd,struct sockaddr *paddr)
  74. {
  75. int packsize;
  76. //将send_buf填上a
  77. memset(send_buf,‘a‘,sizeof(send_buf));
  78. nsend ++;
  79. //打icmp包
  80. packsize = pack(nsend);
  81. if(sendto(sockfd,send_buf,packsize,0,paddr,sizeof(struct sockaddr)) < 0)
  82. {
  83. perror("Fail to sendto");
  84. return -1;
  85. }
  86. return 0;
  87. }
  88. struct timeval time_sub(struct timeval *tv_send,struct timeval *tv_recv)
  89. {
  90. struct timeval ts;
  91. if(tv_recv->tv_usec - tv_send->tv_usec < 0)
  92. {
  93. tv_recv->tv_sec --;
  94. tv_recv->tv_usec += 1000000;
  95. }
  96. ts.tv_sec = tv_recv->tv_sec - tv_send->tv_sec;
  97. ts.tv_usec = tv_recv->tv_usec - tv_send->tv_usec;
  98. return ts;
  99. }
  100. int unpack(int len,struct timeval *tv_recv,struct sockaddr *paddr,char *ipname)
  101. {
  102. struct ip *ip;
  103. struct icmp *icmp;
  104. struct timeval *tv_send,ts;
  105. int ip_head_len;
  106. float rtt;
  107. ip = (struct ip *)recv_buf;
  108. ip_head_len = ip->ip_hl << 2;
  109. icmp = (struct icmp *)(recv_buf + ip_head_len);
  110. len -= ip_head_len;
  111. if(len < 8)
  112. {
  113. printf("ICMP packets\‘s is less than 8.\n");
  114. return -1;
  115. }
  116. if(ntohs(icmp->icmp_id) == getpid() && icmp->icmp_type == ICMP_ECHOREPLY)
  117. {
  118. nrecv ++;
  119. tv_send = (struct timeval *)icmp->icmp_data;
  120. ts = time_sub(tv_send,tv_recv);
  121. rtt = ts.tv_sec * 1000 + (float)ts.tv_usec/1000;//以毫秒为单位
  122. printf("%d bytes from %s (%s):icmp_req = %d ttl=%d time=%.3fms.\n",
  123. len,ipname,inet_ntoa(((struct sockaddr_in *)paddr)->sin_addr),ntohs(icmp->icmp_seq),ip->ip_ttl,rtt);
  124. }
  125. return 0;
  126. }
  127. int recv_packet(int sockfd,char *ipname)
  128. {
  129. int addr_len ,n;
  130. struct timeval tv;
  131. struct sockaddr from_addr;
  132. addr_len = sizeof(struct sockaddr);
  133. if((n = recvfrom(sockfd,recv_buf,sizeof(recv_buf),0,&from_addr,&addr_len)) < 0)
  134. {
  135. perror("Fail to recvfrom");
  136. return -1;
  137. }
  138. if(gettimeofday(&tv,NULL) < 0)
  139. {
  140. perror("Fail to gettimeofday");
  141. return -1;
  142. }
  143. unpack(n,&tv,&from_addr,ipname);
  144. return 0;
  145. }
  146. int main(int argc,char *argv[])
  147. {
  148. int size = 50 * 1024;
  149. int sockfd,netaddr;
  150. struct protoent *protocol;
  151. struct hostent *host;
  152. struct sockaddr_in peer_addr;
  153. if(argc < 2)
  154. {
  155. fprintf(stderr,"usage : %s ip.\n",argv[0]);
  156. exit(EXIT_FAILURE);
  157. }
  158. //获取icmp的信息
  159. if((protocol = getprotobyname("icmp")) == NULL)
  160. {
  161. perror("Fail to getprotobyname");
  162. exit(EXIT_FAILURE);
  163. }
  164. //创建原始套接字
  165. if((sockfd = socket(AF_INET,SOCK_RAW,protocol->p_proto)) < 0)
  166. {
  167. perror("Fail to socket");
  168. exit(EXIT_FAILURE);
  169. }
  170. //回收root权限,设置当前用户权限
  171. setuid(getuid());
  172. /*
  173. 扩大套接子接收缓冲区到50k,这样做主要为了减少接收缓冲区溢出的可能性
  174. 若无影中ping一个广播地址或多播地址,将会引来大量应答
  175. */
  176. if(setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size)) < 0)
  177. {
  178. perror("Fail to setsockopt");
  179. exit(EXIT_FAILURE);
  180. }
  181. //填充对方的地址
  182. bzero(&peer_addr,sizeof(peer_addr));
  183. peer_addr.sin_family = AF_INET;
  184. //判断是主机名(域名)还是ip
  185. if((netaddr = inet_addr(argv[1])) == INADDR_NONE)
  186. {
  187. //是主机名(域名)
  188. if((host = gethostbyname(argv[1])) == NULL)
  189. {
  190. fprintf(stderr,"%s unknown host : %s.\n",argv[0],argv[1]);
  191. exit(EXIT_FAILURE);
  192. }
  193. memcpy((char *)&peer_addr.sin_addr,host->h_addr,host->h_length);
  194. }else{//ip地址
  195. peer_addr.sin_addr.s_addr = netaddr;
  196. }
  197. //注册信号处理函数
  198. signal(SIGALRM,statistics);
  199. signal(SIGINT,statistics);
  200. alarm(5);
  201. //开始信息
  202. printf("PING %s(%s) %d bytes of data.\n",argv[1],inet_ntoa(peer_addr.sin_addr),datalen);
  203. //发送包文和接收报文
  204. while(1)
  205. {
  206. send_packet(sockfd,(struct sockaddr *)&peer_addr);
  207. recv_packet(sockfd,argv[1]);
  208. alarm(5);
  209. sleep(1);
  210. }
  211. exit(EXIT_SUCCESS);
  212. }

注意:由于原始套接字的创建只能是拥有超级权限的进程创建,所以我们需要将我们编译好的可执行文件,把其文件所有者改为root,再将其set-uid-bit位进行设置。操作如下:

阅读(31) | 评论(0) | 转发(0) |

0

上一篇:进程间同步---system v ipc 对象信号灯集

下一篇:u-boot启动完全分析

相关热门文章

  • linux 常见服务端口
  • xmanager 2.0 for linux配置
  • 【ROOTFS搭建】busybox的httpd...
  • openwrt中luci学习笔记
  • 什么是shell

热门推荐

    -->

    给主人留下些什么吧!~~

    评论热议

    Linux 原始套接字--myping的实现

    时间: 2024-10-10 20:41:01

    Linux 原始套接字--myping的实现的相关文章

    关于linux 原始套接字编程

    关于linux 网络编程最权威的书是<<unix网络编程>>,但是看这本书时有些内容你可能理解的不是很深刻,或者说只知其然而不知其所以然,那么如果你想搞懂的话那么我建议你可以看看网络协议栈的实现. 函数原型是 int socket(int domain, int type, int protocol); 其中domain 中AF_INET , AF_UNIT 较为常用,分别创建inet 域套接字和unix域套接字,unix套接字与文件相关.平时80%用的套接字都是AF_INET.这

    linux原始套接字(3)-构造IP_TCP发送与接收

    一.概述                                                    tcp报文封装在ip报文中,创建tcp的原始套接字如下: 1 sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP); 此时只能构造tcp报文,如果想进一步构造ip首部,那么就要开启sockfd的IP_HDRINCL选项: 1 int on = 1; 2 setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on

    linux原始套接字(2)-icmp请求与接收

    一.概述                                                    上一篇arp请求使用的是链路层的原始套接字.icmp封装在ip数据报里面,所以icmp请求可以直接使用网络层的原始套接字,即socket()第一个参数是PF_INET.如下: 1 sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP); icmp报文不同的类型有不同的格式,我们以icmp回显请求和会显应答报文格式(即ping程序使用的报文类型)

    linux原始套接字(1)-arp请求与接收

    一.概述                                                   以太网的arp数据包结构: arp结构op操作参数:1为请求,2为应答. 常用的数据结构如下: 1.物理地址结构位于netpacket/packet.h 1 struct sockaddr_ll 2 { 3 unsigned short int sll_family; 4 unsigned short int sll_protocol; 5 int sll_ifindex; 6 unsi

    linux原始套接字(4)-构造IP_UDP

    一.概述                                                    同上一篇tcp一样,udp也是封装在ip报文里面.创建UDP的原始套接字如下: 1 (sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_UDP); 同样,如果要构造udp的ip首部,要开启IP_HDRINCL选项! udp首部格式: udp的不可靠性,比tcp报文简单很多.上面的16位UDP长度是UDP首部+普通数据的总长度,这点跟ip首部的16位总

    Linux Socket 原始套接字编程

    对于linux网络编程来说,可以简单的分为标准套接字编程和原始套接字编程,标准套接字主要就是应用层数据的传输,原始套接字则是可以获得不止是应用层的其他层不同协议的数据.与标准套接字相区别的主要是要开发之自己构建协议头.对于原始套接字编程有些细节性的东西还是需要注意的. 1. 原始套接字创建 原始套接字的编程和udp网络编程的流程有点类似,但是原始套接字编程中不需要bind操作,因为在数据接收和发送过程中使用sendto和recvfrom函数实现数据的接收和发送.不过不是说原始套接字不能使用bin

    Linux网络编程——原始套接字实例:简单版网络数据分析器

    通过<Linux网络编程--原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 MAC 头部(有线局域网) 注意:CRC.PAD 在组包时可以忽略 链路层数据包的其中一种情况: unsigned char msg[1024] = { //--------------组MAC--------14------ 0xb8, 0x88, 0xe3, 0xe1, 0x10, 0xe6, // dst

    Linux网络编程:原始套接字的魔力【上】

    基于原始套接字编程 在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证: 也就是说,对于TCP或UDP的程序开发,焦点在Data字段,我们没法直接对TCP或UDP头部字段进行赤裸裸的修改,当然还有IP头.换句话说,我们对它们头部操作的空间非常受限,只能使用它们已经开放给我们的诸如源.目的IP,源.目的端口等等. 今天我们讨论一下原始套接字的程序开发,用它作为入门协议栈的进阶跳板太合适不过了.OK闲话不多说,进入正题. 原始

    Linux 网络编程——原始套接字实例:MAC 地址扫描器

    如果 A (192.168.1.1 )向 B (192.168.1.2 )发送一个数据包,那么需要的条件有 ip.port.使用的协议(TCP/UDP)之外还需要 MAC 地址,因为在以太网数据包中 MAC 地址是必须要有的.那么怎样才能知道对方的 MAC 地址?答案是:它通过 ARP 协议来获取对方的 MAC 地址. ARP(Address Resolution Protocol,地址解析协议),是 TCP/IP 协议族中的一个,主要用于查询指定 ip 所对应的的 MAC(通过 ip 找 MA