文章目录
基本功能
处理HTML页面的HTTP GET请求:
- 如果HTTP请求的路径对应于html页面文件,则以200 OK和文件的全部内容响应;
- 如果HTTP请求的路径对应于一个目录,并且该目录包含一个index.html文件,在该文件夹中以200 OK和index.html文件的全部内容响应;
- 如果请求的页面文件不存在,或者请求的目录不包含index.html文件,返回404未找到响应。
处理HTML页面的HTTP POST请求:
1.构造一个HTTP POST请求包含两个键:“NAME”和“ID”(分别填写姓名和学生编号),并将POST请求发给/Post_show。如果HTTP服务器收到此POST请求则以200OK响应,并回显发送的“Name”-“ID”对。POST查找的目录只能是**/Post_show**
2.其他情况返回404 Not Found。
其他请求:返回 501 Not Implemented error
实现思路
先上github链接:http服务器
线程池
通过参数接收线程的个数,提前创建好相应的线程个数。当没有HTTP请求时,线程休眠;新的请求到达时,加入到工作队列中,并唤醒休眠线程
class Threadpool{
private:
pthread_mutex_t lock; //互斥锁
pthread_cond_t queue_cond; //唤醒消息队列等待线程
queue<HttpSer> work_queue; //要处理的套接字工作队列
int thread_num; //线程数
vector<pthread_t> thread; //线程池中的线程
private:
map<int,string> buf;//连接套接字以及相应保存数据的缓冲区
map<int,int> vis;
public:
//vector<int> delsock;//要从epoll中移除的套接字
Threadpool(int num):thread_num(num){
pthread_mutex_init(&lock,NULL);
queue_cond = PTHREAD_COND_INITIALIZER;
thread=vector<pthread_t>(thread_num);
}
~Threadpool(){
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&queue_cond);
}
void start();//创建线程
//借助生产者消费者问题思想,queue_append是生产者,run是消费者
void queue_append(int fd);//向工作队列中加入http请求
void queue_consumer();//消费工作队列中的http请求
static void* work(void *arg);//线程创建后的执行函数
void init_fd(int clntfd); //初始化buf和vis
};
借助生产者/消费者问题处理http请求。
queue_append相当于生产者,将请求加入到工作队列后,唤醒休眠线程处理。
queue_consumer相当于消费者,不断检查工作队列,若为空,进入休眠,否则,取出一个http请求,调用processHttp进行处理。
void Threadpool::queue_consumer(){
/***********借助生产者消费者问题思想*************/
while(1){
pthread_mutex_lock(&lock);
//使用while循环代替if
while(work_queue.empty()){
pthread_cond_wait(&queue_cond,&lock);
}
HttpSer http=work_queue.front();
work_queue.pop();
//无需唤醒生产者,生产者不休眠
pthread_mutex_unlock(&lock);
//调用HttpSer函数处理http请求
//cout<<pthread_self()<<endl;
string rbuf=http.processHttp();
pthread_mutex_lock(&lock);
if(http.get_is_close()){
vis.erase(http.get_fd());
buf.erase(http.get_fd());
}
else{
vis[http.get_fd()]=0;
buf[http.get_fd()]=rbuf;
}
pthread_mutex_unlock(&lock);
}
}
void Threadpool::queue_append(int fd){
pthread_mutex_lock(&lock);
if(vis[fd]==0){
HttpSer t(fd,buf[fd]);
vis[fd]=1;
work_queue.push(t);
pthread_cond_signal(&queue_cond);//唤醒休眠线程
}
pthread_mutex_unlock(&lock);
}
pthread有两种状态joinable状态和unjoinable状态。
①如果线程是joinable状态:当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符。只有当你调用了pthread_join之后这些资源才会被释放。
②若是unjoinable状态的线程:这些资源在线程函数退出时或pthread_exit时自动会被释放。
调用pthread_join会阻塞调用线程,而在Web服务器中当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞。
此时调用pthread_detach设置为脱离线程,即设置为unjoinable状态。
void Threadpool::start(){
//所有子线程创建完成后,子线程才开始运行
pthread_mutex_lock(&lock);
for(int i=0;i<thread_num;i++){
//当某个线程创建失败时,一直重复创建线程,直到成功
while(pthread_create(&thread[i],NULL,work,this)!=0);
//将线程设置为脱离线程,失败则清除成功申请的资源并抛出异常
if(pthread_detach(thread[i])){
throw std::exception();
}
}
pthread_mutex_unlock(&lock);
}
HTTP服务器中存在的问题
HTTP1.0和HTTP1.1
HTTP1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。
HTTP1.1支持长连接,在请求头里面有Connection:Keep-Alive。在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟.
因此,HTTP1.1会引入粘包和拆包的问题.
粘包和拆包
现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种:
①接收端正常收到两个数据包,即没有发生拆包和粘包的现象。
②接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。
③接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。
粘包拆包解决方法
简单来说,每次收到HTTP请求时,只有存在完整的HTTP请求报文时,才进行处理,同时,每次接收完成后,将所有请求处理完。
HTTP请求报文中的首部行以CRLFCRLF
标志结束,所以我们可以识别报文中是否存在“\r\n\r\n”
,来得到首部行结束位置
此时,我们得到了报文中完整的请求行和首部行,若该部分包含”Content-length”
,则存在请求数据。所以根据”Content-length”
得到完整的请求数据,这样,就能分离出完整HTTP请求报文。
将该部分HTTP请求报文保存下来,并从接收数据中删除。
剩下的数据中若不存在完整HTTP请求报文,则保留下来并拼接到下一部分的接收数据前面。
HTTP具体实现
class HttpSer{
private:
int socketfd; //套接字描述符
string strbuf;//保存已经接收但是未处理的数据
bool is_close;//是否关闭套接字
public:
HttpSer(){}
HttpSer(int id,string buf1):socketfd(id),strbuf(buf1){is_close=false;}
~HttpSer(){}
string processHttp();
void Not_Implemented(string method);//非post,get方法
void Not_Found(string url,string method);//文件未找到
void response_get(string url,string method);//处理get方法
void response_post(string name,string id);//处理post方法
bool get_is_close(){return is_close;}
int get_fd(){return socketfd;}
};
Socketfd为套接字描述符,在Linux中,套接字描述符和文件描述符相同,同样可以用write和read写入和读取。
processHttp是HTTP服务器的主要处理函数
processHttp实现流程图
代码太长,就不摆上来了,去我的Github看
判断客户端关闭套接字
当客户端关闭套接字时,会向服务端发送EOF,此时调用recv返回值为0,同时检查错误号errno,若错误号不是EINTR,则表示客户端关闭了套接字,结束通信,此时可以关闭服务端套接字
及时关闭套接字,避免套接字占用操作系统资源,长时间维护一个已经不再使用的套接字是不必要的
解决重启服务器时可能出现的bind error问题
向服务器端控制台输入CTRL+C,强制关闭服务器端时,服务器重新运行时,如果重用同一端口号,将输入bind error消息,无法再次运行。
原因:服务器先发起断开连接请求,进入Time-wait状态,相应端口仍然是正在使用状态
解决方法:更改套接字SO_REUSEADDR状态,允许将Time-wait状态下的套接字端口分配给新的套接字
int option=1;
socklen_t optlen=sizeof(option);
setsockopt(sersocket,SOL_SOCKET,SO_REUSEADDR,(void*)&option,optlen);
进一步提高并发
加入epoll机制,使用主进程监听所有的套接字,进一步提高并发量
使用epoll时,当有HTTP请求进来时才分配线程池中的一个线程进行处理,处理完成就将线程资源释放。相对于只使用线程池,这个对并发提高的帮助是巨大的。
在只使用线程池时,一个套接字在关闭前会一直占用着线程,即使此时该套接字是空闲的,没有请求要处理。但是通过epoll,在套接字空闲时,让出线程给需要处理的套接字,这样并发量就上来了。
epoll函数实现细节
主要掌握三个函数:epoll_wait,epoll_create,epoll_ctl
当进程调用epoll监控多个socket时,会在底层创建一个eventpoll对象,这个对象中包含一个重要的队列:就绪队列
①进程调用epoll函数后,epoll会把这个进程加入eventpoll对象的等待队列中
②然后把eventpoll对象加入到所有socket的等待队列中,并让CPU阻塞住
③当某一个socket有数据返回时,CPU中断程序会把这个socket加入到eventpoll对象的就绪队列中,并把eventpoll中等待的进程唤醒。
④进程被唤醒后直接从就绪队列中获取socket读取数据
⑤数据读取完成后,epoll又会把进程加入到eventpoll的等待队列中,然后让CPU阻塞住。
代码
这里使用边缘处理,将套接字设置成为非阻塞,每次套接字有请求进来,使用while循环将所有数据都处理完,当缓冲区没有数据时,调用recv函数返回-1同时错误号errno为EAGAIN
因为使用边缘触发,在线程池中维护一个数组指示某套接字是否在工作队列中或者正在处理,只有不在工作队列中或者没有被处理才将该套接字加入到工作队列中
int WebServer(const char* ip,int port,int thread){
sersocket=socket(PF_INET,SOCK_STREAM,0);
signal(SIGINT,closesocket);
if(sersocket < 0){
printf("socket error\n");
exit(1);
}
const int size=10*thread;
int option=1;
socklen_t optlen=sizeof(option);
setsockopt(sersocket,SOL_SOCKET,SO_REUSEADDR,(void*)&option,optlen);
struct sockaddr_in servaddr,clnt_addr;
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(port);
servaddr.sin_addr.s_addr=inet_addr(ip);
if(bind(sersocket,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1){
printf("bind() error\n");
exit(1);
}
if(listen(sersocket,size)==-1){
printf("listen() error\n");
exit(1);
}
setnonblockingmode(sersocket);
Threadpool pool(thread);
pool.start();
epfd=epoll_create(size);//将大小设置为和listen第二个参数一样
struct epoll_event *ep_events;
struct epoll_event event;
ep_events=new epoll_event[size];
event.events=EPOLLIN;
event.data.fd=sersocket;
epoll_ctl(epfd,EPOLL_CTL_ADD,sersocket,&event);
while(1){
int event_cnt=epoll_wait(epfd,ep_events,size,-1);
if(event_cnt==-1){
printf("epoll_wait() error\n");
exit(1);
}
for(int i=0;i<event_cnt;i++){
if(ep_events[i].data.fd==sersocket){
socklen_t adr_sz=sizeof(clnt_addr);
int clntsocket=accept(sersocket,(struct sockaddr*)&clnt_addr,&adr_sz);
// time_t t = time(0);
// char tmp[64];
// strftime(tmp, sizeof(tmp), "%Y/%m/%d %X", localtime(&t));
// cout << tmp <<" : "<<clntsocket<< endl;
if(clntsocket!=-1){
setnonblockingmode(clntsocket);
event.events=EPOLLIN|EPOLLET;//边缘触发
event.data.fd=clntsocket;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,clntsocket,&event)==-1){
// strftime(tmp, sizeof(tmp), "%Y/%m/%d %X", localtime(&t));
// cout << tmp <<" : "<<"error"<< endl;
}
pool.init_fd(clntsocket);
}
}
else{
pool.queue_append(ep_events[i].data.fd);
}
}
}
// close(sersocket);
}
测试
运行方法:
./HttpServer --ip ip地址 --port 端口号 --number_thread 线程数目
测试工具:Apache bench
只使用线程池
并发数为100,1000个HTTP请求
线程池+EPOLL
并发数1000,1千万个HTTP请求
通过对比,可以发现使用epoll实现I/O多路复用后,测试速度要快很多
Linux下最多只能打开1024个文件描述符,所以这里并发数最多只能1000,再多就测不了