在Linux下我们使用HTTP协议进行通讯,即客户端在浏览器请求服务器的某个服务(页面)。我们在主机上进行测试的时候,可以开启httpd服务以处理客户端的请求,一般来说我们的页面放在/var/www/html底下,我们这些操作需要切换到root用户。
开启httpd服务 : service httpd start
查看httpd状态 : service httpd status
关闭httpd服务 : service httpd stop
可以看出我不在root用户是开启不了httpd服务滴,所以我们切换到root用户在开启一次。
现在httpd服务已经开启了,我们再来看看我的/var/www/html底下我自己写了一个静态的html页面
我们接下来用火狐浏览器请求一下我主机上的这个index.html页面,看看会不会将页面给我返回。
可以看出httpd将我们请求的这个index页面给我返回了,那么关于httpd这个服务器怎么实现呢?我们一起来看看
我们知道http是应用层的协议,它默认使用的传输层协议是tcp,现在我们来模拟一下http的服务器书写。
首先应注意的是TCP连接从建立到关闭的过程中,客户端(浏览器)仅给服务器发送了一个HTTP请求(比较小),而服务器会将客户端请求的页面响应传给客户端:,在浏览器上显示)
第一行是请求行,剩下这2-4行是请求头部。
1:请求行
由3部分组成,分别为:请求方法、URL(网址)以及协议版本,之间由空格分隔。
其中GET是请求方法,表示客户端以只读的方式申请资源。常见的HTTP请求方法如下:
上面的http://www.baidu.com/index.html即是目标资源的URL ,http是获取目标资源的应用层协议,其他常见的协议还有ftp,file等。www.baidu.com指定了目标资源所在的主机(DNS解析获取IP),index.html即资源文件名称。
HTTP/1.0表示客户端使用的HTTP版本号是1.0,目前的HTTP主流版本是1.1
2:请求头部
请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔
常见请求头如下:
头部字段名 | 说明 |
Host | 接受请求的服务器地址,可以是IP:端口号,也可以是域名 |
User-Agent | 发送请求的应用程序名称(客户端使用的程序) |
Connection | 指定与连接相关的属性,如Keep-Alive(长连接)或者close(短连接) |
Accept-Charset | 通知服务端可以发送的编码格式 |
Accept-Encoding | 通知服务端可以发送的数据压缩格式 |
Accept-Language | 通知服务端可以发送的语言 |
对于头部字段Connection有两种连接属性:长连接与短连接
短连接是指客户端和服务器之间的一个TCP连接只能为一个HTTP请求服务,处理完一个HTTP请求之后,服务器就主动将TCP连接关闭了,之后同一个客户端再发送一个HTTP请求就需要重新建立连接。长连接与之相反,指多个请求可以使用同一个连接。但是长连接编程上稍微复杂一点,但性能有了很大提高。
3:分界
在所有头部字段之后,HTTP请求必须包含一个空行,以标识头部字段的结束。空行必须只包含一个回车符与换行符,不能有其他字符,甚至是空白字符。
4:正文
在空行之后,HTTP请求可以包含可选的消息体,比如get请求就没有请求正文。但如果消息体非空,则HTTP请求报头字段中必须包含描述消息体长度的字段“Content-Length”。
http应答的部分内容如下:
第一行是状态行,其他2--7行是应答的头部字段
1:状态行
由3部分组成,分别为:协议版本,状态码,状态信息,之间由空格分隔
状态代码为3位数字,200~299的状态码表示成功,300~399的状态码指资源重定向,400~499的状态码指客户端请求出错,500~599的状态码指服务端出错(HTTP/1.1向协议中引入了信息性状态码,范围为100~199)
2:响应头部
与请求头部类似,响应报文添加了一些附加信息
响应头部字段 | 说明 |
Server | 服务器应用程序软件的名称和版本 |
Content-Type | 响应正文的类型(是图片还是二进制字符串) |
Content-Length | 响应正文长度 |
Content-Charset | 响应正文使用的编码 |
Content-Encoding | 响应正文使用的数据压缩格式 |
Content-Language | 响应正文使用的语言 |
对于示例Content-Type:text/html;charset=gbk表示目标文档index.html是text类型中的html文档。charset是text文档类型的一个参数,用于指定文档的字符编码。
示例的Cookie是服务器发送给客户端的一个特殊信息,客户端每次向服务器发送请求的时候都需要带上这些信息(通过请求字段“cookie”),这样服务器就可以区分不同的客户了。基于浏览器的自动登陆就是用cookie实现的(客户端会将cookie的信息保存在本机上,只需验证即可实现自动登陆)
3:与HTTP请求相同,头部字段完成之后也需要一个空行进行区分头部和响应正文。
4:响应正文
即发送客户端请求的页面内容,需要在头部中声明正文的长度
首先我们将我们服务器的端口号绑定为80/8080端口(浏览网页服务默认的端口号都是80/8080),那么我们首先要关闭httpd服务,否则就将端口号占了(root用户),接下来的操作代码中体现,话不多说上代码。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//仿写http服务器,需要关闭httpd以及使用root用户操作
void SendError(int c) //发送错误页面(连带头部)
{
char buff[1024]={0};
strcpy(buff,"HTTP/1.1 404 NOT FOUND\n\r");
strcat(buff,"Server:myhttp/1.0\n\r");
strcat(buff,"Content-Length: ");
sprintf(buff+strlen(buff),"%d",0);
strcat(buff,"\n\r");
strcat(buff,"Content-Type:text/html;charset=utf-8\n\r");
strcat(buff,"\n\r"); //空行标识数据部分和头部分开
strcat(buff,"404 NOT FOUND"); //发送给客户端的数据,用于显示
send(c,buff,strlen(buff),0);
}
void SendHead(int size,int c) //发送给客户端应答报头
{
char buff[1024]={0};
strcpy(buff,"HTTP/1.1 200 OK\n\r");
strcat(buff,"Server:myhttp/1.0\n\r");
strcat(buff,"Content-Length: ");
sprintf(buff+strlen(buff),"%d",size);
strcat(buff," \n\r");
strcat(buff,"Content-Type:text/html;charset=utf-8\n\r");
strcat(buff,"\n\r"); //空行标识数据部分和头部分开
send(c,buff,strlen(buff),0);
}
void AnayRequest(char *buff,char *pathname) //解析参数获取到请求的页面地址
{
//GET /index.html HTTP/1.1 Linux上获取到的请求第一行,并没有域名及IP
char *p=strtok(buff," ");
p=strtok(NULL," "); //获取到了/index.html
strcpy(pathname,"/var/www/html"); //请求的页面在本机的var/www/html底下
strcat(pathname,p); //将分割的页面名称与之相连接,得到请求页面的绝对地址
}
void SendData(char *pathname,int c) //发送页面
{
struct stat st;
if(-1!=stat(pathname,&st)) //通过路径获取文件属性
{
SendHead(st.st_size,c); //发送应答报头,传入请求页面文件的大小
}
else
{
SendError(c); //所有错误都发送这个404错误(只发送一句话而已)
return;
}
int fd=open(pathname,O_RDONLY);//发送页面正文给客户端进行显示,即请求的页面
if(fd==-1)
{
SendError(c);
return;
}
while(1)
{
char buff[128]={0};
int n=read(fd,buff,127);
if(n<=0)
{
close(fd);
break;
}
send(c,buff,strlen(buff),0); //发送页面内容给客户端
}
}
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(80);
ser.sin_addr.s_addr=inet_addr("127.0.0.1"); //本地回环地址
int res=bind(sockfd,(struct sockaddr *)&ser,sizeof(ser));
assert(res==0);
listen(sockfd,5);
while(1)
{
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
if(c<0)
continue;
char buff[1024]={0}; //短连接不需要循环读取
int n=recv(c,buff,1023,0);
if(n<=0)
{
close(c);
continue;
}
printf("%s\n",buff); //将客户端HTTP请求内容输出
char pathname[128]={0};
AnayRequest(buff,pathname); //分析参数
SendData(pathname,c); //发送请求的html网页
close(c);
}
}
ps:Linux上HTTP请求报头的请求行与我上面解析的有点不同,URL直接是资源文件名称,没有主机名(IP或者域名)
运行结果:
右侧的代码是客户端的HTTP请求
我们再来测试一个不存在的网页,看看错误页面
可以看出我吧请求资源页面写错了,就会给我返回一个我服务器给客户端发送的错误输出404 NOT FOUND字样,就酱。