Linux 網絡編程
計算機網絡體系結構模式
所有的網絡通信方式分爲兩種:線路交換和包交換。
所謂的線路交換,就是指再傳輸時在發送端和接收端之間建立一個特定的線路連接,數據
可以在這條線路上傳輸。電話是採用的這種方式。
計算機網絡則採用的是包交換,數據的發送端將要傳輸的數據分割成塊,而每個塊經過適
的處理後形成一個數據包,包中有接收端的地址等必要信息,每個包單獨傳輸。包中的數據
不是限定死的,只要保證數據的正確傳輸即可,具體應該定義哪些信息,則與使用的協議有
。
OSI 標準
OSI 標準是開放系統互聯標準( Open Systerm Interconnection )即我們通常所說的網絡 互
的七層框架,他是 1977 年國際標準化組織提出的一種參考模型。值得注意的是, OSI 並沒有
供一個可以實現的方法,它不是一個標準而只是一個制定標準時使用的概念性的框架,更不
一個網絡協議。
1 、物理層( Physical Layer ):主要功能爲定義了網絡的物理結構,傳輸的電磁標準, Bi t
流的編碼及網絡的時間原則,如分時複用及分頻複用。決定了網絡連接類型(端到端或多端連
接)及物理拓撲結構。說的通俗一些,這一層主要負責實際的信號傳輸。
2 、鏈路層( Data Link Review ):在兩個主機上建立數據鏈路連接,向物理層傳輸數據信
號,並對信號進行處理使之無差錯併合理的傳輸。
3 、網絡層( Network Layer ):主要負責路由,選擇合適的路徑,進行阻塞控制等功能。
4 、傳輸層( Transfer Layer ):最關鍵的一層,向擁護提供可靠的端到端( End-to-End )
服務,它屏蔽了下層的數據通信細節,讓用戶及應用程序不需要考慮實際的通信方法。
5 、會話層( Session Layer ):主要負責兩個會話進程之間的通信,即兩個會話層實體之 間
的信息交換,管理數據的交換。
6 、表示層( Presentation Layer ):處理通信信號的表示方法,進行不同的格式之間的翻
譯,並負責數據的加密解密,數據的壓縮與恢復。
7 、應用層( Application Layer ):保持應用程序之間建立連接所需要的數據記錄,爲用 戶
服務。
在工作中,每一層會給上一層傳輸來的數據加上一個信息頭( header ),然後向下層發出,
然後通過物理介質傳輸到對方主機,對方主機每一層再對數據進行處理,把信息頭取掉,最後
還原成實際的數據。本質上,主機的通信是層與層之間的通信,而在物理上是從上向下最後通
過物理信道到對方主機再從下向上傳輸。
TCP/IP 協議
在實際應用中,最重要的是 TCP/IP ( Transport Control Protocol/Internet Protocol )
協議,它是目前最流行的商業化的協議,相對於 OSI ,它是當前的工業標準或 “ 事實的標準 ” ,
在 1974 年由 Kahn 提出的。它分爲四個層次(從高到低):應用層(與 OSI 的應用層對應),傳
輸層(與 OSI 的傳輸層對應),互聯層(與 OSI 的網絡層對應),主機 - 網絡層(與 OSI 的數據 鏈
路層和物理層對應)。
1. 應用層
應用層包括網絡應用程序和網絡進程,是與用戶交互的界面,他爲用戶提供所需要的各種
服務,包括遠程登陸、文件傳輸和電子郵件等。他的作用相當於 OSI 中的應用層及表示層和會
話層。
2. 傳輸層
相當於 OSI 中的傳輸層,他爲應用程序提供通信服務,這種通信又叫端對端通信。他有三
個主要協議:傳輸控制協議( TCP ),用戶數據包協議( UDP ),和互聯網控制消息協議( ICMP )。
TCP 協議:以建立連接高可靠性的傳輸爲目的,他負責把大量的用戶數據按一定的長度組 成
多個數據包進行發送,並在接受到數據包之後按分解順序重組和恢復用戶數據。他是一種面向
連接的可靠的雙向通信的數據流。
UDP 協議:提供無連接數據包傳輸服務,他把用戶數據分解爲多個數據包後發送給接收方。
它具有執行代碼小,系統開銷小和處理速度快等優點。
ICMP 協議:主要用於端主機和網關以及互聯網管理中心等地消息通信,以達到控制管理網絡運
行的目的。 ICMP 協議能發送出錯消息給發送數據包的端主機,還有限制流量的功能。
3. 網絡層
相當於 osi 的網絡層,使用的協議是 ip 協議。他是用來處理機器之間的通信問題,他接 受
輸層請求,傳輸某個具有目的地址信息的分組。該層把分組封裝到 ip 數據包中,填入數據 包
頭部(包頭),使用路由算法來選擇是直接把數據包發送到目標主機還是發送給路由器,然 後
數據包交給下面的網絡接口層中的對應網絡接口模塊。
4. 網絡接口層
相當於 osi 中的數據連接層和物理層。他負責接收 ip 數據包和把數據包通過選定的網絡 發
出去 , 如圖
基於 TCP 協議的客戶機 / 服務器進程圖
l Socket 編程相關函數
socket , bind , listen , accept , connect ,inet_aton , inet_ntoa, htons, htonl ,ntohs, ntohl
( 1 ) socket( 建立連接 )
表頭文件 #include<sys/socket.h>
定義函數: int socket(int family,int type,intprotocol);
函數說明: socket() 函數用來生成一個套接口描述字,也稱爲套接字,指定協議族和套接
口。
參數: family 指定協議族, type 指明字節流方式,而 protocol 一般爲 0
Family 的取值範圍:
AF_LOCAL UNIX 協議族
AF_ROUTE 路由套接口
AF_INET IPv4 協議
AF_INET6 IPv6 協議
AF_KEY 密鑰套接口
參數 type 的取值範圍:
SOCK_STREAM TCP 套接口
SOCK_DGRAM UDP 套接口
SOCK_PACKET 支持數據鏈路訪問
SOCK_RAM 原始套接口
返回值:成功返回非負描述字,失敗返回負值
( 2 ) bind (對 socket 定位)
表頭文件 #include<sys/types.h>
#include<sys/socket.h>
定義函數 int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
函數說明 bind() 用來設置給參數 sockfd 的 socket 一個名稱。此名稱由參數 my_addr 指
向一 sockaddr 結構,對於不同的 socket domain 定義了一個通用的數據結構
struct sockaddr
{
unsigned short int sa_family;
char sa_data[14];
};
sa_family 爲調用 socket ()時的 domain 參數,即 AF_xxxx 值。
sa_data 最多使用 14 個字符長度。
此 sockaddr 結構會因使用不同的 socket domain 而有不同結構定義,例如使用 AF_INET
domain ,其 socketaddr 結構定義便爲
struct socketaddr_in
{
unsigned short int sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct in_addr
{
uint32_t s_addr;
};
sin_family 即爲 sa_family
sin_port 爲使用的 port 編號
sin_addr.s_addr 爲 IP 地址
sin_zero 未使用。
參數 socket 爲套接字, my_addr 是一個指向特定協議地址結構的指針, addrlen 爲
sockaddr 的結構長度。
返回值 成功則返回 0 ,失敗返回 -1 ,錯誤原因存於 errno 中。
錯誤代碼
EBADF 參數 sockfd 非合法 socket 處理代碼。
EACCESS 權限不足
ENOTSOCK 參數 sockfd 爲一文件描述詞,非 socket 。
( 3 ) listen (等待連接)
相關函數 socket , bind , accept , connect
表頭文件 #include<sys/socket.h>
定義函數 int listen(int s,int backlog);
函數說明 listen() 用來等待參數 s 的 socket 連線。參數 backlog 指定同時能處理的最 大
連接要求,如果連接數目達此上限則 client 端將收到 ECONNREFUSED 的錯誤。 Listen() 並未開
始接收連線,只是設置 socket 爲 listen 模式,真正接收 client 端連線的是 accept() 。通常
listen() 會在 socket() , bind() 之後調用,接着才調用 accept() 。
返回值 成功則返回 0 ,失敗返回 -1 ,錯誤原因存於 errno
附加說明 listen() 只適用 SOCK_STREAM 或 SOCK_SEQPACKET 的 socket 類型。如果 socket
爲 AF_INET 則參數 backlog 最大值可設至 128 。
錯誤代碼
EBADF 參數 sockfd 非合法 socket 處理代碼
EACCESS 權限不足
EOPNOTSUPP 指定的 socket 並未支援 listen 模式。
( 4 ) accept
表頭文件 #include<sys/types.h>
#include<sys/socket.h>
定義函數 int accept(int s,struct sockaddr * addr,int * addrlen);
函數說明 accept() 用來接受參數 s 的 socket 連線。參數 s 的 socket 必需先經 bind() 、
isten() 函數處理過,當有連線進來時 accept() 會返回一個新的 socket 處理代碼,往後的數 據
傳送與讀取就是經由新的 socket 處理,而原來參數 s 的 socket 能繼續使用 accept() 來接受新
連線要求。連線成功時,參數 addr 所指的結構會被系統填入遠程主機的地址數據,參數 add rlen
爲 scokaddr 的結構長度。關於結構 sockaddr 的定義請參考 bind() 。
返回值 成功則返回新的 socket 處理代碼,失敗返回 -1 ,錯誤原因存於 errno 中。
錯誤代碼 EBADF 參數 s 非合法 socket 處理代碼。
EFAULT 參數 addr 指針指向無法存取的內存空間。
ENOTSOCK 參數 s 爲一文件描述詞,非 socket 。
EOPNOTSUPP 指定的 socket 並非 SOCK_STREAM 。
EPERM 防火牆拒絕此連線。
ENOBUFS 系統的緩衝內存不足。
ENOMEM 核心內存不足。
( 5 ) connect (建立 socket 連線)
相關函數 socket , bind , listen
表頭文件 #include<sys/types.h>
#include<sys/socket.h>
定義函數 int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
函數說明 connect() 用來將參數 sockfd 的 socket 連至參數 serv_addr 指定的網絡地
址。結構 sockaddr 請參考 bind() 。參數 addrlen 爲 sockaddr 的結構長度。
返回值 成功則返回 0 ,失敗返回 -1 ,錯誤原因存於 errno 中。
錯誤代碼 EBADF 參數 sockfd 非合法 socket 處理代碼
EFAULT 參數 serv_addr 指針指向無法存取的內存空間
ENOTSOCK 參數 sockfd 爲一文件描述詞,非 socket 。
EISCONN 參數 sockfd 的 socket 已是連線狀態
ECONNREFUSED 連線要求被 server 端拒絕。
ETIMEDOUT 企圖連線的操作超過限定時間仍未有響應。
ENETUNREACH 無法傳送數據包至指定的主機。
EAFNOSUPPORT sockaddr 結構的 sa_family 不正確。
EALREADY socket 爲不可阻斷且先前的連線操作還未完成。
( 6 ) send() 和 recv() 這兩個函數用於面向連接的 socket 上進行數據傳輸。
函數定義: int send(int sockfd, const void *msg, int len, int flags);
參數: Sockfd 是你想用來傳輸數據的 socket 描述符; msg 是一個指向要發送數據的指針; Len
是以字節爲單位的數據的長度; flags 一般情況下置爲 0 (關於該參數的用法可參照 man 手冊) 。
返回值: send() 函數返回實際上發送出的字節數,可能會少於你希望發送的數據。在程序
中應該將 send() 的返回值與欲發送的字節數進行比較。當 send() 返回值與 len 不匹配時,應 該
對這種情況進行處理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
函數定義: int recv(int sockfd,void *buf,int len,unsigned int flags);
參數: sockfd 是接受數據的 socket 描述符; buf 是存放接收數據的緩衝區; len 是緩衝 的
長度。 Flags 也被置爲 0 。
返回值: Recv() 返回實際上接收的字節數,當出現錯誤時,返回 -1 並置相應的 errno 值。
sendto() 和 recvfrom() 用於在無連接的數據報 socket 方式下進行數據傳輸。由於本地
socket 並沒有與遠端機器建立連接,所以在發送數據時應指明目的地址。
(七) inet_aton 地址變換
Int inet_aton(const char *cp, struct in_addr *inp);
Char *inet_ntoa(struct in_addr in);
inet_aton把xxx.xxx.xxx.xxx形式轉換成32位的IP,存儲在INP指針所指向的地方,inet_ntoa則是把32位IP地址轉換成XXX.XXX.XXX.XXX的格式。
(八) htons htonl ntohs ntohl字節序轉換
Htons 把unsigned short 類型從主機序轉換到網絡序
Htonl把unsigned long 類型從主機序轉換到網絡序
Ntohs 把unsigned short 類型從網絡序轉換到主機序
Ntohl 把unsigned long 類型從網絡序轉換到主機序
實例一:
// 描述:// 程序名稱: server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
void main()
{
pid_t fd;
int listensock,connsock;
char recvbuff[100]; // 定義要接收的數據緩衝區
struct sockaddr_in serveraddr; // 定義網絡套接字地址結構
listensock = socket(AF_INET,SOCK_STREAM,0); // 創建一個套接字,用於監聽
bzero(&serveraddr,sizeof(struct sockaddr)); // 地址結構清零
serveraddr.sin_family = AF_INET; // 指定使用的通訊協議族
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定接受任何連接
serveraddr.sin_port = htons(5000); // 指定監聽的端口
bind(listensock,(struct sockaddr *)&serveraddr,sizeof(structsockaddr_i
// 給套接口邦定地址
listen(listensock,1024); // 開始監聽
connsock = accept(listensock,(struct sockaddr *)NULL, NULL);
// 建立通訊的套接字, accept 函數,等待客戶端程序使用 connect 函數的連接
recv(connsock,recvbuff,sizeof(recvbuff),0); // 接收服務器的數據
printf("%s\n",recvbuff); // 打印接收到的數據
sleep(2);
close(connsock); // 關閉通訊套接字
close(listensock); // 關閉監聽套接字
}
// 描 述: 客戶端請求連接,成功後發走一段數據
// 程序名稱: client.c
// 頭文件同上
int main(int argc,char **argv)
{
pid_t fd;
// 定義要發送的數據緩衝區;
const char buff[] = "Hello! World!\r\n";
int sockfd,connsock; // 定義一個 socket 套接字,用於通訊
struct sockaddr_in serveraddr; // 定義網絡套接字地址結構
if(argc!= 2)
{
printf("Usage: echo ip 地址 ");
exit(0);
}
sockfd =socket(AF_INET,SOCK_STREAM,0); // 創建一個套接字
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET; // 指定使用的通訊協議族
serveraddr.sin_port = htons(5000); // 指定要連接的服務器的端口
inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);
// 連接服務器
connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
send(sockfd,buff,sizeof(buff), 0); // 向客戶端發送數據
close(sockfd); // 關閉套接字
return(0);
}
實例二:
//tcp_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>
#define portnumber 3333
int main(int argc, char *argv[])
{
int sockfd,new_fd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_size;
int nbytes;
char buffer[1024];
/* 服務器端開始建立sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) // AF_INET:IPV4;SOCK_STREAM:TCP
{
fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
exit(1);
}
/* 服務器端填充 sockaddr結構 */
bzero(&server_addr,sizeof(struct sockaddr_in)); // 初始化,置0
server_addr.sin_family=AF_INET; // Internet
server_addr.sin_addr.s_addr=htonl(INADDR_ANY); // (將本機器上的long數據轉化爲網絡上的long數據)和任何主機通信 //INADDR_ANY 表示可以接收任意IP地址的數據,即綁定到所有的IP
//server_addr.sin_addr.s_addr=inet_addr("192.168.1.1"); //用於綁定到一個固定IP,inet_addr用於把數字加格式的ip轉化爲整形ip
server_addr.sin_port=htons(portnumber); // (將本機器上的short數據轉化爲網絡上的short數據)端口號
/* 捆綁sockfd描述符到IP地址 */
if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
exit(1);
}
/* 設置允許連接的最大客戶端數 */
if(listen(sockfd,5)==-1) //使用最大連接數是5個連接
{
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((nbytes=read(new_fd,buffer,1024))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
exit(1);
}
buffer[nbytes]='\0';
printf("Server received %s\n",buffer);
/* 這個通訊已經結束 */
close(new_fd);
/* 循環下一個 */
}
/* 結束通訊 */
close(sockfd);
exit(0);
}
//tcp_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>
#define portnumber 3333
int main(int argc, char *argv[])
{
int sockfd;
char buffer[1024];
struct sockaddr_in server_addr;
struct hostent *host;
/* 使用hostname查詢host 名字 */
if(argc!=2)
{
fprintf(stderr,"Usage:%s hostname \a\n",argv[0]);
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL) //gethostbyname函數返回hostent結構體的信息
{
fprintf(stderr,"Gethostname error\n");
exit(1);
}
/* 客戶程序開始建立 sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) // AF_INET:Internet;SOCK_STREAM:TCP
{
fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
exit(1);
}
/* 客戶程序填充服務端的資料 */
bzero(&server_addr,sizeof(server_addr)); // 初始化,置0
server_addr.sin_family=AF_INET; // IPV4
server_addr.sin_port=htons(portnumber); // (將本機器上的short數據轉化爲網絡上的short數據)端口號
server_addr.sin_addr=*((struct in_addr *)host->h_addr); // IP地址
/* 客戶程序發起連接請求 */
if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
exit(1);
}
/* 連接成功了 */
printf("Please input char:\n");
/* 發送數據 */
fgets(buffer,1024,stdin);
write(sockfd,buffer,strlen(buffer));
/* 結束通訊 */
close(sockfd);
exit(0);
}