1 协议
我们都知道,协议是一种约定,我们规定好一种信息的格式,如果发送方按照这种请求格式发送信息,那么接收端就要按照这样的格式解析数据。这就是协议。
应用层协议,一方面包含客户端和服务器端需要进行交互的信息,一方面包含如何组织(序列化)以及如何解析信息(反序列化)。
2 自定制协议
我们可以通过一个简单的网络计算器的例子来自定制一个协议,体会其中的含义。
自定制协议一:
首先,客户端规定发送的请求是一串字符串,形如“1+1”;
其次,这串字符串由两个数字,中间一个加号构成,并且他们之间没有空格;
最后,服务器在解析这串字符时,就可以按照它规定的方式解析解析出两个加数。这样,我们规定的字符串的格式就是一种协议。
自定制协议二:
除了上面定义一个字符固定格式的字符串的方法,我们还可以选择定义一个结构体,这个结构提包含我们要传递的信息,比如定义一个结构体,保存着两个加数,在定义一个结构体,保证着两个加数相加之后的结果;
在客户端将该结构体转化为字符串,再将该字符串(序列化)发送给服务器,服务器将该字符串再转化为结构体(反序列化),通过计算,将结果写入另一个结构体中,转化为字符串,在发送回客户端。
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include "proto.h"
/*
* 这是一个服务器实现了简单的计算功能(加法)
* 这里使用一个结构体来保存数据,解析数据时就按照结构体的定义来解析数据
*/
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: ./server [IP] [port]\n");
return 0;
}
//创建socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
perror("socket");
return 1;
}
//绑定IP和port
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
int ret = bind(sock, (struct sockaddr*)&server, sizeof(server));
if(ret < 0)
{
perror("bind");
return 2;
}
//进入循环,处理数据
while(1)
{
Request req;
//将数据读到req中(req是一个结构体,不需要进行强转)
struct sockaddr_in client;
socklen_t len;
recvfrom(sock, &req, sizeof(req), 0, (struct sockaddr*)&client, &len);
//进行计算
ntohl(req.a);
ntohl(req.b);
Response res;
res.sum = req.a + req.b;
htonl(res.sum);
//将结果写回到客户端
sendto(sock, &res, sizeof(res), 0, (struct sockaddr*)&client, len);
}
}
#pragma once
typedef struct Request
{
int a;
int b;
}Request;
typedef struct Response
{
int sum;
}Response;
上面是采用结构体实现的应用层协议,无论是方案一实现的协议,还是方案二实现的,只要能够正确的按照协议解析和传递数据,就是应用层协议。
3 HTTP协议
HTTP协议是大佬定制的非常好用的应用层协议,我们可以直接使用该协议,而不是自己花时间精力定制一个不好用的协议,省去了许多事情。下面我们就来认识以下HTTP协议:
3.1 认识URL
URL(统一资源定位符):平时我们说的网址,就是URL
URL的格式:
-
协议方案名:使用http:或https:等协议方案名获取访问资源时要指定协议类型
-
登录信息:用户名和密码作为从服务器端获取的必要登录信息,是可选项
-
服务器地址:带访问服务器的地址(可以是方面的带解析的地址,也可以是IPv4地址)
-
服务器端口号:指定服务器连接的网络端口号,同样是可选项(用户省略,则使用默认端口号)
-
带层次的文件路径:指定服务器上的文件路径来定位特指的资源
-
查询字符串:针对已指定文件路径内的资源,可以使用查询字符串传入任意参数
-
片段标识符:通常可标记出以获取资源中的子资源
urlencode和urldecode:
在url中,像/,?等都不能随意出现,因为它们都已经具有了特殊的含义。于是当我们需要这些字符可以原模原样的出现时,就需要进行转义,比如说C++中的‘+’:
urldecode就是urlencode的逆过程。
3.2 HTTP协议格式
3.2.1 HTTP请求
- 首行:由方法+url+http版本构成
- header:请求的属性,由冒号分隔的键值对构成
- 空行:分隔了header和body部分
- body:空行后面的内容都是body,允许body为空;当body存在时,header中要有一个Content-Length属性来标识body长度
3.2.2 HTTP响应
- 首行:由版本号+状态码+状态码解释构成
- header:响应的属性,有一个个以冒号为分隔的键值对组成
- 空行:分隔header和body
- body:body允许为空;当body存在,header中要有一个Content-Length属性来标识body长度
3.2.3 方法
以上是HTTP协议常见的方法。
3.2.4 状态码
3.2.5 header
HTTP协议常见header:
- Content-Type:body部分的数据类型
- Content-Length:body部分的长度
- Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上‘
- User-Agent:声明用户的操作系统和浏览器的版本信息
- referer:当前页面是从哪个页面跳转过来的
- location:搭配3xx状态码使用,告诉客户端接下来要去哪里访问
- Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能
3.3 实现一个简单的HTTP服务器
#include <stdio.h>
#include <string.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <unistd.h>
void* Entry(void* arg)
{
int new_sock = (int)arg;
char buf[1024*10] = {0};
//读取请求并响应(响应忽略)
ssize_t s = read(new_sock, buf, sizeof(buf) - 1);
if(s > 0)
{
buf[s] = '\0';
printf("response:%s\n", buf);
}
const char* first_line = "HTTP/1.1 200 OK\n";
const char* blank_line = "\n";
const char* body = "<html>hello world!</html>";
const char header[1024] = {0};
//格式化构造字符串
sprintf(header, "Content_Type: text/html;\nContent_Length: %lu\n", strlen(body));
//注意写的顺序
write(new_sock, first_line, strlen(first_line));
write(new_sock, header, strlen(header));
write(new_sock, blank_line, strlen(blank_line));
write(new_sock, body, strlen(body));
return NULL;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: ./server [ip] [port]\n");
return 1;
}
//创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
return 2;
}
//进行绑定
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
int ret = bind(sock, (struct sockaddr*)&server, sizeof(server));
if(ret < 0)
{
perror("bind");
return 2;
}
//listen
if(listen(sock, 128) < 0)
{
perror("listen");
return 3;
}
//进入循环
while(1)
{
struct sockaddr_in client;
socklen_t len;
int new_sock = accept(sock, (struct sockaddr*)&client, &len);
if(new_sock < 0)
{
close(sock);
continue;
}
pthread_t td;
pthread_create(&td, NULL, Entry, (void*)new_sock);
pthread_detach(td);
}
}
我们使用我们的代码时,还可能出现GET /favicon.ico HTTP/1.1这样的请求,那么这是什么意思呢?
上图中用蓝色圈出来的小图标就是通过这个请求获得的。
另外,不同的状态码的含义不同,200代表成功发送响应,404代表找不到页面。在上面的代码中,如果我们将200改成404,那么页面就加载不出来了..