嵌入式網絡編程

在Linux中的網絡編程是通過socket接口來進行的。是一種文件描述符。socket也有一個類似於打開文件的函數調用,該函數返回一個整型的socket描述符,隨後的連接建立、數據傳輸等操作都是通過socket來實現的。
常見的socket有3種類型:

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

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

(3)原始socket 原始套接字允許對底層協議如IP或ICMP進行直接訪問,它功能強大但使用較爲不便,主要用於一些協議的開發。

1:sockaddr/_in:是用來 保存 socket 信息的 .在 建立 socketadd 或 sockaddr_in 後,就可以對該 socket 進行適當的操作了.

struct sockaddr { 
unsigned short sa_family; /*地址族*/ 
char sa_data[14]; /*14 字節的協議地址,包含該 socket 的 IP 地址和端口號。*/ 
}; 
struct sockaddr_in { 
short int sin_family; /*地址族*/ 
unsigned short int sin_port; /*端口號*/ 
struct in_addr sin_addr; /*IP 地址*/ 
unsigned char sin_zero[8]; /*填充 0 以保持與 struct sockaddr 同樣大小*/ 
}; 常用

sa_family有一下幾種:
AF_INET:IPv4 協議
AF_INET6:IPv6 協議
AF_LOCAL:UNIX 域協議
AF_LINK:鏈路地址協議
AF_KEY:密鑰套接字(socket)

2.數據存儲優先順序
計算機數據存儲有兩種字節優先順序:高位字節優先(大端模式)和低位字節優先(小段模式)。Internet上以高位字節優先的順序在網絡傳輸,而PC機通常採用小端模式,因此有時候需要對兩個字節存儲優先順序進行轉換。用到了4個函數:htons()、ntohs()、htonl()和ntohl()。h代表host,n代表network,s代表short,l代表long。通常16位的IP端口號用s,而IP地址用l。
函數格式說明

uint16_t htons(unit16_t host16bit) 參數是主機字節序的16bit數據

uint32_t htonl(unit32_t host32bit) 參數是主機字節序的32bit數據

uint16_t ntohs(unit16_t net16bit) 參數是網絡字節序的16bit數據

uint32_t ntohs(unit32_t net32bit) 參數是網絡字節序的32bit數據

地址格式轉化

IP地址通常由數字加點(192.168.0.1)的形式表示,而在struct in_addr中使用的IP地址是由32位整數表示,爲了轉換可以使用下面三個函數:

IPv4中用到的函數有inet_aton、inet_addr和inet_ntoa

IPv4和IPv6兼容的函數有inet_pton和inet_ntop,這裏,p表示十進制,n表示二進制。

int inet_pton(int family, const char *strptr, void *addrptr)

int inet_ntop(int family, void *addrptr, char *strptr, size_t len)

family傳入AF_INET或AF_INET6,addrptr是轉化後的地址,strptr是要轉化的值,len是轉化後值的大小,成功返回0,出錯返回-1

int inet_aton(const char *cp,struct in_addr *inp);  
char *inet_ntoa(struct in_addr in);  
in_addr_t inet_addr(const char *cp);  

其中inet_aton將a.b.c.d形式的IP轉換爲32位的IP,存儲在inp指針裏面;inet_ntoa是將32位IP轉換爲a.b.c.d的格式;inet_addr將一個點分十進制的IP轉換成一個長整數型數。

名字地址轉換
通常,人們在使用過程中不願記憶冗長的IP地址,因此,使用主機名是很好的選擇。gethostbyname()將主機名轉化爲IP地址,gethostbyaddr()則是逆操作,將IP地址轉換爲主機名。它們都涉及到一個hostent的結構體,如下:

struct hostent  
{  
      char *h_name; /*正式主機名*/  
      char **h_aliases; /*主機別名*/  
      int h_addrtype; /*地址類型*/  
      int h_length; /*地址字節長度*/  
      char **h_addr_list; /*指向IPv4或IPv6的地址指針數組*/  
};  

我們調用gethostbyname()或者gethostbyaddr()後就能返回hostent結構體的相關信息。

