http

项目:HTTP 服务器框架

步骤:
一. 背景调研(当前不涉及)
二. 需求分析(最核心的一步)
(有产品经理作出完整的文档)

实现一个服务器程序
支持HTTP协议的服务器
从请求角度上,支持GET方法和POST方法
从响应的角度上,支持静态页面和动态页面

静态页面:返回一个服务器上的本地文件
动态页面:根据用户的输入实时计算生成一个响应结果页面
12
能够使用浏览器访问

三. 概要设计(分成几个模块,模块之间的关系)
HTTP服务器框架:

通用的HTTP服务器框架(业界知名的服务器框架有:Nginx 、 httpd(apache))
1). 服务器初始化(socket 的初始化)
2). 处理HTTP请求

从 socket 中读取数据,并且按照HTTP协议的格式解析数据;
根据输入的请求的不同决定是按照静态页面处理还是动态页面处理;
如果是按照静态页面,直接将服务器上对应的文件返回到客户端中;
如果时按照动态页面,就将输入的信息交给对应的 CGI 程序,由 CGI 程序来计算,生成响应。把响应的结果交还给HTTP服务器框架,由框架完成数据返回到客户端的过程。
业务相关的 CGI 程序
1). 根据不同的业务,来分别实现不同的 CGI 程序;
2). 读取 HTTP 服务器框架交给它的参数,根据自身的业务流程计算生成 HTTP 响应数据,交还给框架;

CGI:是一种标准或协议
CGI 优点:

完成了解耦,让通用的HTTP 服务器框架和业务完全的解耦和
CGI 程序可以用任何的编程语言来实现

CGI 的实现过程:
HTTP 服务器框架创建子进程,子进程根据所请求得 CGI 程序的路径(HTTP 请求中的url_path)进行程序替换,CGI 程序就可以根据业务进行计算了。
1. HTTP 服务器是通过什么样的方式把必要的输入参数传给 CGI 程序:进程间通信中的管道 + 环境变量
2. CGI 程序计算的结果通过通过管道写回给客户端

四. 详细设计(模块的实现细节)
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/wait.h>
#include “http_server.h”

typedef struct sockaddr sockaddr;//定义某种类型
typedef struct sockaddr_in sockaddr_in ;//因为它在,所以你就可以去给它起一个别名。
//一次从socket中读取一行数据,把数据放在buf缓冲区中;如果读取失败,返回值就是-1;
//遇到\n或\r\n或\r证明读取完成(一般为’\n’,但是有些浏览器换行符是’\r’,’\r\n’)
int ReadLine(int sock,char buf[],ssize_t size){//在这里读取socket中的一行,其实就是读取我们在浏览器中所输入的一行。
//size表示缓冲区的长度
//1.从socket中一个字符一个字符的读取
char c=’\0’;
ssize_t i=0;//i表示当前读了多少个字符
//结束条件;
//a)读到的长度太长,达到了缓冲区长度的上限
//b)读到了结束标志,并把所有结束标志都转换成\n(此处要兼容\r,\r\n的情况,统一把这两种情况
//转换为\n

while(i<size-1&&c!='\n'){
	ssize_t  read_size=recv(sock,&c,1,0);
    //一次读一个字符,把字符读到c总,0表示flag为0
	//读取失败有两种
	if(read_size<0){
		return -1;
	}
	if(read_size==0){
		//因为预期是要读到\n这样的换行符,
        //结果还没有读得到就先读了EOF,这种情况我们也暂时认为是失败的
		return -1;
	}
    //此时表示大于0的情况
	if(c=='\r')
	{
		recv(sock,&c,1,MSG_PEEK);//要懂MSG_PEEK选项的含义。这次不要似懂非懂。加油。
		if (c=='\n'){//如果下一个字符是\n
			//此时的分隔符就是\r\n,那么就不要这个字符了,下次再读取的就是下一行的开始
			recv(sock,&c,1,0);
		}else 
		{
			//当前分隔符确定是\r,此时把分隔符转换成\n
			c='\n';
		}
	}
	//只要上面的c读到的是\r,那么if结束后,c都变成了\n
	//这种方式就是把前面的\r和\r\n两种情况都统一成了\n
	buf[i++]=c;

}
buf [i]='\0';
return i;//真正想缓冲区中放置的字符的个数

}

