《Linux C/C++服务器开发实践》之第7章 服务器模型设计

7.1 I/O模型7.1.1 基本概念7.1.2 同步和异步7.1.3 阻塞和非阻塞7.1.4 同步与异步和阻塞与非阻塞的关系7.1.5 采用socket I/O模型的原因7.1.6(同步)阻塞I/O模型7.1.7(同步)非阻塞I/O模型7.1.8(同步)I/O多路复用模型7.1.9(同步)信号驱动式I/O模型7.1.10 异步I/O模型7.1.11 五种I/O模型比较07.udpclient.c07.tcpclient.c

7.2 (分时)循环服务器7.2.1 UDP循环服务器07.01.udpserver.c

7.2.2 TCP循环服务器07.02.tcpserver.c

7.3 多进程并发服务器07.03.tcpforkserver.c

7.4 多线程并发服务器07.04.tcpthreadserver.c

7.5 I/O多路复用服务器7.5.1 使用场景7.5.2 基于select的服务器07.05.tcpselectserver.c

7.5.3 基于poll的服务器07.06.tcppollserver.c

7.5.4 基于epoll的服务器07.07.tcpepollserver.c

按使用协议分为TCP服务器和UDP服务器,按处理方式分为循环服务器和并发服务器。

网络服务器的设计模型:

(分时)循环服务器

多进程并发服务器

多线程并发服务器

I/O(Input/Output,输入/输出)复用并发服务器

7.1 I/O模型

7.1.1 基本概念

I/O即数据的读取(接收)和写入(发送)操作,分为内存I/O、网络I/O、磁盘I/O。 进程中的完整I/O分为两个阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网卡等)。

进程无法直接操作I/O设备,通过系统调用请求内核协助完成I/O操作。内核为每个I/O设备维护一个缓冲区。对于输入操作,进程I/O系统调用后,内核先看缓冲区是否有相应数据,无则到设备(比如网卡设备)读取(设备I/O慢,需等待),有则直接复制到用户进程空间。

网络输出操作两阶段: (1)等待网络数据到达网卡,把数据从网卡读取到内核缓冲区,准备好数据。 (2)从内核缓冲区复制数据到用户进程空间。

网络I/O的本质是socket的读取,对流的操作。一次I/O访问,数据先拷贝到操作系统的内核缓冲区,然后从内核缓冲区拷贝到应用程序的地址空间。

网络I/O模型分为异步I/O(asynchronous I/O)和同步I/O(synchronous I/O),同步I/O包括阻塞I/O(blocking I/O)、非阻塞I/O(non-blocking I/O)、多路复用I/O(multiplexing I/O)和信号驱动式I/O(signal-driven I/O)。

7.1.2 同步和异步

是否等请求出最终结果。异步调用完成后,通过状态、通知、信号和回调来通知调用者。

7.1.3 阻塞和非阻塞

与等待消息通知时的状态(调用线程)有关。非阻塞方式可提高CPU的利用率,但同时增加系统的线程切换。

7.1.4 同步与异步和阻塞与非阻塞的关系

异步肯定是非阻塞的。 同步非阻塞效率低,但高于同步阻塞。 线程五种状态:新建、就绪、运行、阻塞、死亡。 阻塞状态线程放弃CPU的使用,暂停运行,等导致阻塞的原因消除后恢复运行,或者被其他线程中断,推出阻塞状态,抛出InterruptedException。 线程进入阻塞原因: (1)sleep休眠。 (2)调用I/O阻塞的操作。 (3)试图获取其他线程持有的锁。 (4)等待某个触发条件。 (5)执行wait()方法,等待其他线程执行notify()或者notifyAll()方法。

引起线程阻塞的函数叫阻塞函数。 阻塞函数一定是同步函数,同步函数不一定是阻塞函数。 同步函数做完事情后才返回;阻塞函数也是做完事情后才返回,且会引起线程阻塞。

可能阻塞套接字的socket api分类: (1)输入操作 recv、recvfrom函数。套接字缓冲区无数据可读,数据到来前阻塞。 (2)输出操作 send、sendto函数。套接字缓冲区无可用空间,线程休眠到有空间。 (3)接受连接 accept函数。无连接请求,会阻塞。 (4)外出连接 connect函数。收到服务器应答前,不会返回。

