应用层网络实现一个服务器版的计算器及理解HTTP协议

应用层

满足我们日常需求的网络程序都在应用层。

协议是一种约定。socket api的接口,在读写数据时都是按照“字符串”的方式发送和接收的。但如果我们要传输一些“结构化的数据”该怎么办呢?例如,我们需要实现一个服务器版的加法器,客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。这里有两个方案可以解决。

方案一:

  • 客户端发送一个形如“1+1”的字符串;
  • 这个字符串中有两个操作数,都是整形;
  • 两个数字之间有一个字符是运算符,运算符只能是+;
  • 数字和运算符之间没有空格。...

方案二:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串(此过程叫“序列化”,方便传输),接收到数据的时候再按照相同的规则把字符串转换回结构体(此过程叫“反序列化”,方便使用)。

相关代码:

Makefile:

.PHONY:all
all:client server
CC=gcc

client:client.c
	$(CC) -o $@ $^ 
server:server.c
	$(CC) -o $@ $^ -lpthread

.PHONY:clean
clean:
	rm -f client server

proto.h

typedef struct Request
{
    int x;
    int y;
    int op;
}Request;

typedef struct Response
{
    int sum;
    int status;
}Response;

client.c

#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<error.h>
#include<string.h>
#include<sys/types.h>
#include<stdlib.h>
#include"proto.h"

typedef struct sockaddr sockaddr;

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("Usage:%s ip port\n",argv[0]);
        return 1;
    }
    //创建套接字
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        return 2;
    }
    printf("Socket:%d\n",sock);
    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(atoi(argv[2]));
    server.sin_addr.s_addr=inet_addr(argv[1]);
    
    //建立连接
    if(connect(sock,(sockaddr*)&server,sizeof(server))<0)
    {
        perror("connect");
        return 3;
    }

    char buf[128];
    Request rq;
    Response rp;
    while(1)
    {
        printf("Please Enter<x,y>:");
        scanf("%d%d",&rq.x,&rq.y);
        fflush(stdout);
        printf("Please Enter op[1(+),2(-),3(*),4(/),5(%)]:");
        scanf("%d",&rq.op);

        write(sock,(void*)&rq,sizeof(rq));
        read(sock,&rp,sizeof(rp));
        printf("status:%d,result:%d\n",rp.status,rp.sum);
    }
    return 0;
}

server.c

#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/wait.h>
#include"proto.h"

typedef struct sockaddr sockaddr;
typedef struct Arg
{
    int fd;
}Arg;

