锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

当前位置:锐英源 / 在线教育 / SOCKET网络通信开发公开课 / Sockets-Multiplexing and Concurrent Servers Socket多路复用和并发服务器
服务方向
人工智能数据处理
人工智能培训
kaldi数据准备
小语种语音识别
语音识别标注
语音识别系统
语音识别转文字
kaldi开发技术服务
软件开发
运动控制卡上位机
机械加工软件
软件开发培训
Java 安卓移动开发
VC++
C#软件
汇编和破解
驱动开发
联系方式
固话:0371-63888850
手机:138-0381-0136
Q Q:396806883
微信:ryysoft

3.23 Sockets-Multiplexing and Concurrent Servers Socket多路复用和并发服务器


3.23.1 多路复用和异步Socket I/O

  • 程序可能需要同时处理从多个源发来的数据。这些源里可能有文件描述符,socket描述符。比如,关注读取:
    • 客户端或服务器端可能等待终端上或文件或管道上发出起的输入
    • 服务器可能监听新的连接
    • 客户端或服务器可能准备好接受已经连接上socket的输入,或者从无连接的socket上输入。

(言下之意,在底层缓冲里有数据时接收才是最优化,在底层缓冲有空间发送数据时发送才是最优化。如果在没有数据时,强行开始接收,要么是阻塞等待时间长,要么是过了等待时间有些数据就接收不到了)

  • 在大部分情况下,默认的I/O是阻塞的:程序不会从读取调用上返回,直到数据读取到,不管是部分数据量或是最低限额数据量。在程序阻塞时,它什么也做不了。
  • 程序也可以用非阻塞模式操作,但是这需要经常做些轮询操作;也就是说,在循环里要不断检查输入的状态。这常常会导致CPU周期上的浪费。
  • 有下面几个方法能够提供更好的解决方法:
    • 使用select(),pselect()或poll()函数进行多路复用。这依然是一种轮询,虽然有变化,不过这允许多个不同的I/O通道,并且内核对调用实现有优化。新的Linux规则里有个以epoll命名的方法,这个方法在处理大量描述符时伸缩性非常强。
    • 使用事件驱动I/O;在协调检查一个描述符时当动作需要激活,I/O活动可由信号处理函数来照顾。这删除了轮询操作,但是需要很多事件活动。
    • 使用真异步I/O;I/O操作初始化了且在完成前返回。用信号来指示I/O活动的结束,而不是开始。
    • 多进程或多线程支持并发操作。和其它技术协调起来进行并行处理。
  • 没有非常通用的方法,程序里I/O通道打开时间有短有长,并发量时高时低,等等。

3.23.2 select()函数

  • select()函数面向多个文件描述符和或socket描述符工作,这样提供了多路复用和异步输入和输出功能。
  • 在使用select()函数时,能够等待一个指定的时长,直到数据准备好来读写或有异常为止:

#include <sys/types.h>

#include <sys/time.h>

#include <unistd.h>

int select(int  n,fd_set *readfds,fd_set*writefds,fd_set*exceptfds,struct timeval *timeout);
struct timeval {  long tv_sec,long tv_usec
};
  • 参数n指定有多少描述符要被检查,这个长度参数里的值要比最大文件描述符值 大1(因为描述符开始位置是0)。
  • Linux内核对这个长度有限制,限制额为1024,限制参数为__FD_SETSIZE=1024。如果想扩大这个长度,需要修改,再重新编译内核,编译时要确保libc的版本足够高能够处理。
  • 参数readfds,writefds,exceptfds是描述符集的指针,描述符集存储在fd_set数据类型里,这类类型规定了监听内核给出的什么信号,对哪个描述符监听哪类动作。描述符集和以前的信号集类似,可以向集里加描述符或去描述符。
  • 当一个描述符准备好读时,在读描述符集里会设置上对应的合适位置。当一个描述符准备好写时,在写描述符集里对应的合适位置会设置上。当socket遇到错误时,2个位都设置上。异常位会在带外数据接收到时设置上。
  • 下面的宏是用来操作描述符集的:

FD_ZERO(fd_set*set);

清除集内所有位

FD_CLR(int fd,fd_set *set);

关闭fd对应的位

FD_SET(int fd,fd_set*set);

开启fd对应的位

FD_ISSET(int fd,fd_set*set);

