LINUX:
網絡編程,一定離不開套接口;那什麼是套接口呢?在Linux下,所有的I/O操作都是通過讀寫文件描述符而產生的,文件描述符是一個和打開的文件相關聯的整數,這個文件並不只包括真正存儲在磁盤上的文件,還包括一個網絡連接、一個命名管道、一個終端等,而套接口就是系統進程和文件描述符通信的一種方法。目前最常用的套接口是字:字節流套接口(基於TCP)和數據報套接口(基於UDP),當然還有原始套接口(原始套接口提供TCP套接口和UDP套接口所不提供的功能,如構造自己的TCP或UDP分組)等,我們這裏主要介紹字節流套接口和數據報套接口。
要學習網絡編程,一定離不開網絡庫的函數,在Linux系統下,可以用"man 函數名"來得到這個函數的幫助,不過爲了照顧E文不大好的朋友,下面就將常用的網絡函數和用法列出來供大家參考:
1、socket函數:爲了執行網絡輸入輸出,一個進程必須做的第一件事就是調用socket函數獲得一個文件描述符。
|
第一個參數指明瞭協議簇,目前支持5種協議簇,最常用的有AF_INET(IPv4協議)和AF_INET6(IPv6協議);第二個參數指明套接口類型,有三種類型可選:SOCK_STREAM(字節流套接口)、SOCK_DGRAM(數據報套接口)和SOCK_RAW(原始套接口);如果套接口類型不是原始套接口,那麼第三個參數就爲0。
2、connect函數:當用socket建立了套接口後,可以調用connect爲這個套接字指明遠程端的地址;如果是字節流套接口,connect就使用三次握手建立一個連接;如果是數據報套接口,connect僅指明遠程端地址,而不向它發送任何數據。
|
第一個參數是socket函數返回的套接口描述字;第二和第三個參數分別是一個指向套接口地址結構的指針和該結構的大小。
這些地址結構的名字均已“sockaddr_”開頭,並以對應每個協議族的唯一後綴結束。以IPv4套接口地址結構爲例,它以“sockaddr_in”命名,定義在頭文件<netinet/in.h>;以下是結構體的內容:
|
3、bind函數:爲套接口分配一個本地IP和協議端口,對於網際協議,協議地址是32位IPv4地址或128位IPv6地址與16位的TCP或UDP端口號的組合;如指定端口爲0,調用bind時內核將選擇一個臨時端口,如果指定一個通配IP地址,則要等到建立連接後內核才選擇一個本地IP地址。
|
第一個參數是socket函數返回的套接口描述字;第二和第第三個參數分別是一個指向特定於協議的地址結構的指針和該地址結構的長度。
4、listen函數:listen函數僅被TCP服務器調用,它的作用是將用sock創建的主動套接口轉換成被動套接口,並等待來自客戶端的連接請求。
|
第一個參數是socket函數返回的套接口描述字;第二個參數規定了內核爲此套接口排隊的最大連接個數。由於listen函數第二個參數的原因,內核要維護兩個隊列:以完成連接隊列和未完成連接隊列。未完成隊列中存放的是TCP連接的三路握手爲完成的連接,accept函數是從以連接隊列中取連接返回給進程;當以連接隊列爲空時,進程將進入睡眠狀態。
5、accept函數:accept函數由TCP服務器調用,從已完成連接隊列頭返回一個已完成連接,如果完成連接隊列爲空,則進程進入睡眠狀態。
|
第一個參數是socket函數返回的套接口描述字;第二個和第三個參數分別是一個指向連接方的套接口地址結構和該地址結構的長度;該函數返回的是一個全新的套接口描述字;如果對客戶段的信息不感興趣,可以將第二和第三個參數置爲空。
6、inet_pton函數:將點分十進制串轉換成網絡字節序二進制值,此函數對IPv4地址和IPv6地址都能處理。
|
第一個參數可以是AF_INET或AF_INET6:第二個參數是一個指向點分十進制串的指針:第三個參數是一個指向轉換後的網絡字節序的二進制值的指針。
7、inet_ntop函數:和inet_pton函數正好相反,inet_ntop函數是將網絡字節序二進制值轉換成點分十進制串。
|
第一個參數可以是AF_INET或AF_INET6:第二個參數是一個指向網絡字節序的二進制值的指針;第三個參數是一個指向轉換後的點分十進制串的指針;第四個參數是目標的大小,以免函數溢出其調用者的緩衝區。
8、fock函數:在網絡服務器中,一個服務端口可以允許一定數量的客戶端同時連接,這時單進程是不可能實現的,而fock就分配一個子進程和客戶端會話,當然,這只是fock的一個典型應用。
|
fock函數調用後返回兩次,父進程返回子進程ID,子進程返回0。
有了上面的基礎知識,我們就可以進一步瞭解TCP套接口和UDP套接口
1、TCP套接口
TCP套接口使用TCP建立連接,建立一個TCP連接需要三次握手,基本過程是服務器先建立一個套接口並等待客戶端的連接請求;當客戶端調用connect進行主動連接請求時,客戶端TCP發送一個SYN,告訴服務器客戶端將在連接中發送的數據的初始序列號;當服務器收到這個SYN後也給客戶端發一個SYN,裏面包含了服務器將在同一連接中發送的數據的初始序列號;最後客戶在確認服務器發的SYN。到此爲止,一個TCP連接被建立。
下面就用二個例子來說明服務器和客戶是怎麼連接的:
例子1:
|
現在讓我們來編譯這兩個程序:
|
然後在一臺計算機上先運行服務器程序,再在另一個終端上運行客戶端就會看到結果;如果不運行服務器程序而先運行客戶程序將立即提示"Connect: Connection refused",這就是TCP套接口的好處,如果是UDP套接口將會有一個延時纔會得到錯誤信息(UDP套接口後面有介紹)。
建立一個TCP連接需要三次握手,而斷開一個TCP則需要四個分節。當某個應用進程調用close(主動端)後(可以是服務器端,也可以是客戶端),這一端的TCP發送一個FIN,表示數據發送完畢;另一端(被動端)發送一個確認,當被動端待處理的應用進程都處理完畢後,發送一個FIN到主動端,並關閉套接口,主動端接收到這個FIN後再發送一個確認,到此爲止這個TCP連接被斷開。
2、UDP套接口
UDP套接口是無連接的、不可靠的數據報協議;既然他不可靠爲什麼還要用呢?其一:當應用程序使用廣播或多播是隻能使用UDP協議;其二:由於他是無連接的,所以速度快。因爲UDP套接口是無連接的,如果一方的數據報丟失,那另一方將無限等待,解決辦法是設置一個超時。
在編寫UDP套接口程序時,有幾點要注意:建立套接口時socket函數的第二個參數應該是SOCK_DGRAM,說明是建立一個UDP套接口;由於UDP是無連接的,所以服務器端並不需要listen或accept函數;當UDP套接口調用connect函數時,內核只記錄連接放的IP地址和端口,並立即返回給調用進程,正因爲這個特性,UDP服務器程序中並不使用fock函數,用單進程就能完成所有客戶的請求。
例子2:
1.頭文件介紹
errno.h
返回錯誤信息,用的是perro(),所以頭文件有errno.h
netdb.h
定義struct hostent *gethostbyname(const char *hostname)要用的頭文件.
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
可能是網絡編程用套節字必須的吧!
這是客戶端程序,從服務器接受數據,別寫入文件中.
#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc, char *argv[])
{
int sockfd, recvbytes;
char buf[MAXDATASIZE];
FILE *fp;
struct hostent *host;
struct sockaddr_in serv_addr;
if( argc < 2)
{
fprintf(stderr, "Please enter the server's hostname!/n");
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL)
{
herror("gethostbyname出錯!");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket創建出錯!");
exit(1);
}
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr = *(struct in_addr*)host->h_addr;
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr,
sizeof(struct sockaddr)) == -1)
{
perror("connect出錯!");
exit(1);
}
if ((fp = fopen("output_file", "w")) == NULL)
{
printf ("can't open file");
exit(0);
}
while (1)
{
memset(buf, 0, 100);
recvbytes=recv(sockfd, buf, 100, 0);
if (recvbytes == 0)
{
printf("數據已經接收完!");
close(fp);
close(sockfd);
exit(0);
}
printf("%s", buf);
fprintf(fp, "%s", buf);
}
close (fp);
close(sockfd);
return 0;
}
服務器從文件讀入數據到緩衝區,再發送給客戶端.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define SERVPORT 3333
#define BACKLOG 10
#define MAXDATASIZE 100
int main()
{
int sockfd,client_fd;
char buff[MAXDATASIZE];
FILE *fp;
struct sockaddr_in my_addr;
struct sockaddr_in remote_addr;
socklen_t sin_size = sizeof(struct sockaddr_in);
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket創建出錯!");
exit(1);
}
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(SERVPORT);
my_addr.sin_addr.s_addr = inet_addr("192.168.1.211");
bzero(&(my_addr.sin_zero),8);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
== -1)
{
perror("bind出錯!");
exit(1);
}
if (listen(sockfd, BACKLOG) == -1)
{
perror("listen出錯!");
exit(1);
}
printf("server is running......../n");
if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr,
&sin_size)) == -1)
{
perror("accept出錯");
exit(1);
}
else
{
printf("客戶端已經連上/n");
}
printf("開始發送數據......./n");
if ((fp = fopen("input_file", "r")) == NULL)
{
printf ("can't open file");
exit (0);
}
while (!feof(fp))
{
if (fgets(buff, 256, fp) == NULL)
{
close(client_fd);
close(sockfd);
printf("發送完畢,退出!/n");
exit(0);
}
if (send(client_fd, buff, 100, 0) == -1)
{
perror("send出錯!");
close(client_fd);
exit(0);
}
printf ("send buff = %s", buff);
}
close(fp);
close(client_fd);
close(sockfd);
return 0;
}
這個是input_file中的信息
name:xiaoxia sex:male age:90
name:xiaoxiao sex:male age:12
name:x sex:male age:15
name:xi sex:male age:32
name:xia sex:male age:18
name:xiao sex:male age:17
name:xiaox sex:male age:16
~
大家有興趣的話可以拷貝程序試驗,需要兩個屏幕操作,結果所得到的文件output_file和input_file信息格式一樣.