10.多进程服务器端
1.并发服务器端实现模型和方法:
多进程服务器:通过创建多个进程提供服务
多路复用服务器:通过捆绑并统一管理I/O对象提供服务
多线程服务器:通过生成与客户端等量的线程提供服务
2.fork函数创建进程
父进程:fork函数返回子进程ID
子进程:fork函数返回0
父子进程拥有完全独立的内存结构,只是共享同一代码而已。
3.僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
子进程应该向其父进程传递子进程的exit参数值或return语句的返回值,如果父进程未主动要求获取子进程的结束状态值,操作系统将一直保存,其进程号也会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
4.wait和waitpid函数
调用wait函数时,如果没有已终止的子进程,程序将阻塞直到有子进程终止
调用waitpid函数时,程序不会阻塞
5.信号处理
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
信号注册函数:
#include<signal.h>
void (*signal(int signo, void (*func)(int)))(int);//返回类型:参数为int,返回void函数指针
SIGALRM:已到通过调用alarm函数注册的时间
SIGINT:输入CTRL+C
SIGCHLD:子进程终止
6.sigaction函数进行信号处理
signal函数在UNIX系列的不同操作系统中可能存在区别,但sigaction函数完全相同。
#include<signal.h>
int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact;
sigaction结构体:
struct sigaction
{
void (*sa_handler)(int);//保存信号处理函数的指针值
sigset_t sa_mask;//初始化为0
int sa_flags;//初始化为0
}
7.基于多任务的并发服务器
通过fork函数只复制了文件描述符,并未复制套接字,复制后的两个文件描述符指向同一套接字。
只有这两个文件描述符都终止(销毁)后才能销毁套接字,所以要在父子进程中分别销毁自己不处理的文件描述符(父进程中close客户端socket,子进程close服务端socket)
8.客户端分割TCP的I/O程序
客户端父进程负责接收数据,额外创建子进程负责发送数据,实现简单,收发独立,可以提高频繁交换数据的程序性能
11.进程间通信
1.管道PIPE
#include <unistd.h>
int pipe(int filedes[2]);
filedes[0] 通过管道接收数据时使用的文件描述符,即管道出口
filedes[1] 通过管道传输数据时使用的文件描述符,即管道入口
使用时,先调用pipe创建管道,再调用fork,子进程中将同时拥有前面创建的两个文件描述符,注意复制的并非管道,而是用于管道I/O的文件描述符。
数据进入管道后成为无主数据,先读的进程会把数据取走
通过管道进行进程间双向通信
12.I/O复用
1.select函数
#include <sys/select.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
maxfd:监视对象文件描述符数量
readfds:将所有关注“是否存在带读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
writefds:将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值
exceptset:将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值
timeout:调用select函数后,为防止陷入无限阻塞状态,传递超时信息
返回值:超时为0,错误为-1;因发生关注的事件返回时,返回大于0的值,该值时发生事件的文件描述符数
2.设置文件描述符
存有0和1的位数组集中多个需要监视的文件描述符
3.设置检查(监控)范围及超时
监视范围与第一个参数maxfd有关,实际上为监控对象文件描述符的数量,由于每次新建文件描述符时,其值都会增1,故只需传递最大的文件描述符+1
超时与最后一个参数有关,如果不想设置超时,传递NULL
struct timeval
{
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
}
4.调用select函数后查看结果
fd_set变量中值仍为1的位置上的文件描述符发生了变化
5.应用
调用select函数后的除发生变化的文件描述符对应位外,剩下的所有位将初始化为0,因此需要将准备好的fd_set变量备份一份,这是通用方法。
调用select后,结构体timeval的成员tv_sec和tv_usec的值将被替换为超时前剩余时间,因此每次调用select前,都需要初始化timeval结构体变量。
13.多种I/O函数
1.Linux中的send&recv
前文为了强调Linux环境下文件I/O和套接字I/O相同,只用了read和write函数
#include <sys/socket.h>
ssize_t send(int sockfd, const void * buf, size_t nbytes, int flags);
ssize_t recv(int sockfd, void * buf, size_t nbytes, int flags);
最后一个参数是收发数据时的可选项,不同操作系统对下述可选项的支持也不同。
可选项 | 含义 | send | recv |
---|---|---|---|
MSG_OOB | 传输紧急消息(Out-of-band data) | · | · |
MSG_PEEK | 验证输入缓冲中是否存在接收的数据 | · | |
MSG_DONTROUTE | 数据传输不参照路由表,在本地网络中寻找目的地 | · | |
MSG_DONTWAIT | 非阻塞I/O | · | · |
MSG_WAITALL | 防止函数返回,直到接收全部请求的字节数 | · |
2.readv&writev函数
对数据进行整合传输以及发送。通过writev函数可以将分散保存在多个缓冲中的数据一并发送,通过readv函数可以由多个缓冲分别接收。适当使用这两个函数可以减少I/O函数的调用次数。
14.多播与广播
1.多播
多播服务器端针对特定多播组,只需发送1次数据,该组内的所有所有客服端都能接收数据。
多播组数可在IP地址范围内任意增加。
加入特定组即可接收发往该多播组的数据
多播组是D类IP地址(224.0.0.0~239.255.255.255),多播是基于UDP完成的,向网络传递一个多播数据包时,路由器将复制该数据包并传递到多个主机。
2.路由和TTL
TTL即生存时间(Time to Live):指服务端发送的数据包最远能传递的距离,用整数表示,并且每经过1个路由器就减1,当为0时,该数据包无法再被传递,只能销毁。因此,这个值设置过大将影响网络流量;当然,设置过小也会无法传递到目标。
3.广播
广播在功能上和多播是一样的,都是同时可以向大量客户传递数据。但他们在网络范围上有区别,多播可以跨越不同的网络,只要加入了多播组就能接收数据。但广播只能向同一网络中的主机传输数据。
直接广播sender的IP地址只需指定网络地址,主机地址全部填255。这样处在这个网络地址里的所有主机就可以接收数据了。
本地广播sender的IP地址写255.255.255.255,这样本地网络所有主机就可以接收数据了。