C++ 进阶 day16 http服务器搭建

main.cpp

  • 确定端口号,自定义或者手动传入端口号
  • 自定义函数初始化服务器并获取套接字文件描述符
  • 循环接受客户端的连接请求,并发服务器使用多线程完成
  • 创建一个分支线程用户跟客户端进行通信
  • 将线程设置成份离态
  • 循环结束就关闭服务器(套接字文件描述符)
点击查看代码
#include <myhead.h>
#include"http.h"

//定义线程体函数
void *msg_request(void *arg)
{
    //解析到传进来的客户端的套接字文件描述符
    int sock = *(int *)arg;

    //调用信息处理函数,该函数是所有请求的入口
    handler_msg(sock);
    return NULL;
}


/*******************主程序**************************** */
int main(int argc, const char *argv[]) 
{
    int port = 8080;         //默认使用8080端口号

    //判断是否手动传入端口号
    if(argc>1)
    {
        port = atoi(argv[1]);
        //atoi:功能:将给定的字符串转变成整数
        //参数:字符串(只能包含数字字符)
        //返回值:整数      例如:“123”  ---> 123
    }

    //初始化服务器
    //自定义函数:功能完成tcp服务器的初始化操作 创建套接字、绑定端口号、监听
    //参数:端口号
    //返回值:服务器套接字
    int lis_socket = init_server(port);

    //循环接收客户端的连接请求
    //并发服务器使用多线程完成
    while(1)
    {
        struct sockaddr_in peer;  //接收客户端的地址信息结构体
        socklen_t len = sizeof(peer);    //接收长度

        //接收客户端的连接请求
        //返回的套接字就是服务器端创建处理的用于跟客户端进行通信的套接字文件描述符
        int sock = accept(lis_socket, (struct sockaddr*)&peer, &len);
        if(sock==-1)
        {
            perror("accept error");
            return -1;
        }

        printf("您有新的客户端发来连接请求了\n");

        //创建一个分支线程用户跟客户端进行通信
        pthread_t tid = -1;
        if(pthread_create(&tid, NULL, msg_request, &sock) >0)
        {
            printf("pthread_create error\n");
            return -1;
        }

        //将线程设置成分离态
        pthread_detach(tid);
    }

    //关闭服务器
    close(lis_socket);
   return 0;
}

http.cpp

点击查看代码
#include"http.h"
#include<myhead.h>

//声明初始化服务器函数
int  init_server(int _port)
{
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock == -1)
    {
        perror("socke error");
        return  -1;
    }

    //端口号快速重用
    int reuse = 1;
    if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse))==-1)
    {
        perror("setsockopt error");
        return -1;
    }

    //绑定ip和端口号
    //填充地址信息结构体
    struct sockaddr_in local; 
    local.sin_family = AF_INET;    //协议族
    local.sin_port = htons(_port);    //端口号
    local.sin_addr.s_addr = INADDR_ANY;     //表示可以接收任意主机的连接请求

    //绑定工作
    if(bind(sock, (struct sockaddr*)&local, sizeof(local))==-1)
    {
        perror("bind error");
        return -1;
    }


    //启动监听
    if(listen(sock, 128) == -1)
    {
        perror("listen error");
        return -1;
    }

    //程序执行至此,说明服务器创建成功
    return sock;

}



int get_line(int sock,char *buf){
	char ch = '\0';//读取一个字符
	int i=0;//填充变量,用于填充buf,所以从0开始
	int ret=0;//记录读取数据的返回值

	//当buf没满并且,没有读取到换行时,继续读取
	while(i<SIZE&&ch!='\n'){
		ret=recv(sock,&ch,1,0);
		if(ret>0&&ch=='\r')
		{
			//下一个字符可能是'\n'
		    int s = recv(sock,&ch,1,MSG_PEEK);
		    if(s>0&&ch=='\n'){
			    recv(sock,&ch,1,0);  //将下一个字符拿出来看看
		    }
		    else{
			    ch='\n';             //直接放回车
		    }
		}

		//程序执行至此,表示该字符需要放入字符串中
		buf[i]=ch;//将读取下来的字符串放入数组中
		i++;
	}
	buf[i]='\0';//将字符串补充完整
	return i;//将字符串中字符串实际长度返回
}


static void clear_header(int sock){
	char buf[4096]="";
	int ret=0;
	do{
		ret=get_line(sock,buf);
	}while(ret!=1&&strcmp(buf,"\n")!=0);
}

