很全的linux網絡編程技巧


1. LINUX網絡編程基礎知識 1

1.1. TCP/IP協議概述 1

1.2. OSI參考模型及TCP/IP參考模型 1

1.3. TCP協議 3

1.4. UDP協議 5

1.5. 協議的選擇 6

2. 網絡相關概念 6

2.1. socket概念 7

2.2. socket類型 8

2.3. socket信息數據結構 8

2.4. 數據存儲優先順序的轉換 8

2.5. 地址格式轉化 9

2.6. 名字地址轉化 10

3. socket編程 13

3.1. 使用TCP協議的流程圖 13

3.2. 使用UDP協議的流程圖 24

3.3. 設置套接口的選項setsockopt的用法 31

3.4. 單播、廣播、組播(多播) 32

 

1. LINUX網絡編程基礎知識

1.1. TCP/IP協議概述

協議protocol:通信雙方必須遵循的規矩 由iso規定  rpc文檔

osi參考模型:(應-表-會-傳-網-數-物)

è 應用層 表示層 會話層 傳輸層 網絡層 數據鏈路層 物理層

tcp/ip模型4層:

應用層{http超文本傳輸協議   ftp文件傳輸協議  telnet遠程登錄   ssh安全外殼協議  stmp簡單郵件發送   pop3收郵件}

傳輸層{tcp傳輸控制協議,udp用戶數據包協議}

網絡層{ip網際互聯協議 icmp網絡控制消息協議 igmp網絡組管理協議}

網絡接口層{arp地址轉換協議,rarp反向地址轉換協議,mpls多協議標簽交換}

TCP協議:傳輸控制協議 面向連接的協議 能保證傳輸安全可靠 速度慢(有3次握手)

UDP協議:用戶數據包協議 非面向連接  速度快 不可靠

通常是ip地址后面跟上端口號:ip用來定位主機  port區別應用(進程)

http的端口號80  ssh-->22  telnet-->23  ftp-->21  用戶自己定義的通常要大於1024

1.2. OSI參考模型及TCP/IP參考模型

 TCP/IP協議族的每一層的作用:

·網絡接口層:負責將二進制流轉換為數據幀,並進行數據幀的發送和接收。要注意的是數據幀是獨立的網絡信息傳輸單元。

·網絡層:負責將數據幀封裝成IP數據報,並運行必要的路由算法。

·傳輸層:負責端對端之間的通信會話連接和建立。傳輸協議的選擇根據數據傳輸方式而定。

·應用層:負責應用程序的網絡訪問,這里通過端口號來識別各個不同的進程。

 

TCP/IP協議族的每一層協議的相關注解:

·ARP:(地址轉換協議)用於獲得同一物理網絡中的硬件主機地址。

·MPLS:(多協議標簽交換)很有發展前景的下一代網絡協議。

·IP:(網際互聯協議)負責在主機和網絡之間尋址和路由數據包。

·ICMP:(網絡控制消息協議)用於發送報告有關數據包的傳送錯誤的協議。

·IGMP:(網絡組管理協議)被IP主機用來向本地多路廣播路由器報告主機組成員的協議。

·TCP:(傳輸控制協議)為應用程序提供可靠的通信連接。適合於一次傳輸大批數據的情況。並適用於要求得到相應的應用程序。

·UDP:(用戶數據包協議)提供了無連接通信,且不對傳送包進行可靠的保證。適合於一次傳輸少量數據。

 1.3. TCP協議

(1) 概述

TCPTCP/IP體系中面向連接的運輸層協議,它提供全雙工和可靠交付的服務。它采用許多機制來確保端到端結點之間的可靠數據傳輸,如采用序列號、確認重傳、滑動窗口等。

首先,TCP要為所發送的每一個報文段加上序列號,保證每一個報文段能被接收方接收,並只被正確的接收一次。

其次,TCP采用具有重傳功能的積極確認技術作為可靠數據流傳輸服務的基礎。這里“確認”是指接收端在正確收到報文段之后向發送端回送一個確認(ACK)信息。發送方將每個已發送的報文段備份在自己的緩沖區里,而且在收到相應的確認之前是不會丟棄所保存的報文段的。“積極”是指發送發在每一個報文段發送完畢的同時啟動一個定時器,加入定時器的定時期滿而關於報文段的確認信息還沒有達到,則發送發認為該報文段已經丟失並主動重發。為了避免由於網絡延時引起遲到的確認和重復的確認,TCP規定在確認信息中捎帶一個報文段的序號,使接收方能正確的將報文段與確認聯系起來。

最后,采用可變長的滑動窗口協議進行流量控制,以防止由於發送端與接收端之間的不匹配而引起的數據丟失。這里所采用的滑動窗口協議與數據鏈路層的滑動窗口協議在工作原理上完全相同,唯一的區別在於滑動窗口協議用於傳輸層是為了在端對端節點之間實現流量控制,而用於數據鏈路層是為了在相鄰節點之間實現流量控制。TCP采用可變長的滑動窗口,使得發送端與接收端可根據自己的CPU和數據緩存資源對數據發送和接收能力來進行動態調整,從而靈活性更強,也更合理。

(2) 三次握手協議

在利用TCP實現源主機和目的主機通信時,目的主機必須同意,否則TCP連接無法建立。為了確保TCP連接的成功建立,TCP采用了一種稱為三次握手的方式,三次握手方式使得“序號/確認號”系統能夠正常工作,從而使它們的序號達成同步。如果三次握手成功,則連接建立成功,可以開始傳送數據信息。

其三次握手分別為:

1)源主機ATCP向主機B發送連接請求報文段,其首部中的SYN(同步)標志位應置為1,表示想跟目標主機B建立連接,進行通信,並發送一個同步序列號X(例:SEQ=100)進行同步,表明在后面傳送數據時的第一個數據字節的序號為X+1(即101)。

2)目標主機BTCP收到連接請求報文段后,如同意,則發回確認。再確認報中應將ACK位和SYN位置為1.確認號為X+1,同時也為自己選擇一個序號Y

3)源主機ATCP收到目標主機B的確認后要想目標主機B給出確認。其ACK置為1,確認號為Y+1,而自己的序號為X+1TCP的標准規定,SYN1的報文段要消耗掉一個序號。