非阻塞socket在发送缓冲区无足够空间时,会部分拷贝,返回拷贝字节数,将errno置为EWOULDBLOCK。 非阻塞socket在接收缓冲区无数据时,返回-1,将errno置为EWOULDBLOCK。

7.1.5 采用socket I/O模型的原因

同步通信操作会阻塞同一线程的其他操作。

同步通信(阻塞通信)+多线程,可改善同步阻塞线程的情况。可运行线程间上下文切换,浪费CPU时间,效率低。

异步方式更好,但不总能保证收发成功。

7.1.6(同步)阻塞I/O模型

一次读取I/O操作的两个阶段: (1)等待数据准备好,到达内核缓冲区。 (2)从内核向进程复制数据。 该模型两个阶段都阻塞,但简单、实时性高、响应及时无延时。

7.1.7(同步)非阻塞I/O模型

非阻塞recvform调用后,内核马上返回给进程,若数据未准备好,返回error(EAGAIN或EWOULDBLOCK)。进程返回后,可先处理其他业务逻辑,稍后继续调用recvform。采用轮询方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。

第二阶段会阻塞。 该模型能够在等待任务完成的时间里做其他工作,但响应延迟增大,整体数据吞吐量降低,因为轮询。

7.1.8(同步)I/O多路复用模型

单进程同时处理多个网络连接的I/O。应用程序不监视,而内核监视文件描述符。 select,epoll。 系统开销小,不需要创建和维护额外的进程或线程,维护少,节省系统资源,主要应用场景: (1)同时处理多个监听状态或连接状态的套接字。 (2)同时处理多种协议的套接字。 (3)监听多个端口或处理多种服务。 (4)同时处理用户输入和网络连接。

7.1.9(同步)信号驱动式I/O模型

注册信号处理函数,进程运行不阻塞。数据准备好时,进程收到SIGIO信号,信号处理函数中调用I/O操作。

7.1.10 异步I/O模型

系统调用后不阻塞进程。等数据准备好,内核直接复制数据到进程空间,然后内核通知进程,数据在用户空间,可以处理。 通过信号方式通知,三种情况: (1)进程进行用户态逻辑,强行打断,调用注册的信号处理函数。 (2)进程在内核态处理,比如同步阻塞读写磁盘,会挂起通知,等内核态事情完成,回到用户态,再触发信号通知。 (3)进程挂起,比如睡眠,唤醒进程,等待CPU调度,触发信号通知。

7.1.11 五种I/O模型比较

前四种同步I/O操作,第二阶段一样:数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。 异步I/O模型在等待和接收数据阶段都是非阻塞的,可以处理其他逻辑,整个I/O操作由内核完成,完成后发送通知。在此期间,用户进程不需要检查I/O操作的状态,也不需要主动拷贝数据。

07.udpclient.c

#include

#include

// #pragma comment(lib, "wsock32")

#define PORT 8888

int main()

{

WSADATA wsadata;

if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0)

{

printf("WSAStartup failed\n");

WSACleanup();

return -1;

}

struct sockaddr_in saddr;

memset(&saddr, 0, sizeof(saddr));

saddr.sin_family = AF_INET;

saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ifconfig

saddr.sin_port = htons(PORT);

/**** get protocol number from protocol name ****/

// struct hostent *phe; // host information

// struct servent *pse; // server information

struct protoent *ppe; // protocol information

if ((ppe = getprotobyname("UDP")) == 0)

{

printf("get protocol information error\n");

WSACleanup();

return -1;

}

SOCKET s = socket(PF_INET, SOCK_DGRAM, ppe->p_proto);

if (s == INVALID_SOCKET)

{

printf(" creat socket error \n");

WSACleanup();

return -1;

}

char wbuf[50] = "hello, server!";

// printf("please enter data:");

// sscanf_s("%s", wbuf, sizeof(wbuf));

int ret = sendto(s, wbuf, strlen(wbuf), 0, (struct sockaddr *)&saddr, sizeof(struct sockaddr));

