UNP总结 Chapter 15~17 Unix域协议、非阻塞式I/O、ioctl操作
一、Unix域协议
Unix域协议并不是一个实际的协议族,它只是在同一台主机上进行客户-服务器通信时,使用与在不同主机上的客户和服务器间通信时相同的API(套接口或XTI)的一种方法。
当客户和服务器在同一台主机上时,Unix域协议是IPC通信方式的一种替代品。
Unix域提供了两种类型的套接口:字节流套接口(与TCP类似)和数据报套接口(与UDP类似)。
1.Unix域套接口地址结构
struct sockaddr_un { sa_family_t sun_family; /* AF_LOCAL */ char sun_path[104]; /* null-terminated pathname */ };
存放sun_path数组中的路径名必须以空字符结尾
2.socketpair函数
socketpair函数建立一对相互连接的套接口,这个函数支队Unix域套接口使用。
#include <sys/socket.h> int socketpair(int family, int type, int protocol, int sockfd[2]); //返回: 成功返回0,出错返回-1
family必须为AF_LOCAL,protocol必须为0,type可以是SOCK_STREAM或SOCK_DGRAM,新创建的两个套接口描述字作为sockfd[0]和sockfd[1]返回
创建的两个套接口是没有名字的,即没有涉及隐式bind。
指定type参数为SOCK_STREAM调用socketpair所得到的结果称为流管道(stream pipe),这和一般的Unix管道(由pipe函数生成)类似,但流管道是全双工的,即两个描述字都是可读写的。
3.描述符传递
一般传递描述符的方法:
- 在fork调用后,子进程共享父进程的所有打开的描述字
- 在调用exec时所有描述字仍保持打开
第一个例子中进程打开一个描述字,调用fork,然后父进程关闭描述字,让子进程处理这个描述字。这样将一个打开的描述字从父进程传递到子进程。
两个进程之间传递描述符涉及的步骤:
1).创建一个字节流的或数据报的Unix域套接口
2).进程可以用任何返回描述字的Unix函数打开一个描述字:譬如open, pipe, mkfifo, socket或accept。
3).发送进程建立一个msghdr结构,其中包含要传递的描述字。
4).接收进程调用recvmsg在来自步骤1的Unix域套接字上接收这个描述符,传递描述字不是传递描述字的编号,而是在接收进程中创建一个新的描述字,指向内核的文件表中与发送进程发送的描述字相同的项。
4.程序实例
#include "unp.h" int my_open(const char *pathname, int mode) { int fd, sockfd[2], status; pid_t childpid; char c, argsockfd[10], argmode[10]; Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd); if ( (childpid = Fork()) == 0) { /* child process */ Close(sockfd[0]); snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]); snprintf(argmode, sizeof(argmode), "%d", mode); execl("./openfile", "openfile", argsockfd, pathname, argmode, (char *) NULL); err_sys("execl error"); } /* parent process - wait for the child to terminate */ Close(sockfd[1]); /* close the end we don't use */ Waitpid(childpid, &status, 0); if (WIFEXITED(status) == 0) err_quit("child did not terminate"); if ( (status = WEXITSTATUS(status)) == 0) Read_fd(sockfd[0], &c, 1, &fd); else { errno = status; /* set errno value from child's status */ fd = -1; } Close(sockfd[0]); return (fd); }
二、非阻塞式I/O
1.概述
1).输入操作: read, readv, recv, recvfrom和recvmsg函数。
2).输出操作: write, writev, send, sendto和sendmsg函数。
3).接收外来连接: accept函数
4).初始化外出的连接: 用于TCP的connect函数
2.非阻塞读和写
我们维护两个缓冲区: to容纳从标准输入到服务器去的数据,fr容纳自服务器到标准输出来的数据。
这里程序仅给出包含非阻塞的前半部分:
#include "unp.h" void str_cli(FILE *fp, int sockfd) { int maxfdp1, val, stdineof; ssize_t n, nwritten; fd_set rset, wset; char to[MAXLINE], fr[MAXLINE]; char *toiptr, *tooptr, *friptr, *froptr; val = Fcntl(sockfd, F_GETFL, 0); Fcntl(sockfd, F_SETFL, val | O_NONBLOCK); val = Fcntl(STDIN_FILENO, F_GETFL, 0); Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK); val = Fcntl(STDOUT_FILENO, F_GETFL, 0); Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK); toiptr = tooptr = to; /* initialize buffer pointers */ friptr = froptr = fr; stdineof = 0; maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1; for ( ; ; ) { FD_ZERO(&rset); FD_ZERO(&wset); if (stdineof == 0 && toiptr < &to[MAXLINE]) FD_SET(STDIN_FILENO, &rset); /* read from stdin */ if (friptr < &fr[MAXLINE]) FD_SET(sockfd, &rset); /* read from socket */ if (tooptr != toiptr) FD_SET(sockfd, &wset); /* data to write to socket */ if (froptr != friptr) FD_SET(STDOUT_FILENO, &wset); /* data to write to stdout */ Select(maxfdp1, &rset, &wset, NULL, NULL);
3.非阻塞connect
非阻塞的connect有三种用途:
1). 我们可以在三路握手同时做一些其他的处理。完成一个connect要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网。
2). 可以用这种技术同时建立多个连接。这在Web浏览器中很普遍
3). 由于我们用select等待连接的完成,因此可以给select设置一个时间限制,从而缩短connect的超时时间。
非阻塞connect虽然听似简单,却有一些必须处理的细节
1).即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况;
2).源自Berkeley的实现(和Posix.1g)有两条与select和非阻塞IO相关的规则:
- 当连接建立成功时,套接字描述符变成可写;
- 当连接出错时,套接子描述符变成既可读又可写;
注意:当一个套接口出错时,它会被select调用标记为既可读又可写;
程序实例:
#include "unp.h" int connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec) { int flags, n, error; socklen_t len; fd_set rset, wset; struct timeval tval; flags = Fcntl(sockfd, F_GETFL, 0); Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); error = 0; if ( (n = connect(sockfd, saptr, salen)) < 0) if (errno != EINPROGRESS) return (-1); /* Do whatever we want while the connect is taking place. */ if (n == 0) goto done; /* connect completed immediately */ FD_ZERO(&rset); FD_SET(sockfd, &rset); wset = rset; tval.tv_sec = nsec; tval.tv_usec = 0; if ( (n = Select(sockfd + 1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0) { close(sockfd); /* timeout */ errno = ETIMEDOUT; return (-1); } if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) { len = sizeof(error); if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) return (-1); /* Solaris pending error */ } else err_quit("select error: sockfd not set"); done: Fcntl(sockfd, F_SETFL, flags); /* restore file status flags */ if (error) { close(sockfd); /* just in case */ errno = error; return (-1); } return (0); }
4.非阻塞accept
阻塞模式下,服务器会一直阻塞在accept调用上,知道其他某个客户建立一个连接为止,但是在此期间,服务器单纯阻塞在accept调用上,无法处理任何其他已就绪的描述符
非阻塞accept模式下解决办法
1).当使用select获悉某个监听套接字上何时有已完成连接准备被accept时候,总是把这个监听套接字设置为非阻塞
2).在后续的accept调用忽略以下错误:EWOULDBLOCK(客户终止连接时)、ECONNABORTED(客户终止连接时)、EPROTO(客户终止连接时)和EINTR(如果有信号被捕获)
三、ioctl操作
网络程序中ioctl常用于在程序启动时获得主机上所有接口的信息:接口的地址,接口是否支持广播,是否支持多播,等等。
#include <unistd.h> int ioctl(int fd, int request, ... /* void *arg */ ); //返回:若成功0,出错-1
其中第三个参数总是一个指针,但指针的类型依赖于request参数,我们可以把和网络相关的请求(request)划分为6类:
- 套接口操作
- 文件操作
- 接口操作
- ARP高速缓存操作
- 路由表操作
- 流系统
用法详见UNP