運行客戶進程的源主機ATCP通知上層應用進程,連接已經建立。當源主機A向目標主機B發送第一個數據報文段時,其序號仍為X+1,因為前一個確認報文段並不消耗序號。

當運行服務進程的目標主機BTCP收到源主機A的確認后,也通知其上層應用進程,連接已經建立。至此建立了一個全雙工的連接。

 

三次握手:為應用程序提供可靠的通信連接適合於一次傳輸大批數據的情況。並適用於要求得到響應的應用程序。 

(3) TCP數據報頭

TCP頭信息

 

·源端口、目的端口:16位長。標識出遠端和本地的端口號。

·序號:32位長。標識發送的數據報的順序。

·確認號:32位長。希望收到的下一個數據報的序列號。

·TCP頭長:4位長。表明TCP頭中包含多少個32位字。

·6位未用。

·ACKACK位置1表明確認號是合法的。如果ACK0,那么數據報不包含確認信息,確認字段被省略。

·PSH:表示是帶有PUSH標志的數據。接收方因此請求數據報一到便可送往應用程序而不必等到緩沖區裝滿時才發送。

·RST:用於復位由於主機崩潰或其他原因而出現的錯誤的連接。還可以用於拒絕非法的數據報或拒絕連接請求。

·SYN:用於建立連接。

·FIN:用於釋放連接。

·窗口大小:16位長。窗口大小字段表示在確認了字節之后還可以發送多少個字節。

·校驗和:16位長。是為了確保高可靠性而設置的。它校驗頭部、數據和偽TCP頭部之和。

·可選項:0個或多個32位字。包括最大TCP載荷,窗口比例、選擇重復數據報等選項。

1.4. UDP協議

1) 概述

UDP用戶數據報協議,它是一種無連接協議,因此不需要像TCP那樣通過三次握手來建立一個連接。同時,一個UDP應用可同時作為應用的客戶或服務器方。由於UDP協議並不需要建立一個明確的連接,因此建立UDP應用要比建立TCP應用簡單得多。

TCP協議更為高效,也能更好地解決實時性的問題。如今,包括網絡視頻會議系統在內的眾多的客戶/服務器模式的網絡應用都使用UDP協議。 

2Udp數據包頭格式

 1.5. 協議的選擇

1)對數據可靠性的要求

對數據要求高可靠性的應用需選擇TCP協議,如驗證、密碼字段的傳送都是不允許出錯的,而對數據的可靠性要求不那么高的應用可選擇UDP傳送。

2)應用的實時性

TCP協議在傳送過程中要使用三次握手、重傳確認等手段來保證數據傳輸的可靠性。使用TCP協議會有較大的時延,因此不適合對實時性要求較高的應用,如VOIP、視頻監控等。相反,UDP協議則在這些應用中能發揮很好的作用。

3)網絡的可靠性

由於TCP協議的提出主要是解決網絡的可靠性問題,它通過各種機制來減少錯誤發生的概率。因此,在網絡狀況不是很好的情況下需選用TCP協議(如在廣域網等情況),但是若在網絡狀況很好的情況下(如局域網等)就不需要再采用TCP協議,而建議選擇UDP協議來減少網絡負荷。 

2. 網絡相關概念

1)套接口的概念:

套接口,也叫“套接字”。是操作系統內核中的一個數據結構,它是網絡中的節點進行相互通信的門戶。它是網絡進程的ID。網絡通信,歸根到底還是進程間的通信(不同計算機上的進程間通信)。在網絡中,每一個節點(計算機或路由)都有一個網絡地址,也就是IP地址。兩個進程通信時,首先要確定各自所在的網絡節點的網絡地址。但是,網絡地址只能確定進程所在的計算機,而一台計算機上很可能同時運行着多個進程,所以僅憑網絡地址還不能確定到底是和網絡中的哪一個進程進行通信,因此套接口中還需要包括其他的信息,也就是端口號(PORT)。在一台計算機中,一個端口號一次只能分配給一個進程,也就是說,在一台計算機中,端口號和進程之間是一一對應關系。所以,使用端口號和網絡地址的組合可以唯一的確定整個網絡中的一個網絡進程。

例如,如網絡中某一台計算機的IP為10.92.20.160,操作系統分配給計算機中某一應用程序進程的端口號為1500,則此時 10.92.20.160  1500就構成了一個套接口。

2)端口號的概念:

在網絡技術中,端口大致有兩種意思:一是物理意義上的端口,如集線器、交換機、路由器等用於連接其他網絡設備的接口。二是指TCP/IP協議中的端口,端口號的范圍從0~65535,一類是由互聯網指派名字和號碼公司ICANN負責分配給一些常用的應用程序固定使用的“周知的端口”,其值一般為0~1023.例如http的端口號是80,ftp為21,ssh為22,telnet為23等。還有一類是用戶自己定義的,通常是大於1024的整型值。

3)ip地址的表示:

通常用戶在表達IP地址時采用的是點分十進制表示的數值(或者是為冒號分開的十進制Ipv6地址),而在通常使用的socket編程中使用的則是二進制值,這就需要將這兩個數值進行轉換。

ipv4地址:32bit, 4字節,通常采用點分十進制記法。

例如對於:10000000 00001011 00000011 00011111

點分十進制表示為:128.11.3.31

ip地址的分類:

特殊的ip地址:

 

2.1. socket概念

Linux中的網絡編程是通過socket接口來進行的。socket是一種特殊的I/O接口,它也是一種文件描述符。它是一種常用的進程之間通信機制,通過它不僅能實現本地機器上的進程之間的通信,而且通過網絡能夠在不同機器上的進程之間進行通信。

每一個socket都用一個半相關描述{協議、本地地址、本地端口}來表示一個完整的套接字則用一個相關描述{協議、本地地址、本地端口、遠程地址、遠程端口}來表示socket也有一個類似於打開文件的函數調用,該函數返回一個整型的socket描述符,隨后的連接建立、數據傳輸等操作都是通過socket來實現的

2.2. socket類型 

1流式socketSOCK_STREAM à用於TCP通信