int Split(char input[], const char split_char,char output[],int output_size)//在对读取后的字符串进行切分。
{
//split_char:分隔符;
//使用strtok_r
int i=0;
char *tmp=NULL;//保存上次的切分结果
char * pch;
//使用线程安全的strtok_r代替strtok
//这一点是以后非常容易出错的一点
pch = strtok_r (input,split_char,&tmp);
while (pch != NULL)
{
if (i>=output_size)
{
return i;
}
output[i++]=pch;//保存在这里面,这是一个输出缓冲区的大小。
pch = strtok_r (NULL, split_char,&tmp);//继续前面的切分继续切分
}

//printf ("Split\n");
return i;//返回切分的个数

}
//strtok是一个字符串切分函数,一次只能切分一个部分,但是实际中大多期望一次切分完所有的,所以采用
//循环调用的方式。
//strtok的缺陷:可能会导致服务器崩溃,strtok可以接着上一次的切分结果进行切分,说明strtok中保存
//了上一次的切分结果,strtok不能用栈上的变量保存,因为函数一旦结束,函数栈上的空间就没有了;
//也不能用堆上的数据保存,因为不确定变量什么时候进行释放。调用一次释放一次,又会导致内存泄漏
//静态变量的生命周期是全局的,使用静态变量来保存,函数结束,静态变量仍然存在;再次调用strtok,变量
//的值,仍然为上一次的结果。所以strtok内部采用静态变量来保存上一次切分的位置。函数内部使用静态变量
//意味着不可重入,说明此函数也是一个线程不安全函数(多个线程同时调用,可能会出现逻辑上的问题。)
//Split函数是一个多线程的函数,每次收到一个请求,就创建一个线程,线程都要进行first_line解析,
//每次解析都得调用strtok_r进行切分,在一个多线程的环境下调用一个线程不安全函数,很可能导致程序崩溃
//不应该使用线程不安全函数在一个多线程环境下进行调用。
//解决这个问题的办法:a、加互斥锁:互斥锁可行,但是没必要。枷锁意味着函数在不同的线程之间要相互竞争
//锁,会导致程序整体的效率被拖慢,很大一部分时间都在等待锁
//b、使用strtok_r函数,是strtok的线程安全版本,不再使用内部的缓冲区,而是通过函数参数的形式显示的
//指定一个缓冲区。这个缓冲区提供一个空间,帮助函数保存上一次的执行结果。
//类似使用内部静态缓冲区的线程不安全函数:a、inet_ntoa把数字的IP地址转换为点分十进制的ip地址,通过
//静态缓冲区保存点分十进制的字符串,同样也是线程不安全;为了解决这个问题通常使用inet_ntop函数解决
//线程不安全问题,不在使用静态缓冲区来保存结果,而是通过栈上开辟内存,把栈上缓冲区中的内容传到
//函数里面,然后基于栈上缓冲区访问结果而解决问题
//b、random函数,也是在内部有一个静态缓冲区,也是线程不安全函数,为了解决这个问题,使用random_r在
//多线程环境下使用

