TCP/IP协议簇之应用层
Posted WoLannnnn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP协议簇之应用层相关的知识,希望对你有一定的参考价值。
文章目录
应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.
再谈 “协议”
协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串”(字符串描述不准确) 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
通过序列化与反序列化实现,我们发送数据至网络要进行序列化,将“结构化的数据”合为一个整体,网络发送数据给另一端接收时,要进行反序列化,将整体数据又转化为“结构化数据”。进行序列化的工具有json,xml
网络版计算器
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案一:
- 客户端发送一个形如"1+1"的字符串;
- 这个字符串中有两个操作数, 都是整形;
- 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
- 数字和运算符之间没有空格;
- …
约定方案二:
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
- 这个过程叫做 “序列化” 和 “反序列化”
protocol.hpp
// protocol.hpp 定义通信的结构体
#pragma once
typedef struct request
int x;
int y;
char op;//操作符
request_t;
typedef struct response
//code为0:运算正常
//code为1:/的除数为0
//code为2:%的除数为0
//code为3:操作符不合法
int code;//退出码
int result;//结果
response_t;
// client.hpp
#pragma once
#include<iostream>
#include<string>
#include<arpa/inet.h>
#include <netinet/in.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/wait.h>
#include"protocol.hpp"
class client
private:
std::string _ip;//服务器ip
int _port;//服务器端口号
int _sock;
public:
//构造函数
client(std::string ip = "127.0.0.1", int port = 8080)
:_ip(ip)
,_port(port)
//初始化
void init_client()
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
std::cerr << "socket error !" << std::endl;
exit(1);
//connect
//服务器信息
sockaddr_in svr;
svr.sin_family = AF_INET;
svr.sin_port = htons(_port);
svr.sin_addr.s_addr = inet_addr(_ip.c_str());
if (connect(_sock, (sockaddr*)&svr, sizeof(svr)) < 0)
std::cerr << "connect error !" << std::endl;
exit(2);
void start()
request_t rq;
response_t rsp;
std::cout << "请输入第一个操作数: ";
std::cin >> rq.x;
std::cout << "请输入操作符: ";
std::cin >> rq.op;
std::cout << "请输入第二个操作数: ";
std::cin >> rq.y;
//发送给服务器计算
send(_sock, &rq, sizeof(rq), 0);
//接收结果
ssize_t s = recv(_sock, &rsp, sizeof(rsp), 0);
if (s > 0)
if (rsp.code != 0)
std::cout << "输入错误 !" << std::endl;
std::cout << "code: " << rsp.code << std::endl;
else
std::cout << rq.x << " " << rq.op << " " << rq.y << " = " << rsp.result << std::endl;
//析构
~client()
close(_sock);
;
server.hpp
#pragma once
#include<iostream>
#include<arpa/inet.h>
#include <netinet/in.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/wait.h>
#include"protocol.hpp"
class server
private:
int _port;//服务器端口号
int _lsock;//监听套接字
public:
//构造函数
server(int port = 8080)
:_port(port)
//初始化
void init_server()
//创建套接字
_lsock = socket(AF_INET, SOCK_STREAM, 0);
if (_lsock < 0)
std::cerr << "socket error !" << std::endl;
exit(1);
//绑定套接字
sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_lsock, (struct sockaddr*)&local, sizeof(local)) < 0)
std::cerr << "bind error !" << std::endl;
exit(2);
//监听
if (listen(_lsock, 5) != 0)
std::cerr << "listen error !" << std::endl;
exit(3);
void cal(int sock)
//短链接完成服务
request_t rq;
response_t rsp = 0, 0;
//接收计算信息
ssize_t s = recv(sock, &rq, sizeof(rq), 0);
if (s > 0)
switch(rq.op)
case '+':
rsp.result = rq.x + rq.y;
break;
case '-':
rsp.result = rq.x - rq.y;
break;
case '*':
rsp.result = rq.x * rq.y;
break;
case '/':
if (rq.y == 0)
rsp.code = 1;
break;
rsp.result = rq.x / rq.y;
break;
case '%':
if (rq.y == 0)
rsp.code = 2;
break;
rsp.result = rq.x % rq.y;
break;
default:
rsp.code = 3;
//返回结果
send(sock, &rsp, sizeof(rsp), 0);
//短链接处理,所以这里就closesock
close(sock);
void start()
//连接客户端
struct sockaddr_in end_point;
socklen_t len = sizeof(&end_point);
while (1)
int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);
if (sock < 0)
std::cerr << "accept error !" << std::endl;
continue;
//创建子进程进行计算
if (fork() == 0)
//子进程
close(_lsock);
if (fork() > 0)
//还是子进程
exit(0);//退出,防止阻塞子进程
else//孙子进程
cal(sock);
exit(0);
close(sock);
waitpid(-1, nullptr, 0);
//析构
~server()
close(_lsock);
;
无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是ok的,比如输入顺序。这种约定, 就是 应用层协议
我们对上述计算进行抓包:sudo tcpdump -i any -nn tcp port 8080
三次握手,四次挥手
HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的.
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一.
认识URL
平时我们俗称的 “网址” 其实就是说的 URL
互联网行为
- 把服务器的数据拿下来
- 把自己的数据传上服务器
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
例如:
“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程
HTTP三大特点
-
无连接。前面我们知道,TCP是要连接的,但TCP建立连接和http无关,http直接向服务器发送http request即可
-
无状态。我们用一个例子来理解无状态:当我们访问一个网站时,需要登陆,如果我们此次登陆了,下一次再访问时,由于http的无状态特性,我们需要再次输入账号密码才能登陆。这就是http的无状态。而我们实际登陆时,其实是登陆一次后之后再访问就不用输入账号密码了,这是由cookie和session实现的
-
简单快速。短链接进行文本(html、img、css、js…)传输,这是早期的http/1.0的传输方式
http/1.0支持长连接
HTTP协议格式
HTTP构成:
可以通过Fiddler进行抓包。
原理:正常上网时,我们直接通过网络发送请求给服务器即可。而Fiddler抓包是我们先给Filddler进行代理,Fiddler再通过网路发送请求给服务器,并接收响应
HTTP请求
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
HTTP响应
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中
HTTP的方法
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINE | 断开连接关系 | 1.0 |
其中最常用的就是GET方法和POST方法.
Head不获取正文信息,只获取前三个信息(请求/响应行、报头、空行)
HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
Cookie的介绍:之前我们了解了http的无状态特性,这样会给用户带来很差的体验,而cookie就是来解决这个问题的。
原理:同样以网站的登录为例来理解,我们在网站上第一次登录时,服务器端就保存了用户名和密码的信息,服务器进行响应时,会在response中包含set-cookie:用户名、密码…的信息,客户端收到后,浏览器就在cookie这个文件中保存了这个信息。下一次进入这个网站,浏览器对网站发送请求时,就会携带这个cookie文件与request一起发送给服务器,服务器进行核对,核对正确后,就不再需要我们手动输入密码了。
cookie的本质是浏览器的一个文件,它分为内存级的和磁盘级的。内存级的cookie只在当前浏览器打开时间内有效,关闭浏览器下次再打开时,我们再次进入网站,还是需要输入账号密码进行登录。磁盘级的cookie则是保存在本地的,这样就不受浏览器打开这一条件限制,可以保存较长的时间。
但实际上,这样也是有风险的,万一我们被黑客入侵,获取到了本地的cookie,他就可以通过这个cookie访问我们同样访问过的信息。
Session就是与Cookie配套使用的。我们第一次登录后,服务器不再将用户名和密码response回来,而是在服务器上用一个session文件保存我们的私密信息,然后生成一个唯一的sid来标识它,之后同样在response里,由set-cookie:sid,此时是将sid返回,本地的cookie中保存sid。下一次访问网站时,浏览器携带cookie发送请求,服务器通过sid找到客户的信息,然后允许客户访问。
cookie和session是相对安全的,但还是有可能会被盗取。此时盗取的只是sid,而不是我们的用户名和密码了。而且他也不知道sid对应的是哪个网站。而要改密码是要原密码的,除非我们密码太简单被别人识破,否则他就不能修改我们的密码然后让我们永远地失去这个账号,除非把后台服务器攻破,拿到密码。
最简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 “hello world”; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到;
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<signal.h>
#include<string>
#define BAGLOG 5
using namespace std;
class httpserver
private:
int _port;
int _lsock;
public:
//构造
httpserver(int port)
:_port(port)
,_lsock(-1)
//析构
~httpserver()
if (_lsock != -1)
close(_lsock);
void initServer()
signal(SIGCHLD, SIG_IGN);
//创建套接字
_lsock = socket(AF_INET, SOCK_STREAM, 0);
if (_lsock < 0)
cerr << "socket error !" << endl;
exit(1);
//绑定
struct sockaddr_in local;
//将local的内容清零
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_lsock, (struct sockaddr*)&local, sizeof(local)) < 0)
cerr << "bind error !" << endl;
exit(2);
//监听
if (listen(_lsock, BAGLOG) != 0)
cerr << "listen error !" << endl;
exit(3);
void EchoHttp(int sock)
//短链接执行
//接收数据
char request[1024];
ssize_t s = recv(sock, request, sizeof(request) - 1, 0);
if ( s > 0 )
request[s] = 0;
//打印请求
cout << request << endl;
//响应
//响应行
string response = "HTTP/1.0 200 OK\\r\\n";
//响应报头
response += "Content-type: text/html\\r\\n";//发送的类型是html,网页类型
//响应空行
response += "\\r\\n";
//响应正文
response += "\\
<html>\\
<head>\\
<title>ysj</title>\\
</head>\\
<body>\\
<h1>Welcome</h1>\\
<p>Hello World !</p>\\
</body>\\
</html>\\r\\n";
send(sock, response.c_str(), response.size(), 0);
close(sock);
void start()
while (1)
//建立连接
sockaddr_in peer;
socklen_t len = sizeof(peer);
bzero(&peer, len);
int sock = accept(_lsock, (struct sockaddr*)&peer, &len);
if (sock < 0)
cerr << "accept error !" << endl;
continue;
cout << "get a link..." << endl;
//创建子进程处理任务
pid_t id = fork();
if (id == 0)//子进程
//父进程忽略子进程发送的SIGCHLD信号
close(_lsock);
EchoHttp(sock);
exit(0);
close(sock);
;
编译, 启动服务. 在浏览器中输入 http://[ip]:[port], 就能看到显示的结果 “Hello World”
备注:
此处我们使用 8080 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口,但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号.
使用chrome测试我们的服务器时, 可以看到服务器打出的请求中还有一个 GET /favicon.ico HTTP/1.1 这favicon.ico_百度百科 (baidu.com)
如果我们把状态码设置成404会不会显示经典的Not Found页面?
答案是:不会的,页面的显示是要靠html来设置的,不是由状态码决定的
临时重定向和永久重定向
配合状态码3xx使用。浏览器在接收到重定向响应的时候,会采用该响应提供的新的 URL ,并立即进行加载;大多数情况下,除了会有一小部分性能损失之外,重定向操作对于用户来说是不可见的。
将上面的服务器改造,设置成重定向:
响应报头中设置:location: http://baidu.com\\r\\n
不需要正文。
//响应行
string response = "HTTP/1.0 302 Found\\r\\n";
//响应报头
response += "Content-type: text/html\\r\\n";以上是关于TCP/IP协议簇之应用层的主要内容,如果未能解决你的问题,请参考以下文章