流式套接字提供可靠的、面向連接的通信流;它使用TCP協議,從而保證了數據傳輸的正確性和順序性

2數據報socketSOCK_DGRAM à用於UDP通信

數據報套接字定義了一種無連接的服務數據通過相互獨立的報文進行傳輸是無序的並且不保證是可靠、無差錯的。使用數據報協議UDP

3原始socket SOCK_RAW à用於新的網絡協議實現的測試等

原始套接字允許對底層協議如IPICMP進行直接訪問,它功能強大但使用較為不便,主要用於一些協議的開發 

2.3. socket信息數據結構

struct sockaddr

{

     unsigned short sa_family; /*地址族*/

     char sa_data[14]; /*14字節的協議地址,包含該socketIP地址和端口號。*/

};

struct sockaddr_in

{

     short int sa_family; /*地址族*/

     unsigned short int sin_port; /*端口號*/

     struct in_addr sin_addr; /*IP地址*/

     unsigned char sin_zero[8]; /*填充0 以保持與struct sockaddr同樣大小*/

};

struct in_addr

{

unsigned long int  s_addr; /* 32IPv4地址,網絡字節序 */

};

頭文件<netinet/in.h>

sa_family:AF_INET  àIPv4協議   AF_INET6  àIPv6協議

2.4. 數據存儲優先順序的轉換

計算機數據存儲有兩種字節優先順序:高位字節優先(稱為大端模式)和低位字節優先(稱為小端模式)。內存的低地址存儲數據的低字節,高地址存儲數據的高字節的方式叫小端模式。內存的高地址存儲數據的低字節,低地址存儲數據高字節的方式稱為大端模式。

eg:對於內存中存放的數0x12345678來說

如果是采用大端模式存放的,則其真實的數是:0x12345678

如果是采用小端模式存放的,則其真實的數是:0x78563412

如果稱某個系統所采用的字節序為主機字節序,則它可能是小端模式的,也可能是大端模式的。而端口號和IP地址都是以網絡字節序存儲的,不是主機字節序,網絡字節序都是大端模式。要把主機字節序和網絡字節序相互對應起來,需要對這兩個字節存儲優先順序進行相互轉化。這里用到四個函數:htons(),ntohs(),htonl()和ntohl().這四個地址分別實現網絡字節序和主機字節序的轉化,這里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口號用s代表,而IP地址用l來代表。

函數原型如下:

 

2.5. 地址格式轉化

通常用戶在表達地址時采用的是點分十進制表示的數值(或者是為冒號分開的十進制Ipv6地址),而在通常使用的socket編程中使用的則是32位的網絡字節序的二進制值,這就需要將這兩個數值進行轉換。這里在Ipv4中用到的函數有inet_aton()、inet_addr()和inet_ntoa(),而IPV4和Ipv6兼容的函數有inet_pton()和inet_ntop()。

IPv4的函數原型:

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

int inet_aton(const char *straddr, struct in_addr *addrptr);

char *inet_ntoa(struct in_addr inaddr);

in_addr_t inet_addr(const char *straddr);

函數inet_aton():將點分十進制數的IP地址轉換成為網絡字節序的32位二進制數值。返回值:成功,則返回1,不成功返回0.

參數straddr:存放輸入的點分十進制數IP地址字符串。

參數addrptr:傳出參數,保存網絡字節序的32位二進制數值。

函數inet_ntoa():將網絡字節序的32位二進制數值轉換為點分十進制的IP地址。

函數inet_addr():功能與inet_aton相同,但是結果傳遞的方式不同。inet_addr()若成功則返回32位二進制的網絡字節序地址。

IPv4IPv6的函數原型:

#include <arpa/inet.h>

int inet_pton(int family, const char *src, void *dst);

const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);

函數inet_pton跟inet_aton實現的功能類似,只是多了family參數,該參數指定為AF_INET,表示是IPv4協議,如果是AF_INET6,表示IPv6協議。

函數inet_ntop跟inet_ntoa類似,其中len表示表示轉換之后的長度(字符串的長度)。

Example:

#include <stdio.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

int main()

{

char ip[] = "192.168.0.101";

struct in_addr myaddr;

/* inet_aton */

int iRet = inet_aton(ip, &myaddr);

printf("%x\n", myaddr.s_addr);

 

/* inet_addr */

printf("%x\n", inet_addr(ip));

 

/* inet_pton */

iRet = inet_pton(AF_INET, ip, &myaddr);

printf("%x\n", myaddr.s_addr);

 

myaddr.s_addr = 0xac100ac4;

/* inet_ntoa */

printf("%s\n", inet_ntoa(myaddr));

 

/* inet_ntop */

inet_ntop(AF_INET, &myaddr, ip, 16);

puts(ip);

return 0;

}

2.6. 名字地址轉化

通常,人們在使用過程中都不願意記憶冗長的IP地址,尤其到Ipv6時,地址長度多達128位,那時就更加不可能一次性記憶那么長的IP地址了。因此,使用主機名或域名將會是很好的選擇。主機名與域名的區別:主機名通常在局域網里面使用,通過/etc/hosts文件,主機名可以解析到對應的ip;域名通常是再internet上使用。

眾所周知,百度的域名為:www.baidu.com,而這個域名其實對應了一個百度公司的IP地址,那么百度公司的IP地址是多少呢?我們可以利用ping www.baidu.com來得到百度公司的ip地址,如圖。那么,系統是如何將www.baidu.com 這個域名轉化為IP地址220.181.111.148的呢?

 

linux中,有一些函數可以實現主機名和地址的轉化,最常見的有gethostbyname()、gethostbyaddr()等,它們都可以實現IPv4和IPv6的地址和主機名之間的轉化。其中gethostbyname()是將主機名轉化為IP地址,gethostbyaddr()則是逆操作,是將IP地址轉化為主機名。

函數原型:

#include <netdb.h>

struct hostent* gethostbyname(const char* hostname);

struct hostent* gethostbyaddr(const char* addr, size_t len, int family);

結構體:

struct hostent

{

     char *h_name; /*正式主機名*/

     char **h_aliases; /*主機別名*/

     int h_addrtype; /*主機IP地址類型 IPv4為AF_INET*/

     int h_length; /*主機IP地址字節長度,對於IPv4是4字節,即32位*/