//解析首行
int ParseFirstLine(char first_line[],char**p_url,char **p_method)
{
//需要找到哪部分是url,哪部分是method,此时我们考虑
//一个指针指向方法的首地址,一个指针指向url的首地址的情况
//为了得到每一个部分,把空格替换成\0,再通过指针指向每一个部分的起始位置,就得到了每一个部分
//对应的字符串,破坏了原有的字符串结构,将其切分成了若干个部分,若干个部分之间不再用空格
//切分,直接用\0切分,我们再得到每一个部分起始位置的指针,就可以通过c风格字符串的方式,得到
//方法、url……
//char **:参数url和method是通过函数参数传递,是一个输出型参数,输出两个指针的指向位置,只能
//用二级指针的方式修改指针的指向。所以需要将指针的地址传递给函数。
//把首行按空格进行字符串切分
//切分得到的每一个部分,就放在tok数组里
//返回值,就是tok数组包含几个元素。

char  *tok[10];
//最后一个参数10表示tok数组中最多可以放几个元素
int tok_size=Split(first_line," ",tok,10);
if (tok_size!=3)
{
	printf ("Split  failed! tok_size =%d\n",tok_size);//因为要切分成三部分,方法,url和路径
	return -1;
}
*p_method=tok[0];
*p_url=tok[1];

// printf ("ParseFirstLine\n");
return 0;

}

//把url字符串进一步切分:url_path指向问号之前、空格之后的字符串;query_string指向问号之后的字符串,这在这里就是对url字符串进行的是进一步的切分。
//把问号替换成’\0’
int ParseQueryString (char *url,char **p_url_path,char **p_query_string )
{
*p_url_path=url;//从url起始位置开始遍历
char *p=url;
for (;*p!=’\0’;++p)
{
if (*p==’?’)
{
*p=’\0’;
*p_query_string =p+1;
return 0;
}
}
//循环结束都没有找到?,说明这个请求不带query_string
*p_query_string=NULL;
return 0;
}

//解析header,解析header的目的是为了读value。
int ParseHeader (int sock ,int *content_length)
{
//1.循环从socket中读取一行
//2.判断当前行是不是Content—Length
//3.如果是Content-Length就直接把value读出来
//4.如果不是就直接丢弃
//5.读到空行,循环结束。
char buf [SIZE]={0};//定义一个缓冲区
while (1)
{
//1.循环从socket中读取一行
ssize_t read_size=ReadLine (sock,buf,sizeof (buf));
//sizeof(buf)没有减1,是什么原因?ReadLine函数中已经考虑了\0的情况,此时可以不考虑\0
//的情况,但是双重保证就更合适。
//处理读失败的情况
if (read_size<=0)
{
return -1;
}
//处理读完的情况
//buf里面只有一个\n,说明读完了
if (strcmp (buf ,"\n")==0)
{
return 0;
}
//2.判定当前行是不是Content-Length
//如果是Content-Length就直接把value读出来
//如果不是就直接丢弃
const char *content_length_str="Content-Length: ";
if (content_length!=NULL&&strncmp(buf,content_length_str,strlen(content_length_str))==0)
{
*content_length=atoi(buf+strlen(content_length_str));
//strncmp比较前n个字符;注意“Content-Length”的写法,只有这种格式。
}
}
return 0;
//更复杂的版本需要把这个字符串以键值对的方式存储(key和value)
}
//刚刚保存时出现了’readonly’ option is set (add ! to override)
//解决方案:按下ESC,输入:set noreadonly;然就就可以使用:wq正常保存了

void Handler404(int sock)
{
//构造一个完整的HTTP响应
//状态码是404
//body部分也应该是一个404相关的错误页面。
const char* first_line=“HTTP/1.1 404 Not Found\n”;
const char* type_line=“Content-Type: text/html;charset=utf-8\n”;//提示浏览器按照utf-8方式解码于下面成双重保证
const char * blank_line="\n";
//html是一个文本文件,用于说明标题,……详细的信息
const char * html="<meta http-equiv=“content-type” content=“text/html;charset=utf-8”>"

您的页面没找到!!!

”;//提示浏览器按照utf8方式解码//否则会产生乱码//
//可以通过:set fileencode查看编码格式,charset叫做字符集
//构造了一个http响应并且把此响应写会到socket之中,返回浏览器。
send (sock,first_line ,strlen(first_line),0);
//send有一个额外的选项flag
send (sock,type_line ,strlen(type_line),0);
send (sock,blank_line ,strlen(blank_line),0);
send (sock,html ,strlen(html),0);
return;
}

