其實從大學學習了C語言後,翻看整本教材只有C語言的語法,根本沒有網絡編程相關的任何內容,現在回想起來,都記不起自己何時在哪本書上學習了套接字編程,說起TCP、UDP,能知道他們的區別,相關的編程的“套路”,即分別在服務器端和客戶端應用程序的固定招數,函數也還是有那麼多,但是功能方面嘛,仔細想想,一般除了通信傳輸數據,文件等,就沒有其他目的了,即使有多播,組播,廣播等,目的也是爲了通信,而且都是在局域網內。以上提到的這些都太中規中矩了,總感覺少了些什麼,比如網絡性能監視類的程序,網絡探測、網絡攻擊等程序肯定光用這個套接字編程技術無法實現。
寫這篇文章是爲了溫故知新,照着Windows網絡編程一書第7章內容而寫。所以全手打。
-----------------------分----割----線-----------------------------------
原始套接字是允許訪問底層傳輸協議的一種套接字類型,提供了普通套接字所不具備的功能,能夠對網絡數據包進行某種程度的控制操作。
原始套接字提供普通套接字不具備的能力有:發送和接收內核不處理其協議字段的IPv4數據包。對於8位IPv4協議字段,大多數內核僅僅處理該字段值爲1(ICMP協議)、2(IGMP協議)、6(TCP協議)、17(UDP協議)的數據報,但是協議字段的值還有很多。例如,OSPF路由協議的值爲89,如果要處理OSPF數據報文,那麼程序必須使用原始套接字讀寫。
直接總結乾貨:
首先,在WinSock2中,很熟悉的兩個函數:socket()和WSASocket(),第二個參數用常量SOCK_RAW指明,第三個參數IPPROTO_RAW或者IPPROTO_IP
其次,可以不用在原始套接字上調用bind()函數,也可以不用調用connect()函數,
然後,接收數據時可以調用recvfrom()或者WSARecvFrom()函數。
當接收數據時,在接收到一個數據包後,IP協議棧會把滿足以下條件的IP數據包傳遞到原始套接字中:
1、既不是UDP的數據也不是TCP的數據包;2、部分ICMP分組;3、所有的IGMP分組;4、其他所有的IP數據包;5、重組後的分片數據。
傳遞到原始套接字中的數據包會根據接收條件決定是否拷貝,若滿足則拷貝到原始套接字的接收緩衝區中。
假如我們希望能夠接收到所有發給網卡的數據,甚至是流經網卡但並非給本機的數據,可以通過設置接收選項SIO_RCVALL就能實現,設置該套接字控制命令則需要通過函數WSAIoctl()。但是比如在設計一個ping程序時,發出請求後接收響應時,很可能有其他的ICMP消息產生干擾,此時就應該從數據來源IP地址,協議類型等方面來判斷接收到的數據,正確匹配。
當然了,發送數據時可以調用sendto()或WSASendTo()函數,廣播地址,多播地址同樣適用(廣播或者多播時需要通過setsocketopt()函數設置選項SO_BROADCAST!!)
connect()函數指明遠端地址。
如果要設置IP首部,IPv4時選項爲IP_HDRINCL,選項級別爲IPPROTO_IP。當然,此後才能調用bind()函數來指明本機IP地址。要知道,只有這個選項開啓了,才能在IP首部中修改源IP地址!可惜Windows對這些作了限制,如果源IP地址不正確,惡意代碼不能通過僞造的源IP地址進行拒絕服務攻擊,也不能發送IP欺騙數據包。如果原始套接字無法滿足需求,還有WInPcap編程直接操控數據幀。
有兩個編程示例:
第一個:使用原始套接字實現ping
ping是很多操作系統上用來檢驗主機是否連通網絡,以及探測遠程主機是否存活的實用工具之一,它通過系那個遠程主機發送ICMP協議包並檢驗取回的遠程主機吸納供應包來實現上述功能。
概念: 位於網絡層的IP協議和ICMP協議屬於同等級的,
正因爲原始套接字才能構造、發送和接收ICMP協議的數據包。ping程序使用了ICMP協議的ECHO類型的請求報文,程序中需要發送ECHO請求和接收響應,並計算間隔時間,判斷網絡狀況。
ECHO請求由ICMP首部和ICMP數據組成,在前面加上IP首部構成IP數據包。即IP數據包=IP首部+ICMP首部+ICMP數據
所以,先定義ICMP首部的結構體,有類型,代碼,校驗和,ECHO請求的標識,序號5個成員共計8個字節
直接貼代碼:
頭文件:
#pragma pack(1)
#define ICMP_ECHOREPLY 0
#define ICMP_ECHOREQ 8
// IP首部結構體
typedef struct tagIPHDR
{
u_char VIHL;
// 版本號和首部長度
u_char
TOS; // 服務類型
short TotLen;
// 總的長度
short ID;
// 標識
short FlagOff;
// 分片標誌及偏移
u_char
TTL; // TTL
u_char
Protocol; // 協議
u_short
Checksum; // 校驗和
struct
in_addr iaSrc; // 源IP地址
struct
in_addr iaDst; // 目的IP地址
}IPHDR, *PIPHDR;
// ICMP首部結構體
typedef struct tagICMPHDR
{
u_char
Type; // 類型(表示ECHO請求)
u_char
Code; // 代碼
u_short
Checksum; // 校驗和
u_short
ID; // 標識
u_short
Seq; // 序號
}ICMPHDR, *PICMPHDR;
#define REQ_DATASIZE 32 // Echo Request Data size
// ICMP ECHO請求結構體
typedef struct tagECHOREQUEST
{
ICMPHDR icmpHdr; //ICMP協議首部
DWORD dwTime; //獲取的當前系統時間戳
char cData[REQ_DATASIZE]; //ICMP數據
}ECHOREQUEST, *PECHOREQUEST;
// ICMP ECHO應答結構體
typedef struct tagECHOREPLY
{
IPHDR ipHdr; //IP首部
ECHOREQUEST
echoRequest; //ICMP請求的結構體
char cFiller[256]; //填充
}ECHOREPLY, *PECHOREPLY;
#pragma pack()
源文件:
#include <stdio.h>
#include <stdlib.h>
#include <winsock.h>
#include "ping.h"
void Ping(LPCSTR pstrHost);//調用了SendEchoRequest函數和RecvEchoReply函數。
void ReportError(LPCSTR pstrFrom);
int WaitForEchoReply(SOCKET s);
u_short in_cksum(u_short *addr, int len);
int SendEchoRequest(SOCKET, LPSOCKADDR_IN);
DWORD RecvEchoReply(SOCKET, LPSOCKADDR_IN, u_char *);
void main(int argc, char **argv)
{
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(1,1);
int nRet;
if (argc != 2)
{
fprintf(stderr,"\nUsage: ping hostname\n");
return;
}
nRet = WSAStartup(wVersionRequested, &wsaData);
if (nRet)
{
fprintf(stderr,"\nError initializing WinSock\n");
return;
}
if (wsaData.wVersion != wVersionRequested)
{
fprintf(stderr,"\nWinSock version not supported\n");
return;
}
Ping(argv[1]);
WSACleanup();
}
void Ping(LPCSTR pstrHost)
{
SOCKET
rawSocket;
LPHOSTENT lpHost;
struct sockaddr_in saDest;
struct sockaddr_in saSrc;
DWORD dwTimeSent;
DWORD dwElapsed;
u_char cTTL;
int nLoop;
int nRet;
// 創建原始套接字,指定協議IPPROTO_ICMP
rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (rawSocket == SOCKET_ERROR)
{
ReportError("socket()");
return;
}
// 獲取IP地址
lpHost = gethostbyname(pstrHost);
if (lpHost == NULL)
{
fprintf(stderr,"\nHost not found: %s\n", pstrHost);
return;
}
saDest.sin_addr.s_addr = *((u_long FAR *) (lpHost->h_addr));
saDest.sin_family = AF_INET;
saDest.sin_port = 0;
printf("\nPinging %s [%s] with %d bytes of data:\n",
pstrHost,
inet_ntoa(saDest.sin_addr),
REQ_DATASIZE);
//連續ping4次
for (nLoop = 0; nLoop < 4; nLoop++)
{
SendEchoRequest(rawSocket, &saDest);//發送請求
nRet = WaitForEchoReply(rawSocket);//等待響應
if (nRet == SOCKET_ERROR)
{
ReportError("select()");
break;
}
if (!nRet)
{
printf("\nTimeOut");
break;
}
// 接收響應
dwTimeSent = RecvEchoReply(rawSocket, &saSrc, &cTTL);
// 計算間隔時間
dwElapsed = GetTickCount() - dwTimeSent;
printf("\nReply from: %s: bytes=%d time=%ldms TTL=%d",
inet_ntoa(saSrc.sin_addr),
REQ_DATASIZE,
dwElapsed,
cTTL);
}
printf("\n");
nRet = closesocket(rawSocket);
if (nRet == SOCKET_ERROR)
ReportError("closesocket()");
}
int SendEchoRequest(SOCKET s,LPSOCKADDR_IN lpstToAddr)
{
static ECHOREQUEST echoReq;
static nId = 1;
static nSeq = 1;
int nRet;
// 結構體成員賦值,填充echo請求
echoReq.icmpHdr.Type
= ICMP_ECHOREQ;
echoReq.icmpHdr.Code
= 0;
echoReq.icmpHdr.Checksum
= 0;
echoReq.icmpHdr.ID
= nId++;
echoReq.icmpHdr.Seq
= nSeq++;
//填寫數據
for (nRet = 0; nRet < REQ_DATASIZE; nRet++)
echoReq.cData[nRet] = ' '+nRet;
//獲取當前時間並記錄
echoReq.dwTime
= GetTickCount();
//計算校驗和
echoReq.icmpHdr.Checksum = in_cksum((u_short *)&echoReq, sizeof(ECHOREQUEST));
nRet = sendto(s,
(LPSTR)&echoReq,
sizeof(ECHOREQUEST),
0,
(LPSOCKADDR)lpstToAddr,
sizeof(SOCKADDR_IN));
if (nRet == SOCKET_ERROR)
ReportError("sendto()");
return (nRet);
}
DWORD RecvEchoReply(SOCKET s, LPSOCKADDR_IN lpsaFrom, u_char *pTTL)
{
ECHOREPLY echoReply;
int nRet;
int nAddrLen = sizeof(struct sockaddr_in);
nRet = recvfrom(s,
(LPSTR)&echoReply,
sizeof(ECHOREPLY),
0,
(LPSOCKADDR)lpsaFrom,
&nAddrLen);
if (nRet == SOCKET_ERROR)
ReportError("recvfrom()");
*pTTL = echoReply.ipHdr.TTL;
return(echoReply.echoRequest.dwTime);
}
void ReportError(LPCSTR pWhere)
{
fprintf(stderr,"\n%s error: %d\n",
WSAGetLastError());
}
int WaitForEchoReply(SOCKET s)
{
struct timeval Timeout;
fd_set readfds;
readfds.fd_count = 1;
readfds.fd_array[0] = s;
Timeout.tv_sec = 5;
Timeout.tv_usec = 0;
return(select(1, &readfds, NULL, NULL, &Timeout));
}
//
// Mike Muuss' in_cksum() function
// and his comments from the original
// ping program
//
// * Author -
// * Mike Muuss
// * U. S. Army Ballistic Research Laboratory
// * December, 1983
/*
* I N _ C K S U M
*
* Checksum routine for Internet Protocol family headers (C Version)
*
*/
u_short in_cksum(u_short *addr, int len)
{
register int nleft = len;
register u_short *w = addr;
register u_short answer;
register int sum = 0;
/*
* Our algorithm is simple, using a 32 bit accumulator (sum),
* we add sequential 16 bit words to it, and at the end, fold
* back all the carry bits from the top 16 bits into the lower
* 16 bits.
*/
while( nleft > 1 ) {
sum += *w++;
nleft -= 2;
}
/* mop up an odd byte, if necessary */
if( nleft == 1 ) {
u_short
u = 0;
*(u_char *)(&u) = *(u_char *)w ;
sum += u;
}
/*
* add back carry outs from top 16 bits to low 16 bits
*/
sum = (sum >> 16) + (sum & 0xffff);
/* add hi 16 to low 16 */
sum += (sum >> 16);
/* add carry */
answer = ~sum;
/* truncate to 16 bits */
return (answer);
}
第二個:使用原始套接字實現的數據包捕獲(極簡單的統計流量,嗅探器)
僅有一個函數:(僞代碼)
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET SnifferSocket = INVALID_SOCKET;
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
struct hostent *local;
char HostName[DEFAULT_NAMELEN];
struct in_addr addr;
struct sockaddr_in LocalAddr, RemoteAddr;
int addrlen = sizeof(struct sockaddr_in);
int in=0,i=0;
DWORD dwBufferLen[10];
DWORD Optval= 1 ;
DWORD dwBytesReturned = 0 ;
// 初始化套接字
//創建原始套接字
SnifferSocket = socket ( AF_INET, SOCK_RAW, IPPROTO_IP);
//獲取本機名稱
iResult = gethostname( HostName, sizeof(HostName));
//獲取本機可用IP
local = gethostbyname( HostName);
printf ("\n本機可用的IP地址爲:\n");
while (local->h_addr_list[i] != 0) {
addr.s_addr = *(u_long *) local->h_addr_list[i++];
printf("\tIP Address #%d: %s\n", i, inet_ntoa(addr));
}
printf ("\n請選擇捕獲數據待使用的接口號:");
scanf_s( "%d", &in);
memset( &LocalAddr, 0, sizeof(LocalAddr));
memcpy( &LocalAddr.sin_addr.S_un.S_addr, local->h_addr_list[in-1], sizeof(LocalAddr.sin_addr.S_un.S_addr));
LocalAddr.sin_family = AF_INET;
LocalAddr.sin_port=0;
//綁定本地地址
iResult = bind( SnifferSocket, (struct sockaddr *) &LocalAddr, sizeof(LocalAddr));
//設置套接字接收命令
iResult = WSAIoctl(SnifferSocket, SIO_RCVALL , &Optval, sizeof(Optval), &dwBufferLen, sizeof(dwBufferLen), &dwBytesReturned , NULL , NULL );
//開始接收數據
printf(" \n開始接收數據");
do
{
//接收數據
iResult = recvfrom( SnifferSocket, recvbuf, DEFAULT_BUFLEN, 0 ,(struct sockaddr *)&RemoteAddr,&addrlen);
if (iResult > 0)
printf ("\n接收到來自%s的數據包,長度爲%d.",inet_ntoa(RemoteAddr.sin_addr),iResult );
else
printf("recvfrom failed with error: %ld\n", WSAGetLastError());
} while(iResult > 0);
return 0;
}