     char **h_addr_list; /*主機的IP地址列表*/

}

#define  h_addr  h_addr_list[0] /*保存的是ip地址*/

函數gethostbyname():用於將域名(www.baidu.com)或主機名轉換為IP地址。參數hostname指向存放域名或主機名的字符串。

函數gethostbyaddr():用於將IP地址轉換為域名或主機名。參數addr是一個IP地址,此時這個ip地址不是普通的字符串,而是要通過函數inet_aton()轉換。len為IP地址的長度,AF_INET為4。family可用AF_INET:Ipv4或AF_INET6:Ipv6。

 

Example1:將百度的www.baidu.com 轉換為ip地址

#include <netdb.h>

#include <sys/socket.h>

#include <stdio.h>

int main(int argc, char **argv)

{

    char *ptr, **pptr;

    struct hostent *hptr;

    char str[32] = {'\0'};

/* 取得命令后第一個參數,即要解析的域名或主機名 */

    ptr = argv[1];  //www.baidu.com

/* 調用gethostbyname()。結果存在hptr結構中 */

    if((hptr = gethostbyname(ptr)) == NULL)

    {

        printf(" gethostbyname error for host:%s\n", ptr);

        return 0;

    }

/* 將主機的規范名打出來 */

    printf("official hostname:%s\n", hptr->h_name);

/* 主機可能有多個別名,將所有別名分別打出來 */

    for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)

        printf(" alias:%s\n", *pptr);

/* 根據地址類型,將地址打出來 */

    switch(hptr->h_addrtype)

    {

  case AF_INET:

  case AF_INET6:

  pptr = hptr->h_addr_list;

  /* 將剛才得到的所有地址都打出來。其中調用了inet_ntop()函數 */

  for(; *pptr!=NULL; pptr++)

  printf(" address:%s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));

  printf(" first address: %s\n", inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));

        break;

  default:

  printf("unknown address type\n");

       break;

    }

    return 0;

}

編譯運行

#gcc test.c

#./a.out www.baidu.com

official hostname:www.a.shifen.com

alias:www.baidu.com

address: 220.181.111.148

……

first address: 220.181.111.148

(注:這里需要聯網才能訪問www.baidu.com。可以嘗試用自己的虛擬機的主機名,利用命令hostname可以查看自己的主機名,用hostname –i可以查看主機名對應的ip地址。那么如何修改主機名呢?直接用hostname wangxiao只是暫時有效,重啟之后就沒有了,想要永久有效,需要修改/etc/sysconfig/networkHOSTNAME修改,當然要重啟虛擬機。如果ip地址不對,你可以修改/etc/hosts這個文件,添加你自己的主機名對應的ip地址)

3. socket編程

3.1. 使用TCP協議的流程圖

TCP通信的基本步驟如下:

服務端:socket---bind---listen---while(1){---accept---recv---send---close---}---close

客戶端:socket----------------------------------connect---send---recv-----------------close

 

服務器端

頭文件包含:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

 

2. socket函數:生成一個套接口描述符。

    原型:int socket(int domain,int type,int protocol);

參數:domainà{ AF_INETIpv4網絡協議   AF_INET6IPv6網絡協議}

  typeàtcpSOCK_STREAM   udpSOCK_DGRAM}

  protocolà指定socket所使用的傳輸協議編號。通常為0.

返回值:成功則返回套接口描述符,失敗返回-1

常用實例:int sfd = socket(AF_INET, SOCK_STREAM, 0);

      if(sfd == -1){perror("socket");exit(-1);}

 

3. bind函數:用來綁定一個端口號和IP地址,使套接口與指定的端口號和IP地址相關聯。

原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);

參數:sockfdà為前面socket的返回值。

  my_addrà為結構體指針變量

對於不同的socket domain定義了一個通用的數據結構 struct sockaddr  //此結構體不常用 {     unsigned short int sa_family;  //調用socket()時的domain參數,即AF_INET值。     char sa_data[14];  //最多使用14個字符長度 }; sockaddr結構會因使用不同的socket domain而有不同結構定義, 例如使用AF_INET domain,其socketaddr結構定義便為 struct sockaddr_in  //常用的結構體 {     unsigned short int sin_family;  //即為sa_family èAF_INET     uint16_t sin_port;  //為使用的port編號     struct in_addr sin_addr;  //IP 地址     unsigned char sin_zero[8];  //未使用 }; struct in_addr {     uint32_t s_addr; }; addrlenàsockaddr的結構長度。通常是計算sizeof(struct sockaddr);

返回值成功則返回0,失敗返回-1

常用實例:struct sockaddr_in my_addr;  //定義結構體變量

  memset(&my_addr, 0, sizeof(struct sockaddr)); //將結構體清空

  //bzero(&my_addr, sizeof(struct sockaddr));

  my_addr.sin_family = AF_INET;  //表示采用Ipv4網絡協議

  my_addr.sin_port = htons(8888);  //表示端口號為8888,通常是大於1024的一個值。

//htons()用來將參數指定的16hostshort轉換成網絡字符順序

my_addr.sin_addr.s_addr = inet_addr("192.168.0.101"); // inet_addr()用來將IP地址字符串轉換成網絡所使用的二進制數字,如果為INADDR_ANY,這表示服務器自動填充本機IP地址。

if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1)

{perror("bind");close(sfd);exit(-1);}