void ProcessRequest(int sock)
{
    while(1)
    {
        Response rp={0,0};
        Request rq;
        read(sock,&rq,sizeof(rq));
        switch(rq.op)
        {
            case 1:
                rp.sum=rq.x+rq.y;
                break;
            case 2:
                rp.sum=rq.x-rq.y;
                break;
            case 3:
                rp.sum=rq.x*rq.y;
                break;
            case 4:
                if(rq.y==0)
                {
                    rp.status=-1;
                    break;
                }
                else
                {
                    rp.sum=rq.x/rq.y;
                    break;
                }
            case 5:
                if(rq.y==0)
                {
                    rp.status=-2;
                    break;
                }
                else
                {
                    rp.sum=rq.x%rq.y;
                    break;
                }
            default:
                rp.status=-3;
                break;
        }
        write(sock,&rp,sizeof(rp));
    }
}
void* service(void* ptr)
{
    Arg* arg=(Arg*)ptr;
    ProcessRequest(arg->fd);
    free(arg);
    return NULL;
}
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("Usage:%s ip port\n",argv[0]);
        exit(1);
    }
    
    //创建套接字
    int listen_sock=socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock<0)
    {
        perror("socket error");
        exit(2);
    }
    printf("Socket:%d\n",listen_sock);
    int opt=1;
    setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    
    //命名套接字
    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(argv[2]));
    local.sin_addr.s_addr=htonl(INADDR_ANY);

    //绑定端口号
    if(bind(listen_sock,(sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind error");
        exit(3);
    }

    //开始监听
    if(listen(listen_sock,5)<0)//这里允许5个客户端连接等待,如果收到更多的请求则忽略
    {
        perror("listen error");
        exit(4);
    }

    //接受请求
    printf("bind and listen success,wait accept...\n");

    for(;;)
    {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        int new_sock=accept(listen_sock,(sockaddr*)&client,&len);
        if(new_sock<0)
        {
            perror("accept error");
        }
        printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
        pthread_t id=0;
        Arg* arg=(Arg*)malloc(sizeof(Arg));
        arg->fd=new_sock;
        pthread_create(&id,NULL,service,&arg);
        pthread_detach(id);
    }
    return 0;
}

无论采用方案一还是方案二,或者其他方案,只要保证一端发送时构造的数据,在另一端能够正确地解析,就可以的,这种约定就是应用层协议。

HTTP协议

实际上,现在已经有大佬们定义了现成的有非常好用的应用层协议供我们直接参考使用,HTTP(超文本传输协议)就是其中之一。

认识URL:平时我们俗称的“网址”就是URL(统一资源定位器),是互联网上用来标识某一处资源的地址。例如http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1#name

1.协议部分:"http:"代表网页使用的是HTTP协议,后面的"//"为分隔符

2.域名部分:该URL的域名部分为"www.aspxfans.com"。一个URL中,也可以使用IP地址作为域名使用

3.端口部分:跟在域名后面的是端口,域名和端口之间使用 ":" 作为分隔符,端口不是一个URL必须的部分,如果省略端口部分,将采用默认端口。

4.虚拟目录部分:从域名后的第一个"/"开始到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是一个URL中必须的部分,本例的虚拟目录为“/news/”

5.文件名部分:从域名后的最后一个“/”开始到“?”为止,是文件部分,如果没有”?“,则是从域名后的最后一个”/“开始到”#“为止,是文件部分。如果没有”?“和”#“,那么从域名后的最后一个”/“开始到结束,都是文件名部分。本例中文件名是"index.asp",文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件。

6.锚部分:从”#“开始到最后,都是锚部分。本例中锚部分是"name"。锚部分也不是一个URL必须的部分

7.参数部分:从”?“开始到”#“为止之间的部分为参数部分,又称搜索部分、查询部分。本例中的参数部分为"boardID=5&ID=24618&page=1"。参数可以允许有多个参数,参数与参数之间用"&"作为分隔符。

urlencode和urldecode

像/?:等字符,已经被URL当作特殊意义理解了。因此这些字符不能随意出项,如果某个参数中需要有这些特殊字符,就必须先对这些字符进行转义。

转义的规则如下:

将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY。例如,当我们在百度上搜索C++,网址栏上http://www.baidu.com/s?wd=c%2B%2B,其中,“+”被转义成“%2B”。

通过urlencode编码后的字符串,可通过urldecode进行解码。urldecode是urlencode的逆过程。

HTTP协议格式

HTTP请求

http请求中的格式:

  • 首行:[方法]+[url]+[版本]
  • Header:请求的属性,冒号分割的键值对,每组属性之间使用\n分隔,遇到空行表示Header部分结束。
  • Body:空行后面的内容都是Body,Body允许位空字符串,如果Body存在,则在Header中会有一个Content-Length属性来标识Body的长度。

HTTP响应

http响应中的格式:

  • 首行:[版本号]+[状态码]+[状态码解释]
  • 冒号分割的键值对,每组属性之间使用\n分隔,遇到空行表示Header部分结束。
  • 空行后面的内容都是Body,Body允许位空字符串,如果Body存在,则在Header中会有一个Content-Length属性来标识Body的长度。r如果服务器返回一个html页面,那么html页面内容就在Body中。

HTTP的方法

  • GET:获取资源。支持HTTP协议版本为1.0、1.1。GET是在url中传参,反映到url中,传的参数一般短小精悍,长度有限制。
  • POST:传输实体主体。支持HTTP协议版本为1.0、1.1。POST是在正文部分中传参,传的参数一般比较私密。表单一般用post更私密。
  • PUT:传输文件。支持HTTP协议版本为1.0、1.1。
  • HEAD:获得报文首部。支持HTTP协议版本为1.0、1.1。
  • DELETE:删除文件。支持HTTP协议版本为1.0、1.1。
  • OPTIONS:询问支持的方法。支持HTTP协议版本为1.1
  • TRACE:追踪路径。支持HTTP协议版本为1.1
  • CONNECT:要求用隧道协议连接代理。支持HTTP协议版本为1.1
  • LINK:建立和资源之间的联系。支持HTTP协议版本为1.0
  • UNLINE:断开连接关系。支持HTTP协议版本为1.0

注:post比get方法更私密,因为参数在正文中,在地址栏中看不到,但post方法不一定更安全。

HTTP有1.0和1.1版本,它们的区别就是1.1默认情况是长连接,而在1.0版本里需要通过头部Connection:Keep-alive来说明连接情况。如果不说明可能就是短连接。这里我介绍一下什么是短连接和长连接。

HTTP连接是在应用层协议,它是建立在传输层协议TCP协议和网络层IP协议上的。IP协议主要解决的是网络路由和寻址的问题,TCP协议主要是解决如何在IP层上可靠的传输数据包,使得发送端的数据都安全可靠的发送到接收端。HTTP的长短连接本质上是TCP的长连接和短连接。我们在用HTTP协议传输之前,需要先让发送端和接收端通过TCP协议连接起来,TCP三次握手成功后,我们就可以在TCP协议的基础上使用HTTP超文本传输协议来传输数据。当数据传输完成之后,就是长短连接做出区分的地方。

长连接比起短连接而言,省去了很多TCP连接创建和断开的时间,减少了资源的浪费。对于哪些服务器频繁请求客户端的操作可以采用长连接。但是这些连接不能一直不关闭,一直建立,如果这个连接存在周期过长,但是又不发送有效的请求,随着客户端连接服务器越来越多,服务器迟早会崩溃,所以我们经常会设置长连接的条件。因此长连接越多越好,长连接确实会省去那些频繁发送请求的场景,但是长连接会用大量的系统资源,对于那些可能会有大量用户访问的网站常会采用短连接。

HTTP的状态码

  类别 原因
1XX Information(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器处理请求出错

常见的状态码如:

200(OK),404(Not Found):服务器无法找到请求的页面或资源。403(Forbidden),301(Redirect):永久重定向。搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的网址。302(Redirect):临时重定向。搜索引擎会抓取新的内容而保留旧的地址,因为服务器返回302,所以搜索引擎认为新的网址是暂时的。500(Internal Server Error):服务器内部错误,不能完成客户的请求。501(Not Implemented):尚未实施,或请求格式错误。

HTTP常见Header

  • Content-Type: 数据类型(text/html等);
  • Content-Length: Body的长度 ;
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • referer: 当前页面是从哪个页面跳转过来的;
  • location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能; 

Cookie是在本地存储的文件,存放在客户端,Cookie记录了用户的账号和密码等个人信息,从而方便下次访问。具体方法就是:当客户使用浏览器访问一个支持Cookie的网站的时候,客户会提供包括用户名在内的个人信息并且提交至服务器上;接着,服务器再向客户端回传相应的超文本的同时也会发回这些个人信息,这些信息并不放在HTTP响应体(Response Body)中,而是存放在HTTP响应头(Response Header);当客户端浏览器接受到来自服务器的响应后,浏览器会将这些信息存放到一个统一的位置。自此,客户端再向服务器发送请求时,都会把相应的Cookie再次发回至服务器。

但这种方法不安全,个人信息很容易被窃取。Session是一个对Cookie的解决方案,把Session放在服务器端更安全一些,因为服务器有专门的人员进行维护,出现问题的情况比较少。

因此http协议是无状态的协议,为了支持客户端与服务器之间的交互,我们就需要用Cookie和Session来解决。

http的核心工作就是把客户请求告诉服务器,服务器将相关资源的页面展示给客户。

发布了119 篇原创文章 · 获赞 17 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/tangya3158613488/article/details/95939622