if (ret < 0)

perror("sendto failed");

char rbuf[100] = {0};

struct sockaddr_in raddr; // endpoint IP address

int fromlen = sizeof(struct sockaddr);

int len = recvfrom(s, rbuf, sizeof(rbuf), 0, (struct sockaddr *)&raddr, &fromlen);

if (len < 0)

perror("recvfrom failed");

printf("server reply: %s\n", rbuf);

closesocket(s);

WSACleanup();

return 0;

}

// gcc 7.udpclient.c -o 7.udpclient.exe -lwsock32 && 7.udpclient.exe

07.tcpclient.c

#include

#include

// #pragma comment(lib, "wsock32")

#define PORT 8888

int main()

{

WSADATA wsadata;

if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0)

{

printf("WSAStartup failed\n");

WSACleanup();

return -1;

}

struct sockaddr_in saddr;

memset(&saddr, 0, sizeof(saddr));

saddr.sin_family = AF_INET;

saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ifconfig

saddr.sin_port = htons(PORT);

SOCKET s = socket(PF_INET, SOCK_STREAM, 0);

if (s == INVALID_SOCKET)

{

printf("creat socket error\n");

WSACleanup();

return -1;

}

if (connect(s, (struct sockaddr *)&saddr, sizeof(saddr)) == SOCKET_ERROR)

{

printf("connect socket error\n");

WSACleanup();

return -1;

}

char wbuf[50] = "hello, server";

// printf("please enter data:");

// sscanf_s("%s", wbuf, sizeof(wbuf));

int len = send(s, wbuf, strlen(wbuf), 0);

if (len < 0)

perror("send failed");

shutdown(s, SD_SEND);

char rbuf[100] = {0};

len = recv(s, rbuf, sizeof(rbuf), 0);

if (len < 0)

perror("recv failed");

printf("server reply: %s\n", rbuf);

closesocket(s);

WSACleanup();

return 0;

}

// gcc 7.tcpclient.c -o 7.tcpclient.exe -lwsock32 && 7.tcpclient.exe

7.2 (分时)循环服务器

串行处理客户端的请求。

7.2.1 UDP循环服务器

socket(...);

bind(...);

while(1)

{

recvfrom(...);

process(...);

sendto(...);

}

07.01.udpserver.c

#include

#include

#include

#include

#include

#include

#include

#include

int main()

{

struct sockaddr_in saddr;

memset(&saddr, 0, sizeof(struct sockaddr_in));

saddr.sin_family = AF_INET;

saddr.sin_addr.s_addr = htonl(INADDR_ANY);

saddr.sin_port = htons(8888);

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

if (sockfd < 0)

{

puts("socket failed");

return -1;

}

char on = 1;

setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

int val = sizeof(struct sockaddr);

int ret = bind(sockfd, (struct sockaddr *)&saddr, val);

if (ret < 0)

{

puts("sbind failed");

return -1;

}

struct sockaddr_in raddr;

char rbuf[50];

char sbuf[100];

while (1)

{

puts("waiting data");

memset(rbuf, 0, 50);

ret = recvfrom(sockfd, rbuf, 50, 0, (struct sockaddr *)&raddr, (socklen_t *)&val);

if (ret < 0)

perror("recvfrom failed");

printf("recv data: %s\n", rbuf);

memset(sbuf, 0, 100);

sprintf(sbuf, "server has received your data(%s)\n", rbuf);

ret = sendto(sockfd, sbuf, strlen(sbuf), 0, (struct sockaddr *)&raddr, sizeof(struct sockaddr));

}

close(sockfd);

return 0;

}

7.2.2 TCP循环服务器

socket(...);

bind(...);

listen(...);

while(1)

{

accept(...);

process(...);

close(...);

}

07.02.tcpserver.c

#include

#include

#include

#include

#include

#include

#include

#include

#define PORT 8888

int main()

{

struct sockaddr_in sin; // endpoint IP address

memset(&sin, 0, sizeof(sin));

sin.sin_family = AF_INET;

sin.sin_addr.s_addr = INADDR_ANY;

sin.sin_port = htons(PORT);

int s = socket(PF_INET, SOCK_STREAM, 0);

if (s == -1)

{

printf("creat socket error\n");

return -1;

}

if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) == -1)