(注:通過將my_addr.sin_port置為0,函數會自動為你選擇一個未占用的端口來使用。同樣,通過將my_addr.sin_addr.s_addr置為INADDR_ANY,系統會自動填入本機IP地址。

 

4. listen函數:使服務器的這個端口和IP處於監聽狀態,等待網絡中某一客戶機的連接請求。如果客戶端有連接請求,端口就會接受這個連接。

原型:int listen(int sockfd, int backlog);

參數:sockfdà為前面socket的返回值.sfd

backlogà指定同時能處理的最大連接要求,通常為10或者5 最大值可設至128

返回值:成功則返回0,失敗返回-1

常用實例:if(listen(sfd, 10) == -1)

  {perror("listen");close(sfd);exit(-1);}

 

5. accept函數:接受遠程計算機的連接請求,建立起與客戶機之間的通信連接。服務器處於監聽狀態時,如果某時刻獲得客戶機的連接請求,此時並不是立即處理這個請求,而是將這個請求放在等待隊列中,當系統空閑時再處理客戶機的連接請求。當accept函數接受一個連接時,會返回一個新的socket標識符,以后的數據傳輸和讀取就要通過這個新的socket編號來處理,原來參數中的socket也可以繼續使用,繼續監聽其它客戶機的連接請求。(也就是說,類似於移動營業廳,如果有客戶打電話給10086,此時服務器就會請求連接,處理一些事務之后,就通知一個話務員接聽客戶的電話,也就是說,后面的所有操作,此時已經於服務器沒有關系,而是話務員跟客戶的交流。對應過來,客戶請求連接我們的服務器,我們服務器先做了一些綁定和監聽等等操作之后,如果允許連接,則調用accept函數產生一個新的套接字,然后用這個新的套接字跟我們的客戶進行收發數據。也就是說,服務器跟一個客戶端連接成功,會有兩個套接字。)

原型:int accept(int s,struct sockaddr * addr,int * addrlen);

參數:為前面socket的返回值.sfd

  addrà為結構體指針變量,和bind的結構體是同種類型的,系統會把遠程主機的信息(遠程主機的地址和端口號信息)保存到這個指針所指的結構體中。

  addrlenà表示結構體的長度,為整型指針

返回值:成功則返回新的socket處理代碼new_fd,失敗返回-1

常用實例:struct sockaddr_in clientaddr;

  memset(&clientaddr, 0, sizeof(struct sockaddr));

  int addrlen = sizeof(struct sockaddr);

  int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);

  if(new_fd == -1)

  {perror("accept");close(sfd);exit(-1);}

  printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));

 

6. recv函數:用新的套接字來接收遠端主機傳來的數據,並把數據存到由參數buf 指向的內存空間

原型:int recv(int sockfd,void *buf,int len,unsigned int flags);

參數:sockfdà為前面accept的返回值.new_fd,也就是新的套接字。

  bufà表示緩沖區

  lenà表示緩沖區的長度

  flagsà通常為0

返回值:成功則返回實際接收到的字符數,可能會少於你所指定的接收長度。失敗返回-1

常用實例:char buf[512] = {0};

  if(recv(new_fd, buf, sizeof(buf), 0) == -1)

  {perror("recv");close(new_fd);close(sfd);exit(-1);}

  puts(buf);

 

7. send函數:用新的套接字發送數據指定的遠端主機

原型:int send(int s,const void * msg,int len,unsigned int flags);

參數:為前面accept的返回值.new_fd

  msgà一般為常量字符串

  lenà表示長度

  flagsà通常為0

返回值:成功則返回實際傳送出去的字符數,可能會少於你所指定的發送長度。失敗返回-1

常用實例:if(send(new_fd, "hello", 6, 0) == -1)

{perror("send");close(new_fd);close(sfd);exit(-1);}

 

8. close函數:當使用完文件后若已不再需要則可使用close()關閉該文件,並且close()讓數據寫回磁盤,並釋放該文件所占用的資源

原型:int close(int fd);

參數:fdà為前面的sfd,new_fd

  返回值:若文件順利關閉則返回0,發生錯誤時返回-1

常用實例:close(new_fd);

  close(sfd);

客戶端:

1. connect函數:用來請求連接遠程服務器,將參數sockfd socket 連至參數serv_addr 指定的服務器IP和端口號上去。

原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);

參數:sockfdà為前面socket的返回值,即sfd

  serv_addrà為結構體指針變量,存儲着遠程服務器的IP與端口號信息。

  addrlenà表示結構體變量的長度

返回值:成功則返回0,失敗返回-1

常用實例:struct sockaddr_in seraddr;//請求連接服務器

  memset(&seraddr, 0, sizeof(struct sockaddr));

  seraddr.sin_family = AF_INET;

  seraddr.sin_port = htons(8888); //服務器的端口號

  seraddr.sin_addr.s_addr = inet_addr("192.168.0.101");  //服務器的ip

  if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)

  {perror("connect");close(sfd);exit(-1);}

 

將上面的頭文件以及各個函數中的代碼全部拷貝就可以形成一個完整的例子,此處省略。

還可以不寫客戶端程序,使用telnet遠程登錄來檢測我們的服務器端程序。比如我們的服務器程序在監聽8888端口,我們可以用telnet 192.168.0.101 8888來查看服務端的狀況。

Example:將一些通用的代碼全部封裝起來,以后要用直接調用函數即可。如下:

通用網絡封裝代碼頭文件: tcp_net_socket.h

#ifndef __TCP__NET__SOCKET__H

#define __TCP__NET__SOCKET__H

 

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <signal.h>

 

extern int tcp_init(const char* ip,int port);

extern int tcp_accept(int sfd);

extern int tcp_connect(const char* ip,int port);

extern void signalhandler(void);

 

#endif

具體的通用函數封裝如下: tcp_net_socket.c

#include "tcp_net_socket.h"

int tcp_init(const char* ip, int port)   //用於初始化操作

