目录
应用层
程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
再谈 "协议"
协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
这个"结构化的数据"是什么呢?比如我们在用聊天软件聊天时,发送的消息只有文字吗?当然不是,我们发送的数据中会包含:头像,昵称,时间和内容等,它们是一个整体。
"结构化的数据"在发送到网络中的时候,先序列化在发送,收到的数据一定是序列字节流,要先进行反序列化,然后才能使用。
网络版计算器
实现一个服务器版的计算器. 我们需要客户端把要计算的两个数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
在原有的tcp网络通信的基础上,添加了几个组件:
- 处理计算任务
- 读取一个完整的请求。因为tcp是面向字节流的,所以怎样读取一个完整的请求也需要我们自己处理;
- 自定义协议报头,定制接口用于添加和删除报头;
- 将结构化的数据进行序列化和反序列化;(自己实现和使用工具)
calServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include "log.hpp"
#include "Protocol.hpp"
namespace Server
{
// using namespace std;
static const uint16_t gport = 8080;
static const int gbacklog = 5;
// const Request& req 输入性参数 | Response& resp 输出型参数
typedef function<bool(const Request& req,Response& resp)> func_t;
void handlerEnter(int sock,func_t func)
{
string inbuffer;
while(true)
{
// 1.读取数据 "content_len"\r\n"x op y"\r\n
// 1.1保证读到的是完整的请求
string req_text,req_str;
// 1.2 到这 req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n
if(!recvPackage(sock,inbuffer,&req_text)) //读取一个完整的请求
return;
// cout << "带报头的请求:\n" << req_text << endl;
if(!deLength(req_text,&req_str)) //去掉报头
return;
// cout << "去掉报头的正文:" << req_str << endl;
// 2.反序列化
// 2.1得到一个结构化的对象
Request req;
if(!req.deserialize(req_str))
return;
// 3.进行处理
// 3.1得到一个结构化的响应
Response resp;
func(req,resp); //req的处理结果,全部放到了resp中
// 4.对响应的Response 进行序列化
// 4.1得到一个 "字符串"
string resp_str;
resp.serialize(&resp_str);
// cout << "计算完成,对响应进行序列化:" << resp_str << endl;
// 5.发送响应
// 5.1 构建一个完整的报文
string send_string = enLength(resp_str);
// cout << "构建完整的报文:\n" << send_string << endl;
send(sock,send_string.c_str(),send_string.size(),0); //发送
}
}
class calServer
{
public:
calServer(const uint16_t& port = gport)
:_listensock(-1),_port(port)
{}
void initServer()
{
//1.创建socket文件套接字对象
_listensock = socket(AF_INET,SOCK_STREAM,0);
if(_listensock < 0)
{
logMessage(FATAL,"create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL,"create socket success: %d",_listensock);
// 2.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;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
logMessage(FATAL,"bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind socket success");
//3. 设置socket为监听状态
if(listen(_listensock,gbacklog) < 0)
{
logMessage(FATAL,"listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen socket success");
}
void start(func_t func)
{
for( ; ; ) //死循环
{
//4. Server获取新链接
//sock 是用于和client进行通信的
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&peer_len);
if(sock < 0)
{
logMessage(ERROR,"accept error,next");
continue;
}
logMessage(NORMAL,"accept a new link success sock:%d",sock);
//5. 未来通信就用这个sock,面向字节流的,后面全是文件操作
// //version 2.1 多进程版
pid_t id = fork();
if(id == 0) //子进程
{
close(_listensock);
if(fork() > 0) exit(0); //让孙子进程执行代码,子进程退出被父进程回收,孙子进程会变成孤儿进程
// serviceIO(sock);
handlerEnter(sock,func);
close(sock);
exit(0);
}
//父进程
pid_t ret = waitpid(id,nullptr,0);
close(sock);
if(ret > 0)
{
logMessage(NORMAL,"wait child success");
}
// //------------------------------------------------------------------------------------
// //version 2.2 多进程版 信号方式
// signal(SIGCHLD,SIG_IGN); //信号忽略,忽略对子进程的管理
// pid_t id = fork();
// if(id == 0) //子进程
// {
// close(_listensock);
// serviceIO(sock);
// close(sock);
// exit(0);
// }
// close(sock);
}
}
~calServer()
{}
private:
int _listensock; //不是用来数据通信的,它是监听链接是否到来的,用于获取新链接的
uint16_t _port;
} ;
}//namespace end Server
calServer.cc
#include "calServer.hpp"
#include <memory>
using namespace Server;
// 提示 运行格式
static void Usage(string proc)
{
cout<<"Usage:\n\t" << proc <<" local_port\n\n";
}
// 计算客户端发来的任务
// req:里面是一个处理好的完整的请求对象
// resp:根据req进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等细节,在这之前已经处理好了
bool cal(const Request& req,Response& resp)
{
// req 里已经有结构化的数据了,直接使用即可
resp._exitcode = OK;
resp._result = OK;
switch(req._op)
{
case '+':
resp._result = req._x + req._y;
break;
case '-':
resp._result = req._x - req._y;
break;
case '*':
resp._result = req._x * req._y;
break;
case '/':
{
if(req._y == 0)
resp._exitcode = DEV_ZERO;
else
resp._result = req._x / req._y;
}
break;
case '%':
{
if(req._y == 0)
resp._exitcode = MOD_ZERO;
else
resp._result = req._x % req._y;
}
break;
default:
resp._exitcode = OP_ERROR;
break;
}
return true;
}
// ./calServer local_port
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<calServer> tsvr(new calServer(port));
tsvr->initServer();
tsvr->start(cal);
return 0;
}
Protocol.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
#define SEP " "
#define SEP_LEN strlen(SEP) //不能使用sizeof
#define LINE_SEP "\r\n"
#define LINE_SEP_LIN strlen(LINE_SEP) //不能使用sizeof
enum{ OK = 0,DEV_ZERO,MOD_ZERO,OP_ERROR};
//定制协议,添加报头
// "x op y" -> "content_len"\r\n"x op y"\r\n
string enLength(const string& text)
{
string send_str = to_string(text.size());
send_str += LINE_SEP;
send_str += text;
send_str += LINE_SEP;
return send_str;
}
//定制协议,去掉报头
// "content_len"\r\n"exitcode result"\r\n -> "exitcode result"
bool deLength(const string& package,string* text)
{
auto pos = package.find(LINE_SEP);
if(pos == string::npos)
return false;
string text_len_str = package.substr(0,pos);
int text_len = stoi(text_len_str);
*text = package.substr(pos+LINE_SEP_LIN,text_len);
return true;
}
// 读取一个完整的请求
// "content_len"\r\n"x op y"\r\n
bool recvPackage(int sock,string& inbuffer,string* text)
{
char buffer[1024];
while(true)
{
ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0);
if(n > 0)
{
buffer[n] = 0;
inbuffer += buffer;
// 分析处理
auto pos = inbuffer.find(LINE_SEP);
if(pos == string::npos) //一个请求都不完整,继续读取
continue;
string text_len_str = inbuffer.substr(0,pos);
int text_len = stoi(text_len_str);
// text_len_str.size() + 2*LINE_SEP_LIN +text_len <= inbuffer.size() 证明至少有一个完整的请求
int total_len = text_len_str.size() + 2*LINE_SEP_LIN +text_len;
if(inbuffer.size() < total_len)//一个请求都不完整,继续读取
continue;
// cout << "inbuffer处理前: \n" << inbuffer << endl;
// 至少有一个完整的请求
*text = inbuffer.substr(0,total_len);
inbuffer.erase(0,total_len);
// cout << "inbuffer处理后: \n" << inbuffer << endl;
break;
}
else return false;
}
return true;
}
//由于是阻塞式读取,所以这种方法暂时用不了
// bool recvRequestAll(int sock,vector<string>* out)
// {
// string line;
// while(recvRequest(sock,&line))
// out->push_back(line);
// }
class Request
{
public:
Request()
:_x(0),_y(0),_op(0)
{}
Request(int x,int y,char op)
:_x(x),_y(y),_op(op)
{}
//序列化,通过条件编译可以随意切换,是用自己写的,还是使用工具
bool serialize(string* out)
{
#ifdef MYSELF
//结构化数据 -> "x op y"
*out = "";
string x_str = to_string(_x);
string y_str = to_string(_y);
*out = x_str;
*out += SEP;
*out += _op;
*out += SEP;
*out += y_str;
#else //使用Json工具
Json::Value root;
root["first"] = _x;
root["second"] = _y;
root["oper"] = _op;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
//反序列化
bool deserialize(const string& in)
{
//"x op y" -> 结构化数据
#ifdef MYSELF
auto left = in.find(SEP);
auto right = in.rfind(SEP);
if(left == string::npos || right == string:: npos || left == right)
return false;
if(right- (left + SEP_LEN) != 1)
return false;
string x_str = in.substr(0,left);
string y_str = in.substr(right+SEP_LEN);
if(x_str.empty() || y_str.empty())
return false;
_x = stoi(x_str);
_y = stoi(y_str);
_op = in[left+SEP_LEN];
#else
Json::Value root;
Json::Reader reader;
reader.parse(in,root);
_x = root["first"].asInt();
_y = root["second"].asInt();
_op = root["oper"].asInt();
#endif
return true;
}
public:
// "x op y"
int _x;
int _y;
char _op;
};
class Response
{
public:
Response()
:_exitcode(0),_result(0)
{}
Response(const int exitcode,int result)
:_exitcode(exitcode),_result(result)
{}
//自己写
//序列化
bool serialize(string* out)
{
#ifdef MYSELF
*out = "";
string ec_str = to_string(_exitcode);
string re_str = to_string(_result);
*out = ec_str;
*out += SEP;
*out += re_str;
#else
Json::Value root;
root["exitcode"] = _exitcode;
root["result"] = _result;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
//反序列化
bool deserialize(const string& in)
{
#ifdef MYSELF
// "exitcode result"
auto pos = in.find(SEP);
if(pos == string::npos)
return false;
string ec_str = in.substr(0,pos);
string re_str = in.substr(pos + SEP_LEN);
if(ec_str.empty() || re_str.empty())
return false;
_exitcode = stoi(ec_str);
_result = stoi(re_str);
#else
Json::Value root;
Json::Reader reader;
reader.parse(in,root);
_exitcode = root["exitcode"].asInt();
_result = root["result"].asInt();
#endif
return true;
}
public:
int _exitcode; // 0:表示计算成功 !0:表示计算失败,具体的是多少会有一个标准
int _result; //计算结果
};
calClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp" //日志信息
#include "Protocol.hpp"
#define NUM 1024
namespace Client
{
// using namespace std;
class calClient
{
public:
calClient(const string& serverip,const uint16_t serverport)
:_sock(-1),_serverip(serverip),_serverport(serverport)
{}
void ininClinet()
{
//1.创建socket
_sock = socket(AF_INET,SOCK_STREAM,0);
if(_sock < 0)
{
logMessage(FATAL,"socket create error");//错误日志信息
exit(2);
}
//2.tcp客户端也要bind,但是不用我们显示的bind,OS会帮我们bind的
}
void start()
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
//向服务器发起链接请求
if(connect(_sock,(struct sockaddr*)&server,sizeof(server)) != 0)
{
logMessage(ERROR,"socket connect error");
}
else
{
string msg;
string inbuffer;
while(true)
{
cout << "Mycal>> ";
getline(cin,msg); //cin>> 1+1
//准备计算任务,
Request req = ParseLine(msg); //计算任务结构化
string content;
if(!req.serialize(&content)) continue; //"1 + 1" 序列化
string send_string = enLength(content); //"content_len"\r\n"x op y"\r\n 添加报头
// 发送计算任务
send(_sock,send_string.c_str(),send_string.size(),0);
// 获取计算结果
string package,text;
if(!recvPackage(_sock,inbuffer,&package)) // 获取一个完整的响应"content_len"\r\n"exitcode result"\r\n
continue;
if(!deLength(package,&text)) //"exitcode result" 去报头
continue;
Response resp;
resp.deserialize(text); //反序列化
// 输出计算结果
cout << "exitcode: " <<resp._exitcode << endl;
cout << "result: " <<resp._result << endl << endl;
}
}
}
// 简易版本的状态机,用于拆分输入的计算字符串
Request ParseLine(const string& line)
{
// "1+1" "10*10" "1234/0"
int status = 0;
int i = 0,cnt = line.size();
string x,y;
char op;
while(i < cnt)
{
switch(status)
{
case 0:
{
if(!isdigit(line[i]))
{
op = line[i];
status = 1;
}
else
x.push_back(line[i++]);
}
break;
case 1:
{
i++;
status = 2;
}
break;
case 2:
y.push_back(line[i++]);
break;
}
}
return Request(stoi(x),stoi(y),op);
}
~calClient()
{
if(_sock >= 0) close(_sock);//可写,可不写
}
private:
int _sock;
string _serverip;
uint16_t _serverport;
};
}//namespace Client end
calClient.cc
#include "calClient.hpp"
#include <memory>
using namespace Client;
// 提示 运行格式
static void Usage(string proc)
{
cout<<"Usage:\n\t" << proc <<" server_ip server_port\n\n";
}
// ./calClient server_ip_ip server_port
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<calClient> tpcl(new calClient(serverip, serverport));
tpcl->ininClinet();
tpcl->start();
return 0;
}
还有一些日志信息和执行指令请看完整代码:lesson13/1_ · 晚风不及你的笑/MyCodeStorehouse - 码云 - 开源中国 (gitee.com)
操作演示:
只要能够保证, 客户端发送时构造的数据, 在服务端能够正确的进行解析, 就是ok的。 这种约定, 就是 应用层协议。
HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的。但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用:HTTP(超文本传输协议)就是其中之一。
认识URL
平时我们俗称的 "网址" 其实就是说的 URL。
我们的上网行为无非两种:1.把网络上的资源拉到本地做显示或使用,2.把本地资源上传或提交到网络中。
http的作用就是找到服务器IP地址、通过端口号和文件路径、把对应的资源返回给客户端。
说白了http就是一个文件传输的协议,它能从服务器上拿到对应的 “资源”,像日常上网中,你看到的图片、视频、文字、音频等这些都属于对应的资源,一切你在网络中看到的都是资源,我们把这些资源看作是资源文件,它存储在服务器的磁盘上。有了对应的路径结构我们就能在网络中从服务器上拿到该资源,又因为文件资源种类特别多,http都能搞定,所以http叫超文本传输协议。
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
urldecode就是urlencode的逆过程;
编码解码工具:url解码在线,在线url编码解码工具,urldecode在线解码-站长工具 (senlt.cn)
HTTP协议格式
HTTP请求
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
HTTP响应
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.
这里有两个问题需要回答:
1. 怎么保证应用层读取的是一个完整的请求或响应呢?
首先不管是请求还是响应,空行之前的内容都是以行为单位的,可以设计接口读取完整的一行,再循环读取请求行和请求报头,一直读取到空行才结束。那么正文怎么读呢?总不能还按行读取吧。我们只要能保证把报头读完,在报头中有一个属性Content-Length标识正文的长度,通过解析出来的长度,在读取正文即可,这样整个求情或响应就读取完毕了。
2. 请求和响应是怎么做到序列化和反序列化的?
是http自己实现的,第一行的内容+请求/响应报头,只需要按照\r\n的方式将字符串由第一行到第n行即可。正文不需要处理。
HTTP的方法
其中最常用的就是GET方法和POST方法。GET通过url传递参数,POST通过http请求的正文提交参数,POST方法通过正文提交参数,所以一般用户看不到,私密性会更好,但是这里私密性不代表安全性。无论GET和POST这两种方法都不安全,要谈安全必须加密,要安全就得用https。
HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(BadGateway)
HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
最简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 "hello world"; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到;
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
namespace Server
{
// using namespace std;
enum { USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t = function<bool(const HttpRequest&, HttpResponse& )>;
class HttpServer
{
public:
HttpServer(func_t func,const uint16_t& port = gport)
:_func(func),_listensock(-1),_port(port)
{}
void initServer()
{
//1.创建socket文件套接字对象
_listensock = socket(AF_INET,SOCK_STREAM,0);
if(_listensock < 0)
{
exit(SOCKET_ERR);
}
// 2.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;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
exit(BIND_ERR);
}
//3. 设置socket为监听状态
if(listen(_listensock,gbacklog) < 0)
{
exit(LISTEN_ERR);
}
}
void handlerHttp(int sock)
{
// 1. 读取一个完整的http请求
// 2. 序列化
// 3. 回调
// 4. resp 序列化
// 5. 发送
char buffer[4096];
HttpRequest req;
HttpResponse resp;
ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0); //大概率能直接读到一个完整的请求
if(n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
_func(req,resp); //req -> resp
send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);
}
}
void start()
{
for( ; ; ) //死循环
{
//4. Server获取新链接
//sock 是用于和client进行通信的
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&peer_len);
if(sock < 0)
{
continue;
}
//5. 未来通信就用这个sock,面向字节流的,后面全是文件操作
// //version 2.1 多进程版
pid_t id = fork();
if(id == 0) //子进程
{
close(_listensock);
if(fork() > 0) exit(0); //让孙子进程执行代码,子进程退出被父进程回收,孙子进程会变成孤儿进程
handlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
//父进程
waitpid(id,nullptr,0);
}
}
~HttpServer()
{}
private:
int _listensock; //不是用来数据通信的,它是监听链接是否到来的,用于获取新链接的
uint16_t _port;
func_t _func;
} ;
}//namespace end Server
HttpServer.cc
#include "HttpServer.hpp"
#include <memory>
using namespace Server;
static void Usage(string argv)
{
cerr << "Usage: \n\t" << argv <<" port \n\n" << endl;
}
bool Get(const HttpRequest& req, HttpResponse& resp)
{
cout<< "----------------------http start------------------------------------" << endl;
cout << req.inbuffer << endl;//请求
cout<< "----------------------http end------------------------------------" << endl;
//响应
string respline = "Http/1.1 200 OK\r\n";
string respheader = "Content-Type:text/html\r\n";
string respblank = "\r\n";
string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>你好呀</p></body></html>";
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
return true;
}
// ./httpserver 8080
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
unique_ptr<HttpServer> httpsvr(new HttpServer(Get,port));
httpsvr->initServer();
httpsvr->start();
return 0;
}
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
const string sep = "\r\n";
using namespace std;
class HttpRequest
{
public:
HttpRequest(){}
~HttpRequest(){}
public:
string inbuffer;
};
class HttpResponse
{
public:
string outbuffer;
};
编译, 启动服务. 在浏览器中输入 [ip]:[port], 就能看到显示的结果 "Hello World"
此处我们使用 8080 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口,但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号。
基于网页的简陋版http服务器:
拓展知识(了解)
长链接
我们看到的网页,实际上可能由多种元素组成,文字、图片、视频等,那么一张网页就需要多次http请求。
http网页中可能包含多个元素,由于http是基于tcp,tcp是面向连接的,如果频繁发起http请求,就会出现频繁创建链接的问题。解决这个问题需要客户端和服务器都支持长连接,建立好一条链接,获取大份资源的时候,通过类似串行化的方式在一条链接上完成。
http周边会话保持
会话保持严格意义来讲不是http天然具备的,是后面使用的时候发现需要的。比如,某个视频网页上登陆自己的账号,多开几个网页,或者关闭网页后重新开启,我们发现网页自动帮我们登录了账号。
http协议是无状态的,也就是说同一个图片资源,我们多次刷新,http每一次都会提交一个请求,这样会导致访问变慢。因为用户查看新的网页是常规操作,如果发生网页跳转,新的页面也就无法识别用户了,但是为了让用户一次登陆,多次访问不受限制,有一个会话保持功能,是浏览器实现的,也就是它会将我们输入的账号密码保存起来,往后我们只要访问同一个网站,浏览器会自动推送历史保留信息。这种我们称为cookie,cookie分为文件级别和内存级别。
基本工具(http)
piostman 不是抓包工具,主要是用来模拟客户端(浏览器)行为的,相当于自己就是一个客户端。
fiddler 是一个抓包工具,是用来抓http的,且只能是抓本地的。主要用于调试,他相当于一个中间代理,它抓到客户端的请求,经过处理再次发给服务器,服务器也会把内容先返回给它,再由它返回给客户端。