網路進程間通信:socket API簡介 不同電腦(通過網路相連)上運行的進程相互通信機制稱為網路進程間通信(network IPC)。 在本地可以通過進程PID來唯一標識一個進程,但是在網路中這是行不通的。其實TCP/IP協議族已經幫我們解決了這個問題,網路層的“ip地址”可以唯一標識網路中的主 ...
網路進程間通信:socket API簡介
不同電腦(通過網路相連)上運行的進程相互通信機制稱為網路進程間通信(network IPC)。
在本地可以通過進程PID來唯一標識一個進程,但是在網路中這是行不通的。其實TCP/IP協議族已經幫我們解決了這個問題,網路層的“ip地址”可以唯一標識網路中的主機,而傳輸層的“協議+埠”可以唯一標識主機中的應用程式(進程)。這樣利用三元組(ip地址,協議,埠)構成套接字,就可以標識網路的進程了,網路中的進程通信就可以利用這個標誌與其它進程進行交互。
套接字是通信埠的抽象!通過套接字網路IPC介面,進程能夠使用該介面和其他進程通信。
幾個定義:
- IP地址:即依照TCP/IP協議分配給本地主機的網路地址,兩個進程要通訊,任一進程首先要知道通訊對方的位置,即對方的IP。
- 埠號:用來辨別本地通訊進程,一個本地的進程在通訊時均會占用一個埠號,不同的進程埠號不同,因此在通訊前必須要分配一個沒有被訪問的埠號。
- 連接:指兩個進程間的通訊鏈路。
- 半相關:網路中用一個三元組可以在全局唯一標誌一個進程:(協議,本地地址,本地埠號)這樣一個三元組,叫做一個半相關,它指定連接的每半部分。
- 全相關:一個完整的網間進程通信需要由兩個進程組成,並且只能使用同一種高層協議。也就是說,不可能通信的一端用TCP協議,而另一端用UDP協議。因此一個完整的網間通信需要一個五元組來標識:(協議,本地地址,本地埠號,遠地地址,遠地埠號),這樣一個五元組,叫做一個相關(association),即兩個協議相同的半相關才能組合成一個合適的相關,或完全指定組成一連接。
套接字描述符
套接字是端點的抽象。與應用進程要使用文件描述符訪問文件一樣,訪問套接字也需要用套接字描述符。套接字描述符在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 108 struct 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 轉化點分十進位IP const char *inet_ntop(int af, const void *src,char *dst, socklen_t size); //點分十進位IP轉換網路位元組序IP int 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()、bind()、listen()之後,就會監聽指定的socket地址了。
- TCP客戶端依次調用socket()、connect()之後就向TCP伺服器發送了一個連接請求。
- TCP伺服器監聽到這個請求之後,就會調用accept()函數取接收請求,這樣連接就建立好了。
- 之後就可以開始網路I/O操作了,即類同於普通文件的讀寫I/O操作。
代碼示例
建立一個基於TCP的socket API
伺服器:
/************************************************************************* > File Name: server.c > Author:Lynn-Zhang > Mail: [email protected] > 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: [email protected] > 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 環境高級編程》