进程间通信的机制包括信号量、共享内存、管道和消息队列等,但是这些机制只能实现在一台计算的进程间通信。本文将介绍另外一种进程间通信的机制——套接字,可以实现计算机网络中的通信。
1、套接字
套接字是一种通信机制,通过使用这种通信机制,客户/服务器系统的开发既可以在本地单机上进行,也可以跨网络进行。套接字明确地将客户和服务器区分开来,可以实现将多个客户连接到一个服务器。
2、套接字连接
套接字维持连接:首先,服务器应用程序通过系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,不能与其他进程共享。然后服务器进程通过系统调用bind给套接字起个名字。命名后服务器进程就开始等待客户连接到这个命名套接字。最后,服务器通过系统调用accept来接受客户的连接。而对于客户来说,首先调用socket创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
3、套接字属性
套接字的特性由3个属性决定:域(domain)、类型(type)和协议(protocol)。
1)套按字的域
域指定套接字通信中使用的网络介质。常用的是AF_INET指的是Internet网络,底层协议是网际协议(IP),地址是IP地址。AF_UNIX是UNIX文件系统域,底层协议是文件输入/输出,地址是文件名。
2)套接字类型:流(stream)是数据报(datagram)
流套接字提供的是一个有序、可靠、双向字节流的连接。因此发送的数据可以确保不会丢失、复制或乱序到达,并且在这一过程中发生的错误也不会显示出来。大的消息将分片、传输、再重组。流套接字由类型SOCK_STREAM指定,在AF_INET域中通过TCP/IP连接实现,它同时也是AF_UNIX域中常用的套接字类型。
数据报套接字由类型SOCK_DGRAM指定。它不建立和维持一个连接,对可以发送的数据报的长度有限制。在传输过程中,数据报可能会丢失、复制或乱序到达。数据报套接字在AF_INET域中是通过UDP/IP连接实现的,提供一种无序的不可靠服务。但从资源的角度来看,相对开销比较小,因为不需要维持网络连接,而且因为无需花费时间建立连接,所以速度也很快。
3)套接字协议
4、套接字操作
1)创建套接字
socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。
- #include <sys/types.h>
- #include <sys/socket.h>
- int socket(int domain, int type, int protocol);
建立套接字后,可以用read和write系统调用利用返回的描述符在套接字上发送和接收数据。
2)套接字地址
每个套接字域都有自己的地址格式。对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述。
- #include <sys/un.h>
- struct sockaddr_un
- {
- sa_family_t sun_family; /* AF_UNIX */
- char sun_path[]; /* pathname */
- };
- #incldue <netinet/in.h>
- struct sockaddr_in
- {
- short int sin_family; /* AF_INET */
- unsigned short int sin_port; /* Port Number */
- struct in_addr sin_addr; /* Internet address */
- };
通过套接字接口传递的端口(sin_port)和地址(sin_addr)都是二进制数字。而不同计算机使用不同的字节序来表示整数,因此客户和服务器程序必须在传输之前,将它们的内部整数表示方式转换为网络字节序。
- #include <netinet/in.h>
- unsigned long int htonl(unsigned long int hostlong);
- unsigned short int htons(unsigned short int hostshort);
- unsigned long int ntohl(unsigned long int netlong);
- unsigned short int ntohs(unsigned short int netshort);
上述这些函数将16位和32位整数在主机字节序和标准网络字节序之间进行转换。IP地址in_addr的定义为
- struct in_addr
- {
- unsigned long int s_addr;
- };
3)命名套接字
要想让通过socket调用创建的套接字可以被其他进程使用,服务器程序就必须给该套接字命名,AF_UNIX套接字关联到一个文件系统的路径名,AF_INET套接字关联到一个IP端口号。
- #include <sys/socket.h>
- int bind(int socket, const struct sockaddr *address, size_t address_len);
4)创建套接字队列
为了能够在套接字上接受进行的连接,服务器程序必须创建一个队列来保存未处理的请求。
- #include <sys/socket.h>
- int listen(int socket, int backlog);
5)接受连接
服务器程序创建并命名了套接字之后,可以通过accept系统调用来等待客户建立对该套接字的连接。
- #include <sys/socket.h>
- int accept(int socket, struct sockaddr *address, size_t *address_len);
如果套接字队列中没有未处理的连接,accept将阻塞直到有客户建立连接为止。如果想改变这一行为,可以设置套接字文件描述符指定O_NONBLOCK标志。
- int flags = fcntl(socket, F_GETFL, 0);
- fcntl(socket, F_SETFL, O_NONBLOCK | flags);
客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器。
- #include <sys/socket.h>
- int connect(int socket, const struct sockaddr *address, size_t address_len);
7)关闭套接字
通过调用close函数终止服务器和客户上的套接字连接。
5、网络信息
对于一台主机,如何通过已经地址信息或主机名获取其他网络信息,是非常重要的。在Linux系统中,头文件netdb.h定义了与之相关的一些函数接口。
- #include <netdb.h>
- struct hostent *gethostbyaddr(const void *addr, size_t len, int type);
- struct hostent *gethostbyname(const char *name);
- int gethostname(char *name, int namelength); /* 将当前主机的名字写入name指向的字符串中 */
这些函数返回hostent结构体,结构体定义如下
- struct hostent
- {
- char *h_name; /* name of the host */
- char **h_aliases; /* list of aliases(nicknames) */
- int h_addrtype; /* address type */
- int h_length; /* length in bytes of the address */
- char **h_addr_list; /* list of address(network order) */
- };
- #include <netdb.h>
- struct servent *getservbyname(const char *name, const char *proto);
- struct servent *getservbyport(int port, const char *proto);
- struct servent
- {
- char *s_name; /* name of the service */
- char **s_aliases; /* list of aliases(alternative names) */
- int s_port; /* The IP port number */
- char *s_proto; /* The service type, usually "tcp" or "udp" */
- };
- #include <arpa/inet.h>
- char *inet_ntoa(struct in_addr in); /* 将一个因特网主机地址转换成一上点分四元组格式的字符串 */
Linux系统通常是以超级服务器的方式来提供多项网络服务。超级服务器程序也就是因特网守护进程xinetd或inetd同时监听多个端口地址上的连接。当有客户连接到某项服务时,守护进程就运行相应的服务器。这使得针对各项网络服务的服务器不需要一直运行着,可以在需要时启动。配置文件通常是/etc/xinetd.conf和/etc/xinetd.d目录中的文件。Ubuntu下图形化工具常用的是Bootup-Manager。
7、select系统调用
select系统调用允许程序同时在多个底层文件描述符上等待输入的到达。select函数对数据结构fd_set进行操作,它是由打开的文件描述符构成的集合。
- #include <sys/types.h>
- #include <sys/time.h>
- void FD_ZERO(fd_set *fdset); /* 初始化集合 */
- void FD_CLR(int fd, fd_set *fdset); /* 在集合中设置由参数fd传递的文件描述符 */
- void FD_SET(int fd, fd_set *fdset); /* 在集合中清除由参数fd传递的文件描述符 */
- int FD_ISSET(int fd, fd_set *fdset); /* 判断由fd指定的文件描述符是否在fset集合中 */
- struct timeval
- {
- time_t tv_sec; /* seconds */
- long tv_usec; /* microseconds */
- };
- #include <sys/types.h>
- #include <sys/time.h>
- int select(int nfds, fd_set *readfds, fd_set *wrimefds, fd_set *errorfds, struct timeval *timeout);
8、数据报
当客户需要发送一个短小的查询请求给服务器,并且期望收到一个短小的响应时,一般使用由UDP提供的服务。因为UDP提供的是不可靠的服务,所以数据报或响应可能会丢失,,因此需要检查错误并在必要时重传。UDP提供使用两个专用的系统调用sendto和recvfrom来代替原来使用在套接字上的read和write调用。
- int sento(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen_t tolen);
- int recvfrom(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);