简版服务器(c语言实现)

时间:2022-05-22 17:59:25

声明:仅个人小记

目录:
(1) 简单交代及效果展示
(2) 开发日志
(3) 源代码
(4) 小结

(1) 简单交代及效果展示
环境:Ubantu 15.10 gcc5.2.1

这是一个用c语言实现的服务器(平台无所谓的,只是windows和linux的头文件有点不同,网上查阅下即可),可以充当静态网页服务器。只是静态。功能简陋,主要是学习socket通信知识。

效果展示:
1.启动服务器(server文件是编译好的可执行文件)
简版服务器(c语言实现)
2.静态网页(我把它们放在同目录下的www目录下)
简版服务器(c语言实现)
2.浏览器访问服务器
简版服务器(c语言实现)

(2) 开发日志

本质上就是借助socket通信,实现服务器与客户端(浏览器)的通信

静态网页的展现:

(写起来可能会繁琐一些,因为涉及到很多字符串的处理,用c语言从底层实现,效率提升。这是一个值得做的小项目)
昨天开始的小项目

今天2016-08-22 16:41:53
先尝试使用各种socket函数和一些头文件,熟悉linux下socket的环境。

2016-08-22 16:52:36 sockaddr_in server;这个数据类型报错,没有这样的类型,加上头文件 #include <netinet/in.h>

2016-08-22 16:56:31 error: unknown type name ‘sockaddr_in’ 仍然报错。解决方案 添加 struct 显式强调这是一个结构体类型。即 struct sockaddr_in server;

2016-08-22 17:19:37 gcc 支持c99 ,真假值 true 和 false ,c99 没有把他们作为内置关键字,所以使用true或者false用gcc编译时候会报错。解决方案: 1.使用g++编译。 2. 不使用true和false关键字,使用非零和零来代替

2016-08-22 17:37:35 memset 函数,头文件加 #include <string.h>

2016-08-22 17:59:03 已经可以接受来自浏览器的访问

Have a Rest.

2016-08-22 20:49:26 打开电脑,继续开工。 分析浏览器发送的请求数据:
GET / HTTP/1.1
Host: localhost:63633
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:41.0) Gecko/20100101 Firefox/41.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
这是浏览器发送的请求数据信息。请求方式为‘GET’ 请求内容‘/’ HTTP版本信息 HTTP/1.1 请求方地址端口信息,请求端等等信息(我现在还不能详细的分析,好多不知道,暂时用不着,先继续原目标)
我要实现的是类似apache服务器,提供给搭建网站的开发人员一个类似www目录,只要吧网页文件放在这个目录然后开启服务就可以启动网站这样的功能。而实现这样的表述方式需要涉及对文件的操作。所以,下面再linux上开始熟悉下文件操作。使用test.c 文件来练习

//TODO : 目前只考虑GET方式

2016-08-22 22:37:01 简单的增加了一个配置文件INIT,用来给开发人员调整网站根目录。 写了一个numberizeMethod() 将http的方式数字化,八种方式分别对应一个数字,这是为了方便后续的处理

2016-08-23 15:19:16 今天开工。今天要做的是完成读取网页并发送给客户端,需要知道http发送数据的格式。

2016-08-23 15:28:52 我用浏览起访问这个服务器,第一次访问,服务器这边显示的是正常的请求数据。然后我刷新的时候,服务器这边出现bad file descriptor 错误,这是accept返回了一个不正常的socket套接字描述符。这让我想起了多线程问题。我一直没搞清楚这个服务器允许多个客户端链接,我觉得在listen(),这个函数体现的就是允许多线程。但到底是不是多线程呢?

2016-08-23 15:34:52 问题解决了,原来是服务器套接字被我直接个给关闭了,就是在我只使用了一次就关闭了。close(server);这句话就是意味着关闭对server的监视。或者解除server的关联信息。 位置放的不对。

2016-08-23 15:39:23 TODO:多线程问题暂时先放这,基本的框架搭好再来完成这一部分

