Linux - 实现HTTP服务器和客户端

项目功能:
(1)能接收客户端的GET请求;

(2)能够解析客户端的请求报文,根据客户端要求找到相应的资源;

(2)能够回复http应答报文;

(3)能够读取服务器中存储的文件,并返回给请求客户端,实现对外发布静态资源;

(4)使用I/O复用来提高处理请求的并发度;

(5)服务器端支持错误处理,如要访问的资源不存在时回复404错误等。

 

1. HTTP协议格式

1.1 HTTP请求格式

客户端有get请求和post请求

过程:浏览器->发给->服务器,客户端(浏览器)发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成

步骤:

请求行:说明请求类型,要访问的资源,以及使用的 http 版本

请求头:说明服务器要使用的附加信息,每一行都需要 \r\n 表示某一个属性结束

空 行:必须!,即使没有请求数据,其实就是  \r\n

请求数据:也叫主体,可以添加任意的其他数据,是客户端需要的数据,由服务器发送

注意:在连续读取http请求头部时,如果碰到两个连续的回车换行,即表示请求头部结束

浏览器请求的样例:

 
GET /demo.html HTTP/1.1
Host: 47.100.162.191
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 Safari/537.36 Core/1.63.6788.400 QQBrowser/10.3.2767.400
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie:cna=BT0+EoVi1FACAXH3Nv5I7h6k;isg=BIOD99I03BNYvZDfE2FJUOsMB0ftUBZcBFi4E7VgYOJZdKOWPcvRinAl6kSfVG8y

 

1.2 HTTP响应格式

服务器获取浏览器的状态消息

过程:服务器->发给->浏览器

步骤:

  • 状态行: 包括 http 协议版本号,状态码,状态信息

  • 消息报头: 说明客户端要使用的一些附加信息

  • 空 行:必须!

  • 响应正文:服务器返回给客户端的文本信息

 

服务器响应的样例:

 
HTTP/1.0 200 OK
Server: Martin Server
Content-Type: text/html
Connection: Close
Content-Length: 526
 
<html lang="zh-CN">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>This is a test</title>

一些响应代号及代号描述

 

2. 简单的http服务器

步骤:

1.创建用于连接的服务器socket套接字

2.处理客户端连接请求的消息,把请求行和请求头部的内容读取出来,并且判断客户端实现的是get请求还是post请求

3.服务器响应客户端的请求,如果客户端申请的是一个网页,那就需要在服务器编写出符合http协议的请求行和请求头,并且发送给客户端,再把客户端请求的内容发送给浏览器;如果客户端申请的不是网页,那就不需要发送请求头和请求行,直接发送内容即可。

2.1 简易版本

 
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>
int create_serverfd();

void handle_request(int cfd);

int main(){
	/*
	1. 建立tcp连接
		1.1 创建socket
		1.2 setsockopt
		1.3 bind
		1.4 listen
	*/

	int  sockfd = create_serverfd();

	while(1){
		//2. 等待连接
		int fd = accept(sockfd,NULL,NULL);
		printf("有客户端连接了!\n");
		//3. 解析请求
		//4. 制作响应头
		//5. 响应
		handle_request(fd);

		//因为没有做并发处理     。。。。
		close(fd);
	}
	
	close(sockfd);


	while(1);
	return 0;
}

int create_serverfd(){
	//1.1 创建socket
	int fd = socket(AF_INET,SOCK_STREAM,0);
	//1.2 setsockopt
	int n = 1;
	setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&n,4);
	//1.3 bind
	struct sockaddr_in sin = {0};

	sin.sin_family = AF_INET;
	sin.sin_port = htons(80);  //http默认端口号
	sin.sin_addr.s_addr = INADDR_ANY; //任意ip

	int r = bind(fd,(struct sockaddr*)&sin,sizeof sin);
	if(-1 == r){
	 	perror("bind");
	 	return -1;
	}
	printf("bind 成功!\n");

	//1.4 listen
	r = listen(fd,100);
	if(-1 == r){
	 	perror("listen");
	 	return -1;
	}
	printf("listen 成功!\n");

	return fd;
}

