函数原型:int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const timeval* timeout)
select()的调用path如下:sys_select -> core_sys_select -> do_select
1、sys_select()
select属于系统调用,它进入内核态之后就调用sys_select()。
功能:
检查参数“超时时间”是否有效以及对超时时间进行转化。
2、core_sys_select()
功能:
1)检查select()第一个参数,是否大于“当前进程所能打开的最大文件数量(一般为1024)”。若大于,则调整为“当前进程所能打开的最大文件数量”。2)用结构体 fd_set_bits 保存用户传进来的参数:
typedef struct
{
unsigned long *in, *out, *ex;
unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;
注:
in、out、ex分别保存用户注册的感兴趣事件(个人理解为将 fd_set* 转化为二进制位图形式),res_in、res_out、res_ex分别保存用户感兴趣的已发生事件(二进制位图形式,最终需转化为 fd_set*)。
3、do_select()
原理:
1)每次同时从 fd_set_bits.in、fd_set_bits.out、fd_set_bits.ex 取一个值,即取一个 long 字节长度数目的 socket。将三者进行“或运算”。
若运算结果不为0,则表明其中有用户感兴趣的 socket,否则对下一个 long 字节长度数目的 socket 进行判断。2)对 long 字节的每一位进行遍历:(bit=1)
- 调用该位所对应 socket 的驱动程序的poll函数。
- 若 poll 函数 检测到该位上有“POLLIN_SET”事件发生,即“函数返回值 & POLLIN_SET”不为0,即可读,则进一步判断是否为 fd_set_bits.in 该 long 字节的某一位发生事件(每次遍历将bit左移一位,并与 fd_set_bits.in 的该 long 位进行“与运算”,这是因为 POLLIN_SET 仅表示3者中至少一个发生可读事件)。
- 若 poll 函数 检测到该位上有“POLLOUT_SET”事件发生,即可写,则进一步判断是 fd_set_bits.out 该 long 字节的哪一位发生事件。
- 若 poll 函数 检测到该位上有“POLLEX_SET”事件发生,即异常,则进一步判断是 fd_set_bits.ex 该 long 字节的哪一位发生事件。
3)若对所有 fd_set遍历后,满足条件:用户感兴趣的 socket 上有事件发生、超时或当前进程有未处理的信号,则停止继续遍历;否则,调用 schedule_timeout 进行睡眠,直至超时或因事件发生而被唤醒后,重复步骤1、2,继续对所有 fd_set 进行遍历。
4)返回值:返回发生事件的 socket 个数。
源码
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
struct poll_wqueues table;
poll_table *wait;
int retval, i;
rcu_read_lock();
/*根据已经打开fd的位图检查用户打开的fd, 要求对应fd必须打开, 并且返回最大的fd*/
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
/*将当前进程放入自已的等待队列table, 并将该等待队列加入到该测试表wait*/
poll_initwait(&table);
wait = &table.pt;
if (!*timeout)
wait = NULL;
retval = 0;
for (;;) {/*死循环*/
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
long __timeout;
/*注意:可中断的睡眠状态*/
set_current_state(TASK_INTERRUPTIBLE);
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {/*遍历所有fd*/
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
/*
__NFDBITS定义为(8 * sizeof(unsigned long)),即long的位数。
因为一个long代表了__NFDBITS位,所以跳到下一个位图i要增加__NFDBITS
*/
i += __NFDBITS;
continue;
}
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
/*测试每一位*/
if (!(bit & all_bits))
continue;
/*得到file结构指针,并增加引用计数字段f_count*/
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
/*对于socket描述符,f_op->poll对应的函数是sock_poll
注意第三个参数是等待队列,在poll成功后会将本进程唤醒执行*/
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);
/*释放file结构指针,实际就是减小他的一个引用计数字段f_count*/
fput_light(file, fput_needed);
/*根据poll的结果设置状态,要返回select出来的fd数目,所以retval++。
注意:retval是in out ex三个集合的总和*/
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
}
/*
注意前面的set_current_state(TASK_INTERRUPTIBLE);
因为已经进入TASK_INTERRUPTIBLE状态,所以cond_resched回调度其他进程来运行,
这里的目的纯粹是为了增加一个抢占点。被抢占后,由等待队列机制唤醒。
在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),cond_resched是空操作
*/
cond_resched();
}
/*根据poll的结果写回到输出位图里*/
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
}
wait = NULL;
if (retval || !*timeout || signal_pending(current))/*signal_pending前面说过了*/
break;
if(table.error) {
retval = table.error;
break;
}
if (*timeout < 0) {
/*无限等待*/
__timeout = MAX_SCHEDULE_TIMEOUT;
} else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
/* 时间超过MAX_SCHEDULE_TIMEOUT,即schedule_timeout允许的最大值,用一个循环来不断减少超时值*/
__timeout = MAX_SCHEDULE_TIMEOUT - 1;
*timeout -= __timeout;
} else {
/*等待一段时间*/
__timeout = *timeout;
*timeout = 0;
}
/*TASK_INTERRUPTIBLE状态下,调用schedule_timeout的进程会在收到信号后重新得到调度的机会,
即schedule_timeout返回,并返回剩余的时钟周期数
*/
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout += __timeout;
}
/*设置为运行状态*/
__set_current_state(TASK_RUNNING);
/*清理等待队列*/
poll_freewait(&table);
return retval;
}
实例
int tcp_select(fd_set *readfds)
{
// FD_SET(int fd, fd_set *fdset); // 将fd加入set集合
// FD_CLR(int fd, fd_set *fdset); // 将fd从set集合中清除
// FD_ISSET(int fd, fd_set *fdset); // 检测fd是否在set集合中,不在则返回0
// FD_ZERO(fd_set *fdset); // 将set清零使集合中不含任何fd
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
// int select(int nfds, fd_set* readset, fd_set* writeset, fd_set* exceptset, struct timeval* timeout);
// nfds:readset、writeset、exceptset 3个数组中 socket 最大值+1,即告诉 fd_set 检查矢量图的第1位至第 maxfd+1 位。windows平台,该值可以设置不正确。
// readset:select需要监视的可读 socket 的集合
// writeset:select需要监视的可写 socket 的集合
// exceptsetselect需要监视的异常 socket 的集合
// timeout:本次 select 的超时结束时间,NULL:阻塞模式,0为非阻塞模式,其他为超时返回。timeval.tv_sec:秒,timeval.tv_usec:微秒
// 返回值:错误返回 SOCKET_ERROR(-1),否则返回 可读、可写、异常 socket 的总数
//
// 函数功能:select会更新3个集合,把其中不可读、不可写、异常的 socket 去掉。
return select(0, readfds, NULL, NULL, &tv);
}
bool server(int port)
{
WORD wVersionRequested = MAKEWORD(1, 1); // 指定 Windows Sockets API 的版本号
WSADATA wsaData;
// 初始化 Winsock 服务
// 返回值:成功返回0
if (WSAStartup(wVersionRequested, &wsaData) != 0)
{
return false;
}
// int socket(int af, int type, int protocol);
// af:地址描述。AF_INET:IPv4 网络协议的套接字类型,其中 AF 表示 ADDRESS FAMILY 地址族
// type:socket类型。TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。
// protocol:协议。0:不指定。IPPROTO_TCP(TCP)、IPPROTO_UDP(UDP)、IPPROTO_STCP(STCP)、IPPROTO_TIPC(TIPC)。
// 返回值:正确,返回新套接口的描述字,否则返回 INVALID_SOCKET
int nServerSocket = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == nServerSocket)
{
return false;
}
// htons:将主机的无符号短整形数转换成网络字节顺序
// htonl:将主机的无符号长整形数转换成网络字节顺序
/*
网络字节序与主机字节序:
字节序,顾名思义字节的顺序,即大于一个字节(长度超过8bit)的数据在内存中的存放顺序,而一个字节的数据没有顺序的问题。
1、主机字节序即通常所说的大、小端模式,不同的CPU有不同的字节序类型(即,整数在内存中保存的顺序)。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
2、网络字节序:TCP/IP中规定好的一种数据表示格式,采用大端模式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。
*/
sockaddr_in ser;
memset(&ser, 0, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(port);
ser.sin_addr.s_addr = htonl(INADDR_ANY);// 本机上所有 IP 或 inet_addr("192.168.2.71")
// int PASCAL FAR bind(SOCKET s, const sockaddr* name,int namelen)
// s:套接口的描述字
// name:赋予套接口的地址
// namelen:name名字的长度
// 返回值:成功返回0,失败返回-1
if (bind(nServerSocket, (sockaddr*)&ser, sizeof(ser))<0)
{
return false;
}
// int listen(SOCKET s, int backlog)
// backlog:等待连接队列的最大长度
// 返回值:成功返回0,失败返回-1。若队列已满,则客户将收到一个 WSAECONNREFUSED 错误(连接被拒绝)。
if (listen(nServerSocket, 5)<0)
{
return false;
}
fd_set readfds;
vector<int> vecSocket;
vecSocket.push_back(nServerSocket);
while (1)
{
FD_ZERO(&readfds);
for (int nIndex = 0; nIndex < vecSocket.size(); nIndex++)
{
FD_SET(vecSocket.at(nIndex), &readfds);
}
int nReadyCount = tcp_select(&readfds);
if (nReadyCount <= 0)
{
Sleep(10);
continue;
}
for (int nIndex = 0; nIndex < vecSocket.size(); nIndex++)
{
int nReadySocket = vecSocket.at(nIndex);
if (!FD_ISSET(nReadySocket, &readfds))
{
continue;
}
if (nReadySocket == nServerSocket)
{
// SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// addr:客户端地址信息
// 返回值:正确返回客户端 socket,错误返回 INVALID_SOCKET
int nClientSocket = accept(nServerSocket, NULL, NULL);
if (nClientSocket == INVALID_SOCKET)
{
// error
}
else
{
vecSocket.push_back(nClientSocket);
}
}
else
{
char buffer[1024] = "";
// ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen)
// buf:接收数据缓冲区
// len:缓冲区长度
// flags:选项
// from:源地址
// fromlen:from缓冲区长度
//
// 返回值:正确返回接收到的字节数,失败返回-1
int nSize = recvfrom(nReadySocket, buffer, sizeof(buffer), 0, NULL, NULL);
if (nSize > 0)
{
if (sendto(nReadySocket, "receive success", sizeof("receive success"), 0, NULL, NULL) <= 0)
{
// WSAGetLastError()
}
}
}
}
}
}
close()、shutdown()区别
int close(int sockfd):
将套接字的引用计数-1并立即返回,若引用计数<=0,则该套接字不能再作为read或write的第一个参数。int shutdown(int sockfd,int howto):
不管套接字的引用计数是否为0,切断该套接字的所有连接
该函数的行为依赖于howto:
1)SHUT_RD:值为0,关闭连接的读。
2)SHUT_WR:值为1,关闭连接的写。
3)SHUT_RDWR:值为2,关闭连接的读和写。
为什么需要bind
tcp/udp server为什么都必须bind IP和Port:若不bind,server将无法知道应该去哪一块网卡、哪一个Port监听连接(tcp)、接收数据等。
为什么client不需要bind:
1)当client调用connect(tcp)、send(upd)等函数,系统自动bind ip/Port(1024-5000,1-1024是保留端口号),同时在报文中会包含client ip/port等信息。
这样后续server就知道应该向client 哪一个ip/port发送数据,client也会在该端口等待数据到达。
2)若client绑定ip/port,则使用指定的ip/port发送数据。ip/port可同时指定/不指定或只指定其中一个(bind()会返回错误,但仍然可以继续发送数据),不指定的将由系统自动分配。
缺点:会因端口不能被重复占用导致一台机器上只能运行一个client程序。
3)udp在调用 recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) 接收数据时,
除非src_addr/addrlen填NULL,否则参数addrlen初始值必须指定为sizeof(sockaddr_in)或sizeof(sockaddr),否则recvfrom()直接返回错误10022(参数错误)或10014(地址错误)。
send/recv、sendto/recvfrom
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
ssize_t recv(int sockfd, 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 recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
注:两者可以搭配使用
flags:
1)MSG_DONTROUTE:目的主机在本地网络上,发送数据不使用网关。(send函数使用的标志)
2)MSG_OOB:可以发送、接收带外数据。(带外数据:当通信一方有重要的数据需通知对方时,为了快速发送数据,协议一般不使用与普通数据相同的通道,而是使用另外的通道。)
3)MSG_PEEK:recv函数只从系统缓冲区中读取内容,而不清除系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容。
4)MSG_WAITALL:阻塞recv函数,直至收到 len 个字节数据或发生错误。send()流程:
(1)比较待发送数据的长度len和套接字s的发送缓冲的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;
(2)检查协议s的发送缓冲中的数据是否正在发送,若是,则等待协议把数据发送完,否则比较s的发送缓冲区的剩余空间和len;
(3)若len大于剩余空间大小,则send一直等待协议把s的发送缓冲中的数据发送完;
(4)若len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里。注:
1)并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区。
2)send函数把buf中的数据成功copy到s的发送缓冲后就返回了,但是这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。recv()流程:
(1)等待s的发送缓冲中的数据被协议传送完毕,若协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;
(2)若接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。
当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中。注:
若协议接收到的数据大于buf的长度,则需要调用多次recv函数才能把s的接收缓冲中的数据copy完。udp应该只能使用sendto/recvfrom方式,因为无连接模式,所以需要通过这种方式指定sendto 终端或获取recvfrom 源端地址。
tcp/udp流程
tcp:
WSAStartup() –>> socket() –>> bind() –>> listen() –>> accept() –>> send()/recv()、sendto()/recvfrom() –>> closesocket()/WSACleanup()
WSAStartup() –>> socket() –>> connect() –>> send()/recv()、sendto()/recvfrom() –>> closesocket()/WSACleanup()udp:
WSAStartup() –>> socket() –>> bind() –>> sendto()/recvfrom() –>> closesocket()/WSACleanup()
WSAStartup() –>> socket() –>> sendto()/recvfrom() –>> closesocket()/WSACleanup()
参考资料:
http://blog.csdn.net/martin_liang/article/details/9124911