首页 > 网络 > 网络编程 > I/O复用、套接字选项
2018
03-07

I/O复用、套接字选项

I/O复用

概述

之前的学习中,我们的程序输入往往只有标准输入一项,即从屏幕读取输入,读取方式是程序阻塞在读取函数上,等待输入。进入到网络编程后,程序还还需要从套接字中进行输入,输入方式也是阻塞等待套接字可读。如果一个程序即要从标准输入读取数据,又要从套接字中读取数据,那么程序同一时间只能阻塞一其中一个接口中,从而造成另一接口的数据不能及时读取或影响。这样,进程就需要一种可以同时检测多个接口是否有数据可读(准确说是I/O条件是否就绪,即输入是否已经准备好被读取,或是描述符已能承接更多的输出,或是文件描述符是否发生异常)的能力,一旦检测到有数据可读,再从对应的接口上读取数据。这种能力称为I/O复用。通过select和poll两个函数支持。

I/O复用典型使用在以下场合:

  • 当客户需要处理多个描述符(通常是交互输入和网络套接字)时,必须使用I/O复用。
  • 当客户需要同时处理监听套接字,又要处理已连接的套接字时,一般要使用多路复用。
  • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

I/O模型

linux可用的I/O模式一共有5种,分别是:

  • 阻塞式I/O。
  • 非阻塞式I/O。
  • I/O复用(select和poll和epoll)。
  • 信号驱动I/O(SIGIO)。
  • 异步I/O(POSIX的aio_系列函数)。

I/O复用、套接字选项 - 第1张  | rs232的博客

select函数

该函数允许进程指示内核等待多个事件中的任何一个发生,并只在一个或多个事件发生或经历一段指定的时间后才唤醒它。
作为一个例子,我们可以调用select,告知内核仅在下列情况发生时才返回:

  • 集合{1, 4, 5}中的任何描述符准备好读;
  • 集合{2, 7}中的任何描述符准备好写;
  • 集合{1, 4}中的任何描述符有异常条件要处理;
  • 已经历10.2秒。

也就是说,我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。描述符不局限于套接字描述符,任何描述符都可以用select来测试。

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

功能:
测试描述符集合中的I/O是否就绪。
参数:

  • nfds:最大文件描述符加1,select会逐个去检测0~nfds-1所指的文件描述符。
  • readfds:需要检测是否可读的文件描述符集合,可以为空,表示不关心可读描述符。正在读取的套接字被对端关闭时可读条件会满足;监听套接字上有新的连接时可读条件满足。
  • writefds:需要检测是否可写的文件描述符集合,可以为空。正在写入的套接字被对面关闭时可写条件满足并产生SIGPIPE信号。
  • exceptfds:需要检测是否发生异常的文件描述符集合,可以为空。TCP套接字中的带外数据可以通过它来检测。TCP的带外数据是指具有紧急模式标志的数据。它可以无视TCP的发送缓冲区直接由套接字发送。
  • timeout:等待的时间,可以有三种可能:
    NULL:表示永远等待下去,直到任何集合中的任何一个描述符I/O条件就绪。
    具体的时间值:在有一个描述符准备好I/O时返回,等待的时间超过指定值时也返回。
    时间值为0:检查描述符后立即返回,称为轮询(polling)。timeout中的tv_sec成员和tv_usec成员都要指定成0.

返回值:
如果描述符中有I/O条件就绪,则返回条件就绪I/O的个数;如果在任何描述符就绪之前定时器到时,则返回0.出错返回-1,并设置errno。比如被中断打断时,返回EINTER。
注意:
三个文件描述符集参数都是值-结果类型,调用函数时,通过参数指定所关心的描述符,函数返回时,用于指示哪些描述符已就绪,未就绪文件描述符对应比特位将会被清0。因此,重新调用select函数时,需要再次初始化描述符集合。

对于文件描述符集合的操作可以使用以下4个宏:

void FD_ZERO(fd_set *set); #清空一个文件描述符集
void FD_SET(int fd, fd_set *set); #将fd加入文件描述符集
void FD_CLR(int fd, fd_set *set); #将fd从文件描述符集中删除
int FD_ISSET(int fd, fd_set *set); #检测fd是否在文件描述符集中

示例:tcp_select_client.c tcp_select_server.c

shutdown函数