void handle_request(int cfd){
	//1 客户端会先发送一个字符串(请求)过来

	char buff[1024*1024] = {0};

	int nread = read(cfd,buff,sizeof(buff));
#if 0
	if(nread>0)
		printf("客户端请求文本:%s\n",buff);
#endif
	//2 从请求中解析出 文件名     字符串解析  正则表达式
	char fileName[10] = {0};
	//sprintf   
	sscanf(buff,"GET /%s",fileName);
	printf("网页文件名:%s\n",fileName);

	//3 根据文件名或者 html文件中的mime文件类型 获取对应的文件并防入响应头当中,告诉浏览器服务器发过去的是什么类型的文件
	
	//strstr 找子串   找到返回子串首地址 找不到返回NULL
	char* mime = NULL;
	if( strstr(fileName,".html") ){
		mime = "text/html";//文本类型  html文本
	}else if( strstr(fileName,".jpg") ){
		mime = "image/jpg";//图片类型   jpg格式图片
	}


	//4 打开文件 读取内容 构建响应头  发回给客户端

	//构建响应
	char response[1024*1024] = {0};
	sprintf(response,
		"HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n",
		mime);//响应头

	int headLen = strlen(response);

	//响应的内容
	int fileFd = open(fileName,O_RDONLY);
	int fileLen = read(fileFd,response + headLen,
		sizeof(response) - headLen);
	close(fileFd);
	//发回给客户端
	write(cfd,response,headLen + fileLen);
	close(cfd);
	sleep(1);
}

运行效果:

 

==================================================

参考连接

HTTP服务器:https://shuyeidc.com/wp/890.html

HTTP服务器:https://zhuanlan.zhihu.com/p/612913176

HTTP客户端与服务器:https://agents.baidu.com/content/question/b8eee1b3a1f07655193b479b

HTTP服务器:https://blog.csdn.net/BABA8891/article/details/142289237

==================================================

 

3. HTTP服务器并发

可以让服务器同时处理多个客户端请求。

3.1 使用多线程实现

查看代码
 
 

 

3.2 使用epoll实现

查看代码
 
 
 

3.3 访问的注意事项

1.访问的格式

 

2.连接状态

注意:

如果访问服务器时没有指定要访问的资源路径,那么浏览器会自动帮我们添加/,但此时仍然没有指明要访问web根目录下的哪一个资源文件,这时默认访问的是目标服务的首页。
大部分URL中的端口号都是省略的,因为常见协议对应的端口号都是固定的,比如HTTP、HTTPS和SSH对应的端口号分别是80、443和22,在使用这些常见协议时不必指明协议对应的端口号,浏览器会自动帮我们进行填充。

 

4. HTTP协议CS架构

4.1 HTTP服务器

服务器使用多线程+守护进程实现

httpserver.c

 
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUFFSIZE 1024
char LOGBUF[1024];


//http 消息头
#define HEAD "HTTP/1.0 200 OK\r\n\
Content-Type: %s;charset=utf-8\r\n\
Content-Length:%d\r\n\r\n"
//http 消息尾
#define TAIL "\r\n\r\n"

//日志文件存储
void save_log(char *buf);
//创建socket
int socket_create(int port);
//接受连接
int socket_accept(int st);

//根据扩展名返回文件类型描述
const char *get_filetype(const char *filename); 
//根据用户在GET中的请求,生成相应的回复内容
int get_file_content(const char *file_name, char **content);
//设置守护进程
void setdaemon();
//得到http 请求中 GET后面的字符串
void get_http_command(char *http_msg, char *command);
//制作http响应
int  make_http_content(const char *command, char **content);
//线程处理函数
void * http_thread(void *argc);



int main(int argc,char *argv[]){
	if(argc<2)
	{
		printf("usage:server port \n");
		return 0;
	}
	int port = atoi(argv[1]);

	if(port <= 0)
	{
		printf("port must be positive integer: \n");
		return 0;
	}

	int st =socket_create(port);
	if(st==0)
	{
		return 0;
	}
	//setdaemon();
	printf("my http server begin\n");
	socket_accept(st);
	close(st);
}

//日志文件存储
void save_log(char *buf){
      FILE *fp = fopen("log.txt","a+");  
      fputs(buf,fp);  
      fclose(fp);  
}

