目录
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表单
1.HTTP 协议
虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现
成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其
中之一。
在互联网世界中, HTTP(HyperText Transfer Protocol, 超文本传输协议) 是一个至
关重要的协议。 它定义了客户端(如浏览器) 与服务器之间如何通信, 以交换或传输
超文本(如 HTML 文档) 。
HTTP 协议是客户端与服务器之间通信的基础。 客户端通过 HTTP 协议向服务器发送
请求, 服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、 无状态的协
议, 即每次请求都需要建立新的连接, 且服务器不会保存客户端的状态信息。
2.认识 URL
平时我们俗称的 "网址" 其实就是说的 URL
URL通常由以下几部分组成:
- 协议(Scheme):指定了访问资源所使用的网络协议,如
http
、https
、ftp
等。协议部分以“://”为分隔符。- 用户名和密码(可选):格式为“用户名:密码@”,用于需要认证的场合,但现代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 的长度;
请求行:位于最上方,由请求方法(如GET或POST)、一个空格、统一资源标识符(URI,表示请求的资源)、另一个空格、HTTP协议版本(如HTTP/1.1)以及一个换行符组成。请求方法指明了对资源执行的操作类型,URI则指定了资源的具体位置。
请求报头:紧随请求行之后,由多个键值对组成,每个键值对之间通过换行符分隔。每个键值对以“Key: Value”的形式出现,其中Key是报头的名称,Value是对应的值,二者之间用冒号和空格分隔。请求报头包含了关于请求的各种元信息,如内容类型、认证信息等。
空行:在请求报头之后,有一个单独的空行,仅包含一个换行符。这个空行用于明确分隔请求报头和请求正文,使得HTTP请求的结构更加清晰。
请求正文:位于空行之后,是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
状态码 含义 应用样例 100 Continue 上传大文件时, 服务器告诉客户端可以
继续上传200 OK 访问网站首页, 服务器返回网页内容 201 Created 发布新文章, 服务器返回文章创建成功
的信息204 No Content 删除文章后, 服务器返回“无内容”表示操
作成功301 Moved
Permanently网站换域名后, 自动跳转到新域名; 搜
索引擎更新网站链接时使用302 Found 或 See
Other用户登录成功后, 重定向到用户首页 304 Not Modified 浏览器缓存机制, 对未修改的资源返回
304 状态码400 Bad Request 填写表单时, 格式不正确导致提交失败 401 Unauthorized 访问需要登录的页面时, 未登录或认证
失败403 Forbidden 尝试访问你没有权限查看的页面 404 Not Found 访问不存在的网页链接 500 Internal Server
Error服务器崩溃或数据库错误导致页面无法
加载502 Bad Gateway 使用代理服务器时, 代理服务器无法从
上游服务器获取有效响应503 Service
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.8Accept
Encoding客户端支持的数
据压缩格式Accept-Encoding: gzip, deflate, br
Accept
Language客户端可接受的
语言类型Accept-Language: zh
CN,zh;q=0.9,en;q=0.8Host 请求的主机名和
端口号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.36Cookie 客户端发送给服
务器的 HTTP
cookie 信息Cookie: session_id=abcdefg12345;
user_id=123Referer 请求的来源 URL Referer:
http://www.example.com/previous_pag
e.htmlContent-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=3600Connection 请求完后是关闭
还是保持连接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 GMTETag 资源的唯一标识
符, 用于缓存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 2const 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){// 加过滤逻辑 --- TODOLockGuard 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 addrif (::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
代码逻辑:
- 命名空间和类定义:
- 定义了一个命名空间
socket_ns
,用于封装Socket相关的类和函数。- 定义了一个基类
Socket
,它是一个抽象类,提供了Socket操作的基本接口,如创建、绑定、监听、接收连接、发送和接收数据等。- 定义了一个派生类
TcpSocket
,它继承自Socket
类,并实现了所有虚函数,提供了TCP Socket的具体实现。- Socket基类:
- 定义了多个纯虚函数,包括创建Socket、绑定、监听、接受连接、连接服务器、获取文件描述符、关闭Socket、接收和发送数据等。
- 提供了一个构建监听Socket的成员函数
BuildListenSocket
,它依次调用创建Socket、绑定和监听函数来初始化监听Socket。- 提供了一个构建客户端Socket的成员函数
BuildClientSocket
,它调用创建Socket和连接服务器函数来初始化客户端Socket。- TcpSocket类:
- 实现了
Socket
类中的所有纯虚函数,提供了TCP Socket的具体实现。- 在构造函数中,可以初始化一个已存在的文件描述符,或者通过调用
CreateSocketOrDie
函数创建一个新的Socket文件描述符。CreateSocketOrDie
函数用于创建一个新的Socket文件描述符。CreateBindOrDie
函数用于将Socket绑定到一个指定的端口上。CreateListenOrDie
函数用于将Socket设置为监听模式,以便接受连接。Accepter
函数用于接受一个新的连接,并返回一个表示该连接的TcpSocket
对象。Conntecor
函数用于连接到一个指定的服务器。Sockfd
函数用于获取Socket的文件描述符。Close
函数用于关闭Socket。Recv
函数用于从Socket接收数据。Send
函数用于向Socket发送数据。- 日志和错误处理:
- 使用了自定义的日志系统(
log_ns
命名空间中的LOG
宏)来记录日志和错误信息。- 在发生错误时,使用
exit
函数终止程序,并传递一个错误码。- 内存管理:
- 使用了智能指针(
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问题 };
代码逻辑:
- 初始化与监听:
- 构造函数中,服务器通过调用
_listensock->BuildListenSocket(_port);
创建并监听指定端口上的 TCP 套接字。_listensock
是一个智能指针,指向TcpSocket
对象,它负责管理监听套接字。- 事件循环:
Loop()
方法是服务器的主循环,它持续运行直到_isrunning
标志被设置为false
。- 在循环中,服务器使用
_listensock
的Accepter
方法等待并接受客户端的连接请求。- 一旦接受到新的连接,服务器会打印客户端的信息和套接字文件描述符,然后为每个新连接创建一个新线程来处理。
- 线程处理:
- 对于每个新的连接,服务器创建一个
ThreadData
对象,其中包含套接字、服务器实例指针和客户端地址信息。- 使用
pthread_create
创建一个新线程,线程执行Execute
静态成员函数。Execute
函数中,线程首先分离自身,然后调用_service
回调函数来处理客户端的连接,传入套接字和客户端地址作为参数。- 处理完成后,关闭套接字并删除
ThreadData
对象。- 回调函数:
_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\ndatareturn 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 /registerauto 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.pngauto 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_sepHttpRequest() : _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 TESTstd::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-Typestd::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服务器的框架,包括请求解析、响应构建以及静态资源服务和简单动态服务的能力。以下是详细逻辑:
- 定义常量:
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方法中参数的分隔符,即"?"。- HttpRequest类:
- 负责解析HTTP请求。
- 成员变量包括请求行、请求报头、空行、请求正文等。
Deserialize
方法用于将字符串形式的请求反序列化为HttpRequest对象。GetLine
方法用于从请求字符串中提取一行。ParseReqLine
和ParseReqHeader
方法分别用于解析请求行和请求报头。- 提供了一系列方法来获取请求的方法、URL、路径、后缀等。
- HttpResponse类:
- 负责构建HTTP响应。
- 成员变量包括HTTP版本、状态码、状态描述、响应报头、空行、响应正文等。
AddCode
方法用于设置响应的状态码和描述。AddHeader
方法用于添加响应报头。AddBodyText
方法用于设置响应正文。Serialize
方法用于将HttpResponse对象序列化为字符串形式的响应。- 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; }
下面是详细的逻辑解析:
- 程序入口(
main
函数):
- 检查命令行参数。程序需要一个参数:本地端口号。
- 如果参数数量不正确,打印使用说明并退出。
- 将命令行参数(端口号)转换为整数。
- 创建一个
HttpServer
对象,用于处理HTTP请求。- 通过
HttpServer
对象的InsertService
方法,注册一个处理登录请求的服务。该服务将调用Login
函数来处理/login
路径的请求。- 创建一个
TcpServer
对象,绑定到之前创建的HttpServer
对象上,以便处理TCP连接和请求。TcpServer
对象监听指定的端口。- 调用
TcpServer
对象的Loop
方法,开始监听和处理请求。- 处理登录请求(
Login
函数):
- 创建一个
HttpResponse
对象,用于构建响应。- 打印一条消息,表示函数已被调用。
- 调用
HttpRequest
对象的GetResuestBody
方法,获取请求体内容。这可能是用户提交的登录信息。- 设置响应的状态码为200(表示请求成功),并添加一段简单的HTML作为响应体。
- 函数返回构建的
HttpResponse
对象,该对象将被发送回客户端。- 代码中的注释:
- 注释提到了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 - 搜索https://cn.bing.com/search?q=hello+world&form=ANNTH1&refig=66bccef910ab4eff9d687deeefe5e836&pc=U531&adppc=EdgeStart
7.6.5 Header之一:cookie
Cookie在Web开发中扮演着重要角色,其主要作用包括:
- 用户身份识别:帮助网站记住用户的登录状态和个性化设置,如用户偏好、语言选择等。
- 购物车和购买记录:跟踪用户添加到购物车中的商品和购买记录,确保购物过程的连续性和方便性。
- 网站分析和统计:收集匿名的用户访问数据,如访问次数、页面浏览量等,用于网站的分析和优化。
- 广告定向:允许广告商跟踪用户在网上的活动,从而投放更加相关和个性化的广告。
- 网站功能优化:记住用户选择的语言、地区等,以提供更好的用户体验。
比如我们登录b站的时候,登录过一次即使关掉浏览器再通过网页打开b站,我们的账号也是登录的,这就是因为浏览器会自动的记住Cookie