使用close函数将使套接字进行正常的结束连接四次握手,四次握手之后,套接字两个方向上的读写都被终止了。但有时候我们希望只关闭其中一半的连接,比如上面的tcp_select_client.c中,客户端在终端上收到stop之后,如果直接就break退出,则套接字将被关闭,停留在传输线路上的数据包将不能被收到,造成用户数据的丢失。如果在终端收到stop之后,只关闭套接字的写入端,读出端仍保留,则可以一直等到所有数据读完之后再退出,保证数据的完整性。完成关闭套接字在一个方向上的数据连接功能的函数是shutdown。

#include <sys/socket.h>
int shutdown(int sockfd, int how);

功能:
关闭套接字一个方向或两个方向的连接。
参数:
sockfd:套接字描述符
how:关闭选项,可以取以下值:
SHUT_RD:关闭套接字的读取端,此时,套接字中将不再有数据可接收,而且套接字中接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数,该套接字接收的来自对端的任何数据都会被确认,然后被丢弃。
SHUT_WR:关闭套接字的写入端,称为半关闭。效果是当前留在套接字缓冲区中的数据将被发送,然后TCP进入半关闭流程,即四次握手的前两次握手。不能对半关闭后的套接字调用任何写函数。
SHUT_RDWR:关闭套接字的读半部和写半部,相当于调用shutdown两次,一次SHUT_RD,一次SHUT_WR。
返回值:
成功返回0,失败返回-1
示例:修改版的tcp_select_client_by_shutdown.c

poll函数

poll函数提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能:
等待一个或多个文件描述符的I/O就绪条件。
参数:
fds:指向一个struct pollfd类型的结构体数组的首地址,每个数据元素都是一个pollfd结构,用于指定测试某个给定描述符的条件。

struct pollfd
{
int fd; /*文件描述符,可为负,此时函数将忽略该结构体成员*/
short events; /*fd关心的事件*/
short revents; /*fd已经发生的事件*/
};

要测试的条件由events成员指定,events和revents中通过不同的位来指定不同的条件,以下是用于指定events标志和测试revents标志的一些常用值。

I/O复用、套接字选项 - 第2张  | rs232的博客

nfds:用于指定参数一中结构体数组的有效成员个数,不包括fd为负数的成员。
timeout:指定poll函数返回前等待多长时间,单位是毫秒,可以有以下取值:
<0:永远等待
0:立即返回,不阻塞进程
>0:等待指定的毫秒数。
返回值:
若有就绪描述符则返回其数目,若超时则返回0,出错返回-1
示例:tcp_poll_client.c

套接字选项

使用以下两个函数来获取和设置套接字选项:

#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

功能:
获取或设置套接字选项
参数:
sockfd:已打开的套接字描述符
level:级别,指定系统解释选项的代码或为通用套接字代码,或为某个特定协议的代码。
optname:套接字选项名。
optval:指向某个变量的指针,不同的选项在设置和获取时需要提供一个变量用于存储选项的值。
optlen:选项的长度。
返回值:
成功返回0,失败返回-1.

I/O复用、套接字选项 - 第3张  | rs232的博客

I/O复用、套接字选项 - 第4张  | rs232的博客
示例:设置SO_REUSEADDR选项 tcp_select_server_reuseaddr.c

其他功能函数

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能:
获取一个打开的的套接字在本地关联的地址,比如一个已经经过connect但是没有调用bind的TCP套接字在本地的IP地址和端口号。

int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能:
获取对端的地址,比如调用accept之后服务端用于获取客户端的IP地址和端口号。

#include <netdb.h>
extern int h_errno;
struct hostent *gethostbyname(const char *name);

功能:
查找主机名对应的主机信息。
参数:
name:主机名,如localhost, www.baidu.com
返回值:
成功返回主机结构体,失败返回NULL。

struct hostent
{
char *h_name; /*主机的正式名称*/
char **h_aliases; /*主机的预备名称指针列表*/
int h_addrtype; /*主机地址类型,一般是AF_INET*/
int h_length; /*主机地址长度*/
char **h_addr_list; /*主机网络地址列表,以二维数组形式存放,每一个数组成员 是一个网络字节序形式的IP地址*/
}
#define h_addr h_addr_list[0] /*主机的第一个网络地址*/
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);

功能:
通过二进制的IP地址查找主机信息。
参数:
addr:网络字节序的IP地址,比如struct in_addr结构体指针。
len:地址长度
type:地址类型。
返回值:
成功返回主机结构体,失败返回NULL。

最后编辑:
作者:rs232
这个作者貌似有点懒,什么都没有留下。

留下一个回复

你的email不会被公开。