一、實驗目的:
本設計的功能是填充一個TCP資料包,并發送給目的主機。
二、實驗要求:
1)以指令行形式運作:SendTCP source_IP source_port dest_ip dest_port
其中,SendTCP為程式名,source_IP為源端IP位址,source_port為源端口,dest_ip 為目的IP位址,dest_port為目的端口
2)其他的TCP頭部參數請自行設定。
3)資料字段為“This is my homework of network ,I am happy!”.
4)成功發送後在螢幕上輸出“Send OK”。
三、設計相關知識:
TCP,英文全稱Transmission Control Protocol,譯為傳輸控制協定,是一種面向連接配接的,可靠的,基于比特流的運輸層通信協定。TCP協定與IP協定等一起構成目前網際網路應用最廣的TCP/IP協定族。
1、TCP的功能概述
當應用層向TCP層發送用于網間傳輸的、用8位位元組表示的資料流,TCP則把資料流分割成适當長度的封包段,最大傳輸段大小(MSS)通常受該計算機連接配接的網絡的資料鍊路層的最大傳送單元(MTU)限制。之後TCP把資料包傳給IP層,由它來通過網絡将包傳送給接收端實體的TCP層。 TCP為了保證封包傳輸的可靠,就給每個包一個序号,同時序号也保證了傳送到接收端實體的包的按序接收。然後接收端實體對已成功收到的位元組發回一個相應的确認(ACK);如果發送端實體在合理的往返時延(RTT)内未收到确認,那麼對應的資料(假設丢失了)将會被重傳。
(1)在資料正确性與合法性上,TCP用一個校驗和函數來檢驗資料是否有錯誤,在發送和接收時都要計算校驗和;同時可以使用md5認證對資料進行加密。
(2)在保證可靠性上,采用逾時重傳和捎帶确認機制。
(3)在流量控制上,采用滑動視窗協定,協定中規定,對于視窗内未經确認的分組需要重傳。
在擁塞控制上,采用廣受好評的TCP擁塞控制算法(也稱AIMD算法)。該算法主要包括三個主要部分:1)加性增、乘性減;2)慢啟動;3)對逾時事件做出反應。
2、TCP連接配接建立
TCP是網際網路中的傳輸層協定,使用三次握手協定建立連接配接。當主動方發出SYN連接配接請求後,等待對方回答SYN+ACK,并最終對對方的 SYN 執行 ACK 确認。這種建立連接配接的方法可以防止産生錯誤的連接配接,TCP使用的流量控制協定是可變大小的滑動視窗協定。
TCP三向交握的過程如下:
(1)用戶端發送SYN(SEQ=x)封包給伺服器端,進入SYN_SEND狀态。
(2)伺服器端收到SYN封包,回應一個SYN (SEQ=y)ACK(ACK=x+1)封包,進入SYN_RECV狀态。
(3)用戶端收到伺服器端的SYN封包,回應一個ACK(ACK=y+1)封包,進入Established狀态。
三次握手完成,TCP用戶端和伺服器端成功地建立連接配接,可以開始傳輸資料了。
3、TCP連接配接終止
建立一個連接配接需要三次握手,而終止一個連接配接要經過四次握手,這是由TCP的半關閉(half-close)造成的。具體過程如下圖所示。
(1) 某個應用程序首先調用close,稱該端執行“主動關閉”(active close)。該端的TCP于是發送一個FIN分節,表示資料發送完畢。
(2) 接收到這個FIN的對端執行 “被動關閉”(passive close),這個FIN由TCP确認。
注意:FIN的接收也作為一個檔案結束符(end-of-file)傳遞給接收端應用程序,放在已排隊等候該應用程序接收的任何其他資料之後,因為,FIN的接收意味着接收端應用程序在相應連接配接上再無額外資料可接收。
(3) 一段時間後,接收到這個檔案結束符的應用程序将調用close關閉它的套接字。這導緻它的TCP也發送一個FIN。
(4) 接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)确認這個FIN。
既然每個方向都需要一個FIN和一個ACK,是以通常需要4個分節。[2]
注意:
(1) “通常”是指,某些情況下,步驟1的FIN随資料一起發送,另外,步驟2和步驟3發送的分節都出自執行被動關閉那一端,有可能被合并成一個分節。
(2) 在步驟2與步驟3之間,從執行被動關閉一端到執行主動關閉一端流動資料是可能的,這稱為“半關閉”(half-close)。
(3) 當一個Unix程序無論自願地(調用exit或從main函數傳回)還是非自願地(收到一個終止本程序的信号)終止時,所有打開的描述符都被關閉,這也導緻仍然打開的任何TCP連接配接上也發出一個FIN。
無論是客戶還是伺服器,任何一端都可以執行主動關閉。通常情況是,客戶執行主動關閉,但是某些協定,例如,HTTP/1.0卻由伺服器執行主動關閉。
4、TCP如何提供可靠性
TCP提供一種面向連接配接的、可靠的位元組流服務。面向連接配接意味着兩個使用TCP的應用(通常是一個客戶和一個伺服器)在彼此交換資料包之前必須先建立一個TCP連接配接。這一過程與打電話很相似,先撥号振鈴,等待對方摘機說“喂”,然後才說明是誰。在一個TCP連接配接中,僅有兩方進行彼此通信。廣播和多點傳播不能用于TCP。
TCP通過下列方式來提供可靠性:
(1)應用資料被分割成TCP認為最适合發送的資料塊。這和UDP完全不同,應用程式産生的資料長度将保持不變。由TCP傳遞給IP的資訊機關稱為封包段或段(segment)。
(2)當TCP發出一個段後,它啟動一個定時器,等待目的端确認收到這個封包段。如果不能及時收到一個确認,将重發這個封包段。當TCP收到發自TCP連接配接另一端的資料,它将發送一個确認。TCP有延遲确認的功能,在此功能沒有打開,則是立即确認。功能打開,則由定時器觸發确認時間點。
(3)TCP将保持它首部和資料的檢驗和。這是一個端到端的檢驗和,目的是檢測資料在傳輸過程中的任何變化。如果收到段的檢驗和有差錯,TCP将丢棄這個封包段和不确認收到此封包段(希望發端逾時并重發)。
(4)既然TCP封包段作為IP資料報來傳輸,而IP資料報的到達可能會失序,是以TCP封包段的到達也可能會失序。如果必要,TCP将對收到的資料進行重新排序,将收到的資料以正确的順序交給應用層。
(5)既然IP資料報會發生重複,TCP的接收端必須丢棄重複的資料。
(6)TCP還能提供流量控制。TCP連接配接的每一方都有固定大小的緩沖空間。TCP的接收端隻允許另一端發送接收端緩沖區所能接納的資料。這将防止較快主機緻使較慢主機的緩沖區溢出。
兩個應用程式通過TCP連接配接交換8bit位元組構成的位元組流。TCP不在位元組流中插入記錄辨別符。我們将這稱為位元組流服務(bytestreamservice)。如果一方的應用程式先傳10位元組,又傳20位元組,再傳50位元組,連接配接的另一方将無法了解發方每次發送了多少位元組。隻要自己的接收緩存沒有塞滿,TCP 接收方将有多少就收多少。一端将位元組流放到TCP連接配接上,同樣的位元組流将出現在TCP連接配接的另一端。
另外,TCP對位元組流的内容不作任何解釋。TCP不知道傳輸的資料位元組流是二進制資料,還是ASCⅡ字元、EBCDIC字元或者其他類型資料。對位元組流的解釋由TCP連接配接雙方的應用層解釋。
這種對位元組流的處理方式與Unix作業系統對檔案的處理方式很相似。Unix的核心對一個應用讀或寫的内容不作任何解釋,而是交給應用程式處理。對Unix的核心來說,它無法區分一個二進制檔案與一個文本檔案。
5、TCP首部
TCP的首部格式圖下圖所示:
---Source Port是源端口,16位。
---Destination Port是目的端口,16位。
---Sequence Number是發送資料包中的第一個位元組的序列号,32位。
---Acknowledgment Number是确認序列号,32位。
---Data Offset是資料偏移,4位,該字段的值是TCP首部(包括選項)長度除以4。
---标志位: 6位,URG表示Urgent Pointer字段有意義:
ACK表示Acknowledgment Number字段有意義
PSH表示Push功能,RST表示複位TCP連接配接
SYN表示SYN封包(在建立TCP連接配接的時候使用)
FIN表示沒有資料需要發送了(在關閉TCP連接配接的時候使用)
Window表示接收緩沖區的空閑空間,16位,用來告訴TCP連接配接對端自己能夠接收的最大資料 長度。
---Checksum是校驗和,16位。
---Urgent Pointers是緊急指針,16位,隻有URG标志位被設定時該字段才有意義,表示緊急資料相對序列号(Sequence Number字段的值)的偏移。
四、實驗設計分析:
本課程設計的目标是發送一個TCP資料包,可以利用原始套接字來完成這個工作。整個程式由初始化原始套接字和發送TCP資料包兩個部分組成。
1、建立一個原始套接字,并設定IP頭選項
SOCKET sock;
sock = socket(AF_INET,SOCK_RAW,IPPROTO_IP);
或者:
sock=WSASoccket(AF_INET,SOCK_RAW,IPPROTO_IP,NULL,0,WSA_FLAG_OVERLAPPED);
這裡,我們設定了SOCK_RAW标志,表示我們聲明的是一個原始套接字類型。為使用發送接收逾時設定,必須将标志位置位置為WSA_FLAG_OVERLAPPED。在本課程設計中,發送TCP包時隐藏了自己的IP位址,是以我們要自己填充IP頭,設定IP頭操作選項。其中flag設定為ture,并設定 IP_HDRINCL 選項,表明自己來構造IP頭。注意,如果設定IP_HDRINCL 選項,那麼必須具有 administrator權限,要不就必須修改系統資料庫:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Afd\Parameter\
修改鍵:DisableRawSecurity(類型為DWORD),把值修改為 1。如果沒有,就添加。
BOOL Flag=TRUE;
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char *)&Flag, sizeof(Flag));
int timeout=1000;
setsockopt(sock, SOL_SOCKET,SO_SNDTIMEO,(char*)&timeout, sizeof(timeout));
在這裡我們使用基本套接字SOL_SOCKET,設定SO_SNDTIMEO表示使用發送逾時設定,逾時時間設定為1000ms。
2、構造IP頭和TCP頭
這裡, IP頭和TCP頭以及TCP僞部的構造請參考下面它們的資料結構。
3、計算校驗和的子函數
在填充資料包的過程中,需要調用計算校驗和的函數checksum兩次,分别用于校驗IP頭和TCP頭部(加上僞頭部),其實作代碼如下:
USHORT checksum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while(size >1)
{
cksum+=*buffer++;
size -=sizeof(USHORT);
}
if(size )
{
cksum += *(UCHAR*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (USHORT)(~cksum);
}
五、程式流程圖:
六、具體程式代碼:
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <time.h>
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#include <iostream.h>
#pragma comment(lib,"ws2_32.lib")
#define IPVER 4 //IP協定預定
#define MAX_BUFF_LEN 65500 //發送緩沖區最大值
typedef struct ip_hdr //定義IP首部
{
UCHAR h_verlen; //4位首部長度,4位IP版本号
UCHAR tos; //8位服務類型TOS
USHORT total_len; //16位總長度(位元組)
USHORT ident; //16位辨別
USHORT frag_and_flags; //3位标志位
UCHAR ttl; //8位生存時間 TTL
UCHAR proto; //8位協定 (TCP, UDP 或其他)
USHORT checksum; //16位IP首部校驗和
ULONG sourceIP; //32位源IP位址
ULONG destIP; //32位目的IP位址
}IP_HEADER;
typedef struct tsd_hdr //定義TCP僞首部
{
ULONG saddr; //源位址
ULONG daddr; //目的位址
UCHAR mbz; //沒用
UCHAR ptcl; //協定類型
USHORT tcpl; //TCP長度
}PSD_HEADER;
typedef struct tcp_hdr //定義TCP首部
{
USHORT th_sport; //16位源端口
USHORT th_dport; //16位目的端口
ULONG th_seq; //32位序列号
ULONG th_ack; //32位确認号
UCHAR th_lenres; //4位首部長度/6位保留字
UCHAR th_flag; //6位标志位
USHORT th_win; //16位視窗大小
USHORT th_sum; //16位校驗和
USHORT th_urp; //16位緊急資料偏移量
}TCP_HEADER;
//CheckSum:計算校驗和的子函數
USHORT checksum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while(size >1)
{
cksum+=*buffer++;
size -=sizeof(USHORT);
}
if(size)
{
cksum += *(UCHAR*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (USHORT)(~cksum);
}
int main(int argc, char* argv[])
{
WSADATA WSAData;
SOCKET sock;
IP_HEADER ipHeader;
TCP_HEADER tcpHeader;
PSD_HEADER psdHeader;
char Sendto_Buff[MAX_BUFF_LEN]; //發送緩沖區
unsigned short check_Buff[MAX_BUFF_LEN]; //檢驗和緩沖區
const char tcp_send_data[]={"This is my homework of networt,I am happy!"};
BOOL flag;
int rect,nTimeOver;
if (argc!= 5)
{
printf("Useage: SendTcp soruce_ip source_port dest_ip dest_port \n");
return false;
}
if (WSAStartup(MAKEWORD(2,2), &WSAData)!=0)
{
printf("WSAStartup Error!\n");
return false;
}
if((sock=WSASocket(AF_INET,SOCK_RAW,IPPROTO_RAW,NULL,0,WSA_FLAG_OVERLAPPED))==INVALID_SOCKET)
{
printf("Socket Setup Error!\n");
return false;
}
flag=true;
if(setsockopt(sock,IPPROTO_IP,IP_HDRINCL,(char*)&flag,sizeof(flag))==SO CKET_ERROR)
{
printf("setsockopt IP_HDRINCL error!\n");
return false;
}
nTimeOver=1000;
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&nTimeOver, sizeof(nTimeOver))==SOCKET_ERROR)
{
printf("setsockopt SO_SNDTIMEO error!\n");
return false;
}
//填充IP首部
ipHeader.h_verlen=(IPVER<<4 | sizeof(ipHeader)/sizeof(unsigned long));
ipHeader.tos=(UCHAR)0;
ipHeader.total_len=htons((unsignedshort)sizeof(ipHeader)+sizeof(tcpHeader)+sizeof(tcp_send_data));
ipHeader.ident=0; //16位辨別
ipHeader.frag_and_flags=0; //3位标志位
ipHeader.ttl=128; //8位生存時間
ipHeader.proto=IPPROTO_UDP; //協定類型
ipHeader.checksum=0; //檢驗和暫時為0
ipHeader.sourceIP=inet_addr(argv[1]); //32位源IP位址
ipHeader.destIP=inet_addr(argv[3]); //32位目的IP位址
//計算IP頭部檢驗和
memset(check_Buff,0,MAX_BUFF_LEN);
memcpy(check_Buff,&ipHeader,sizeof(IP_HEADER));
ipHeader.checksum=checksum(check_Buff,sizeof(IP_HEADER));
//構造TCP僞首部
psdHeader.saddr=ipHeader.sourceIP;
psdHeader.daddr=ipHeader.destIP;
psdHeader.mbz=0;
psdHeader.ptcl=ipHeader.proto;
psdHeader.tcpl=htons(sizeof(TCP_HEADER)+sizeof(tcp_send_data));
//填充TCP首部
tcpHeader.th_dport=htons(atoi(argv[4])); //16位目的端口号
tcpHeader.th_sport=htons(atoi(argv[2])); //16位源端口号
tcpHeader.th_seq=0; //SYN序列号
tcpHeader.th_ack=0; //ACK序列号置為0
//TCP長度和保留位
tcpHeader.th_lenres=(sizeof(tcpHeader)/sizeof(unsigned long)<<4|0);
tcpHeader.th_flag=2; //修改這裡來實作不同的标志位探測,2是SYN,1是//FIN,16是ACK探測 等等
tcpHeader.th_win=htons((unsigned short)16384); //視窗大小
tcpHeader.th_urp=0; //偏移大小
tcpHeader.th_sum=0; //檢驗和暫時填為0
//計算TCP校驗和
memset(check_Buff,0,MAX_BUFF_LEN);
memcpy(check_Buff,&psdHeader,sizeof(psdHeader));
memcpy(check_Buff+sizeof(psdHeader),&tcpHeader,sizeof(tcpHeader));
memcpy(check_Buff+sizeof(PSD_HEADER)+sizeof(TCP_HEADER),tcp_send_data,sizeof(tcp_send_data));
tcpHeader.th_sum=checksum(check_Buff,sizeof(PSD_HEADER)+sizeof(TCP_HEADER)+sizeof(tcp_send_data));
//填充發送緩沖區
memset(Sendto_Buff,0,MAX_BUFF_LEN);
memcpy(Sendto_Buff,&ipHeader,sizeof(IP_HEADER));
memcpy(Sendto_Buff+sizeof(IP_HEADER),&tcpHeader,sizeof(TCP_HEADER));
memcpy(Sendto_Buff+sizeof(IP_HEADER)+sizeof(TCP_HEADER),
tcp_send_data,sizeof(tcp_send_data));
int datasize=sizeof(IP_HEADER)+sizeof(TCP_HEADER)+sizeof(tcp_send_data);
//發送資料報的目的位址
SOCKADDR_IN dest;
memset(&dest,0,sizeof(dest));
dest.sin_family=AF_INET;
dest.sin_addr.s_addr=inet_addr(argv[3]);
dest.sin_port=htons(atoi(argv[4]));
rect=sendto(sock,Sendto_Buff,datasize, 0,(struct sockaddr*)&dest, sizeof(dest));
if (rect==SOCKET_ERROR)
{
printf("send error!:%d\n",WSAGetLastError());
return false;
}
else
printf("send ok!\n");
closesocket(sock);
WSACleanup();
return 1;
}
七、試驗小結:
這次的課程設計讓我學到了很多東西。我最大的收獲是學會了TCP資料包的填充和發送。在對TCP資料包進行填充時,首先需要我們去充分了解它的資料結構,在這個過程中可以了解相應位元組上應該存放的内容和它們的功能。在實作TCP資料包的發送中,我第一次深深接觸了網絡程式設計接口socket套接字的相關知識,雖然本次的課程設計隻用到了其中一小部分知識,但這并不會影響到我對這方面知識的全面了解。總之這次的課程設計,讓我對網絡中的資料收發有了一定的了解,并激發了我對計算機網絡的濃厚興趣。