一、具体功能实现
- GET方法请求解析
- POST方法请求解析
- 返回请求资源页面
- 利用GET方法实现加减法
- 利用POST方法实现加减法
- HTTP请求行具体解析
- 400、403、404错误码返回的处理
二、什么是web服务器
- web服务器就是在物理服务器基础上的具有服务端功能的网络连接程序,简而言之就是处理客户端发来的各种请求然后根据服务器的逻辑处理返回一个结果给客户端。在web服务器和客户端之间的通信是基于HTTP协议进行的。而客户端可以是浏览器也可以是支持HTTP协议的APP。
- 那么浏览器应该怎么连接上自己的web服务器呢,最简单的web服务器就是通过TCP三次握手建立连接后,服务器直接返回一个结果给浏览器。浏览器和服务器是通过TCP三路握手建立连接的。浏览器在通过URL(统一资源定位符,就是我们俗称的网络地址)去请求服务器的连接,并且通过URL中的路径请求服务器上的资源。举个栗子就是这样的:
最简单的web服务器:
-
#include<stdio.h>
-
#include<stdlib.h>
-
#include<sys/socket.h>
-
#include<sys/types.h>
-
#include<sys/stat.h>
-
#include<sys/sendfile.h>
-
#include<fcntl.h>
-
#include<netinet/in.h>
-
#include<arpa/inet.h>
-
#include<assert.h>
-
#include<unistd.h>
-
#include<string.h>
-
const int port = 8888;
-
int main(int argc,char *argv[])
-
{
-
if(argc<0)
-
{
-
printf("need two canshu\n");
-
return 1;
-
}
-
int sock;
-
int connfd;
-
struct sockaddr_in sever_address;
-
bzero(&sever_address,sizeof(sever_address));
-
sever_address.sin_family = PF_INET;
-
sever_address.sin_addr.s_addr = htons(INADDR_ANY);
-
sever_address.sin_port = htons(8888);
-
-
sock = socket(AF_INET,SOCK_STREAM,0);
-
-
assert(sock>=0);
-
-
int ret = bind(sock, (struct sockaddr*)&sever_address,sizeof(sever_address));
-
assert(ret != -1);
-
-
ret = listen(sock,1);
-
assert(ret != -1);
-
while(1)
-
{
-
struct sockaddr_in client;
-
socklen_t client_addrlength = sizeof(client);
-
connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
-
if(connfd<0)
-
{
-
printf("errno\n");
-
}
-
else{
-
char request[1024];
-
recv(connfd,request,1024,0);
-
request[strlen(request)+1]=\'\0\';
-
printf("%s\n",request);
-
printf("successeful!\n");
-
char buf[520]="HTTP/1.1 200 ok\r\nconnection: close\r\n\r\n";//HTTP响应
-
int s = send(connfd,buf,strlen(buf),0);//发送响应
-
//printf("send=%d\n",s);
-
int fd = open("hello.html",O_RDONLY);//消息体
-
sendfile(connfd,fd,NULL,2500);//零拷贝发送消息体
-
close(fd);
-
close(connfd);
-
}
-
}
-
return 0;
-
}
最简单的html文件:
-
<html>
-
<body bgcolor="blue">
-
this is the html.
-
<hr>
-
<p>hello word! waste young! </p><br>
-
</body>
-
</html>
运行web.c文件,生成执行文件a.out,在终端执行后,我们在浏览器的网址栏中输入:http://localhost:8888 然后确认后,就会返回hello.html的文件页面
这里的URL,localhost:实际就是hostname,然后8888是端口,如果在端口后面再加上比如/hello.html这样的路径就表示请求服务器上的一个hello.html,这里请求方法是GET,所以要求服务器返回该资源的页面。
那么此时再来看下服务器接收到的东西,就是HTTP请求。
第一行就是请求行,请求行的格式是这样的:请求方法+空格+URL+空格+协议版本+\r+\n 这里的请求方法是GET ,URL是/(在这里,URL就相当于资源的路径,若在网址栏输入的是http://localhost:8888/hello.html的话,这里浏览器发送过来的URL就是/hello.html),协议版本是HTTP/1.1(现在多数协议版本都是这个)。
第二行到最后一行都是请求头部,请求头部的格式是这样的: 头部字段:+空格+数值+\r+\n 然后多个头部子段组织起来就是请求头部,在最后的头部字段的格式中需要有两个换行符号,最后一行的格式是:头部字段:+空格+数值+\r+\n+\r+\n 因为在后面还要跟着请求数据,为了区分请求数据和请求头的结束,就多了一个换行符。
三、HTTP请求和响应
(1)HTTP请求
简而言之就是客户端发送给服务端的请求。请求格式上面略提到了一点点,大概的格式就如下所示:
其中的细节就很多了,但是主要的是请求方法。其中头部字段有很多,大家可以上网百度。主要实现的就是GET方法和POST方法,其中GET方法是请求资源,但是不改变服务器上资源的,POST方法的话就会请求更改服务器上的资源。除了这两个方法外,还有PUT,DELETE,HEAD,TRACE等等。对应增删查改的就是PUT、DELETE、POST、GET。
然后URL就是要请求的资源路径,协议版本为HTTP/1.1,头部字段根据每个头部字段名都代表着给服务器的一个信息,具体可以根据以下网址查看:https://blog.csdn.net/sinat_22840937/article/details/64438253
(2)HTTP响应
HTTP响应就是服务端返回给客户端的响应消息。响应格式大概如下:
其中响应首行格式如:HTTP/1.1+状态响应码+\r\n 状态响应码参考如下:https://baike.baidu.com/item/HTTP状态码/5053660?fr=aladdin
这里大概用的是200,400,403,404,其中头部字段需要注意content-length,在服务器中响应码若没有消息题的长度,浏览器就只能通过关闭客户端才可以得知消息体的长度,才可以显示出消息体的具体表现。而且消息体的长度必须要和消息体吻合。如果服务端发送的消息体长度不正确的话,会导致超时或者浏览器一直显示不了要的资源文件。详细可以参考博客:https://www.cnblogs.com/lovelacelee/p/5385683.html
四、如何写出小型 web服务器
1、代码预备知识
- 了解TCP三次握手和TCP四次挥手
- 线程同步机制包装类
- 线程池创建
- epoll多路复用
(1)TCP三次握手
- 服务器需要准备好接受外来连接,通过socket bind listen三个函数完成,然后我们称为被动打开。
- 客户则通过connect发起主动连接请求,这就导致客户TCP发送一个SYN(同步)分节去告诉服务器客户将在待建立的连接中发送的数据的初始序列号,通常SYN不携带数据,其所在IP数据只有一个IP首部,一个TCP首部以及可能有的TCP选项。
- 服务器确认客户的SYN后,同时自己也要发送一个SYN分节,它含有服务器将在同一个连接中发送的数据的初始化列序号,服务器在单个分节中发送SYN和对客户SYN的确认
- 客户必须去确认服务器的SYN
(2)TCP四次挥手
- 某一个应用进程首先调用close,称为该端执行主动关闭,该端的TCP会发送一个FIN分节,表示数据已经发送完毕
- 接到FIN的对端将执行被动关闭,这个FIN由TCP确认,它的接受也作为一个文件结束符传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上已无额外数据可以接收
- 一段时间后,接收到这个文件结束符的应用进程会调用close关闭它的套接字,这会导致它的TCP也要发送一个FIN
- 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN
参考网站:https://www.cnblogs.com/Andya/p/7272462.html
(3)线程池的创建
我用的是半同步/半反应堆线程池。该线程池通用性比较高,主线程一般往工作队列中加入任务,然后工作线程等待后并通过竞争关系从工作队列中取出任务并且执行。而且应用到服务器程序中的话要保证客户请求都是无状态的,因为同一个连接上的不同请求可能会由不同的线程处理。
ps:若工作队列为空,则线程就处于等待状态,就需要同步机制的处理。
代码:
-
-
#ifndef _THREADPOOL_H
-
#define _THREADPOOL_H
-
#include<iostream>
-
#include<list>
-
#include<cstdio>
-
#include<semaphore.h>
-
#include<exception>
-
#include<pthread.h>
-
#include"myhttp_coon.h"
-
#include"mylock.h"
-
using namespace std;
-
-
template<typename T>
-
/*线程池的封装*/
-
class threadpool
-
{
-
private:
-
int max_thread;//线程池中的最大线程总数
-
int max_job;//工作队列的最大总数
-
pthread_t *pthread_poll;//线程池数组
-
std::list<T*> m_myworkqueue;//请求队列
-
mylocker m_queuelocker;//保护请求队列的互斥锁
-
sem m_queuestat;//由信号量来判断是否有任务需要处理
-
bool m_stop;;//是否结束线程
-
public:
-
threadpool();
-
~threadpool();
-
bool addjob(T* request);
-
private:
-
static void* worker(void *arg);
-
void run();
-
};
-
/*线程池的创建*/
-
template <typename T>
-
threadpool<T> :: threadpool()
-
{
-
max_thread = 8;
-
max_job = 1000;
-
m_stop = false;
-
pthread_poll = new pthread_t[max_thread];//为线程池开辟空间
-
if(!pthread_poll)
-
{
-
throw std::exception();
-
}
-
for(int i=0; i<max_thread; i++)
-
{
-
cout << "Create the pthread:" << i << endl;
-
if(pthread_create(pthread_poll+i, NULL, worker, this)!=0)
-
{
-
delete [] pthread_poll;
-
throw std::exception();
-
}
-
if(pthread_detach(pthread_poll[i]))//将线程分离
-
{
-
delete [] pthread_poll;
-
throw std::exception();
-
}
-
}
-
}
-
-
template <typename T>
-
threadpool<T>::~threadpool()
-
{
-
delete[] pthread_poll;
-
m_stop = true;
-
}
-
-
template <typename T>
-
bool threadpool<T>::addjob(T* request)
-
{
-
m_queuelocker.lock();
-
if(m_myworkqueue.size()> max_job)//如果请求队列大于了最大请求队列,则出错
-
{
-
m_queuelocker.unlock();
-
return false;
-
}
-
m_myworkqueue.push_back(request);//将请求加入到请求队列中
-
m_queuelocker.unlock();
-
m_queuestat.post();//将信号量增加1
-
return true;
-
}
-
template <typename T>
-
void* threadpool<T>::worker(void *arg)
-
{
-
threadpool *pool = (threadpool*)arg;
-
pool->run();
-
return pool;
-
}
-
-
template <typename T>
-
void threadpool<T> :: run()
-
{
-
while(!m_stop)
-
{
-
m_queuestat.wait();//信号量减1,直到为0的时候线程挂起等待
-
m_queuelocker.lock();
-
if(m_myworkqueue.empty())
-
{
-
m_queuelocker.unlock();
-
continue;
-
}
-
T* request = m_myworkqueue.front();
-
m_myworkqueue.pop_front();
-
m_queuelocker.unlock();
-
if(!request)
-
{
-
continue;
-
}
-
request->doit();//执行工作队列
-
}
-
}
-
#endif
(4)同步机制的包装类
因为采用了线程池,就相当于用了多线程编程,此时就需要考虑各个线程对公共资源的访问的限制,因为方便之后的代码采用了三种包装机制,分别是信号量的类,互斥锁的类和条件变量的类。在服务器中我使用的是信号量的类。其中信号量的原理和System V IPC信号量一样(不抄书了,直接拍照了。。。)
代码实现:
-
#ifndef _MYLOCK_H
-
#define _MYLOCK_H
-
#include<iostream>
-
#include<list>
-
#include<cstdio>
-
#include<semaphore.h>
-
#include<exception>
-
#include<pthread.h>
-
#include"myhttp_coon.h"
-
using namespace std;
-
-
/*封装信号量*/
-
class sem{
-
private:
-
sem_t m_sem;
-
public:
-
sem();
-
~sem();
-
bool wait();//等待信号量
-
bool post();//增加信号量
-
};
-
//创建信号量
-
sem :: sem()
-
{
-
if(sem_init(&m_sem,0,0) != 0)
-
{
-
throw std ::exception();
-
}
-
}
-
//销毁信号量
-
sem :: ~sem()
-
{
-
sem_destroy(&m_sem);
-
}
-
//等待信号量
-
bool sem::wait()
-
{
-
return sem_wait(&m_sem) == 0;
-
}
-
//增加信号量
-
bool sem::post()
-
{
-
return sem_post(&m_sem) == 0;
-
}
-
-
/*封装互斥锁*/
-
class mylocker{
-
private:
-
pthread_mutex_t m_mutex;
-
public:
-
mylocker();
-
~mylocker();
-
bool lock();
-
bool unlock();
-
};
-
-
mylocker::mylocker()
-
{
-
if(pthread_mutex_init(&m_mutex, NULL) != 0)
-
{
-
throw std::exception();
-
}
-
}
-
-
mylocker::~mylocker()
-
{
-
pthread_mutex_destroy(&m_mutex);
-
}
-
/*上锁*/
-
bool mylocker::lock()
-
{
-
return pthread_mutex_lock(&m_mutex)==0;
-
}
-
/*解除锁*/
-
bool mylocker::unlock()
-
{
-
return pthread_mutex_unlock(&m_mutex) == 0;
-
}
-
-
/*封装条件变量*/
-
class mycond{
-
private:
-
pthread_mutex_t m_mutex;
-
pthread_cond_t m_cond;
-
public:
-
mycond();
-
~mycond();
-
bool wait();
-
bool signal();
-
};
-
-
mycond::mycond()
-
{
-
if(pthread_mutex_init(&m_mutex,NULL)!=0)
-
{
-
throw std::exception();
-
}
-
if(pthread_cond_init(&m_cond, NULL)!=0)
-
{
-
throw std::exception();
-
}
-
}
-
-
mycond::~mycond()
-
{
-
pthread_mutex_destroy(&m_mutex);
-
pthread_cond_destroy(&m_cond);
-
}
-
-
/*等待条件变量*/
-
bool mycond::wait()
-
{
-
int ret;
-
pthread_mutex_lock(&m_mutex);
-
ret = pthread_cond_wait(&m_cond,&m_mutex);
-
pthread_mutex_unlock(&m_mutex);
-
return ret == 0;
-
}
-
-
/*唤醒等待条件变量的线程*/
-
bool mycond::signal()
-
{
-
return pthread_cond_signal(&m_cond) == 0;
-
}
-
-
#endif
(5)epoll多路复用
epoll系列系统调用函数(#include<sys/epoll.h>):
int epoll_create(int size);创建内核事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);操作epoll的内核事件表
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);一段时间内等待一组文件描述符上的就绪事件
除此这些函数外,还需要了解epoll的LT模式和ET模式还有EPOLLONESHOT事件.
下面三篇博客了解下: