之前写过一篇C++
关于网络编程入门的博文:
socket网络编程入门
主要是介绍C++
网络编程的API
接口的使用,该博文中的例子对socket
的
调用流程是最简单且最基本的,它只能实现一对一通信,因为它使用的是同步阻塞的方式。
现代网络编程都需要考虑到并发,也就是一对多的通信状态,
继续使用之前一对一的通信模型是行不通的,只有通过改进网络I/O
模型来实现。
C10k
问题提出该问题的作者是Dan Kegel
是Winetricks
的作者,博客原文
《The C10K problem》。
相关的资源拓展:
高性能网络编程经典:《The C10K problem(英文)》[附件下载]
原文介绍了一些I/O
框架,其中libevent
是目前后端开发较常用的网络库,
本人曾经也在项目中使用过该库,是实现多并发较理想的解决方案之一。
C10k
问题的本质是尽可能的减少网络程序并发状态下的服务器资源消耗,
比如避免或减少频繁的创建和销毁进程和线程,针对此问题的解决方案有使用线程池来管理线程资源;
避免或减少频繁的数据拷贝,引申出了零拷贝的解决方案。
总的来说解决C10k
问题的关键就是尽可能减少这些CPU资源消耗!
这里我想通过逐一介绍不同的网络I/O
模型来引出现阶段网络编程处理多并发场景较理想的解决方案。
以最基本的实现一对一通信模型的socket
代码为例展开修改,
这里仅以tcp
服务端代码举例,客户端使用其它软件连接:
/* 同步阻塞基本socket服务端模型 */
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_LENGTH 128
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //
if (listenfd == -1) {
return -1;
}
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
listen(listenfd, 10);
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
unsigned char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
printf("buffer : %s, ret: %d\n", buffer, ret);
ret = send(clientfd, buffer, ret, 0);
close(clientfd);
return 0;
}
socket
连接传统的I/O
网络模型就是阻塞式I/O
模型,在 socket网络编程入门
这篇文章中的例子就是该模型。刚才已经在上文说过这类模型不适合直接应用于多并发场景下的网络编程。
上述的代码很简单,问题也很明显:
接下来我们使用循环来处理上面的问题,
当然像下面这样直接将接受和发送的代码放入循环是不能解决根本问题的:
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_LENGTH 128
int main() {
// block
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //
if (listenfd == -1) return -1;
// listenfd
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
listen(listenfd, 10);
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
while (1) {
unsigned char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret == 0) {
close(clientfd);
break;
}
printf("buffer : %s, ret: %d\n", buffer, ret);
ret = send(clientfd, buffer, ret, 0);
}
return 0;
}
这里推荐阅读多线程模型
结合下面的代码一起理解。这里直接跳过了多进程模型是因为进程占用的资源较大,
而且进程的善后工作没做好会产生僵尸进程,这类进程越多就会逐渐耗尽我们的系统资源。
所以采用轻量级的多线程模型,是现阶段较好的解决方案。
首先将处理接受和发送数据的代码放入到线程中,用多线程模型来处理socket
连接的并发场景:
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_LENGTH 128
void *routine(void *arg) {
int clientfd = *(int *)arg;
printf("listen --> clientfd: %d\r\n",clientfd);
while (1) {
unsigned char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret == 0) {
printf("close clientfd: %d\r\n",clientfd);
close(clientfd);
break;
}
printf("buffer : %s, ret: %d clientfd: %d\r\n", buffer, ret,clientfd);
ret = send(clientfd, buffer, ret, 0); //
}
}
int main(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //
if (listenfd == -1) return -1;
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
listen(listenfd, 10);
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
pthread_t threadid;
pthread_create(&threadid, NULL, routine, &clientfd);
}
return 0;
}
select
模型来处理多个socket
连接使用多线程循环处理socket
的结果似乎已经能满足一对多的想法了,但它仍然存在一些问题。
比如一个线程对应一个socket
请求,造成服务器资源的消耗较大,
频繁的创建和销毁线程对系统也是一个不小的开销,当然我们可以使用线程池的方式来避免线程的频繁创建和销毁,
但最关键的一点是,多线程模型还不能解决C10k
问题。
多线程循环处理socket
和select
的区别是,selcet
对线程进行了复用,也是I/O
多路复用模型的优势,
系统不需要频繁的创建和销毁线程,一个线程管理所有socket
,
它相对于多线程模型的优势是同时能处理更多的socket
连接:
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_LENGTH 128
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) return -1;
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
listen(listenfd, 10);
fd_set rfds, wfds, rset, wset;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int maxfd = listenfd;
unsigned char buffer[BUFFER_LENGTH] = {0}; // 0
int ret = 0;
while (1) {
/* 每次调用之前都要初始化fd_set结构体 */
rset = rfds;
wset = wfds;
int nready = select(maxfd+1, &rset, &wset, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) {
printf("listenfd --> %d\r\n",listenfd);
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
FD_SET(clientfd, &rfds);
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = listenfd+1; i <= maxfd;i ++) {
if (FD_ISSET(i, &rset)) { //
memset(buffer, 0, sizeof(buffer));
ret = recv(i, buffer, BUFFER_LENGTH, 0);
if (ret == 0) {
printf("close clientfd: %d\r\n",i);
close(i);
FD_CLR(i, &rfds);
} else if (ret > 0) {
printf("buffer : %s, ret: %d, clientfd: %d\r\n", buffer, ret,i);
FD_SET(i, &wfds);
}
} else if (FD_ISSET(i, &wset)) {
ret = send(i, buffer, ret, 0); //
FD_CLR(i, &wfds); //
FD_SET(i, &rfds);
}
}
}
return 0;
}
poll
模型来处理多个socket
连接上面的select
模型的优势很明显,实现了I/O
多路复用,但问题也不少,
比如监视文件句柄的数量上存在上限;每次调用之前都要重新初始化fd_set
结构体;需要通过遍历数组的方式来监视fd
的状态,存在不必要的消耗。
poll
模型主要解决了前面两个问题,它将select
使用的三个fd_set
结构体参数修改成了struct pollfd *
类型的数组的形式,原理还是和select
模型一样的,只是理论上解决了文件句柄数量的上限和避免了避免重复初始化的问题,遍历数组监视fd
状态的效率依旧存在。
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_FD 1024
#define BUFFER_LENGTH 128
int main(){
//创建一个侦听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) return -1;
//默认使用阻塞模式
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
printf("bind error.");
return -2;
}
//启动侦听
if (-1 == listen(listenfd, SOMAXCONN)){
printf("listen error.");
close(listenfd);
return -3;
}
struct pollfd fds[MAX_FD] = {0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int cur_max_fd = listenfd;
/* 初始化poll数组 */
int i = 0;
for(i=1; i < MAX_FD; i++){
fds[i].fd = -1;
}
unsigned char buffer[BUFFER_LENGTH] = {0}; // 0
int ret = 0;
while(1){
int nready = poll(fds, cur_max_fd+1, -1);
if (nready < 0){
perror("poll error\r\n");
return -4;
}
if(fds[0].revents & POLLIN){
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
fds[clientfd].fd = clientfd;//将客户端socket加入到集合中
fds[clientfd].events = POLLIN;
printf("listen --> \r\n");
if (clientfd > cur_max_fd) {
cur_max_fd = clientfd;
}
if (--nready == 0) continue;
}
for(i=listenfd+1; i<= cur_max_fd; i++){
if(fds[i].revents & POLLIN){
ret = recv(i, buffer, BUFFER_LENGTH, 0);
if(ret > 0){
buffer[ret] = '\0';
printf("buffer : %s, ret: %d, clientfd: %d\r\n", buffer, ret,i);
send(i, buffer, ret, 0);
}else if(ret == 0){
fds[i].fd = -1;
printf("close clientfd: %d\r\n",i);
close(i);
}
if (--nready == 0) break;
}
}
}
close(listenfd);
return 0;
}
epoll
epoll
使用了红黑树的数据结构来监视所有fd
的状态变化,它只返回状态有变化的fd
,
而不像select/poll
那样通过轮询扫描的方式去遍历整个fd
数组。
它利用三个关键的api
对fd
进行管理,先用epoll_create
创建一个 epoll
对象 epfd
,
再通过 epoll_ctl
将需要监视的 socket
添加到 epfd
中,最后调用 epoll_wait
等待数据。
在客户端请求的fd
数量达到10k
时,epoll
的性能优于select/poll
,这样的设计解决了C10k
问题:
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_FD 1024
#define BUFFER_LENGTH 128
int main(){
//创建一个侦听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == listenfd) return -1;
//默认使用阻塞模式
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
printf("bind error.");
return -2;
}
//启动侦听
if (-1 == listen(listenfd, SOMAXCONN)){
printf("listen error.");
close(listenfd);
return -3;
}
int epfd = epoll_create(1); //int size
struct epoll_event events[MAX_FD] = {0};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
unsigned char buffer[BUFFER_LENGTH] = {0}; // 0
int ret = 0;
while (1) {
int nready = epoll_wait(epfd, events, MAX_FD, 5);
if (nready == -1) continue;
int i = 0;
for (i = 0;i < nready;i ++) {
int clientfd = events[i].data.fd;
if (clientfd == listenfd) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int connfd = accept(listenfd, (struct sockaddr *)&client, &len);
printf("listen --> \r\n");
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else if (events[i].events & EPOLLIN) {
ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret > 0) {
buffer[ret] = '\0';
//printf("recv msg from client: %s\n", buffer);
printf("buffer : %s, ret: %d, clientfd: %d\r\n", buffer, ret,clientfd);
send(clientfd, buffer, ret, 0);
} else if (ret == 0) { //
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
printf("close clientfd: %d\r\n",clientfd);
close(clientfd);
}
}
}
}
close(listenfd);
return 0;
}
引用列表
《深入理解计算机系统》- 第三部分 程序间的交流和通信
深入理解LinuxIO复用之epoll
深入了解epoll模型(特别详细)
《图解系统》- 九、网络系统 -小林coding
《高性能网络编程》- 即时通讯网博客