2016-08-23 15:45:12 关于文件的发送,http 是三段式,文件是一次发过去还是分批发过去,考虑到网页文件不是大文件,可以开销这样的内存,将文件直接读进内存,检测文件长度,整体打包好一起发送过去。至于分批发送的可行性,目前不清楚。以后需要了解下(TODO)

2016-08-23 18:22:23 刚刚出去一趟,吃完晚饭了

2016-08-23 18:53:22 现在浏览器已经可以访问到网页了,刚刚遇到了一个问题,就是文件路径出错,一个是我把网站根目录放在了配置文件中INIT,在读取该文件时候,循环多了一次,路径错误,第二个是我使用的是按行读取读取,读取到的路径的尾部是换行符,影响了我路径的连接

2016-08-23 19:08:18 网页解析的速度很慢,很慢,是我的服务器问题吗??

2016-08-23 19:28:36 多文件编译 eg: g++ server_preset.h server_function.h server.c -o server

2016-08-23 19:57:02 对程序的结构进行了修整,分文件,测试;也确认网页解析速度我代码的问题

2016-08-23 20:21:12 刚刚洗了澡。 recv() 函数; 如果缓冲区中没有数据,recv将进入等待状态直到有数据。我对recv()函数使用的是while循环,我这样做是考虑这POST交互方式,用户发来的数据可能缓冲区不够存放,所以需要多次对缓冲区进行读取。
2016-08-23 20:38:47 缓冲区判断是否为空,方案:1.为recv()设置接收超时, 涉及到新的知识,select()函数,阻塞和非阻塞
2016-08-23 20:52:23 目前采用的方式就是将缓冲区设置的适当大一些,recv就默认执行一次
2016-08-23 21:01:25 现在开始增加图片链接解析
2016-08-23 21:21:51 接收缓冲区的大小固定设置, 发送缓冲区动态指定,动态分配
2016-08-23 21:32:52 图片jpeg格式图片发送成功

2016-08-23 21:40:42 实际上http协议中对于图片的响应并不要求再协议交代图片的格式,所以,图片随意发

2016-08-23 21:55:24 今晚休息了

2016-08-24 16:19:02 表单提交数据,如果method=’GET’,数据写在url里面(eg: localhost:63633/test/php?username=JACK&password=122674),按照http协议客户端数据打包的话,三段式,这些数据是放在第一段,也就是第一行的url那里;如果method=’POST’,数据就是写在第三段数据体那边; 说GET方式不安全,因为再url那边就可以直接看到。依我看,POST的安全性也没高到哪里去,我也是直接就再第三段那边直接看到了明文。

(3) 源代码

一共三个文件(放在一起嫌长):
1. main.c
2. server_function.h //主要函数都在这里
3. server_preset.h // 一些宏,以及一些头文件放在一起

main.c

#include "server_function.h"


int main(void)
{
int server = configure();
run(server);

return 0;
}

server_function.h

#ifndef SERVER_FUNCTION_H
#define SERVER_FUNCTION_H

#include "server_preset.h"

const char METHOD[8][10] = {
"GET","POST","PUT","DELETE","OPTIONS","HEAD","TRACE","CONNECT",
};

int configure();// 启动之前的相关配置,返回配置好的socket描述符号
void run(int server); // 服务器正式运行
short numberizeMethod(char method[]);// 将交互方式数字化
void sendTextfile(int client, char * url);// 给定套接字client,给这个client发送文本文件
void sendImagefile(int client, char * url); // 给定套接字client,给这个client发送图片文件


