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实现
查看代码
注意:
如果访问服务器时没有指定要访问的资源路径,那么浏览器会自动帮我们添加/,但此时仍然没有指明要访问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;
}






浙公网安备 33010602011771号