{

printf("socket bind error\n");

return -1;

}

if (listen(s, 10) == -1)

{

printf(" socket listen error\n");

return -1;

}

int alen = sizeof(struct sockaddr);

struct sockaddr_in fsin;

int connum = 0;

while (1)

{

puts("waiting client...");

int clisock = accept(s, (struct sockaddr *)&fsin, (socklen_t *)&alen);

if (clisock == -1)

{

printf("accept failed\n");

return -1;

}

connum++;

printf("%d client comes\n", connum);

char rbuf[64] = {0};

int len = recv(clisock, rbuf, sizeof(rbuf), 0);

if (len < 0)

perror("recv failed");

char buf[128] = {0};

sprintf(buf, "Server has received your data(%s).", rbuf);

send(clisock, buf, strlen(buf), 0);

close(clisock);

}

close(s);

return 0;

}

7.3 多进程并发服务器

客户端有请求时,服务器创建子进程处理,父进程继续等待其他客户端的请求。

#include

#include

pid_t fork();

//成功返回0(子进程)和大于0(父进程中返回子进程ID),错误-1(进程上限或内存不足)

子进程复制父进程资源:进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,Linux内核采取写时拷贝技术(Copy on Write)提高效率。

#include

#include

#include

int main()

{

pid_t pid = fork();

if(pid == -1)//创建子进程失败

{

perror("cannot fork");

return -1;

}

else if(pid == 0)//子进程

{

printf("This is child process\n");

//getpid()获取自己的进程号

printf("Pid is %d, My PID is %d\n", pid, getpid());

}

else//父进程,pid为子进程ID

{

printf("This is parent process\n");

printf("Pid is %d, My PID is %d\n", pid, getpid());

}

return 0;

}

int sockfd = socket(...);

bind(...);

listen(...);

while(1)

{

int connfd = accept(...);

if(fork() == 0)//子进程

{

close(sockfd);//关闭监听套接字

process(...);//具体事件处理

close(connfd);//关闭已连接套接字

exit(0);//结束子进程

}

close(connfd);//关闭已连接套接字

}

close(sockfd);

07.03.tcpforkserver.c

#include

#include

#include

#include

#include

#include

#include

// #include

int main()

{

unsigned short port = 8888;

struct sockaddr_in my_addr;

bzero(&my_addr, sizeof(my_addr));

my_addr.sin_family = AF_INET;

my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

// inet_pton(AF_INET, "127.0.0.1", &my_addr.sin_addr);

my_addr.sin_port = htons(port);

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd < 0)

{

perror("socket");

exit(-1);

}

char on = 1;

setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

int err_log = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));

if (err_log != 0)

{

perror("binding");

close(sockfd);

exit(-1);

}

err_log = listen(sockfd, 10);

if (err_log != 0)

{

perror("listen");

close(sockfd);

exit(-1);

}

socklen_t cliaddr_len = sizeof(struct sockaddr_in);

while (1)

{

puts("Father process is waitting client...");

struct sockaddr_in client_addr;

int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len);

if (connfd < 0)

{

perror("accept");

close(sockfd);

exit(-1);

}

pid_t pid = fork();

if (pid < 0)

{

perror("fork");

_exit(-1);

}

else if (0 == pid)

{

close(sockfd);

/*

INT WSAAPI inet_pton(

INT Family, //地址家族 IPV4使用AF_INET IPV6使用AF_INET6

PCSTR pszAddrString, //指向以NULL为结尾的字符串指针,该字符串包含要转换为数字的二进制形式的IP地址文本形式。

PVOID pAddrBuf//指向存储二进制表达式的缓冲区

);

*/

/*

PCWSTR WSAAPI InetNtopW(

INT Family, //地址家族 IPV4使用AF_INET IPV6使用AF_INET6

const VOID *pAddr, //指向网络字节中要转换为字符串的IP地址的指针

PWSTR pStringBuf,//指向缓冲区的指针,该缓冲区用于存储IP地址的以NULL终止的字符串表示形式。

size_t StringBufSize//输入时,由pStringBuf参数指向的缓冲区的长度(以字符为单位)

);

*/

char cli_ip[INET_ADDRSTRLEN] = {0};

memset(cli_ip, 0, sizeof(cli_ip));

inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

printf("----------------------------------------------\n");

printf("client ip=%s, port=%d\n", cli_ip, ntohs(client_addr.sin_port));

char recv_buf[1024];

int recv_len = 0;

while ((recv_len = recv(connfd, recv_buf, sizeof(recv_buf) - 1, 0)) > 0)

{

recv_buf[recv_len] = 0;

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

send(connfd, recv_buf, recv_len, 0);

}

printf("client_port %d closed!\n", ntohs(client_addr.sin_port));

close(connfd);

exit(0);

}