3.socket編程的基本函數有socket()、bind()、listen()、accept()、sent()、sendto()、recv()、以及recvfrom()等,具體介紹如下:
這裏寫圖片描述
基於TCP-服務器:創建socket()—>bind()綁定IP地址、端口信息到socket上—>listen()設置允許最大連接數—>accept()等待來自客戶端的連接請求—>send()、recv()或者read()、write()收發數據—>關閉連接。
基於TCP-客戶端:創建socket()—>設置要連接的服務器IP地址和端口等屬性—>connect()連接服務器—>send()、recv()或read()、write()收發數據—>關閉網絡連接。

循環服務器:服務器在同一時間只能響應一個客戶端的請求。

socket(...);  
bind(...);  
listen(...);  
while(1)  
{  
   accept(...);  
   process(...);  
   close(...);  
}  

3.1. socket:該函數用於建立一個 socket 連接,可指定 socket 類型等信息。在建立了 socket連接之後,可對 socketaddr 或 sockaddr_in 進行初始化,以保存所建立的 socket 信息。

sockfd = socket(AF_INET,SOCK_STREAM,0))

int socket(int family, int type, int protocol) 函數原型。family就是上面那幾種,
type:
SOCK_STREAM:字節流套接字socket
SOCK_DGRAM:數據報套接字 socket
SOCK_RAW:原始套接字 socket 。

protoco:0(原始套接字除外)

成功:非負套接字描述符
出錯:−1

調用sockfd函數後,建立了一個socket連接,接着就對sockaddr進行初始化。
server_sockaddr.sin_family=AF_INET; //使用ipv4協議
server_sockaddr.sin_port=htons(SERVPORT);// 設置服務器監聽端口號,若直接爲零,則系統可隨機選擇未被使用的端口號
server_sockaddr.sin_addr.s_addr=INADDR_ANY; //系統會自動填入本機ip地址
bzero(&(server_sockaddr.sin_zero),8);這個是把剩餘位置零,匹配另一種結構體

3.2. bind:該函數是用於將本地 IP 地址綁定端口號的,若綁定其他地址則不能成功。另外,它主要用於 TCP 的連接,而在 UDP 的連接中則無必要。

bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))

int bind(int sockfd,struct sockaddr *my_addr, int addrlen)
sockfd:套接字描述符
my_addr:本地sockaddr地址信息,轉化成sockaddr格式了,原來是in格式的,in賦值簡單。
addrlen:地址長度
端口號和地址在 my_addr 中給出了,若不指定地址,則內核隨意分配一個臨時端口給該 應用程序。
成功爲0,否則-1

3.3. listen():在服務程序成功建立套接字和地址進行綁定後,調用listen()函數來創建一個等待隊列,在其中存放未處理的客戶端連接請求。

listen(sockfd,BACKLOG)

listen函數把一個未連接的套接口轉換成一個被動套接口,指示內核應接受指向該套接口的連接請求。根據TCP狀態轉換圖,調用listen導致套接口從CLOSED狀態轉換到LISTEN狀態。

本函數的第二個參數規定了內核應該爲相應套接口排隊的最大連接個數。
爲了更好的理解backlog參數,我們必須認識到內核爲任何一個給定的監聽套接口維護兩個隊列:
1、未完成連接隊列(incomplete connection queue),每個這樣的SYN分節對應其中一項:已由某個客戶發出併到達服務器,而服務器正在等待完成相應的TCP三路握手過程。這些套接口處於SYN_RCVD狀態。
2、已完成連接隊列(completed connection queue),每個已完成TCP三路握手過程的客戶對應其中一項。這些套接口處於ESTABLISHED狀態。
現在backlog用來確定已完成隊列(完成三次握手等待accept)的長度,而不再是已完成隊列和未完成連接隊列之和。未完成隊列(incomplete connection queue)的長度現在由/proc/sys/net/ipv4/tcp_max_syn_backlog設置。
爲了接受連接,先用socket()創建一個套接口的描述字,然後用listen()創建套接口併爲申請進入的連接建立一個後備日誌,然後便可用accept()接受連接了。listen()僅適用於支持連接的套接口,如SOCK_STREAM類型的。
int PASCAL FAR listen( SOCKET s, int backlog);
S:用於標識一個已捆綁未連接套接口的描述字。
backlog:等待連接隊列的最大長度,默認缺省爲5。
成功爲0,否則-1
3.4 accept():服務器調用listen()創建等待隊列之後,調用accept()等待並接收客戶端的連接請求。通常從由bind()所創建的等待隊列中取出第一個未處理的連接請求。