//设置为守护进程
void setdaemon() {
	pid_t pid, sid;
	pid = fork();
	if (pid < 0)
	{
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"fork failed %s\n", strerror(errno));
		save_log(LOGBUF);
		exit (EXIT_FAILURE);
	}
	if (pid > 0)
	{
		exit (EXIT_SUCCESS);
	}

	if ((sid = setsid()) < 0)
	{
		printf("setsid failed %s\n", strerror(errno));
		exit (EXIT_FAILURE);
	}
  	/*if (chdir("/") < 0)
	 {
	 printf("chdir failed %s\n", strerror(errno));
	 exit(EXIT_FAILURE);
	 }*/
	 umask(0);
	 close(STDIN_FILENO);
	 close(STDOUT_FILENO);
	 close(STDERR_FILENO);

}

//根据扩展名返回文件类型描述
const char *get_filetype(const char *filename) {
	////////////得到文件扩展名///////////////////
	char sExt[32];
	const char *p_start=filename;
	memset(sExt, 0, sizeof(sExt));
	while(*p_start)
	{
		if (*p_start == '.')
		{
			p_start++;
			strncpy(sExt, p_start, sizeof(sExt));
			break;
		}
		p_start++;
	}

	////////根据扩展名返回相应描述///////////////////

	if (strncmp(sExt, "bmp", 3) == 0)
		return "image/bmp";

	if (strncmp(sExt, "gif", 3) == 0)
		return "image/gif";

	if (strncmp(sExt, "ico", 3) == 0)
		return "image/x-icon";

	if (strncmp(sExt, "jpg", 3) == 0)
		return "image/jpeg";

	if (strncmp(sExt, "avi", 3) == 0)
		return "video/avi";

	if (strncmp(sExt, "css", 3) == 0)
		return "text/css";

	if (strncmp(sExt, "dll", 3) == 0)
		return "application/x-msdownload";

	if (strncmp(sExt, "exe", 3) == 0)
		return "application/x-msdownload";

	if (strncmp(sExt, "dtd", 3) == 0)
		return "text/xml";

	if (strncmp(sExt, "mp3", 3) == 0)
		return "audio/mp3";

	if (strncmp(sExt, "mpg", 3) == 0)
		return "video/mpg";

	if (strncmp(sExt, "png", 3) == 0)
		return "image/png";

	if (strncmp(sExt, "ppt", 3) == 0)
		return "application/vnd.ms-powerpoint";

	if (strncmp(sExt, "xls", 3) == 0)
		return "application/vnd.ms-excel";

	if (strncmp(sExt, "doc", 3) == 0)
		return "application/msword";

	if (strncmp(sExt, "mp4", 3) == 0)
		return "video/mpeg4";

	if (strncmp(sExt, "ppt", 3) == 0)
		return "application/x-ppt";

	if (strncmp(sExt, "wma", 3) == 0)
		return "audio/x-ms-wma";

	if (strncmp(sExt, "wmv", 3) == 0)
		return "video/x-ms-wmv";

	return "text/html";
}

//创建socket
int socket_create(int port){
	int st = socket(AF_INET, SOCK_STREAM, 0);
	int on =1;
	if (st == -1){
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"%s,%d:socker error %s\n", __FILE__, __LINE__, strerror(errno));
		save_log(LOGBUF);
		return 0;
	}
	if (setsockopt(st, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1){
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"setsockopt failed %s\n", strerror(errno));
		save_log(LOGBUF);
		return 0;
	}
	struct sockaddr_in sockaddr;
	memset(&sockaddr, 0, sizeof(sockaddr));
	sockaddr.sin_port = htons(port); //指定一个端口号并将hosts字节型传化成Inet型字节型(大端或或者小端问题)
	sockaddr.sin_family = AF_INET;	//设置结构类型为TCP/IP
	sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);	//服务端是等待别人来连,不需要找谁的ip
	//这里写一个长量INADDR_ANY表示server上所有ip,这个一个server可能有多个ip地址,因为可能有多块网卡
	if (bind(st, (struct sockaddr *) &sockaddr, sizeof(sockaddr)) == -1){
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"%s,%d:bind error %s \n", __FILE__, __LINE__, strerror(errno));
		save_log(LOGBUF);
		return 0;
	}
	// 	服务端开始监听
	if (listen(st, 100) == -1) {
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"%s,%d:listen failture %s\n", __FILE__, __LINE__,
				strerror(errno));
				save_log(LOGBUF);
		return 0;
	}
	printf("start server success!\n");
	return st;

}