else

close(connfd);

}

close(sockfd);

return 0;

}

7.4 多线程并发服务器

进程消耗较大的系统资源。一个进程内的所有线程共享相同的全局内存、全局变量等,注意同步问题。 针对客户端的每个请求,主线程都会创建一个工作者线程,负责和客户端通信。

void *client_fun(void *arg)

{

int connfd = *(int *)arg;

fun();

close(connfd);

}

int sockfd = socket(...);

bind(...);

listen(...);

while(1)

{

int connfd = accept(...);

pthread_t tid;

pthread_create(&tid, NULL, (void *)client_fun, (void *)connfd);

pthread_deatch(tid);

}

close(sockfd);//关闭监听套接字

07.04.tcpthreadserver.c

#include

#include

#include

#include

#include

#include

#include

#include

void *client_process(void *arg)

{

int recv_len;

char recv_buf[1024];

int connfd = *(int *)arg;

while ((recv_len = recv(connfd, recv_buf, sizeof(recv_buf) - 1, 0)) > 0)

{

recv_buf[recv_len] = 0;

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

send(connfd, recv_buf, recv_len, 0);

}

printf("client closed!\n");

close(connfd);

return NULL;

}

int main()

{

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd < 0)

{

perror("socket error");

exit(-1);

}

unsigned short port = 8888;

struct sockaddr_in my_addr;

bzero(&my_addr, sizeof(my_addr));

my_addr.sin_family = AF_INET;

my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

my_addr.sin_port = htons(port);

printf("Binding server to port %d\n", port);

char on = 1;

setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

int err_log = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));

if (err_log != 0)

{

perror("bind");

close(sockfd);

exit(-1);

}

err_log = listen(sockfd, 10);

if (err_log != 0)

{

perror("listen");

close(sockfd);

exit(-1);

}

int connfd;

while (1)

{

printf("Waiting client...\n");

struct sockaddr_in client_addr;

socklen_t cliaddr_len = sizeof(client_addr);

connfd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len);

if (connfd < 0)

{

perror("accept this time");

continue;

}

char cli_ip[INET_ADDRSTRLEN] = "";

inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

printf("----------------------------------------------\n");

printf("client ip=%s, port=%d\n", cli_ip, ntohs(client_addr.sin_port));

if (connfd > 0)

{

pthread_t thread_id;

pthread_create(&thread_id, NULL, client_process, (void *)&connfd);

pthread_detach(thread_id);

}

}

close(sockfd);

return 0;

}

7.5 I/O多路复用服务器

select、pselect、poll、epoll等系统调用支持I/O多路复用,通过进程监视多个描述符,描述符就绪(可读写),通知程序进行相应处理。本质上是同步I/O,读写过程是阻塞的,需要读写事件就绪后自己负责读写。异步I/O无需自己负责读写,它会负责把数据从内核拷贝到用户空间。

I/O多路复用的最大优势是系统开销小,无进程/线程的创建和维护。 epoll是Linux特有,select是POSIX规定,一般操作系统均可实现。

7.5.1 使用场景

客户端处理多个描述符(交互式输入和网络套接字),必须使用客户端处理多个套接字,很少出现TCP服务器处理监听套接字和已连接套接字服务器处理TCP和UDP服务器处理多个服务或多个协议

7.5.2 基于select的服务器

进程调用select(阻塞),内核监视多个socket,任一socket准备好(可读、可写、异常),返回。此时进程执行read、write、exit等。