{

    int sfd = socket(AF_INET, SOCK_STREAM, 0);//首先創建一個socket,向系統申請

    if(sfd == -1)

    {

        perror("socket");

        exit(-1);

    }

    struct sockaddr_in serveraddr;

    memset(&serveraddr, 0, sizeof(struct sockaddr));

    serveraddr.sin_family = AF_INET;

    serveraddr.sin_port = htons(port);

    serveraddr.sin_addr.s_addr = inet_addr(ip);//INADDR_ANY

if(bind(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)

//將新的socket與制定的ipport綁定

    {

        perror("bind");

        close(sfd);

        exit(-1);

    }

    if(listen(sfd, 10) == -1)//監聽它,並設置其允許最大的連接數為10

    {

        perror("listen");

        close(sfd);

        exit(-1);

    }

    return sfd;

}

 

int tcp_accept(int sfd)   //用於服務端的接收

{

    struct sockaddr_in clientaddr;

    memset(&clientaddr, 0, sizeof(struct sockaddr));

    int addrlen = sizeof(struct sockaddr);

int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);

//sfd接受客戶端連接,並創建新的socketnew_fd,將請求連接的客戶端的ipport保存在結構體clientaddr

    if(new_fd == -1)

    {

        perror("accept");

        close(sfd);

        exit(-1);

    }

    printf("%s %d success connect...\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));

return new_fd;

}

 

int tcp_connect(const char* ip, int port)   //用於客戶端的連接

{

    int sfd = socket(AF_INET, SOCK_STREAM, 0);//向系統注冊申請新的socket

    if(sfd == -1)

    {

        perror("socket");

        exit(-1);

    }

    struct sockaddr_in serveraddr;

    memset(&serveraddr, 0, sizeof(struct sockaddr));

    serveraddr.sin_family = AF_INET;

    serveraddr.sin_port = htons(port);

    serveraddr.sin_addr.s_addr = inet_addr(ip);

if(connect(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)

//sfd連接至制定的服務器網絡地址serveraddr

    {

perror("connect");

close(sfd);

exit(-1);

    }

    return sfd;

}

 

void signalhandler(void)   //用於信號處理,讓服務端在按下Ctrl+cCtrl+\的時候不會退出

{

sigset_t sigSet;

    sigemptyset(&sigSet);

    sigaddset(&sigSet,SIGINT);

    sigaddset(&sigSet,SIGQUIT);

    sigprocmask(SIG_BLOCK,&sigSet,NULL);

}

服務器端: tcp_net_server.c

#include "tcp_net_socket.h"

int main(int argc, char* argv[])

{

if(argc < 3)

{

printf("usage:./servertcp  ip  port\n");

exit(-1);

}

signalhandler();

int sfd = tcp_init(argv[1], atoi(argv[2]));  //int sfd = tcp_init("192.168.0.164", 8888);

while(1) //while循環表示可以與多個客戶端接收和發送但仍是阻塞模式的

{

int cfd = tcp_accept(sfd);

char buf[512] = {0};

if(recv(cfd, buf, sizeof(buf), 0) == -1)//cfd客戶端接收數據存於buf

{

perror("recv");

close(cfd);

close(sfd);

exit(-1);

}

puts(buf);

if(send(cfd, "hello world", 12, 0) == -1)//buf中取向cfd客戶端發送數據

{

perror("send");

close(cfd);

close(sfd);

exit(-1);

}

close(cfd);

}

    close(sfd);

}

客戶端: tcp_net_client.c

#include "tcp_net_socket.h"

int main(int argc, char* argv[])

{

    if(argc < 3)

    {

printf("usage:./clienttcp  ip  port\n");

exit(-1);

    }

    int sfd = tcp_connect(argv[1],atoi(argv[2]));

    char buf[512] = {0};

    send(sfd, "hello", 6, 0);     //sfd服務端發送數據

    recv(sfd, buf, sizeof(buf), 0); //sfd服務端接收數據

    puts(buf);

    close(sfd);

}

#gcc –o tcp_net_server tcp_net_server.c tcp_net_socket.c

#gcc –o tcp_net_client tcp_net_client.c tcp_net_socket.c

#./tcp_net_server 192.168.0.164 8888

#./tcp_net_client 192.168.0.164 8888

/* 備注

可以通過將上述經常用到的函數做成動態庫,這樣以后再用到的時候就可以直接用。步驟如下:

gcc –fpic –c tcp_net_socket.c –o tcp_net_socket.o

gcc –shared tcp_net_socket.o –o libtcp_net_socket.so

cp lib*.so /lib    //這樣以后就可以直接使用該庫了

cp tcp_net_socket.h /usr/include/    //這樣頭文件包含可以用include <tcp_net_socket.h>

gcc –o main main.c –ltcp_net_socket  //其中main.c要包含頭文件 :  include <tcp_net_socket.h>

./main

*/

上面的雖然可以實現多個客戶端訪問但是仍然是阻塞模式即一個客戶訪問的時候會阻塞不讓另外的客戶訪問。解決辦法有

  1. 多進程因為開銷比較大所以不常用

#include <tcp_net_socket.h>

 

int main()

{

int sfd = tcp_init("192.168.0.101", 8888);

while(1)

{

int cfd = tcp_accept(sfd);

if(fork() == 0)

{

send(cfd, "hello", 6, 0);

sleep(10);

close(cfd);

}

else

{

close(cfd);

}

}

close(sfd);

return 0;

}

  1. 多線程

#include <tcp_net_socket.h>

#include <pthread.h>

 

void* pthfunc(void* arg)

{

int cfd = (int)arg;

send(cfd, "hello", 6, 0);

sleep(10);

close(cfd);

}

 

int main()

{

int sfd = tcp_init("192.168.0.101", 8888);

pthread_t pthid = 0;

while(1)

{

    int cfd = tcp_accept(sfd);

    pthread_create(&pthid, NULL, pthfunc, (void*)cfd);

}

    close(sfd);

return 0;

}

/* 備注 讀寫大容量的文件時,通過下面的方法效率很高

ssize_t readn(int fd, char *buf, int size)//讀大量內容

{

char *pbuf = buf;

int total ,nread;

for(total = 0; total < size; )

{

nread=read(fd,pbuf,size-total);

if(nread==0)

       return total;

    if(nread == -1)

    {

      if(errno == EINTR)

          continue;

      else

         return -1;

    }

    total += nread;

    pbuf += nread;

    }

   return total;

}

ssize_t writen(int fd, char *buf, int size)//寫大量內容

{

   char *pbuf=buf;

   int total ,nwrite;

   for(total = 0; total < size; )

   {

      nwrite=write(fd,pbuf,size-total);

  if( nwrite <= 0 )

  {

   if( nwrite == -1 && errno == EINTR )

continue;

   else

    return -1;

  }

     total += nwrite;

     pbuf += nwrite;

   }

  return total;

}

*/

  1. 調用fcntlsockfd設置為非阻塞模式。(不常見)

#include <unistd.h>

#include <fcntl.h>

……

sockfd = socket(AF_INET,SOCK_STREAM,0);

iflags = fcntl(sockfd, F_GETFL, 0);

fcntl(sockfd,F_SETFL,O_NONBLOCK | iflags);

……

  1. 多路選擇select

#include <sys/select.h>

#include "tcp_net_socket.h"

#define MAXCLIENT 10

main()

{

int sfd = tcp_init("192.168.0.164", 8888);

int fd = 0;

char buf[512] = {0};

fd_set rdset;

while(1)

{

FD_ZERO(&rdset);

FD_SET(sfd,&rdset);

if(select(MAXCLIENT + 1, &rdset, NULL, NULL, NULL) < 0)

continue;

for(fd = 0; fd < MAXCLIENT; fd++)

{

if(FD_ISSET(fd,&rdset))

{

if(fd == sfd)

{

int cfd = tcp_accept(sfd);

FD_SET(cfd,&rdset);

//……

}

else

{

bzero(buf, sizeof(buf));

recv(fd, buf, sizeof(buf), 0);

puts(buf);

send(fd, "java", 5, 0);

//FD_CLR(fd, &rdset);

close(fd);

}

}

}

}

close(sfd);

}

具體例子請參考《網絡編程之select.doc》或《tcp_select

3.2. 使用UDP協議的流程圖

UDP通信流程圖如下:

服務端:socket---bind---recvfrom---sendto---close

客戶端:socket----------sendto---recvfrom---close

 

·sendto()函數原型:

int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);

該函數比send()函數多了兩個參數,to表示目地機的IP地址和端口號信息,而tolen常常被賦值為sizeof (struct sockaddr)sendto 函數也返回實際發送的數據字節長度或在出現發送錯誤時返回-1

·recvfrom()函數原型:

int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);

