目录
1.前言
尽管多进程与多线程等并发服务器模型在处理并发连接方面取得了显著的进步,允许服务器能够同时处理多个请求,但它们并没有完全解决网络编程中的所有问题。在这些模型中,服务器的效率和性能仍然受到IO操作的影响。例如,当服务器使用传统的阻塞IO模型时,它会在等待数据读写操作时被阻塞,导致无法同时处理其他请求,这在高并发场景下会导致资源的浪费和响应时间的延长。 此外,多进程和多线程模型虽然提升了并发处理能力,但它们也带来了额外的开销,如线程的上下文切换、资源竞争、进程间通信等问题。这些开销在处理大量并发连接时可能会变得尤为显著,影响服务器的整体性能。 因此,引入不同的IO模型对于优化服务器的IO操作至关重要。通过学习阻塞IO模型、非阻塞IO模型以及复用IO模型,我们可以更高效地管理服务器的IO操作,减少不必要的阻塞和资源消耗,提高服务器的吞吐量和响应速度。这不仅能提升用户体验,还能使服务器更好地适应现代网络环境中的高并发需求。因此,深入学习IO模型依然是网络编程中不可或缺的一部分。
2. IO模型
2.1三种模型的含义与区别
让我们用一个餐厅点餐和上菜的比喻来形象地说明阻塞IO、非阻塞IO和复用IO(以
epoll
为例)三种模型的区别:阻塞IO模型
想象一下,你进入一家餐厅,坐下后点了一份特色菜。服务员告诉你需要等待一段时间,因为这道菜需要现做。于是你开始等待,什么也做不了,既不能点别的菜,也不能离开桌子去做其他事。直到这道菜做好了,服务员才会给你上菜,你才能继续点餐或用餐。
在这个比喻中,你就是服务器,特色菜的准备过程就像是阻塞的IO操作。你(服务器)必须一直等待直到IO操作完成,这期间你不能处理其他任何事情。
非阻塞IO模型
现在,你来到了另一家餐厅,这家餐厅允许你点完菜后不必一直等待。你可以出去散步,或者在餐厅内四处逛逛。但你需要不时回来问问服务员,看看你的菜是否已经准备好。如果没好,你就得再次离开,过会儿再来。
在非阻塞IO模型中,你可以发起一个IO请求,然后立即去做其他事情。但你需要不断地检查IO操作是否完成,这就像是你不断地回到餐厅询问你的菜是否准备好。
复用IO模型(
select
/poll
)再想象一下,你是一家繁忙餐厅的经理,需要同时关注多张桌子的上菜情况。你站在餐厅中央,桌子上有小红旗,当菜准备好时,服务员会在相应的桌子上插上小红旗。你需要不断地环顾四周,查看哪些桌子上有小红旗,然后去上菜。
select
和poll
就像是这种方式,你需要不断地检查所有桌子(文件描述符),看看哪个桌子(IO操作)准备好了,然后去处理。复用IO模型(
epoll
)最后,你还是那家餐厅的经理,但现在你有了一种更先进的系统。服务员会用对讲机告诉你哪些菜已经准备好了,你只需要在对讲机里听到哪个桌子的菜好了,就去上菜,而不需要时刻关注所有桌子。
epoll
模型就像这个先进的系统,它会通知你哪些桌子(IO操作)已经准备好,你只需要处理那些真正准备好的桌子,而不需要不断地检查所有桌子。总结
- 阻塞IO:必须等待当前操作完成,不能同时处理其他事情。
- 非阻塞IO:可以同时做其他事情,但需要不断检查操作是否完成。
- 复用IO(
select
/poll
):可以同时监控多个操作,但效率随着监控数量增加而降低。- 复用IO(
epoll
):更高效地监控多个操作,只处理那些真正准备好的操作,减少了不必要的检查。通过这个比喻,我们可以更直观地理解这三种IO模型策略的区别。在网络编程中,选择合适的IO模型对于提高服务器的并发处理能力和整体性能至关重要。
2.2 poll函数原型
因笔者需要,暂时只需学习poll函数,所以这里只总结poll函数的应用,读者有其它学习需要可以另找资料。
poll
函数原型:
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:请求系统帮忙看一下在意的IO口是否有变化
参数说明:
fds
:指向pollfd
结构体数组的指针,每个pollfd
结构体表示一个要监控的文件描述符和相关的事件。nfds
:fds
数组中的结构体数量。timeout
:超时时间,单位为毫秒。如果设置为0,poll
会立即返回,不进行阻塞。如果设置为-1,poll
会一直阻塞直到有文件描述符就绪或者发生错误。
pollfd
结构体:struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生的事件 */ };
fd
:要监控的文件描述符。events
:指定要监控的事件,可以是以下值的按位或:
POLLIN
:文件描述符可读。POLLOUT
:文件描述符可写。POLLPRI
:文件描述符有紧急数据可读。POLLERR
:文件描述符发生错误。POLLHUP
:文件描述符挂断。POLLNVAL
:文件描述符无效。revents
:实际发生的事件,由poll
函数填充,表示在events
中设置的哪些事件已经就绪。返回值:
- 如果有文件描述符就绪或者超时,
poll
返回大于或等于0的值,表示至少有一个文件描述符就绪。- 如果发生错误,返回-1。
使用
poll
时,程序需要不断地调用poll
函数来检查文件描述符的状态,直到有文件描述符就绪或者超时。当poll
返回时,程序需要检查每个pollfd
结构体的revents
字段,以确定哪些事件发生了。
3.poll创建并发服务器代码示例
3.1效果展示
这里是用poll复用IO建立并发服务器模型,相较于之前多进程并发服务器模型,使用poll
函数创建并发服务器的优势在于它能够在单个进程内高效地管理大量并发连接,减少了资源消耗和上下文切换开销,提高了服务器的可扩展性和整体性能。
3.2代码框架及核心逻辑
此代码的逻辑框架是一个简单的TCP服务器,使用
poll
函数实现对多个客户端连接的并发服务。下面是对代码逻辑框架的总结和并发服务核心逻辑的解释:逻辑框架总结:
初始化服务器:在
main
函数中,首先通过调用tcp_init
函数初始化TCP服务器,监听8080端口。准备
pollfd
数组:初始化一个pollfd
数组,用于存储服务器需要监控的文件描述符及其事件。主循环:服务器进入一个无限循环,不断调用
poll
函数来等待文件描述符上的事件。接受新连接:如果监听的socket(
sfd
)上有新连接到达,服务器通过accept
函数接受连接,并获取新的客户端socket(cfd
)。存储新连接:在
pollfd
数组中找到一个空闲的位置,存储新连接的文件描述符。消息处理:对于已经建立的连接,如果
poll
检测到有可读事件,服务器调用Recv_Msg
函数来读取客户端发送的消息,并将消息长度发送回客户端。错误和超时处理:如果
poll
超时或Recv_Msg
函数返回错误,服务器将关闭相应的客户端连接,并从pollfd
数组中移除该连接。并发服务核心逻辑:
非阻塞监听:服务器的监听socket(
sfd
)通常设置为非阻塞模式,这样accept
调用在没有新连接到达时不会阻塞。
poll
函数:poll
函数是实现并发服务的核心。它允许服务器监控多个文件描述符上的事件,而不需要为每个连接创建一个线程或进程。当poll
检测到某个文件描述符上有事件时,服务器可以相应地处理该事件。事件驱动:服务器根据
poll
返回的事件(如可读POLLIN
)来决定对哪个客户端socket进行操作。这种事件驱动的方式使得服务器能够有效地管理多个客户端连接。动态管理连接:服务器使用一个数组来动态管理客户端连接。当有新连接到达时,服务器将其添加到
pollfd
数组中;当连接关闭时,服务器将其从数组中移除。循环检测:服务器在一个循环中不断检测
pollfd
数组中的事件,这样可以持续地为所有客户端提供服务。通过这种逻辑框架和核心逻辑,服务器能够以非阻塞的方式同时为多个客户端提供服务,提高了资源利用率和程序的并发处理能力。然而,这种实现方式在客户端数量非常多时可能不够高效,因为它使用了一个固定大小的数组来管理连接。在实际应用中,可能需要更高级的连接管理机制,如使用哈希表或数据库来跟踪每个连接的状态。
3.3代码展示
3.3.1 server.c
#include <stdio.h>
#include "net.h"
#include <poll.h>
#include <unistd.h>
#include <string.h>
int Recv_Msg(int cfd)
{
char buf[64] = {};
int ret = read(cfd, buf, sizeof(buf));
if(ret <= 0){
perror("read");
return -1;
}
int len = strlen(buf);
write(cfd, &len, sizeof(len));
return 0;
}
int main()
{
int sfd = tcp_init(8080, 20);
if(sfd < 0){
printf("init error\n");
return -1;
}
printf("wait connect\n");
struct pollfd pfd[1024];
int i;
for(i = 0; i < 1024; i++){
pfd[i].fd = -1;
pfd[i].events = POLLIN;
}
pfd[0].fd = sfd;
int maxfd = sfd;
int ret, cfd;
struct sockaddr_in clt;
int len = sizeof(clt);
while(1){
ret = poll(pfd, maxfd+1, 2000);
switch(ret){
case -1:
perror("poll");
return -1;
case 0:
printf("timeout\n");
break;
default:
if(pfd[0].revents == POLLIN){
cfd = accept(sfd, (struct sockaddr *)&clt, &len);
if(cfd < 0){
perror("accept");
break;
}
printf("%s:%d to connect, fd = %d\n",
inet_ntoa(clt.sin_addr),
ntohs(clt.sin_port), cfd);
for(int j = 1; j < 1024; j++){
if(pfd[j].fd == -1){
pfd[j].fd = cfd;
break;
}
}
maxfd = cfd >= maxfd ? cfd : maxfd;
}
for(i = 1; i < 1024; i++){
if(pfd[i].fd != -1 && pfd[i].revents == POLLIN){
//read
if(0 > Recv_Msg(pfd[i].fd)){
printf("client %d exit\n", pfd[i].fd);
close(pfd[i].fd);
pfd[i].fd = -1;
}
}
}
}
}
}
3.3.2client.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main()
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd < 0){
perror("socket");
return -1;
}
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(8080);
ser.sin_addr.s_addr = inet_addr("192.168.10.110");
int ret = connect(sfd, (struct sockaddr *)&ser, sizeof(ser));
if(ret < 0){
perror("connect");
return -1;
}
char buf[64];
while(1){
//fgets
memset(buf, 0, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
//write
ret = write(sfd, buf, sizeof(buf));
if(ret < 0){
perror("write");
return -1;
}
//
//read
//memset(buf, 0, sizeof(buf));
int str_len;
ret = read(sfd, &str_len, sizeof(str_len));
if(ret < 0){
perror("read");
return -1;
}
if(ret == 0){
printf("sys : disconnect\n");
return -1;
}
printf("result : %d\n", str_len);
}
}
3.3.3net.c
#include <stdio.h>
#include "net.h"
int tcp_init(int port, int backlog)
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd < 0)
return -1;
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(port);
ser.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sfd, (struct sockaddr *)&ser, sizeof(ser));
if(ret < 0)
return -1;
ret = listen(sfd, backlog);
if(ret < 0)
return -1;
return sfd;
}
3.3.4net.h
#ifndef __NET_H___
#define __NET_H___
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int tcp_init(int port, int backlog);
#endif
4.后续
接下来做一个网络编程的小项目,对网络编程的所学知识进行一个应用与总结。