HTTP协议及TCP通讯流程
1. TCP协议通讯流程
服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
1.1 简单初识三次握手和四次挥手
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为三次握手;
举个例子来形象的阐述三次握手的概念:
男:我希望你做我女朋友
女:什么时候
男:就是现在
数据传输的过程
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
TCP 和 UDP 对比
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流(类比于自来水,想要多少就要多少) vs 数据报(类比于取快递,要么取一个,不可能取半个快递的概念)
2. 网络版计算器
我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案一:
客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
约定方案二:
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;这个过程叫做 "序列化" 和 "反序列化"
Protocol.hpp 定义通信的结构体
#ifndef __PROTOCOL_HPP__
#define __PROTOCOL_HPP__
#include<iostream>
typedef struct Request{
int x;//左操作数
int y;//右操作数
char op;//操作符
}Request;
typedef struct Response{
int code;//表示结果值是否可被信任,为0的时候,说明结果可以信任
int result;//结果
}Response;
#endif
无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是ok的. 这种约定, 就是应用层协议
Server.hpp
#ifndef __SERVER_HPP__
#define __SERVER_HPP__
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "Protocol.hpp"
using namespace std;
class server{
private:
int port;
int lsock;
public:
server(int _p):port(_p),lsock(-1)
{}
void initServer()
{
lsock = socket(AF_INET, SOCK_STREAM, 0);
if( lsock < 0 )
{
cerr << "socket error " << endl;
exit(2);
}
struct 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){
cerr << "bind error!" << endl;
exit(3);
}
if(listen(lsock, 5) < 0){
cerr << "listen error!\n" << endl;
}
}
void cal(int sock)
{
//短连接来完成对应的计算!
Request rq;
Response rsp={4, 0};
ssize_t s = recv(sock, &rq, sizeof(rq), 0); //BUG
if(s > 0){
rsp.code = 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.result = rq.x / rq.y;
}
else{
rsp.code = 1;
}
break;
case '%':
if(rq.y != 0){
rsp.result = rq.x + rq.y;
}
else{
rsp.code = 2;
}
break;
default:
rsp.code = 3;
break;
}
}
send(sock, &rsp, sizeof(rsp), 0);
close(sock);
}
void start()
{
struct sockaddr_in peer;
for(;;){
socklen_t len = sizeof(peer);
int sock = accept(lsock, (struct sockaddr*)&peer,&len);
if( sock < 0 ){
cerr << "accept error!" << endl;
continue;
}
if(fork() == 0){
if(fork() > 0){
exit(0);
}
//孙子
close(lsock);
//todo
cal(sock);
exit(0);
}
close(sock);
waitpid(-1, nullptr, 0);
}
}
~server()
{
close(lsock);
}
};
#endif
Client.hpp
#ifndef __CLIENT_HPP__
#define __CLIENT_HPP__
#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/wait.h>
#include"Protocol.hpp"
class client{
private:
std::string ip;
int port;
int sock;
public:
client(std::string _ip,int _port)
:ip(_ip),port(_port),sock(-1)
{}
void initclient()
{
sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
std::cerr<<"socket error!"<<std::endl;
exit(2);
}
}
void start()
{
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){
std::cerr << "connect error"<<std::endl;
exit(3);
}
//建立的是短链接,所以只需要C发过去,S返回回来,那么一次通信就算结束了
Request rq;
Response rsp;
std::cout << "date1# ";
std::cin >> rq.x;
std::cout << "date2# ";
std::cin >> rq.y;
std::cout << "op# ";
std::cin >> rq.op;
send(sock,&rq,sizeof(rq),0);
ssize_t s = recv(sock,&rsp,sizeof(rsp),0);
if(s > 0){
std:: cout <<"code : "<< rsp.code <<std::endl;
std::cout <<"result : "<<rsp.result << std::endl;
}
}
~client()
{
close(sock);
}
};
#endif
2.1 tcpdump
这是一个抓包命令tcpdump -i any -nn tcp port 8080
参数解释:
-i:表示想要抓那个网络
-nn:第一个n表示能把主机名用数字表示的都用数字表示,第二个n表示把更多的信息能用数字表示的用数字表示。
tcp:表示想要抓的协议
三次挥手
四次挥手
3. 初识HTTP协议
HTTP(超文本传输协议),是一个应用层的协议。
基本特征:
- 无连接:TCP建立连接和http无关,http直接向对方发送http request即可
- 无状态:http本身是无状态的,并不会记录任何用户信息,但是我们有的时候会发现,我刚才登录过的网站,第二次不再需要输入账号+密码就可以直接的进行访问,其实真正记录你的基本信息的技术是cookie+session
- 简单快速
3.1 HTTP协议格式
使用Fiddler工具进行网络抓包
Request
- 首行(请求行): [请求方法] + [url] + [版本]
- Header(请求报头): 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body(请求正文): 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
Response
- 首行(响应行): [版本号] + [状态码] + [状态码解释]
- Header(响应报头): 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body(响应正文): 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.
3.2 HTTP的请求方法
HTTP/1.1协议中共定义了八种方法(有时也叫“动作”),来表明Request-URL指定的资源不同的操作方式
- HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
- HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法
最常使用的是GET和POST请求方法。
- GET:url传参(参数的长度是有限的),你的账号和密码可能会在url中直接的携带,相当不安全。
- POST:正文传参(参数可以是无限长的),除非对正文进行加密,不然这种方式也是不安全的,但相较于GET方法又好一些。
3.3 HTTP的状态码
HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型:
下面是常见的HTTP状态码:
- 200 - 请求成功
- 301 - 资源(网页等)被永久转移到其它URL
- 404 - 请求的资源(网页等)不存在
- 500 - 内部服务器错误
3.4 HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
3.5 cookie和session
http是无状态的(无法保存记录),但是http是给用户使用的,用户的体验感会非常差,为了解决使用了cookie技术。如下图所示:如果没有cookie技术,那么在访问完资源一以后,在访问资源二的时候就又需要输入name和passwd。
但是我们的电脑或者手机中了病毒,那么黑客也是可以拿到我们的cookie文件(在这个文件中保存着name和passwd),黑客是不需要知道cookie里面是具体的内容,他只需要拿着这个cookie文件和我访问同样的资源,比如说:QQ,那么他就可以进行一系列的操作,就如同我在使用一样。非常的不安全。所以为了能够相对安全一些引入了session技术
对比cookie和session:
- cookie敏感(name && passwd)信息在本地
- cookie + session敏感信息在server端,但也不是绝对安全,但是比cookie要好很多,对于本地机来说,你的防火墙只能是360或者XX,但是对于session来说,是一大堆的集群以及专业的维护团队。
cookie 和session 的区别
安全性:
-
cookie数据存放在客户的浏览器上,session数据放在服务器上。
-
cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,所以session相对安全些。
占用空间:
-
session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能。考虑到减轻服务器性能方面,应当使用COOKIE。
-
单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
数据类型
- cookie只能存储 int float string。Session还可以存储 数组、对象
所以个人建议:
- 将登陆信息等重要信息存放为SESSION
- 其他信息如果需要保留,可以放在COOKIE中
参考:
cookie和session详解链接: link.