from是一個struct sockaddr類型的變量,該變量保存連接機的IP地址及端口號。fromlen常置為sizeof (struct sockaddr)。當recvfrom()返回時,fromlen包含實際存入from中的數據字節數。Recvfrom()函數返回接收到的字節數或 當出現錯誤時返回-1,並置相應的errno

Example:UDP的基本操作

服務器端:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

main()

{

    int sfd = socket(AF_INET, SOCK_DGRAM, 0);

    if(sfd == -1)

    {

        perror("socket");

        exit(-1);

    }

 

    struct sockaddr_in saddr;

    bzero(&saddr, sizeof(saddr));

    saddr.sin_family = AF_INET;

    saddr.sin_port = htons(8888);

    saddr.sin_addr.s_addr = INADDR_ANY;

    if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)

    {

        perror("bind");

        close(sfd);

        exit(-1);

    }

 

    char buf[512] = {0};

    while(1)

    {

        struct sockaddr_in fromaddr;

        bzero(&fromaddr, sizeof(fromaddr));

        int fromaddrlen = sizeof(struct sockaddr);

        if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)

        {

            perror("recvfrom");

            close(sfd);

            exit(-1);

        }

printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);

 

        sendto(sfd, "world", 6, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));

}

 

    close(sfd);

}

客戶端:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

int main(int argc, char* argv[])

{

    int sfd = socket(AF_INET, SOCK_DGRAM, 0);

    if(sfd == -1)

    {

        perror("socket");

        exit(-1);

    }

 

    struct sockaddr_in toaddr;

    bzero(&toaddr, sizeof(toaddr));

    toaddr.sin_family = AF_INET;

    toaddr.sin_port = htons(atoi(argv[2])); //此處的端口號要跟服務器一樣

    toaddr.sin_addr.s_addr = inet_addr(argv[1]); //此處為服務器的ip

    sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));

 

    char buf[512] = {0};

    struct sockaddr_in fromaddr;

    bzero(&fromaddr, sizeof(fromaddr));

    int fromaddrlen = sizeof(struct sockaddr);

    if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)

    {

        perror("recvfrom");

        close(sfd);

        exit(-1);

    }

printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);

 

    close(sfd);

}

ExampleUDP發送文件  先發文件大小  再發文件內容

服務器端:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/stat.h>

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

main()

{

    int sfd = socket(AF_INET, SOCK_DGRAM, 0);

    if(sfd == -1)

    {

        perror("socket");

        exit(-1);

    }

 

    struct sockaddr_in saddr;

    bzero(&saddr, sizeof(saddr));

    saddr.sin_family = AF_INET;

    saddr.sin_port = htons(8888);

    saddr.sin_addr.s_addr = INADDR_ANY;

    if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)

    {

        perror("bind");

        close(sfd);

        exit(-1);

    }

 

    char buf[512] = {0};

    struct sockaddr_in fromaddr;

    bzero(&fromaddr, sizeof(fromaddr));

    int fromaddrlen = sizeof(struct sockaddr);

    if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)

    {

        perror("recvfrom");

        close(sfd);

        exit(-1);

    }

    printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);

    

    FILE* fp = fopen("1.txt","rb");

    struct stat st;  //用於獲取文件內容的大小

    stat("1.txt", &st);

    int filelen = st.st_size;

    sendto(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));

    while(!feof(fp))   //表示沒有到文件尾

    {

        int len = fread(buf,1,sizeof(buf),fp);

        sendto(sfd, buf, len, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));

}

 

    close(sfd);

}

客戶端:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

#define BUFSIZE 512

int main(int argc, char* argv[])

{

    int sfd = socket(AF_INET, SOCK_DGRAM, 0);

    if(sfd == -1)

    {

        perror("socket");

        exit(-1);

    }

 

    struct sockaddr_in toaddr;

    bzero(&toaddr, sizeof(toaddr));

    toaddr.sin_family = AF_INET;

    toaddr.sin_port = htons(atoi(argv[2]));

    toaddr.sin_addr.s_addr = inet_addr(argv[1]);

    sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));

 

    char buf[BUFSIZE] = {0};

    struct sockaddr_in fromaddr;

    bzero(&fromaddr, sizeof(fromaddr));

    int fromaddrlen = sizeof(struct sockaddr);

    int filelen = 0;   //用於保存文件長度

    FILE* fp = fopen("2.txt","w+b");

//接收文件的長度

recvfrom(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);

    printf("the length of file is %d\n",filelen);

    printf("Create a new file!\n");

    printf("begin to reveive file content!\n");

    //接收文件的內容

while(1)

    {

        int len = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);

        if(len < BUFSIZE)

