close

一、套接字的類型 A.流套接字(SOCK_STREAM) 用於提供面向連接、可靠的數據傳輸服務,其使用傳輸層的TCP協議 B.數據報套接字(SOCK_DGRAM) 用於提供一個無連接、不可靠的服務,其使用傳輸層上的UDP協議 C.原始套接字(SOCK_RAM) 原始套接字是相對表中套接字(即前面兩種套接字)而言的。它與標準套接字的區別是原始套接字可以讀寫內核沒有處理的IP數據包,流套接字只能讀取TCP協議的數據,數據報套接字只能讀取UDP協議的數據。 所以要訪問其他協議的數據必須使用原始套接字。 二、ping命令 ping命令是用來查看網絡上另一個主機系統的網絡連接是否正常的一個工具。 ping命令的工作原理:向網絡上的另一個主機系統發送ICMP報文,如果指定系統得到了報文,它將把報文一模一樣地傳回給發送者。 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,需要關註一下數據: IP報頭長度IHL(Internet Header Length) 其以4字節為一個單位來記錄IP報頭的長度,由上述IP數據結構的ip_hl變量。所以實際IP報頭的長度是ip_hl 生存時間TTL(Time To Live),是以秒為單位,指出IP數據報能在網絡上停留的最長時間,其值由發送方設定,並在經過路由的每一個節點減一,當該值為0時,數據報將被丟棄,是上述IP數據結構的ip_ttl變量。 B. ICMP報文 IPCMP報文分為兩種:一是錯誤報告報文,二是查詢報文。 註意:每個ICMP報頭均包含類型、編碼、校驗和這三項內容,長度為:8位、8位、16位。其余選項則隨ICMP的功能不同而不同。 ping命令只使用眾多ICMP報文中的兩種:"請求(ICMP_ECHO)"和"回應(ICMP_ECHOREPLY)"。在linux中定義如下: 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字節 協議頭校驗和算法 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。 rtt往返時間 為了實現這一功能,可利用ICMP數據報攜帶一個時間戳。使用以下函數生成時間戳: 獲取系統時間,放在struct timeval的變量中,第二個參數tzp指針表示時區,一般都是NULL,大多數代碼都是這樣,我也沒關註過。 其中tv_sec為秒數,tv_usec微秒數。在發送和接收報文時由gettimeofday分別生成兩個timeval結構,兩者之差即為往返時間,即ICMP報文發送與接收的時間差。 數據統計 系統自帶的ping命令當它發送完所有ICMP報文後,會對所有發送和所有接收的ICMP報文進行統計,從而計算ICMP報文丟失的比率。 註意:為達到此目標,我們在編寫代碼時,定義兩個全局變量:接收計數器和發送計數器,用於記錄ICMP報文接收和發送數目。丟失數目 = 發送總數 - 接收總數,丟失比率 = 丟失數目 / 發送總數。 四、myping的實現 補充知識 判斷一個字符串是否是 string :"192.168.1.45" 這樣的字符串 if( inet_addr(string) == INADDR_NONE ) { ......... } 補充知識 通過協議名如"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)) 補充知識 通過主機名或域名獲取其對應的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 #include #include 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 myping源碼 #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_SIZE 1024 char send_buf[MAX_SIZE]; char recv_buf[MAX_SIZE]; int nsend = 0,nrecv = 0; int datalen = 56; //統計結果 void statistics(int signum) { printf("\n----------------PING statistics---------------\n"); printf("%d packets transmitted,%d recevid,%%%d lost\n",nsend,nrecv,(nsend - nrecv)/nsend * 100); exit(EXIT_SUCCESS); } //校驗和算法 int calc_chsum(unsigned short *addr,int len) { int sum = 0,n = len; unsigned short answer = 0; unsigned short *p = addr; //每兩個字節相加 while(n > 1) { sum += *p ++; n -= 2; } //處理數據大小是奇數,在最後一個字節後面補0 if(n == 1) { *((unsigned char *)&answer) = *(unsigned char *)p; sum += answer; } //將得到的sum值的高2字節和低2字節相加 sum = (sum >> 16) + (sum & 0xffff); //處理溢出的情況 sum += sum >> 16; answer = ~sum; return answer; } int pack(int pack_num) { int packsize; struct icmp *icmp; struct timeval *tv; icmp = (struct icmp *)send_buf; icmp->icmp_type = ICMP_ECHO; icmp->icmp_code = 0; icmp->icmp_cksum = 0; icmp->icmp_id = htons(getpid()); icmp->icmp_seq = htons(pack_num); tv = (struct timeval *)icmp->icmp_data; //記錄發送時間 if(gettimeofday(tv,NULL) icmp_cksum = calc_chsum((unsigned short *)icmp,packsize); return packsize; } int send_packet(int sockfd,struct sockaddr *paddr) { int packsize; //將send_buf填上a memset(send_buf,'a',sizeof(send_buf)); nsend ++; //打icmp包 packsize = pack(nsend); if(sendto(sockfd,send_buf,packsize,0,paddr,sizeof(struct sockaddr)) tv_usec - tv_send->tv_usec tv_sec --; tv_recv->tv_usec += 1000000; } ts.tv_sec = tv_recv->tv_sec - tv_send->tv_sec; ts.tv_usec = tv_recv->tv_usec - tv_send->tv_usec; return ts; } int unpack(int len,struct timeval *tv_recv,struct sockaddr *paddr,char *ipname) { struct ip *ip; struct icmp *icmp; struct timeval *tv_send,ts; int ip_head_len; float rtt; ip = (struct ip *)recv_buf; ip_head_len = ip->ip_hl icmp_id) == getpid() && icmp->icmp_type == ICMP_ECHOREPLY) { nrecv ++; tv_send = (struct timeval *)icmp->icmp_data; ts = time_sub(tv_send,tv_recv); rtt = ts.tv_sec * 1000 + (float)ts.tv_usec/1000;//以毫秒為單位 printf("%d bytes from %s (%s):icmp_req = %d ttl=%d time=%.3fms.\n", len,ipname,inet_ntoa(((struct sockaddr_in *)paddr)->sin_addr),ntohs(icmp->icmp_seq),ip->ip_ttl,rtt); } return 0; } int recv_packet(int sockfd,char *ipname) { int addr_len ,n; struct timeval tv; struct sockaddr from_addr; addr_len = sizeof(struct sockaddr); if((n = recvfrom(sockfd,recv_buf,sizeof(recv_buf),0,&from_addr,&addr_len)) p_proto)) h_addr,host->h_length); }else{//ip地址 peer_addr.sin_addr.s_addr = netaddr; } //註冊信號處理函數 signal(SIGALRM,statistics); signal(SIGINT,statistics); alarm(5); //開始信息 printf("PING %s(%s) %d bytes of data.\n",argv[1],inet_ntoa(peer_addr.sin_addr),datalen); //發送包文和接收報文 while(1) { send_packet(sockfd,(struct sockaddr *)&peer_addr); recv_packet(sockfd,argv[1]); alarm(5); sleep(1); } exit(EXIT_SUCCESS); } 註意:由於原始套接字的創建只能是擁有超級權限的進程創建,所以我們需要將我們編譯好的可執行文件,把其文件所有者改為root,再將其set-uid-bit位進行設置。

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 成功运行 的頭像
    成功运行

    成功运行的部落格

    成功运行 發表在 痞客邦 留言(0) 人氣()