(一)功能概述
- 顺利解析GET请求的html类型和png类型,并返回资源给客户端
- 顺利解析POST请求各字段,但未做存储(为了展示效果,直接在服务器中加了后台处理程序段,理论上服务器上应该没有这些代码)
- 框架完整,可扩展性强,方便后期添加更加详细的请求解析和响应,只需为对应的结构体添加成员并增加适当的数据结构即可。
- epoll实现IO多路复用 Unix C学习之IO多路复用之epoll
- 多线程异步处理IO事务,并发线程数,由信号量控制
C语言学习之进程同步、线程同步——信号量(semaphore)
C语言学习之线程 - 服务器的配置由server.conf配置文件实现,配置文件需要放在服务器可执行文件的同目录下
(二)代码实现
- 服务器主框架代码
server.c
#include "t_stdio.h" #include <sys/epoll.h> #include "info.h" #include <semaphore.h> #include "serv_tool.h" #include <pthread.h> #define THREAD_NUM 100 sem_t tnum;//define a semaphore variable void *run_deal(void *);//declare a function which is used to dispose a I/O event via a new thread int main(void){ serv_info_t serv_info;//structure defined in info.h used to store server infomation. /* defined in serv_tool.c, #include "serv_tool.h" generate a socket device which has been listened by function listen() and update serv_info note: using the function get_listened_socket() update serv_info is not better which is ought to be replaced with a independent function split from the function get_listened_socket() */ int sfd = get_listened_socket(&serv_info); if(-1 == sfd) { printf("in server.c main...get_listened_socket...\n"); return -1; } //epoll create and control int epfd = epoll_create(1); if(-1 == epfd) E_MSG("epoll_create", -1); struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sfd; int v = epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); if(-1 == v) E_MSG("epoll_ctl", -1); //sem initn, THREAD_NUM is the max number of threads //note:using macro is not better which is ought to be written in server.conf sem_init(&tnum, 0, THREAD_NUM); //epoll wait and deal with client's request struct epoll_event events[100]; while(1){ int maxfd = epoll_wait(epfd, events, 100, -1); if(-1 == maxfd) E_MSG("epoll_wait", -1); for(int i=0; i<maxfd; i++){ if(events[i].data.fd == sfd){ int cfd = accept(sfd); if(-1 == cfd) { printf("in server.c main...h_accept...error\n"); return -1; } ev.events = EPOLLIN|EPOLLONESHOT; //EPOLLONESHOT is important for socket fd ev.data.fd = cfd; v = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); if(-1 == v) E_MSG("epoll_ctl", -1); }else{ printf("wait.....\n"); //check the remaining amount of threads and if it is 0, suspend the thread. sem_wait(&tnum); //create a thread, tid is unused, so don't store it. pthread_t tmp; /* structure deal_arg_t defined in serv_tool.h, stores function deal_cli_req()'s arguments.When create a thread, the arg will be passed into the function pthread_create() to be the arguments of function deal_cli_req(). */ deal_arg_t args; args.fd = events[i].data.fd; args.p_s_info = &serv_info; pthread_create(&tmp, NULL, run_deal, (void *)&args); //mark it detached pthread_detach(tmp); //print the id of the thread which will terminate. printf("cread %lu end...\n", tmp); } } } return 0; } void *run_deal(void *args){ //Separate function deal_cli_req()'s arguments from structure deal_arg_t. deal_arg_t *tmp = (deal_arg_t *)args; //deal with client's request deal_cli_req(tmp->fd, tmp->p_s_info); //tnum + 1 //note: That sem_post() is invoked here is not good //because the current thread do not really terminate yet. sem_post(&tnum); return NULL; }
t_stdio.h
错误码宏函数#ifndef __T_STDIO_H__ #define __T_STDIO_H__ #include <stdio.h> #define E_MSG(STR,VAL) do{\ perror(STR);\ return (VAL);\ }while(0) #endif
info.c
info.h
包含了一个用来接收配置文件中配置信息的结构体和客户端请求信息的结构体,对外提供一个服务器配置信息读取并存到结构体中的函数。
info.h
#ifndef __SERVER_INFO_H__ #define __SERVER_INFO_H__ #include <sys/socket.h> #include <netinet/in.h> typedef struct server_info{ unsigned short int port; int listen_backlog; struct in_addr serv_ip; char release_dir[128]; }serv_info_t; //request info from client typedef struct{ char method[12]; char URL[128]; char version[12]; int Content_Length; }req_info_t; //get the infomation from configuration file and store it in the structure serv_info_t int serv_info_init(serv_info_t *); #endif //__SERVER_INFO_H__
info.c
#include "info.h" #include "t_stdio.h" #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> #define CONFIG_PATH "server.conf" //configuration directory int serv_info_init(serv_info_t *info){ //open server.conf int fd = open(CONFIG_PATH, O_RDONLY); if(-1 == fd) E_MSG("open", -1); //read and assignment //note: if file is bigger than 4096B will error char buf[4096]; int rcount = read(fd, buf, sizeof(buf)); if(-1 == rcount) E_MSG("read", -1); //printf("%s", buf); char *content = buf; char *next = NULL; do{ next = strsep(&content, "\n"); //printf("next:\n%s\n\n\n", next); char *val = next; char *key = strsep(&val, "="); //printf("key:%s\n", key); //printf("val:%s\n\n", val); if(!strcmp(key, "port")) info->port = htons((unsigned short int)atoi(val));//network byte order else if(!strcmp(key, "listen_backlog")) info->listen_backlog = atoi(val); else if(!strcmp(key, "serv_ip")) inet_pton(AF_INET, val, &info->serv_ip); else if(!strcmp(key, "release_dir")) strcpy(info->release_dir, val); else printf("unknown config...\n"); }while(strncmp(content, "\n", 1)); //close file close(fd); return 0; }
serv_tool.c
serv_tool.h
封装了server.c中直接用到的自定义函数
serv_tool.h
#ifndef __SERV_TOOL_H__ #define __SERV_TOOL_H__ #include <sys/epoll.h> #include "info.h" typedef struct{ int fd; serv_info_t *p_s_info; }deal_arg_t; extern int get_listened_socket(serv_info_t *); extern int deal_cli_req(int, const serv_info_t *); #endif //__SERV_TOOL_H__
serv_tool.c
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <pthread.h> #include <string.h> #include <unistd.h> #include <t_stdio.h> #include "info.h" // return a file discriptor which has been listened int get_listened_socket(serv_info_t *serv_info){ int sfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sfd) E_MSG("socket", -1); //reuse the addr int optval = 1; setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int)); //initialize server's info //data in struct s_info has the network byte order if necessary serv_info_init(serv_info); //bind struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = serv_info->port; addr.sin_addr = serv_info->serv_ip; int bin = bind(sfd, (const struct sockaddr *)&addr, sizeof(addr)); if(-1 == bin) E_MSG("bind", -1); //listen int lis = listen(sfd, serv_info->listen_backlog); if(-1 == lis) E_MSG("listen", -1); return sfd; } //deal with client's request int deal_cli_req(int fd, const serv_info_t *s_info){ char buf[4096]; //store request infomation, the structure is defined in serv_tool.h req_info_t req_info; int r = 0; int entity = 0; //if request line and head line has been resolved, set 1 while((r = read(fd, buf, sizeof(buf)))>0){ char *rows = buf; char *next = NULL; char *word = NULL; next = strsep(&rows, "\r"); int line_count = 0;//record the resolving line //resolve request line and head line do{ if(entity) break; //should resolve entity body line_count++; if(!next || (*rows != '\n')){ printf("request format error\n"); return -1; } if(1 == line_count){ word = strsep(&next, " "); printf("%lumethod:%s\n", pthread_self(), word); strcpy(req_info.method, word); word = strsep(&next, " "); printf("%luURL:%s\n", pthread_self(), word); printf("%luversion:%s\n", pthread_self(), next); strcpy(req_info.URL, word); strcpy(req_info.version, next); }else{ rows++; word = strsep(&rows, ":"); int choice = 0; if(!strcmp(word, "Content-Length")) choice = 1; //printf("%s:", word);//if necessary, store this in structure. rows++; word = strsep(&rows, "\r"); //store other key: val in this code area switch(choice){ case 1: req_info.Content_Length = atoi(word); break; default: break; } } if((*(rows+1)) == '\r') entity = 1; }while(!entity); //resolve Entity body //note: the job should be done by Backend language such java, but the code is written here because I need test the client's POST request. if(!strcmp(req_info.method, "POST")){ rows = rows + 3; *(rows+req_info.Content_Length) = '\0'; char *post_key = NULL; char *post_val = NULL; char usrname[32]; char password[32]; post_key = strsep(&rows, "="); post_val = strsep(&rows, "&"); strcpy(usrname, post_val); printf("%s=%s\n", post_key, post_val); post_key = strsep(&rows, "="); post_val = strsep(&rows, "&"); strcpy(password, post_val); printf("%s=%s\n", post_key, post_val); if((!strcmp(usrname, "cjl")) && (!strcmp(password, "123456"))) write(fd, "success", 7); else write(fd, "failed", 6); } break;//note : !!!!!!!!!!!!!need modify, if the request is bigger than 4096B, error } char type[64] = {0}; char *tmp = strrchr(req_info.URL, '.'); strcpy(type, ++tmp); if(!strcmp(req_info.method, "GET")){ char path[128] = {0}; strcpy(path, s_info->release_dir); strcat(path, req_info.URL); //404 if(access(path, F_OK|R_OK)){ if(!strcmp(type, "html")){ char *response = "HTTP/1.1 404 \r\nContent-Type: text/html\r\n\r\n"; write(fd, response, strlen(response)); char *str404 = "<html><head>404</head><body>NOT FOUND</body></html>"; write(fd, str404, strlen(str404)); }else if(!strcmp(type, "png")){ char *response = "HTTP/1.1 404 \r\n\r\n"; write(fd, response, strlen(response)); }else{ } }else{ if(!strcmp(type, "html")){ char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"; write(fd, response, strlen(response)); }else if(!strcmp(type, "png")){ char *response = "HTTP/1.1 200 OK\r\nContent-Type: image/png\r\n\r\n"; write(fd, response, strlen(response)); }else{ } int src_fd = open(path, O_RDONLY); while((r=read(src_fd, buf, sizeof(buf))) > 0){ write(fd, buf, r); } } } close(fd); printf("%lu deal end...\n", pthread_self()); return 0; }
可改进:
- serv_info_t结构体的读取(从配置文件中读),不应该在函数get_listened_socket()中,应该单独拿出来作为一个函数。
- 最多线程数应该加载配置文件中,目前是定义在了宏THREAD_NUM中
- 线程调用函数中,执行信号量的+1是不太好的。如此会造成,同时在运行的线程数可能大于最大线程数。比如想象一种极端的情况:最大线程数为100,目前同时运行线程数为100,且这100个线程都执行到了void *run_deal(void *args)函数的
sem_post(&tnum);
语句,此时100个线程都未结束,但是信号量为100,即可创建的线程数为100,此时如果有新的I/O事件,比如又来了100个I/O事件,会创建100个新线程,然后信号量为0,但是加上之前执行到sem_post(&tnum);
的100个线程,此时有200个线程同时运行,当然,执行到sem_post(&tnum);
的100个线程是即将结束的,也不占用太多的资源,但终归是逻辑上有点不好的。
解决方法:在全局变量区维护一个链表(插入和删除频繁,所以不选择数组),存储正在运行的线程的tid。在主线程中,新开一个线程,时刻遍历并且tryjoin链表中的线程。如果tryjoin成功,则从链表中删除节点,并将信号量+1(post),注意,要讲线程设置成可汇合。 - serv_info_init()函数中,读取配置文件时,只读取了一次4096B,没有循环读取,如果配置文件过大,可能读不全。
解决方法:循环读取,直到读到文件结尾。但是可能遇到一次4096个读,一次4096个读,某个单词会被切割成两半,增加这种情况的处理。 - 读取客户端请求,有同上的问题
- 为了测试POST请求,将本应由后端语言编写的后台处理程序,直接将后台处理,写到了服务器里,并且也没有加数据库。==账号cjl 密码123456 ==则会登录成功
配置文件说明
port=9999
listen_backlog=100
serv_ip=0.0.0.0
release_dir=/home/cjl/uc/uc_project/server2.0/web
- 配置要写成key=value的格式,不要有空格
- 最后要加一个空行\n
- port:服务器监听端口
- listen_backlog监听最大连接数
- serv_ip服务器ip,0.0.0.0表示监听本机所有的ip,访问本机的任意一个ip都可以访问到本机
- release_dir html文件存放的路径,即项目路径
编译:gcc server.c info.c serv_tool.c -lpthread -o server
,运行server,打开浏览器访问本机任意ip加配置文件指定端口号(默认9999)/index.html即可访问,注意:下图的ip是我自己的ip
小收获:
- 熟悉了线程、信号量、epoll的使用
- epoll中,struct epoll_event 的成员 events, 代表监听的事件,其中有一个选项是EPOLLONESHOT,如果有IO响应,只提醒一次就删除该事件的监听,正好和http协议一样,建立连接只发送一次请求就断开。
- serv_tool.c中,创建socket对象时使用了地址重用