void PrintRequest (Request*req)
{
printf (“method: %s\n”,req->method);
printf (“url_path: %s\n”,req->url_path);
printf (“query_string: %s\n”,req->query_string);
printf (“content_length: %d\n”,req->content_length);
return;
}

int IsDir(const char *file_path)
{
struct stat st;
int ret=stat(file_path,&st);
if (ret<0)//可能是文件不存在,则不是目录
{
return 0;
}
if (S_ISDIR(st.st_mode))
{
//通过这个宏,判定st_mode选项是不是目录,
return 1;
}
return 0;
}

//http服务器的根目录:用户指定的任意一个存在的目录作为根目录,url_path中访问的文件都是以http服务器的
//根目录为基准,取相对路径。假如:在http_server的路径下有一个wwroot的目录,这个目录下包含了一个
//index.html的文件,如何访问这个index.html,可以通过/index.html访问,是在wwwroot下取相对路径index.html
//若是该目录下有一个文件image/1.jpg,想要取到1.jpg访问时path变为/image/1.jpg
void HandlerFilePath(const char*url_path,char file_path[])
{
//把url_path转换成本地真实的文件路径,即进行字符拼接。
//(a,给url_path 加上前缀名(HTTP服务器的根目录)
//url_path–>/index.html
//file_path—>./wwwroot/index.html
sprintf (file_path,"./wwwroot%s",url_path);
//向file_path的输出缓冲区中进行拼接
//b)例如url_path是/,此时url_path 其实就是一个目录。
//如果是目录的话,就给这个目录之中追加一个文件(默认文件)index.html
//url_path, /或者 /image/
if (file_path[strlen(file_path)-1]==’/’)
//判定最后一个字符是不是’/’
{
strcat(file_path,“index.html”);
//字符串追加,因为以’/‘结尾,追加时,不需要加’/’
}
//c)url_path=>/image,
if (IsDir(file_path))
{
strcat (file_path,"/index.html");
}
return ;

}

//帮助获取文件的大小
ssize_t GetFileSize(const char* file_path)
{
struct stat st;
//stat函数可以获取一些文件的源信息
int ret=stat(file_path,&st);
//把文件对应的原信息放到结构体里
if (ret<0)
{
//打开文件失败,很可能是文件不存在
//此时直接返回文件长度为0
return 0;
}
return st.st_size;
// st_size是文件的大小
}

int WriteStaticFile (int sock,const char* file_path)
{
//1.打开文件
int fd=open (file_path,O_RDONLY);
//打开文件失败的情况:a、文件描述符不够用
//b、文件不存在(用户输入的可能不正确)
if (fd<0)
{
perror (“open”);
return 404;
}
//2.把构造出来的HTTP响应写到socket之中
//1).写入首行
//2).写入head
//4).写入body(文件内容)
const char * first_line=“HTTP/1.1 200 OK\n”;
send (sock,first_line,strlen(first_line ),0);
//插入图片
//图片和文本应该分开进行解析,或者让浏览器自动识别
//const char* type_line=“Content-Type: text/html;charset=utf-8\n”;//用此方式,解析照片时会发生错误
//const char* type_line=“Content-Type: imag/png;charset=utf-8\n”;
//send (sock,type_line ,strlen(type_line),0);
//两个都不添,让浏览器自己决定
//(3)写入空行
const char * blank_line="\n";
send (sock,blank_line ,strlen(blank_line),0);
/*ssize_t file_size=GetFileSize(file_path);
//获取文件的真实长度
ssize_t i=0;
for(;i<file_size;i++)
{
char c;
//读一个字节
read(fd,&c,1);
//用户中调用read就是把文件中的每一个字符都拷贝一份到用户的缓冲区里面
//写一个字节
send (sock,&c,1,0);
//把刚刚读取到的数据写回网卡
//这种方式会在用户空间和内核空间中拷贝多次
}
*/
sendfile(sock,fd,NULL,GetFileSize(file_path));
//sendfile函数,第一个参数时输出的参数;第一个参数是输入的参数,第三个参数文件的参数;第四个参
//数是拷贝的字节数,文件本身的大小
//把文件的数据直接在内核中拷回到网卡,只在内核中进行
//3.关闭文件
close (fd);
return 200;
}

