#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/sendfile.h>
#include <signal.h>
#define MAX 1024
//根据TCP服务流程取得套接字与客户端的套接字与客户端的文件描述符
//根据客户端的文件描述符将客户端的响应报头取出来
//根据客户端的文件响应描述符将报文分离并取出报头
int getLine(int sock, char line[], int num)//获得报文中的一行,将第一行信息保存在line[]数组中
{
char c = 'x';
int i = 0;
while(c != '\n' && i < num - 1){
ssize_t s = recv(sock, &c, 1, 0);//recv函数只看一下此文件的一个字符并不将其拿出来,将看到的字符放入c中
if(s > 0){
//\r\n, \n, \r -> \n
if(c == '\r'){ // \r\n, \r //如果看到的是 \r 替换成\n \n不变 \r\n
recv(sock, &c, 1, MSG_PEEK);
if(c == '\n'){
recv(sock, &c, 1, 0);
}
else{
c = '\n';
}
} //\n
line[i++] = c;
}
else{
break;
}
}
line[i] = 0;
return i;
}
void clearHeader(int sock)//明却报头 即:获得空行
{
char line[MAX];
do{
getLine(sock, line, MAX);
}while(strcmp(line, "\n") != 0);
}
void echoError(int sock, int statusCode)
{
switch(statusCode){
case 404:
// show_404();
break;
default:
break;
}
}
int echoHtml(int sock, char path[], int size)
{
//通过他的第一行就直接可以知道他要访问那个资源
char line[MAX];
clearHeader(sock);
int fd = open(path, O_RDONLY);//open函数打开指定的文件,只读方式打开
if(fd < 0){
return 404;
}
char *stuff = path + strlen(path) - 1;
while(*stuff != '.'){
stuff--;
}
//一个响应报文发两次给TCP,其实TCP是面向字节流,所以不管你分几次,TCP都不管,只交给http来解释
sprintf(line, "http/1.0 200 OK\r\n");
send(sock, line, strlen(line), 0);//发送数据
if(0 == strcmp(stuff, ".html")){
sprintf(line, "Content-Type: text/html\r\n");
}
// else if(0 == strcmp(stuff, ".css")){
// sprintf(line, "Content-Type: text/css\r\n");
// }else if(0 == strcmp(stuff, ".js")){
// sprintf(line, "Content-Type: application/x-javascript\r\n");
// }else{
// sprintf(line, "Content-Type: text/html\r\n");
// }
send(sock, line, strlen(line), 0);
sprintf(line, "Content-Length: %d\r\n", size);//size获得文件大小
send(sock, line, strlen(line), 0);
sprintf(line, "\r\n");
send(sock, line, strlen(line), 0);
//打开一个文件,用while循环读多少写多少 可以 但是太搓了
//用sendfile()函数,将文件的描述符给socket让socket自己来决定咋调咋读
//从fd里将数据读出来然后发送给sock
sendfile(sock, fd, NULL, size);
close(fd);
}
int exeCgi(int sock, char *method, char *path, char *query_string)
{
char line[MAX];
int content_length = -1;
if(strcmp(method, "GET") == 0){
clearHeader(sock);
}else{
do{
getLine(sock, line, MAX);
if(strncmp(line, "Content-Length: ", 16) == 0){
content_length = atoi(line + 16);
}
}while(strcmp(line, "\n") != 0);
if(content_length == -1){
return 404;
}
}
///////////////////////////////////////
}
void *handlerRequest(void *arg)//真正对客户进行服务的函数接口,args是接收的客户端申请的文件描述符
{//http报文第一行 请求方法 请求的资源 请求的版本信息
int sock = (int)arg;
char line[MAX];//根据http报文形式来获得第一行信息
char method[MAX/16]; //用来存储请求方法的字段
char url[MAX]; //
char path[MAX]; //
int i = 0;
int j = 0;
int statusCode = 200;//状态码
int cgi = 0;
char *query_string = NULL;
getLine(sock, line, MAX);//获得报文的第一行的信息 将请求方法分离出来
while(i < sizeof(method)-1 && j < sizeof(line) && !isspace(line[j])){
method[i] = line[j]; //获得第一个字段即:方法字段 将这个字段存储进method数组里
i++, j++;
}
method[i] = '\0';
if(strcmp(method, "GET") == 0){ //如果是get方法 不处理
}
else if(strcmp(method, "POST") == 0){
cgi = 1;
}
else{ //如果不是POST也不是GET那么处理不了报错并结束处理
clearHeader(sock); //把头部清掉
statusCode = 404; //
goto end;
}
while(j < sizeof(line) && isspace(line[j])){//让j指针获得URL字段
j++;
}
i = 0;
while(i < sizeof(url) - 1 && j < sizeof(line) && !isspace(line[j])){
url[i] = line[j]; //url字段保存在url数组中
i++, j++;
}
url[i] = '\0';
//走到这里说明他就是GET方法或者POST方法
//上传数据 POST GET 方法都能传参 参数位置不同 服务器要处理的数据cgi GET方法传参需要cgi POST方法都需要cgi
//去除掉url中的cji
printf("method: %s, url: %s\n", method, url);//打印出此次方法与URL字段
//URL字段即是要访问的网址
if(strcmp(method, "GET") == 0){
query_string = url;
while(*query_string != '\0'){
if(*query_string == '?'){//获取到? 那么后面的就不是路径是带参的get方法的参数
cgi = 1;
*query_string = '\0'; // /a/b/c.html\0a=100&b=200
query_string++;
break;
}
query_string++;
}
}
//method, url, query_string
//根据URL将需要访问的目录加上web放在path数组中
//从当前的web服务器目录下找
sprintf(path, "web%s", url); //path表示从当前目录下的web目录下找
if(path[strlen(path)-1] == '/'){//当别人只给一个斜杠,你需要把首页给人家
strcat(path, "index.html");//在path里放入index.html
}
//将需要的资源目录已经准备好了
//一个命令 stat 命令 可以获得文件的大小,文件名称
//stat() 系统调用函数
struct stat st;
//获得文件属性信息
//stat接口用来判断
if(stat(path, &st) < 0){
//文件不存在
clearHeader(sock);
statusCode = 404;
goto end;
}
else{
//path结尾一定不带‘\’
if(S_ISDIR(st.st_mode)){//判断它是不是目录
strcat(path, "/index.html");//给它一个欢迎界面
}
//判断是否是cgi
//如果是cgi
if(cgi){
statusCode = exeCgi(sock, method, path, query_string);
}
else{
//如果不是cgi直接给他响应一个path资源
statusCode = echoHtml(sock, path, st.st_size);
}
}
end:
if(statusCode != 200){
echoError(sock, statusCode);//如果是错误码那么就返回一个错误信息的页面
} //因为sock是全双工的那么就需要从socket里面度,写也往socket里面写
close(sock);
}
int startup(char *ip, int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(ip);
local.sin_port = htons(port);
int len=sizeof(local)
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("bind");
exit(3);
}
if(listen(sock, 5) < 0){
perror("listen");
exit(4);
}
return sock;
}
void usage(const char *proc)
{
printf("Usage: %s [ip] [port]\n", proc);
}
// ./myTomcat ip 8080
int main(int argc, char *argv[])
{
if(argc != 3){
usage(argv[0]);
return 1;
}
signal(SIGPIPE, SIG_IGN);
int listen_sock = startup(argv[1], atoi(argv[2])); //ip, port//根据ip和udp来获得监听文件
for(;;){
struct sockaddr_in peer; //获得客户端的套接字
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);//接收客户端申请
if(sock < 0){
continue;
}
printf("get a new link... create thread handler!\n");//获得一个新连接
pthread_t tid;
pthread_create(&tid, NULL, handlerRequest, (void *)sock);//用多线程来服务此次申请
pthread_detach(tid);
}
}