不一样计算机(经过网络相连)上运行的进程相互通讯机制称为网络进程间通讯(network IPC)。编程
在本地能够经过进程PID来惟一标识一个进程,可是在网络中这是行不通的。其实TCP/IP协议族已经帮咱们解决了这个问题,网络层的“ip地址”能够惟一标识网络中的主机,而传输层的“协议+端口”能够惟一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)构成套接字,就能够标识网络的进程了,网络中的进程通讯就能够利用这个标志与其它进程进行交互。服务器
套接字是通讯端口的抽象!经过套接字网络IPC接口,进程可以使用该接口和其余进程通讯。网络
几个定义:dom
- IP地址:即依照TCP/IP协议分配给本地主机的网络地址,两个进程要通信,任一进程首先要知道通信对方的位置,即对方的IP。
- 端口号:用来辨别本地通信进程,一个本地的进程在通信时均会占用一个端口号,不一样的进程端口号不一样,所以在通信前必需要分配一个没有被访问的端口号。
- 链接:指两个进程间的通信链路。
- 半相关:网络中用一个三元组能够在全局惟一标志一个进程:(协议,本地地址,本地端口号)这样一个三元组,叫作一个半相关,它指定链接的每半部分。
- 全相关:一个完整的网间进程通讯须要由两个进程组成,而且只能使用同一种高层协议。也就是说,不可能通讯的一端用TCP协议,而另外一端用UDP协议。所以一个完整的网间通讯须要一个五元组来标识:(协议,本地地址,本地端口号,远地地址,远地端口号),这样一个五元组,叫作一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或彻底指定组成一链接。
套接字是端点的抽象。与应用进程要使用文件描述符访问文件同样,访问套接字也须要用套接字描述符。套接字描述符在UNIX系统中是用文件描述符实现的。socket
要建立一个套接字,能够调用socket函数。函数
#include<sys/socket.h> int socket(int domain, int type, int protocol);
参数:ui
做用:socket()用于建立一个socket描述符(socket descriptor),它惟一标识一个socket。spa
网络协议指定了字节序,所以异构计算机系统可以交换协议信息而不会混淆字节序。TCP/IP协议栈采用大端字节序。应用进程交换格式化数据时,字节序问题就会出现。对于TCP/IP,地址用网络字节序来表示,因此应用进程有时须要在处理器的字节序与网络字节序之间转换。.net
#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);
sendto()适用于已链接的数据报或流式套接口发送数据。
参数:
三个函数用来接收数据:
#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: 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; }
服务器:
客户端:
注意:
UDP没有创建链接的过程!
建立一个基于udp协议的套接字,使用socket函数时第二个参数不能传递SOCK_STREAM,而是传递SOCK_DGRAM
如建立一个基于IPv4地址族的UDP套接字: socket(AF_INET,SOCK_DGRAM, 0);
一般用于基于UDP协议的I/O通常使用 recvfrom 和 sendto 两个函数进行数据收发!
服务器:
/************************************************************************* > File Name: server.c > Author:Lynn-Zhang > Mail: iynu17@yeah.net > Created Time: Wed 03 Aug 2016 01:14:30 PM CST ************************************************************************/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<string.h> void usage(const char* proc) { printf("Usage: %s [ip] [port]\n",proc); } int main(int argc,char* argv[]) { //要求输出配套到套接字 if(argc!=3) { usage(argv[0]); return 1; } //创建套接字描述符 int sock=socket(AF_INET,SOCK_DGRAM,0); if(sock<0) { perror("socket"); return 2; } 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 ret=bind(sock,(struct sockaddr*)&local,sizeof(local)); if(ret<0) { perror("bind"); return 3; } int done=0; struct sockaddr_in peer; socklen_t len=sizeof(peer); char buf[1024]; while(!done) { memset(buf,'\0',sizeof(buf)); recvfrom(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,&len); printf("##########################\n"); printf("get a client , socket:%s:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port)); printf("client:%s,echo client!\n",buf); printf("##########################\n"); printf("server:"); fflush(stdout); memset(buf,'\0',sizeof(buf)); ssize_t _s= read(0,buf,sizeof(buf)-1); buf[_s]='\0'; sendto(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,len); } return 0; }
客户端:
/************************************************************************* > File Name: client.c > Author:Lynn-Zhang > Mail: iynu17@yeah.net > Created Time: Wed 03 Aug 2016 03:48:21 PM CST ************************************************************************/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<string.h> static void usage(const char* proc) { printf("Usage:%s [remote_ip] [remote_port]\n",proc); } int main(int argc,char* argv[]) { if(argc!=3) { usage(argv[0]); return 1; } int sock=socket(AF_INET,SOCK_DGRAM,0); if(sock<0) { perror("socket"); return 2; } struct sockaddr_in remote; remote.sin_family=AF_INET; remote.sin_port=htons(atoi(argv[2])); remote.sin_addr.s_addr=inet_addr(argv[1]); int done=0; char buf[1024]; struct sockaddr_in peer; socklen_t len=sizeof(peer); while(!done) { printf("Please Enter: "); fflush(stdout); ssize_t _s=read(0,buf,sizeof(buf)-1); if(_s>0) { buf[_s-1]='\0'; sendto(sock,buf,sizeof(buf),0,(struct sockaddr*)&remote,sizeof(remote)); memset(buf,'\0',sizeof(buf)); recvfrom(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,&len); printf("server echo %s\nsocket: %s:%d\n",buf,inet_ntoa(peer.sin_addr),ntohs(peer.sin_port)); } } return 0; }
运行结果:
服务器:
客户端:
部分参考:
吴秦 http://www.cnblogs.com/skynet/
《Unix 环境高级编程》