//如果接收的長度小於BUFSIZE,則表示最后一次接收,此時要用break退出循環

        {

            fwrite(buf,sizeof(char),len,fp);

            break;

        }

        fwrite(buf,sizeof(char),len,fp);

    }

    printf("receive file finished!\n");

    close(sfd);

}

3.3. 設置套接口的選項setsockopt的用法

函數原型:

#include <sys/types.h >

  #include <sys/socket.h>

  int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

sockfd:標識一個套接口的描述字

level:選項定義的層次;支持SOL_SOCKETIPPROTO_TCPIPPROTO_IPIPPROTO_IPV6

optname:需設置的選項

optval:指針,指向存放選項值的緩沖區

optlenoptval緩沖區長度

 

全部都必須要放在bind之前,另外通常是用於UDP的。

1. 如果在已經處於 ESTABLISHED狀態下的socket(一般由端口號和標志符區分)調用closesocket(一般不會立即關閉而經歷TIME_WAIT的過程)后想繼續重用該socket

int reuse=1;

setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));

2. 如果要已經處於連接狀態的soket在調用closesocket后強制關閉,不經歷TIME_WAIT的過程:

int reuse=0;

setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));

3.send(),recv()過程中有時由於網絡狀況等原因,發收不能預期進行,而設置收發時限:

int nNetTimeout=1000; // 1

// 發送時限

setsockopt(socketSOL_S0CKET,SO_SNDTIMEO(char *)&nNetTimeout,sizeof(int));

// 接收時限

setsockopt(socketSOL_S0CKET,SO_RCVTIMEO(char *)&nNetTimeout,sizeof(int));

4.send()的時候,返回的是實際發送出去的字節(同步)或發送到socket緩沖區的字節(異步),系統默認的狀態發送和接收一次為8688字節(約為8.5K);在實際的過程中發送數據和接收數據量比較大,可以設置socket緩沖區,而避免了send(),recv()不斷的循環收發:

// 接收緩沖區

int nRecvBuf=32*1024;    // 設置為32K

setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));

// 發送緩沖區

int nSendBuf=32*1024;    // 設置為32K

setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));

5. 如果在發送數據時,希望不經歷由系統緩沖區到socket緩沖區的拷貝而影響程序的性能:

int nZero=0;

setsockopt(socketSOL_SOCKET,SO_SNDBUF(char *)&nZero,sizeof(int));

6.同上在recv()完成上述功能(默認情況是將socket緩沖區的內容拷貝到系統緩沖區)

int nZero=0;

setsockopt(socketSOL_SOCKET,SO_RCVBUF(char *)&nZero,sizeof(int));

7.一般在發送UDP數據報的時候,希望該socket發送的數據具有廣播特性:

int bBroadcast = 1;

setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(int));

3.4. 單播、廣播、組播(多播)

多播廣播是用於建立分步式系統:例如網絡游戲、ICQ聊天構建、遠程視頻會議系統的重要工具。使用多播廣播的程序和UDP的單播程序相似。區別在於多播廣播程序使用特殊的IP地址。

對於單播而言,單播用於兩個主機之間的端對端通信

對於廣播而言,廣播用於一個主機對整個局域網上所有主機上的數據通信。廣播只能用於客戶機向服務器廣播,因為客戶機要指明廣播的IP地址“192.168.0.255”和廣播的端口號。服務器端bing的時候,綁定的端口號要跟廣播的端口號是同一個。這樣才能收到廣播消息。實例請參考《udp_廣播》。

對於多播而言,也稱為組播,將網絡中同一業務類型主機進行了邏輯上的分組,進行數據收發的時候其數據僅僅在同一分組中進行,其他的主機沒有加入此分組不能收發對應的數據。單播和廣播是兩個極端,要么對一個主機進行通信,要么對整個局域網上的主機進行通信。實際情況下,經常需要對一組特定的主機進行通信,而不是整個局域網上的所有主機,這就是多播的用途。例如,我們通常所說的討論組。IPv4多播地址采用DIP地址確定多播的組。在Internet中,多播地址范圍是從224.0.0.0234.255.255.255

多播的程序設計也要使用setsockopt()函數和getsockopt()函數來實現。其中對於setsockopt的第二個參數level不再是SOL_SOCKET,而是IPPROTO_IP;而且第三個參數optname常見的選項有:

optname

含 義

IP_ADD_MEMBERSHIP

在指定接口上加入組播組

IP_DROP_MEMBERSHIP

退出組播組

選項IP_ADD_MEMBERSHIPIP_DROP_MEMBERSHIP加入或者退出一個組播組,通過選項IP_ADD_MEMBERSHIPIP_DROP_MEMBERSHIP,對一個結構struct ip_mreq類型的變量進行控制

struct ip_mreq原型如下:

struct ip_mreq

{

struct in_addr    imr_multiaddr; /*加入或者退出的播組IP地址*/

struct in_addr    imr_interface; /*加入或者退出的網絡接口IP地址,本機IP*/

};

選項IP_ADD_MEMBERSHIP用於加入某個播組,之后就可以向這個播組發送數據或者從播組接收數據。此選項的值為mreq結構,成員imr_multiaddr是需要加入的播組IP地址,成員imr_interface是本機需要加入播組的網絡接口IP地址。例如:

struct ip_mreq mreq;

memset(&mreq, 0, sizeof(struct ip_mreq));

mreq.imr_interface.s_addr = INADDR_ANY;

mreq.imr_multiaddr.s_addr = inet_addr("224.1.1.1");

if(-1 == setsockopt(sfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)))

{

perror("setsockopt");

exit(-1);

}

接下來再綁定組播的port號(如65000),就可以接收組播消息了。實例請參考《udp_組播》

選項IP_ADD_MEMBERSHIP每次只能加入一個網絡接口的IP地址到多播組,但並不是一個多播組僅允許一個主機IP地址加入,可以多次調用IP_ADD_MEMBERSHIP選項來實現多個IP地址加入同一個廣播組,或者同一個IP地址加入多個廣播組。

選項IP_DROP_MEMBERSHIP用於從一個播組中退出。例如:

if(-1 == setsockopt(sfd, IPPROTP_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)))

{

perror("setsockopt");

exit(-1);

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM