【高性能服务器】select模型

我要成为C++领域大神 2024-07-22 17:35:02 阅读 66

🔥博客主页: 我要成为C++领域大神

🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】

❤️感谢大家点赞👍收藏⭐评论✍️

本博客致力于知识分享,与更多的人进行学习交流

IO多路复用就是复用一个线程,从原先一个客户端需要一个线程去调用recv询问内核数据是否已经就绪,那么多个客户端就需要多个线程,转变成现在多个客户端都用一个线程使用select/poll去统一管理,主动通知用户哪些数据已经就绪(read,write,accept等事件),所以复用了这个线程,减少了系统开销。

在客户端增加时,线程不会呈O(n)增加

关于recv和accept工作流程

<code>accpet通过服务端文件描述符监听socket事件,当监听到READ_EVENT事件时,说明有其他网络端向此socket发送数据,触发socket读事件(三次握手中客户端会发送数据),建立TCP连接。

recv通过客户端文件描述符监听socket事件,当监听到READ_EVENT事件,处理事件,将数据读取到用户缓冲区buffer

通过IO复用,实现监听到socket事件就绪后,直接调用accpet或recv即可,直接完成TCP连接或者数据读取,两个函数不会阻塞。

可以实现单进程一对多效果,但是没有使用并发技术

处理的业务复杂度不能过高,要在极短的时间内处理若干任务,投入二次监听

IO多路复用第一版select


实现原理

select 实现多路复用的方式是,将已连接的 Socket 都放到一个监听集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,就是通过遍历监听集合的方式进行检查。

当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。

所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。

select 使用固定长度的 BitsMap,表示文监听集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,最多只能监听1021个用户socket,因为0、1、2是标准文件描述符。

监听集合中对应的socket位码是1,表示监听次socket,为0表示不监听

在select这种I/O多路复用机制下,我们需要把想监控的文件描述集合通过函数参数的形式告诉select,然后select会将这些文件描述符集合拷贝到内核中,我们知道数据拷贝是有性能损耗的,因此为了减少这种数据拷贝带来的性能损耗,Linux内核对集合的大小做了限制,并规定用户监控的文件描述集合不能超过1024个,同时当select返回后我们仅仅能知道有些文件描述符可以读写了,但是我们不知道是哪一个,因此必须再遍历一边找到具体是哪个文件描述符可以读写了。 


实现流程

核心接口

<code>void FD_ZERO(fd_set *fdset) 初始化监听集合为0

void FD_SET(int fd,fd_set *fdset) 对set集合中fd对应位码设置为1

void FD_CLR(int fd,fd_set *fdset) 对set集合中fd对应位码设置为0

int bitcode=void FD_ISSET(int fd,fd_set *fdset) 查看fd在监听集合中是1还是0,并直接返回


int ready=select(int nfds, fd_set* readset, fd_set* writeset, fd_set* exeptset,struct timeval* timeout);

nfds表示被select管理的描述符个数。值为最大描述符+1.不是描述符最大值

readsetwritesetexeptset可读事件集合、可写事件集合、异常事件集合。这三者都可以填null

timeout超时时间有三种含义: 阻塞(null)、正常超时、非阻塞(0)


使用服务器测试业务:

客户端向标准输入发送小写字符串,服务端响应回复对应大写字符,"abcAS"->"ABCAS"

客户端向服务端发送关键字localtime,服务端响应回复系统时间、

代码实现

MySock.h

#ifndef _MYSOCK_H_

#define _MYSOCK_H_

#include <arpa/inet.h>

#include <ctype.h>

#include <errno.h>

#include <netinet/in.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/select.h>

#include <sys/socket.h>

#include <sys/types.h>

#include <time.h>

#include <unistd.h>

int SOCKET(int domain, int type, int protocol);

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

ssize_t RECV(int sockfd, void* buf, size_t len, int flags);

ssize_t SEND(int sockfd, void* buf, size_t len, int flags);

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

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

int LISTEN(int sockfd, int backlog);

char* FGETS(char* s, int size, FILE* stream);

int SELECT(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,

struct timeval* timeout);

int socket_init();