//处理静态文件
int HandlerStaticFile (int sock ,Request *req)
{
//1根据url_path获取到文件在服务器上的真实路径。
char file_path[SIZE]={0};
HandlerFilePath(req->url_path,file_path);//file_path输出参数
//2。读取文件,把文件的内容直接写道socket之中
int err_code=WriteStaticFile (sock,file_path);
return err_code;
}

int HandlerCGIFather(int new_sock,int father_read,int father_write,int child_pid,Request *req){
//1如果是POST请求,就把body写到管道中
if (strcasecmp(req->method,“POST”)==0){
int i=0;
char c=’\0’;
for (;icontent_length;++i){
//每一次循环都读取一个字符,读一个字符向管道中写一个字符。
read(new_sock,&c,1);
//前面已经对CGI读取了很多次,在头部被解析之后才调用,
//缓冲区中首行,haader,空行都已经读取结束,存放在缓冲区中了
//所以此时读到的数据是body
write(father_write,&c,1);
}

}
//2构造HTTP响应
const char *  first_line="HTTP/1.1 200 OK\n";
send (new_sock,first_line,strlen(first_line ),0);
const char*  type_line="Content-Type: text/html;charset=utf-8\n";
//const char*  type_line="Content-Type: image/png;charset=utf-8\n";
send (new_sock,type_line ,strlen(type_line),0);
//两个都不添,让浏览器自己决定
const char *  blank_line="\n";
send (new_sock,blank_line ,strlen(blank_line),0);
//循环的从管道中读取数据并写入数据到socket
char  c='\0';
//管道如果所有的写端全部关闭再尝试继续读,read就会返回0
//管道读端全部关闭,再尝试继续写,SIGPIPE信号导致进程异常终止
while (read(father_read,&c,1)>0){//从father_read中读取数据,读取的数据放到c里面,
	send(new_sock,&c,1,0);
}
//4回收子进程的资源
waitpid(child_pid,NULL,0);
//wait()//handlerrequet函数是在新线程中执行的,不存在阻塞不能执行多线程的情况。
//一个服务器同一时间收到的请求是非常多的,每一个请求都得创建单独的线程,每个线程都有
//可能处理CGI的逻辑,就有可能创建子进程
//此处若是调用wait,线程都调用一个wait,wait是无差别的等待,
//可能导致线程1创建的线程被线程一回收
return 200;

}

int HandlerCGIChild(int child_read,int child_write,Request* req){
//1.设置必要的环境变量
char method_env[SIZE]={0};
sprintf (method_env,“REQUEST_METHOD=%s”,req->method);
putenv(method_env);//设置环境变量
//还需要设置QUERY_STRING(get方式时需要设置)或者是CONTENT_LENGTH(post方法需要设置)
if(strcasecmp(req->method,“GET”)==0){
char query_string_env[SIZE]={0};
sprintf (query_string_env,“QUERY_STRING=%s”,req->query_string);
putenv(query_string_env);
}else {
char content_length_env[SIZE]={0};
sprintf (content_length_env,“CONTENT_LENGTH=%d”,req->content_length);
putenv(content_length_env);//设置环境变量
}//名字必须满足CGI标准
//2把标准输入输出重定向到管道里
dup2(child_read,0);//把第二个参数重定向到第一个参数
dup2(child_write,1);
//3对子进程进行程序替换
// url_path: /cgi-bin/test
// file_path: ./wwwroot/cgi-bin/test
//根据url_path获取对应的file_path
char file_path[SIZE]={0};
HandlerFilePath(req->url_path,file_path);//把req->url_path生成file_path

execl(file_path,file_path,NULL);//
    exit(1);//当程序替换失败时。一定让子进程终止,如果子进程不终止,因为父子进程是同一块代码,父进程一直在Listen状态一直等待,子进程也会等待,所以要直接结束进程,避免一直等待端口数据的返回;

//有 exec l lp le v vp ve,首先看知不知道可执行文件的完整路径 ,如有就可以不带P,因为P是在PATH中找,再看要不要环境变量,如果不用,就不需要带e,这里因为通过putenv ()的方式直接设到了ENV里面所以不用,说一直接用execl()
return 200;
}