select缺点: I/O线程不断轮询套接字集合状态,浪费CPU资源。 不适合管理大量客户端连接。 性能低下,需大量查找和拷贝。 传递给select函数的参数告诉内核的信息: 需要监视的文件描述符 每个文件描述符的监视状态(读、写、异常) 等待时间(无限长、固定、0) select返回后,可获取的内核信息: 准备好的文件描述符个数 文件描述符的具体状态(读、写、异常) 可以调用合适的I/O(read或write),不会被阻塞

#include

int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

//maxfd,最大文件描述符的值+1

//readfds,套接字读变化

//writefds,套接字写变化

//exceptfds,套接字异常变化

//timeout,等待时间,NULL阻塞,0非阻塞,大于0超时时间

select函数返回时,fd_set结构中填入相应套接字。

readfds数组包含套接字:

有数据可读,recv立即读取连接已关闭、重设或终止有请求建立连接的套接字,accept会成功

writefds数组包含套接字:

有数据可发出,send立即发送connect,已连接成功

exceptfds数组包含套接字:

connect,已连接失败带外数据可读

struct timeval

{

long tv_sect;

long tv_usect;

};

非0则等到超时,若成功timeval会被修改为剩余时间。

typedef struct fd_set

{

u_int fd_count;

socket fd_array[FD_SETSIZE];

} fd_set;

//set集合初始化为空集合

void FD_ZERO(fd_set *set);

//套接字fd加入set集合中

void FD_SET(int fd, fd_set *set);

//set集合中删除套接字fd

void FD_CLR(int fd, fd_set *set);

//检查fd是否为set集合成员

void FD_ISSET(int fd, fd_set *set);

套接字可读写判断步骤:

初始化套接字集合,FD_ZERO(&readfds)指定套接字放入集合,FD_SET(s, &readfds)调用select函数,返回所有fd_set集合中变化套接字总个数,并会更新集合中变化套接字状态遍历集合,判断s是否在某个集合内。FD_ISSET(s, &readfds)调用相应socket api函数操作套接字

07.05.tcpselectserver.c

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define MYPORT 8888

#define MAXCLINE 5

#define BUF_SIZE 200

int conn_amount = 0;

int fd[MAXCLINE] = {0};

void showclient()

{

printf("client amount: %d\n", conn_amount);

for (int i = 0; i < MAXCLINE; i++)

printf("[%d]: %d ", i, fd[i]);

printf("\n\n");

}

int main()

{

int sock_fd;

if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)

{

perror("setsockopt");

exit(1);

}

int yes = 1;

if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)

{

perror("setsockopt error \n");

exit(1);

}

struct sockaddr_in server_addr;

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(MYPORT);

if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)

{

perror("bind error!\n");

close(sock_fd);

exit(1);

}

if (listen(sock_fd, MAXCLINE) == -1)

{

perror("listen error!\n");

close(sock_fd);

exit(1);

}

printf("listen port %d\n", MYPORT);

int maxsock = sock_fd;

struct timeval tv = {30, 0};

while (1)

{

fd_set fdsr;

FD_ZERO(&fdsr);

FD_SET(sock_fd, &fdsr); // 监听套接字

for (int i = 0; i < MAXCLINE; i++)

if (fd[i] != 0)

FD_SET(fd[i], &fdsr); // 连接套接字

int ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);

if (ret < 0)

{

perror("select error!\n");

break;

}

else if (ret == 0)

{

printf("timeout\n");

continue;

}

for (int i = 0; i < conn_amount; i++)

{

if (FD_ISSET(fd[i], &fdsr))

{

char buf[BUF_SIZE];

ret = recv(fd[i], buf, sizeof(buf), 0);

if (ret <= 0)

{

printf("client[%d] close\n", i);

close(fd[i]);

FD_CLR(fd[i], &fdsr);

fd[i] = 0;

conn_amount--;

}

else

{

if (ret < BUF_SIZE)

{

memset(&buf[ret], '\0', 1);

ret += 1;

}

printf("client[%d] send: %s\n", i, buf);

send(fd[i], buf, ret, 0);

}

}

}