int configure()// 启动之前的相关配置
{
text_header_length = strlen(text_header);
image_header_length = strlen(image_header);
FILE * fp = fopen("INIT","r");
if (!fp){ perror("INIT file open fail"); exit(-1);}
//while(!feof(fp)) {// 这里不使用循环纯粹是简化对INIT的设置,以及代码简化
fgets(ROOT,256,fp);// 换行符也被读了进去
int t = strlen(ROOT);
ROOT[t-1] = '\0';
printf("%s",ROOT);
//}
// TODO: 这里对INIT文件的使用以及约定还没有设计好
fclose(fp);

int PORT;
printf("input the port: \n");
scanf("%d",&PORT);
int server = socket(AF_INET,SOCK_STREAM,0);// 创建socket套接字
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本地地址 0.0.0.0 这样我们可以再本机上通过 127.0.0.1:63633 或者 192.168.1.2:63633(局域网) 或者 120.27.125.158:63633 (公网) 来访问,意思就是绑定所有本机的ip
if (bind(server,(struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) {
perror("bind error perror");// 打印出编译器提供的报错信息
printf("bind error");
exit(-1);
}
if (listen(server,QUEUE) == -1) {// 设置侦听,其实不是真的侦听
perror("listen\n");
exit(-1);
}

return server;
}
void run(int server)// 服务器正式运行
{
char url[URL_SIZE]; // 用来记录客户端请求的url
char buffer[BUFFER_SIZE]; // 缓冲区设定
char method[METHOD_NAME_SIZE]; // 用来存放 http协议中客户端与服务端的交互方式 通常: GET POST PUT DELETE 查改增删(好像一定是大写) 一共八种,详细查阅同目录下 相关资料.txt 文件

struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
int client;
int len;

while(1) {
printf("listen...\n");
client = accept(server,(struct sockaddr*)&client_addr,&length);// 实行真正的侦听,对套接字server进行侦听(所谓侦听就是检测server这个套接字,socket的信息我们已经指定,一旦有对这个socket发送数据,我们将数据源的识别信息(也就是发送方的socket信息)用本地变量client_addr记录下来,并且将client_addr和本地的client变量绑定(所以accept需要第三个变量length),然后脱离侦听状态)
if (client < 0) { // 如果accept失败, client 将返回 -1 表示出错
perror("clientect"); // 打印连接出错信息
continue;
}
printf("client file descriptor : %d\n",client);
memset(buffer,0,BUFFER_SIZE);// 对缓冲数据全部置零处理
//while((len = recv(client,buffer,BUFFER_SIZE,0)) > 0) {// 将来自client的数据写入缓冲区buffer ,并且最大写入量就是BUFFER_SIZE,返回真实的写入数据量
len = recv(client,buffer,BUFFER_SIZE,0);
buffer[1023] = '\0';
printf("receive data len: %d \n",len);
int j = 0;
while(buffer[j] != ' ') {
method[j] = buffer[j];
j ++;
}
method[j] = '\0'; // 构成字符串
j ++;
int t = 0;
while(buffer[j+t] != ' '){
url[t] = buffer[j+t];
t++;
}
url[t] = '\0';

switch(numberizeMethod(method)){
case 0: printf("it is method GET \n");
sendTextfile(client,url);
break;
//TODO: 暂时只是实现GET方式
default : break;
}
printf("method is : %s the number is : %d\n",method,numberizeMethod(method));
printf("url is : %s\n",url);

printf("\n\ninfo:\n%s\n",buffer);
memset(buffer,0,BUFFER_SIZE); //break;
// }
close(client);
}

close(server);

return ;
}

short numberizeMethod(char method[])// 将交互方式数字化
{
int i;
//printf("it is numberizeMethod method is %s\n",method);
for (i = 0; i < 8; i ++) {
if (!strcmp(method,METHOD[i]))
return i;
}

return -1;

}

void sendTextfile(int client, char * url)// 给定套接字client,给这个client发送文本文件
{
printf("sendTextfile()\n");
char root[256];

strcpy(root,ROOT);
if (strcmp(url,"/") == 0) {
strcpy(url,"/index.html");
}
strcat(root,url);
printf("the final url is %s\n",root);
FILE * fp = fopen(root,"rb");
if (!fp) {
//TODO 这里应该返回一个 404 NOT FOUND 页面给用户
printf("url is %s\n",root);
perror("sendTextfile file open fail ");
return ;
}
fseek(fp,0,SEEK_END);
int fileSize = ftell(fp);
rewind(fp);
char * send_buffer = (char*)malloc(sizeof(char)*(fileSize+text_header_length));
memcpy(send_buffer,text_header,text_header_length); // 按照http的发送协议数据格式,加头
fread(send_buffer+text_header_length,sizeof(char),fileSize,fp);
if (send(client,send_buffer,text_header_length+fileSize,0) <= 0) {
perror("Send Failed ");
return ;
}
printf("send_buffer start\n");
for (int i = 0; i < fileSize+text_header_length; i ++) {
printf("%c",send_buffer[i]);
}
printf("send_buffer stop\n");
free(send_buffer);
fclose(fp);
printf("exit sendtextfile\n");
return ;
}

void sendImagefile(int client, char * url) // 给定套接字client,给这个client发送图片文件
{
char root[256];
strcpy(root,ROOT);
strcat(root,url);
printf("the final url is %s\n",root);
FILE * fp = fopen(root,"rb");
if (!fp) {
//TODO 这里应该返回一个 404 NOT FOUND 页面给用户
printf("url is %s\n",root);
perror("sendTextfile file open fail ");
return ;
}
fseek(fp,0,SEEK_END);
int fileSize = ftell(fp);
rewind(fp);
char * send_buffer = (char*)malloc(sizeof(char)*(fileSize+image_header_length));
memcpy(send_buffer,image_header,image_header_length); // 按照http的发送协议数据格式,加头
fread(send_buffer+image_header_length,sizeof(char),fileSize,fp);
if (send(client,send_buffer,image_header_length+fileSize,0) <= 0) {
perror("Send Failed ");
return ;
}
printf("send_buffer image start\n");
for (int i = 0; i < fileSize+image_header_length; i ++) {
printf("%c",send_buffer[i]);
}
printf("send_buffer image stop\n");
free(send_buffer);
fclose(fp);
printf("exit sendimagefile\n");
return ;
}

#endif // SERVER_FUNCTION)H

server_preset.h


#ifndef SERVER_PRESET_H
#define SERVER_PRESET_H

#include <sys/socket.h> // 套接字
#include <stdio.h>
#include <netinet/in.h> // struct sockaddr_in 数据类型
#include <stdlib.h> // exit() 函数
#include <string.h>// memset
#include <unistd.h> // close(socket)

//#define PORT 63633
#define BUFFER_SIZE 1024 // 这是接收缓冲区的大小, 发送缓冲区动态指定,动态分配
#define QUEUE 10
#define METHOD_NAME_SIZE 10 // HTTP 交互方式名字长度
#define URL_SIZE 256

char * text_header = "HTTP/1.x 200 OK\nContent-Type:text\n\n";
char * image_header = "HTTP/1.x 200 OK\nContent-Type:image\n\n";
//int text_header_length = strlen(text_header);
//int image_header_length = strlen(image_header);// error: initializer element is not constant ; gcc报错,原因是gcc要求变量值在编译是确定 比如int i= 90; 这是明确值,而strlen函数只是在运行时候执行,编译时候不会执行,所以返回值不确定。因此我改用下面两句,只是声明,赋值放在init函数中
int text_header_length;
int image_header_length; //
char ROOT[URL_SIZE]; // 只用来存放网站根目录

#endif // SERVER_PRESET_H

(4) 小结

以上就是我的开发记录和源代码
我的这个开发日志就是我在开发过程的记录,这些理解都只是我目前层次的理解,好多还没有透彻,会有很多问题存在。

对于各文件解释:
…/server.c
…/server_.preset.h
…/server_function.h
…/INIT 这个是网站根目录配置文件,因为简陋,所以只在第一行纺织网站的根目录(eg: …/www 那么 …/www就是网站的根目录,静网页相关资源放在这个目录下就可以了)

多文件编译 eg: gcc server.c server_function.h server_preset.h -o server

2016-08-24 20:18:07 By Jack Lu