int HandlerCGI(int new_sock,Request *req)//第二个参数是格式化的请求数据
{
int err_code=200;
//1.创建一对匿名管道
int fd1[2],fd2[2];
int ret =pipe(fd1);
//创建第一对匿名管道
if (ret<0)
{
return 404;
}
ret=pipe (fd2);
//创建第二对匿名管道
if (ret<0)
{
//走到这一步,说明上面的文件描述符已经分配好了,所以需要关闭前面创建好的文件描述符
close(fd1[0]);
close(fd1[1]);
return 404;
}
//fd1,fd2这种变量名的描述性太差,后面直接用的话
//是非常容易弄混的,所以直接在此处定义几个
//更加明确的变量名来描述该文件描述符的用途
int father_read=fd1[0];
int child_write=fd1[1];
int father_write=fd2[1];
int child_read=fd2[0];
//2.创建子进程
ret=fork();
//3.父子进程各执行不同的逻辑
if (ret>0){

	//father
	//此处父进程优先关闭这两个管道的文件描述符
	//是为了后续父进程从子进程这里读数据时,能够读到EOF,对于管道来说,所有写端关闭,
    //继续读,才有EOF,而此时所有写端,一方面是父进程需要关闭,另一方面子进程也需要关闭。
    //所以此处父进程先关闭不必要的写端之后,后续子进程用完了
	//直接关闭,父进程也就读到了EOF
	close (child_read);
	close(child_write);
    //如果写端不关闭,随时都有可能有人往管道中写,此时应保证管道的写端全都关闭(父进程先
    //关闭,子进程用完了也关闭,两者都关闭了,才意味着全部写端端关闭,全部写端关闭,父进程一直在
    //再去读,才能读到read返回的结果
    //因为父进程先创建管道,再fork,父子进程管道各占一半
	err_code=HandlerCGIFather(new_sock,father_read,father_write,ret,req);
}else if (ret==0){

	//child
	close (father_read);
	close (father_write);
	err_code=HandlerCGIChild(child_read,child_write,req);
}else{
    //fd[]未初始化时,是一个随机值,直接关闭会出错
	perror("fork");
    err_code=404;
	goto END;
}

END:
perror(“fork”);
close (fd1[0]);
close (fd1[1]);
close (fd2[0]);
close (fd2[1]);

//4.收尾工作和错误处理
return 200;

}

void HandlerRequest(int new_sock)
{
int err_code=200;//错误码初始200,默认没错
//1.读取并解析请求(反序列化)
Request req;
memset (&req,0,sizeof (req));
//a)从socket中读取出首行
if (ReadLine(new_sock,req.first_line,sizeof (req.first_line))<0)
{
//失败处理
err_code =404;
goto END;
}
//b)解析首行,从首行中解析出url和method
if (ParseFirstLine(req.first_line,&req.url,&req.method))
{
//失败处理
err_code =404;
goto END;

}
//c)解析url,从url中解析出url_path和query_string
if (ParseQueryString(req.url,&req.url_path,&req.query_string))
{
	//失败处理
	err_code =404;
	goto END;

}
//d)解析Header,丢弃了大部分header,只读取Content—Length
if (ParseHeader(new_sock,&req.content_length))
{
	//失败处理
	err_code =404;
	goto END;

}
PrintRequest (&req);
//2.静态/动态方式生成页面
//3.把生成结果写回客户端上
if (strcasecmp(req.method,"GET")==0&&req.query_string==NULL)
{
	//a)如果是GET方式请求,并且没有query_string,
	//那么返回静态页面
	err_code=HandlerStaticFile(new_sock,&req);
}else  if (strcasecmp(req.method,"GET")==0&&req.query_string!=NULL)
{
	//b)如果是GET方式请求,并且有query_string,
	//那么返回动态页面
	err_code=HandlerCGI(new_sock,&req);
}else  if (strcasecmp(req.method,"POST")==0)
{
	//c)如果请求是POST类型的(一定是带参的,参数是通过body来传给服务器的),那么也返回动态页面
	err_code=HandlerCGI(new_sock,&req);
}else  
{
	//失败处理
	err_code =404;
	goto END;
}

//错误处理;直接返回一个404的HTTP响应

END:
if (err_code !=200)
{
Handler404(new_sock);
}
close(new_sock);
return;

}