//接受客户端连接
int socket_accept(int st){
	int client_st;
	struct sockaddr_in client_sockaddr;
	socklen_t len = sizeof(client_sockaddr);

	pthread_t thrd_t;
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //初始化线程为可分离的
	memset(&client_sockaddr, 0, sizeof(client_sockaddr));

	while (1){
		client_st = accept(st, (struct sockaddr *) &client_sockaddr, &len);
		if (client_st == -1){
			memset(LOGBUF,0,sizeof(LOGBUF));
			sprintf(LOGBUF,"%s,%d:accept failture %s \n", __FILE__, __LINE__,
					strerror(errno));
			save_log(LOGBUF);
			return 0;
		} 
		else{
			int *tmp = (int *) malloc(sizeof(int));
			*tmp = client_st;
			pthread_create(&thrd_t, &attr, http_thread, tmp);
		}
	}
	pthread_detach(thrd_t);//释放资源
}

// 得到文件内容
int  get_file_content(const char *file_name, char **content) {
	int  file_length = 0;
	FILE *fp = NULL;

	if (file_name == NULL){
		return file_length;
	}

	fp = fopen(file_name, "rb");

	if (fp == NULL){
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"file name: %s,%s,%d:open file failture %s \n",file_name, __FILE__, __LINE__,
				strerror(errno));
		save_log(LOGBUF);
		printf("get_file_content: fopen  filename:%s error!\n",file_name);
		return file_length;
	}
	printf("get_file_content: fopen  filename:%s success!\n",file_name);

	fseek(fp, 0, SEEK_END);
	file_length = ftell(fp);
	rewind(fp);

	*content = (char *) malloc(file_length);
	if (*content == NULL){
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"%s,%d:malloc failture %s \n", __FILE__, __LINE__,
				strerror(errno));
		save_log(LOGBUF);
		return 0;
	}
	fread(*content, file_length, 1, fp);
	fclose(fp);

	return file_length;
}

//得到http 请求中 GET后面的字符串
void get_http_command(char *http_msg, char *command){
	char *p_end = http_msg;
	char *p_start = http_msg;
	//GET /
	while (*p_start) {
		if (*p_start == '/'){break;}
		p_start++;
	}
	p_start++;
	p_end = strchr(http_msg, '\n');
	while (p_end != p_start){
		if (*p_end == ' '){
			break;
		}
		p_end--;
	}
	strncpy(command, p_start, p_end - p_start);
}

//根据用户在GET中的请求,生成相应的回复内容
int make_http_content(const char *command, char **content){
	printf("make_http_content\n");
	char *file_buf;
	int file_length;
	char headbuf[1024];

	if (command[0] == 0){
		file_length = get_file_content("index.html", &file_buf);
	}else{
		//file_length = get_file_content(command, &file_buf);
		//file_length = get_file_content("index.html", &file_buf);
		file_length = get_file_content("index2.html", &file_buf);
	}
	if (file_length == 0){
		printf("get_file_content error\n");
		return 0;
	}
	printf("get_file_content success\n");
	memset(headbuf, 0, sizeof(headbuf));
	sprintf(headbuf, HEAD, get_filetype(command), file_length); //设置消息头

	int iheadlen = strlen(headbuf); //得到消息头长度
	int itaillen = strlen(TAIL); //得到消息尾长度
	int isumlen = iheadlen + file_length + itaillen; //得到消息总长度
	*content = (char *) malloc(isumlen); //根据消息总长度,动态分配内存
	if(*content==NULL){
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"malloc failed %s\n", strerror(errno));
		save_log(LOGBUF);
	}
	printf("malloc ok!\n");
	char *tmp = *content;
	memcpy(tmp, headbuf, iheadlen); //安装消息头
	memcpy(&tmp[iheadlen], file_buf, file_length); //安装消息体
	memcpy(&tmp[iheadlen] + file_length, TAIL, itaillen); //安装消息尾
	printf("headbuf:\n%s", headbuf);
	if (file_buf){
		free(file_buf);
	}
	return isumlen; //返回消息总长度
}

