项目背景
目前主流的服务器协议是 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层来做的。
http协议每当有新的请求产生,就会有对应的新响应产生。协议本身并不会保留你之前的一切请求或者响应,这是为了更快的处理大量的事务,确保协议的可伸缩性。
可是,随着web的发展,因为无状态而导致业务处理变的棘手起来。比如保持用户的登陆状态。
http/1.1虽然也是无状态的协议,但是为了保持状态的功能,引入了cookie技术,后面我们就会学到。
URI & URL & URN
下面我们来区分三个概念:
URI,是uniform resource identififier,统一资源标识符,用来唯一的标识一个资源
URL,是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
简而言之,URL是URI的一种,他不仅仅可以向URI一样表示唯一的一个资源,而且还可以通过这个来找到这个资源,我们可以把现在的网址叫成URL。
URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java
net@java.sun.com。(比较少见)
URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI.
URL是 URI 的子集。任何东西,只要能够唯一地标识出来,都可以说这个标识是 URI 。如果这个标识是一个可获取到上述对象的路径,那么同时它也可以是一个 URL ;但如果这个标识不提供获取到对象的路径,那么它就必然不是URL 。
举个例子:
URI: /home/index.html
URL: www.xxx.com:/home/index.html
HTTP URL (URL是一种特殊类型的URI,包含了如何获取指定资源)的格式如下:
http表示要通过HTTP协议来定位网络资源
host表示合法的Internet主机域名或者IP地址,本主机IP:127.0.0.1
port指定一个端口号,为空则使用缺省端口80(可以省略,因为都知道)
abs_path指定请求资源的URI
如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。
如:
如果用户的URL没有指明要访问的某种资源(路径),虽然浏览器默认会添加/,但是依旧没有告知服务器,要访问什么资源,此时默认返回对应服务的首页。
回车一下你会发现:
我们没有指明要访问baidu的什么,结果就是返回我们百度的首页。
接下来我们先开始写一点代码,逐步实现一下这个机制:
创建tcp服务端
来完成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)下一个特定的路径。
这个时候我们就要读取请求了,但是有的人心想,我们刚才不是已经读取了请求了吗,然而实际上,我们刚才读取的那种请求其实是不标准的,我们是按照4KB大小进行读取的,但是tcp是
面向字节流的,我们不能排除tcp同时有大量的连接请求或者大量的内容进行发送,在没有明确约定的情况下进行读取,很容易发生
数据包粘包的问题,这个时候我们就应该正确的去处理这些问题:
我们读取的基本单位,主要是按照行读取。
但是这里要注意,我们不能使用那些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();
}
};
【说明】:
● 本机制的原理如上,如果是\n,则不用处理,为正常的一行分割为获取一个字符
● 首先判断是否是‘ \r ‘,这个时候有两种可能,一种是/r 还有一种是 /r/n,我们这里需要统一处理成\n,达到我们的目的。
● 当拿到\r的时候,这时候需要确定下一个字符是否为\n,我们这里需要进行探测,注意,我们不能直接读取下一个数据,如果下一个不是\n,那么读取后直接已经拿到接收缓冲区,相当于多读了下一行的开头,是绝对不能允许的,所以我们要进行探测,即把recv的第三个参数设成 MSG_PEEK,就会进行下一次的探测,不会直接读到接收缓冲区。
测试较兼容的行读取
简单的进行一下调整,测试读取一行,保存退出,运行。
我们用浏览器再进行一下测试:
我们可以看到:
每一次读取了一行,我们的兼容性行读取就实现了。
版权归原作者 萌小檬 所有, 如有侵权,请联系我们删除。