int return_response(int clientfd, const char* clientip);

//void strDeal(int *client_fd);

// 全局变量声明

char recv_buf[1024];

char time_buf[64];

int serverFd, clientFd;

struct sockaddr_in clientAddr;

fd_set set, oset;

int client_array[1020];

int maxfd, ready;

socklen_t addrlen;

char clientip[16];

time_t tp;

ssize_t recvlen;

int toupper_flag;

#define SHUTDOWN 1

#endif

MySock.c

#include "MySock.h"

int SOCKET(int domain, int type, int protocol) {

int reval = socket(domain, type, protocol);

if (reval == -1) {

perror("socket call failed");

exit(0);

}

return reval;

}

int BIND(int sockfd, struct sockaddr* addr, socklen_t addrlen) {

int reval = bind(sockfd, addr, addrlen);

if (reval == -1) {

perror("bind call failed");

exit(0);

}

return reval;

}

ssize_t RECV(int sockfd, void* buf, size_t len, int flags) {

ssize_t reval;

reval = recv(sockfd, buf, len, flags);

return reval;

}

ssize_t SEND(int sockfd, void* buf, size_t len, int flags) {

ssize_t reval;

reval = send(sockfd, buf, len, flags);

if (reval == -1)

perror("send call failed");

return reval;

}

int CONNECT(int sockfd, struct sockaddr* addr, socklen_t addrlen) {

int reval = connect(sockfd, addr, addrlen);

if (reval == -1) {

perror("connect call failed");

exit(0);

}

return reval;

}

int ACCEPT(int sockfd, struct sockaddr* addr, socklen_t* addrlen) {

int reval = accept(sockfd, addr, addrlen);

if (reval == -1) {

perror("accept call failed");

exit(0);

}

return reval;

}

int LISTEN(int sockfd, int backlog) {

int reval = listen(sockfd, backlog);

if (reval == -1) {

perror("listen call failed");

exit(0);

}

return reval;

}

char* FGETS(char* s, int size, FILE* stream) {

char* str;

if ((str = fgets(s, size, stream)) != NULL) {

return str;

} else {

perror("fgets call failed");

exit(0);

}

}

int SELECT(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,

struct timeval* timeout) {

int reval = select(nfds, readfds, writefds, exceptfds, timeout);

if (reval == -1) {

perror("select call failed");

exit(0);

}

return reval;

}

int socket_init() {

struct sockaddr_in sockAddr;

sockAddr.sin_family = AF_INET;

sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);

sockAddr.sin_port = htons(8080);

int sock_fd = SOCKET(AF_INET, SOCK_STREAM, 0);

BIND(sock_fd, (struct sockaddr*)&sockAddr, sizeof(sockAddr));

LISTEN(sock_fd, 5);

return sock_fd;

}

int return_response(int clientfd, const char* clientip) {

char response[1024];

bzero(response, sizeof(response));

sprintf(response, "Hi [%s],This is TCP Server Working...\n", clientip);

SEND(clientfd, response, sizeof(response), 0);

}

SelectServer.c

#include "MySock.h"

