Linux--HTTP协议(http服务器构建)

news2025/1/11 1:26:11

目录

1.HTTP 协议

2.认识 URL

3.urlencode 和 urldecode(编码)

urlencode(URL编码)

urldecode(URL解码)

4.HTTP 协议请求与响应格式

4.1HTTP 常见方法(三种)

 5.HTTP 的状态码

6.HTTP 常见 Header

 7.HTTP请求和处理(代码)

7.1辅助库

7.2基于TCP的Socket封装

 7.3我的TCP服务器

 7.4HTTP服务器的基本框架

7.4.1 http的web根目录中的服务

 7.5服务器启动

7.6 基于测试阐述清一些概念 

7.6.1关于重定向:

7.6.2 GET和POST

7.6.form表单

7.6.4 百度的form表单

7.6.5 Header之一:cookie


1.HTTP 协议

        虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现
成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其
中之一。
在互联网世界中, HTTP(HyperText Transfer Protocol, 超文本传输协议) 是一个至
关重要的协议。 它定义了客户端(如浏览器) 与服务器之间如何通信, 以交换或传输
超文本(如 HTML 文档) 。
HTTP 协议是客户端与服务器之间通信的基础。 客户端通过 HTTP 协议向服务器发送
请求, 服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、 无状态的协
议, 即每次请求都需要建立新的连接, 且服务器不会保存客户端的状态信息。


2.认识 URL

平时我们俗称的 "网址" 其实就是说的 URL

URL通常由以下几部分组成:

  • 协议(Scheme):指定了访问资源所使用的网络协议,如httphttpsftp等。协议部分以“://”为分隔符。
  • 用户名和密码(可选):格式为“用户名:密码@”,用于需要认证的场合,但现代Web应用中较少见。
  • 主机名(Host):可以是域名(域名最终会自动转换成IP地址)或IP地址,用于标识资源所在的服务器。
  • 端口号(Port)(可选):指定了服务器用于接收请求的端口。如果省略,则使用协议的默认端口号,如HTTP默认是80,HTTPS默认是443。
  • 路径(Path):指定了服务器上资源的具体位置。
  • 查询字符串(Query String)(可选):用于向服务器传递额外的参数信息,以“?”开头,参数之间以“&”分隔。
  • 片段标识符(Fragment Identifier)(可选):用于指定资源内部的某个位置,以“#”开头,仅由客户端使用。


         在HTTP的URL中,端口号通常是不显示的,因为HTTP有一个默认的端口号80。当你访问一个使用HTTP协议的网站时,如果URL中没有指定端口号,浏览器会自动使用80端口来连接到服务器。

        对于HTTPS协议,标准的端口号是443。当你在浏览器中输入一个URL时,如果URL中没有指定端口号,浏览器会自动使用标准端口号进行连接。

        所谓的URL域名就是服务器地址,就是唯一的一台主机,端口号对应唯一的服务进程,文件路径标明该主机上的唯一的文件资源。域名加路径就表明了互联网中唯一的一个文件资源,所以URL称为统一资源定位符


3.urlencode 和 urldecode(编码)

urlencode(URL编码)

urlencode 函数用于将URL中的非ASCII字符或其他特殊字符转换为一种特定的编码格式。这是因为URL只能包含ASCII字符集中的字符,包括字母、数字和一些特定的符号。如果URL中包含空格、中文、特殊符号等,就需要使用urlencode进行编码。

编码后的URL会将特殊字符转换为%后跟两位十六进制数的形式。例如,空格会被编码为%20,中文字符也会被编码为相应的十六进制字符串。

urldecode(URL解码)

urldecode 函数是urlencode的逆过程,它用于将编码后的URL还原为原始的URL。解码过程会将%后跟的两位十六进制数转换回对应的字符。

eg:

urldecode 就是 urlencode 的逆过程;
 


4.HTTP 协议请求与响应格式

HTTP 请求

• 首行: [方法] + [url] + [版本]
• Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n 分隔;遇到空行
表示 Header 部分结束
• Body: 空行后面的内容都是 Body. Body 允许为空字符串. 如果 Body 存在, 则在
Header 中会有一个 Content-Length 属性来标识 Body 的长度;

  1. 请求行:位于最上方,由请求方法(如GET或POST)、一个空格、统一资源标识符(URI,表示请求的资源)、另一个空格、HTTP协议版本(如HTTP/1.1)以及一个换行符组成。请求方法指明了对资源执行的操作类型,URI则指定了资源的具体位置。

  2. 请求报头:紧随请求行之后,由多个键值对组成,每个键值对之间通过换行符分隔。每个键值对以“Key: Value”的形式出现,其中Key是报头的名称,Value是对应的值,二者之间用冒号和空格分隔。请求报头包含了关于请求的各种元信息,如内容类型、认证信息等。

  3. 空行:在请求报头之后,有一个单独的空行,仅包含一个换行符。这个空行用于明确分隔请求报头和请求正文,使得HTTP请求的结构更加清晰。

  4. 请求正文:位于空行之后,是HTTP请求的可选部分,用于发送额外的数据给服务器。请求正文的内容取决于请求方法和请求的资源,可以是表单数据、JSON对象等。在请求正文的末尾,通常也会有一个换行符。

HTTP 响应

• 首行: [版本号] + [状态码] + [状态码解释]
• Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n 分隔;遇到空行
表示 Header 部分结束
• Body: 空行后面的内容都是 Body. Body 允许为空字符串. 如果 Body 存在, 则在
Header 中会有一个 Content-Length 属性来标识 Body 的长度; 如果服务器返回了一
个 html 页面, 那么 html 页面内容就是在 body 中.

基本的应答格式


4.1HTTP 常见方法(三种)

1. GET 方法(重点)(见7.6.2)
用途: 用于请求 URL 指定的资源。
示例: GET /index.html HTTP/1.1
特性: 指定资源经服务器端解析后返回响应内容。
form 表单: https://www.runoob.com/html/html-forms.html
要通过历史写的 http 服务器, 验证 GET 方法,这里需要了解一下 FORM 表单的问题
2.POST 方法(重点)
用途: 用于传输实体的主体, 通常用于提交表单数据。
示例: POST /submit.cgi HTTP/1.1
特性: 可以发送大量的数据给服务器, 并且数据包含在请求体中。
form 表单: https://www.runoob.com/html/html-forms.htm
3.OPTIONS 方法
用途: 用于查询针对请求 URL 指定的资源支持的方法。
示例: OPTIONS * HTTP/1.1
特性: 返回允许的方法, 如 GET、 POST 等。

// 搭建一个 nginx 用来测试
// sudo apt install nginx
// sudo nginx -- 开启
// ps ajx | grep nginx -- 查看
// sudo nginx -s stop -- 停止服务
$ sudo nginx -s stop
$ ps ajx | grep nginx
2944845 2945390 2945389 2944845 pts/1 2945389 S+ 1002 0:00
grep --color=auto nginx
$ sudo nginx
$ ps axj | grep nginx
1 2945393 2945393 2945393 ? -1 Ss 0 0:00
nginx: master process nginx
2945393 2945394 2945393 2945393 ? -1 S 33 0:00
nginx: worker process
2945393 2945395 2945393 2945393 ? -1 S 33 0:00
nginx: worker process
2944845 2945397 2945396 2944845 pts/1 2945396 S+ 1002 0:00
grep --color=auto nginx
// -X(大 x) 指明方法
$ curl -X OPTIONS -i http://127.0.0.1/
HTTP/1.1 405 Not Allowed
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 16 Jun 2024 08:48:22 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
<html>
<head><title>405 Not Allowed</title></head>
<body>
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

效果:

HTTP/1.1 200 OK
Allow: GET, HEAD, POST, OPTIONS
Content-Type: text/plain
Content-Length: 0
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 16 Jun 2024 09:04:44 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
// 注意: 这里没有响应体, 因为 Content-Length 为 0


 5.HTTP 的状态码