static void show_404(int sock){
	//将获取的消息头全部清除掉,上面解析数据时,只解析了请求行
	clear_header(sock);

	//组装首部信息
	const char *msg="HTTP/1.1 404 Not Found\r\n";

	//发送首部
	send(sock,msg,strlen(msg),0);
	send(sock,"\r\n",strlen("\r\n"),0);

	//打开一个html页面文档
	struct stat st;
	stat("wwwroot/404.html",&st);
	int fd=open("wwwroot/404.html",O_RDONLY);

	//定义一个文件状态结构体
	//获取wwwroot目录下的404界面信息,主要使用该信息中文件大小
	//以只读的形式打开文件
	if(fd==-1){
		perror("open 404 error");
		return;
	}
	//将整个文件发送出去
	//函数原型:ssize_t sendfile(int out_fd,int in_fd,off_t *offset,size_t count);

	//功能:将一个文件描述符中的数据发送到另一个文件描述符中
	//参数1:要被拷贝到文件描述符
	//参数2:从那刻文件中拷贝
	//参数3:从该文件的哪个位置开始拷贝
	//参数4:拷贝的文件大小
	sendfile(sock,fd,NULL,st.st_size);
	close(fd);
}

//定义处理错误信息的响应
//参数1:套接字文件描述符
//参数2:错误码
void echo_error(int sock,int err_code){
	//对错误码进行多分枝选择
	switch(err_code){
		case 403:
			break;
		case 404:
			show_404(sock);//调用展示404界面
			break;
		case 405:
			break;
		case 500:
			break;
		default:
			break;
	}
}

static int echo_www(int sock,const char *path,size_t s){
	//以只读的形式打开文件
	int fd = open(path,O_RDONLY);
	if(fd==-1){
		perror("open error");
		return -1;
	}

	const char *msg = "HTTP/1.1 200 OK\r\n";
	send(sock,msg,strlen(msg),0);
	send(sock,"\r\n",strlen("\r\n"),0);

	//将整个文件发送网页端
	if(sendfile(sock,fd,NULL,s)==-1){
		echo_error(sock,500);//返回错误界面
		return -1;
	}

	close(fd);
	return 0;
}

static int handle_request(int sock,const char *method,const char* path,const char *querry_string){
	char line[SIZE]="";//存储一行信息
	int ret=0;//接受读取的内容
	int content_len=-1;//文本长度

	//判断是什么请求
	if(strcasecmp(method,"GET")==0){
		//清空消息报头
		clear_header(sock);
	}
	else{
		//获取POST请求的参数大小
		do{
			ret=get_line(sock,line);
			if(strncasecmp(line,"content-length",14)==0){
				content_len=atoi(line+16);
			}
		}while(ret!=1&&(strcmp(line,"\n")!=0));
	}

	//获取函数传参的数据
	printf("method = %d\n",method);//方法
	printf("query_string= %s\n",querry_string);//请求内容
	printf("content_len = %d\n",content_len);//请求数据的长度

	char req_buf[4096]={0};

	//如果时POST方法,那么肯定携带请求数据,那么就需要请求把要请求的数据解析出来
	
	if(strcasecmp(method,"POST")==0){
		int len=recv(sock,req_buf,content_len,0);
		//1.文件描述符
		//2.首地址
		//3.长度
		//4.方法
		printf("len = %d\n",len);//将所需信息读取出来
		printf("req_buf = %s\n",req_buf);
	}

	//发送给客户端消息
	//发送状态码
	const char *msg="HTTP/1.1 200 OK\r\n\r\n";//回复报文响应首部
	send(sock,msg,strlen(msg),0);

	//请求交给自定义的函数来处理业务逻辑
	//parse_and_process(sock,querry_string,req_buf);


}