int main() {

bzero(recv_buf, sizeof(recv_buf));

bzero(time_buf, sizeof(time_buf));

bzero(clientip,sizeof(clientip));

serverFd = socket_init();

FD_SET(serverFd, &set); // 设置监听

int i;

for (i = 0; i < 1020; ++i) {

client_array[i] = -1;

}

maxfd = serverFd;

printf("Test Select Server is Running...\n");

while (SHUTDOWN) {

oset = set;

ready = SELECT(maxfd+1, &oset, NULL, NULL, NULL);

while (ready) { // 辨别就绪

if (FD_ISSET(serverFd, &oset)) {

addrlen = sizeof(clientAddr);

clientFd =ACCEPT(serverFd, (struct sockaddr*)&clientAddr, &addrlen);

inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, clientip, 16);

printf("Listen Server Socket Successfully Call Accept, Client IP [%s], PORT[%d]\n",clientip, ntohs(clientAddr.sin_port));

return_response(clientFd, clientip);

if (maxfd < clientFd)

maxfd = clientFd;

for (i = 0; i < 1020; ++i)

if (client_array[i] == -1) {

client_array[i] = clientFd;

break;

}

FD_SET(clientFd, &set);//新socket设置监听

FD_CLR(serverFd,&oset);//处理完毕,清理就绪集合

} else {

// 仅处理一次客户端请求,单进程不允许客户端持续占用

for (i = 0; i < 1020; ++i)

{if (client_array[i] != -1)

if (FD_ISSET(client_array[i], &oset))

{

if ((recvlen = RECV(client_array[i], recv_buf, sizeof(recv_buf), 0)) >0)

{ // 处理客户端业务

printf("Client Say:%s\n", recv_buf);

if (strcmp(recv_buf, "localtime") == 0) {

tp = time(NULL); // 获取时间种子

ctime_r(&tp, time_buf);

time_buf[strcspn(time_buf, "\n")] = '\0';

printf("[%s]Response SysTime Successfully!\n", clientip);

SEND(client_array[i], time_buf, strlen(time_buf) + 1, 0);

bzero(time_buf, sizeof(time_buf));

} else {

toupper_flag = 0;

while (recvlen > toupper_flag) {

recv_buf[toupper_flag] = toupper(recv_buf[toupper_flag]);

++toupper_flag;

}

printf("[%s]Response Toupper Successfully!\n", clientip);

SEND(client_array[i], recv_buf, recvlen, 0);

bzero(recv_buf, sizeof(recv_buf));

}

} else if (recvlen == 0) {

FD_CLR(client_array[i], &set); // 删除监听

close(client_array[i]);

client_array[i] = -1;

printf("Client is Exiting, Delete Listen Item.\n");

}

FD_CLR(client_array[i],&oset);//处理完毕,清理就绪集合

break;

}

}

}

ready--;

}

}

printf("Server is Over\n");

close(serverFd);

return 0;

}

Client.c

#include "MySock.c"

//客户端源码编写,连接服务器成功,服务器反馈信息

#define _IP "xxx.xxx.xxx.xxx"

#define _PORT 8080

int main()

{

struct sockaddr_in ServerAddr;

bzero(&ServerAddr,sizeof(ServerAddr));

ServerAddr.sin_family=AF_INET;

ServerAddr.sin_port=htons(_PORT);

inet_pton(AF_INET,_IP,&ServerAddr.sin_addr.s_addr);

int Myfd=SOCKET(AF_INET,SOCK_STREAM,0);

//看需求决定是否要绑定

char Response[1024];//存放服务端反馈信息

ssize_t recvlen;

bzero(Response,sizeof(Response));

char sendbuf[1024];

if((CONNECT(Myfd,(struct sockaddr *)&ServerAddr,sizeof(ServerAddr)))==0)

{

while(1)

{

if((recvlen=RECV(Myfd,Response,sizeof(Response),0))>0)

{

printf("%s\n",Response);

}

printf("Please Type Some text:");//读取标准输入发送给服务端

FGETS(sendbuf,sizeof(sendbuf),stdin);

sendbuf[strcspn(sendbuf,"\n")]='\0';

SEND(Myfd,sendbuf,sizeof(sendbuf),0);

}

}

close(Myfd);

printf("Client is Over\n");

return 0;

}

运行结果

select模型使用一个进程实现对多个客户端的统一管理

select模式优缺点

优点

1.可以通过简单的代码实现一对多效果, 比较轻量

2.select模型拥有较强的兼容性,各个平台和语言都有实现

3.支持微秒级别的定时阻塞监听,如果对时间精度有需求,select可以满足

4.较为适合监听数量较小(局域网)等场景

缺点

1.监听数量较小,最大只能监听1024,无法满足 高并发需求

2.轮询问题, 随着轮询数量的增长,IO处理性能呈线性下降

3.用户需要对传出传出监听集合进行分离设置

4.select只返回就绪的数量,没有反馈就绪的socket,需要用户自行遍历查找,开销较大

5.select可以监听的事件数量较少,select设置监听是批处理以集合为单位的无法对不同的socket 设置不同的事件监听

6.select多轮使用会出现大量重复的拷贝开销和挂载监听开销



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。