//线程函数
void *http_thread(void *argc){
	printf("thread begin \n");
	if(argc==NULL){
		return NULL;
	}
	int st = *(int *) argc;
	free((int *)argc);
	char buf[1024];
	memset(buf, 0, sizeof(buf));
	int rc = recv(st, buf, sizeof(buf), 0);
	if (rc <= 0){
		memset(LOGBUF,0,sizeof(LOGBUF));
		sprintf(LOGBUF,"recv failed %s\n", strerror(errno));
		save_log(LOGBUF);
	}else{
		printf("recv:\n%s", buf);
		char command[1024];
		memset(command, 0, sizeof(command));
		get_http_command(buf, command); //得到http 请求中 GET后面的字符串
		printf("get:%s \n", command);
		char *content = NULL;
		int ilen = make_http_content(command, &content); //根据用户在GET中的请求,生成相应的回复内容
		printf("-----------------------------------------------\n");
		printf("回复给客户端:%s\n",content);
		printf("-----------------------------------------------\n");
		if (ilen > 0){
			send(st, content, ilen, 0); //将回复的内容发送给client端socket
			free(content);
		}
	}
	close(st); //关闭client端socket
	printf("thread_is end\n");
	return NULL;

}

4.2 HTTP客户端

httpclient.c

 
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>


#define IPSTR "192.168.249.139"
//#define IPSTR "ws.webxml.com.cn"
#define PORT 80
#define BUFSIZE 1024
 
int main(int argc, char **argv)
{
        int sockfd, ret, i, h;
        struct sockaddr_in servaddr;
        char str1[4096], str2[4096], buf[BUFSIZE], *str;
        socklen_t len;
        fd_set   t_set1;
        struct timeval  tv;
        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) {
                printf("创建socket失败---socket error!\n");
                exit(0);
        };
 
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(PORT);


        struct hostent * host = NULL;
        in_addr_t iAddr = inet_addr(IPSTR);
        if(INADDR_NONE == iAddr){
                printf("%s不是个ip地址!\n",argv[1]);
                host = gethostbyname(IPSTR);
                if(NULL == host){
                        printf("输入错误!\n");
                       exit(-1);
                }
                memcpy(&(servaddr.sin_addr),host->h_addr,host->h_length);
                printf("ip地址为:%s\n",inet_ntoa(servaddr.sin_addr));
        }else{
                if (inet_pton(AF_INET, IPSTR, &servaddr.sin_addr) <= 0 ){
                printf("ip地址转化--inet_pton error!\n");
                exit(0);
                }
        }

        


        if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){
                printf("连接服务器,connect error:%m!\n");
                exit(0);
        }
        printf("连接服务器成功\n");
 

        memset(str2, 0, 4096);
        strcat(str2, "qqCode=507817159");
        str=(char *)malloc(128);
        len = strlen(str2);
        sprintf(str, "%d", len);
 
        memset(str1, 0, 4096);
        strcat(str1, "POST /webservices/qqOnlineWebService.asmx/qqCheckOnline HTTP/1.1\n");
        strcat(str1, "Host: ws.webxml.com.cn\n");
        strcat(str1, "Content-Type: application/x-www-form-urlencoded\n");
        strcat(str1, "Content-Length: ");
        strcat(str1, str);
        strcat(str1, "\n\n");
 
        strcat(str1, str2);
        strcat(str1, "\r\n\r\n");
        printf("%s\n",str1);
 
        ret = write(sockfd,str1,strlen(str1));
        if (ret < 0) {
                printf("发送失败 错误号%d错误原因%s\n",errno, strerror(errno));
                exit(0);
        }else{
                printf("发送成功%d\n\n", ret);
        }
 
        FD_ZERO(&t_set1);
        FD_SET(sockfd, &t_set1);
 
        while(1){
                sleep(2);
                tv.tv_sec= 0;
                tv.tv_usec= 0;
                h= 0;
                printf("--------------->1\n");
                h= select(sockfd +1, &t_set1, NULL, NULL, &tv);
                printf("--------------->2\n");
 
                if (h == 0) continue;
                if (h < 0) {
                        close(sockfd);
                        printf("异常结束!\n");
                        return -1;
                }
 
                if (h > 0){
                        memset(buf, 0, 4096);
                        i= read(sockfd, buf, 4095);
                        if (i==0){
                                close(sockfd);
                                printf("正常结束\n");
                                return -1;
                        }
                        printf("buf:%s\n", buf);
                       
                }
        }
        close(sockfd);
 
 
        return 0;
}

 

 

posted @ 2025-01-06 10:02  [BORUTO]  阅读(484)  评论(0)    收藏  举报