網路IPC:通訊端,網路ipc接字
網路處理序間通訊:socket API簡介
不同電腦(通過網路相連)上啟動並執行進程相互連信機制稱為網路處理序間通訊(network IPC)。
在本地可以通過進程PID來唯一標識一個進程,但是在網路中這是行不通的。其實TCP/IP協議族已經幫我們解決了這個問題,網路層的“ip地址”可以唯一標識網路中的主機,而傳輸層的“協議+連接埠”可以唯一標識主機中的應用程式(進程)。這樣利用三元組(ip地址,協議,連接埠)構成通訊端,就可以標識網路的進程了,網路中的進程通訊就可以利用這個標誌與其它進程進行互動。
通訊端是通訊連接埠的抽象!通過通訊端網路IPC介面,進程能夠使用該介面和其他進程通訊。
幾個定義:
通訊端描述符
通訊端是端點的抽象。與應用進程要使用檔案描述符訪問檔案一樣,訪問通訊端也需要用通訊端描述符。通訊端描述符在UNIX系統中是用檔案描述符實現的。
要建立一個通訊端,可以調用socket函數。
#include<sys/socket.h>int socket(int domain, int type, int protocol);
參數:
作用:socket()用於建立一個socket描述符(socket descriptor),它唯一標識一個socket。
網路位元組序
網路通訊協定指定了位元組序,因此異構電腦系統能夠交換協議資訊而不會混淆位元組序。TCP/IP協議棧採用大端位元組序。應用進程交換格式化資料時,位元組序問題就會出現。對於TCP/IP,地址用網路位元組序來表示,所以應用進程有時需要在處理器的位元組序與網路位元組序之間轉換。
#include<arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);
這些函數名很好記,h表示host,n表示network, l表示32位長整數,s表示16位短整數
在將一個地址綁定到socket的時候,請先將主機位元組序轉換成為網路位元組序,對主機位元組序不要做任何假定,務必將其轉化為網路位元組序再賦給socket!
將通訊端與地址綁定
與用戶端的通訊端關聯的地址意義不大,可以讓系統選擇一個預設的地址。然而,對於伺服器,需要給一個接收用戶端請求的通訊端綁定一個眾所周知的地址。用戶端應有一種方法用以串連伺服器的地址,最簡單的方法就是為伺服器保留一個地址並且在/etc/services或某個名字服務(name service)中註冊。
可以用bind函數來搞定這個問題:
#include <sys/types.h> #include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數:
第一個參數:bind()函數把一個地址族中的特定地址賦給該sockfd(通訊端描述字)。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和連接埠號碼組合賦給socket。
第二個參數:struct sockaddr *指標,指向要綁定給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不同而不同:
地址格式地址標識了特定通訊域中的通訊端端點,地址格式與特定的通訊域相關。為使不同格式地址能夠被傳入到通訊端函數,地址需被強轉為通用的地址結構sockaddr表示。
//標頭檔#include<netinet/in.h>
struct sockaddr 是一個通用地址結構,該結構定義如下:
struct sockaddr{ sa_family_t sa_family; char sa_data[14];}IPV4網際網路域:
//ipv4對應的是: /* 網路地址 */struct in_addr { uint32_t s_addr; /* address in network byte order */};struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */};IPv6網際網路域:
//ipv6對應的是: struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ };struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; Unix域對應的是:
#define UNIX_PATH_MAX 108struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
第三個參數:addrlen 對應的是地址的長度
傳回值:成功返回0,出錯返回-1
作用:將通訊端與連接埠號碼綁定,即把一個ip地址和連接埠號碼組合賦給socket
點分十進位IP與網路位元組序IP之間的轉換
有時需要列印出能被人而不是電腦所理解的地址格式。我們可以利用函數來進行二進位地址格式與點分十進位格式的相互轉換。但是這些函數僅支援IPv4地址。
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> //點分十進位IP轉換網路位元組序IP int inet_aton(const char *cp, struct in_addr *inp); //點分十進位IP轉換網路位元組序IP in_addr_t inet_addr(const char *cp); //網路位元組序IP 轉化點分十進位IP char *inet_ntoa(struct in_addr in);
其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數介面是void* 類型!
#include <arpa/inet.h>//網路位元組序IP 轉化點分十進位IPconst char *inet_ntop(int af, const void *src,char *dst, socklen_t size);//點分十進位IP轉換網路位元組序IPint inet_pton(int af, const char *src, void *dst);
監聽
如果作為一個伺服器,在調用socket()、bind()之後就會調用listen()來監聽這個socket,如果用戶端這時調用connect()發出串連請求,伺服器端就會接收到這個請求。
伺服器調用 listen 來宣告可以接收串連請求!
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog);
參數:sockfd為要監聽的socket描述字,backlog為相應socket可以排隊的最大串連個數
傳回值:成功返回0,出錯返回-1
作用:socket函數建立一個通訊端時,預設是一個主動通訊端,listen函數把一個未調用connect的未串連的通訊端轉換成一個被動通訊端,指示核心應接收指向該通訊端的串連請求。(主動/客戶 -> 被動/伺服器)
串連
如果是連線導向的網路服務,在開始交換資料前,都要在請求服務的進程通訊端(用戶端)和提供服務的進程通訊端(伺服器)之間建立一個串連,使用connect函數:
#include <sys/types.h> #include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數:第一個參數sockfd為用戶端的socket描述字,第二參數為伺服器的socket地址,第三個參數為socket地址的長度。
傳回值:成功返回0,出錯返回-1
作用:用戶端通過調用connect函數來建立與TCP伺服器的串連
注意:在connect中所指定的地址是想與之通訊的伺服器位址。如果sockfd沒有綁定到一個地址,connect會給調用者綁定一個預設地址!
使用accept函數獲得串連請求並建立串連
#include <sys/types.h> #include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
參 數 :第一個參數為伺服器的socket描述字,第二個參數為指向struct sockaddr *的指標,用於返回用戶端的協議地址,第三個參數為協議地址的長度
傳回值:如果accpet成功,那麼其傳回值是由核心自動產生的一個全新的描述字,該描述符串連到調用connect的用戶端。這個新的通訊端描述符和原始的通訊端描述符具有相同的通訊端類型和地址族。
注 意:傳給accept的原始通訊端沒有關聯到這個串連,而是繼續保持可用狀態並接受其它串連請求!
通俗點來說,accept的第一個參數為伺服器的socket描述字,是伺服器開始調用socket()函數產生的,稱為監聽socket描述字;而accept函數返回的是已串連的socket描述字。一個伺服器通常通常僅僅只建立一個監聽socket描述字,它在該伺服器的生命週期內一直存在。核心為每個由伺服器處理序接受的客戶串連建立了一個已串連socket描述字,當伺服器完成了對某個客戶的服務,相應的已串連socket描述字就被關閉。
資料轉送
既然通訊端端點表示檔案描述符,那麼只要建立串連,就可以使用write和read來通過通訊端通訊了。
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);ssize_t read(int fd, void *buf, size_t count);
write()會把指標buf所指的記憶體寫入count個位元組到參數fd所指的檔案內(檔案讀寫位置也會隨之移動),如果順利write()會返回實際寫入的位元組數。當有錯誤發生時則返回-1,錯誤碼存入errno中!
read()會把參數fd所指的檔案傳送nbyte個位元組到buf指標所指的記憶體中,成功返回讀取的位元組數,出錯返回-1並設定errno,如果在調read之前已到達檔案末尾,則這次read返回0 。
如果想指定多個選項、從多個用戶端接收資料包或發送帶外資料,需要採用6個傳遞資料的通訊端函數中的一個。
三個函數用來發送資料:
#include <sys/types.h>#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
三個函數用來接收資料:
#include <sys/types.h>#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
關閉通訊端描述符
close函數用來關閉檔案描述符:
#include <unistd.h>int close(int fd);
注意:close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發TCP用戶端向伺服器發送終止串連請求。
地址“重用”
預設條件下,一個通訊端不能與一個已在使用中的本地地址捆綁。但有時會需要“重用”地址。因為每一個串連都由本地地址和遠端地址的組合唯一確定,所以只要遠端地址不同,兩個套介面與一個地址捆綁並無大礙。為了通知套介面實現不要因為一個地址已被一個套介面使用就不讓它與另一個套介面捆綁,應用程式可在bind()調用前先設定SO_REUSEADDR選項。請注意僅在bind()調用時該選項才被解釋;故此無需(但也無害)將一個不會共用地址的通訊端設定該選項,或者在bind()對這個或其他套介面無影響情況下設定或清除這一選項。
解決這個問題的方法是使用setsockopt()設定socket描述符的 選項SO_REUSEADDR為1,表示允許建立連接埠號碼相同但IP地址不同的多個socket描述符。 在server代碼的socket()和bind()調用之間插入如下代碼:
int opt=1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
socket通訊基本流程:
程式碼範例
建立一個基於TCP的socket API
伺服器:
/************************************************************************* > File Name: server.c > Author:Lynn-Zhang > Mail: iynu17@yeah.net > Created Time: Fri 29 Jul 2016 12:15:28 PM CST ************************************************************************/#include<stdio.h>#include<netinet/in.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#include<stdlib.h>#include<arpa/inet.h>#include<pthread.h>static void usage(const char* proc){ printf("Usage: %s [ip] [port]\n",proc);}void *thread_run(void *arg){ printf("create a new thread\n"); int fd=(int)arg; char buf[1024]; while(1) { //伺服器端將通訊端描述符中到資料讀到buf並列印,再將自己的回複寫入通訊端描述符 memset(buf,'\0',sizeof(buf)); ssize_t _s=read(fd,buf,sizeof(buf)-1); if(_s>0) { buf[_s]='\0'; printf("client:# %s",buf); printf("server:$ "); fflush(stdout); //伺服器將回複寫入fd memset(buf,'\0',sizeof(buf)); ssize_t _in=read(0,buf,sizeof(buf)-1); if(_in>=0) { buf[_in-1]='\0'; write(fd,buf,strlen(buf)); } printf("please wait ...\n"); } else if(_s==0) { printf("client close...\n"); break; } else { printf("read error ...\n"); break; } } return (void*)0;}int main(int argc,char *argv[]){ //參數必須能構成完整的socket if(argc!=3) { usage(argv[0]); exit(1); } //建立伺服器端socket int listen_sock=socket(AF_INET,SOCK_STREAM,0); if(listen_sock<0) { perror("socket"); return 1; } struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(atoi(argv[2])); local.sin_addr.s_addr=inet_addr(argv[1]); int opt=1; if(setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))<0) { perror("setsockopet error\n"); return -1; } //將通訊端綁定到伺服器端的ip地址和連接埠號碼綁定 if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0) { perror("bind"); return 2; } //建立監聽隊列,等待通訊端的串連請求 listen(listen_sock,5); struct sockaddr_in peer; socklen_t len=sizeof(peer); while(1) { //獲得串連請求並建立串連 int client_sock=accept(listen_sock,(struct sockaddr*)&peer,&len); if(client_sock<0) { perror("accept faild ...\n"); return 3; } printf("get a new link,socket -> %s:%d\n",inet_ntoa(peer.sin_addr)); pthread_t id; pthread_create(&id,NULL,thread_run,(void*)client_sock); pthread_detach(id);// pid_t id=fork();// if(id==0)// {//child// char buf[1024];// while(1)// {// //將監聽到的套接子描述符指定檔案描述中的資料讀到buf中// memset(buf,'\0',sizeof(buf));// ssize_t _s=read(client_sock,buf,sizeof(buf)-1);// if(_s>0)// {// buf[_s-1]='\0'// printf("client:# %s\n",buf); // printf("server:$ ");// fflush(stdout);// memset(buf,'\0',sizeof(buf));// ssize_t _s=read(0,buf,sizeof(buf)-1);// if(_s>0)// { // buf[_s-1]='\0';// write(client_sock,buf,strlen(buf));// }// else// {// printf("Fail !\n");// }// }// else// {// printf("read done...\n");// break;// }// }//// }// else// {//father// waitpid(-1,NULL,WNOHANG);// }// } close(listen_sock); return 0;}
用戶端:
/************************************************************************* > File Name: client.c > Author:Lynn-Zhang > Mail: iynu17@yeah.net > Created Time: Fri 29 Jul 2016 09:00:01 AM CST ************************************************************************/#include<stdio.h>#include<netinet/in.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#include<stdlib.h>#include<arpa/inet.h>#include<errno.h>#include<pthread.h>static usage(const char* proc){ printf("Usage: %s [ip] [port]\n",proc);}int main(int argc,char* argv[]){ //傳入的參數是一個完整的socket(ip地址+連接埠號碼) if(argc!=3) { usage(argv[0]); exit(1); } //建立一個通訊端描述符 int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { perror("socket"); return 2; } //IPv4網際網路域(AF_INET)中,通訊端地址用sockaddr_in表示 struct sockaddr_in remote; remote.sin_family=AF_INET; //socket通訊域 remote.sin_port=htons(atoi(argv[2])); //連接埠號碼 remote.sin_addr.s_addr=inet_addr(argv[1]); //ip地址 //建立串連請求 int ret=connect(sock,(struct sockaddr*)&remote,sizeof(remote)); if(ret<0) { printf("connect failed ... ,errno is :%d,errstring is: %s\n",errno,strerror(errno)); return 3; } printf("connect success ...\n"); char buf[1024]; while(1) { //從標準輸入將資料讀入buf中,再寫入sock中 memset(buf,'\0',sizeof(buf)); printf("client:# "); fflush(stdout); ssize_t _s=read(0,buf,sizeof(buf)-1); fflush(stdin); if(_s<0) { perror("read\n"); break; } buf[_s]='\0'; write(sock,buf,strlen(buf)); if(strcmp(buf,"quit")==0) { printf("quit!\n"); break; } _s=read(sock,buf,sizeof(buf)); if(_s>0) { buf[_s]='\0'; printf("server:$ %s\n",buf); } } close(sock); printf("sock close"); return 0;}
伺服器:
用戶端:
本篇總結若有不足,希望指正 (。⌒∇⌒)
部分參考:
吳秦 http://www.cnblogs.com/skynet/
《Unix 環境進階編程》