目录
项目背景
目前主流的服务器协议是 http1.1,而我们这次要实现的是1.0,其主要的特点就是短链接,所谓短链接,就是请求,相应,客户端关闭连接,这样就完成了一次http请求,使用其主要的原因是因为其简单。
下面我们来谈一谈具体的几个1.0版本的特征:
1.简单快速,HTTP服务器的程序规模小,因而通信速度很快。
2.灵活,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
3.无连接,每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用 这种方式可以节省传输时间。(http/1.0具有的功能,http/1.1兼容)
众所周知,http的底层是基于tcp的,而tcp是面向连接的,为什么还说http是无连接的呢?
解释:这个地方可以这么理解,有链接是tcp的概念,而http对于连接是没有感知的,他只是把我的请求信息填写好之后,交给下层协议,而对端的http对于连接也是没有概念的,只是把我的相应信息写好之后交给我的下层协议,而下层协议怎么做,那是下层协议的工作,和我无关,所以对我们来讲,所谓的无连接,就是相当于对比tcp的,因为连接是tcp已经帮我们做了,http就不关心了。
4.无状态 本身是不会记录对方任何状态。
解释:我们每次登录 B站 CSDN的时候,我们发现之前登录过的时候,不再需要身份认证。但是http本身是无状态的,所以http现在是无法满足我们现在的用户要求的。所以这个并不是我们http层来做的。
URI & URL & URN
http://host [":"port][abs_path]
如 :输入 : www.baidu.com ,浏览器自动转换成: http(s):// www.baidu.com/
![](https://i-blog.csdnimg.cn/blog_migrate/ec4234945a7966268f2a9cbacdf4a93d.png)
我们没有指明要访问baidu的什么,结果就是返回我们百度的首页。
接下来我们先开始写一点代码,逐步实现一下这个机制:
创建tcp服务端
1 #pragma once
2
3 #include <iostream>
4 #include <cstdlib>
5 #include <cstring>
6 #include <sys/types.h>
7 #include <sys/socket.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10 #include <unistd.h>
11 #include <pthread.h>
12
13 #define BACKLOG 5
14
15 class TcpServer{
16 private:
17 int port;
18 int listen_sock;
19 static TcpServer *svr;
20 private:
21 TcpServer(int _port):port(_port),listen_sock(-1)
22 {}
23 TcpServer(const TcpServer &s){}
24 public:
25 static TcpServer *getinstance(int port)
26 {
27 static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
28 if(nullptr == svr){
29 pthread_mutex_lock(&lock);
30 if(nullptr == svr){
31 svr = new TcpServer(port);
32 svr->InitServer();
33 }
34 pthread_mutex_unlock(&lock);
35 }
36 return svr;
37 }
38 void InitServer()
39 {
40 Socket();
41 Bind();
42 Listen();
43 }
44 void Socket()
45 {
46 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
47 if(listen_sock < 0){
48 exit(1);
49 }
50 int opt = 1;
51 setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt) );
对于这个地方内容还是不太清楚的,可以看一下下面这个博主的分享:
网络编程套接字(二)_2021dragon的博客-CSDN博客
说明:这个地方我们先多一步,把它设计成单例模式,这样的话更有利于在后面的文件中直接使用tcpserver,而临界资源需要引入互斥锁。
tcp测试
我们可以对刚刚写的代码进行一下测试:
#include"TcpServer.hpp"
#include<iostream>
#include<string>
static void Usage(std::string proc)
{
std::cout <<"Usage: \n\t" <<proc<<" prot "<< std::endl;
}
int main(int argc,char *argv[])
{
if(argc != 2){
Usage(argv[0]);
exit(4);
}
int port =atoi(argv[1]);
TcpServer *svr = TcpServer::getinstance(port);
for(;;)
{
}
return 0;
}
说明:
1.这里的Usage是使用手册,当启动客户端的时候输入参数不合法(参数错误或者参数缺少)时候,就会告诉你怎么使用,这就是手册,如下图:
这个时候我们没有输入端口号,手册会提示我们,要进行端口号的输入。
2.这里我们先写一个死循环,仅仅是来测试我们之前写的内容没有什么问题。
这个时候我们通过监视看到,端口号8081的tcp启动,并且为监听状态,说明我们之前写的内容没有什么问题,接下来我们进行下一步的实现。
创建HTTP服务端
接下来,我们就开始写HTTP的请求:
#pragma once
#include <iostream>
#include <pthread.h>
#include"Protocol.hpp"
#include "TcpServer.hpp"
#define PORT 8081
class HttpServer{
private:
int port;
TcpServer *tcp_server;
bool stop;
public:
HttpServer(int _port = PORT):port(_port),tcp_server(nullptr),stop(false)
{}
void InitServer()
{
tcp_server =TcpServer::getinstance(port);
}
void Loop()
{
int listen_sock = tcp_server->Sock();
while(!stop){
struct sockaddr_in peer;
socklen_t len =sizeof(peer);
int sock =accept(listen_sock,(struct sockaddr*)&peer,&len);
if(sock < 0 ){
continue;
}
int *_sock = new int(sock);
pthread_t tid;
pthread_create(&tid,nullptr,Entrance:: HandlerRequest,_sock);
pthread_detach(tid);
}
}
~HttpServer()
{}
};
说明:
1.我们先完成HTTP的初始化,获取一个tcp的对象,拿到其监听套件字,从底层连接队列拿到任务套件字,创建一个线程,线程分离,使得任务套件字去执行对应的任务。
2.这个地方要说明一下的是, int *_sock = new int(sock) 只是暂时的一个方案,sock在中间处理的过程中可能会被修改,这样我们先new一下,后面讲解到任务Task的时候再把这里优化掉。
HTTP服务端测试
执行方法
刚才 HandlerRequest,_sock 中的方法是什么呢,这里我们就是先让其去打印我们获取到的链接,然后打印对应的文件描述符,然后关闭我们对应的文件描述符。
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
class Entrance{
public:
static void *HandlerRequest(void *_sock)
{
int sock =*(int*)_sock;
delete (int*)_sock;
std::cout<<"get a new link ..."<<sock<<std::endl;
close(sock);
return nullptr;
}
};
主文件修改调试:
那么对应的,主文件中的也要稍作调整,不再是测试TcpServer了,而是测试HttpServer,创建对应的一个对象,初始化,启动。
#include"HttpServer.hpp"
#include<iostream>
#include<string>
#include<memory>
static void Usage(std::string proc)
{
std::cout <<"Usage: \n\t" <<proc<<" prot "<< std::endl;
}
int main(int argc,char *argv[])
{
if(argc != 2){
Usage(argv[0]);
exit(4);
}
int port =atoi(argv[1]);
std::shared_ptr<HttpServer> http_server(new HttpServer(port));
http_server->InitServer();
http_server->Loop();
return 0;
}
然后编译、链接通过,我们进行测试一下。
我们可以看到,现在已经启动起来了,我们尝试用浏览器对他进行一下访问。
发现有很多链接打印,证明链接成功了。
报文打印测试
这个时候我们其实可以做一个更加深入一点的测试,我们想看一看在服务器一段接收到的信息是什么样子的,我们可以具体来看一下。
我们可以设置一个接收的buffer,设置成4KB的大小,在套件字中读取数据,去获取报文的具体内容。
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
class Entrance{
public:
static void *HandlerRequest(void *_sock)
{
int sock =*(int*)_sock;
delete (int*)_sock;
std::cout<<"get a new link ..."<<sock<<std::endl;
//For Test
char buffer[4096];
recv(sock,buffer,sizeof(buffer),0);
std::cout<<"-------------------begin--------------------"<<std::endl;
std::cout<<buffer<<std::endl;
std::cout<<"-------------------end--------------------"<<std::endl;
close(sock);
return nullptr;
}
};
这个时候我们再编译链接通过,用我们的浏览器进行访问。
这个时候我们可以看到:
拿到了我们的报文,那么报文具体是什么意思呢,我们接着往下看
略谈报文
【说明】:
1.POST 是请求方法
2.HTTP 后面跟的是版本
2.剩下的都是以key:value 的形式的属性
包括 主机号:XXXX 连接方式:是长连接的XXX(Key:value)
这里重点是这个GET 后面的 / 是不是linux服务器的根目录开始呢?
不一定,通常不会设置成为根目录,我们通常由http服务器设置为自己的WEB根目录(就是Linux)下一个特定的路径。
我们读取的基本单位,主要是按照行读取。
但是这里要注意,我们不能使用那些C/C++中按照行读取的接口,因为他们在有些平台是以“\n 或者“\r”来结尾的,有的是以‘\r\n‘来的所以,我们要兼容各种行分隔符
这个地方问题又来了,我们怎么这以\n ,\r,或者\r\n这三种形式的分隔符呢,显然系统调用是不可能实现的,我们只能自己来写
实现较兼容的行读取
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
//工具类
class Util{
public:
static int ReadLine(int sock, std::string &out)
{
char ch = 'X';
while(ch != '\n'){
ssize_t s = recv(sock, &ch, 1, 0);
if(s > 0){
if(ch == '\r'){
recv(sock, &ch, 1, MSG_PEEK);
if(ch == '\n'){
//把\r\n->\n
//窥探成功,这个字符一定存在
recv(sock, &ch, 1, 0);
}
else{
ch = '\n';
}
}
//1. 普通字符
//2. \n
out.push_back(ch);
}
else if(s == 0){
return 0;
}
else{
return -1;
}
}
return out.size();
}
};
测试较兼容的行读取
简单的进行一下调整,测试读取一行,保存退出,运行。
![](https://i-blog.csdnimg.cn/blog_migrate/31dd5b2b2d4dfa352dce0142dce0d30c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/23dee3c147256026260d71f061fb457d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/76804158bc5f27ae76ad3b56bcf800cd.png)
每一次读取了一行,我们的兼容性行读取就实现了。