不同計算機(通過網絡相連)上運行的進程相互通信機制稱為網絡進程間通信(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 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
第三個參數:addrlen 對應的是地址的長度
返回值:成功返回0,出錯返回-1
作用:將套接字與端口號綁定,即把一個ip地址和端口號組合賦給socket
有時需要打印出能被人而不是計算機所理解的地址格式。我們可以利用函數來進行二進制地址格式與點分十進制格式的相互轉換。但是這些函數僅支持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會給調用者綁定一個默認地址!
#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));
建立一個基於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 環境高級編程》
http://xxxxxx/Linuxjc/1147477.html TechArticle