//用于处理客户端信息函数的定义
int handler_msg(int sock)
{
    //定义容器存储客户端发来的数据
    char del_buf[SIZE] = "";

    //读取客户端发来的请求
    //通常我们使用recv接收客户端请求时,最后一个参数如果是0表示阻塞读取,并将数据读走
    //而最后一个参数为MSG_PEEK时,仅仅指示表示查看套接字中的数据,并不读走
    recv(sock, del_buf, SIZE, MSG_PEEK);

#if 1
    //看一看客户端发来的数据
    printf("-------------------------------------------------\n");
    printf("%s\n", del_buf);
    printf("-------------------------------------------------\n");
#endif

	//接下来就是解析HTTP请求
	//获取请求行
	char buf[SIZE]="";//用来存储取下来的客户端请求信息的一行
	int count = get_line(sock,buf);
	//功能:获取套接字中一行信息
	//参数1:套接字文件描述符
	//参数2:读取一行的字符串
	//返回值:当前一行的长度

	//程序执行至此,表示buf中已经存储了请求行的信息
	//接下来需要解析请求行中的请求方法,请求nrl...
	char method[32]="";//存储请求方法的容器
	int k=0;//填充请求方法

	//遍历请求行首部-->buf
	int i = 0;
	for(i;i<count;i++){
		//找到了任意一个字符
		if(isspace(buf[i]))//如果该字符是空格,直接结束遍历
		{
			break;
		}

		method[k++]=buf[i];
	}
	method[k]='\0';
	//程序执行至此,method数组中就存储了请求方法

	//空格跳过
	while(isspace(buf[i])&&i<SIZE){
		i++;
	}
	//程序执行至此,i就纪录了buf中后面第一个非空字符串
	printf("method = %s\n",method);
	//判断请求方法是GET请求还是POST请求
	if(strcasecmp(method,"GET")!=0&&strcasecmp(method,"POST")!=0){
		printf("method error\n");
		//echo error(sock,405);向客户端回复一个错误页面
		close(sock);
		return -1;
	}

	int need_handle=0;//标识是否为POST请求
	if(strcasecmp(method,"POST")==0){
		need_handle=1;
	}

	//拿去要处理的url以及发送的数据(如果有?的话)
	char url[SIZE]="";//存储要解析的
	int t=0;//填充url字符串
	char *querry_string=NULL;//指向url中,是否有要处理的数据,如果有,则指向要处理数据的起始地址
	for(i;i<SIZE;i++)//继续向后遍历请求首部
	{
		if(isspace(buf[i])){
			break;//表示url读取结束
		}
		//此时表示buf[i]是一个url中的一个字符
		if(buf[i]=='?'){//说明请求中有数据要处理
			//将资源路径保存至url字符数组中并使用querry_string指向附加数据
			querry_string=&url[t];
			querry_string++;//表示指向问号后的字符串
			url[t]='\0';
		}
		else{
			url[t]=buf[i];//只是普通字符,直接放入url容器中
		}
		t++;//继续填充下一个url内容
	}
	url[t]='\0';//将字符串补充完整
	printf("url = %s\n",url);//
	printf("querry_string = %s\n",querry_string);//

	//程序执行至此,表示url数据已经拿下了

	//如果是GET请求,并且有附带的数据,也是需要手动处理数据的
	//例如:http:/192.168.31.245:8080/index.html?tt=234,需要进行额外处理
	if(strcasecmp(method,"GET")==0&&querry_string!=NULL){
		need_handle=1;
	}
	//我们可以把请求资源路径固定为wwwroot下的资源
	char path[SIZE]="";//确定要响应的文件路径
	sprintf(path,"wwwroot%s",url);
	//将url合成一个服务器中的路径

	//如果请求的没有的地址没有携带资源那么默认返回index.html
	if(path[strlen(path)-1]=='/'){
		strcat(path,"index.html");
	}
	printf("path = %s\n",path);

	//判断当前服务器中是否有该path
	struct stat st;//定义文件状态的结构体
	if(stat(path,&st)==-1){
		printf("can not find file\n");
		echo_error(sock,404);
		close(sock);
		return -1;
	}

	//程序执行至此,表示能够确定是否需要手动书写逻辑了
	//如果是POST请求或者是携带数据的GET请求,都需要手动书写逻辑进行处理
	if(need_handle==1){
		//handle_request(sock,method,path,querry_string);
		//调用处理请求函数
		//参数1:套接字文件描述符
		//参数2:请求方法
		//参数3:请求的路径
		//参数4:请求附带的数据
	}
	else{
		//调用清除 请求首部剩余的内容
		clear_header(sock);

		//此时表示是GET请求,并且没有附加数据,则直接返回请求的界面即可
		echo_www(sock,path,st.st_size);
	}

	//表示所有请求都正常处理完了
	close(sock);
	return 0;
}

http.h

点击查看代码
#ifndef HTTP_H
#define HTTP_H

#define SIZE 4096
//声明初始化服务器函数
int init_server(int _port);

//声明消息处理函数,用于处理客户端发来的消息
int handler_msg(int sock);

int get_line(int sock,char *buf);
#endif

页面

404.html

点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>404 Nod Found</title>
</head>
<body>
	<h1>404 Not Found</h1>
</body>
</html>

index.html

点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width,initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<h1>主页 GET 请求示例</h1>
	<h1>欢迎来到星球部落</h1>
</body>
</html>

post.html

点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>POST 请求测试</title>
</head>
<body>
	data1:<input type="text" id="data1" value="1"><br>
	data2:<input type="text" id="data2" value="2"><br>
	求和:<input type="text" id="sum" placeholder="在此处显示和"><br><br>
	<button onclick="sendPOSTequest()">求和</button>
</body>
</html>

补充

  • windows浏览器无法返回信息,可以改用liunx的firefox
  • 编译运行.html,可以安装Live Server插件

posted @ 2025-06-05 22:28  北燃  阅读(114)  评论(0)    收藏  举报