client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)

int accept(int sockfd, struct sockaddr *addr(客戶端地址), socklen_t *addrlen(地址長度))
實際上是這樣的: accept函數指定服務端去接受客戶端的連接,接收後,返回了客戶端套接字的標識,且獲得了客戶端套接字的“地方”(包括客戶端IP和端口信息等)。你調用 accept() 告訴它你有空閒的連接。它將返回一個新的套接字文 件描述符!這樣你就有兩個套接字了,原來的一個還在偵聽你的那個端口, 新的在準備發送 (send()) 和接收 ( recv()) 數據。這就是這個過程!在系統調用 send() 和 recv() 中你應該使用新的套接字描述符 new_fd。如果你只想讓一個連接進來,那麼你可以使用 close() 去關閉原 來的文件描述符 sockfd 來避免同一個端口更多的連接。
accept函數非常地癡情,癡心不改:如果沒有客戶端套接字去請求,它便會在那裏一直癡癡地等下去,直到永遠(阻塞式)。當你第一次調用 socket() 建立套接口描述符的時候,內核就將他設置爲阻塞。如果你不想套接口阻塞,你就要調用函數 fcntl():通過設置套接口爲非阻塞,你能夠有效地”詢問”套接口以獲得信息,但是一般來說輪詢不是一個好主意,會浪費cpu時間,更好的方法是用 select()方法 去查詢是否有數據要讀進來select()–多路同步 I/O
select() 讓你可以同時監視多個套接口。如果你想知道的話,那麼他就會告訴你哪個套接口準備讀,哪個又準備好了寫,哪個套接口又發生了例外 (exception)。
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
成功爲0,否則-1

3.5. connect:該函數在TCP中是用於 bind 的之後的 client 端,用於與服務器端建立連接,而在 UDP 中由於沒有了 bind 函數,因此用 connect 有點類似 bind 函數的作用。

client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)

int connect(int sockfd, struct sockaddr *serv_addr(服務器端地址), int addrlen)
用來將參數sockfd 的socket 連至參數serv_addr 指定的網絡地址。connect函數將使用參數sockfd中的套接字連接到參數serv_addr中指定的服務器
參數一:套接字描述符
參數二:指向數據結構sockaddr的指針,其中包括目的端口和IP地址
參數三:參數二sockaddr的長度,可以通過sizeof(struct sockaddr)獲得
成功則返回0,失敗返回非0,錯誤碼GetLastError()。

其中socket沒有什麼可疑問的,主要是創建一個套接字用於與服務端交換數據,並且通常它 會迅速返回,此時並沒有數據通過網卡發送出去,而緊隨其後的connect函數則會產生網絡數據的發送,TCP的三次握手也正是在此時開 始,connect會先發送一個SYN包給服務端,並從最初始的CLOSED狀態進入到SYN_SENT狀態,在此狀態等待服務端的確認包,通常情況下這 個確認包會很快到達,以致於我們根本無法使用netstat命令看到SYN_SENT狀態的存在,不過我們可以做一個極端情況的模擬,讓客戶端去連接一個 隨意指定服務器(如IP地址爲88.88.88.88),因爲該服務器很明顯不會反饋給我們SYN包的確認包(SYN ACK),客戶端就會在一定時間內處於SYN_SENT狀態,並在預定的超時時間(比如3分鐘)之後從connect函數返回,connect調用一旦失 敗(沒能到達ESTABLISHED狀態)這個套接字便不可用,若要再次調用connect函數則必須要重新使用socket函數創建新的套接字。
至此。算是連上了,接下來就要傳輸數據進行握手了。

3.6. send()和recv():這兩個函數分別用於發送和接收數據,可以用在TCP或者UDP中。用在UDP時可以在connect()建立連接之後再用。

sendbytes=send(sockfd,"hello",5,0)
recvbytes=recv(client_fd,buf,MAXDATASIZE,0)

