前言
一、实现高性能网络服务器的四种方式
在之前我们实现的tcp server, tcp client, udp server, udp client中,默认只允许一个连接,在代码逻辑中表现为一个while死循环。为了允许更多的连接,我们需要使用一些机制来实现高性能网络服务器,具体的有以下四种方式。
二、通过fork实现高性能网络服务器
- 每收到一个连接就创建一个子进程。
- 父进程负责接收连接。
- 通过fork来创建子进程
#include<iostream>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<unistd.h>
#define PORT 8111
#define MESSAGE_LEN 1024
int main(int argc,char* argv[])
{
int socket_fd = -1,accept_fd = -1;
int on = 1;
int curpos = 0;
int backlog = 1000;
int flag = 1;
int ret = -1;
char in_buffer[MESSAGE_LEN] = {0,};
struct sockaddr_in localaddr,remoteaddr;
//create
socket_fd = socket(AF_INET, SOCK_STREAM,0);
if(socket_fd == -1)
{
std::cout<<"FAILED to CREATE SOCKET!"<<std::endl;
exit(-1);
}
ret = setsockopt(socket_fd,
SOL_SOCKET,
SO_REUSEADDR,
&flag,
(socklen_t)(sizeof(flag)));
if(ret == -1)
{
std::cout<<"FAILED to set socket option!"<<std::endl;
exit(-1);
}
//bind
localaddr.sin_family = AF_INET;
localaddr.sin_port = PORT;
localaddr.sin_addr.s_addr = INADDR_ANY;
int socketlen = sizeof(localaddr);
// bzero() 会将内存块(字符串)的前n个字节清零;
// s为内存(字符串)指针,n 为需要清零的字节数。
// 在网络编程中会经常用到。
// void bzero(void *s, int n);
bzero(&(localaddr.sin_zero),8);
//int ret_bind = bind(socket_fd,(struct sockaddr *)&localaddr,(socklen_t)(sizeof(struct sockaddr_in)));
int bind_ret = bind(socket_fd,
(struct sockaddr *)&localaddr,
sizeof(struct sockaddr));
if(bind_ret < 0)
{
std::cout<<"FAILED to bind addr!"<<std::endl;
exit(-1);
}
int ret_listen = listen(socket_fd,backlog);
if(ret_listen == -1)
{
std::cout<<"FAILED to listen!"<<std::endl;
exit(-1);
}
pid_t pid = 0;
for(;;){
socklen_t sockaddr_len = sizeof(struct sockaddr_in);
int accept_fd = accept(socket_fd,
(struct sockaddr *) &remoteaddr,
&sockaddr_len);
pid = fork();
if(pid == 0){
//子进程收发数据
for(;;)
{
//memset(in_buffer,0,MESSAGE_LEN);
int recv_ret = recv(accept_fd,(void *)in_buffer,MESSAGE_LEN,0);
if(recv_ret == 0)
{
break;
}
std::cout<<"recv : "<<in_buffer<<std::endl;
send(accept_fd,(void*)in_buffer,MESSAGE_LEN,0);
}
std::cout<<"Close Client Connection"<<std::endl;
close(accept_fd);
}
}
if(pid!=0)close(socket_fd);
return 0;
}
缺点:
- 以fork方式创建进程会一直占用系统资源。
- 在高并发情况下下创建子进程的时间开销大。
三、通过select实现高性能网络服务器
select实现高性能网络服务器其实是基于异步IO的思想。
异步IO,是指以事件触发的机制来对IO操作进行处理。
与多进程和多线程技术相比,异步IO技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些线程和进程,从而大大减小了系统的开销。
select步骤:
- 遍历文件描述符集中的所有描述符,找出有变化的描述符。
- 对于监听的socket和进行数据处理的socket要区别对待。
- socket必须要设置为非阻塞方式工作。
代码如下:
#include<iostream>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<unistd.h>
#include<fcntl.h>
#define max(a,b) ((a)>(b)?(a):(b))
#define PORT 8111
#define MESSAGE_LEN 1024
#define FD_SIZE 1024 //select默认只允许1024个连接
int main(int argc,char* argv[])
{
int socket_fd = -1,accept_fd = -1;
int on = 1;
int curpos = -1;
int backlog = 1000;
int flag = 1;
int max_fd = -1;
int event = 0;
int ret = -1;
fd_set fd_sets;
int accept_fds[FD_SIZE] = {-1};
char in_buffer[MESSAGE_LEN] = {0,};
struct sockaddr_in localaddr,remoteaddr;
//create
socket_fd = socket(AF_INET, SOCK_STREAM,0);
if(socket_fd == -1)
{
std::cout<<"FAILED to CREATE SOCKET!"<<std::endl;
exit(-1);
}
flag = fcntl(socket_fd,F_GETFL,0);
//将fd设置为非阻塞
fcntl(socket_fd,F_SETFL,flag | O_NONBLOCK);
max_fd = socket_fd;
ret = setsockopt(socket_fd,
SOL_SOCKET,
SO_REUSEADDR,
&flag,
(socklen_t)(sizeof(flag)));
if(ret == -1)
{
std::cout<<"FAILED to set socket option!"<<std::endl;
exit(-1);
}
//bind
localaddr.sin_family = AF_INET;
localaddr.sin_port = PORT;
localaddr.sin_addr.s_addr = INADDR_ANY;
int socketlen = sizeof(localaddr);
// bzero() 会将内存块(字符串)的前n个字节清零;
// s为内存(字符串)指针,n 为需要清零的字节数。
// 在网络编程中会经常用到。
// void bzero(void *s, int n);
bzero(&(localaddr.sin_zero),8);
//int ret_bind = bind(socket_fd,(struct sockaddr *)&localaddr,(socklen_t)(sizeof(struct sockaddr_in)));
int bind_ret = bind(socket_fd,
(struct sockaddr *)&localaddr,
sizeof(struct sockaddr));
if(bind_ret < 0)
{
std::cout<<"FAILED to bind addr!"<<std::endl;
exit(-1);
}
int ret_listen = listen(socket_fd,backlog);
if(ret_listen == -1)
{
std::cout<<"FAILED to listen!"<<std::endl;
exit(-1);
}
pid_t pid = 0;
for(;;){
FD_ZERO(&fd_sets);
FD_SET(socket_fd,&fd_sets);
for(int i = 0;i < FD_SIZE;i++)
{
//是一个有效的socket,才加入到fd_set中
if(accept_fds[i] != -1){
max_fd = max(max_fd,accept_fds[i]);
FD_SET(accept_fds[i],&fd_sets);
}
}
event = select(max_fd + 1,&fd_sets,NULL,NULL,NULL);
if(event < 0){
std::cout<<"Failed to use select!"<<std::endl;
break;
}else if(event == 0)
{
std::cout<<"Timeout!"<<std::endl;
//由于没有设置超时时间,因此不可能等于零
continue;
}else{
//该socket是否是监听的socket
if(FD_ISSET(socket_fd,&fd_sets))
{
for(int i = 0;i < FD_SIZE;i++)
{
if(accept_fds[i] == -1) {
curpos = i;
break;
}
}
socklen_t sockaddr_len = sizeof(struct sockaddr_in);
accept_fd = accept(socket_fd,(struct sockaddr *) &remoteaddr, &sockaddr_len);
flag = fcntl(accept_fd,F_GETFL,0);
fcntl(accept_fd,F_SETFL,flag | O_NONBLOCK);
//插入当前创建的accept_fd;
accept_fds[curpos] = accept_fd;
}
//如果当前的socket不是用于监听的socket
for(int i = 0;i<FD_SIZE;i++)
{
if(accept_fds[i] != -1 && FD_ISSET(accept_fds[i],&fd_sets))
{
memset(in_buffer,0,MESSAGE_LEN);
int recv_ret = recv(accept_fds[i],(void *)in_buffer,MESSAGE_LEN,0);
if(recv_ret == 0)
{
close(accept_fds[i]);
break;
}
std::cout<<"recv : "<<in_buffer<<std::endl;
send(accept_fds[i],(void*)in_buffer,MESSAGE_LEN,0);
}
}
}
}
close(socket_fd);
return 0;
}
四、通过epoll实现高性能网络服务器
关于epoll的优点请参考我的另一篇博客。
#include<iostream>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<fcntl.h>
#define PORT 8111
#define MESSAGE_LEN 1024
#define TIMEOUT 500 // 500 ms
#define MAX_EVENTS 20 //同一时刻的并发数
int main(int argc,char* argv[])
{
int socket_fd = -1,accept_fd = -1;
int on = 1;
int curpos = 0;
int backlog = 1000;
int flag = 1;
int ret = -1;
int epoll_fd = -1;
int event_number = -1;
int recv_ret = -1;
struct epoll_event ev,events[MAX_EVENTS];
char in_buffer[MESSAGE_LEN] = {0,};
struct sockaddr_in localaddr,remoteaddr;
//create
socket_fd = socket(AF_INET, SOCK_STREAM,0);
if(socket_fd == -1)
{
std::cout<<"FAILED to CREATE SOCKET!"<<std::endl;
exit(-1);
}
//需要将socket_fd设置为非阻塞
flag=fcntl(socket_fd,F_GETFL,0);
fcntl(socket_fd,F_SETFL,flag | O_NONBLOCK);
ret = setsockopt(socket_fd,
SOL_SOCKET,
SO_REUSEADDR,
&flag,
(socklen_t)(sizeof(flag)));
if(ret == -1)
{
std::cout<<"FAILED to set socket option!"<<std::endl;
exit(-1);
}
//bind
localaddr.sin_family = AF_INET;
localaddr.sin_port = PORT;
localaddr.sin_addr.s_addr = INADDR_ANY;
int socketlen = sizeof(localaddr);
// bzero() 会将内存块(字符串)的前n个字节清零;
// s为内存(字符串)指针,n 为需要清零的字节数。
// 在网络编程中会经常用到。
// void bzero(void *s, int n);
bzero(&(localaddr.sin_zero),8);
//int ret_bind = bind(socket_fd,(struct sockaddr *)&localaddr,(socklen_t)(sizeof(struct sockaddr_in)));
int bind_ret = bind(socket_fd,
(struct sockaddr *)&localaddr,
sizeof(struct sockaddr));
if(bind_ret < 0)
{
std::cout<<"FAILED to bind addr!"<<std::endl;
exit(-1);
}
int ret_listen = listen(socket_fd,backlog);
if(ret_listen == -1)
{
std::cout<<"FAILED to listen!"<<std::endl;
exit(-1);
}
epoll_fd = epoll_create(256);
ev.events = EPOLLIN; // 为了保证客户端的连接一定能被响应,我们使用水平触发
ev.data.fd = socket_fd;
//负责监听的socket
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,
&ev);
for(;;){
event_number = epoll_wait(epoll_fd,events,MAX_EVENTS,TIMEOUT);
for(int i=0;i<event_number;i++){
//监听的socket,创建新链接
if(events[i].data.fd==socket_fd){
socklen_t sockaddr_len = sizeof(struct sockaddr_in);
accept_fd = accept(socket_fd,
(struct sockaddr *) &remoteaddr,
&sockaddr_len);
flag=fcntl(accept_fd,F_GETFL,0);
fcntl(accept_fd,F_SETFL,flag | O_NONBLOCK);
ev.events = EPOLLIN | EPOLLET; //边缘触发
ev.data.fd = accept_fd;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);
}else if(events[i].events & EPOLLIN)
{
//出现了输入的事件
do{
memset(in_buffer,0,MESSAGE_LEN);
recv_ret = recv(events[i].data.fd,(void *)in_buffer,MESSAGE_LEN,0);//recv_ret表示收到消息的长度
if(recv_ret == 0)
{
std::cout<<"Quit Socket"<<std::endl;
close(events[i].data.fd);
}
if(recv_ret==MESSAGE_LEN)//进行阶段
{
std::cout<<"maybe need to truncate data..."<<std::endl;
}
}while(ret < 0 && errno == EINTR);
if(recv_ret < 0)
{
switch(errno){
case EAGAIN: break;
default: break;
}
}
if(recv_ret > 0){
std::cout<<"recv : "<<in_buffer<<std::endl;
send(events[i].data.fd,(void*)in_buffer,MESSAGE_LEN,0);
}
}
}
}
close(socket_fd);
return 0;
}
五、epoll + fork实现高性能网络服务器
一般在服务器上,CPU是多核的,上述epoll实现方式只使用了其中的一个核,造成了资源的大量浪费。因此我们可以将epoll和fork结合来实现更高性能的网络服务器。
但是在这种情况下,异步IO会出现惊群现象。我们可以通过在一个线程进行监听,加锁等方法解决。
六、通过异步事件的开源库来实现高性能网络服务器
一些著名的异步事件的开源库有
- libevent(Linux平台下基于epoll的开源库,用堆来管理超时事件)
- libevthp(基于libevent的http应用开发)
- libuv(Node.js正是基于此,在libev基础上进行调整,支持linux以外的几个平台)
- libev(改善了libevent的一些不合理之处,更加高效)
libevent重要的函数:
- event_base_new ( ~ epoll_create)
- event_base_dispatch ( ~ epoll_wait)
- event_new, event_add,event_del,event_free字面含义,这四个组成了epoll_ctl这个API
- evconnlistener_new_bind(把socket和事件的触发绑定到了一起)