目录
一、初识复用
1. 认识复用
“复用” 的含义:为了提高物理设备的效率,用最少的物理要素传递最多数据时使用的技术。
上图远距离的 3 人 可以同时通话的 3 方 对话纸杯电话系统。为使 3 人 同时对话,需准备图中所示系统。
另外,为了完成 3 人 对话,说话时需同时对着两个纸杯,接听时也需要耳朵同时对准两个纸杯。此时引入复用技术会使通话更加方便,如下图。
2. 复用的优点
- 减少连线长度。
- 减少纸杯个数。
3. 复用技术在服务端的应用
纸杯电话系统引入复用技术后,可以减少纸杯数和连线长度。同样,服务器端引入复用技术
可以减少所需进程数。
下图模型中引入复用技术,可以减少进程数。重要的是,无论连接多少客户端,提供服务的进程只有1个。
二、select 技术
上图为从调用 select 函数 到获取结果所经过程。
1. 设置文件描述符(fd_set)
利用 select 函数 可以同时监视多个文件描述符。
监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述 3 种 监视项分成 3 类。
图中最左端的位表示文件描述符 0(所在位置)。如果该位设置为 1,则表示该文件描述符是监视对象。
2. 文件描述符的控制
(1)FD_ZERO
FD_ZERO(fd_set * fdset)
将 fd_set 变量的 所有位初始化为 0。
(2)FD_SET
FD_SET(int fad, fd_set * fdset)
在参数 fdset 指向的变量中注册文件描述符 fd 的信息。
(3)FD_CLR
FD_CLR(int fd, fd_set * fdset)
从参数 fdset 指向的变量中清除文件描述符 fd 的信息。
(4)FD_ISSET
FD_ISSET(int fd, fd_set * fdset)
用于验证select函数的调用结果。若参数 fdset 指向的变量中包含文件描述符 fd 的信息,则返回“真”。
3. select 函数
#include <sys/select.h>
#include<sys/time.h>
int select(int maxfd
,fd_set * readset
,fd_set * writeset
,fd_set * exceptset
,const struct timeval * timeout);
① maxfd
监视对象文件 描述符数量。文件描述符的监视范围 与这个参数有关。
② readset
将所有关注 “是否存在待读取数据” 的文件描述符注册到 fd_set 型变量,并传递其 地址值。
③ writeset
将所有关注 “是否可传输无阻塞数据” 的文件描述符注册到 fd_set 型变量,并传递其地址值。
④ exceptset
将所有关注 “是否发生异常” 的文件描述符注册到 fd_set 型变量,并传递其地址值。
⑤ timeout
调用 select 函数后,为防止 陷入无限阻塞的 状态,传递 超时(time-out)信息。
struct timeval
{
long tv_sec; // seconds(秒)
long tv_usec; // microseconds(毫秒)
}
⑥ 返回值
发生错误时 返回 -1,超时返回时 返回 0。因发生关注的事件 返回时,返回大于 0 的值,该值是发生事件的 文件描述符数。
补充:
① select 函数 用来验证 3 种 监视项的 变化情况。根据 监视项声明 3 个 fd_set 型变量,分别向其注册文件描述符 信息,并把 变量的地址值 传递到上述函数的 第二 到 第四个参数。
② select 函数要求通过 第一个参数 传递监视对象文件 描述符的 数量。因此,需要得到 注册在 fd_set 变量中的 文件描述符数。但 每次新建文件描述符时,其值都会 增 1,故 只需将 最大的 文件描述符值 加 1 再传递到 select 函数 即可。加 1 是因为文件描述符的值 从 0 开始。
③ 由上图可知,select函数调用完成后,向其传递的 fd_set 变量中将 发生变化。原来为 1 的所有位均变为 0,但发生变化的文件描述符 对应位 除外。因此,可以认为值 仍为 1 的位置上的文件描述符发生了变化。
4. select 实例
(1)客户端
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <iostream>
#include <unistd.h>
using namespace std;
#define MAX_LEN 1024
int main(int argc, char const *argv[])
{
int client_sock;
struct sockaddr_in server_addr;
socklen_t server_addr_len;
char message[MAX_LEN];
if (argc != 3) {
cout << "Using: " << argv[0] << " 10.211.55.8 <port>" << endl;
exit(1);
}
// 定义客户端 socket
client_sock = socket(AF_INET, SOCK_STREAM, 0);
if (client_sock == -1) {
perror("socket()");
exit(1);
}
// 连接服务器
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
server_addr_len = sizeof(server_addr);
if (connect(client_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1) {
perror("connect()");
exit(1);
}
else {
cout << "connect success ·······" << endl;
}
for (int i = 0; i < 3; i++) {
memset(&message, 0, sizeof(message));
cout << "client send: ";
cin >> message;
send(client_sock, message, sizeof(message), 0);
recv(client_sock, message, MAX_LEN, 0);
cout << "client recv: " << message << endl;
}
close(client_sock);
return 0;
}
(2)服务器端
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <iostream>
#include <unistd.h>
using namespace std;
#define MAX_LEN 1024
int main(int argc, char const *argv[])
{
int server_sock, client_sock;
struct sockaddr_in server_addr, client_addr;
struct timeval timeset;
socklen_t client_addr_len;
int fd_max, df_num, str_len;
char message[MAX_LEN];
if (argc != 2) {
cout << "Using: " << argv[0] << " <port>" << endl;
exit(1);
}
// 创建服务端 socket
server_sock = socket(PF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
perror("socket()");
exit(1);
}
// 绑定 IP和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind()");
exit(1);
}
// 开启监听
if (listen(server_sock, 5) == -1) {
perror("listen()");
exit(1);
}
// 初始化监听窗口
fd_set fdset, copy_fdset;
FD_ZERO(&fdset);
FD_SET(server_sock, &fdset);
fd_max = server_sock;
while (1) {
// 设置超时
// timeset.tv_sec = 5; // 5秒后超时
// timeset.tv_usec = 0;
copy_fdset = fdset;
if(df_num = select(fd_max+1, ©_fdset, 0, 0, 0) == -1) {
perror("select()");
break;
}
// if (df_num == 0) {
// cout << "not client ·······" << endl;
// continue;
// }
for (int i = 0; i < fd_max + 1; i++) { // 循环到最大的文件标识符的索引处
if (FD_ISSET(i, ©_fdset)) {
if (i == server_sock) {
// 检测到的标识符为 服务端的
// 表示服务端的 socket 接收到连接请求
client_addr_len = sizeof(client_addr);
if ((client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) {
perror("accept()");
exit(1);
}
FD_SET(client_sock, &fdset); // 将新添加的客户端 socket 加入监视队列中
if (client_sock > fd_max) fd_max = client_sock; // 更新最大文件标识符
char client_ip[16];
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, 16);
cout << "connected client IP: " << client_ip << " post: " << ntohs(client_addr.sin_port) << endl;
// cout << "continue ··········" << endl;
}
else {
// 检测到的标识符不是服务端
// 表示为客户端向服务端 传输数据,或者是 发送的断开连接请求
str_len = recv(i, message, MAX_LEN, 0);
if (str_len == -1) {
perror("recv");
exit(1);
}
else if (str_len > 0) {
cout << "receive " << i << " client data: " << message << endl;
send(i, message, sizeof(message), 0);
}
else if (str_len == 0) {
// 断开连接
FD_CLR(i, &fdset);
cout << "closed client: " << i << " successed ········" << endl;
}
}
}
}
}
close(server_sock);
return 0;
}
三、epoll 技术
1. 认识 epoll
每次调用 select 函数时向操作系统传递 监视对象信息。应用程序 向操作系统传递数据 将对程序造成 很大负担,而且 无法通过优化代码解决,因此将成为 性能上的致命弱点。
有些函数 不需要操作系统的帮助 就能完成功能,而有些 则必须借助于操作系统。select 函数与 文件描述符有关,更准确地说,是 监视套接字变化的 函数。而 套接字是由 操作系统管理的,所以 select 函数绝对 需要借助于操作系统 才能完成功能。select 函数的这一缺点可以通过 如下方式弥补:
仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。
2. epoll 优点
- 无需编写以 监视状态变化为目的的 针对所有文件描述符的 循环语句。
- 调用对应于 select 函数的 epoll_wait 函数时无需 每次传递监视对象信息。
3. epoll 中的事件保存
epoll 方式中通过 如下结构体 epoll_event 将发生变化的(发生事件的)文件描述符单独集中到一起。
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}
typedef union epol1_data
{
void * ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
声明足够大的 epoll_event 结构体数组后,传递给 epoll_ wait 函数时,发生变化的 文件描述符信息将被填人该数组。因此,无需像 select 函数那样针 对所有文件描述符进行循环。
4. epoll_create
#include <sys/epoll.h>
int epol1_create(int size);
① size
epoll 实例的大小。
② 返回
成功时返回 epoll 文件描述符,失败时返回 -1。
补充:
- 调用 epoll_create 函数时创建的文件描述符保存空间称为 “epoll 例程 ” ,但有些情况下名称不同。
- 通过 参数 size 传递的值决定 epoll 例程 的大小,但该值 只是向操作系统 提的建议。换言之,size 并非用来决定 epoll 例程 的大小,而 仅供操作系统 参考。
- epoll_create 函数创建的资源 与 套接字相同,也由 操作系统管理。因此,该函数和创建 套接字的 情况相同,也会 返回 文件描述符。也就是说,该函数返回的 文件描述符主要用与于区分 epoll 例程。需要终止时,与 其他文件描述符 相同,也要调用 close 函数。
5. epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
① epfd
用于注册监视对象的epoll例程的文件描述符。
② op
EPOLL_CTL_ADD: 将文件描述符注册到epoll例程。
EPOLL_CTL_DEL: 从epoll例程中删除文件描述符。
EPOLL_CTL_MOD: 更改注册的文件描述符的关注事件发生情况。
用于指定监视对象的添加、删除或更改等操作。
③ fd
需要注册的监视对象文件描述符。
④ event
监视对象的事件类型。
⑤ 返回
成功时返回 0,失败时返回 -1。
补充:
epoll_ctl(A, EPOLL_CTL_ADD, B, C); 表示为:从epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件。
struct epoll_event event; ······ event.events = EPOLLIN;//发生需要读取数据的情况(事件)时 event.data.fd = sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd,&event); ······
EPOLLIN:需要读取数据的情况。 EPOLLOUT:输出缓冲为空,可以立即发送数据的情况。 EPOLLPRI: 收到OOB数据的情况。 EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用。 EPOLLERR:发生错误的情况。 EPOLLET:以边缘触发的方式得到事件通知。 EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。
6. epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
① epfd
表示事件发生监视范围的 epoll 例程的 文件描述符。
② events
保存发生事件的文件描述符集合 的结构体 地址值。
③ maxevents
第二个参数中 可以保存的 最大事件数。
④ timeout
以 1/1000 秒 为单位的等待时间,传递 -1 时,一直等待直到 发生事件。
⑤ 返回
成功时返回发生事件的 文件描述符数,失败时返回 -1。同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。
补充:
第二个参数 所指缓冲需要动态分配。
int event_cnt; struct epoll_event * ep_events; ······ ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE); // EPOLL_SIZE 是宏常量 event_Cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE,-1); ······
7. 条件触发和边缘触发
条件触发(Level Trigger)方式中,只要输入缓冲有数据就会一直通知该事件,就将以事件方式再次注册。
边缘触发(Edge Trigger)中输入缓冲收到数据时仅注册 1 次 该事件。即使输入缓冲中还留有数据,也不会再进行注册。
8. epoll 实例
(1)客户端
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <iostream>
#include <unistd.h>
using namespace std;
#define MAX_LEN 1024
int main(int argc, char const *argv[])
{
int client_sock;
struct sockaddr_in server_addr;
socklen_t server_addr_len;
char message[MAX_LEN];
if (argc != 3) {
cout << "Using: " << argv[0] << " 10.211.55.8 <port>" << endl;
exit(1);
}
// 定义客户端 socket
client_sock = socket(AF_INET, SOCK_STREAM, 0);
if (client_sock == -1) {
perror("socket()");
exit(1);
}
// 连接服务器
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
server_addr_len = sizeof(server_addr);
if (connect(client_sock, (struct sockaddr *)&server_addr, server_addr_len) == -1) {
perror("connect()");
exit(1);
}
else {
cout << "connect success ·······" << endl;
}
for (int i = 0; i < 3; i++) {
memset(&message, 0, sizeof(message));
cout << "client send: ";
cin >> message;
send(client_sock, message, sizeof(message), 0);
recv(client_sock, message, MAX_LEN, 0);
cout << "client recv: " << message << endl;
}
close(client_sock);
return 0;
}
(2)服务端(边缘触发模式)
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/epoll.h>
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
#define MAX_LEN 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
int main(int argc, char const *argv[])
{
int server_sock, client_sock;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len;
int epfd, event_cnt;
struct epoll_event * ep_events; // 单独保存发生的事件
struct epoll_event event; // 单个时间
int str_len;
char message[MAX_LEN];
if (argc != 2) {
cout << "Using: " << argv[0] << " <port>" << endl;
exit(1);
}
// 定义服务端 socket
server_sock = socket(PF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
perror("socket()");
exit(1);
}
// 绑定 IP和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind()");
exit(1);
}
// 开启监听
if (listen(server_sock, 5) == -1) {
perror("listen()");
exit(1);
}
epfd = epoll_create(EPOLL_SIZE); // 创建文件描述符保存空间
ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE); // 为储存事件的空间开辟内存
setnonblockingmode(server_sock);
event.events = EPOLLIN;
event.data.fd = server_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, &event); // 将服务器的 socket 注册进去
while (1) {
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); // 等待有 socket 被填入到 ep_events 中
if (event_cnt == -1) {
perror("epoll_wait()");
break;
}
cout << "return epoll_wait" << endl;
for (int i = 0; i < event_cnt; i++) {
if (ep_events[i].data.fd == server_sock) {
// 客户端发来了连接请求
client_addr_len = sizeof(client_addr);
client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_sock == -1) {
perror("accept()");
exit(1);
}
setnonblockingmode(client_sock);
event.events = EPOLLIN | EPOLLET; // 边缘触发模式
event.data.fd = client_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &event); // 将新的客户端注册进去
char client_ip[16];
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, 16);
cout << "client connected: IP " << client_ip << " port " << ntohs(client_addr.sin_port) << endl;
}
else {
// 发送信息或者断开连接
while (1) { // 循环把输入缓存中的数据全都读完
str_len = recv(ep_events[i].data.fd, message, MAX_LEN, 0);
if (str_len == 0) {
// 断开连接
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd); // 关闭断开连接的 socket
cout << "closed client: " << ep_events[i].data.fd << endl;
}
else if (str_len < 0) {
if (errno == EAGAIN) { // 没有数据可读
break;
}
}
else {
// cout << "receive client " << ep_events[i].data.fd << " : " << message << endl;
send(ep_events[i].data.fd, message, sizeof(message), 0);
}
}
}
}
}
close(server_sock);
close(epfd);
return 0;
}
void setnonblockingmode(int fd) {
// 设置非阻塞模式
int flag = fcntl(fd, F_GETFL, 0); // 获取之前设置的属性信息
fcntl(fd, F_SETFL, flag | O_NONBLOCK); // 在此基础上添加非阻塞的 O_NONBLOCK
}