if (FD_ISSET(sock_fd, &fdsr))

{

struct sockaddr_in client_addr;

socklen_t sin_size = sizeof(struct sockaddr_in);

int new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);

if (new_fd <= 0)

{

perror("accept error\n");

continue;

}

if (conn_amount < MAXCLINE)

{

for (int i = 0; i < MAXCLINE; i++)

{

if (fd[i] == 0)

{

fd[i] = new_fd;

break;

}

}

conn_amount++;

printf("new connection client[%d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

if (new_fd > maxsock)

maxsock = new_fd;

}

else

{

printf("max connections arrive, exit\n");

send(new_fd, "bye", 4, 0);

close(new_fd);

continue;

}

}

showclient();

}

for (int i = 0; i < MAXCLINE; i++)

if (fd[i] != 0)

close(fd[i]);

close(sock_fd);

return 0;

}

7.5.3 基于poll的服务器

poll和select本质一样,管理多个描述符进行轮询,根据描述符状态进行处理,但poll无文件描述符数量的限制(过多性能下降)。相同缺点是大量文件描述符数组整体在用户态和内核的地址空间之间进行复制,无论描述符是否就绪。 poll函数在指定时间内轮询一定数量的文件描述符,测试是否有就绪者,监测多个事件,若无事件发生,进程睡眠,放弃CPU控制权。若监测的任一事件发生,唤醒进程,判断事件,执行相应操作。退出后,struct pollfd变量清零,需重新设置。

#include

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

//timeout:-1永远等待,0立即返回,大于0,等待毫米数。

//失败返回-1,errno值如下:

//EBADF,结构体中存在无效文件描述符

//EFAULT,fds指针指向的地址超出进程的地址空间

//EINTR,请求的事件之前产生一个信号,调用可以重新发起。

//EINVAL,nfds参数超出PLIMIT_NOFILE值

//ENOMEM,可用内存不足,无法完成请求

struct pollfd{

int fd;//文件描述符

short events;//等待的事件,用户设置,告诉内核我们关注什么

short revents;//实际发生的事件,内核调用返回时设置,说明该描述符发生了什么事件

};

//POLLIN

//POLLOUT

//POLLERR

ssize_t write(int fd, const void *buf, size_t count);

ssize_t read(int fd, void *buf, size_t count);

#include

#include

#include

int main()

{

char *p1 = "This is a c test code";

volatile int len = 0;

int fp = open("/home/test.txt", O_RDWR|O_CREAT);

while(1){

int n;

if((n=write(fp, pl+len, strlen(pl)-len)) == 0)

{

printf("n = %d\n", n);

break;

}

len += n;

}

return 0;

}

07.06.tcppollserver.c

#ifndef _GNU_SOURCE

#define _GNU_SOURCE

#endif

#include

#include

#include

#include

#include

#include

#include

#include

#include

void errExit()

{

exit(-1);

}

const char resp[] = "HTTP/1.1 200\r\n\

Content-Type: application/json\r\n\

Content-Length: 13\r\n\

Date: Thu, 2 Aug 2021 04:02:00 GMT\r\n\

Keep-Alive: timeout=60\r\n\

Connection: keep-alive\r\n\

\r\n\

[HELLO WORLD]";

int main()

{

int sd = socket(AF_INET, SOCK_STREAM, 0);

if (sd == -1)

errExit();

fprintf(stderr, "created socket\n");

int opt = 1;

if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)) == -1)

errExit();

fprintf(stderr, "socket opt set\n");

const int port = 8888;

struct sockaddr_in addr = {0};

addr.sin_family = AF_INET;

addr.sin_addr.s_addr = INADDR_ANY;

addr.sin_port = htons(port);

socklen_t addrLen = sizeof(addr);

if (bind(sd, (struct sockaddr *)&addr, sizeof(addr)) == -1)

errExit();

fprintf(stderr, "socket binded\n");

if (listen(sd, 1024) == -1)

errExit();

fprintf(stderr, "socket listen start\n");

// number of poll fds

int currentFdNum = 1;

struct pollfd *fds = (struct pollfd *)calloc(100, sizeof(struct pollfd));

