前期准备
首先对linux提供的socket进行了一些简单的封装,使之更加符合面向对象的习惯:
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#define SER_PORT (6789)
class Socket{
public:
sockaddr_in addr;//地址
int fd;//文件描述符
//返回字符串 :"ip:端口号"
std::string getAddr(){
char* ip = inet_ntoa(this->addr.sin_addr);
std::string port = ":" + std::to_string(ntohs(this->addr.sin_port));
return ip + port;
}
~ Socket(){
close(this->fd);
}
};
class ListenSocket:public Socket{//用于监听客户端连接的Socket
public:
//创建、绑定、监听
ListenSocket(uint32_t ip, int port, int listenNumber) : Socket(){
//初始化文件描述符
this->fd = socket(AF_INET, SOCK_STREAM, 0);
//初始化地址
this->addr.sin_family = AF_INET;
this->addr.sin_addr.s_addr = htonl(ip);
this->addr.sin_port = htons(port);
//绑定
bind(this->fd,(sockaddr*)&this->addr, sizeof(this->addr));
//开始监听
listen(this->fd,listenNumber);
std::cout<<"正在监听"<<std::endl;
//设置端口复用
int opt = 1;
setsockopt(this->fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
};
class ConnectSocket:public Socket{//用于与每个客户端通信的socket
public:
//构造函数:需要指定由哪个监听套接字 accept,构造时会进入停滞状态等待客户端连接
//关于explicit的知识:https://blog.csdn.net/K346K346/article/details/82779248
explicit ConnectSocket(ListenSocket* &pServer) : Socket(){
socklen_t addr_len = sizeof(this->addr);
this->fd = accept(pServer->fd, (sockaddr *)&this->addr, &addr_len);
}
//接收函数,返回客户端发来的消息, 没有消息时会停滞, 客户端退出时返回空字符串
std::string receive(){
while(true){
char recvBuf[1024];
int r = read(this->fd, recvBuf, sizeof(recvBuf));
if(r == 0){
return "";
}
return recvBuf;
}
}
//发送函数
void send(const std::string &message){
write(this->fd, message.c_str(), message.length());
}
};
能够使用socket进行通信后 ,可以采用以下五种方式处理客户端的并发访问:
- 多进程
- 多线程
- 使用select
- 使用poll
- 使用epoll
一、多进程并发服务器
思路:主进程创建listen套接字,每当监听到一个客户端连接时, 创建一个子进程来处理与它的通信。
实现:
#include <iostream>
#include "mysocket.h"
int main() {
auto * pServer = new ListenSocket(INADDR_ANY, SER_PORT, 128);
while(1){
auto * pClient = new ConnectSocket(pServer);//接受客户端连接
std::cout<<pClient->getAddr()<<std::endl;//输出其地址
pid_t pid = fork();//创建子进程
if(pid == 0){//子进程
close(pServer->fd); //关闭监听socket
while(1){//处理客户端发来的消息
std::string recvMessage = pClient->receive();
if(recvMessage == ""){
std::cout << "连接中断" << std::endl;
break;
}
std::cout << recvMessage << std::endl;
//向客户端发送一个简单的http回复报文
std::string sendMessage = "HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>";
pClient->send(sendMessage);
}
std::cout << "子进程返回" << std::endl;
close(pClient->fd);
return 0;
}
else if(pid > 0){//父进程
continue;
}
else{
std::cout << "创建进程出错" << std::endl;
}
}
}
多进程并发服务器的缺点:多个进程的创建和切换会耗费大量的系统资源。
二、多线程并发服务器
思路:和多进程服务器的思路基本一致, 只是改为采用线程来处理每个connectSocket, 减少了创建和切换的代价。
实现:
#include <iostream>
#include <pthread.h>
#include "mysocket.h"
void* task(void *arg){
auto * pClient = (ConnectSocket *) arg;
std::cout<<"客户端"<<pClient->getAddr()<<std::endl;
//将子线程分离,这样一来,该线程运行结束后会自动释放所有资源。 防止产生僵尸进程
pthread_detach(pthread_self());
while (1){
std::string recvMessage = pClient->receive();
if(recvMessage.empty()){
std::cout << "连接中断" << std::endl;
break;
}
std::cout << recvMessage << std::endl;
std::string sendMessage = "HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>";
pClient->send(sendMessage);
}
close(pClient->fd);
delete(pClient);
return nullptr;
}
int main(){
auto* pServer = new ListenSocket(INADDR_ANY, SER_PORT, 128);
while(1){
auto * pClient = new ConnectSocket(pServer);
pthread_t tid;
pthread_create(&tid, NULL, task, (void*) pClient);
}
}
多进程并发服务器的缺点:虽然强于多进程, 但是在面对成千上万的访问时, 仍然力有不逮。
三、select 实现多路IO转接服务器
思路:将所有socket放入一个监听集合中,然后程序进入阻塞等待状态。 当监听到有socket发生io事件时, 操作系统标记这些socket并将程序唤醒。 然后程序轮询监听集合,找到有标记的socket并处理他们的io事件。
实现:
#include <iostream>
#include <sys/select.h>
#include <set>
#include "mysocket.h"
using namespace std;
int main(){
auto* pServer = new ListenSocket(INADDR_ANY, SER_PORT, 1024);
fd_set listeningSet,allSet; //声明监听集合以及全部的文件描述符集合
FD_ZERO(&allSet);//清空文件描述符集合
FD_SET(pServer->fd, &allSet);//将Listen套接字加入文件描述符集合
int maxFd = pServer->fd;//监听的最新对象是Listen套接字
set<ConnectSocket*> pClients;
while(true){//逐帧监听
listeningSet = allSet;//更新监听集合
int todoNums = select(maxFd+1, &listeningSet, nullptr, nullptr, nullptr);
if (todoNums < 0){
cout << "select 错误" << endl;
return -1;
}
if(FD_ISSET(pServer->fd, &listeningSet)){
//如果listen套接字监听到事件, 说明有新的客户端发生了连接
auto * pClient = new ConnectSocket(pServer);
pClients.insert(pClient);
cout << "客户端" << pClient->getAddr() << endl;
//将新的客户端加入文件描述符集合
maxFd = maxFd > pClient->fd ? maxFd : pClient->fd;
FD_SET(pClient->fd, &allSet);
--todoNums;//待处理事件减一
if(!todoNums) continue;//待处理事件为0, 只发生了客户端连接事件, 没有io事件
}
//发生了io事件
for(auto & pClient : pClients){//轮询连接套接字
if(FD_ISSET(pClient->fd, &listeningSet)){//如果监听到事件
string recvMessage = pClient->receive();
if(recvMessage.empty()){//客户端退出
cout<<pClient->getAddr()<<"退出"<<endl;
close(pClient->fd);
delete(pClient);
pClients.erase(pClient);
FD_CLR(pClient->fd, &allSet);
continue;
}
cout << recvMessage << endl;
pClient->send("HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>");
--todoNums;//待处理事件减一
if(!todoNums) break;
}
}
}
}
select的缺点:监听上限受制于操作系统,只能达到1024个, 还是无法面对更高的并发访问
四、poll 实现多路IO转接服务器
思路:与select基本一致, 不同的是可以监听超过1024个套接字
由于监听集合中保存的是套接字的fd而不是我们自己定义的Socket类, 所以在接受和发送消息时最好直接用fd进行定位:
std::string Receive(int fd){
char recvBuf[1024];
int r = read(fd, recvBuf, sizeof(recvBuf));
if(r == 0){
return "";
}
return recvBuf;
}
void Send(std::string message, int fd){
write(fd, message.c_str(), message.length());
}
实现:
//
// Created by haozl on 2019/11/2.
//
#include <iostream>
#include <poll.h>
#include "mysocket.h"
#define MAX_CLIENT_NUM 128
using namespace std;
class pollMonitor{
public:
pollfd Set[MAX_CLIENT_NUM]{};
int maxIndex;
//构造函数, 初始化监听集合和它的最大有效下标
pollMonitor(){
maxIndex = 0;
for(int i=0; i<MAX_CLIENT_NUM; i++){
Set[i].fd = -1;
Set[i].events = 0;
Set[i].revents = 0;
}
}
//将某个套接字加入监听集合
bool add(int fd){
for (int i=0; i<MAX_CLIENT_NUM; i++){
if(Set[i].fd < 0){
Set[i].fd = fd;
Set[i].events = POLLIN;
maxIndex = i > maxIndex ? i : maxIndex;
return true;
}
}
return false;
}
//将某个套接字从监听集合中删除
void drop(int index){
close(this->Set[index].fd);
this->Set[index].fd = -1;
this->Set[index].events = 0;
this->Set[index].revents = 0;
}
//阻塞监听, 返回监听到的事件个数
int wait(int timeout){
return poll(this->Set, this->maxIndex+1, timeout);
}
};
int main(){
//实例化listen套接字和监视器, 将listen套接字加入监听
auto * pServer = new ListenSocket(INADDR_ANY, SER_PORT, MAX_CLIENT_NUM);
auto * monitor = new pollMonitor();
monitor->add(pServer->fd);
while(true){//逐帧监听
int todoNum = monitor->wait(-1);
if(todoNum < 0){
cout<<"poll 错误"<<endl;
return -1;
}
if(todoNum > 0){//如果监听到事件发生
if(monitor->Set[0].revents == POLLIN){
//如果是从listen套接字监听到事件的, 说明有客户端连接
auto * pClient = new ConnectSocket(pServer);
cout <<pClient->getAddr()<<endl;
monitor->add(pClient->fd);//将新的connect套接字加入监听
--todoNum;//未完成事件减一
if (!todoNum) continue;//如果事件全部处理完成, 处理下一帧
}
//遍历监听集合中剩下的元素
for(int i=1; i < monitor->maxIndex+1; i++){
if(monitor->Set[i].fd < 0) continue;//如果不是有效的套接字 跳过
if(monitor->Set[i].revents == POLLIN){
//有读入事件发生
string recvMessage = Receive(monitor->Set[i].fd);
if(recvMessage.empty()){
monitor->drop(i);//将该connect套接字移出监听
cout << "连接中断" << endl;
continue;
}
cout<<recvMessage<<endl;
Send("HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>", monitor->Set[i].fd);
--todoNum;//未完成事件减一
if (!todoNum) break;//如果事件全部处理完成, 跳出处理下一帧
}
}
}
}
}
poll和select的缺点:监听到事件后,无法直接定位到发生事件的socket,需要程序自己对监听集合进行轮询,在并发量过多的情况下仍然效率低下
五、epoll 实现多路IO转接服务器
思路:监听到事件后,直接定位到发生事件的socket,无需轮询
实现:
//
// Created by haozl on 2019/11/3.
//
#include <iostream>
#include <sys/epoll.h>
#include "mysocket.h"
#define MAX_CLIENT_NUM 128
using namespace std;
class ePollMonitor{
public:
int epfd;
epoll_event Set[MAX_CLIENT_NUM]{};
//构造函数, 创建epoll句柄, 并初始化监听集合
epollMonitor(){
this->epfd = epoll_create(MAX_CLIENT_NUM);
for(auto & item : this->Set){
item.data.fd = -1;
}
}
//将某个套接字加入监听集合
void add(int fd){
epoll_event tmp{};
tmp.events = EPOLLIN;
tmp.data.fd = fd;
epoll_ctl(this->epfd, EPOLL_CTL_ADD, fd, &tmp);
}
//将某个套接字从监听集合中删除
void drop(int index){
close(this->Set[index].data.fd);
epoll_ctl(this->epfd, EPOLL_CTL_DEL, this->Set[index].data.fd, nullptr);
}
//阻塞监听, 返回监听到的事件个数
int wait(int timeout){
return epoll_wait(this->epfd, this->Set, MAX_CLIENT_NUM, timeout);
}
};
int main(){
//实例化listen套接字和监视器, 将listen套接字加入监听
auto * pServer = new ListenSocket(INADDR_ANY, SER_PORT, MAX_CLIENT_NUM);
auto * monitor = new epollMonitor();
monitor->add(pServer->fd);
while(true){//逐帧监听
int todoNum = monitor->wait(-1);
if(todoNum < 0){
cout<<"epoll 错误"<<endl;
return -1;
}
if(todoNum > 0){//如果监听到事件发生
for(int i=0; i<MAX_CLIENT_NUM; i++){//遍历发生事件的套接字
if(monitor->Set[i].events != EPOLLIN)//如果不是读入事件 跳过
continue;
if(monitor->Set[i].data.fd == pServer->fd){
//如果从listen套接字监听到事件, 说明有客户端连接
auto * pClient = new ConnectSocket(pServer);
cout <<pClient->getAddr()<<endl;
monitor->add(pClient->fd);//将新的connect套接字加入监听
todoNum--;//未完成事件减一
if(!todoNum) break;//如果事件全部处理完成, 跳出处理下一帧
}
else{//如果从其他套接字监听到事件, 说明有客户端发来请求
string recvMessage = Receive(monitor->Set[i].data.fd);
if(recvMessage.empty()){
monitor->drop(i);//将该connect套接字移出监听
cout << "连接中断" << endl;
continue;
}
cout<<recvMessage<<endl;
Send("HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>", monitor->Set[i].data.fd);
todoNum--;//未完成事件减一
if(!todoNum) break;//如果事件全部处理完成, 跳出处理下一帧
}
}
}
}
}
参考文章:
https://mp.weixin.qq.com/s/WO2GuaUCtvUFWIupgpWcbg
https://blog.csdn.net/weixin_40204595/article/details/83211064
https://blog.csdn.net/weixin_40204595/article/details/83212900