文末分享Linux網(wǎng)絡(luò)編程電子書PDF版
1. LINUX網(wǎng)絡(luò)編程基礎(chǔ)知識
1.1. TCP/IP協(xié)議概述
協(xié)議protocol:通信雙方必須遵循的規(guī)矩 由iso規(guī)定 rpc文檔
osi參考模型:(應(yīng)-表-會-傳-網(wǎng)-數(shù)-物)
è 應(yīng)用層 表示層 會話層 傳輸層 網(wǎng)絡(luò)層 數(shù)據(jù)鏈路層 物理層
tcp/ip模型4層:
應(yīng)用層{http超文本傳輸協(xié)議 ftp文件傳輸協(xié)議 telnet遠(yuǎn)程登錄 ssh安全外殼協(xié)議 stmp簡單郵件發(fā)送 pop3收郵件}
傳輸層{tcp傳輸控制協(xié)議,udp用戶數(shù)據(jù)表協(xié)議}
網(wǎng)絡(luò)層{ip網(wǎng)際互聯(lián)協(xié)議 icmp網(wǎng)絡(luò)控制消息協(xié)議 igmp網(wǎng)絡(luò)組管理協(xié)議}
網(wǎng)絡(luò)接口層{arp地址轉(zhuǎn)換協(xié)議,rarp反向地址轉(zhuǎn)換協(xié)議,mpls多協(xié)議標(biāo)簽交換}
TCP協(xié)議:傳輸控制協(xié)議 面向連接的協(xié)議 能保證傳輸安全可靠 速度慢(有3次握手)
UDP協(xié)議:用戶數(shù)據(jù)包協(xié)議 非面向連接 速度快 不可靠
通常是IP地址后面跟上端口號:ip用來定位主機(jī) port區(qū)別應(yīng)用(進(jìn)程)
http的端口號80 ssh–>22 telnet–>23 ftp–>21 用戶自己定義的通常要大與1024
1.2. OSI參考模型及TCP/IP參考模型
TCP/IP協(xié)議族的每一層的作用:
·網(wǎng)絡(luò)接口層:負(fù)責(zé)將二進(jìn)制流轉(zhuǎn)換為數(shù)據(jù)幀,并進(jìn)行數(shù)據(jù)幀的發(fā)送和接收。要注意的是數(shù)據(jù)幀是獨立 的網(wǎng)絡(luò)信息傳輸單元。
·網(wǎng)絡(luò)層:負(fù)責(zé)將數(shù)據(jù)幀封裝成IP數(shù)據(jù)包,并運行必要的路由算法。
·傳輸層:負(fù)責(zé)端對端之間的通信會話連接和建立。傳輸協(xié)議的選擇根據(jù)數(shù)據(jù)傳輸方式而定。
·應(yīng)用層:負(fù)責(zé)應(yīng)用程序的網(wǎng)絡(luò)訪問,這里通過端口號來識別各個不同的進(jìn)程。
TCP/IP協(xié)議族的每一層協(xié)議的相關(guān)注解:
·ARP:(地址轉(zhuǎn)換協(xié)議)用于獲得同一物理網(wǎng)絡(luò)中的硬件主機(jī)地址。
·MPLS:(多協(xié)議標(biāo)簽交換)很有發(fā)展前景的下一代網(wǎng)絡(luò)協(xié)議。
·IP:(網(wǎng)際互聯(lián)協(xié)議)負(fù)責(zé)在主機(jī)和網(wǎng)絡(luò)之間尋址和路由數(shù)據(jù)包。
·ICMP:(網(wǎng)絡(luò)控制消息協(xié)議)用于發(fā)送報告有關(guān)數(shù)據(jù)包的傳送錯誤的協(xié)議。
·IGMP:(網(wǎng)絡(luò)組管理協(xié)議)被IP主機(jī)用來向本地多路廣播路由器報告主機(jī)組成員的協(xié)議。
·TCP:(傳輸控制協(xié)議)為應(yīng)用程序提供可靠的通信連接。適合于一次傳輸大批數(shù)據(jù)的情況。并適用于要求得到相應(yīng)的應(yīng)用程序。
·UDP:(用戶數(shù)據(jù)包協(xié)議)提供了無連接通信,且不對傳送包進(jìn)行可靠的保證。適合于一次傳輸少量數(shù)據(jù)。
1.3. TCP協(xié)議
(1) 概述
TCP是TCP/IP體系中面向連接的運輸層協(xié)議,它提供全雙工和可靠交付的服務(wù)。它采用許多機(jī)制來確保端到端結(jié)點之間的可靠數(shù)據(jù)傳輸,如采用序列號、確認(rèn)重傳、滑動窗口等。
首先,TCP要為所發(fā)送的每一個報文段加上序列號,保證每一個報文段能被接收方接收,并只被正確的接收一次。
其次,TCP采用具有重傳功能的積極確認(rèn)技術(shù)作為可靠數(shù)據(jù)流傳輸服務(wù)的基礎(chǔ)。這里“確認(rèn)”是指接收端在正確收到報文段之后向發(fā)送端回送一個確認(rèn)(ACK)信息。發(fā)送方將每個已發(fā)送的報文段備份在自己的緩沖區(qū)里,而且在收到相應(yīng)的確認(rèn)之前是不會丟棄所保存的報文段的?!胺e極”是指發(fā)送發(fā)在每一個報文段發(fā)送完畢的同時啟動一個定時器,加入定時器的定時期滿而關(guān)于報文段的確認(rèn)信息還沒有達(dá)到,則發(fā)送發(fā)認(rèn)為該報文段已經(jīng)丟失并主動重發(fā)。為了避免由于網(wǎng)絡(luò)延時引起遲到的確認(rèn)和重復(fù)的確認(rèn),TCP規(guī)定在確認(rèn)信息中捎帶一個報文段的序號,使接收方能正確的將報文段與確認(rèn)聯(lián)系起來。
最后,采用可變長的滑動窗口協(xié)議進(jìn)行流量控制,以防止由于發(fā)送端與接收端之間的不匹配而引起的數(shù)據(jù)丟失。這里所采用的滑動窗口協(xié)議與數(shù)據(jù)鏈路層的滑動窗口協(xié)議在工作原理上完全相同,唯一的區(qū)別在于滑動窗口協(xié)議用于傳輸層是為了在端對端節(jié)點之間實現(xiàn)流量控制,而用于數(shù)據(jù)鏈路層是為了在相鄰節(jié)點之間實現(xiàn)流量控制。TCP采用可變長的滑動窗口,使得發(fā)送端與接收端可根據(jù)自己的CPU和數(shù)據(jù)緩存資源對數(shù)據(jù)發(fā)送和接收能力來進(jìn)行動態(tài)調(diào)整,從而靈活性更強(qiáng),也更合理。
(2) 三次握手協(xié)議
在利用TCP實現(xiàn)源主機(jī)和目的主機(jī)通信時,目的主機(jī)必須同意,否則TCP連接無法建立。為了確保TCP連接的成功建立,TCP采用了一種稱為三次握手的方式,三次握手方式使得“序號/確認(rèn)號”系統(tǒng)能夠正常工作,從而使它們的序號達(dá)成同步。如果三次握手成功,則連接建立成功,可以開始傳送數(shù)據(jù)信息。
其三次握手分別為:
1)源主機(jī)A的TCP向主機(jī)B發(fā)送連接請求報文段,其首部中的SYN(同步)標(biāo)志位應(yīng)置為1,表示想跟目標(biāo)主機(jī)B建立連接,進(jìn)行通信,并發(fā)送一個同步序列號X(例:SEQ=100)進(jìn)行同步,表明在后面?zhèn)魉蛿?shù)據(jù)時的第一個數(shù)據(jù)字節(jié)的序號為X 1(即101)。
2)目標(biāo)主機(jī)B的TCP收到連接請求報文段后,如同意,則發(fā)回確認(rèn)。在確認(rèn)報中應(yīng)將ACK位和SYN位置為1.確認(rèn)號為X 1,同時也為自己選擇一個序號Y。
3)源主機(jī)A的TCP收到目標(biāo)主機(jī)B的確認(rèn)后要向目標(biāo)主機(jī)B給出確認(rèn)。其ACK值為1,確認(rèn)號為Y 1,而自己的序號為X 1。TCP的標(biāo)準(zhǔn)規(guī)定,SYN置1的報文段要消耗掉一個序號。
運行客戶進(jìn)程的源主機(jī)A的TCP通知上層應(yīng)用進(jìn)程,連接已經(jīng)建立。當(dāng)源主機(jī)A向目標(biāo)主機(jī)B發(fā)送第一個數(shù)據(jù)報文段時,其序號仍為X 1,因為前一個確認(rèn)報文段并不消耗序號。
當(dāng)運行服務(wù)進(jìn)程的目標(biāo)主機(jī)B的TCP收到源主機(jī)A的確認(rèn)后,也通知其上層應(yīng)用進(jìn)程,連接已經(jīng)建立。至此建立了一個全雙工的連接。
三次握手:為應(yīng)用程序提供可靠的通信連接。適合于一次傳輸大批數(shù)據(jù)的情況。并適用于要求得到響應(yīng)的應(yīng)用程序。
(3) TCP數(shù)據(jù)報頭
TCP頭信息
·源端口、目的端口:16位長。標(biāo)識出遠(yuǎn)端和本地的端口號。
·序號:32位長。標(biāo)識發(fā)送的數(shù)據(jù)報的順序。
·確認(rèn)號:32位長。希望收到的下一個數(shù)據(jù)報的序列號。
·TCP頭長:4位長。表明TCP頭中包含多少個32位字。
·6位未用。
·ACK:ACK位置1表明確認(rèn)號是合法的。如果ACK為0,那么數(shù)據(jù)報不包含確認(rèn)信息,確認(rèn)字段被省略。
·PSH:表示是帶有PUSH標(biāo)志的數(shù)據(jù)。接收方因此請求數(shù)據(jù)報一到便可送往應(yīng)用程序而不必等到緩沖區(qū)裝滿時才發(fā)送。
·RST:用于復(fù)位由于主機(jī)崩潰或其他原因而出現(xiàn)的錯誤的連接。還可以用于拒絕非法的數(shù)據(jù)報或拒絕連接請求。
·SYN:用于建立連接。
·FIN:用于釋放連接。
·窗口大?。?6位長。窗口大小字段表示在確認(rèn)了字節(jié)之后還可以發(fā)送多少個字節(jié)。
·校驗和:16位長。是為了確保高可靠性而設(shè)置的。它校驗頭部、數(shù)據(jù)和偽TCP頭部之和。
·可選項:0個或多個32位字。包括最大TCP載荷,窗口比例、選擇重復(fù)數(shù)據(jù)報等選項。
1.4. UDP協(xié)議
(1) 概述
UDP即用戶數(shù)據(jù)報協(xié)議,它是一種無連接協(xié)議,因此不需要像TCP那樣通過三次握手來建立一個連接。同時,一個UDP應(yīng)用可同時作為應(yīng)用的客戶或服務(wù)器方。由于UDP協(xié)議并不需要建立一個明確的連接,因此建立UDP應(yīng)用要比建立TCP應(yīng)用簡單得多。
它比TCP協(xié)議更為高效,也能更好地解決實時性的問題。如今,包括網(wǎng)絡(luò)視頻會議系統(tǒng)在內(nèi)的眾多的客戶/服務(wù)器模式的網(wǎng)絡(luò)應(yīng)用都使用UDP協(xié)議。
(2) Udp數(shù)據(jù)包頭格式
1.5. 協(xié)議的選擇
(1)對數(shù)據(jù)可靠性的要求
對數(shù)據(jù)要求高可靠性的應(yīng)用需選擇TCP協(xié)議,如驗證、密碼字段的傳送都是不允許出錯的,而對數(shù)據(jù)的可靠性要求不那么高的應(yīng)用可選擇UDP傳送。
(2)應(yīng)用的實時性
TCP協(xié)議在傳送過程中要使用三次握手、重傳確認(rèn)等手段來保證數(shù)據(jù)傳輸?shù)目煽啃?。使用TCP協(xié)議會有較大的時延,因此不適合對實時性要求較高的應(yīng)用,如VOIP、視頻監(jiān)控等。相反,UDP協(xié)議則在這些應(yīng)用中能發(fā)揮很好的作用。
(3)網(wǎng)絡(luò)的可靠性
由于TCP協(xié)議的提出主要是解決網(wǎng)絡(luò)的可靠性問題,它通過各種機(jī)制來減少錯誤發(fā)生的概率。因此,在網(wǎng)絡(luò)狀況不是很好的情況下需選用TCP協(xié)議(如在廣域網(wǎng)等情況),但是若在網(wǎng)絡(luò)狀況很好的情況下(如局域網(wǎng)等)就不需要再采用TCP協(xié)議,而建議選擇UDP協(xié)議來減少網(wǎng)絡(luò)負(fù)荷。
2. 網(wǎng)絡(luò)相關(guān)概念
1)套接口的概念:
套接口,也叫“套接字”。是操作系統(tǒng)內(nèi)核中的一個數(shù)據(jù)結(jié)構(gòu),它是網(wǎng)絡(luò)中的節(jié)點進(jìn)行相互通信的門戶。它是網(wǎng)絡(luò)進(jìn)程的ID。網(wǎng)絡(luò)通信,歸根到底還是進(jìn)程間的通信(不同計算機(jī)上的進(jìn)程間通信)。在網(wǎng)絡(luò)中,每一個節(jié)點(計算機(jī)或路由)都有一個網(wǎng)絡(luò)地址,也就是IP地址。兩個進(jìn)程通信時,首先要確定各自所在的網(wǎng)絡(luò)節(jié)點的網(wǎng)絡(luò)地址。但是,網(wǎng)絡(luò)地址只能確定進(jìn)程所在的計算機(jī),而一臺計算機(jī)上很可能同時運行著多個進(jìn)程,所以僅憑網(wǎng)絡(luò)地址還不能確定到底是和網(wǎng)絡(luò)中的哪一個進(jìn)程進(jìn)行通信,因此套接口中還需要包括其他的信息,也就是端口號(PORT)。在一臺計算機(jī)中,一個端口號一次只能分配給一個進(jìn)程,也就是說,在一臺計算機(jī)中,端口號和進(jìn)程之間是一一對應(yīng)關(guān)系。所以,使用端口號和網(wǎng)絡(luò)地址的組合可以唯一的確定整個網(wǎng)絡(luò)中的一個網(wǎng)絡(luò)進(jìn)程。
例如,如網(wǎng)絡(luò)中某一臺計算機(jī)的IP為10.92.20.160,操作系統(tǒng)分配給計算機(jī)中某一應(yīng)用程序進(jìn)程的端口號為1500,則此時 10.92.20.160 1500就構(gòu)成了一個套接口。
2)端口號的概念:
在網(wǎng)絡(luò)技術(shù)中,端口大致有兩種意思:一是物理意義上的端口,如集線器、交換機(jī)、路由器等用于連接其他網(wǎng)絡(luò)設(shè)備的接口。二是指TCP/IP協(xié)議中的端口,端口號的范圍從0~65535,一類是由互聯(lián)網(wǎng)指派名字和號碼公司ICANN負(fù)責(zé)分配給一些常用的應(yīng)用程序固定使用的“周知的端口”,其值一般為0~1023.例如http的端口號是80,ftp為21,ssh為22,telnet為23等。還有一類是用戶自己定義的,通常是大于1024的整型值。
3)ip地址的表示:
通常用戶在表達(dá)IP地址時采用的是點分十進(jìn)制表示的數(shù)值(或者是為冒號分開的十進(jìn)制Ipv6地址),而在通常使用的socket編程中使用的則是二進(jìn)制值,這就需要將這兩個數(shù)值進(jìn)行轉(zhuǎn)換。
IPV4地址:32bit, 4字節(jié),通常采用點分十進(jìn)制記法。
例如對于:10000000 00001011 00000011 00011111
點分十進(jìn)制表示為:128.11.3.31
ip地址的分類:
特殊的ip地址:
2.1. socket概念
Linux中的網(wǎng)絡(luò)編程是通過socket接口來進(jìn)行的。socket是一種特殊的I/O接口,它也是一種文件描述符。它是一種常用的進(jìn)程之間通信機(jī)制,通過它不僅能實現(xiàn)本地機(jī)器上的進(jìn)程之間的通信,而且通過網(wǎng)絡(luò)能夠在不同機(jī)器上的進(jìn)程之間進(jìn)行通信。
每一個socket都用一個半相關(guān)描述{協(xié)議、本地地址、本地端口}來表示;一個完整的套接字則用一個相關(guān)描述{協(xié)議、本地地址、本地端口、遠(yuǎn)程地址、遠(yuǎn)程端口}來表示。socket也有一個類似于打開文件的函數(shù)調(diào)用,該函數(shù)返回一個整型的socket描述符,隨后的連接建立、數(shù)據(jù)傳輸?shù)炔僮鞫际峭ㄟ^socket來實現(xiàn)的;
2.2. socket類型
(1)流式socket(SOCK_STREAM) à用于TCP通信
流式套接字提供可靠的、面向連接的通信流;它使用TCP協(xié)議,從而保證了數(shù)據(jù)傳輸?shù)恼_性和順序性。
(2)數(shù)據(jù)報socket(SOCK_DGRAM) à用于UDP通信
數(shù)據(jù)報套接字定義了一種無連接的服務(wù),數(shù)據(jù)通過相互獨立的報文進(jìn)行傳輸,是無序的,并且不保證是可靠、無差錯的。它使用數(shù)據(jù)報協(xié)議UDP。
(3)原始socket (SOCK_RAW) à用于新的網(wǎng)絡(luò)協(xié)議實現(xiàn)的測試等
原始套接字允許對底層協(xié)議如IP或ICMP進(jìn)行直接訪問,它功能強(qiáng)大但使用較為不便,主要用于一些協(xié)議的開發(fā)。
2.3. socket信息數(shù)據(jù)結(jié)構(gòu)
struct sockaddr{ unsigned short sa_family; /*地址族*/ char sa_data[14]; /*14字節(jié)的協(xié)議地址,包含該socket的IP地址和端口號。*/};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; /* 32位IPv4地址,網(wǎng)絡(luò)字節(jié)序 */};頭文件<netinet/in.h>sa_family:AF_INET àIPv4協(xié)議 AF_INET6 àIPv6協(xié)議
2.4. 數(shù)據(jù)存儲優(yōu)先順序的轉(zhuǎn)換
計算機(jī)數(shù)據(jù)存儲有兩種字節(jié)優(yōu)先順序:高位字節(jié)優(yōu)先(稱為大端模式)和低位字節(jié)優(yōu)先(稱為小端模式)。內(nèi)存的低地址存儲數(shù)據(jù)的低字節(jié),高地址存儲數(shù)據(jù)的高字節(jié)的方式叫小端模式。內(nèi)存的高地址存儲數(shù)據(jù)的低字節(jié),低地址存儲數(shù)據(jù)高字節(jié)的方式稱為大端模式。
eg:對于內(nèi)存中存放的數(shù)0x12345678來說
如果是采用大端模式存放的,則其真實的數(shù)是:0x12345678
如果是采用小端模式存放的,則其真實的數(shù)是:0x78563412
如果稱某個系統(tǒng)所采用的字節(jié)序為主機(jī)字節(jié)序,則它可能是小端模式的,也可能是大端模式的。而端口號和IP地址都是以網(wǎng)絡(luò)字節(jié)序存儲的,不是主機(jī)字節(jié)序,網(wǎng)絡(luò)字節(jié)序都是大端模式。要把主機(jī)字節(jié)序和網(wǎng)絡(luò)字節(jié)序相互對應(yīng)起來,需要對這兩個字節(jié)存儲優(yōu)先順序進(jìn)行相互轉(zhuǎn)化。這里用到四個函數(shù):htons(),ntohs(),htonl()和ntohl().這四個地址分別實現(xiàn)網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的轉(zhuǎn)化,這里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口號用s代表,而IP地址用l來代表。
2.5. 地址格式轉(zhuǎn)化
通常用戶在表達(dá)地址時采用的是點分十進(jìn)制表示的數(shù)值(或者是為冒號分開的十進(jìn)制Ipv6地址),而在通常使用的socket編程中使用的則是32位的網(wǎng)絡(luò)字節(jié)序的二進(jìn)制值,這就需要將這兩個數(shù)值進(jìn)行轉(zhuǎn)換。這里在Ipv4中用到的函數(shù)有inet_aton()、inet_addr()和inet_ntoa(),而IPV4和Ipv6兼容的函數(shù)有inet_pton()和inet_ntop()。
IPv4的函數(shù)原型:
#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);
函數(shù)inet_aton():將點分十進(jìn)制數(shù)的IP地址轉(zhuǎn)換成為網(wǎng)絡(luò)字節(jié)序的32位二進(jìn)制數(shù)值。返回值:成功,則返回1,不成功返回0.
參數(shù)straddr:存放輸入的點分十進(jìn)制數(shù)IP地址字符串。
參數(shù)addrptr:傳出參數(shù),保存網(wǎng)絡(luò)字節(jié)序的32位二進(jìn)制數(shù)值。
函數(shù)inet_ntoa():將網(wǎng)絡(luò)字節(jié)序的32位二進(jìn)制數(shù)值轉(zhuǎn)換為點分十進(jìn)制的IP地址。
函數(shù)inet_addr():功能與inet_aton相同,但是結(jié)果傳遞的方式不同。inet_addr()若成功則返回32位二進(jìn)制的網(wǎng)絡(luò)字節(jié)序地址。
IPv4和IPv6的函數(shù)原型:
#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);
函數(shù)inet_pton跟inet_aton實現(xiàn)的功能類似,只是多了family參數(shù),該參數(shù)指定為AF_INET,表示是IPv4協(xié)議,如果是AF_INET6,表示IPv6協(xié)議。
函數(shù)inet_ntop跟inet_ntoa類似,其中l(wèi)en表示表示轉(zhuǎn)換之后的長度(字符串的長度)。
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("%xn", myaddr.s_addr); /* inet_addr */printf("%xn", inet_addr(ip)); /* inet_pton */iRet = inet_pton(AF_INET, ip, &myaddr);printf("%xn", myaddr.s_addr); myaddr.s_addr = 0xac100ac4;/* inet_ntoa */printf("%sn", inet_ntoa(myaddr)); /* inet_ntop */inet_ntop(AF_INET, &myaddr, ip, 16);puts(ip);return 0;}
2.6. 名字地址轉(zhuǎn)化
通常,人們在使用過程中都不愿意記憶冗長的IP地址,尤其到Ipv6時,地址長度多達(dá)128位,那時就更加不可能一次性記憶那么長的IP地址了。因此,使用主機(jī)名或域名將會是很好的選擇。主機(jī)名與域名的區(qū)別:主機(jī)名通常在局域網(wǎng)里面使用,通過/etc/hosts文件,主機(jī)名可以解析到對應(yīng)的ip;域名通常是在internet上使用。
眾所周知,百度的域名為:www.baidu.com,而這個域名其實對應(yīng)了一個百度公司的IP地址,那么百度公司的IP地址是多少呢?我們可以利用ping www.baidu.com來得到百度公司的ip地址,如圖。那么,系統(tǒng)是如何將www.baidu.com 這個域名轉(zhuǎn)化為IP地址220.181.111.148的呢?
在linux中,有一些函數(shù)可以實現(xiàn)主機(jī)名和地址的轉(zhuǎn)化,最常見的有g(shù)ethostbyname()、gethostbyaddr()等,它們都可以實現(xiàn)IPv4和IPv6的地址和主機(jī)名之間的轉(zhuǎn)化。其中g(shù)ethostbyname()是將主機(jī)名轉(zhuǎn)化為IP地址,gethostbyaddr()則是逆操作,是將IP地址轉(zhuǎn)化為主機(jī)名。
函數(shù)原型:
#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
struct hostent* gethostbyaddr(const char* addr, size_t len, int family);
結(jié)構(gòu)體:
struct hostent{ char *h_name; /*正式主機(jī)名*/ char **h_aliases; /*主機(jī)別名*/ int h_addrtype; /*主機(jī)IP地址類型 IPv4為AF_INET*/ int h_length; /*主機(jī)IP地址字節(jié)長度,對于IPv4是4字節(jié),即32位*/ char **h_addr_list; /*主機(jī)的IP地址列表*/}#define h_addr h_addr_list[0] /*保存的是ip地址*/
函數(shù)gethostbyname():用于將域名(www.baidu.com)或主機(jī)名轉(zhuǎn)換為IP地址。參數(shù)hostname指向存放域名或主機(jī)名的字符串。
函數(shù)gethostbyaddr():用于將IP地址轉(zhuǎn)換為域名或主機(jī)名。參數(shù)addr是一個IP地址,此時這個ip地址不是普通的字符串,而是要通過函數(shù)inet_aton()轉(zhuǎn)換。len為IP地址的長度,AF_INET為4。family可用AF_INET:Ipv4或AF_INET6:Ipv6。
Example1:將百度的www.baidu.com 轉(zhuǎn)換為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'};/* 取得命令后第一個參數(shù),即要解析的域名或主機(jī)名 */ ptr = argv[1]; //如www.baidu.com/* 調(diào)用gethostbyname()。結(jié)果存在hptr結(jié)構(gòu)中 */ if((hptr = gethostbyname(ptr)) == NULL) { printf(" gethostbyname error for host:%sn", ptr); return 0; }/* 將主機(jī)的規(guī)范名打出來 */ printf("official hostname:%sn", hptr->h_name);/* 主機(jī)可能有多個別名,將所有別名分別打出來 */ for(pptr = hptr->h_aliases; *pptr != NULL; pptr ) printf(" alias:%sn", *pptr);/* 根據(jù)地址類型,將地址打出來 */ switch(hptr->h_addrtype) { case AF_INET: case AF_INET6: pptr = hptr->h_addr_list; /* 將剛才得到的所有地址都打出來。其中調(diào)用了inet_ntop()函數(shù) */ for(; *pptr!=NULL; pptr ) printf(" address:%sn", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str))); printf(" first address: %sn", inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str))); break; default: printf("unknown address typen"); break; } return 0;}
編譯運行
#gcc test.c#./a.out www.baidu.comofficial hostname:www.a.shifen.comalias:www.baidu.comaddress: 220.181.111.148……first address: 220.181.111.148
(注:這里需要聯(lián)網(wǎng)才能訪問www.baidu.com??梢試L試用自己的虛擬機(jī)的主機(jī)名,利用命令hostname可以查看自己的主機(jī)名,用hostname –i可以查看主機(jī)名對應(yīng)的ip地址。那么如何修改主機(jī)名呢?直接用hostname wangxiao只是暫時有效,重啟之后就沒有了,想要永久有效,需要修改/etc/sysconfig/network將HOSTNAME修改,當(dāng)然要重啟虛擬機(jī)。如果ip地址不對,你可以修改/etc/hosts這個文件,添加你自己的主機(jī)名對應(yīng)的ip地址)
3. socket編程
3.1. 使用TCP協(xié)議的流程圖
TCP通信的基本步驟如下:
服務(wù)端:socket—bind—listen—while(1){—accept—recv—send—close—}—close
客戶端:socket———————————-connect—send—recv—————–close
服務(wù)器端
頭文件包含:
#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函數(shù):生成一個套接口描述符。
原型:int socket(int domain,int type,int protocol);
參數(shù):domainà{ AF_INET:Ipv4網(wǎng)絡(luò)協(xié)議 AF_INET6:IPv6網(wǎng)絡(luò)協(xié)議}
typeà{tcp:SOCK_STREAM udp:SOCK_DGRAM}
protocolà指定socket所使用的傳輸協(xié)議編號。通常為0.
返回值:成功則返回套接口描述符,失敗返回-1。
常用實例:int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){perror("socket");exit(-1);}
3. bind函數(shù):用來綁定一個端口號和IP地址,使套接口與指定的端口號和IP地址相關(guān)聯(lián)。
原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
參數(shù):sockfdà為前面socket的返回值。
my_addrà為結(jié)構(gòu)體指針變量
對于不同的socket domain定義了一個通用的數(shù)據(jù)結(jié)構(gòu) struct sockaddr //此結(jié)構(gòu)體不常用 { unsigned short int sa_family; //調(diào)用socket()時的domain參數(shù),即AF_INET值。 char sa_data[14]; //最多使用14個字符長度 }; 此sockaddr結(jié)構(gòu)會因使用不同的socket domain而有不同結(jié)構(gòu)定義, 例如使用AF_INET domain,其socketaddr結(jié)構(gòu)定義便為 struct sockaddr_in //常用的結(jié)構(gòu)體 { 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的結(jié)構(gòu)體長度。通常是計算sizeof(struct sockaddr);
返回值:成功則返回0,失敗返回-1
常用實例:struct sockaddr_in my_addr; //定義結(jié)構(gòu)體變量
memset(&my_addr, 0, sizeof(struct sockaddr)); //將結(jié)構(gòu)體清空
//或bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; //表示采用Ipv4網(wǎng)絡(luò)協(xié)議
my_addr.sin_port = htons(8888); //表示端口號為8888,通常是大于1024的一個值。
//htons()用來將參數(shù)指定的16位hostshort轉(zhuǎn)換成網(wǎng)絡(luò)字符順序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101"); // inet_addr()用來將IP地址字符串轉(zhuǎn)換成網(wǎng)絡(luò)所使用的二進(jìn)制數(shù)字,如果為INADDR_ANY,這表示服務(wù)器自動填充本機(jī)IP地址。
if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1)
{perror("bind");close(sfd);exit(-1);}
(注:通過將my_addr.sin_port置為0,函數(shù)會自動為你選擇一個未占用的端口來使用。同樣,通過將my_addr.sin_addr.s_addr置為INADDR_ANY,系統(tǒng)會自動填入本機(jī)IP地址。)
4. listen函數(shù):使服務(wù)器的這個端口和IP處于監(jiān)聽狀態(tài),等待網(wǎng)絡(luò)中某一客戶機(jī)的連接請求。如果客戶端有連接請求,端口就會接受這個連接。
原型:int listen(int sockfd, int backlog);
參數(shù):sockfdà為前面socket的返回值.即sfd
backlogà指定同時能處理的最大連接要求,通常為10或者5。 最大值可設(shè)至128
返回值:成功則返回0,失敗返回-1
常用實例:if(listen(sfd, 10) == -1)
{perror("listen");close(sfd);exit(-1);}
5. accept函數(shù):接受遠(yuǎn)程計算機(jī)的連接請求,建立起與客戶機(jī)之間的通信連接。服務(wù)器處于監(jiān)聽狀態(tài)時,如果某時刻獲得客戶機(jī)的連接請求,此時并不是立即處理這個請求,而是將這個請求放在等待隊列中,當(dāng)系統(tǒng)空閑時再處理客戶機(jī)的連接請求。當(dāng)accept函數(shù)接受一個連接時,會返回一個新的socket標(biāo)識符,以后的數(shù)據(jù)傳輸和讀取就要通過這個新的socket編號來處理,原來參數(shù)中的socket也可以繼續(xù)使用,繼續(xù)監(jiān)聽其它客戶機(jī)的連接請求。(也就是說,類似于移動營業(yè)廳,如果有客戶打電話給10086,此時服務(wù)器就會請求連接,處理一些事務(wù)之后,就通知一個話務(wù)員接聽客戶的電話,也就是說,后面的所有操作,此時已經(jīng)于服務(wù)器沒有關(guān)系,而是話務(wù)員跟客戶的交流。對應(yīng)過來,客戶請求連接我們的服務(wù)器,我們服務(wù)器先做了一些綁定和監(jiān)聽等等操作之后,如果允許連接,則調(diào)用accept函數(shù)產(chǎn)生一個新的套接字,然后用這個新的套接字給我們的客戶進(jìn)行收發(fā)數(shù)據(jù)。也就是說,服務(wù)器跟一個客戶端連接成功,會有兩個套接字。)
原型:int accept(int s,struct sockaddr * addr,int * addrlen);
參數(shù):sà為前面socket的返回值.即sfd
addrà為結(jié)構(gòu)體指針變量,和bind的結(jié)構(gòu)體是同種類型的,系統(tǒng)會把遠(yuǎn)程主機(jī)的信息(遠(yuǎn)程主機(jī)的地址和端口號信息)保存到這個指針?biāo)傅慕Y(jié)構(gòu)體中。
addrlenà表示結(jié)構(gòu)體的長度,為整型指針
返回值:成功則返回新的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 connectn",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
6. recv函數(shù):用新的套接字來接收遠(yuǎn)端主機(jī)傳來的數(shù)據(jù),并把數(shù)據(jù)存到由參數(shù)buf 指向的內(nèi)存空間
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
參數(shù):sockfdà為前面accept的返回值.即new_fd,也就是新的套接字。
bufà表示緩沖區(qū)
lenà表示緩沖區(qū)的長度
flagsà通常為0
返回值:成功則返回實際接收到的字符數(shù),可能會少于你所指定的接收長度。失敗返回-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函數(shù):用新的套接字發(fā)送數(shù)據(jù)給指定的遠(yuǎn)端主機(jī)
原型:int send(int s,const void * msg,int len,unsigned int flags);
參數(shù):sà為前面accept的返回值.即new_fd
msgà一般為常量字符串
lenà表示長度
flagsà通常為0
返回值:成功則返回實際傳送出去的字符數(shù),可能會少于你所指定的發(fā)送長度。失敗返回-1
常用實例:
if(send(new_fd, "hello", 6, 0) == -1){perror("send");close(new_fd);close(sfd);exit(-1);}
8. close函數(shù):當(dāng)使用完文件后若已不再需要則可使用close()關(guān)閉該文件,并且close()會讓數(shù)據(jù)寫回磁盤,并釋放該文件所占用的資源
原型:int close(int fd);
參數(shù):fdà為前面的sfd,new_fd
返回值:若文件順利關(guān)閉則返回0,發(fā)生錯誤時返回-1
常用實例:
close(new_fd); close(sfd);
客戶端:
1. connect函數(shù):用來請求連接遠(yuǎn)程服務(wù)器,將參數(shù)sockfd 的socket 連至參數(shù)serv_addr 指定的服務(wù)器IP和端口號上去。
原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
參數(shù):sockfdà為前面socket的返回值,即sfd
serv_addrà為結(jié)構(gòu)體指針變量,存儲著遠(yuǎn)程服務(wù)器的IP與端口號信息。
addrlenà表示結(jié)構(gòu)體變量的長度
返回值:成功則返回0,失敗返回-1
常用實例:
struct sockaddr_in seraddr;//請求連接服務(wù)器 memset(&seraddr, 0, sizeof(struct sockaddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(8888); //服務(wù)器的端口號 seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); //服務(wù)器的ip if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1) {perror("connect");close(sfd);exit(-1);}
將上面的頭文件以及各個函數(shù)中的代碼全部拷貝就可以形成一個完整的例子,此處省略。
還可以不寫客戶端程序,使用telnet遠(yuǎn)程登錄來檢測我們的服務(wù)器端程序。比如我們的服務(wù)器程序在監(jiān)聽8888端口,我們可以用telnet 192.168.0.101 8888來查看服務(wù)端的狀況。
Example:將一些通用的代碼全部封裝起來,以后要用直接調(diào)用函數(shù)即可。如下:
通用網(wǎng)絡(luò)封裝代碼頭文件:
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
具體的通用函數(shù)封裝如下:
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);//首先創(chuàng)建一個socket,向系統(tǒng)申請 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_ANYif(bind(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1)//將新的socket與制定的ip、port綁定 { perror("bind"); close(sfd); exit(-1); } if(listen(sfd, 10) == -1)//監(jiān)聽它,并設(shè)置其允許最大的連接數(shù)為10個 { perror("listen"); close(sfd); exit(-1); } return sfd;} int tcp_accept(int sfd) //用于服務(wù)端的接收{(diào) 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接受客戶端連接,并創(chuàng)建新的socket為new_fd,將請求連接的客戶端的ip、port保存在結(jié)構(gòu)體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);//向系統(tǒng)注冊申請新的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連接至制定的服務(wù)器網(wǎng)絡(luò)地址serveraddr {perror("connect");close(sfd);exit(-1); } return sfd;} void signalhandler(void) //用于信號處理,讓服務(wù)端在按下Ctrl c或Ctrl 的時候不會退出{sigset_t sigSet; sigemptyset(&sigSet); sigaddset(&sigSet,SIGINT); sigaddset(&sigSet,SIGQUIT); sigprocmask(SIG_BLOCK,&sigSet,NULL);}
服務(wù)器端:
tcp_net_server.c#include "tcp_net_socket.h"int main(int argc, char* argv[]){if(argc < 3){printf("usage:./servertcp ip portn");exit(-1);}signalhandler();int sfd = tcp_init(argv[1], atoi(argv[2])); //或int sfd = tcp_init("192.168.0.164", 8888);while(1) //用while循環(huán)表示可以與多個客戶端接收和發(fā)送,但仍是阻塞模式的{int cfd = tcp_accept(sfd);char buf[512] = {0};if(recv(cfd, buf, sizeof(buf), 0) == -1)//從cfd客戶端接收數(shù)據(jù)存于buf中{perror("recv");close(cfd);close(sfd);exit(-1);}puts(buf);if(send(cfd, "hello world", 12, 0) == -1)//從buf中取向cfd客戶端發(fā)送數(shù)據(jù){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 portn");exit(-1); } int sfd = tcp_connect(argv[1],atoi(argv[2])); char buf[512] = {0}; send(sfd, "hello", 6, 0); //向sfd服務(wù)端發(fā)送數(shù)據(jù) recv(sfd, buf, sizeof(buf), 0); //從sfd服務(wù)端接收數(shù)據(jù) 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/* 備注
可以通過將上述經(jīng)常用到的函數(shù)做成動態(tài)庫,這樣以后再用到的時候就可以直接用。步驟如下:
gcc –fpic –c tcp_net_socket.c –o tcp_net_socket.ogcc –shared tcp_net_socket.o –o libtcp_net_socket.socp 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*/
注:上面的雖然可以實現(xiàn)多個客戶端訪問,但是仍然是阻塞模式(即一個客戶訪問的時候會阻塞不讓另外的客戶訪問)。解決辦法有:
- 多進(jìn)程(因為開銷比較大,所以不常用)
#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;}
- 多線程
#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)//讀大量內(nèi)容{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)//寫大量內(nèi)容{ 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;}*/
- 調(diào)用fcntl將sockfd設(shè)置為非阻塞模式。(不常見)
#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);……
- 多路選擇select
#include <sys/select.h>#include "tcp_net_socket.h"#define MAXCLIENT 10main(){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);}
3.2. 使用UDP協(xié)議的流程圖
UDP通信流程圖如下:
服務(wù)端:socket—bind—recvfrom—sendto—close
客戶端:socket———-sendto—recvfrom—close
·sendto()函數(shù)原型:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
該函數(shù)比send()函數(shù)多了兩個參數(shù),to表示目地機(jī)的IP地址和端口號信息,而tolen常常被賦值為sizeof (struct sockaddr)。sendto 函數(shù)也返回實際發(fā)送的數(shù)據(jù)字節(jié)長度或在出現(xiàn)發(fā)送錯誤時返回-1。
·recvfrom()函數(shù)原型:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
from是一個struct sockaddr類型的變量,該變量保存連接機(jī)的IP地址及端口號。fromlen常置為sizeof (struct sockaddr)。當(dāng)recvfrom()返回時,fromlen包含實際存入from中的數(shù)據(jù)字節(jié)數(shù)。Recvfrom()函數(shù)返回接收到的字節(jié)數(shù)或 當(dāng)出現(xiàn)錯誤時返回-1,并置相應(yīng)的errno。
Example:UDP的基本操作
服務(wù)器端:
#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:%sn", 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])); //此處的端口號要跟服務(wù)器一樣 toaddr.sin_addr.s_addr = inet_addr(argv[1]); //此處為服務(wù)器的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:%sn", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf); close(sfd);}
Example:UDP發(fā)送文件 先發(fā)文件大小 再發(fā)文件內(nèi)容
服務(wù)器端:
#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:%sn", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf); FILE* fp = fopen("1.txt","rb"); struct stat st; //用于獲取文件內(nèi)容的大小 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 512int 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 %dn",filelen); printf("Create a new file!n"); printf("begin to reveive file content!n"); //接收文件的內(nèi)容while(1) { int len = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen); if(len < BUFSIZE)//如果接收的長度小于BUFSIZE,則表示最后一次接收,此時要用break退出循環(huán) { fwrite(buf,sizeof(char),len,fp); break; } fwrite(buf,sizeof(char),len,fp); } printf("receive file finished!n"); close(sfd);}
3.3. 設(shè)置套接口的選項setsockopt的用法
函數(shù)原型:
#include <sys/types.h > #include <sys/socket.h> int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd:標(biāo)識一個套接口的描述字
level:選項定義的層次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
optname:需設(shè)置的選項
optval:指針,指向存放選項值的緩沖區(qū)
optlen:optval緩沖區(qū)長度
全部都必須要放在bind之前,另外通常是用于UDP的。
1. 如果在已經(jīng)處于 ESTABLISHED狀態(tài)下的socket(一般由端口號和標(biāo)志符區(qū)分)調(diào)用closesocket(一般不會立即關(guān)閉而經(jīng)歷TIME_WAIT的過程)后想繼續(xù)重用該socket:
int reuse=1;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));
2. 如果要已經(jīng)處于連接狀態(tài)的soket在調(diào)用closesocket后強(qiáng)制關(guān)閉,不經(jīng)歷TIME_WAIT的過程:
int reuse=0;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)& reuse,sizeof(int));
3.在send(),recv()過程中有時由于網(wǎng)絡(luò)狀況等原因,發(fā)收不能預(yù)期進(jìn)行,而設(shè)置收發(fā)時限:
int nNetTimeout=1000; // 1秒// 發(fā)送時限setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));// 接收時限setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
4.在send()的時候,返回的是實際發(fā)送出去的字節(jié)(同步)或發(fā)送到socket緩沖區(qū)的字節(jié)(異步),系統(tǒng)默認(rèn)的狀態(tài)發(fā)送和接收一次為8688字節(jié)(約為8.5K);在實際的過程中發(fā)送數(shù)據(jù)和接收數(shù)據(jù)量比較大,可以設(shè)置socket緩沖區(qū),而避免了send(),recv()不斷的循環(huán)收發(fā):
// 接收緩沖區(qū)int nRecvBuf=32*1024; // 設(shè)置為32Ksetsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));// 發(fā)送緩沖區(qū)int nSendBuf=32*1024; // 設(shè)置為32Ksetsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
5. 如果在發(fā)送數(shù)據(jù)時,希望不經(jīng)歷由系統(tǒng)緩沖區(qū)到socket緩沖區(qū)的拷貝而影響程序的性能:
int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(int));
6.同上在recv()完成上述功能(默認(rèn)情況是將socket緩沖區(qū)的內(nèi)容拷貝到系統(tǒng)緩沖區(qū)):
int nZero=0;
setsockopt(socket,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
7.一般在發(fā)送UDP數(shù)據(jù)包的時候,希望該socket發(fā)送的數(shù)據(jù)具有廣播特性:
int bBroadcast = 1;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(int));
3.4. 單播、廣播、組播(多播)
多播廣播是用于建立分布式系統(tǒng):例如網(wǎng)絡(luò)游戲、ICQ聊天構(gòu)建、遠(yuǎn)程視頻會議系統(tǒng)的重要工具。使用多播廣播的程序和UDP的單播程序相似。區(qū)別在于多播廣播程序使用特殊的IP地址。
對于單播而言,單播用于兩個主機(jī)之間的端對端通信。
對于廣播而言,廣播用于一個主機(jī)對整個局域網(wǎng)上所有主機(jī)上的數(shù)據(jù)通信。廣播只能用于客戶機(jī)向服務(wù)器廣播,因為客戶機(jī)要指明廣播的IP地址“192.168.0.255”和廣播的端口號。服務(wù)器端bing的時候,綁定的端口號要跟廣播的端口號是同一個。這樣才能收到廣播消息。實例請參考《udp_廣播》。
對于多播而言,也稱為“組播”,將網(wǎng)絡(luò)中同一業(yè)務(wù)類型主機(jī)進(jìn)行了邏輯上的分組,進(jìn)行數(shù)據(jù)收發(fā)的時候其數(shù)據(jù)僅僅在同一分組中進(jìn)行,其他的主機(jī)沒有加入此分組不能收發(fā)對應(yīng)的數(shù)據(jù)。單播和廣播是兩個極端,要么對一個主機(jī)進(jìn)行通信,要么對整個局域網(wǎng)上的主機(jī)進(jìn)行通信。實際情況下,經(jīng)常需要對一組特定的主機(jī)進(jìn)行通信,而不是整個局域網(wǎng)上的所有主機(jī),這就是多播的用途。例如,我們通常所說的討論組。IPv4多播地址采用D類IP地址確定多播的組。在Internet中,多播地址范圍是從224.0.0.0到234.255.255.255。
多播的程序設(shè)計也要使用setsockopt()函數(shù)和getsockopt()函數(shù)來實現(xiàn)。其中對于setsockopt的第二個參數(shù)level不再是SOL_SOCKET,而是IPPROTO_IP;而且第三個參數(shù)optname常見的選項有:
選項IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP加入或者退出一個組播組,通過選項IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP,對一個結(jié)構(gòu)struct ip_mreq類型的變量進(jìn)行控制。
struct ip_mreq原型如下:
struct ip_mreq{struct in_addr imr_multiaddr; /*加入或者退出的多播組IP地址*/struct in_addr imr_interface; /*加入或者退出的網(wǎng)絡(luò)接口IP地址,本機(jī)IP*/};
選項IP_ADD_MEMBERSHIP用于加入某個多播組,之后就可以向這個多播組發(fā)送數(shù)據(jù)或者從多播組接收數(shù)據(jù)。此選項的值為mreq結(jié)構(gòu),成員imr_multiaddr是需要加入的多播組IP地址,成員imr_interface是本機(jī)需要加入多播組的網(wǎng)絡(luò)接口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每次只能加入一個網(wǎng)絡(luò)接口的IP地址到多播組,但并不是一個多播組僅允許一個主機(jī)IP地址加入,可以多次調(diào)用IP_ADD_MEMBERSHIP選項來實現(xiàn)多個IP地址加入同一個廣播組,或者同一個IP地址加入多個廣播組。
選項IP_DROP_MEMBERSHIP用于從一個多播組中退出。例如:
if(-1 == setsockopt(sfd, IPPROTP_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(struct ip_mreq))){perror("setsockopt");exit(-1);}
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。