void ThreadEntry(voidarg)// 因为在64位机中指针占8个字节,如果强转回int会有丢失数据的危险
//void*是一个指针,32位机器下占4个字节,64位机器下占8个字节,而一个int在32位和64位机器下都为4个字节
//在64位机器下,需要改为int64_t
{
int new_sock=(int)arg;
//使用HandlerRequest完成具体的处理请求过程这个过程单独提取出来是为了解耦和
//相当于线程入口函数只有一个包装,真正干活的是这个函数,这样最大的好处还是解耦和
//一旦需要把服务器改成多进程或者IO多路复用的形式,整体代码的改动都是比较小的
HandlerRequest(new_sock);
return NULL;
}

//服务器启动
void HttpServerStart(const char* ip,short port)
{
int listen_sock=socket(AF_INET,SOCK_STREAM,0);
if (listen_sock<0)
{
perror(“socket”);
return ;
}
//加上这个选项是为了重用TIME_WAIT连接//解决出现大量TIME_WAIT的现象
//服务器出现大量的TIME_WAIT状态,说明是服务器率先断开了连接,为了大量TIME_WAIT连接之后还能够
//有新的连接可用,那么就需要重用已有的TIME_WAIT,加上SO_REUSEADDR进行重用
//服务器出现大量的CLOSE_WAIT状态意味着代码出现了bug,四次挥手只出现了两次,没有正确的分配socket
//导致服务器出错,文件泄漏,需要找到对应的代码进行修复。
int opt=1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt));
//加上这个函数是为了使端口处于TIME-WAIT时复用地址.
//第一个参数表示要对哪一个参数进行设置,第二、三个参数决定了要给哪个选项设置一个值
//最后两个参数决定了要设置什么样的值,SO_REUSEADDR重用addr连接。opt为1表示开启选项
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(ip);
addr.sin_port=htons(port);

int ret=bind(listen_sock,(sockaddr*)&addr,sizeof (addr));
if (ret<0)
{
	perror("bind");
	return ;
}
ret=listen(listen_sock,5);
if (ret<0)
{
	perror("listen");
	return ;
}
printf ("ServerInit  OK\n");
while (1)
{
	sockaddr_in peer;
	socklen_t  len=sizeof (peer);
	int   new_sock=accept(listen_sock,(sockaddr*)&peer,&len);
	if (new_sock<0)
	{
		perror("accept");
		continue;
	}
	//使用多线程的方式来实现TCP服务器
	pthread_t  tid;
	//       printf ("ThreadEntry\n");
    //把new_sock传递给线程入口函数
	pthread_create(&tid,NULL,ThreadEntry,(void *)new_sock);
    //回收线程
	pthread_detach(tid);
}

}
//主函数的参数 ./http_server [ip] [port]
int main (int argc,char* argv[])
{
if (argc!=3)
{
printf (“Usage ./http_server [ip] [port]\n”);
return 1;
}
HttpServerStart( argv[1],atoi(argv[2]));
return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章