最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定
向), 504(Bad Gateway

状态码含义应用样例
100Continue上传大文件时, 服务器告诉客户端可以
继续上传
200OK访问网站首页, 服务器返回网页内容
201Created发布新文章, 服务器返回文章创建成功
的信息
204No Content删除文章后, 服务器返回“无内容”表示操
作成功
301Moved
Permanently
网站换域名后, 自动跳转到新域名; 搜
索引擎更新网站链接时使用
302Found 或 See
Other
用户登录成功后, 重定向到用户首页
304Not Modified浏览器缓存机制, 对未修改的资源返回
304 状态码
400Bad Request填写表单时, 格式不正确导致提交失败
401Unauthorized访问需要登录的页面时, 未登录或认证
失败
403Forbidden尝试访问你没有权限查看的页面
404Not Found访问不存在的网页链接
500Internal Server
Error
服务器崩溃或数据库错误导致页面无法
加载
502Bad Gateway使用代理服务器时, 代理服务器无法从
上游服务器获取有效响应
503Service
Unavailable
服务器维护或过载, 暂时无法处理请求

HTTP 状态码 301(永久重定向) 和 302(临时重定向) 都依赖 Location 选项。 以下
是关于两者依赖 Location 选项的详细说明:
HTTP 状态码 301(永久重定向) :
• 当服务器返回 HTTP 301 状态码时, 表示请求的资源已经被永久移动到新的位
置。
• 在这种情况下, 服务器会在响应中添加一个 Location 头部, 用于指定资源的新位
置。 这个 Location 头部包含了新的 URL 地址, 浏览器会自动重定向到该地址。
HTTP 状态码 302(临时重定向) :
• 当服务器返回 HTTP 302 状态码时, 表示请求的资源临时被移动到新的位置。
• 同样地, 服务器也会在响应中添加一个 Location 头部来指定资源的新位置。 浏览
器会暂时使用新的 URL 进行后续的请求, 但不会缓存这个重定向。
• 例如, 在 HTTP 响应中, 可能会看到类似于以下的头部信息:

        无论是 HTTP 301 还是 HTTP 302 重定向, 都需要依赖 Location 选项来指定资源的新位置。 这个 Location 选项是一个标准的 HTTP 响应头部, 用于告诉浏览器应该将请求重定向到哪个新的 URL 地址。;


6.HTTP 常见 Header

• Content-Type: 数据类型(text/html 等)
• Content-Length: Body 的长度
• Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
• User-Agent: 声明用户的操作系统和浏览器版本信息;
• referer: 当前页面是从哪个页面跳转过来的;
• Location: 搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问;
• Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

 关于 connection 报头
HTTP 中的 Connection 字段是 HTTP 报文头的一部分, 它主要用于控制和管理客户
端与服务器之间的连接状态
核心作用
管理持久连接: Connection 字段还用于管理持久连接(也称为长连接) 。 持久
连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接, 以便在同一个连接
上发送多个请求和接收多个响应。
持久连接(长连接)
• HTTP/1.1: 在 HTTP/1.1 协议中, 默认使用持久连接。 当客户端和服务器都不明
确指定关闭连接时, 连接将保持打开状态, 以便后续的请求和响应可以复用同一个连
接。因为http都是一请求一应答的,由于连接不需要频繁建立和关闭,服务器可以将更多的资源用于处理实际的HTTP请求和响应,从而提高了并发处理能力。
• HTTP/1.0: 在 HTTP/1.0 协议中, 默认连接是非持久的。 如果希望在 HTTP/1.0
上实现持久连接, 需要在请求头中显式设置 Connection: keep-alive。
语法格式
• Connection: keep-alive: 表示希望保持连接以复用 TCP 连接。
• Connection: close: 表示请求/响应完成后, 应该关闭 TCP 连接。

下面附上一张关于 HTTP 常见 header 的表格

字段名含义样例
Accept客户端可接受的
响应内容类型
Accept:
text/html,application/xhtml+xml,app
lication/xml;q=0.9,image/webp,image
/apng,*/*;q=0.8
Accept
Encoding
客户端支持的数
据压缩格式
Accept-Encoding: gzip, deflate, br
Accept
Language
客户端可接受的
语言类型
Accept-Language: zh
CN,zh;q=0.9,en;q=0.8
Host请求的主机名和
端口号
Host: www.example.com:8080
User-Agent客户端的软件环
境信息
User-Agent: Mozilla/5.0 (Windows NT
10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/91.0.4472.124
Safari/537.36
Cookie客户端发送给服
务器的 HTTP
cookie 信息
Cookie: session_id=abcdefg12345;
user_id=123
Referer请求的来源 URLReferer:
http://www.example.com/previous_pag
e.html
Content-Type实体主体的媒体
类型
Content-Type: application/x-www
form-urlencoded (对于表单提交) 或
Content-Type: application/json (对于
JSON 数据)
Content-Length实体主体的字节
大小
Content-Length: 150
Authorization认证信息, 如用
户名和密码
Authorization: Basic
QWxhZGRpbjpvcGVuIHNlc2FtZQ== (Base64
编码后的用户名:密码)
Cache-Control缓存控制指令请求时: Cache-Control: no-cache 或
Cache-Control: max-age=3600; 响应
时: Cache-Control: public, max
age=3600
Connection请求完后是关闭
还是保持连接
Connection: keep-alive 或
Connection: close
Date请求或响应的日
期和时间
Date: Wed, 21 Oct 2023 07:28:00 GMT
Location重定向的目标
URL(与 3xx 状
态码配合使用)
Location:
http://www.example.com/new_location
.html (与 302 状态码配合使用)
Server服务器类型Server: Apache/2.4.41 (Unix)
Last-Modified资源的最后修改
时间
Last-Modified: Wed, 21 Oct 2023
07:20:00 GMT
ETag资源的唯一标识
符, 用于缓存
ETag: "3f80f-1b6-5f4e2512a4100"
Expires响应过期的日期
和时间
Expires: Wed, 21 Oct 2023 08:28:00
GMT


 7.HTTP请求和处理(代码)

        TCP/IP协议族是互联网的基础,它包括了TCP和IP两个核心协议,以及其他一些辅助协议。HTTP协议是在TCP协议之上实现的,它使用TCP作为传输层协议,确保了数据的可靠传输。HTTP客户端(如浏览器)和服务器之间通过TCP连接进行通信,传输HTTP请求和响应。

        接下来我们就来写一个客户端是浏览器,通过TCP连接我自己的服务器,进行一个请求一个响应的网络传输。

这就是一段http的请求


7.1辅助库

 用于封装和处理 IP 地址及其端口号:InetAddr.hpp

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        // _ip = inet_ntoa(addr.sin_addr);
        char ip_buf[32];
        // inet_p to n
        // p: process
        // n: net
        // inet_pton(int af, const char *src, void *dst);
        // inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);
        ::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
        _ip = ip_buf;
    }

public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }
    InetAddr()
    {}
    bool operator == (const InetAddr &addr)
    {
        return (this->_ip == addr._ip && this->_port == addr._port);
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    struct sockaddr_in Addr()
    {
        return _addr;
    }
    std::string AddrStr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

日志库:Log.hpp

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"

namespace log_ns
{

    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    std::string GetCurrTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    // log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );
    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }
        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._id,
                   lg._filename.c_str(),
                   lg._filenumber,
                   lg._curr_time.c_str(),
                   lg._message_info.c_str());
        }
        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;
            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._id,
                     lg._filename.c_str(),
                     lg._filenumber,
                     lg._curr_time.c_str(),
                     lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }
        void FlushLog(const logmessage &lg)
        {
            // 加过滤逻辑 --- TODO

            LockGuard lockguard(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }
        void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._message_info = log_info;

            // 打印出来日志
            FlushLog(lg);
        }
        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

#define LOG(Level, Format, ...)                                        \
    do                                                                 \
    {                                                                  \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)
#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)
#define EnableFILE()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)
};

给日志库上锁,保证线程安全:LockGuard.hpp

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t *_mutex;
};

7.2基于TCP的Socket封装

使得Socket的使用更加面向对象。 

#include <iostream>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>

#include "Log.hpp"
#include "InetAddr.hpp"
//以下是对socket的封装,方便面向对象式的使用socket
namespace socket_ns
{
    using namespace log_ns;
    class Socket;
    using SockSPtr = std::shared_ptr<Socket>;//Socket是虚基类,实际上是拿TcpSocket
                                             //定义的对象
    enum//创建失败的常量
    {
        SOCKET_ERROR = 1,
        BIND_ERROR,
        LISTEN_ERR
    };
    const static int gblcklog = 8;//监听队列默认大小。
    // 模版方法模式
    class Socket
    {
    public:
        virtual void CreateSocketOrDie() = 0;
        virtual void CreateBindOrDie(uint16_t port) = 0;
        virtual void CreateListenOrDie(int backlog = gblcklog) = 0;
        virtual SockSPtr Accepter(InetAddr *cliaddr) = 0;
        
        virtual bool Conntecor(const std::string &peerip, uint16_t peerport) = 0;
        virtual int Sockfd() = 0;
        virtual void Close() = 0;

        virtual ssize_t Recv(std::string *out) = 0;//进行读取
        virtual ssize_t Send(const std::string &in) = 0;//进行发送

    public:
        void BuildListenSocket(uint16_t port)//创建监听套接字
        {
            CreateSocketOrDie();
            CreateBindOrDie(port);
            CreateListenOrDie();

        }
        //创建客户端套接字
        bool BuildClientSocket(const std::string &peerip, uint16_t peerport)
        {
            CreateSocketOrDie();
            return Conntecor(peerip, peerport);
        }
        // void BuildUdpSocket()
        // {}
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket()
        {
        }
        //监听套接字初始化/构造函数式的初始化
        TcpSocket(int sockfd) : _sockfd(sockfd)
        {
        }
        ~TcpSocket()
        {
        }
        void CreateSocketOrDie() override
        {
            // 1. 创建socket
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(FATAL, "socket create error\n");
                exit(SOCKET_ERROR);
            }
            LOG(INFO, "socket create success, sockfd: %d\n", _sockfd); // 3
        }
        void CreateBindOrDie(uint16_t port) override//bind
        {
            
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;

            // 2. bind sockfd 和 Socket addr
            if (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                LOG(FATAL, "bind error\n");
                exit(BIND_ERROR);
            }
            LOG(INFO, "bind success, sockfd: %d\n", _sockfd); // 3
        }
        //监听
        void CreateListenOrDie(int backlog) override
        {
            // 3. 因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接
            if (::listen(_sockfd, gblcklog) < 0)
            {
                LOG(FATAL, "listen error\n");
                exit(LISTEN_ERR);
            }
            LOG(INFO, "listen success\n");
        }
        //方便获取客户端地址,accept获取一个新的文件描述符
        //而该文件描述符本质就是ip+端口号
        //之前我们使用文件描述符都是面向过程,都是作为函数参数进行传递的
        //我们需要面向对象的使用套接字,我们将得到的IO文件描述符设置进套接字里面
        //返回该套接字
        //using SockSPtr = std::shared_ptr<Socket>;//Socket是虚基类,实际上是拿TcpSocket
                                                   //定义的对象
        SockSPtr Accepter(InetAddr *cliaddr) override
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 4. 获取新连接:得到一个新的文件描述符,得到新的客户端
            int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                return nullptr;
            }
            *cliaddr = InetAddr(client);
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", cliaddr->AddrStr().c_str(), sockfd);
            return std::make_shared<TcpSocket>(sockfd); // C++14
        }
        //连接目标服务器(是否成功)
        //客户端ip和端口号
        bool Conntecor(const std::string &peerip, uint16_t peerport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(peerport);
            //将IPv4地址的字符串形式转换为网络字节顺序的二进制形式,
            //并将其存储在server.sin_addr中
            ::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);
            
            int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            { 
                return false;
            }
            return true;
        }
        int Sockfd()//文件描述符
        {
            return _sockfd;
        }
        void Close()
        {
            if (_sockfd > 0)
            {
                ::close(_sockfd);
            }
        }
        ssize_t Recv(std::string *out) override//读到的消息
        {
            char inbuffer[4096];
            //从sockfd中读
            ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            if (n > 0)
            {
                inbuffer[n] = 0;
                //这里不能是=,不可以覆盖式的读取,因为每一次可能并不是读取到一条完整的报文
                // "len"\r\n
                // "len"\r\n"{json}"\r\n
                //向上面的情况如果覆盖的读取将读取不到完整的报文了
                //所以要用+=
                *out += inbuffer;
            }
            return n;
        }
        ssize_t Send(const std::string &in) override
        {
            return ::send(_sockfd, in.c_str(), in.size(), 0);
        }

    private:
        int _sockfd; // 可以是listensock,普通socketfd
    };
    // class UdpSocket : public Socket
    // {};
} // namespace socket_n

代码逻辑:

  1. 命名空间和类定义
    • 定义了一个命名空间socket_ns,用于封装Socket相关的类和函数。
    • 定义了一个基类Socket,它是一个抽象类,提供了Socket操作的基本接口,如创建、绑定、监听、接收连接、发送和接收数据等。
    • 定义了一个派生类TcpSocket,它继承自Socket类,并实现了所有虚函数,提供了TCP Socket的具体实现。
  2. Socket基类
    • 定义了多个纯虚函数,包括创建Socket、绑定、监听、接受连接、连接服务器、获取文件描述符、关闭Socket、接收和发送数据等。
    • 提供了一个构建监听Socket的成员函数BuildListenSocket,它依次调用创建Socket、绑定和监听函数来初始化监听Socket。
    • 提供了一个构建客户端Socket的成员函数BuildClientSocket,它调用创建Socket和连接服务器函数来初始化客户端Socket。
  3. TcpSocket类
    • 实现了Socket类中的所有纯虚函数,提供了TCP Socket的具体实现。
    • 在构造函数中,可以初始化一个已存在的文件描述符,或者通过调用CreateSocketOrDie函数创建一个新的Socket文件描述符。
    • CreateSocketOrDie函数用于创建一个新的Socket文件描述符。
    • CreateBindOrDie函数用于将Socket绑定到一个指定的端口上。
    • CreateListenOrDie函数用于将Socket设置为监听模式,以便接受连接。
    • Accepter函数用于接受一个新的连接,并返回一个表示该连接的TcpSocket对象。
    • Conntecor函数用于连接到一个指定的服务器。
    • Sockfd函数用于获取Socket的文件描述符。
    • Close函数用于关闭Socket。
    • Recv函数用于从Socket接收数据。
    • Send函数用于向Socket发送数据。
  4. 日志和错误处理
    • 使用了自定义的日志系统(log_ns命名空间中的LOG宏)来记录日志和错误信息。
    • 在发生错误时,使用exit函数终止程序,并传递一个错误码。
  5. 内存管理
    • 使用了智能指针(std::shared_ptr)来管理TcpSocket对象的内存,以避免内存泄漏。

 7.3我的TCP服务器

TcpServer.hpp:

#include <functional>
#include "Socket.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

//处理连接获取问题
using namespace socket_ns;

static const int gport = 8888;
//套接字和客户端地址信息
using service_io_t = std::function<void(SockSPtr, InetAddr &)>;//解决IO的问题

class TcpServer
{
public:
    TcpServer(service_io_t service, int port = gport)
        : _port(port),
          _listensock(std::make_shared<TcpSocket>()),//基类指向子类,这里就有了一个TcpSocket对象
          _isrunning(false),
          _service(service)
    {
        //模板模式
        _listensock->BuildListenSocket(_port);//创建监听套接字,直接启动了服务器套接字
    }
    class ThreadData
    {
    public:
        SockSPtr _sockfd;//封装过的套接字,方便获取文件描述符,智能指针自动管理了这个对象的内存
        //当智能指针的最后一个实例被销毁时,它所管理的TcpSocket对象也会被自动删除,从而避免了内存泄漏。
        TcpServer *_self;
        InetAddr _addr;
    public:
        ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr):_sockfd(sockfd), _self(self), _addr(addr)
        {}
    };
    void Loop()
    {
        // signal(SIGCHLD, SIG_IGN);
        _isrunning = true;
        while (_isrunning)
        {
            InetAddr client;//这里就体现面向对象的好处了,模板式的获取对应的参数
            //调用Accepter,获取到客户端的地址,返回一个底层的套接字
            SockSPtr newsock = _listensock->Accepter(&client);
            if(newsock == nullptr)
                continue;
            //打印客户端信息和文件描述符
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", client.AddrStr().c_str(), newsock->Sockfd());

            //多线程版本 --- 不能关闭fd了,也不需要了
            pthread_t tid;
            ThreadData *td = new ThreadData(newsock, this, client);
            pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离
        }
        _isrunning = false;
    }
    static void *Execute(void *args)//任务执行,回调service方法
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_self->_service(td->_sockfd, td->_addr);//展开回调,交给外部处理
        td->_sockfd->Close();//任务执行完后,关闭文件描述符
        delete td;
        return nullptr;
    }
    ~TcpServer() {}

private:
    uint16_t _port;
    SockSPtr _listensock;//套接字对象
    bool _isrunning;
    service_io_t _service;//解决IO问题
};

代码逻辑:

  1. 初始化与监听
    • 构造函数中,服务器通过调用 _listensock->BuildListenSocket(_port); 创建并监听指定端口上的 TCP 套接字。
    • _listensock 是一个智能指针,指向 TcpSocket 对象,它负责管理监听套接字。
  2. 事件循环
    • Loop() 方法是服务器的主循环,它持续运行直到 _isrunning 标志被设置为 false
    • 在循环中,服务器使用 _listensock 的 Accepter 方法等待并接受客户端的连接请求。
    • 一旦接受到新的连接,服务器会打印客户端的信息和套接字文件描述符,然后为每个新连接创建一个新线程来处理。
  3. 线程处理
    • 对于每个新的连接,服务器创建一个 ThreadData 对象,其中包含套接字、服务器实例指针和客户端地址信息。
    • 使用 pthread_create 创建一个新线程,线程执行 Execute 静态成员函数。
    • Execute 函数中,线程首先分离自身,然后调用 _service 回调函数来处理客户端的连接,传入套接字和客户端地址作为参数。
    • 处理完成后,关闭套接字并删除 ThreadData 对象。
  4. 回调函数
    • _service 是一个函数对象,其类型定义为 std::function<void(SockSPtr, InetAddr &)>,表示它接受一个套接字和一个客户端地址作为参数,并返回 void
    • 这个回调函数由外部提供,服务器在接收到新的连接时调用它来处理客户端的请求。

 7.4HTTP服务器的基本框架

Http.hpp

#pragma once
//http是基于tcp的
//以下是简单的HTTP服务器的基本框架,包括请求解析、
//响应构建以及静态资源服务和简单动态服务的能力。
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <functional>
#include <fstream>
#include <unordered_map>

const static std::string base_sep = "\r\n";//分割符
const static std::string line_sep = ": ";//请求报头的分割符
// 当浏览器或其他客户端请求网站上的资源时,服务器会根据请求的 URL 路径,
// 在根目录及其子目录中查找对应的文件。
// 如果找到了请求的文件,服务器就会将其发送给客户端。
const static std::string prefixpath = "wwwroot"; // web根目录
//当用访问/,也就是根目录,那当然不能把所有的文件都给用户了 
//因此我们引入首页,首页通常位于网站的根目录下,
//并且具有简洁明了的布局和易于导航的菜单结构。(百度的首页也是index.html)
//因此当用户访问/时,我们自动的把index.html拼接在htl后面,表示访问首页
const static std::string homepage = "index.html";//首页
const static std::string httpversion = "HTTP/1.0";//功能限制
const static std::string spacesep = " ";//空
const static std::string suffixsep = ".";//后缀
const static std::string html_404 = "404.html";
const static std::string arg_sep = "?";//get方法参数分割

class HttpRequest//请求
{
private:
    std::string GetLine(std::string &reqstr)
    {//获取请求行
        auto pos = reqstr.find(base_sep);//找分割符
        if (pos == std::string::npos)
            return std::string();
        std::string line = reqstr.substr(0, pos);//[0,pos)
        reqstr.erase(0, line.size() + base_sep.size());//移走头一行和换行符
        // \r\n,读到空的情况,那我们就返回分割符,这也是读完了一行
        // \r\ndata
        return line.empty() ? base_sep : line;
    }
    //解析请求行,进一步的反序列化
    void ParseReqLine()
    {
        std::stringstream ss(_req_line);   // cin >>,将字符串转化为流
        //依次以空格为分割符输入在下面的串中
        ss >> _method >> _url >> _version; 

        if (strcasecmp(_method.c_str(), "GET") == 0)//是否为get方法
        {
            // /a/b/c.html or /login?user=XXX&passwd=1234 /register
            auto pos = _url.find(arg_sep);//将url和参数分开
            if (pos != std::string::npos)
            {
                _body_text = _url.substr(pos + arg_sep.size());//提取参数部分
                _url.resize(pos);//只保留url
            }
        }

        _path += _url;//将客户访问资源的路径拿到
        //访问/,则加上首页
        if (_path[_path.size() - 1] == '/')
        {
            _path += homepage;
        }

        // wwwroot/index.html
        // wwwroot/image/1.png
        auto pos = _path.rfind(suffixsep);
        if (pos != std::string::npos)
        {
            _suffix = _path.substr(pos);
        }
        else
        {
            _suffix = ".default";
        }
    }
    //对请求报头进一步的反序列化
    void ParseReqHeader()
    {
        for (auto &header : _req_headers)
        {
            auto pos = header.find(line_sep);
            if (pos == std::string::npos)
                continue;
            std::string k = header.substr(0, pos);
            std::string v = header.substr(pos + line_sep.size());
            if (k.empty() || v.empty())
                continue;
            //成功插入一组
            _headers_kv.insert(std::make_pair(k, v));
        }
    }

public:
    //空行直接构造为base_sep
    HttpRequest() : _blank_line(base_sep), _path(prefixpath)
    {
    }
    void Deserialize(std::string &reqstr)
    {
        // 基本的反序列化
        _req_line = GetLine(reqstr);//获取请求行
        std::string header;
        do
        {
            header = GetLine(reqstr);
            if (header.empty())//读到空串
                break;
            else if (header == base_sep)//读到空行
                break;
            _req_headers.push_back(header);//合法报头
        } while (true);

        if (!reqstr.empty())
        {
            _body_text = reqstr;//获取正文
        }

        // 在进一步反序列化
        ParseReqLine();
        ParseReqHeader();
    }
    std::string Url()//请求的基本资源
    {
        //客户端想要什么资源
        LOG(DEBUG, "Client Want url %s\n", _url.c_str());
        return _url;
    }
    std::string Path()
    {
        //资源的路径,真实的资源
        LOG(DEBUG, "Client Want path %s\n", _path.c_str());
        return _path;
    }
    std::string Suffix()//后缀
    {
        return _suffix;
    }
    std::string Method()//请求方法是什么
    {
        LOG(DEBUG, "Client request method is %s\n", _method.c_str());
        return _method;
    }
    std::string GetResuestBody()//给get方法参数提供
    {
        LOG(DEBUG, "Client request method is %s, args: %s, request path: %s\n",
            _method.c_str(), _body_text.c_str(), _path.c_str() );
        return _body_text;
    }
    void Print()
    {
        std::cout << "----------------------------" << std::endl;
        std::cout << "###" << _req_line << std::endl;//请求行
        for (auto &header : _req_headers)//请求报头
        {
            std::cout << "@@@" << header << std::endl;
        }
        std::cout << "***" << _blank_line;//空行
        std::cout << ">>>" << _body_text << std::endl;//正文

        std::cout << "Method: " << _method << std::endl;
        std::cout << "Url: " << _url << std::endl;
        std::cout << "Version: " << _version << std::endl;

        for (auto &header_kv : _headers_kv)
        {
            std::cout << ")))" << header_kv.first << "->" << header_kv.second << std::endl;
        }
    }
    ~HttpRequest()
    {
    }

private:
    // 基本的httprequest的格式
    std::string _req_line;//请求行
    std::vector<std::string> _req_headers;//请求报头
    std::string _blank_line;//空行
    std::string _body_text;//请求正文

    // 更具体的属性字段,需要进一步反序列化
    std::string _method;//存储HTTP请求的方法,如GET、POST、PUT等。
    std::string _url;//存储请求的完整URL。
    std::string _path;//存储URL中的路径部分,不包括查询字符串或锚点。
    std::string _suffix; // 资源后缀
    std::string _version;//存储HTTP协议的版本,如HTTP/1.1。
    //一个无序映射(unordered_map),用于存储HTTP请求头中的键值对。
    //键是请求头的名称,值是对应的报文。
    std::unordered_map<std::string, std::string> _headers_kv;
};


/
class HttpResponse
{
public:
    //
    HttpResponse() : _verison(httpversion), _blank_line(base_sep)
    {
    }
    //状态行
    void AddCode(int code, const std::string &desc)
    {
        _status_code = code;
        _desc = desc;
    }
    //报头
    void AddHeader(const std::string &k, const std::string &v)
    {
        _headers_kv[k] = v;
    }
    //正文
    void AddBodyText(const std::string &body_text)
    {
        _resp_body_text = body_text;
    }
    std::string Serialize()//序列化
    {
        // 1. 构建状态行
        //http版本+空格+状态码+空格+描述+换行符
        _status_line = _verison + spacesep + std::to_string(_status_code) + spacesep + _desc + base_sep;

        // 2. 构建应答报头
        //一行为单位的,key:[空格]value 换行符
        for (auto &header : _headers_kv)
        {
            std::string header_line = header.first + line_sep + header.second + base_sep;
            _resp_headers.push_back(header_line);
        }

        // 3. 空行和正文
        //空行构造就行了,正文已经添加过了
        // 4. 正式序列化
        std::string responsestr = _status_line;//状态行
        for (auto &line : _resp_headers)
        {
            responsestr += line;//响应报头
        }
        responsestr += _blank_line;//空行
        responsestr += _resp_body_text;//正文

        return responsestr;//完整的应答串
    }
    ~HttpResponse()
    {
    }

private:
    // httpresponse base 属性
    std::string _verison;//版本
    int _status_code;//状态码
    std::string _desc;//状态码描述
    std::unordered_map<std::string, std::string> _headers_kv;//报头和内容的映射关系

    // 基本的httprequest的格式
    std::string _status_line;//状态行
    std::vector<std::string> _resp_headers;//响应报头
    std::string _blank_line;//空行
    std::string _resp_body_text;//响应正文
};
//get方法可以给服务器传递参数,那么我们就可以接收他请求附带的参数
//在根据我们服务器内部自己的方法处理后,返回给客户端,而不是将wwwroot中的资源交给用户
//请求给我,我给你应答
using func_t = std::function<HttpResponse(HttpRequest&)>;

class HttpServer
{
private:
    //读取文件内容,并将文件内容返回
    std::string GetFileContent(const std::string &path)
    {
        // std::ifstream in(path, std::ios::binary);/以二进制模式打开
        if (!in.is_open())
            return std::string();
        in.seekg(0, in.end);
        int filesize = in.tellg(); // 告知我你的rw偏移量是多少
        in.seekg(0, in.beg);

        std::string content;
        content.resize(filesize);
        in.read((char *)content.c_str(), filesize);
        in.close();

        return content;
    }

public:
    HttpServer()
    {
        //支持的后缀
        _mime_type.insert(std::make_pair(".html", "text/html"));
        _mime_type.insert(std::make_pair(".jpg", "image/jpeg"));
        _mime_type.insert(std::make_pair(".png", "image/png"));
        _mime_type.insert(std::make_pair(".default", "text/html"));
        //状态码描述
        _code_to_desc.insert(std::make_pair(100, "Continue"));
        _code_to_desc.insert(std::make_pair(200, "OK"));
        _code_to_desc.insert(std::make_pair(201, "Created"));
        _code_to_desc.insert(std::make_pair(301, "Moved Permanently"));
        _code_to_desc.insert(std::make_pair(302, "Found"));
        _code_to_desc.insert(std::make_pair(404, "Not Found"));
    }

    // #define TEST
    //交给TCPserver的方法,处理得到的http请求
    std::string HandlerHttpRequest(std::string &reqstr) // req 曾经被客户端序列化过!!!
    {
#ifdef TEST
        std::cout << "---------------------------------------" << std::endl;
        std::cout << reqstr;

        std::string responsestr = "HTTP/1.1 200 OK\r\n";
        responsestr += "Content-Type: text/html\r\n";
        responsestr += "\r\n";
        responsestr += "<html><h1>hello Linux, hello World!</h1></html>";

        return responsestr;
#else
        //看到一个完整的http的请求
        std::cout << "---------------------------------------" << std::endl;
        std::cout << reqstr;
        std::cout << "---------------------------------------" << std::endl;

        HttpRequest req;
        HttpResponse resp;
        req.Deserialize(reqstr);//反序列化
        // req.Method();

        if (req.Path() == "wwwroot/redir")
        {
            // 处理重定向
            std::string redir_path = "https://www.qq.com";
            // resp.AddCode(302, _code_to_desc[302]);
            resp.AddCode(301, _code_to_desc[301]);//状态码->永久重定向
            resp.AddHeader("Location", redir_path);//位置信息
        }
        else if(!req.GetResuestBody().empty())//如果请求带参数的,那么就进入这里
        {//从服务列表中获取
            if(IsServiceExists(req.Path()))//如果服务列表中存在该服务
            {
                resp = _service_list[req.Path()](req);//调用该方法
            }
            else//否则重定向到错误
            {
                std::string redir_path = "wwwroot/404.html";
                resp.AddCode(301, _code_to_desc[301]);
                LOG(DEBUG, "Client Want path:301");
                resp.AddHeader("Location", redir_path);
            }
        }
        else
        {
            // 最基本的上层处理,处理静态资源
            std::string content = GetFileContent(req.Path());//读文件资源
            if (content.empty())//什么都没读到
            {
                content = GetFileContent("wwwroot/404.html");
                resp.AddCode(404, _code_to_desc[404]);//状态码
                resp.AddHeader("Content-Length", std::to_string(content.size()));
                resp.AddHeader("Content-Type", _mime_type[".html"]);
                resp.AddBodyText(content);
            }
            else
            {
                resp.AddCode(200, _code_to_desc[200]);//状态 
                //请求报头的长度
                resp.AddHeader("Content-Length", std::to_string(content.size()));
                
                resp.AddHeader("Content-Type", _mime_type[req.Suffix()]);
                //浏览器会记录cookie信息
                resp.AddHeader("Set-Cookie", "username=zhangsan");//Cookie值
                //resp.AddHeader("Set-Cookie", "passwd=12345");
                //正文部分
                resp.AddBodyText(content);
            }
        }

        return resp.Serialize();//序列化
#endif
    }
    //添加服务列表
    void InsertService(const std::string &servicename, func_t f)
    {
        std::string s = prefixpath + servicename;
        _service_list[s] = f;
    }
    //服务是否存在?
    bool IsServiceExists(const std::string &servicename)
    {
        auto iter = _service_list.find(servicename);
        if(iter == _service_list.end()) return false;
        else return true;
    }
    ~HttpServer() {}

private:   
    //Content-Type
    std::unordered_map<std::string, std::string> _mime_type;
    //状态码描述
    std::unordered_map<int, std::string> _code_to_desc;
    //服务列表
    std::unordered_map<std::string, func_t> _service_list;
};

代码实现了一个简单的HTTP服务器的框架,包括请求解析、响应构建以及静态资源服务和简单动态服务的能力。以下是详细逻辑:

  1. 定义常量
    • base_sep:定义请求报头与正文之间的分隔符,即"\r\n"。
    • line_sep:定义请求报头中键值对的分隔符,即": "。
    • prefixpath:定义服务器的根目录,即"wwwroot"。
    • homepage:定义首页的文件名,即"index.html"。
    • httpversion:定义HTTP协议的版本,即"HTTP/1.0"。
    • spacesep:定义一个空格字符,用于分隔。
    • suffixsep:定义文件后缀的分隔符,即"."。
    • html_404:定义404错误页面的文件名,即"404.html"。
    • arg_sep:定义GET方法中参数的分隔符,即"?"。
  2. HttpRequest类
    • 负责解析HTTP请求。
    • 成员变量包括请求行、请求报头、空行、请求正文等。
    • Deserialize方法用于将字符串形式的请求反序列化为HttpRequest对象。
    • GetLine方法用于从请求字符串中提取一行。
    • ParseReqLineParseReqHeader方法分别用于解析请求行和请求报头。
    • 提供了一系列方法来获取请求的方法、URL、路径、后缀等。
  3. HttpResponse类
    • 负责构建HTTP响应。
    • 成员变量包括HTTP版本、状态码、状态描述、响应报头、空行、响应正文等。
    • AddCode方法用于设置响应的状态码和描述。
    • AddHeader方法用于添加响应报头。
    • AddBodyText方法用于设置响应正文。
    • Serialize方法用于将HttpResponse对象序列化为字符串形式的响应。
  4. HttpServer类
    • 负责处理HTTP请求并返回响应。
    • 成员变量包括MIME类型映射、状态码描述映射、服务列表等。
    • GetFileContent方法用于读取文件内容。
    • HandlerHttpRequest方法是处理HTTP请求的主要逻辑:
      • 如果请求路径是"wwwroot/redir",则处理重定向。
      • 如果请求带有参数,则尝试从服务列表中获取对应的处理函数并执行。
      • 否则,处理静态资源请求:读取文件内容,设置响应的状态码、报头和正文,然后返回响应。
    • InsertService方法用于向服务列表中添加服务。
    • IsServiceExists方法用于检查服务是否存在。

整体而言,这段代码实现了一个简单的HTTP服务器,能够处理静态资源请求和简单的动态服务请求,并支持重定向和Cookie的设置。


7.4.1 http的web根目录中的服务

index.html(首页)

<!DOCTYPE html>
<html>
<head>
    <title>比特(w3cschool.cn)</title>        
    <meta charset="UTF-8">
</head>
<body>
    <div id="container" style="width:800px">
    <div id="header" style="background-color:#FFA500;">
    <h1 style="margin-bottom:0;">我的网站</h1></div>
    <div id="menu" style="background-color:#FFD700;height:200px;width:100px;float:left;">
    <b>Menu</b><br>
    HTML<br>
    CSS<br>
    JavaScript</div>
    <div id="content" style="background-color:#EEEEEE;height:200px;width:700px;float:left;">
    内容就在这里</div>
    <div id="footer" style="background-color:#FFA500;clear:both;text-align:center;">
    Copyright © test.com</div>
    </div>
    <a href="/login.html">点击测试: 登陆页面</a>

    <div>
        <img src="/image/1.png" alt="一张图片">
        <!-- <img src="/image/2.jpg" alt="第二张图片"> -->
    </div>

    <div>
    </div>
</body>
</html>

404.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>(404) The page you were looking for doesn't exist.</title>
  <link rel="stylesheet" type="text/css" href="//cloud.typography.com/746852/739588/css/fonts.css" />
  <style type="text/css">
    html,
    body {
      margin: 0;
      padding: 0;
      height: 100%;
    }

    body {
      font-family: "Whitney SSm A", "Whitney SSm B", "Helvetica Neue", Helvetica, Arial, Sans-Serif;
      background-color: #2D72D9;
      color: #fff;
      -moz-font-smoothing: antialiased;
      -webkit-font-smoothing: antialiased;
    }

    .error-container {
      text-align: center;
      height: 100%;
    }

    @media (max-width: 480px) {
      .error-container {
        position: relative;
        top: 50%;
        height: initial;
        -webkit-transform: translateY(-50%);
        -ms-transform: translateY(-50%);
        transform: translateY(-50%);
      }
    }

    .error-container h1 {
      margin: 0;
      font-size: 130px;
      font-weight: 300;
    }

    @media (min-width: 480px) {
      .error-container h1 {
        position: relative;
        top: 50%;
        -webkit-transform: translateY(-50%);
        -ms-transform: translateY(-50%);
        transform: translateY(-50%);
      }
    }

    @media (min-width: 768px) {
      .error-container h1 {
        font-size: 220px;
      }
    }

    .return {
      color: rgba(255, 255, 255, 0.6);
      font-weight: 400;
      letter-spacing: -0.04em;
      margin: 0;
    }

    @media (min-width: 480px) {
      .return {
        position: absolute;
        width: 100%;
        bottom: 30px;
      }
    }

    .return a {
      padding-bottom: 1px;
      color: #fff;
      text-decoration: none;
      border-bottom: 1px solid rgba(255, 255, 255, 0.6);
      -webkit-transition: border-color 0.1s ease-in;
      transition: border-color 0.1s ease-in;
    }

    .return a:hover {
      border-bottom-color: #fff;
    }
  </style>
</head>

<body>

<div class="error-container">
  <h1>404</h1>
  <p class="return">Take me back to <a href="/">designernews.co</a></p>
</div>

</body>
</html>

content.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>内容页面</title>
</head>
<body>
    <h1>内容页面</h1>
    <a href="/register.html">进入注册页面</a>
</body>
</html>

login.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登陆页面</title>
</head>

<body>
    <h1>登陆页面</h1>
    <a href="/content.html">进入内容页面</a><br>
    <a href="/a/b/c.html">测试404</a><br>
    <a href="/redir">测试重定向</a><br>

    <div>
        <!-- 默认就是GET -->
        <form action="/login" method="POST">
            用户名: <input type="text" name="username" value="."><br>
            密码: <input type="password" name="userpasswd" value=""><br>
            <input type="submit" value="提交">
        </form>
    </div>

</body>

</html>

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注册页面</title>
</head>
<body>
    <h1>注册页面</h1>
    <a href="/">回到首页</a>
</body>
</html>

 7.5服务器启动

ServerMain.cc

#include "TcpServer.hpp"
#include "Http.hpp"

HttpResponse Login(HttpRequest &req)
{
    HttpResponse resp;
    std::cout << "外部已经拿到了参数了: "<< std::endl;
    req.GetResuestBody();
    std::cout << "####################### "<< std::endl;
    resp.AddCode(200, "OK");
    resp.AddBodyText("<html><h1>result done!</h1></html>");
    // username=helloworld&userpasswd=123456

    // 1. pipe
    // 2. dup2
    // 3. fork();
    // 4. exec* -> python, PHP, 甚至是Java!
    return resp;
}

// ./tcpserver 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);

    HttpServer hserver;
    hserver.InsertService("/login", Login);//登录服务

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        std::bind(&HttpServer::HandlerHttpRequest, &hserver, std::placeholders::_1), 
        port
    );
    tsvr->Loop();

    return 0;
}

下面是详细的逻辑解析:

  1. 程序入口(main函数)
    • 检查命令行参数。程序需要一个参数:本地端口号。
    • 如果参数数量不正确,打印使用说明并退出。
    • 将命令行参数(端口号)转换为整数。
    • 创建一个HttpServer对象,用于处理HTTP请求。
    • 通过HttpServer对象的InsertService方法,注册一个处理登录请求的服务。该服务将调用Login函数来处理/login路径的请求。
    • 创建一个TcpServer对象,绑定到之前创建的HttpServer对象上,以便处理TCP连接和请求。TcpServer对象监听指定的端口。
    • 调用TcpServer对象的Loop方法,开始监听和处理请求。
  2. 处理登录请求(Login函数)
    • 创建一个HttpResponse对象,用于构建响应。
    • 打印一条消息,表示函数已被调用。
    • 调用HttpRequest对象的GetResuestBody方法,获取请求体内容。这可能是用户提交的登录信息。
    • 设置响应的状态码为200(表示请求成功),并添加一段简单的HTML作为响应体。
    • 函数返回构建的HttpResponse对象,该对象将被发送回客户端。
  3. 代码中的注释
    • 注释提到了UNIX系统编程中的几个概念:管道(pipe)、dup2函数、fork函数和exec系列函数。这些概念通常用于进程间通信和创建新进程。尽管这些概念在代码中没有直接使用,但它们表明开发者可能考虑在将来扩展服务器功能,例如,通过创建子进程来执行其他程序(如Python、PHP脚本或Java程序)来处理请求。这里就不进行服务器功能的扩展了,针对字符串的处理不是C++所擅长的。

总的来说,这段代码实现了一个简单的HTTP服务器,它监听指定端口,并能够处理简单的登录请求,返回一个固定的HTML响应。

7.6 基于测试阐述清一些概念 

服务器启动:


使用浏览器访问首页:图片太大没加载出来

服务器显示:

  • 请求行GET /image/1.png HTTP/1.1
    • GET:表示这是一个GET请求,用于请求访问或被请求的数据。
    • /image/1.png:表示请求的资源路径,即请求服务器上的1.png图片文件。
    • HTTP/1.1:表示使用的HTTP协议版本是1.1。
  • 请求头
    • Host: 43.138.76.197:8888:指定请求的主机名和端口号,即服务器的IP地址和监听端口。
    • Connection: keep-alive:表示客户端希望与服务器的连接保持活跃,以便在同一个TCP连接上发送和接收多个HTTP请求/响应。
    • User-Agent:提供关于请求客户端的信息,包括浏览器类型、版本和操作系统。
    • Accept:指定客户端能够接收的内容类型,按优先级排序。
    • Referer:表示当前请求是从哪个页面链接过来的。
    • Accept-Encoding:指定客户端能够理解的压缩编码。
    • Accept-Language:指定客户端期望的语言。
    • Cookie:包含客户端发送给服务器的cookie信息,这里username=zhangsan可能表示用户的登录状态或身份信息。

7.6.1关于重定向:

        服务器配置了重定向规则,且当前请求的网页符合重定向条件,服务器则不会直接返回请求的资源。

        服务器通过HTTP响应状态码(如301表示永久重定向,302表示临时重定向)告知浏览器需要进行重定向,并在响应头部(Header)中设置“Location”字段,指明重定向目标网页的URL。


7.6.2 GET和POST

 这里先用postman进行测试:

浏览器默认方法是以GET请求的

        GET方法也可以向服务器发送数据。浏览器会将请求的参数附加在URL之后,以“?”分隔URL和传输数据,参数之间以“&”相连。GET一般是用来获取静态资源,也可以通过url向服务器传递参数。GET请求的数据对用户是可见的,因为它显示在浏览器的地址栏中。

  


当然我们也可以使用POST方法请求:与GET方法不同,POST方法将请求的数据放置在HTTP请求的消息体中,而不是附加在URL之后。因此,使用POST方法提交的数据对用户是不可见的,这在一定程度上提高了数据的安全性。


7.6.form表单

我们让用户取使用GET/POST,前端是要结合from表单的

        <form action="/login" method="POST">
            用户名: <input type="text" name="username" value="."><br>
            密码: <input type="password" name="userpasswd" value=""><br>
            <input type="submit" value="提交">
        </form>
  • <form> 标签用于创建一个表单,以便用户能够输入数据。
  • action="/login" 属性指定了表单数据提交到服务器的位置,即当表单提交时,数据会发送到服务器的 /login 路径。
  • method="POST" 属性指定了表单数据应该使用 HTTP 的 POST 方法发送到服务器。这意味着数据将在请求的主体中发送,而不是附加在 URL 后面。

接着我们就要构建外部方法了:这样服务器就可以接收和处理简单的动态服务请求

        通过HttpServer对象的InsertService方法,注册一个处理登录请求的服务。该服务将调用Login函数来处理/login路径的请求。

ServerMain.cc

#include "TcpServer.hpp"
#include "Http.hpp"

HttpResponse Login(HttpRequest &req)
{
    HttpResponse resp;
    std::cout << "外部已经拿到了参数了: "<< std::endl;
    req.GetResuestBody();
    std::cout << "####################### "<< std::endl;
    resp.AddCode(200, "OK");
    resp.AddBodyText("<html><h1>result done!</h1></html>");
    // username=helloworld&userpasswd=123456

    // 1. pipe
    // 2. dup2
    // 3. fork();
    // 4. exec* -> python, PHP, 甚至是Java!
    return resp;
}

// ./tcpserver 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);

    HttpServer hserver;
    hserver.InsertService("/login", Login);//登录服务

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        std::bind(&HttpServer::HandlerHttpRequest, &hserver, std::placeholders::_1), 
        port
    );
    tsvr->Loop();

    return 0;
}

这样写有什么效果呢?

        这个表单允许用户输入用户名和密码,然后点击提交按钮将数据发送到服的 /login 路径进行处理。

        当我们使用post方法进行提交,也就是输入密码提交,最终执行/login。我们提交的用户名和密码就是正文,这也说明post方法确实是通过正文向服务器传参

此次我们把方法设置成get:说明get方法确实是通过url向服务器传递参数

服务器给我一个请求,我给他应答:首先进行服务列表构建,一个方法对应一个服务,如果客户端的请求存在参数,他就会调用外部方法,将参数传给外部方法处理,放回结果。如果该方法不存在则跳转404页面。


7.6.4 百度的form表单

我们在百度搜索栏搜索hello world:我们可以看到hello world作为参数之一,传递给/search方法

hello world - 搜索icon-default.png?t=N7T8https://cn.bing.com/search?q=hello+world&form=ANNTH1&refig=66bccef910ab4eff9d687deeefe5e836&pc=U531&adppc=EdgeStart


7.6.5 Header之一:cookie

Cookie在Web开发中扮演着重要角色,其主要作用包括:

  1. 用户身份识别:帮助网站记住用户的登录状态和个性化设置,如用户偏好、语言选择等。
  2. 购物车和购买记录:跟踪用户添加到购物车中的商品和购买记录,确保购物过程的连续性和方便性。
  3. 网站分析和统计:收集匿名的用户访问数据,如访问次数、页面浏览量等,用于网站的分析和优化。
  4. 广告定向:允许广告商跟踪用户在网上的活动,从而投放更加相关和个性化的广告。
  5. 网站功能优化:记住用户选择的语言、地区等,以提供更好的用户体验。

比如我们登录b站的时候,登录过一次即使关掉浏览器再通过网页打开b站,我们的账号也是登录的,这就是因为浏览器会自动的记住Cookie


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2040551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

node速起架子

链接&#xff1a;https://pan.baidu.com/s/1NF1e75P8pNDzphO1jBUSyg 提取码&#xff1a;sf3w 下载node 安装好node -v 配置npm的全局安装路径 使用管理员身份运行命令行&#xff0c;在命令行中&#xff0c;执行如下指令&#xff1a; npm config set prefix "E:\develop\…

【网络】TCP协议通信的重要策略——滑动窗口,快重传,流量控制,拥塞控制,延时应答

目录 MSS值 滑动窗口 滑动窗口与重发机制 快重传机制 滑动窗口与流量控制 滑动窗口与拥塞控制 延时应答 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 相关文章 【网络】传输层TCP协议的报头和传输机制-CSDN博客 【网络】详解TCP协议通信时客户/服务端的状态-CSDN博…

「MyBatis」数据库相关操作2

&#x1f387;个人主页 &#x1f387;所属专栏&#xff1a;Spring &#x1f387;欢迎点赞收藏加关注哦&#xff01; #{} 和 ${} 我们前面都是采用 #{} 对参数进行赋值&#xff0c;实际上也可以用 ${} 客户端发送⼀条 SQL 给服务器后&#xff0c;大致流程如下&#xff1a; 1.…

图像识别,图片线条检测

import cv2 import numpy as np # 读取图片 img cv2.imread(1.png)# 灰度化 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 边缘检测 edges cv2.Canny(gray, 100, 200) 当某个像素点的梯度强度低于 threshold1 时&#xff0c;该像素点被认为是非边缘&#xff1b;当梯度强度…

代码随想录——无重复字串的最长子串(Leetcode hot8)

题目链接 滑动窗口&#xff08;双指针&#xff09; 思路&#xff1a; 初始化: 检查字符串的长度。如果长度为0或1&#xff0c;则直接返回长度&#xff0c;因为这样的字符串本身就是无重复的。初始化两个指针 slow 和 fast&#xff0c;分别代表当前最长无重复子字符串的起始…

Django 数据库迁移:makemigrations 和 migrate 命令详解及常见问题解决

目录 1. 问题所示2. python manage.py makemigrations3. python manage.py migrate4. 拓展 1. 问题所示 最初始的状态是遇到这个问题 由于刚开始跑python web项目&#xff0c;开源项目附带的Readme&#xff0c;个别命令不太懂&#xff0c;对此详细研究其基本知识 最终的解决方…

高清无损,尽在掌握:2024年电脑录屏新标准

随着科技的飞速发展和数字化生活的普及&#xff0c;电脑录屏已经成为了我们日常工作、学习、娱乐中不可或缺的一部分。本文将带你一起探索电脑如何录屏操作。 1.福昕REC大师 链接&#xff1a;www.foxitsoftware.cn/REC/ 这款软件的便捷性令人赞叹不已。其体积小巧&#xff0…

谷歌发布会现场尴尬瞬间:AI助手Gemini展示挑战苹果

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

软件开发者的首选:最佳Bug测试工具Top 10

本篇文章介绍了以下软件bug测试管理工具&#xff1a;PingCode、Worktile、Test360、禅道、码云Gitee、优云测试、Jira、GitHub、Axosoft、Bugzilla。 在开发过程中&#xff0c;Bug的管理往往是最让人头疼的问题之一。小问题积累起来不仅会拖延项目进度&#xff0c;还可能影响到…

Win10下载安装Mysql服务

Win10下载安装MySQL 一、官网下载MySQL 1.官网地址&#xff1a; https://www.mysql.com/ 2.在官网首页拉到最下方&#xff0c;点击MySQL Community Server&#xff1a; 3.根据个人电脑的操作系统选择&#xff0c;此处以Windows x64为例&#xff0c;选择第2个&#xff0c;点击…

Nature:7个提升科研产出的实用建议

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 一个值得思考的问题是&#xff1a;层出不穷的效率工具到底是提升还是降低了科研产出&#xff1f; 大学教授萨拉 (Sara) 描述了她典型的工作日场景&#xff1a;"…

【博客23】缤果Android_XXX调试助手模板(3款)V1.0(中级篇)

超级好用的Android_XXX调试助手模板 ( Android Studio Java) 备注: 仅模板无通信协议 开发工具: android-studio-2024.1.1.12-windows.exe 目录 一、软件概要&#xff1a; 二、软件界面&#xff1a; 1.App演示 2.其他扩展展示 2.1 自定义指令集 2.2 修改自定义指令集 …

实用技巧分享:笔记本和台式电脑传输文件!

现在&#xff0c;一个人拥有两台电脑已变得十分普遍&#xff0c;通常是一台笔记本和一台台式机的组合。它们各有优势&#xff0c;比如台式机在价格相同的情况下&#xff0c;性能超过笔记本&#xff0c;还能随意更换CPU、显卡、主板等硬件&#xff0c;且使用自由。而笔记本因其便…

JAVA web项目转客户端(nativefier)(url打包客户端)

1.环境&#xff1a; windows 2.下载 node.js 3.安装node.js;记住安装目录 4.命令行进入安装目录 5.执行语句&#xff1a; npm install nativefier –g 进行安装 6.新建空文件夹用于存放生成的客户端 7.命令行进入该文件夹 8.执行语句&#xff1a; nativefier &quo…

【秋招笔试】8.11大疆秋招(第二套)-测开岗

🍭 大家好这里是 春秋招笔试突围,一起备战大厂笔试 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 编程一对一辅导 ✨ 本系列打算持续跟新 春秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 和 手里的小花花🌸 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 🍒 本专栏已收…

谷粒商城实战笔记-190-192商城业务-检索服务-面包屑导航

文章目录 一&#xff0c;什么是面包屑导航1&#xff0c;京东商城的面包屑2&#xff0c;面包屑是怎么产生的 二&#xff0c;面包屑导航的后台实现 这三节的主要内容是开发面包屑的前后端功能。 190-商城业务-检索服务-面包屑导航191-商城业务-检索服务-条件删除与URL编码问题192…

【Verilog HDL 入门教程】 —— 学长带你学Verilog(基础篇)

文章目录 一、Verilog HDL 概述1、Verilog HDL 是什么2、Verilog HDL产生的背景3、Verilog HDL 和 VHDL的区别 二、Verilog HDL 基础知识1、Verilog HDL 语言要素1.1、命名规则1.2、注释符1.3、关键字1.4、数值1.4.1、整数及其表示1.4.2、实数及其表示1.4.3、字符串及其表示 2、…

Spire.PDF for .NET【文档操作】演示:检测 PDF 文件是否为 PDF/A

Spire.PDF 为开发人员提供了两种方法来检测 PDF 文件是否为 PDF/A。一种是使用 PdfDocument.Conformance 属性&#xff0c;另一种是使用 PdfDocument.XmpMetaData 属性。以下示例演示了如何使用这两种方法检测 PDF 文件是否为 PDF/A。 Spire.PDF for .NET 是一款独立 PDF 控件…

详细分析JWT的基本知识(附Demo)

目录 前言1. 基本知识2. JWT验证过程3. Demo 前言 对于Java的基本知识推荐阅读&#xff1a; java框架 零基础从入门到精通的学习路线 附开源项目面经等&#xff08;超全&#xff09;【Java项目】实战CRUD的功能整理&#xff08;持续更新&#xff09; 1. 基本知识 紧凑的、U…

机器学习回归分析系列2-二项回归模型

04 二项回归模型 4.1 简介 二项回归模型用于处理二元响应变量&#xff0c;即因变量是0或1的分类变量。最常见的二项回归模型是逻辑回归&#xff0c;它可以用来预测事件发生的概率。 逻辑回归模型假设&#xff1a; 其中&#xff0c;p 是事件发生的概率&#xff0c;x1,x2,…,x…