int send(int sockfd, const void *msg, int len, int flags)
int recv(int sockfd, void *buf, int len, unsigned int flags)
msg指向要發送內容的指針,len發送內容長度,flags一般爲0
buf指向存放接受數據的緩衝區。

ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
1) send先比較發送數據的長度nbytes和套接字sockfd的發送緩衝區的長度,如果nbytes > 套接字sockfd的發送緩衝區的長度, 該函數返回SOCKET_ERROR;
2) 如果nbtyes <= 套接字sockfd的發送緩衝區的長度,那麼send先檢查協議是否正在發送sockfd的發送緩衝區中的數據,如果是就等待協議把數據發送完,如果協議 還沒有開始發送sockfd的發送緩衝區中的數據或者sockfd的發送緩衝區中沒有數據,那麼send就比較sockfd的發送緩衝區的剩餘空間和 nbytes
3) 如果 nbytes > 套接字sockfd的發送緩衝區剩餘空間的長度,send就一起等待協議把套接字sockfd的發送緩衝區中的數據發送完
4) 如果 nbytes < 套接字sockfd的發送緩衝區剩餘空間大小,send就僅僅把buf中的數據copy到剩餘空間裏(注意並不是send把套接字sockfd的發送緩衝 區中的數據傳到連接的另一端的,而是協議傳送的,send僅僅是把buf中的數據copy到套接字sockfd的發送緩衝區的剩餘空間裏)。
5) 如果send函數copy成功,就返回實際copy的字節數,如果send在copy數據時出現錯誤,那麼send就返回SOCKET_ERROR; 如果在等待協議傳送數據時網絡斷開,send函數也返回SOCKET_ERROR。
6) send函數把buff中的數據成功copy到sockfd的改善緩衝區的剩餘空間後它就返回了,但是此時這些數據並不一定馬上被傳到連接的另一端。如果 協議在後續的傳送過程中出現網絡錯誤的話,那麼下一個socket函數就會返回SOCKET_ERROR。(每一個除send的socket函數在執行的 最開始總要先等待套接字的發送緩衝區中的數據被協議傳遞完畢才能繼續,如果在等待時出現網絡錯誤那麼該socket函數就返回SOCKET_ERROR)
7) 在unix系統下,如果send在等待協議傳送數據時網絡斷開,調用send的進程會接收到一個SIGPIPE信號,進程對該信號的處理是進程終止。

3.7. sendto()和recvfrom():作用與前兩個類似,當用在TCP時,後面的幾個與地址有關參數不起作用,等同於send()、recv();用在UDP時,可用在之前沒有使用connect的情況下,這兩個函數可自動尋找指定地址並進行連接。

int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen(地址長度))

int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen(地址長度))

3.8 close(sockfd);關閉套接字。

**4.**UDP傳輸模式
這裏寫圖片描述

基於UDP-服務器:創建socket()—>bind()綁定IP地址、端口等信息到socket上—>循環接受數據,用recvfrom()—>關閉網絡連接。
基於UDP-客戶端:創建socket()—>bind()綁定IP地址、端口等信息到socket上—>設置對方IP地址和端口信息—>sendto()發送數據—>關閉網絡連接。

5:服務器類型
循環服務器:
TCP循環服務器一次只能處理一個客戶端的請求,只有這個客戶的所有請求都滿足後,纔可以繼續後面的請求。這樣如果一個客戶端佔住服務器不放,其他的客戶都不能工作,所以TCP服務器一般很少用循環服務器模型。而UDP循環服務器可以同時相應多個客戶端的請求。

UDP循環服務器

socket(...);  
bind(...);  
while(1)  
{  
   recvfrom(...);  
   process(...);  
   sendto(...);  
}  

TCP循環服務器

socket(...);  
bind(...);  
listen(...);  
while(1)  
{  
   accept(...);  
   process(...);  
   close(...);  
}  

併發服務器:服務器在同一時刻可以響應多個客戶端的請求。
TCP併發服務器
併發服務器的思想是每一個客戶端的請求並不由服務器直接處理,而是有服務器創建一個子進程或者線程來處理。

