電腦店電訊 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來代替就可以了sock
et為網絡通訊做基本的准備.成功時返回文件描述符,失敗時返回-1,看errno可知道出錯的詳細情況.
2.2 bind
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd:是由socket調用返回的文件描述符.
addrlen:是sockaddr結構的長度.
my_addr:是一個指向sockaddr的指針. 在<linux/socket.h>中有sockaddr的定義
struct sockaddr{
unisgned short as_family;
char sa_data[14];
};
不過由於系統的兼容性,我們一般不用這個頭文件,而使用另外一個結構(struct sock
addr_in) 來代替.在<linux/in.h>中有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,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 <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
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 <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
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中出現的號碼就用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)
在<netdb.h>中有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 <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
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而得到信息.