fds[0].fd = sd;

fds[0].events = POLLIN;

nfds_t nfds = 1;

fprintf(stderr, "polling\n");

while (1)

{

int timeout = -1;

int ret = poll(fds, nfds, timeout);

fprintf(stderr, "poll returned with ret value: %d\n", ret);

if (ret == -1)

errExit();

else if (ret == 0)

fprintf(stderr, "return no data\n");

else

{

fprintf(stderr, "checking fds\n");

if (fds[0].revents & POLLIN)

{

struct sockaddr_in childAddr;

socklen_t childAddrLen;

int childSd = accept(sd, (struct sockaddr *)&childAddr, &(childAddrLen));

if (childSd == -1)

errExit();

fprintf(stderr, "child got\n");

// set non_block

int flags = fcntl(childSd, F_GETFL);

if (fcntl(childSd, F_SETFL, flags | O_NONBLOCK) == -1)

errExit();

fprintf(stderr, "child set nonblock\n");

// add child to list

fds[currentFdNum].fd = childSd;

fds[currentFdNum].events = (POLLIN | POLLRDHUP);

nfds++;

currentFdNum++;

fprintf(stderr, "child: %d pushed to poll list\n", currentFdNum - 1);

}

// child read & write

for (int i = 1; i < currentFdNum; i++)

{

if (fds[i].revents & (POLLHUP | POLLRDHUP | POLLNVAL))

{

fprintf(stderr, "child: %d shutdown\n", i);

close(fds[i].fd);

fds[i].events = 0;

fds[i].fd = -1;

continue;

}

// read

if (fds[i].revents & POLLIN)

{

char buffer[1024] = {0};

while (1)

{

ret = read(fds[i].fd, buffer, 1024);

fprintf(stderr, "read on: %d returned with value: %d\n", i, ret);

if (ret == 0)

{

fprintf(stderr, "read returned 0(EOF) on: %d, breaking\n", i);

break;

}

else if (ret == -1)

{

const int tmpErrno = errno;

if (tmpErrno == EWOULDBLOCK || tmpErrno == EAGAIN)

{

fprintf(stderr, "read would block, stop reading\n");

fds[i].events |= POLLOUT;

break;

}

else

{

errExit();

}

}

}

}

// write

if (fds[i].revents & POLLOUT)

{

ret = write(fds[i].fd, resp, sizeof(resp));

fprintf(stderr, "write on: %d returned with value: %d\n", i, ret);

if (ret == -1)

errExit();

fds[i].events &= !(POLLOUT);

}

}

}

}

return 0;

}

7.5.4 基于epoll的服务器

epoll只需要监听已经准备好的队列集合中的文件描述符。 select主要缺点: (1)单个进程监视的文件描述符有上限,通常1024. (2)内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,巨大开销。 (3)返回整个句柄数组,遍历才能发现发生事件的句柄。 (4)水平触发,已就绪的文件描述符未完成I/O操作,每次调用select都会通知。 poll用链表保存文件描述符,无数量限制。

epoll三大关键要素:mmap、红黑树、链表。mmap将用户空间和内核空间的地址映射到相同物理内存地址,减少用户态和内核态的数据交换。内核可以直接看到epoll监听的句柄,效率高。红黑树存储epoll监听套接字,epoll_ctr在红黑树上插入或删除套接字。添加事件时,会建立与相应设备(网卡)驱动程序的回调关系ep_poll_callback,回调函数ep_poll_callback会将发生的事件放入双向链表rdllist中。epoll_wait时,只检测rdlist中是否存在注册的事件,效率非常高,这里需要将发生了的事件复制到用户态内存中。

红黑树+双链表+回调机制,造就epoll的高效。

select、poll采用轮询遍历,检测就绪事件,LT工作方式。 epoll采用回调检测就绪事件,支持ET高效模式。

epoll的两种工作方式:

水平触发(LT),缺省,描述符就绪,内核通知,未处理,下次还通知。边缘触发(ET),只支持非阻塞描述符。需保证缓存区的数据全部读取或写出,下次不会通知。

07.07.tcpepollserver.c

在这里插入代码片

精彩内容

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。