检测fd对应的位是否设置上

  • 参数timeval指定了select()函数需要等待的时间,等待过程直到状态位有变化时才结束(超时也会结束)。如果一个或多个描述符在超时前准备好了,函数就在哪个时间点返回。timeval里有秒成员和微秒成员。
  • 如果timeval=NULL,等待时长是无限的,尽管它能被信号中断。如果tv_sec=tv_usec=0,会立即返回;也就是说,函数进行了一次非阻塞检查。
  • 任何描述符集都能是NULL指针;如果所有三个都是,select()函数只是象一个高精度的定时器方式来工作,象个sleep()函数一样;这个技巧经常用。
  • 在Linux下,参数timeout里的值会被修改,修改结果是没有休眠的时间。很多其它操作系统不这样做,在调用后不使用这个结果会安全些。注意在调用后描述符集会被修改掉,所以在再次调用select前需要重新初始化一下。
  • 在成功时,select()函数返回准备好做某些动作的描述符个数;在Linux下,如果某个文件描述符准备好读和写,它会在返回的2个描述符集里体现出来;在某些操作系统里,只有一个,所以再次提醒在重要场合不要使用这个值。返回0表示超时了。返回-1表示有错误。注意,在文件描述符修改状态前捕获了个信号,错误情况会发生。
  • 注意,在错误发生时,文件描述符集被清除了。如果select()在超时后再次调用,需要再调用一次FD_SET操作。这个方法适用于输入是部分事件驱动(和文件输入相比)和部分轮询(和按时间超时比)。

示例
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/select.h>

int main (void)
{
fd_set fd_rset, fd_wset;
struct timeval timeout;
int rc;
FD_ZERO (&fd_rset);//对描述符集清0
FD_ZERO (&fd_wset);
/* Watch stdin for input, stdout for output. 监视标准输入和标准输出*/
FD_SET (0, &fd_rset);//相当于把0描述符加到fd_rset集里
FD_SET (1, &fd_wset);
timeout.tv_sec = 3;
timeout.tv_usec = 50000;
rc = select (2, &fd_rset, &fd_wset, NULL, &timeout);//注意这里的2是上面0,1中间找最大值1加1的值,0代表输入,1代表输出
if (rc) {
printf ("Data is available now.\n");
if (FD_ISSET (0, &fd_rset))//判断fd_rset集里0描述符位置设置上没有,设置上了就表示它有状态了
printf ("Data ready for reading on fd=0\n");
if (FD_ISSET (1, &fd_wset))
printf ("Data ready on   writing on fd=1\n");
} else {
printf ("No data found in 3.05 seconds.\n");
}
printf (" I am exiting\n");
exit (0);
}

3.23.3 pselect()函数

  • pselect是select函数的增强版本

#include <sys/select.h>

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

int pselect(int  n,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,const struct  timespec*timeout,const sigset_t*sigmask);
  • 和select相比,有3个不同:

不使用struct timeal,使用了:

struct timespec  {
long tv_sec;/*seconds*/
long tv_nsec;/*nanoseconds纳秒*/
}

这个有纳秒的精度(这并不暗示你的系统能够正常解析这样的级别)

timeout参数不会被函数修改了。

sigmask参数能够用来设置临时的事件掩码(哪些信号会被阻塞),在pselect调用时这些信号会被阻塞。

  • 如果你使用最近的新版本libc,肯定能正常使用pselect。否则你要写个封装函数来伪装pselect()一下。

3.23.4 poll()函数

  • poll()函数本质上和select功能一样:

#include <sys/poll.h>

int poll(struct  pollfd*ufds,unsigned int nfds,int timeout);
  • poll()函数需要以nfds指定长度的数组来工作,数组元素的类型是
struct pollfd {
int fd;/*file descriptor文件描述符*/
short events;/*requested events请求事件*/
host revents;/*returned events返回事件*/
}
  • 第三个参数timeout给定了以毫秒为单位的等待时间。0值表示直接返回(不阻塞),任何负值意味着无限等待。
  • 在数据结构pollfd里,fd成员给定了用于检查的文件描述符;负值表示忽略。
  • events成员是被检查事件的位掩码。由下面值组合而成:

POLLIN:正常或普通级别的带内数据能被读取
POLLRDNORM:正常数据能被读取
POLLRDBAND:普通级别带内数据能被读取
POLLPRI:高级别带内数据能够读取
POLLOUT:正常数据能够写入
POLLWRNORM:正常数据能够写
POLLWRBAND:正常级别带内数据能够写
POLLMSG:非标准值

  • 在大部分情况下,只用POLLIN来检查是否有数据能读,POLLOUT来看数据是否能写
  • revents成员在poll()函数调用时填充,理由也正是为了函数调用(比如POLLIN,POLLOUT)。它也可以是下面错误值之一:

POLLERR:有错误发生
POLLHUP:socket已经挂起
POLLINVAL:无效的描述符

  • poll()函数的返回值是结构体变量的个数,这些结构体里的revents成员的值是非0的。0意味着调用超时了,-1意味着错误(包含EINTR,这意味着信号触发了)
  • 使用poll()而不用select()有一些好处。特别是对描述符个数没有限制的场合。
  • 实际上,在Linux下,从2.2内核开始select()内部实现时就使用了poll。