socket(...);  
bind(...);  
listen(...);  
while(1)  
{  
   accept(...);  
   /*fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,也就是兩個進程可以做完全相同的事,但如果初始參數或者傳入的變量不同,兩個進程也可以做不同的事。
   fork函數總是“調用一次,返回兩次”,在父進程中調用一次,在父進程和子進程中各返回一次。fork在子進程中的返回值是0,而在父進程中的返回值則是子進程的id。 
   子進程在創建的時候會複製父進程的當前狀態*/
   if(fork()==0)  
     {  
        process(...);  
        close(...);  
        exit(...);  
     }  
     close(...);  
}  

6.在實際情況中,人們往往遇到多個客戶端連接服務端的情況。由於之前介紹的如connet、recv、send都是阻塞性函數,若資源沒有準備好,則調用該函數的進程將進入睡眠狀態,這樣就無法處理I/O多路複用的情況了。由於在Linux中把socket也作爲一種特殊文件描述符,這給用戶的處理帶來了很大方便。
6.1:fcntl可以改變已打開文件的屬性。
int fcntl(int fd,int cmd,….int arg)
第三個參數可以是一個整數,也可以是指向一個結構的指針flock;
fd是文件描述符,第二個參數是CMD

在基於套接字的異步I/O(當一個描述符已準備好,可以啓動I/O時,進程會通知內核。)中,當從套接字中讀取數據時,或者當套接字寫隊列中空間變得可用時,可以安排要發送的i信號SIGIO。啓動異步I/O的步驟:
1)建立套接字所有權,這樣信號就可以被傳遞到合適的進程。
在fcntl中使用F_SETOWN命令 或
在fcntl中使用FIOSETOWN命令 或
在fcntl中使用FIOCSPGRP命令
2)通知套接字當I/O操作不會阻塞時發信號。
在fcntl中使用F_SETFL命令並且啓用文件標誌O_ASYNC
在ioctl中使用FIOASYNC命令。

/*調fcntl 函數設置非阻塞參數*/ 
if((flags=fcntl( sockfd, F_SETFL, 0))<0) //先清除標誌
perror("fcntl F_SETFL"); 
flag |= O_NONBLOCK; 
if(fcntl(fd,F_SETEL,flags)<0) //設置非阻塞I/O
perror("fcntl"); 
//在server中listen後加入後,accept資源不可用時,系統就會返回。

6.2 使用fcntl函數雖然可以實現非阻塞I/O或信號驅動I/O,但在實際使用時往往會對資源是否準備完畢進行循環測試,這樣就大大增加了不必要的CPU資源。
在這裏可以使用select函數來解決這個問題,同時,使用select函數還可以設置等待時間,可以說功能更加強大。

Int select(int maxfdpl,fd_set *restrict readfds, fd_set *restrict writefds,fd_set *restrict exceptfds,  
struct timeval *restrict tvptr)
/*返回值:準備就緒的描述符數目;若超時,返回0,若出錯返回-1
第一個參數是最大文件描述符編號值加1,這的目的是指定我們關注的最大描述符,內核就在此範圍內尋找打開的位。
第234參數分別代表讀集,寫集,異常集。
對讀集的一個描述符進行read不會阻塞,則表示準備好了
對寫集的一個描述符進行write不會阻塞,則表示準備好了
若描述符有一個未決異常,則異常集準備好了
最後一個參數是tv_sec和tv_unsec可設置等待時間。微妙集*/

FD_ZERO(fd_set *set) 清除一個文件描述符集
FD_SET(int fd,fd_set *set)將一個文件描述符加入集中
FD_CLR(int fd,fd_set *set)清除一個文件描述符從集中
FD_ISSET(int fd,fd_set *set)測試集中一個給定文件描述符是否有變化

select函數主要就是要把這些文件描述符添加進各種集,通過返回值可知道3個描述符集中已準備好的描述符數之和,接着就用FD_ISSET函數測試該集中的一個給定文件描述符是否處於打開狀態。

程序編寫步驟:
FD_ZERO(&set);
FD_SET(fd,&set);
FD_SET(STDIN_FILENO,&set);
select;
if(FD_ISSET(fd,&set)){}
也是在listen後運行,accept後的代碼都在大括號內。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章