HTTP(应用层协议)
理论
HTTP是基于客户端/服务器的请求、响应的应用层协议:
请求:由客户端向服务器发起,指定了要从服务器获取的资源。请求包含了协议首部,指明了客户端处理能力信息,如可以处理的文件类型,支持的语言,编码方式等。
响应:服务器收到客户端的请求后,解析这个请求,构造响应,并发送给客户端。响应同样包含了协议首部,指明了服务器的相关信息。
http(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式。HTTP协议的主要特点是:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。由于HTTP协议简单,通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。类型由Content-Type加以标记。
4.无连接:即每次连接只处理一个请求,处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:无状态是指协议对于事务处理没有记忆能力。
http1.0协议默认的是非持久连接, HTTP1.1默认的连接方式为持久连接。
非持久连接:每次服务器发出一个对象后,相应的TCP连接就被关闭,也就是说每个连接都没有持续到可用于传送其他对象。每个TCP连接只用于传输一个请求消息和一个响应消息。
持久连接:服务器在发出响应后让TCP连接继续打开着。同一对客户/服务器之间的后续请求和响应可以通过这个连接发送。HTTP/1.1的默认模式使用带流水线的持久连接。
HTTP服务器与客户端实质:
1.http服务器其实就是一个socket的服务器程序
2.浏览器其实就是一个socket的客服端程序
3.它们按照http协议进行传输
HTTP协议是建立在socket之上的,本质上是两个程序通过socket相互发送数据。HTTP协议,规定了发送方发送数据的格式以及接受方如何使用接受的数据。实现HTTP服务器与客户端,HTTP协议的实现体现在双发对发送与接受数据的处理上。最简单的例子,客户端向服务器发送一个"GET 1.html"数据,服务器收到数据后,解读"GET 1.html",明白客户端想得(GET)到1.html文件,服务器将1.html文件的内容发送给客户端,客户端接收到含1.html文件内容的数据后,新建1.html文件并写入服务器端发送来的数据。
HTTP协议有自己的格式:
http请求的三部分:请求行、消息报头、请求正文。
进行http服务器开发时候核心是关注第一部分-----请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下:
Method Request-URI HTTP-Version CRLF
其中 Method表示请求方法(如POST、GET、PUT、DELETE等);
Request-URI是一个统一资源标识符;
HTTP-Version表示请求的HTTP协议版本;
CRLF表示回车和换行。
HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文
状态行格式如下:
HTTP-Version Status-Code Reason-Phrase CRLF
其中,HTTP-Version表示服务器HTTP协议的版本;
Status-Code表示服务器发回的响应状态代码;
Reason-Phrase表示状态代码的文本描述。
代码
- 主函数
int main()
{
//ws2_32.DLL初始化,调用的2.0版本
WSADATA wsa;
WSAStartup(MAKEWORD(2, 0), &wsa);
SOCKET serversoc, acceptsoc;
SOCKADDR_IN serveraddr;
SOCKADDR_IN fromaddr;
char Recv_buf[1024];
int from_len = sizeof(fromaddr);
int result;
int Recv_len;
//创建socket
/*
*socket函数调用成功,他就会返回一个新的socket数据类型的套接字描述符;
如果调用失败,这个函数返回一个INVALID_SOCKET值,错误信息可以通过WSAGetLastError函数返回.
*/
serversoc = socket(AF_INET, SOCK_STREAM, 0);
//#define INVALID_SOCKET (SOCKET)(~0)
/*
(SOCKET)(0):将0转换成SOCKET类型,0占用的存储空间长度 = sizeof(SOCKET)
(SOCKET)(~0) :把上面那个0占用的空间全部变成1。
这是为了常量的长度与系统当期使用的SOCKET类型长度匹配
*/
if (serversoc == INVALID_SOCKET)
{
printf("[Web]创建套接字失败!");
return -1;
}
//初始化服务器IP,Port
/*几个特殊的地址:
INADDR_LOOPBACK (127.0.0.1) 总是代表经由回环设备的本地主机;
INADDR_ANY (0.0.0.0) 表示任何可绑定的地址;
INADDR_BROADCAST (255.255.255.255) 表示任何主机,这与绑定为INADDR_ANY 有同样的效果.
*/
serveraddr.sin_family = AF_INET; //地址类型
serveraddr.sin_port = htons(8080); //端口号
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址
//绑定socket
result = bind(serversoc, (SOCKADDR*)&serveraddr, sizeof(serveraddr));
//#define SOCKET_ERROR (-1)
if (result == SOCKET_ERROR)
{
closesocket(serversoc);
printf("[Web]绑定套接字失败!");
return -1;
}
//监听socket请求
result = listen(serversoc, 3);
printf("[Web]服务器正在运行.....\n");
while (1)
{
//接收请求
acceptsoc = accept(serversoc,(SOCKADDR*)&fromaddr,&from_len);
if (acceptsoc == INVALID_SOCKET)
{
printf("[Web]接收请求失败!");
break;
}
//inet_ntoa:将一个网络字节序的IP地址转化为点分十进制的IP地址(字符串)。
printf("[Web]连接来自 IP: %s Port: %d \n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port));
//接收来自客户端的请求
Recv_len = recv(acceptsoc,Recv_buf,1024,0); //返回读到的字节大小
if (Recv_len == SOCKET_ERROR)
{
printf("[Web]接收数据失败!");
break;
}
Recv_buf[Recv_len] = 0;
//向客户端发送响应数据
result = http_send_response(acceptsoc, Recv_buf, Recv_len);
closesocket(acceptsoc);
}
//关闭套接字,停止DLL的使用
closesocket(serversoc);
WSACleanup();
return 0;
}
- http_send_response函数
int http_send_response(SOCKET soc, char* buf, int buf_len)
{
int read_len, file_len, hdr_len, send_len;
char* type;
char read_buf[1024];
char http_header[1024];
char file_name[256];
char suffix[16];
FILE* res_file;
//通过解析URL,得到文件名
http_parse_request_cmd(buf, buf_len, file_name, suffix);
//打开文件
res_file = fopen(file_name, "rb+");
if (res_file == NULL)
{
printf("[Web]文件:%s 不存在!\n", file_name);
return 0;
}
//计算文件大小
fseek(res_file, 0, SEEK_END);
file_len = ftell(res_file);
fseek(res_file, 0, SEEK_SET);
//获得文件content-type
type = http_get_type_by_suffix(suffix);
if (type == NULL)
{
printf("[Web]没有相关的文件类型!\n");
return 0;
}
//构造响应首部,加入文件长度,content-type信息
hdr_len = sprintf(http_header,http_res_hdr_tmpl,file_len,type);
send_len = send(soc, http_header, hdr_len, 0);
if (send_len == SOCKET_ERROR)
{
fclose(res_file);
printf("[Web]发送失败,错误:%d\n", WSAGetLastError());
return 0;
}
//发送文件
do {
read_len = fread(read_buf,sizeof(char),1024,res_file);
if (read_len > 0)
{
send_len = send(soc, read_buf, read_len, 0);
file_len -= read_len;
}
} while ((read_len > 0) && (file_len > 0));
fclose(res_file);
return 1;
}
- http_parse_request_cmd函数,用于解析客户端发送过来的请求,解析请求行,如:GET /index.html http/1.1(关键函数)
void http_parse_request_cmd(char* buf, int buflen, char* file_name, char* suffix)
{
int length = 0;
char* begin, * end, * bias;
//查找URL开始位置,method和uri之间有空格,这个空格成为定位的标记
begin = strchr(buf, ' ');//返回空格位置的指针
begin++;//指针后移,指向URI第一个字符
//查找URL结束位置,method和url之间有空格,这个空格称为定位的标记
end = strchr(begin, ' '); //返回空格位置的指针
*end = 0; //字符串结束标记
bias = strrchr(begin, '/'); //URL的开始位置
length = end - bias; //URL长度
//找到文件名开始的位置
if ((*bias == '/') || (*bias == '\\'))
{
bias++;
length--;
}
//得到客户端请求的文件名
if (length > 0)
{
memcpy(file_name, bias, length);
file_name[length] = 0;
begin = strchr(file_name, '.');
if (begin)
strcpy(suffix,begin+1);
}
}
- http_get_type_by_suffix函数,通过suffix(后缀),查找到对应的content-type
char* http_get_type_by_suffix(const char* suffix)
{
struct doc_type* type;
for (type = file_type; type->suffix; type++)
{
if (strcmp(type->suffix, suffix) == 0)
return type->type;
}
return NULL;
}
- 定义的结构体以及响应首部内容
//定义文件类型对应的content-type
struct doc_type {
char* suffix;
char* type;
};
//文件(声明一个结构体变量并初始化)
struct doc_type file_type[] = {
{"html","text/html"},
{"gif","imag/gif"},
{"jpeg","imag/jpeg"},
{NULL,NULL}
};
//响应首部内容
char* http_res_hdr_tmpl = "HTTP/1.1 200 OK \r\n"
"Accept-Ranges:bytes\r\nContent-Length:%d\r\nConnection:close\r\n"
"Content-Type:%s\r\n\r\n";
- 整体代码
#include<stdio.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib") //加载ws2_32.lib
#pragma warning(disable:4996)
//定义文件类型对应的content-type
struct doc_type {
char* suffix;
char* type;
};
//文件(声明一个结构体数组并初始化)
struct doc_type file_type[] = {
{"html","text/html"},
{"gif","imag/gif"},
{"jpeg","imag/jpeg"},
{NULL,NULL}
};
//响应首部内容
char* http_res_hdr_tmpl = "HTTP/1.1 200 OK \r\n"
"Accept-Ranges:bytes\r\nContent-Length:%d\r\nConnection:close\r\n"
"Content-Type:%s\r\n\r\n";
//通过suffix(后缀),查找到对应的content-type
char* http_get_type_by_suffix(const char* suffix)
{
struct doc_type* type;
for (type = file_type; type->suffix; type++)
{
if (strcmp(type->suffix, suffix) == 0)
return type->type;
}
return NULL;
}
//解析客户端发送过来的请求,解析请求行,如:GET /index.html http/1.1
void http_parse_request_cmd(char* buf, int buflen, char* file_name, char* suffix) //buf是客户端请求数据
{
int length = 0;
char* begin, * end, * bias;
//查找URL开始位置,method和url之间有空格,这个空格成为定位的标记。
/*
* char* strchr(const char* str, int c)
-参数
str -- 要被检索的 C 字符串。
c -- 在 str 中要搜索的字符。
-返回值
该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL。
*/
//begin = strchr(buf,'/');
begin = strchr(buf, ' ');//返回空格位置的指针
begin++;//指针后移,指向URL第一个字符
//查找URL结束位置,method和url之间有空格,这个空格称为定位的标记
end = strchr(begin, ' '); //返回空格位置的指针
*end = 0; //字符串结束标记
//‘\0’ 是 c/c++ 语言中的字符串结束符,在ASCII字符集中对应空字符NULL,数值为0。
// 其作用是识别字符串,简化字符串处理过程。在使用过程中要为其分配内存空间,但不计入字符串长度。
bias = strchr(begin, '/'); //URL的开始位置
length = end - bias; //URL长度
//找到文件名开始的位置
if ((*bias == '/') || (*bias == '\\'))
{
bias++;
length--;
}
//得到客户端请求的文件名
if (length > 0)
{
//memcpy函数
//原型:void *memcpy(void *dest, const void *src, size_t n);
//功能:从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
//注意:注意:如果目标数组destin本身已有数据,执行memcpy()后,将覆盖原有数据(最多覆盖n)。如果要追加数据,则每次执行memcpy后,要将目标数组地址增加到你要追加数据的地址
memcpy(file_name, bias, length);
file_name[length] = 0;
begin = strchr(file_name, '.');
if (begin)
strcpy(suffix,begin+1);
}
}
int http_send_response(SOCKET soc, char* buf, int buf_len)
{
int read_len, file_len, hdr_len, send_len;
char* type;
char read_buf[1024];
char http_header[1024];
char file_name[256];// = "index.html"; //文件名
char suffix[16];// = "html"; //文件后缀
FILE* res_file; //声明文件指针
//通过解析URL,得到文件名
http_parse_request_cmd(buf, buf_len, file_name, suffix); //buf是接收到的来自浏览器的请求数据
printf("filename == %s\n",file_name);
//打开文件
res_file = fopen(file_name, "rb+");
if (res_file == NULL)
{
printf("[Web]文件:%s 不存在!\n", file_name);
return 0;
}
/*
* fseek函数
* 原型:int seek(FILE *filepointer,long offset,int whence);
* 功能:将filepointer所指向的文件的读写位置指针移动到特定的位置,即将位置指针移到距离whence的offset字节处。
* 如果offset为正值,表明新的位置在whence的后面,如果offset为负值,表明新的位置在whence的前面。
* whence的常量值:
* SEEK_SET: 文件开头
* SEEK_CUR: 当前位置
* SEEK_END: 文件结尾
*/
//计算文件大小
fseek(res_file, 0, SEEK_END);
file_len = ftell(res_file); //返回当前读写位置指针的值。
fseek(res_file, 0, SEEK_SET);
//获得文件content-type
type = http_get_type_by_suffix(suffix);
if (type == NULL)
{
printf("[Web]没有相关的文件类型!\n");
return 0;
}
//构造响应首部,加入文件长度,content-type信息
/*
*char* http_res_hdr_tmpl = "HTTP/1.1 200 OK \r\n"
"Accept-Ranges:bytes\r\nContent-Length:%d\r\nConnection:close\r\n"
"Content-Type:%s\r\n\r\n";
*/
hdr_len = sprintf(http_header,http_res_hdr_tmpl,file_len,type); //把格式化的数据写入某个字符串缓冲区,返回字符串长度。
send_len = send(soc, http_header, hdr_len, 0);
if (send_len == SOCKET_ERROR)
{
fclose(res_file);
printf("[Web]发送失败,错误:%d\n", WSAGetLastError());
return 0;
}
//发送文件
do {
/*
*unsigned fread(void *ptr,unsigned size,unsigned n,FILE *filepointer);
* 功能:从filepointer所指向的文件中读取n个数据项,每个数据项的大小是size个字节,这些数据将被存放到ptr所指向的内存中。
* 返回值:成功则返回读取的数据项的个数。失败或遇到文件尾,则返回0.
*/
read_len = fread(read_buf,sizeof(char),1024,res_file); //1024字节,即1kb。1字节==8位。。就是说每次读1kb直到读完整个文件
if (read_len > 0)
{
send_len = send(soc, read_buf, read_len, 0); //soc---->acceptsoc
file_len -= read_len;
}
} while ((read_len > 0) && (file_len > 0));
printf("%s文件发送成功!\n",file_name);
fclose(res_file);
return 1;
}
int main()
{
WSADATA wsa;
WSAStartup(MAKEWORD(2, 0), &wsa);
SOCKET serversoc, acceptsoc;
SOCKADDR_IN serveraddr;
SOCKADDR_IN fromaddr;
char Recv_buf[1024];
int from_len = sizeof(fromaddr);
int result;
int Recv_len;
//创建socket
serversoc = socket(AF_INET, SOCK_STREAM, 0);
if (serversoc == INVALID_SOCKET)
{
printf("[Web]创建套接字失败!");
return -1;
}
//初始化服务器IP,Port
/*几个特殊的地址:
INADDR_LOOPBACK (127.0.0.1) 总是代表经由回环设备的本地主机;
INADDR_ANY (0.0.0.0) 表示任何可绑定的地址;
INADDR_BROADCAST (255.255.255.255) 表示任何主机,这与绑定为INADDR_ANY 有同样的效果.
*/
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8080);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定socket
result = bind(serversoc, (SOCKADDR*)&serveraddr, sizeof(serveraddr));
//#define SOCKET_ERROR (-1)
if (result == SOCKET_ERROR)
{
closesocket(serversoc);
printf("[Web]绑定套接字失败!");
return -1;
}
//监听socket请求
result = listen(serversoc, 3);
printf("[Web]服务器正在运行.....\n");
//listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。
//accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
while (1)
{
//接收请求
acceptsoc = accept(serversoc,(SOCKADDR*)&fromaddr,&from_len);
/*
* #define INVALID_SOCKET (SOCKET)(~0)
* (SOCKET)(0):将0转换成SOCKET类型,0占用的存储空间长度 = sizeof(SOCKET)
(SOCKET)(~0) :把上面那个0占用的空间全部变成1。
是为了常量的长度与系统当期使用的SOCKET类型长度匹配
*/
if (acceptsoc == INVALID_SOCKET)
{
printf("[Web]接收请求失败!");
break;
}
//inet_ntoa:将一个网络字节序的IP地址转化为点分十进制的IP地址(字符串)。
printf("[Web]连接来自 IP: %s Port: %d \n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port));
//接收来自客户端的请求
Recv_len = recv(acceptsoc,Recv_buf,1024,0); //Recv_buf为接收数据的缓冲区,返回接收数据的字节数。
if (Recv_len == SOCKET_ERROR)
{
printf("[Web]接收数据失败!");
break;
}
Recv_buf[Recv_len] = 0;
//向客户端发送响应数据
result = http_send_response(acceptsoc, Recv_buf, Recv_len);
closesocket(acceptsoc);
}
//关闭套接字,停止DLL的使用
closesocket(serversoc);
WSACleanup();
return 0;
}
测试
测试时将编译好的exe文件和index.html文件放在同一个文件夹下,随后点击exe文件,运行服务器,在浏览器输入http://localhost:8080/index.html即可接收到页面。
- 关于exe文件:
我用的是vs2022,编译后在x64–>Debug文件夹下即可找到编译好的exe文件。