示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/poll.h>

int main (void)
{
struct pollfd ufds[2];//定义2个fd
int timeout;
int rc;
/* Watch stdin for input, stdout for output.监听标准输入和标准输出 */
ufds[0].fd = 0;
ufds[0].events = POLLIN;//初始化监听信号ufds[1].fd = 1;
ufds[1].events = POLLOUT;
timeout = 3050;//超时时间
rc = poll (ufds, 2, timeout);
printf
 (" poll returns %d,  ufds[0].revents = 0x%03x, ufds[1].revents = 0x%03x\n",
   rc, ufds[0].revents, ufds[1].revents);
if (rc < 0) {
printf ("An error occurred\n");
exit (EXIT_FAILURE);
}
if (rc == 0) {
printf ("No data found in 3.05 seconds.\n");
exit (EXIT_SUCCESS);
}
printf ("Data is available now.\n");
if (ufds[0].revents & POLLIN)//用按位与操作来判断是否来了读取操作
printf ("Data ready for reading on fd=0\n");
if (ufds[1].revents & POLLOUT)//用按位与操作来判断是否来了写入操作
printf ("Data ready on   writing on fd=1\n");
exit (EXIT_SUCCESS);
}

3.23.5 epoll

3.23.6 信号驱动和异步I/O

  • 事件驱动I/O能避免阻塞和轮询,是select和poll的代替品
  • 首先,socket必须允许事件驱动I/O;这意味着设置socket的拥有者;也就是说,在socket有活动时,决定哪个进程应该接收SIGIO信号。
  • 记住设置可用fcntl()或setsockopt()函数。在Linux下,我们同样能设置信号自身(怎样处理)。
  • 信号处理函数必须注册上,最好用sigaction()系统调用(不要用signal()函数)。
  • 信号处理函数要么读取数据(比如recvfrom)且通知主程序数据可用了,或者通知主程序来读取数据。同时主程序可以循环工作。
  • 在异步I/O模式下,操作模式和上面所述大不一样。I/O操作初始化后,程序继续执行。当I/O操作结束时,信号被触发来意味着完成
  • 通过显式使用信号可以完成这些工作,但是更高级的实现是使用AIO接口,AIO的功能定义在/usr/include/aio.h。它里面有特别的读取,写入和其它必须的函数。

3.23.7 并发服务器

  • 并发服务器模型允许服务器同时处理多个请求,避免了在处理某个请求时阻塞,防止了整个操作停转
  • 基本的概念是主进程监听连接,接收到请求后,创建子进程来处理请求连接。当客户端请求处理完成后,子进程终止。
  • 示例代码如下:
sd=socket(…)
…
listen(…);
…
for(;;) {
cd=accept(…);
pid=fork();
if(pid==0) {/*子进程*/
close(sd);//只是减少计数,没有真正关闭服务器端socket
handle_the_client();
close(cd);
exit(0);
}
close(cd);//父进程也要调用减少计数,才会真正关闭
}
  • handle_the_client()函数处理实际工作。
  • 注意子进程继承了一个监听socket的描述符,这样应该关闭它,同时对于accept()返回的描述符也要关闭。
  • 这是最常用最成熟的模型;能处理非常多的请求。
  • 然而,创建进程消耗大。每个客户端有巨量的事情要处理,且/或客户端要求服务器处理的时间也冗长,这些都有可能。客户端请求持续短时间或尝试性质情况也会经常遇到。
  • 在短连接多时,并发服务器要以多线程方式来实现,子进程会变轻量进程或LWP,不过现在Linux内核里已经真正实现了线程。
  • 在多线程并行实现里,所有线程共享了同样的文件描述符空间,内存,信号处理函数和其它属性。这样,当客户端请求处理完后需要和父进程通信时,有方法可以实现,可能的方法是让数据可用(比如全局变量)。
  • 示例如下:
#include  <pthread.h>
sd=socket(…);
…
listen(…);
…
for(;;){
cd=accept(…);
pthread_create(&thr[cd],NULL,handle_the_client,&Data[cd]);
pthread_detach(&the[cd]);
}
…
  • 当线程启动时,它执行handle_the_client()函数。注意它并不关闭socekt描述符;线程共享它们的描述符空间,服务器端父进程会关闭它!显然,以其它方式来获取线程时要小心。
友情链接
版权所有 Copyright(c)2004-2021 锐英源软件
公司注册号:410105000449586 豫ICP备08007559号 最佳分辨率 1024*768
地址:郑州市金水区郑州大学北校区院(文化路97号院)内