网络通讯学习(2)-----HTTP(应用层协议)通讯

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文件。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值