当前位置: 首页 > news >正文

圣玺企业网站建设扶贫工作网站建设方案

圣玺企业网站建设,扶贫工作网站建设方案,公司排名100强,wordpress搭建小程序商城目录 应用层 再谈 协议 协议是一种 约定. socket api的接口, 在读写数据时, 都是按 字符串 的方式来发送接收的. 如果我们要传输一些结构化的数据 怎么办呢? 为什么要转换呢#xff1f; 如果我们将struct message里面…目录 应用层 再谈 协议 协议是一种 约定. socket api的接口, 在读写数据时, 都是按 字符串 的方式来发送接收的. 如果我们要传输一些结构化的数据 怎么办呢?  为什么要转换呢 如果我们将struct message里面的信息分别作为独立的内容将这个4个信息分别传过去可以吗 什么是序列化和反序列化 为什么需要进行序列化和反序列化 我们之前的tcpudp通信有没有进行任何的序列化和反序列化呢 如何进行序列化和反序列化呢 网络版计算器 Sock.hpp CalServer.cc CalClient.cc 测试结果 存在的问题  json 如何进行序列化和反序列化 json的使用 序列化 反序列化 我们对计算器进行优化内置序列化和反序列化 Protocol.hpp Makefile CalServer.cc CalClient.cc 测试 对网络计算器的总结 HTTP协议 认识URL url字段解析 urlencode和urldecode 转义的规则如下 HTTP协议格式 HTTP请求 请求行  请求报头  空行  请求正文  如何理解普通用户的上网行为 http的响应 状态行 响应报头 空行 响应正文  请求与响应的整体格式 http request和http response 被如何看待 如何解包和封包呢 如何分用 HTTP操作 http中面向字节流读的函数recv 最简单的HTTP服务器 测试 为什么客户端发了多条请求呢 HTTP请求  这个http的请求和响应为什么都带了版本呢 对于HTTP请求的报头字段的解释  服务器构建响应 send  Sock.hpp HTTP.cc 测试 HTTP的请求与响应总结 观察百度的网页 再谈http请求的细节 HTTP请求 理解Content-Length 读取要求 你怎么保证你每次读到的是一个完整的http呢 假设有正文的话你如何知道空行之后有多少个字符呢 如何读到一个完整的http协议 HTTP响应 HTTP的请求方法 短链接  早期的http1.0为什么用短链接呢 HTTP的方法 / (根目录) 实验 响应报头部分  响应正文  完整代码 测试 验证GET和POST方法 GET方法 html的表单  制作表单 POST方法  GET和POST的区别 通过抓包观察GET与POST 抓包的原理 fiddler查看POST 方法 fiddler查看GET GET和PSOT 概念问题 区别 如何选择 所谓的文本分析 HTTP常见的状态码 具有指导意义的状态码  404的错误属于客户端问题还是服务器问题 服务器的问题有哪些呢 3开头的状态码 重定向是什么 什么叫做永久重定向什么叫做临时重定向 模拟重定向 测试302,临时重定向 永久重定向遗留的问题 再谈HTTP常见Header 长连接与短连接 短连接 长连接  Connection cookie 与 session 背景引入 问题引入 Cookie  对我们来讲http是不记录上下文的是无状态的那网站是如何认识我的呢 会话与会话管理  cookie 验证cookie 添加cookie cookie文件的存在形式 cookie的安全问题  如果别人盗取我的cookie文件有两个安全问题 session 为什么私密信息会被盗取呢 session处理策略  cookie安全的第一个问题 cookie安全的相关策略 再谈http无状态 为什么网站需要认证用户也就是为什么登录这个网站的时候它需要永久的认识用户呢 HTTPS 背景认识一 背景认识二(数据的加密方式) 1.对称加密 2.非对称加密 背景认识三 假设现在有一篇论文那么如何防止文本中的内容被篡改以及识别到是否被篡改 我是一个通信端我现在要发送这段文本我怎么保证这段文本没有被篡改 校验 https是如何通信的呢 如何选择加密算法 方案一 方案二 对称加密  非对称加密  两对非对称秘钥的问题  实际中的加密方法 什么叫做安全 中间人  那么在服务端把公钥给客户端的时候可不可能出现问题呢 证书 证书是什么 CA机构 创建证书 有了证书后请求该怎么做呢 ​编辑数字签名中间人改不了吗 如果中间人也是一个合法的服务方呢 客户端是如何知道CA机构的公钥信息呢 传输层 再谈端口号 端口号范围划分  认识知名端口号(Well-Know Port Number) 两个问题 pidof netstat UDP协议 UDP协议端格式 对于16位UDP长度的理解 UDP如何做到封装和解包的 UDP如何做到向上交付(分用问题) 我们写代码的时候为什么需要绑定端口号 端口号为什么是16位 Linux内核是C语言写的请问如何看待udp报头 UDP的特点 面向数据报 UDP的缓冲区 UDP全双工 什么叫做一个协议通信时全双工呢 UDP使用注意事项 基于UDP的应用层协议 TCP协议  TCP协议段格式  TCP如何做到封装和解包的 TCP如何做到向上交付的(分用问题) 确认应答(ACK)机制 TCP常规可靠性-确认应答的工作方式 确认应答 如何保证按序到达呢 如何确认信息和发送信息的对应关系呢 为什么一个报文里面既有序号又有确认序号 16位窗口大小 TCP为什么要弄两个缓冲区 16位窗口大小 server有接受缓冲区客户端给server发消息server是会进行应答的其中server如何让客户端慢一点呢 我如何把我自己的接受缓冲区中剩余空间的大小通告给你呢 如何通过端口号找到目标进程 系统中存在很多文件为什么你读取文件的时候这个文件读取到系统之后是读给你的 6个标记位 为什么要建立连接呢 如何建立连接呢 作为一个server在任何时刻可能有成百上千个client都向server发消息。server首先面临的是面对大量的TCP报文如何区分各个报文的类别 ACK标记 SYN标记 server端可能会收到一个连接建立的请求请求虽然叫请求但是它也是数据所以也要进行交换server端如何区分发来的报文是请求呢 建立连接三次握手的过程 3次握手的目的就是建立连接我们理解下什么叫是连接 为什么是3次握手呢 RST标记位  PSH标记位 如何理解这个让上层尽快将数据取走是怎么个取法 URG 16位紧急指针是什么呢 这个带外数据有什么用呢  FIN标记位 四次挥手 如何理解序号 超时重传机制  超时时间间隔应该是多长 当你把报文发出去了发送方没有收到确认ACK接收方是一定没有收到对应的报文数据吗 我们怎么保证对方收到的数据不是重复的呢 那么, 如果超时的时间如何确定? 连接管理机制 为什么是三次握手  为什么4,5,6次握手不行呢 为什么1,2次握手不行呢 3次握手就可以预防了洪水问题了吗 半连接  为什么是四次挥手 四次挥手的状态变化 理解TIME_WAIT状态 验证主动断开连接的一方要进入TIME_WAIT 为什么会要有TIME_WAITTIME_WAIT通常是多长 为什是TIME_WAIT时间一般等于2倍MSL呢 为什么会断开服务器后立即重启会bind error  如何解决bind error 服务器无法立即重启会有什么危害 CLOSE_WAIT状态 验证CLOSE_WAIT状态 CLOSE_WAIT给我们带来的启示 滑动窗口 如果我们运行一个主机向另一个主机发送大量数据时那么一次给对方多少呢 滑动窗口在哪里是什么 滑动窗口有没有可能缩小呢 滑动窗口有没有可能扩大呢(向右移动) 滑动窗口可能向左滑动吗 如果出现了丢包, 如何进行重传?  滑动窗口发送1001~5001这么多报文最后ACK确认先确认的是1001~2001后面的2001~3001也会陆陆续续确认但是如果中间的ACK丢了呢 如果此时我给对方发消息还是1001~50011001~2001数据对方收到了但是2001~3001数据丢了因为对方收到了5001可是2001~3001的数据没了此时对方给我的ACK是什么呢 再次理解滑动缓冲区 再次总结下丢包的两种情况 超时重传 vs 快重传 实际上TCP里这两种重传机制都是存在的为什么超时重传还存在呢 流量控制 什么时候发送方就知道了接收方的接收能力 如果我的接收缓冲区的窗口大小为0怎么办 此时发送方就不发数据了就停下来了因为我们有流量控制。那么发送方什么时候再向接收方发消息呢 一旦发送方给接收方发了消息接收方要不要应答呢 总结  拥塞控制 背景引入 拥塞窗口 之前不是说滑动窗口是我向网络里塞数据一次可以塞多少数据而暂时可以不用应答的这样的一个范围吗滑动窗口不是说好的是由对方的窗口大小也就是接收能力决定的吗 网络拥塞了TCP该怎么办 为什么慢启动前期使用指数增长呢 什么叫慢启动呢 那网络的拥塞窗口变来变去的是不是会造成网络的数据量一会升一会降 延迟应答 那么所有的包都可以延迟应答么? 假设今天适用于延迟应答延迟应答有哪些策略呢 捎带应答  重新认识3次握手 面向字节流 什么叫做字节流 什么叫做流呢  回忆http 为什么打开文件叫做文件流 粘包问题 TCP异常情况  比如进程终止了曾经建立好的连接会怎么样呢 如果我在电脑上建立好大量的连接突然我的机器直接重启了会怎么样呢 如果机器掉电或者网络断开了会怎么样呢 TCP小结 基于TCP应用层协议 TCP/UDP对比 看直播延迟是怎么做到的呢 为什么要这么干呢 网络通信都不允许丢包吗 用UDP实现可靠传输 TCP 相关实验 理解 listen 的第二个参数 Sock.hpp Http.cc 为什么要进行1呢 半连接队列是如何移到全连接队列的 那我作为一个服务器有人不攻击我的全连接队列而是攻击我的半连接队列怎么办 为什么要维护队列为什么这个队列不能太长为什么这个队列不能没有 为什么要维护门口的桌椅板凳让客人可以等待 应用层 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层. 再谈 协议 协议是一种 约定. socket api的接口, 在读写数据时, 都是按 字符串 的方式来发送接收的. 如果我们要传输一些结构化的数据 怎么办呢?  这就是曾经我们在写C/C定义出来的对象我们要把它发出。我们要把数据发送到网络当中肯定不是直接就把这样的结构化数据放到网络里传过去让对方收到。因为这些消息里大家的大小长度都是变长的都是不一样的所以我们在实际发给对方的时候是不方便统一发送的那我们实际上就需要把这里的各种结构化信息我们要把它转化成一个长的“字符串”实际上应该转化成一个长的字节流或者数据包发出去 为什么要转换呢 我们的message是一个结构体之前我们存在一个概念是结构体的内存对齐那么如果我是一个message那么将来你接收的时候也一定是struct message。但是发送方和接收方的message的大小不一定一样其中双方里面字段开辟的长度也都不一定一样。所以直接把信息传过去这种做法是不正确的。我们需要将其转化成一个长字符串。 如果我们将struct message里面的信息分别作为独立的内容将这个4个信息分别传过去可以吗 从技术实现上如果你想分别传过去这个时候每一个信息就是一个独立的信息整体就不是结构化数据了因为这样成本太高了假如有成百上千的人给我们这样单独的一条一条发信息那么我们最后还得组合区分哪几条是组合在一起的。所以我们一般不会这么做我们需要将它转换成对应的长字符串也就是把它打包。 什么是序列化和反序列化 所以我们要把这种结构化的数据转化成某种长字符串的信息传递给对方对方在根据这里的长字符串以此定义一个message对象然后将数据由一个字符串转化成一个结构化的数据。其中我们把从结构化数据转换成长字符串的过程我们就叫做序列化的过程。当我们把长字符串转化上来的时候 新的结构体里面就会有各种信息这些信息我们再由我们的分析算法把字符串里面的内容在一个个的分析出来然后填入到结构体当中在形成一个新的结构化的数据这个过程我们称之为反序列化的过程。 说白了就是我们要将结构化的数据转换成字符串然后再将字符串反序列化变成结构化的数据这是我们在网络通信里必须得做的。  为什么需要进行序列化和反序列化 答1.这种结构化的数据是不便于网络传输的。结构化的数据在网络上不好传输但我把整个对象转成一个字符串字符串里面包含了你的每一个字段的信息到了对方以后我再把字段的信息一个个提出来形成一个对象整个就是序列化和返反序列化的过程。字符串便于网络传输。归根结底就是为了应用层网络通信的方便。 2.序列和反序列化不是目的在我们两个人的上层还有其他应用比如图形界面显示要拿你的昵称头像消息时间因为是结构化的数据所以我们可以用类名方法就把他的属性拿到了。如果我们在对端只拿这个字符串去用上层就需要把这个字符串解析的工作全部由我们自己再次完成这太麻烦了所以我们先反序列化得到结构化的数据就方便我们一次一次的向上层去拿。总结为了方便上层进行使用内部成员将应用和网络进行了解耦应用压根不关心网络发送作为应用我只关心结构体数据里的成员结构化的数据怎么在网络里传输如何序列化和反序列化应用层压根不关心。这就完成了解耦。 我们之前的tcpudp通信有没有进行任何的序列化和反序列化呢 压根就没有原因就是我们没有应用场景我们不知道我们要干什么我们就不知道应用场景是什么就没办法定制序列和反序列化的一些结构化数据也就不需要字符串的序列和反序列化的过程所以之前我们并没有。         这里结构化的数据本质就是协议的表现就好比QQ软件就知道昵称对应的信息是什么样子的头像是连接还是文件时间是以什么格式展示的...这就叫做协议 如何进行序列化和反序列化呢 像xml,json,protobuff就是专门用来负责做序列化和反序列化这样的过程的。 网络版计算器 目的 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端. 约定方案一: 客户端发送一个形如11的字符串; 这个字符串中有两个操作数, 都是整形; 两个数字之间会有一个字符是运算符, 运算符只能是 ; 数字和运算符之间没有空格; ... 这就是约定当客户端发送一个数据的时候服务端立马就意识到它一定有操作数和操作符而且操作符左右两侧一定是数据没有空格所以服务端收到11这样的字符串就需要根据操作符把1和1解开。 我们这样去做的伪代码 这个过程是比较麻烦的这个序列化和反序列化动作都是由我们自己做的  约定方案二: 定义结构体来表示我们需要交互的信息; (结构化的数据)发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体; 这个过程叫做 序列化 和 反序列化 这种方式发的时候发了一个结构体收的时候也收了一个结构体因为他俩成员一样一旦收到内容后就直接可以在上层拿到内容了。但是这种方式存在问题因为这种方式是语言的特性做了序列化和反序列化但是不太推荐。 我们接下来就实现一个网络版本的计算器我们利用约定方案二进行实现  Protocol.hpp 因为存在10/0或者10%0这样的错误操作所以我们需要一个code代表运算完毕的状态。换言之我将来想要拿到退出结果的时候我们要做的第一件事情是先检查code只有code是0的时候result才有意义否则result是没有意义的。 以上就定制好了我们的协议双方通信时采用的数据格式我们发的一定是request这种数据格式收的一定是response这种格式上面的这两种数据就叫做结构化数据我们发的时候可以把request定义的对象直接发过去但是这样发存在问题客户端和服务器对于结构体的大小可能不一样而且不同平台的大小也不一样eg:这里的结构体里面用的是整形有一天你将客户端发出去了服务器升级了一个int占64个比特位这样的话客户端发过来的数据结构体大小就匹配不上了就出问题了。所以我们需要序列化和反序列化的。 做一个套接字接口的封装 Sock.hpp #pragma once#includeiostream #includestring #includecstring #includecstdlib #includesys/socket.h #includenetinet/in.h #includearpa/inet.h #includeunistd.husing namespace std; class Sock { public:static int Socket(){int sock socket(AF_INET, SOCK_STREAM, 0);if (sock 0){cerr socket error endl;exit(2); //直接终止进程}return sock;}static void Bind(int sock,uint16_t port){struct sockaddr_in local;local.sin_family AF_INET;local.sin_port htons(port);local.sin_addr.s_addr INADDR_ANY;if (bind(sock, (struct sockaddr *)local, sizeof(local)) 0){cerrbind error!endl;exit(3);}}static void Listen(int sock){if (listen(sock, 5) 0){cerr listen error ! endl;exit(4);}}static int Accept(int sock){struct sockaddr_in peer;socklen_t len sizeof(peer);int fd accept(sock, (struct sockaddr *)peer, len);if (fd 0){return fd;}return -1;}static void Connect(int sock, std::string ip, uint16_t port){struct sockaddr_in server;memset(server, 0, sizeof(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){cout Connect Success! endl;}else{cout Connect Failed! endl;exit(5);}} }; CalServer.cc #includeProtocol.hpp #includeSock.hpp #includepthread.h static void Usage(string proc) {cout Usage: proc port endl;exit(1); }void* HandlerRequest(void* args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());//version1 原生方法没有明显的序列化和反序列化的过程//业务逻辑做一个短服务 request - 分析处理 - 构建response -sent(response) -close(sock)//1.读取请求request_t req;ssize_t s read(sock, req, sizeof(req));if (s sizeof(req)){//读取到了完整的请求待定//req.x,req.y,req.op// 2.分析请求 3.计算结果response_t resp {0, 0}; //响应默认设置0,0// 4.构建响应并进行返回switch (req.op){case :resp.result req.x req.y;break;case -:resp.result req.x - req.y;break;case *:resp.result req.x * req.y;break;case /:if (req.y 0){resp.code -1; //代表除0}else{resp.result req.x / req.y;}break;case %:if (req.y 0){resp.code -2; //代表模0}else{resp.result req.x % req.y;}break;default:resp.code -3; //代表请求方法异常break;}cout request: req.x req.op req.y endl; write(sock, resp, sizeof(resp));cout 服务结束 endl;}//5.关闭连接close(sock); }// ./CalServer port int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for( ; ; ){int sock Sock::Accept(listen_sock);if (sock 0){cout get a new client... endl;int *pram new int(sock);pthread_t tid;pthread_create(tid, nullptr, HandlerRequest, pram);}}return 0; } CalClient.cc #include Protocol.hpp #include Sock.hppvoid Usage(string proc) {cout Usage: proc server_ip server_port endl; }//./CalClient server_ip server_port int main(int argc, char *argv[]) {if (argc ! 3){Usage(argv[0]);exit(1);}int sock Sock::Socket();Sock::Connect(sock, argv[1], atoi(argv[2]));//业务逻辑request_t req;memset(req, 0, sizeof(req));cout Please Enter Date One# ;cin req.x;cout Please Enter Date Two# ;cin req.y;cout Please Enter operator# ;cin req.op;ssize_t s write(sock, req, sizeof(req));response_t resp;s read(sock, resp, sizeof(resp));if (s sizeof(resp)){cout code[0:success]: resp.code endl;cout result: resp.result endl;}return 0; } 测试结果 ps:如果你想改成长服务你就需要在服务端把刚刚做的那一批请求包裹在死循环中就可以了。 服务器怎么知道是x op y 呢服务器怎么知道op的-*/%是什么含义呢服务器怎么知道code为0,1,2,3的意思呢我怎么知道客户端和服务器又怎么知道 这就叫做约定所以我们刚刚用结构化的数据又结合我们自己的约定然后我们就定义了一个简单的协议这就叫做约定。 存在的问题  我们今天的协议逻辑上是没有问题的不过如果客户和服务器使用这样的原生结构体的方式发送二进制数据的内容一个结构化的数据发过去到对方事实证明这样是行的。但是能起效果仅仅会满足90% 的情况。当我们正常通信的时候客户端和服务器如果软件版本是迭代的那么如果现在你有一个老的客户端你用这样的方式比如以前结构体的内存对齐方式结构体的大小使用的是标准A后来经过5年10年的发展计算机不断进步内存对齐的方式变得和之前不一样了客户端已经发给客户了那么作为客户来讲客户就喜欢老东西就喜欢老的客户端但你的服务器早就升级成了很高很高的版本那么当客户端发来消息的时候那么如果哪怕有一个字节发生变化那你的协议就跑不起来了。所以这样的方案并不好而且并不适合后序大型的业务处理。在应用层这种结构化的数据直接发虽然在我们目前看来可行但我们不建议因为我们少了序列化和反序列化的步骤。 json 如何进行序列化和反序列化 jsoncpp是C当中使用频率很高的一个进行序列化和反序列化的一个组件。 云服务器通过该条指令进行安装 sudo yum install -y jsoncpp-devel 这条命令就是安装我们的开发包。 安装完毕。 这就是我们安装的json安装一个库说白了就是安装一堆对应头文件和它对应的一堆库我们就可以直接使用这个库了。 json的使用 序列化 #includeiostream #includestring #includejsoncpp/json/json.htypedef struct request {int x;int y;char op; }request_t;int main() {request_t req {10, 20, *};//进行序列化Json::Value root; //可以承装任何对象json是一种kv式的序列化方案root[datax] req.x;root[datay] req.y;root[operator] req.op;//FastWriter StyledWriterJson::StyledWriter writer;std::string json_string writer.write(root);std::cout json_string std::endl;return 0; } 执行结果编译的时候要指明链接的动态库 此时我们就形成了一个序列化的结果有人说这不还是结构化的数据吗事实上已经完成不一样了我们自己写的结构体是一个二进制的数据也就是内存是什么样子网络里就是什么样子但我们的运行结果是一个字符串的数据。 我们将StyledWriter改为FastWriter我们再次运行。 这个时候得到的就像一个字符串了实际上人家就是一个字符串其中每一个字段datax就是10datay是20operator是42*的ASCII是42这就是一种kv的方案。这就是序列化的过程我们通过网络发送的不是我们自己定义的结构体发送的是我们的运行结果。  反序列化 序列化是把结构化的数据转换成一个字符串反序列化的目的就是把一个字符串转换成一个结构化的数据。 运行结果  ps:关于R 新的C标准可以在代码里嵌入一段原始字符串该原始字符串不作任何转义所见即所得这个特性对于编写代码时要输入多行字符串或者含引号的字符串提供了巨大方便。 先介绍特性如下 原始字符串的开始符号 R(   原始字符串的结束符号)。R 与  (  之间可以插入其它任意字符串。 eg2:  int main() {//反序列化std::string json_string R({ datax:10, datay:20,operator:42 });// 这个R就是代表原生字符串把里面的内容统一当做最原始的内容来看。 Json::Reader reader;Json::Value root;reader.parse(json_string, root);request_t req;req.x root[datax].asInt();req.y root[datay].asInt();req.op (char)root[operator].asInt();std::cout req.x req.op req.y std::endl; } 执行结果 我们对计算器进行优化内置序列化和反序列化 Protocol.hpp //这个头文件就是客户端和服务器通信时的协议内容也就是我们自己需要定制协议了。 #pragma once#includeiostream #includestring #includejsoncpp/json/json.husing namespace std;//定制协议的过程目前就是定制结构化数据的过程 //请求格式 //我们自己定义的协议clientserver都必须遵守这就叫做自定义协议。 typedef struct request {int x;int y;char op; //,-,*,/,% }request_t;//响应格式 typedef struct response {int code; //server运算完毕的计算状态code(0:success) code(-1:div 0)...int result; //计算结果能否区分是正常的计算结果还是异常的退出结果 }response_t;//序列化 request_t - string std::string SerializeRequest(const request_t req) {Json::Value root; //可以承装任何对象json是一种kv式的序列化方案root[datax] req.x;root[datay] req.y;root[operator] req.op;// FastWriter StyledWriterJson::FastWriter writer;std::string json_string writer.write(root);return json_string; }//反序列化 string - request_t void DeserializeRequest(const std::string json_string,request_t out) {Json::Reader reader;Json::Value root;reader.parse(json_string, root); out.x root[datax].asInt();out.y root[datay].asInt();out.op (char)root[operator].asInt();}std::string SerializeResponse(const response_t resp) {Json::Value root;root[code] resp.code;root[result] resp.result;Json::FastWriter writer;std::string res writer.write(root);return res; }void DeserializeRequest(const std::string json_string, response_t out) {Json::Reader reader;Json::Value root;reader.parse(json_string, root);out.code root[code].asInt();out.result root[result].asInt(); } Makefile .PHONY:allall: CalClient CalServerCalClient:CalClient.cc g -o $ $^ -stdc11 -ljsoncppCalServer:CalServer.cc g -o $ $^ -stdc11 -lpthread -ljsoncpp.PHONY:clean clean:rm -rf CalClient CalServer CalServer.cc #include Protocol.hpp #include Sock.hpp #include pthread.h static void Usage(string proc) {cout Usage: proc port endl;exit(1); }void *HandlerRequest(void *args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());// version1 原生方法没有明显的序列化和反序列化的过程//业务逻辑做一个短服务 request - 分析处理 - 构建response -sent(response) -close(sock)// 1.读取请求char buffer[1024];request_t req;ssize_t s read(sock, buffer, sizeof(buffer) - 1); //读到buffer里面if (s 0){buffer[s] 0;cout get a new request: buffer std::endl; //看到反序列化之前的结果std::string str buffer;DeserializeRequest(str, req); //反序列化请求//读取到了完整的请求待定// req.x,req.y,req.op// 2.分析请求 3.计算结果response_t resp {0, 0}; //响应默认设置0,0// 4.构建响应并进行返回switch (req.op){case :resp.result req.x req.y;break;case -:resp.result req.x - req.y;break;case *:resp.result req.x * req.y;break;case /:if (req.y 0){resp.code -1; //代表除0}else{resp.result req.x / req.y;}break;case %:if (req.y 0){resp.code -2; //代表模0}else{resp.result req.x % req.y;}break;default:resp.code -3; //代表请求方法异常break;}cout request: req.x req.op req.y endl;// write(sock, resp, sizeof(resp)); //这次就不能直接写入了你得先序列化std::string send_string SerializeResponse(resp); //序列化之后的字符串write(sock, send_string.c_str(), send_string.size());cout 服务结束 send_string endl;}// 5.关闭连接close(sock); }// ./CalServer port int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for (;;) //创建线程完成服务器就周而复始的运行每来一个请求它就创建线程分离现场然后进行业务处理{int sock Sock::Accept(listen_sock);if (sock 0){cout get a new client... endl;int *pram new int(sock);pthread_t tid;pthread_create(tid, nullptr, HandlerRequest, pram);}}return 0; } CalClient.cc #include Protocol.hpp #include Sock.hppvoid Usage(string proc) {cout Usage: proc server_ip server_port endl; }//./CalClient server_ip server_port int main(int argc, char *argv[]) {if (argc ! 3){Usage(argv[0]);exit(1);}int sock Sock::Socket();Sock::Connect(sock, argv[1], atoi(argv[2]));//业务逻辑request_t req;memset(req, 0, sizeof(req));cout Please Enter Date One# ;cin req.x;cout Please Enter Date Two# ;cin req.y;cout Please Enter operator# ;cin req.op;std::string json_string SerializeRequest(req);ssize_t s write(sock, json_string.c_str(), json_string.size());char buffer[1024];s read(sock,buffer,sizeof(buffer)-1);if(s 0){response_t resp;buffer[s] 0;std::string str buffer;DeserializeRequest(str,resp);cout code[0:success]: resp.code endl;cout result: resp.result endl;}// response_t resp;// s read(sock, resp, sizeof(resp));// if (s sizeof(resp))// {// cout code[0:success]: resp.code endl;// cout result: resp.result endl;// }return 0; } 在客户端我们输入了一下数据然后把构建的请求序列化成字符串然后就发字符串发送字符串后对端收到了然后就响应响应后我们就读读的时候读到的一定是响应字符串然后我们再对响应字符串进行反序列化反序列化到response然后就拿到了response的结果。对于server来讲就是先进行读取读取到的内容一定是序列化后的请求然后我们先对请求进行反序列化然后得到对应的结果并进行分析分析完后得到响应响应得到之后再对响应进行序列化然后把它写回去。所以我们就在代码中植入了序列化和反序列化的过程。 测试 这就是序列化和反序列化我们的最终目的是让网络程序在通信的时候我们不想让他们直接传递结构化的数据而是让他们进行互相发送字符串方便我们后序做调整。  对网络计算器的总结 我们刚刚写的cs模式的在线版本计算器本质就是一个应用层网络服务我们的客户端用的是我们对应的linux的客服端服务器是我们自己写的服务器。 我们所做的工作 1.基本通信代码是自己写的一堆socket 2.序列和反序列化是我们用组件完成的 3.业务逻辑是我们自己定的 4.请求结果格式code含义等约定是我们自己做的 这就叫做我们完成了一个自定义的应用层网络服务。 结合上述我们在看看OSI的七层协议 OSI和TCP/IP的差别无非就是上三层TCP/IP上三层就叫做应用层。会话层就对应了我们做的工作1让我们能进行正常的网络通信。表示层对应了工作2。应用层对应我们的工作3,4。OSI考虑的就是这个点可是实际做下来后我们发现这三层很多都是我们写的甚至是用别人的组件写的。OSI制定的这三层是不可能完成独立开的也不能写到内核当中所以我们就把它三个在TCP/IP里放到应用层。说白了就是上层应用层我不写了你自己实现吧。所以就有了基本套接字序列化和反序列化的基本组件然后还有特定应用场景的各种协议的出现。 HTTP协议 虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议) 就是其中之一. http协议本质上在定位上和我们刚刚写的网络计算器没有区别都是应用层协议。 http的网络通信序列化和反序列化协议细节都是http协议内部需要自己实现的。 认识URL 平时我们俗称的 网址 其实就是说的 URL.网址是定位网络资源的一种方式。 我们请求的图片htmlcssjs视频音频标签文档等这些我们都称之为资源。服务器后台使用linux做的。目前我们可以用ipport唯一的确定一个进程。但是我们无法唯一的确认一个资源公网IP地址是唯一确定一台主机的而我们所谓的网络“资源”都一定是存在与网络中的一台linux机器上linux或者传统的操作系统保存资源的方式都是以文件的方式保存的。 eg:你今天刷的抖音在抖音的服务器上一定是一个个的短视频今天刷的视频音频看的文档查看的网页全部是在linux当中以文件的方式存的。但linux系统标识一个唯一资源的方式我们通过路径进行 所以IPlinux路径就可以唯一的确定一个网络资源 ip通常是以域名的方式呈现的。路径可以通过目录名分隔符确认。 URL全称Uniform Resource Location译为统一资源定位符。它就是通过网址确认哪一个资源在哪一个服务器上。 eg: url字段解析 https:就是请求该资源的方法说白了就是使用的协议 。 所以一个基本的URL构成就是通过协议域名资源路径可能还会带参这样的方式去构成的URL。URL存在的意义叫做确认全网中唯一的一个资源。          urlencode和urldecode 像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义. eg: 说明我们在URL处理的时候有些特殊符号是需要被特殊处理的其中我们把像这些在URL中不能直接出现的符号由原始的字面值的样子转化成这种16进制方案我们称之为urlencode.叫做对字符进行转码。  将C??转化成C%3F%3F,这种样子我们称之为encode编码。 将C%3F%3F转化成C??我们叫做decode解码。编码和解码是由浏览器自动帮我们编码服务器收到之后可能要自己解码但是我们一般不自己做。 这样处理的根本原因就是它其实不想让这些特殊字符在URL中出现而影响URL本身的合理性。 转义的规则如下 将需要转码的字符转为16进制然后从右到左取4位(不足4位直接处理)每2位做一位前面加上%编码成%XY 格式 eg: HTTP协议格式 简化对请求和响应的认识 无论是请求还是响应基本上http都是按照行\n为单位进行构建请求或者响应的 无论是请求还是响应几乎都是由3或者4部分组成。 HTTP请求 http request的格式 http的请求的所有内容都是字符串传视频音频可能有二进制但在它的报头里面我们不考虑 请求行  请求行一般分为三部分 请求方法 URL(请求的资源定位服务) http version(http协议的版本) 常见的方法比如get方法URL一般是去掉域名之后的内容也就是你要访问的网络的一个路径资源http version也是一个字符串。 eg:http / 1.1。这三部分都已字符串的形式构成一个请求行这个请求行中间以空格作为分隔符分成三个区域最后以\n结束。 请求报头  第二大块我们称之为请求抱头这里画这么大仅仅是因为这里有多个请求抱头实际上每个请求抱头依旧是以行为单位进行陈列的构建成多行内容。每一行都是以key:value的方式呈现的。它的请求抱头是由一个一个的kv形式的属性构成。每一个属性的结束符都是\n。站在http服务器读取的时候这部分全部是一行内容。 空行  第三部分是空行 请求正文  第四部分是请求正文如果有的话常见的请求正文主要是用户提交的数据。 如何理解普通用户的上网行为 我们这个答案是简洁版本。仅仅分为两步 1.从目标服务器拿到你要的资源。 2.向目标服务器上传你的数据。 我们现在所有上网行为仅仅分为这两步。 eg:你平时刷抖音抖音的视频可不是在你的手机上的而是在服务器上通过网络传的所以那些资源是在服务器上的。你平时用的淘宝京东只要你是你看到的东西基本是从服务器拿下来的。 比如我们进行登录注册支付下单再比如上传一个抖音发朋友圈这全部都叫做上传一个数据。 我们所有的上网行为无外乎这两种。这个行为对应到计算机当中就是IO的过程。而且人的所有上网行为本质都是进程间通信进行间通信的过程你给我发我给你发对我来讲就是IO。 所有用户提交的数据比如说是你要进行登录注册...这样的信息包括你上传的视频音频都是在正文部分。 综上就是http的请求内容。 http的响应 http的响应的构成和请求在宏观上的构成是一毛一样的它也是由三部分构成。 状态行 状态行也由三部分构成 http version的版本 状态码 状态码描述 http version比较常见的也是http/1.1 。状态码最常见的一个就是404404就是资源找不到我怎么知道404是什么含义呢所以404的状态码描述就叫做not found表示你的资源找不到。 响应报头 同请求报头 空行 整行就是一个空行 响应正文  响应正文比如你今天要进行数据请求的时候你要进行登录最后登录成功的时候我要给你响应的时候返回的肯定是登录成功之后我的网站的首页。所以这个响应里面经常是htmlcssjs音乐视频图片等这些就叫做响应正文。 请求与响应的整体格式 思考无论请求还是响应最终所有的内容都是以行呈现的http也是一个完整的协议。上面的两部分是服务器或者浏览器关心的下面正文部分是人关心的。 我们需要讨论的问题 1.http如何解包如何分用如何封装 2.http请求或者响应是被如何读取的 3.http请求是如何被发送的 23结合就是一个问题http request和http response 被如何看待 http request和http response 被如何看待 我们可以将请求和响应整体看做一个非常大的字符串 所以对于http来讲但是http是由很多行构成的看起来我们需要一行一行进行发送但是实际上我们看他的时候就是一个长字符串它只是文本格式是一行一行的。一旦它被看做一个大的字符串那么它的读取和发送都是按照字符串进行读取和发送的。换句话说越靠近头部的地方一定是越先被发送的越靠近尾部的地方越后被发送或者接受。 如何解包和封包呢 在众多行中出现了一个空行空行是一个特殊字符空行什么都不写换一行结束时\n再换一行用空行可以做到将长字符串一切为二这个大字符串以空格为分割符将它分为前半部分和后半部分。所以当我们解包一个http的时候我们按行进行读只要我们读到的一行内容是空行我们就认为我把报头读完了剩下的全部都是有效载荷。所以我们就能做到解包。封装的时候要构建http请求请求行和报头全加上最后再带上一个空行然后在跟上正文这就叫做封装。这就是http的请求响应也同样如此。所以我们的http用来区分报头和有效载荷在编码层面上是通过一个特殊字符来区分报头和有效载荷的。 如何分用 分用就是把你的有效载荷交给谁这个问题不是http解决的是具体的应用代码解决http需要有接口来帮助上层获取参数。 http的请求和响应在我看来是以行为单位罗列了一大批内容在网络看来它就是一个大字符串然后这个字符串左半部分和右半部分是通过空行分割的所以我们能做到封装了封装的时候就是报头空行正文解析的时候一直读只要读到空行前半部分全都它的报头后半部分就是正文。 HTTP操作 1.看看http请求 2.发送一个响应 http底层采用的依旧是tcp协议 http中面向字节流读的函数recv 这个函数是专门来进行网络读取所定制的一个函数。这个函数和read的使用几乎没有任何的差别recv唯一多了一个flags,这个参数可以让我们以阻塞非阻塞包括读取后序的解密指针这样的一些东西不过我们全程不设置或者默认为0. 最简单的HTTP服务器 实现一个最简单的HTTP服务器, 目前我们的服务端啥也不干; #includeSock.hpp #includepthread.h void Usage(std::string proc) {std::cout Usage: proc port std::endl; }void *HandlerHttpRequest(void *args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());#define SIZE 1024*10char buffer[SIZE];memset(buffer, 0, sizeof(buffer));ssize_t s recv(sock, buffer, sizeof(buffer), 0);if (s 0){buffer[s] 0;std::cout buffer std::endl; //查看http的请求格式}close(sock);return nullptr; }int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for ( ; ; ){int sock Sock::Accept(listen_sock);if (sock 0){pthread_t pid;int *parm new int(sock);pthread_create(pid, nullptr, HandlerHttpRequest, parm);}} } 测试 我们运行是这个样子的 然后我们借助浏览器在搜索栏输入服务器的ip:port 此时因为没响应没给他发任何数据但是此时我已近向服务器发送了多条http请求了 为什么客户端发了多条请求呢 主要是因为http server得不到响应所以它把自己请求在重复的发送。只要点击这个域名就能把你的请求发送到我的云服务器上了。 HTTP请求  这就是一个较为完整的http请求 如果我们把换行符去掉我们就能看到两个请求之间只空一行 这个http的请求和响应为什么都带了版本呢 因为客户端是个软件服务器也是个软件两个都有版本。 eg:所有的软件都会升级包括OS也会升级现在有些人升级了微信有些人没升级作为服务器是不会给新微信和就微信分别提供一个server提供服务的。而是根据你的微信的版本如果你是老版本给你提供一些功能如果你是新版本在给你提供一些功能。软件的版本决定它所能看到的功能对于服务器来讲就可以统一使用一套服务来处理所有的新老客户端了。 对于HTTP请求的报头字段的解释  Host代表这个请求你想访问谁后面跟着的就是我的公网ip和port。Connection代表的是连接类型我们都是基于短链接的Cache-Control代表双方通信时的一些缓存信息Upgrade-Insecure-Requests代表协议升级情况User-Agent代表我们的浏览器访问时客户端的信息比如平时我们下载软件的时候User-Agent详解 它有很多的OS默认给我们选中的就是windows这就是因为你的http请求里就包括了User-Agent里面就写了你的客户端信息。 博主用手机进行访问 我们可以看出 User-Agent显示的就是博主手机的信息。 所以我们用不同的设备去访问网站的时候浏览器会自动根据你的本地平台帮你去识别你的主机客户端是什么然后服务端收到这些请求之后它可以识别这些信息尤其是下载平台它会根据你的设备给你推测出你的设备所适合下载的软件。 Accept-Encoding:代表接受的编码类型Accept-Language: 代表接受的语言类型        服务器构建响应 现在我们的程序已经见到这个请求了接下来我们想在服务端构建一个响应。 我们现在的响应就是无论请求什么我都返回hello这样的字符串内容。返回时候就是向客户端写入写入我们可以采用write。但是这里我们推荐send这个是在linux中特有的针对于tcp设计的接口。 send  除了多了一个参数其他的和write一毛一样。 这样就可以直接进行发送响应吗答案是不可以因为我们是在模拟http的行为你要写的可是一个http的响应一个响应由三部分构成我们不能只把响应正文给它状态行报头空行我们都要带上。 当我们读这个http的响应或者请求时是从上往下读的也就是一行一行的去获取的其中Content-Type属于响应报头的属性之一所以我是如何知道你的有效载荷是什么类型呢Content代表内容Type代表类型。Content-Type就代表的是我的请求或者响应如果携带了正文那么正文的类型是什么就叫做Content-Type。 Sock.hpp #pragma once#includeiostream #includestring #includecstring #includecstdlib #includesys/socket.h #includenetinet/in.h #includearpa/inet.h #includeunistd.husing namespace std; class Sock { public:static int Socket(){int sock socket(AF_INET, SOCK_STREAM, 0);if (sock 0){cerr socket error endl;exit(2); //直接终止进程}return sock;}static void Bind(int sock,uint16_t port){struct sockaddr_in local;local.sin_family AF_INET;local.sin_port htons(port);local.sin_addr.s_addr INADDR_ANY;if (bind(sock, (struct sockaddr *)local, sizeof(local)) 0){cerrbind error!endl;exit(3);}}static void Listen(int sock){if (listen(sock, 5) 0){cerr listen error ! endl;exit(4);}}static int Accept(int sock){struct sockaddr_in peer; //对端的信息socklen_t len sizeof(peer);int fd accept(sock, (struct sockaddr *)peer, len);if (fd 0){return fd;}return -1;}static void Connect(int sock, std::string ip, uint16_t port){struct sockaddr_in server;memset(server, 0, sizeof(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){cout Connect Success! endl;}else{cout Connect Failed! endl;exit(5);}} }; HTTP.cc #includeSock.hpp #includepthread.h void Usage(std::string proc) {std::cout Usage: proc port std::endl; }void *HandlerHttpRequest(void *args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());#define SIZE 1024*10char buffer[SIZE];memset(buffer, 0, sizeof(buffer));// 这种读法是不正确的只不过现在没有被暴露出来罢了ssize_t s recv(sock, buffer, sizeof(buffer), 0);if (s 0){buffer[s] 0;std::cout buffer; //查看http的请求格式std::string http_response http/1.0 200 OK\n;//版本状态码状态码描述\n做结尾 这就是状态行//在我看来就是一个长字符串所以不用一行一行发只要构建好一块发出去就可http_response Content-Type: text/plain\n; //text/plain正文是普通的文本 这就是响应报头http_response \n; //这就是空行用来区分报头和有效载荷http_response Shi Jiayi is a beautiful and hard-working girl; //这就是正文send(sock, http_response.c_str(), http_response.size(), 0);}close(sock);return nullptr; }int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for ( ; ; ){int sock Sock::Accept(listen_sock);if (sock 0){pthread_t pid;int *parm new int(sock);pthread_create(pid, nullptr, HandlerHttpRequest, parm);}} } 测试 我们就在浏览器上看到了刚刚写的hello pxl.这就叫做http的响应。这就叫做http的简单响应和简单请求。 HTTP的请求与响应总结 http的请求就是请求行请求报头空行请求正文 响应就是状态行响应报头空行响应正文 。 对我们来讲就是3~4部分中间用空行作为区分我们就能很快的区分清楚他们之间的一个格式要求。实际上在编码层面上你发过来的请求都是被我读到buffer里面从前向后读。虽然你的请求和响应是行状的格式但是在我看来就是一个大字符串所以我就可以在读取的时候对我们的协议内容做读取分析。所以http协议如果自己写的话本质是我们要根据协议内容进行文本分析因为协议别人已经给我们定好了得到对应的响应然后将响应构成字符串在响应回别人。 观察百度的网页 我们登录一下百度的服务器.没有telent命令我们可以手动安装一下 sudo yum install telnet telnet-server 手动的用Telnet构建一个请求 1.telnet www.baidu.com 80 2.Telnet窗口中按下“Ctrl]” 3.先按下回车输入 GET  /  HTTP/1.0 此时就得到了百度对应的响应 百度的http版本是1.0状态码是200状态描述是OK这是百度的老的服务器紧接着就是百度一堆的响应报头包括了Content-Type时间Server等。紧接着就是传说之中的网页 这些就是给我们压缩之后的一些html网页它里面内置了很多htmljs的内容。 我们在浏览器Edge打开百度官网 ,然后进入开发人员工具  这个元素里面就是百度的网页内容对应我们linux的那一大段内容linux是为了效率减少成本把\n空行全部去掉了所以看起来乱 。 总结一下我们实际上在http请求时它响应的正文部分一般都是我们的网页内容。包括图片视频这些。所以才有了http响应标识的这部分。 再谈http请求的细节 http请求没有使用json或者没有直接使用json那么它的整个报头或者正文用空行作为分隔符然后整体以行为单位这种也算一个序列化这种和发送结构体不一样http本身所有的文本行都是按行陈列的那么其中写入的时候一行一行去写读取再一行一行去读本质上就是一种序列化和反序列化的过程只不过没有显示化的把他显示出来。http里有很多的方法包括很多的属性。 HTTP请求 首行: [方法] [url] [版本] 像如图这种的URL有时候服务器要做一个代理服务器要拿去你要访问资源的全的URL大多数的URL还是像之前一样去掉域名的那种。Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束。Body: 就是http请求或者响应的正文空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度。 理解Content-Length 实际上我们代码在读取http请求的时候这种recv的读法是不对的只不过现在没有被暴露出来 http的请求是通过多行的方式呈现的每一行用\n作为分隔符。我们今天考虑的是server端进行读取当我们读取的时候一次是定义了一个大的缓冲区。当你在recv的时候这个请求一定在底层http的底层就是tcp当你在进行recv时无论是从网络里读还是从tcp里读一定是tcp协议给你的数据那么其中你在recv的时候你定义的是1024*10个字节。 实际上 1.当http发来请求的时候实际上并不是像你所想一样一个一个发送的有可能http客户端会以某种方式向我们发送多个请求。 2.你今天的缓冲区定义的是1024*10这个大小如果它是多个请求的话每个请求是1024的大小你一次就把所有请求一起读完了。如果缓冲区大小是1025这个大小那么除了把整个报文读完还把下一个报文多读了一个字节。 读取要求 第一我们要保证每次读取都是读取完整的一个http请求。 第二保证每次读取都不要将下一个http请求的一部分读到我在读取的时候客户端有可能发送来多个http请求我可能在读取的时候一个http请求读完了因为我缓冲区设置的问题而导致它把下一个报文多读了这个时候下面的报文是残缺的报文上面的报文也不正确因为多了一块数据这次就需要你自己去对他做分析处理这样是比较麻烦的而且容易出问题。 你怎么保证你每次读到的是一个完整的http呢 当我们读取一个完整的http请求的时候我们按行读取如何判定我们将报头部分包括请求行和请求报头读完了呢  读到空行我们就可以确定报头全部读完了。 报头读完了就看后面还有没有正文如果有正文如何保证把正文全部读取完成呢而且不要把下一个http的部分数据读到呢 你要决定报头后面有没有正文本质上是决定一个请求或者是一个响应它对应的有没有涵盖它的请求或者响应的数据。报头后面有没有正文这个和请求方法有关。 假设有正文的话你如何知道空行之后有多少个字符呢 我不知道但是当我读到正文的时候我很清楚我已近把报头读完了报头读完了我们就能正确提取报头中的各种属性其中包括一个字段Content-Length。如果后面还有正文的话那么报头部分有一个属性这个属性就是Content-Length它表明正文部分有多少个字节 虽然没有学tcp但是我们知道所有的数据都是通过fd读取的所以我无脑读一直读到空行我就能保证把http协议报头部分全部读上来其中就包括了Content-LengthContent-Length就表明了如果有正文的话正文部分的字节数。所以当我们正常读取的时候一旦读到空行然后我们再来看它的Content-Length根据Content-Length确定读取多少个len自己的正文。Content-Length的存在就允许我们一字不差的吧它的正文部分全部读取到。 换句话说报头读取的时候以空行为分隔符把报头先得到然后根据报头中的Content-Length在读取正文这个正文我们就称之为http协议的有效载荷Content-Length表明http协议如果携带有效载荷它的 有效载荷是多长。Content-Length也叫自描述字段。 如何读到一个完整的http协议 1.有了空行我们可以保证把报头全部读完 2.报头全部读完我们就能分析方法和分析Content-Length从而得出要不要读正文以及正文多长 3.正文是多少字节我们按照Content-Length把正文全部读上来 至此我们就能读到一个完整的http请求或者响应。            Content-Length帮助我们读取到完整的http请求或者响应。同时根据空行能做到将报头和有效载荷进行分离分离的过程就是解包的过程。当你没有正文的时候报头属性里面是不存在Content-Length属性的。这就是规定。 HTTP响应 首行: [版本号] [状态码] [状态码解释] Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束 Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中. 同样的如果没有写Content-Length浏览器也会正常显示就比如我们自己实现的服务器发送的响应这个就和浏览器的编写应用层的支持是否严格有很大的关系其实应用层实际上做了很多的约定但是应用层要与人进行打交道所以你曾经定好的再好的规则只要是人参与进来了它的支持度并不好相当于我做了规定但是有人就是不遵守而且这个人还不少那么不遵守只能让浏览器厂商针对这种情况做一些优化但是一旦设计到内核大家基本上都会遵守的。 HTTP的请求方法 http请求是包括请求的方法URL和http的版本。http的版本最早的是1.0版本使用最广泛的是1.1版现在最新的还有2.0版。http 1.0和1.1最主要的区别是是否支持长连接。 短链接  短链接一个请求一个响应close socket。客户端发起一个请求服务器给一个响应服务器响应完毕后把链接一关。我们刚刚缩写的就是短链接。http协议最早使用的就是短链接。一个请求一般就是请求一个资源比如一个请求就是请求一张网页一个图片一个视频一个音频。请求完资源后链接关闭。 早期的http1.0为什么用短链接呢 1.当时的网络资源并不丰富主要以文本最多是图片为主。 2.当时的服务器压力并不大请求的资源都比较短小。网速比较好服务器上的资源就比较大网速特别差服务器上的资源就比较小。eg:1,2G的时代我们能看文字3G的时代我们能看图片4G的时代我们可以看视频。资源变的越来越丰富本质就是每一个资源的体积变的越来越大了。 最主要是因为短链接简单。 HTTP的方法 无论是请求还是响应都会携带http的版本。分别代表客户端和服务器采用的http版本。 http方法中GET和POST方法是最重要的两个方法没有之一。 GET大部分都是获取资源 POST是传输资源但其实他俩都可以进行获取和传输。 PUT方法你在浏览器上访问某些资源的时候你一点击就自动给你下载了实际上那个下载就对应的是PUT方法。HEAD方法其实就是客户端告诉服务器我不要正文只要报头。eg: HEAD方法就只拿到了http的报头没有正文。 用GET方法就拿到了报头和正文。 实际上HTTP看起来服务很多但是对于一个web服务器来说很多方法都是默认关掉的 eg:OPTIONSHTTP 1.1虽然支持但有可能用协议的人把这个方法给禁掉了。 像PUTDELETEOPTIONSTRACECONNECT这些方法http协议是支持的但实际上不一定被使用协议的人所打开。别人就把这些方法禁止掉了不让你用一般的http协议最多给你提供的方法就是GETPOSTHEAD。其他的不给你暴露出来。主要是为了防止出现一下恶意用户比如你把PUT暴露出来了有些恶意分子不断像你的服务器上传数据最后把你的磁盘打满还有一些直接通过DELETE方法删除你的数据。这肯定是不行的所以我们只给你暴露出有限的方法。 / (根目录) 首先我们在做请求的时候永远会带一个 / 这个 / 叫做要请求的资源。 当我请求时请求的就是这里的  / 服务器是怎么看待这个 / 的linux中这个 / 代表了根目录。但是在http请求中 / 并不是根目录而叫做web根目录。 eg:我们再次运行我们的服务器 如果请求默认是8080(8080后面啥也不带)我们看到对应的请求就是一个 /  如果你后面带了对应访问服务器上的某个路径/a/b/c/d其中就会把路径显示到这里。 所以这个 / 就叫做我们要访问的资源所在的路径 我们一般要请求的一定是一个具体的资源 比如你要请求的是一个网页图片。你给我一个 /  意思就是说我要请求的是web根目录下的所有内容我把这个网站的所有内容全部发给你。但是这样肯定是不行的我们要具体指明网页或图片的路径。 但是如果请求是 / ,意味着我们要请求该网站的首页首页一般叫做index.html 或者是index.htm 。换句话说你是 / 这么请求的别人的服务器可能最终默认的会把它的首页信息给你返回。   eg我访问百度两种访问的方式都能拿到百度的首页 实际上当你请求时如果你的请求时 / 我们的服务器它不会把web根目录下所有的内容给你返回而是想办法给你把根目录下的首页返回一般所有的网站都要有默认的首页信息对应的就是index.html这样的内容。 总结当我们请求某个资源的时候我可以通过带一个完整路径的方式也可以直接请求 / 默认就是首页。 实验 依然是基于我们上面的服务器代码 首先在当前路径下建立一个wwwroot目录这个wwwroot我们就称之为http的web根目录 在该目录下我们新建一个index.html 就是http它的一个首页信息。 这个http请求就是当用户在请求时通过http协议来把我们所对应的当前目录下的web根目录下的网页信息给你返回。  响应报头部分  首先我们指明访问文件的路径。此时别人给我请求时我就可以把这个网页信息返回。 但是返回的时候不仅仅返回正文网页信息而是还要包括http的请求  Content-Type 代表正文部分的数据类型今天我们响应回去的不是字符串而是把web服务器的根目录的index.html响应回去。 ps:Content-Type 对照表 HTTP Content-type 对照表 (oschina.net) 接下来我们还可以带一个Content-Length代表这个文件的大小。 stat 我们今天用stat获取文件大小。 stat可以通过指定的文件路径获取文件的指定属性 。-1就是失败0是成功。这个stat的结构体是一个输出型参数我们要把所有的属性获取出去。  st_szie就代表了文件的大小。  ​​ 响应正文  接下来就是正文上面的步骤就叫做构建一个响应. 这次的正文就是要拿的这个网页内容。 完整代码 Http.cc  #includeSock.hpp #includepthread.h #includesys/types.h #includesys/stat.h #includeunistd.h #includefstream#define WWWROOT ./wwwroot/ #define HOME_PAGE index.html// 其中wwwroot就叫做web根目录wwwroot目录下放置的内容都叫做资源 // wwwroot目录下的index.html就叫做网站的首页 void Usage(std::string proc) {std::cout Usage: proc port std::endl; }void *HandlerHttpRequest(void *args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());#define SIZE 1024*10char buffer[SIZE];memset(buffer, 0, sizeof(buffer));// 这种读法是不正确的只不过现在没有被暴露出来罢了ssize_t s recv(sock, buffer, sizeof(buffer), 0);if (s 0){buffer[s] 0;std::cout buffer; //查看http的请求格式// std::string http_response http/1.0 200 OK\n;//版本状态码状态码描述\n做结尾 这就是状态行// //在我看来就是一个长字符串所以不用一行一行发只要构建好一块发出去就可// http_response Content-Type: text/plain\n; //text/plain正文是普通的文本 这就是响应报头// http_response \n; //这就是空行用来区分报头和有效载荷// http_response Shi Jiayi is a beautiful and hard-working girl; //这就是正文// send(sock, http_response.c_str(), http_response.size(), 0);std::string html_file WWWROOT; //访问的文件在这个路径下html_file HOME_PAGE;struct stat st;stat(html_file.c_str(), st);//返回的时候不仅仅返回正文网页信息而是还要包括http的请求std::string http_response http/1.0 200 OK\n;// Content-Type 代表正文部分的数据类型今天我们响应回去的不是字符串而是把web服务器的根目录的// index.html响应回去。http_response Content-Type:text/html;charsetutf8\n;http_response Content-Length: ;http_response std::to_string(st.st_size);http_response \n;http_response \n; //空行//接下来才是正文std::ifstream in(html_file);if(!in.is_open()){std::cerr open html error! std::endl;}else{std::string content; //正文内容std::string line; while(std::getline(in,line)) //按行读取 {content line; //正文就全部在content里面了。}http_response content;in.close();send(sock, http_response.c_str(), http_response.size(), 0); //响应回去}}close(sock);return nullptr; }int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for ( ; ; ){int sock Sock::Accept(listen_sock);if (sock 0){pthread_t pid;int *parm new int(sock);pthread_create(pid, nullptr, HandlerHttpRequest, parm);}} } 测试 ps:对于port0-1023是系统内置的端口号我们用不了1024之后的随便用  比如我们将网页内容调整一下服务器不用关闭直接修改  此时我们就看到了修改后对应的网页内容 其中wwwroot就叫做web根目录wwwroot目录下放置的内容都叫做资源换句话说我将来可以在这个wwwroot下定义一个图片目录网页目录视屏目录...最后你要访问什么资源就是从这个wwwroot开始来去访问你想访问的资源的。 eg:我们在访问网站的时候上面是有对应的路径资/ 这个一定是它的服务器上的web根目录后面的就是具体路径 web根目录可以拷贝到linux下的任何目录下。wwwroot目录下的index.html就叫做网站的首页 当一个用户发起请求的时候如果它的请求是根目录服务器内部会对你的方法做判断你访问的是 / 就直接把你的路径改成web服务器下的首页信息。 验证GET和POST方法 上面我们做的事情就是就是把http的响应由字符串转换成了文件实际上将来要访问很多资源就是在web根目录下有很多的资源让你去访问。 我们平时有注册或者登录的经历输入账号和密码就需要有一个输入框你所看到的输入框实际上就是网页。 推荐学习前端网站w3school 在线教程https://www.w3school.com.cn/ GET方法 html的表单  制作表单 我们将我们的index.html首页进行修改  结果 我们在增加一个登录选项 我们发现输入信息点击登录以后后面没有任何内容,这是因为表单中这两个输入框我们没有给他起名字所以就无法获取参数 ps:URL 通过来带参数连接域名和参数经常会用到 再次修改 我们这里用的是GET方法当我们传入参数密码一点击登录提交它就会直接访问 /a/b/handler_from?namepxlpassword123456。 服务器的http请求 我们用的是GET方法我们没有正文提交的这两个参数是拼接在URL后面的以作为分隔符两个参数用进行隔离 。 GET方法如果提交参数是通过URL进行提交的。 换句话说实际上在服务器中当我们输入表单提交用的是GET方法的时候提交参数的时候浏览器自动把你的表单里的信息姓名和密码拼接在URL的后面然后让你的http请求拿到这样的数据这样的话前端的数据就被后端的C代码拿到了我们实际上拿到的这个请求参数是在我们读到的这个请求当中的所以数据就被C程序拿到了C程序拿到后就可以做字符串分割提取出用户名和密码再继续用C在服务器后端做一些连接数据库访问数据库和你输入上来的用户名密码作对比从而让你实现登录过程。 POST方法  我们直接将method改成POST  这次我们发现上面什么也没有。 我们查看下服务器的请求 POST请求的资源还是在URL中但是它的参数在正文当中所以POST方法是通过提交正文提交参数的。 GET和POST的区别 这就是GET和POST的区别。GET和POST都可以提交参数只不过GET是将参数拼接到URL后的POST是通过正文提交参数的。 通过抓包观察GET与POST 接下来我们使用Fiddler Classic工具这是一个抓包工具它是可以抓http的 我们直接打开该工具刷新下我们自己的网页就可以看到当前就抓到了第一个报文81.70.240.1968080这个端口 双击后我们就能看到发起的请求及得到的响应  我们输入密码后进行登录 就能看到我们提交的请求因为我们用的fiddler所以它的URL那样显示。 抓包的原理 本来我的client直接请求服务器服务器直接给client响应现在变成了我的client先把请求交给fiddler然后fiddler在帮我们去请求server给的响应再给fiddlerfiddler再给我们的client对应的浏览器所以所有的http请求都会流经fiddler所以fiddler就可以抓包。因为fiddler要帮助客户端去请求server所以它的URL显示的是该样式http://81.70.240.196:8080/a/b/handler_from  前面就是我的公网ip,因为它要给我请求。 fiddler查看POST 方法 fiddler查看GET 同样我们输入用户名密码后进行登录  这个就是GET方法fiddler把参数都抓到了正文部分没有。 总结首先请求的时候请求的是什么资源都是会出现在URL当中只不过POST和GET传参如果你进行提取参数的时候通常参数的位置是不一样的GET就在URL后POST在正文部分。 GET和PSOT 概念问题 GET方法叫做获取是最常用的方法。默认一般获取所有的网页都是GET方法但是如果GET要提交参数它也是可以进行提交的是通过URL来进行参数拼接从而提供给server端。 POST方法叫做推送是提交参数比较常用的方法但是如果提交参数一般是通过正文部分提交的但是你不要忘记Content-LengthXXX 表示参数的长度  区别 参数的提交位置不同 1.参数提交的位置不同POST方法比较私密但是私密安全很多人说POST传参更安全这种说法是错误的因为我们通过了fiddler抓包工具把他的数据给抓到了因为它不会回显到浏览器的URL输入框GET方法不私密它是会将重要信息回显到URL的输入框中增加了被盗取的风险。并不代表POST方法就没有被盗取的风险所有在网络里传送的数据没有经过加密全部都是不安全的随时都会被人直接扒出来。安全对我们来讲就是要进行加密 2.GET是通过URL传参的而URL是有大小限制的和具体的浏览器有关比如有的浏览器只允许你输入1024个长度的URL那么你在请求时超过他就没办法显示了。POST方法是由正文部分传参的一般大小没有限制。 如何选择 1.GET如果提交的参数不敏感数量非常少可以采用GET 2.POST提交的参数敏感数量多可以采用POST 所以选择时以GET为主GET顶不住了选用POST。 ps:HEAD方法就是当一个请求读上来后就是在buffer里面,当我知道它是HEAD以后我把响应响应回去正文部分不给他就可以了。 当我们在浏览器中输入数据这叫做前端数据当你一登录以GET或者POST方法提交参数那么这个参数不管是在URL当中还是在正文里面一定会保存到我们读取到的buffer里面。所以http协议处理本质是文本分析。 所谓的文本分析 1.0 http协议本身的字段比如将你的第一行拿出来空行一分拆出来你的请求方法请求的URL请求的版本空行之前的所有内容kv值把他放在一个map里我们就有了一个kv的请求报头。 2.0提取参数如果有的话。有可能客户端是给我们提取参数的最大的变化是前端的数据经过表单提交直接被你的C程序或者其他语言读到。读到之后就将前端数据转换成了后端语言。然后我们就可以用后端语言处理。GET或者POST其实是前后端交互的一个重要方式。 HTTP常见的状态码 应用层是人要参与的人的水平参差不齐http的状态码很多的人根本就不清楚如何使用又因为浏览器的种类太多了导致大家可能对状态码的支持并不是特别好。 比如我们把我们代码的响应的状态码改成404状态描述改为Not Found 我们通过fiddler看到响应是404 Not Found 但是访问服务器的时候仍然是正常显示的并没有做任何处理 类似于404的状态码对浏览器没有任何的指导意义浏览器就是正常的显示你的网页你返回的网页是什么我就给你显示什么浏览器不根据404做显示。 eg:类似于这种我们发现访问失联的网页并不是浏览器给你显示的而是服务器给你显示的。也就是浏览器对404不做处理就做一个正常的页面显示就完了。 具有指导意义的状态码  但是也存在一些状态码对我们是有指导意义的。  1开头的状态码表示请求正在处理 比如服务器收到一个请求服务器处理要花很长时间它要给你返回一个响应就相当于说客户端不要着急这个请求正在处理。1开头的用的非常少。2开头最常见的是200OK-请求正常处理完毕。3开头的状态码叫做重定向有301,302,307,308。4开头的最典型的是403Forbidden禁止访问,404Not Found我们要处理404这种错误不要指望浏览器而是说如果是404错误就需要自己进行处理比如如果404那么你就应该有一个404的相关处理当你请求的资源不存在你的服务器就应该构建一个正常的响应告诉客户端我不存在。5开头的状态码表示服务器内部错误最典型的右500,503,504。 eg:手动写一个404 我们将代码进行修改专门弄一个不存在的路径 #includeSock.hpp #includepthread.h #includesys/types.h #includesys/stat.h #includeunistd.h #includefstream#define WWWROOT ./wwwroot/ #define HOME_PAGE index.html-back// 其中wwwroot就叫做web根目录wwwroot目录下放置的内容都叫做资源 // wwwroot目录下的index.html就叫做网站的首页 void Usage(std::string proc) {std::cout Usage: proc port std::endl; }void *HandlerHttpRequest(void *args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());#define SIZE 1024*10char buffer[SIZE];memset(buffer, 0, sizeof(buffer));// 这种读法是不正确的只不过现在没有被暴露出来罢了ssize_t s recv(sock, buffer, sizeof(buffer), 0);if (s 0){buffer[s] 0;std::cout buffer; //查看http的请求格式// std::string http_response http/1.0 200 OK\n;//版本状态码状态码描述\n做结尾 这就是状态行// //在我看来就是一个长字符串所以不用一行一行发只要构建好一块发出去就可// http_response Content-Type: text/plain\n; //text/plain正文是普通的文本 这就是响应报头// http_response \n; //这就是空行用来区分报头和有效载荷// http_response Shi Jiayi is a beautiful and hard-working girl; //这就是正文// send(sock, http_response.c_str(), http_response.size(), 0);std::string html_file WWWROOT; //访问的文件在这个路径下html_file HOME_PAGE;//接下来才是正文std::ifstream in(html_file);if(!in.is_open()){std::string http_response http/1.0 404 Not Found\n;http_response Content-Type:text/html;charsetutf8\n; http_response \n; //空行http_response htmlp你访问的资源不存在/phtml;send(sock, http_response.c_str(), http_response.size(), 0); //响应回去}else{struct stat st;stat(html_file.c_str(), st);//返回的时候不仅仅返回正文网页信息而是还要包括http的请求std::string http_response http/1.0 200 OK\n;// Content-Type 代表正文部分的数据类型今天我们响应回去的不是字符串而是把web服务器的根目录的// index.html响应回去。http_response Content-Type:text/html;charsetutf8\n;http_response Content-Length: ;http_response std::to_string(st.st_size);http_response \n;http_response \n; //空行std::string content; //正文内容std::string line; while(std::getline(in,line)) //按行读取 {content line; //正文就全部在content里面了。}http_response content;in.close();send(sock, http_response.c_str(), http_response.size(), 0); //响应回去}}close(sock);return nullptr; }int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for ( ; ; ){int sock Sock::Accept(listen_sock);if (sock 0){pthread_t pid;int *parm new int(sock);pthread_create(pid, nullptr, HandlerHttpRequest, parm);}} } 404的错误属于客户端问题还是服务器问题 404的错误属于客户端的问题就好比你去京东去看抖音短视频你请求的资源在京东服务器上是没有的。只要一个网站的资源是具体的那么他一定是有限的只要是有限的就一定会碰到没有的 资源。 服务器的问题有哪些呢 比如今天来了个新请求创建线程失败了。处理请求时因为代码里存在问题而导致程序崩溃等等与服务器逻辑有关的错误客服端请求是常规请求但你服务器内部因为创建线程创建进程...出错了这就叫做服务器出错。 这就是5开头的状态码表示服务器内部错误最典型的右500,503,504。 3开头的状态码 3XX的状态码是有特殊含义的3开头的状态码主要是叫做重定向。 重定向 1.永久重定向 301 2.临时重定向 302或者307 重定向是什么 当访问某一个网站的时候会让我们跳转到另一个网址 当我访问某种资源的时候提示我登录跳转到了登录页面输入完毕密码登录的时候会自动跳转回来登录美团下单。比如你在淘宝上买个东西你下单支付后会告诉你支付成功然后说3秒内网页会自动跳转回去你可选择立即跳转你不管的时候3秒以后它就会自动跳转回去。像这种现象我们都叫做重定向。 比如在访问力扣的时候力扣想把老的网址废弃掉让我们使用新的网址。如果它直接把老网站封掉这样就会让很多老用户以为网站没了所以力扣就在网站上做处理直接访问新网址没有问题访问老网站时会自动跳转到新网站只不过力扣选择的是让我们手动点击跳转到新网站这就叫做重定向。  什么叫做永久重定向什么叫做临时重定向 因为老网址的服务器配置太低了所以我改了改成新网站了。但是其他的老的用户只认识老网站我不能直接把老网站关掉。所以我就在老网站的服务器中做了一个永久重定向。意思就是说现在有3个老用户依旧习惯访问老网站当他访问时老网站的服务器不对他们提供服务而是直接告诉老用户你请求的已经不是我了而是叫做www.new.com请你访问这个网站所以这个客户不需要知道它的浏览器会自动向新网站重新发起请求然后新网站重新得到响应给用户提供服务。如果用户将来还想访问老网站那么这种永久重定向会把用户以前记录的比如说书签你添加的这个书签是在浏览器里添加的当浏览器收到这个是永久重定向除了让浏览器访问新网站后还要浏览器把用户记录的书签由新的域名替换调旧的域名。以后用户点击书签就会直接去访问新网站再也不访问就网站了。 永久性重定向通常用来进行网站搬迁域名更换。对我们来讲就相当于把以前的服务全部迁到了新网站上面然后在老的服务上添加一个重定向功能然后一个客户来的时候老的服务器不提供服务直接通过永久性重定向告诉浏览器说我已近搬到最新的地方了。然后在更新下本地浏览器的缓存的一些数据包括书签当用户再次点击书签的时候就可以跳转到新网站不在访问老网站随着老用户不断去访问旧网站最后用户就全部被切换到了新网站中。这就是永久重定向。 比如说我今天在美团上下单支付成功了 就提示我支付已经成功正在返回中我从A页面跳转到支付页面支付成功后又返回到B页面商家接单的页面但是对我们来讲一定是从一个页面跳转到另外一个页面而且每次下单都有如此操作。这个跳转是为了完成某种业务的需求比如我进行注册后要给用户提供一个注册页面注册成功后要返回到登录页面登录成功后返回到首页当每一次登录或者注册成功不需要你自己手动的再去回到登录或注册首页而是服务器自动让你回去。每次登录注册都需要重复做这个工作属于业务的环节。这种情况就是临时重定向。 模拟重定向 重定向是需要浏览器给我们提供支持的浏览器必须识别310,302,307这样的状态码。server要告诉浏览器我应该再去哪里。http响应的报头属性Location新的地址            eg:服务器构建响应的时候状态码是301描述是永久重定向Location告诉我要重定向到哪里我们以重定向到腾讯的官网。访问我的时候就直接跳转到腾讯。 完整代码  #includeSock.hpp #includepthread.h #includesys/types.h #includesys/stat.h #includeunistd.h #includefstream#define WWWROOT ./wwwroot/ #define HOME_PAGE index.html-back// 其中wwwroot就叫做web根目录wwwroot目录下放置的内容都叫做资源 // wwwroot目录下的index.html就叫做网站的首页 void Usage(std::string proc) {std::cout Usage: proc port std::endl; }void *HandlerHttpRequest(void *args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());#define SIZE 1024*10char buffer[SIZE];memset(buffer, 0, sizeof(buffer));// 这种读法是不正确的只不过现在没有被暴露出来罢了ssize_t s recv(sock, buffer, sizeof(buffer), 0);if (s 0){buffer[s] 0;std::cout buffer; //查看http的请求格式std::string response http/1.1 301 Permanently moved\n;response Location:https://www.qq.com/\n;response \n;send(sock, response.c_str(), response.size(), 0);}close(sock);return nullptr; }int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for ( ; ; ){int sock Sock::Accept(listen_sock);if (sock 0){pthread_t pid;int *parm new int(sock);pthread_create(pid, nullptr, HandlerHttpRequest, parm);}} } 测试 从fiddler看出别人给服务器请求服务器给一个响应直接就是301永久重定向Location填的就是腾讯官网此时就直接跳转到了腾讯的官网  这个过程对我们来讲我们并不知道你以为你当前访问的是这个服务器实际上自动就别浏览器跳转到了新的网站。 测试302,临时重定向 永久重定向遗留的问题 做完301永久重定向这个实验后我们会发现自己的浏览器把这个服务器端口记住了这点还体现在你的服务端只有第一次访问域名服务器出现请求之后多次进行访问服务器并未出现任何请求不管你的服务端时候运行只要你访问这个域名就会直接给你重定向到腾讯官网如何解决 我们只需要清除一下301重定向这段时间内的浏览器缓存即可博主的浏览器是Edge 。清除的内容不需要手动选择浏览器默认的即可。清楚完毕后我们就发现我们的域名不在进行重定向了。 再谈HTTP常见Header Content-Type: 数据类型(text/html等) Content-Length: Body的长度 Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上; User-Agent: 声明用户的操作系统和浏览器版本信息; referer: 当前页面是从哪个页面跳转过来的; location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问; Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;我们之前的所有实验全部都是请求响应断开链接服务器上有很多资源。 一个大型的网页内部是由非常多个资源组成的每个资源都要发起http请求这个就是基于短连接的策略。所有http/1.0叫做短连接。http/1.1之后我们引入了长链接。其中客户端发起请求要设置一个Connection字段叫做Keep-alive叫做保持活说白了就是这个连接要一直使用长连接服务端构建响应也要包括Connection双方就任务我们都是1.1以上的版本就都用长连接这样的话双方通信时复用同一个连接此时就不用再打开连接关闭连接。这样的成本就降低了。 长连接与短连接 一般而言一个大网页是由多个元素组成的。 短连接 http/1.0 采用的网络请求的方案是短连接。 短连接的运算规则request-response-close。实际上进行一次http请求的时候本质就是把你要访问的对应资源打开返回一个资源这个资源可能是一张网页可能是网页里面的一张图片可能是一个视频等。所以如果网页由多个元素构成那么一次请求只返回一个资源在访问一个由多个元素构成的网页的时候如果是http/1.0就需要多次进行http请求比如我的网页构成里面有100个元素此时就要重复的进行http的100次请求而http协议是基于tcp协议的tcp要通信必须执行以下步骤 建立连接-传送数据-断开链接。每一次的http请求都要执行该步骤整体比较耗时间效率不高。短连接的效率比较低所以就诞生了长连接的概念http/1.1支持长连接。 服务端有一张网页这个网页里面又有很多很多的其他资源比如图片之类的。当我们的一个客户端发起请求的时候它请求到这个网页这个网页给他进行返回返回之后客户端拿到了这个网页本身我们发现这个网页里面有很多的包含第三方的链接或者另外一些资源客户端发现这些资源还是在你的服务器上所以客户端不断的进行请求和响应得到多份资源最后经过不断的重复请求连接得到对应的资源构建的一个网页这叫做短连接。 长连接  长连接建立好一条连接双方进行请求响应的时候都用这一条连接就能把这个网站中的所用的各种资源全部拿下来拿下来的时候这条连接始终不关闭这种技术就称之为长连接。 长连接主要解决的就是每一次请求都要进行请求建立tcp请求而导致每一次请求资源都要重新建立连接这样的频繁建立连接的过程。长连接通过减少频繁建立tcp连接来达到提高效率的目的我们虽然不知道tcp如何建立连接但是我们写tcp套接字我们的客户端必须得connect当connect成功才必须进行后序访问之前我们的实验中connect都很快这是因为你的服务器资源本身只有你一个人用没有人和你抢还有就是你connect的次数并不多如果tcp请求的频率特别高然后连接建立的次数太频繁其实就会降低我们的效率而我们通过长连接就可以直接通过一个连接把网页的所有资源得到在浏览器中构建一个完整的网页。 Connection 有可能双方有一方不支持长连接我们就可以通过报头里Connection这个选项Connection如果携带了Keep-Alive表示它支持长连接。有时候没有这个Connection选项或者这个选项是close就代表它不支持长连接只支持短连接。 类比生活中的例子我现在要让你办5件事情我通知第一件事情我给你打一个电话打完电话一挂过来一会我在给你打第二个电话以此类推让你办5件事情就给你打5次电话这5次电话每一次都要重新给你拨号然后你接通之后我们两建立连接成功在进行沟通对应的一件事情现在变成了我给你打一次电话你不要挂电话然后一个电话就把5件事情全部说完了这就是长连接。 cookie 与 session 背景引入 现实生活中很多地方我们用的都是http协议比如我们登录某些网站然后你会发现你把浏览器关掉了或者直接把网页关掉了过一会再重复的访问这个网站的时候第一次需要你进行账号密码的登录但是第二次第三次在访问这个网站的时候你就不需要在输入账号密码就能直接登入了。 http协议本身是一种无状态的协议就代表它很简单。 在比如我们打开一个视频网站我要看一个电影但是这个电影是VIP才能看的我登录账号以后这个视频网站是如何知道我是不是VIP呢如果是VIP就直接播放电影不是VIP就不能播放电影。 我们的日常经验在网站中网站是认识我的。当我访问一个网页就是发起了一次新的http请求我打开一部电影也一定是跳转到一个新的网页中。各种页面跳转的时候本质就是进行各种http请求这个网站照样认识我 但是http协议本身是一种无状态的协议它的无状态的含义就是今天我发起第一次http请求那么再发起第二次请求这两次请求对于http的客户端和服务器来讲它不知道曾经发起了第一次也不关心即将发起的第二次只关心当前次干了什么也就是http协议它并不记录我们发起这次http请求的上下文信息谁发的什么时候发的历史上有没有发过全部都不关心http只关心本次请求有没有成功对历史上的请求它不做任何的记录这就叫做无状态。 问题引入 目前。上述两个知识比较矛盾 1.经验告诉我们你今天进行各种页面跳转本质就是发起了各种http请求让我们得到网页的内容但是我们发现不管怎么跳转网站都是认识我们的我是不是VIP它一看就知道。 2.http协议本身是一种无状态的也就是说http协议它压根就不知道发起这次http请求的是谁历史上这个人有没有发起过历史上它发起的内容有哪些通通不关心每一次http请求就是从最原生的方式帮我们继续重新请求资源历史上的信息它从来不记录也就是说http请求在任何一次请求的时候都不知道是谁发起的请求只要告诉我你要访问谁就可以了。 eg你的男朋友容易失忆你今天和他一起玩了但是第二天他就不认识你的去了什么地方你是谁他都不认识因为你的男朋友不记录历史数据他只记录这次谁陪我去玩了。这就是无状态。 Cookie  对我们来讲http是不记录上下文的是无状态的那网站是如何认识我的呢 当我请求各种各样新的网页的时候有的视频就是需要VIP才能播放的我登录的时候只是在登录页面发起http请求当我再去访问新的VIP视频的时候它怎么能认识我呢 我们肯定是有新的技术保证客户始终在线网站认识我并不是http协议本身要解决的问题网站认识我和http无状态是两种层面上的东西让网站认识我http可以提供技术支持(但是我的http照样是无状态的)来保证网站具有“会话保持”的功能。说白了假设今天的网站有10000个网页http的角度这就是10000个网页每个网页都是独立的站在网站的视角就是我要知道谁都访问了哪些网页。 我们让网站认识我实际是一种cookie技术cookie主要是用来做“会话保持的”会话保持更高级的说法叫做会话管理。 会话与会话管理  会话我们登录xshell的时候输入账号密码xshell就认识我了其中我登录的时候就叫做一个建立会话的过程。 会话管理同样的我们登录一个网站的时候一旦登录成功网站记录你的个人信息让你在这个网站中可以进行各种你的权限范围之内的各种资源访问这就叫做会话管理。 http的核心功能主要是帮助我们解决网络资源获取的问题这些会话保持的功能本身http不给你彻底解决但是我可以给你提供技术支持会话保持就需要你自己解决。 eg:我当前的b站是处于登录状态的关闭网页后在打开还是登录状态但是我将它的cookie信息删除 再次刷新页面网站就不认识我了就需要我们再次登录了  刚刚的cookie就能决定网站是否认识我。 cookie 1.浏览器角度cookie是一个文件(这个文件在浏览器中)该文件里面保存的是我们的用户的私密信息。 2.http协议角度一旦该网站对应有cookie在发起任何请求的时候都会自动在request中携带该cookie信息 我们进行登录注册的时候服务器端得到用户名和密码后端经过认证登录成功。然后浏览器内部会形成一个cookie文件里面保存的就是username和password一旦登录成功说明这次输入的用户名和密码是合法的浏览器就把服务器认证成功的用户名和密码写到这个cookie文件中。后续的请求每一个请求的请求报头属性都会自动携带对应的cookie。 意思就是说你一旦第一次登录成功登录成功认证之后浏览器自动会把你登录成功的用户名密码写在一个cookie文件里后续所有的读写请求都会在自己的请求报头属性里都会自动携带上对应的cookie也就是它会把这个cookie里面的usernamepassword这样的字段携带上然后每一次都会发送给这个server所以server就可以每一次针对你访问的所有请求因为你自动就携带了用户名密码所以在后端没访问一个网页每对应一个网页都可以进行用户名密码的认证只有你通过了server在给你响应回你的请求如果认证不通过就告诉你这个视频是VIP要看到你看不了。或者你得先登录登录后才能看。浏览器会自动帮你在请求的报头属性里携带这个cookie。所以服务器在后续请求时就认识了你。以上是基本理解仅仅是为了理解实际上我们现在很少有情况是把用户名密码保存到cookie。 验证cookie Set-Cookie服务器向浏览器设置一个cookie。当我们一旦使用了这样包含Set-Cookie这样的选项这样的请求返回给浏览器时就是在指挥浏览器让浏览器帮我把Set-Cookie后面的内容写在你自己的cookie文件里从此往后你每次向我请求时都把这个信息带上。 Sock.hpp #pragma once#includeiostream #includestring #includecstring #includecstdlib #includesys/socket.h #includenetinet/in.h #includearpa/inet.h #includeunistd.husing namespace std; class Sock { public:static int Socket(){int sock socket(AF_INET, SOCK_STREAM, 0);if (sock 0){cerr socket error endl;exit(2); //直接终止进程}return sock;}static void Bind(int sock,uint16_t port){struct sockaddr_in local;local.sin_family AF_INET;local.sin_port htons(port);local.sin_addr.s_addr INADDR_ANY;if (bind(sock, (struct sockaddr *)local, sizeof(local)) 0){cerrbind error!endl;exit(3);}}static void Listen(int sock){if (listen(sock, 5) 0){cerr listen error ! endl;exit(4);}}static int Accept(int sock){struct sockaddr_in peer; //对端的信息socklen_t len sizeof(peer);int fd accept(sock, (struct sockaddr *)peer, len);if (fd 0){return fd;}return -1;}static void Connect(int sock, std::string ip, uint16_t port){struct sockaddr_in server;memset(server, 0, sizeof(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){cout Connect Success! endl;}else{cout Connect Failed! endl;exit(5);}} }; Http.cc  #includeSock.hpp #includepthread.h #includesys/types.h #includesys/stat.h #includeunistd.h #includefstream#define WWWROOT ./wwwroot/ #define HOME_PAGE index.html// 其中wwwroot就叫做web根目录wwwroot目录下放置的内容都叫做资源 // wwwroot目录下的index.html就叫做网站的首页 void Usage(std::string proc) {std::cout Usage: proc port std::endl; }void *HandlerHttpRequest(void *args) {int sock *(int *)args;delete (int *)args;pthread_detach(pthread_self());#define SIZE 1024*10char buffer[SIZE];memset(buffer, 0, sizeof(buffer));// 这种读法是不正确的只不过现在没有被暴露出来罢了ssize_t s recv(sock, buffer, sizeof(buffer), 0);if (s 0){buffer[s] 0;std::cout buffer; //查看http的请求格式std::string html_file WWWROOT; //访问的文件在这个路径下html_file HOME_PAGE;//接下来才是正文std::ifstream in(html_file);if(!in.is_open()){std::string http_response http/1.0 404 Not Found\n;http_response Content-Type:text/html;charsetutf8\n; http_response \n; //空行http_response htmlp你访问的资源不存在/phtml;send(sock, http_response.c_str(), http_response.size(), 0); //响应回去}else{struct stat st;stat(html_file.c_str(), st);//返回的时候不仅仅返回正文网页信息而是还要包括http的请求std::string http_response http/1.0 200 OK\n;// Content-Type 代表正文部分的数据类型今天我们响应回去的不是字符串而是把web服务器的根目录的// index.html响应回去。http_response Content-Type:text/html;charsetutf8\n;http_response Content-Length: ;http_response std::to_string(st.st_size);http_response \n;http_response \n; //空行std::string content; //正文内容std::string line; while(std::getline(in,line)) //按行读取 {content line; //正文就全部在content里面了。}http_response content;in.close();send(sock, http_response.c_str(), http_response.size(), 0); //响应回去}}close(sock);return nullptr; }int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock);for ( ; ; ){int sock Sock::Accept(listen_sock);if (sock 0){pthread_t pid;int *parm new int(sock);pthread_create(pid, nullptr, HandlerHttpRequest, parm);}} } 首先我们不加 Set-Cookie 我们看到cookie是空的没有任何的cookie信息 添加cookie 至此我们就添加了一个cookie 这个Set-Cookie就是我们在响应报头里添加的报头属性id...就是我们要设置进文件中的内容\n代表它是一行。 我们访问我们的服务器就看到了两个cookie  然后我们在多刷新几次这个网页。 我们发现第一次访问这个网页正常请求然后进行Set-Cookie然后浏览器就包含了cookie信息然后从第二次访问开始在进行刷新的时候我们就会发现请求中就会带上Cookie属性。以后每一次请求浏览器都会自动检查你访问的这个网站也叫作你访问的域只要这里有cookie都会自动给你把cookie携带上。 所以只要你访问的是目标的网站浏览器会自动把你曾经给这个目标网站对应的浏览器内部写入的cookie信息给你携带上作为请求的一部分。你想设置cookie我们的选项就是Set-Cookie: ... 你登录任何网站的时候只要你登录上了一定会有cookie正在使用如果你不想让他认识你了你把这个cookie信息移除掉再次刷新网页它就不认识你了如果你把这个cookie一直保留着此时对应的浏览器向任何目标网站发起对应请求时请求里面都会携带上Cookie字段然后支持服务后端对你的身份进行多次认证每一次都要进行认证。 类比生活当客户端第一次请求时服务端说给你个工牌你把它保存好以后你再出入我这个服务器的时候你都把这个工牌带上客户端说好的。再比如你去了腾讯当你入职第一天你的领导就给你一个工牌以后进公司就没有人拦住你了第一次请求的时候就相当于你入职的过程往后你每天上班带着你的工牌也就是每次发起请求服务端也就是保安总是认识你因为它发现你带着工牌它可以核实你的信息。这就是为什么网站认识我。 cookie文件的存在形式 它有两种存在形式 1.文件版如果是在文件中那么它就是在浏览器的安装目录下以及它使用的某些相关的用户级目录下他会把用户对应的cookie信息保存在文件里。即便你把电脑关了你再去访问目标网站这个cookie信息他还能知道。 2.内存版意思就是说浏览器一关闭访问的目标网站就不认识你了。 cookie的安全问题  我今天把登录把账号密码输入给服务器服务器认证通过后给我返回登录成功请求然后我本地就把账号密码这样的私密信息更新到cookie里面包括我的浏览痕迹等。目前最私密的就是账号密码。如果今天我不小心点击了恶意网站这些恶意网站今天向我的浏览器端注入了一些木马程序盗取了我的cookie文件然后别人把这个cookie文件放在他自己浏览器的特定目录下然后访问和我访问一样的网址此时因为有cookie信息了他就可以以我的身份访问目标网站了。所以这就是大部分人被盗取账号的底层重要的理论。 如果别人盗取我的cookie文件有两个安全问题 1.别人可以以我的身份进行认证访问特定的资源 2.cookie如果保存的是我的用户名和密码那么这个账号就直接泄露了。 单纯使用cookie是具有一定的安全隐患的。 比如你自己的账号经常被盗。当你在你的电脑上登录QQ 的时候QQ的服务器是如何知道你是登录状态它如何维持它的会话保持呢包括你只要登录上你的QQ我要访问QQ邮箱等一些其他功能基本上都是以我当前的这个账号就进行访问了也就是访问其他功能就不需要我再次输入账号密码了。QQ的好多功能都是相互打通的就是因为你在登录你的账号的时候它也会形成对应的cookie信息保存在它自己的服务当中。就比如你访问QQ空间然后就自动打开了浏览器浏览器为什么知道是你这个人的QQ空间而不是其他人的说明你在登录状态打开QQ空间的时候QQ的请求就自带cookie信息了说明cookie信息早就被保留了。这也说明当你在访问恶意网站的时候别人盗取了你的cookie信息别人就可以以你的身份访问你的QQ空间了甚至登录你的QQ然后就可以访问你的资源了。 别人盗取我们的cookie信息现在仍然存在信息泄露的问题也是永远解决不了的安全级别更高那么攻击手段就越强。所以才有了各种各样的杀毒软件对我们的软件进行保护我们的服务器也有自己的安全策略但是这样的问题依旧避免不了只要用cookie信息就有泄露的风险。 这两个问题最严重的是第二个问题我的身份认证信息泄露了其实也不影响比如你的QQ被盗了以你的身份行骗但是你的人际关系不好没有人借给你钱这样也算从另一个维度保护了账号安全。最严重的是个人私密信息泄露了用户名密码浏览痕迹都会被泄露这是很危险的我们要想办法把这些私密的信息保留起来的所以我们今天有了一个新的技术就叫做session。 现在市面上主流的使用方式是cookiesession。但是session是没法举例的因为session的实现是需要服务端有更多的实现方案的session的代码量太大了我们没办法实现。 session 核心思路将用户的私密信息保存在服务端。 为什么私密信息会被盗取呢 因为你是个普通小白作为普通的用户你的私密信息是所有的恶意分子想要的并且你的防护级别很低。所以有些人就喜欢安装些杀毒软件但是这些软件的级别也不够。因此衍生出session。 当你登录某个网站的时候一定携带用户名密码服务端首先就要先认证认证通过之后构建一个响应告诉你认证通过了客户端这里就可以通过服务端给的Set-Cookie命令然后把个人认证的信息保存起来。这是我们刚刚所说的。 session处理策略  现在服务端认证通过后在服务端的磁盘上(直接理解成linux的目录中)就形成一个session文件这个session文件比如说叫做123这个文件里面保存的就是该用户的私密信息用户名密码浏览痕迹...然后服务端进行构建http响应构建响应的时候需要设置Set-Cookiesession_id123。换言之依旧会给客户写回一个cookie值这个cookie值照样会正常的保存在浏览器的cookie文件中只不过这次保存的cookie文件里面只有一个数字叫做123我们把这个123称为当前用户的会话id(session id)。如果这个网站有100个人访问那么每一个人都要形成一个session文件这个文件的文件名必须具有唯一性所以这里的session id 在服务端一定是一个具有唯一性的值。也就是有100个人每一个人都会形成唯一的session文件每个人的session id都不一样这个是可以通过算法解决的比如文件名我们就可以把时间戳带上然后给每个人添加一个递增编号组和就形成了唯一值。此时响应回去照样写cookie只不过这次的cookie里面保存的就是用户对应的session id。所以后续所有的关于server的访问所有的http请求都会由浏览器自动携带cookie文件中内容(就是当前用户的session id)。当服务端搜到客户发来的session id 它对用户的身份认证就不在需要用户名密码了只需要确定session id 有没有在服务端中只要通过session id找到对应的文件就能找到用户的私密信息从而对用户做认证。后续server依旧可以做到认识client这也是一种会话保持的功能。换言之我们应该看到所有的客户端访问服务器上所有的请求包括看所有视频所有的网页只要你曾经登录过往后server端就可以根据你的session id来确认你的存在。 cookie安全的第一个问题 因为客户端的cookie里面不在保存用户任何的私密信息所以也就杜绝了即便是用户他将自己的cookie文件泄露了也不会导致用户的私人信息被泄露。但是我们还有cookie文件被泄露的风险这个问题是无法被杜绝的因为这个cookie文件是在用户的电脑上用户没有防护意识用户电脑上它被盗取几乎是必然的所以是没有办法解决的。这也就是为什么腾讯这么大的公司它的QQ照样还能被人盗取解决不了。 cookie安全的相关策略 cookie文件被泄露了别人拿着我的cookie文件去访问曾经这个cookie对应的网址别人就冒充我的身份了这个也是只能解决cookie安全的第二个问题cookie安全的第一个问题照样没法解决。 但是可以有一些衍生的防御方案了。比如你的QQ如果你跨越地区了你的QQ就会提示你当前QQ登录地点异常请确认是否是本人操作包括用一台新设备登录也是如此...这是因为ip地址是可以确认出地域的比如:最近的好多软件如果你评论的话是会显示你的对应的地址的这就是通过ip的归属处理的不同种类的ip隶属不同的片区所以通过ip就能确认地区。每次登录QQ就对你的ip做认证如果你的QQ被盗了假如你是山西的盗取你QQ号的人是缅甸的那么1分钟前你还在山西1分钟后你就在缅甸了此时QQ就立马识别到用户异常然后让你重新登录让你重新登录的本质是废弃掉刚刚的session id重新给你形成新的session id。所以一旦重新登录了对端的session id也就失效了。 再比如如果我是一个不法分子我盗取了你的账号以你的身份访问了某些网站我最想做的事情不是立马试试诈骗我最想做的就是把你这个人的用户名和密码改掉让这个账号永远是我的了但是这种情况是不可能存在的因为诞生了一个新的设备--手机所以以前没手机的时候我们用的是邮箱认证现在有手机了如果你要改密码第一件事情永远是输入旧密码第二件事情如果你的登录地址有异常或者它的审核标准比较严还需要你进行短信认证。也就是数据层面上你把cookie文件丢了但是手机没丢即便是手机也丢了手机和cookie都被同一个人拿走了你手机也有密码。短信上面的认证就相当于即便你的信息被盗取了他要改你密码他也改不了这也就是一般你的账号被泄露或者盗取我们经常说的申诉就是重新认证你因为是账号的拥有者当初绑定的是你的手机号所以最后可以通过手机进行二次认证使别人盗取到的cookie信息失效就可以了。所以只要session id是server去指派的它的session id管理工作是由server去做的虽然你的客户端保留了一个session id但是server随时可以让这个session id失效让别人盗取也没有意义所以就可以有各种各样的策略比如异地登录短信认证包括QQ检查内容一旦发现有些账号有异常行为比如频繁添加好友...侦测之后就可以强制用户下线强制用户下线也很简单只要在服务端把对应的session 文件干掉。这就是cookie与session。 再谈http无状态 再来看看最开始的问题网站是认识我的但是http是一种无状态的协议也就是你请求什么网页请求前请求后和我没关系http只是你告诉我要什么我给你拿什么哪怕是上次刚要过这次还要要我也会给你拿这就是http的无状态它不记录用户的任何行为但是网站是需要认识这个用户的。 为什么网站需要认证用户也就是为什么登录这个网站的时候它需要永久的认识用户呢 因为http无状态所以今天你登录成功了如果我的网站不认识你你只在访问该页面的时候它认识你但是我一旦点击一个新的页面查看一个新的视频那么对不起你要先输入用户名和密码你才能看你要手动进行一次认证换言之引入cookiesession本质就是为了提高用户访问网站或者平台的体验你只要登录我的网站所有内容你都可以随便访问当然增强用户体验就是上下文要记录用户的状态而http是无状态的所有就有了cookie和session来解决这个问题。 HTTPS http的信息在互联网中传送基本就是数据在互联网中裸奔别人想在随时随地想抓取你的数据就能抓取。局域网通信本质上就是你发收到数据和你在同一个局域网的所有人都能够看到只不过别人不处理罢了所以局域网通信本质就相当于有两个人在自认为双方在通信实际上有一大批吃瓜群众在 围观如果你不加密数据本来就是在裸奔这也就是不管用POST还是GET方法都解决不了的我们只能对网络数据进行加密。自从中国互联网因为以前的互联网巨头出现了很严重的安全隐患我们国家的互联网从安全领域就变的越来越重视了现在你访问的所有网站都是HTTPS eg:你现在能叫上名字的所有网站全部是HTTPS 像我们自己搭建的浏览器就提示是不安全的 背景认识一 https其实是httpTLS/SSL。TSL/SSL简单的理解成http数据的加密解密层这一层也是软件层。 我们使用http然后直接使用系统调用就叫做http如果我们使用http然后向下访问时不直接访问系统调用接口而是访问一些安全相关的接口然后再用安全访问的相关层在访问系统调用接口在把数据发出去因为http的请求和响应都要经过这个加密解密层所以请求时完成了加密响应时完成了解密这个协议就叫做HTTPS。因为这里的TLS/SSL是属于应用层协议也就是说它只会在客户端和server端两端出现换言之也就意味着我们的数据在网络中总是被加密的对于下三层数据是没有被加密的也不需要你加密主要是为了保护用户的隐私所以只要把应用层数据加密就可以了到了对端对端向上贯穿的时候也会自动进行解密所以同样的在http依旧是请求和响应就是加了一层软件层。 实际上在网络里是整个http请求有效载荷被全部加密了当然有些版本只对有效载荷加密因为http本身就是一些私密信息但只要服务器和客户端双方认识就可以其实http的报头也可能包含用户私密信息(egsession id)一般是对整个http进行加密加密后交付给下层去传输然后到对端解密出来之后就是http相当于在同层看来依旧正常通信没有加密解密的过程。 背景认识二(数据的加密方式) 1.对称加密 这里有个秘钥的概念这个秘钥只有一个比如说秘钥是X所谓的对称秘钥就是用X加密也要用X解密。就像你家里的门钥匙锁门你用这个钥匙开门也用这个钥匙。 假设现在有个数据data data 异或 X result X 异或 data result 你要发的数据是12345实际发的是12644服务端收到后进行解密就是12345。所以曾经学的异或运算让不同数字异或同一个值src_key这样的src_key就称之为对称秘钥。  对称加密就是使用同一把秘钥进行加密解密。异或其实就是一种简单的加密算法。 2.非对称加密 有一对秘钥分别叫做公钥和私钥。可以用公钥加密但是只能用私钥解密如果用私钥加密只能用公钥解密。你必须用一个加密另一个解密这就叫做非对称加密最典型的非对称加密算法常见的就是RSA。 一般而言公钥是全世界公开的私钥是必须自己进行私有保存的也就是私钥不能暴露给外部你必须自己保存起来。 背景认识三 假设现在有一篇论文那么如何防止文本中的内容被篡改以及识别到是否被篡改 我现在需要有一个方法来甄别是否有人改过这篇论文哪怕是改过一个标点都不行。我们这篇文本的文本量是可大可小的我们可以针对该文本进行Hash散列形成固定长度唯一的字符序列。这种算法的特点就是对文本进行任何改变哪怕是一个标点符号都会形成一个差异非常大的hash结果也就是说我选择的hash散列算法如果文本有一点点不一样那么形成的字符序列的变化就特别大。最典型的这样一个算法时md5。所以我们把这个固定长度唯一的字符序列我们称之为数据摘要或者叫做数据指纹。然后我们再采用加密算法(一般是非对称的)我们将这个固定长度唯一的字符序列在进行加密得到加密结果这个加密结果我们一般把它叫做数字签名。 我是一个通信端我现在要发送这段文本我怎么保证这段文本没有被篡改 我就可以在发送的文本当中把原始文本带上另外在文本的尾部带上该文本的数据签名最后在我们在网络里就把它俩作为一个整体发送出去当接受端收到数据就要确认这个文本是否被篡改。 校验 1.从接收到的数据中把原始文本拿出来紧接着对原始文本采用相同的hash散列对于这段文本重新形成数据摘要。 2.把数据签名拿出来根据解密算法把这个数据签名解密出数据签名对应的数据摘要。 然后对比两份数据摘要如果相等说明没有被篡改如果不同说明被改掉了 https是如何通信的呢 首先我们进行通信双方的数据是必须得被加密的。既然加密也必须解密。 如何选择加密算法 1.对称加密 2.非对称加密。 如果我们选择对称加密假如客户端用X秘钥进行加密那么server端怎样得知这里要用X秘钥解密呢反过来也一样如果server端给客户端发消息server端用X秘钥加密那么客户端如何得知X呢。 方案一 预装。也就是我们给服务端和客户端把世界上所有采用的对称加密的秘钥信息都给他俩预装好在你开始买机器的时候天然就有了这样的话双方就可以直接用了。 但是这样有几个问题 预装的成本太高了。        如果我没有预装就得进行下载我下载的时候就要把一些软件及秘钥的相关信息全部下载下来还是在网络上跑。既然这个信息已经被预装了那么代表别人也能预装如果你的秘钥是明文那么别人也就知道了如果你的秘钥是暗文那么对这个暗文也需要进行解密然后就有非常大的鸡生蛋蛋生鸡的问题。所以这个预装是非常的不靠谱的是不行的。 方案二 对称加密  双方通信的时候协商秘钥。这种方案看起来可行假设服务器现在并不知道秘钥是什么客户端形成了一个秘钥X然后因为是对称加密所以它发过去的数据加密是需要被server知道的所以它需要把自己的X交给https所以就进行通信但是第一次决定是没有加密的。就是说客户端告诉服务端我要用X进行加密我把X先给你你一会用X进行解密这里就特别搞笑因为第一次传送第一条数据的时候服务端还没有收到X所以客户端不能进行加密因为客户端一加密了对方就不知道了所以客户端的第一条消息必须是把秘钥以明文的方式发送过去这里就很尴尬我把秘钥以明文发送过去如果这个秘钥信息被盗取了后续双方进行加密通信就没有任何意义了。说明在刚开始通信时协商秘钥阶段采用对称加密的方式是根本不可能的。 秘钥协商采用对称方式是绝对不可能的  非对称加密  我们采用非对称方式加密我们用S表示公钥S^表示私钥。 当客户端请求服务端的时候服务端首先进行秘钥协商服务端把他的公钥S给客户端此时客户端就拿到了一个公钥S紧接着客户端拿到了公钥S之后用公钥S对数据进行加密此时客户端在把加密号的数据发送给服务端所以客户端发出去的数据是用服务端提供的公钥S进行加密的。因为这个世界上只有server具有私钥S^也就只有server能进行解密如果其他人拿到了这个数据也是没办法的进行解密的因为其他人没有私钥S^。此时服务端收到加密数据根据私钥S^解密出data就拿到了数据。换句话说就是server端把公钥暴露给了全世界全世界的客户端都能拿到公钥但是任何人拿到公钥一旦加密那么除了服务端没有任何人可以解密。所以经过这样的方案我们就能保证数据从客端传输到服务端的安全。但是从服务端到客户端是不能用私钥S^加密的因为一旦用私钥S^加密那么只能用公钥S解密可是全世界都知道公钥S所以从服务端发送给客户端的数据就是不安全的。也就意味着如果只要一对公钥和私钥只能保证单向数据安全。 那么我们给客户端一对公钥和私钥给服务端一对公钥和私钥在通信阶段提前交换双方的公钥就能保证双向数据安全。 既然一对非对称秘钥可以保证数据的单向安全那么两对就可以保证数据的双向安全了。 两对非对称秘钥的问题  但是事实并非如此 依旧有被非法窃取的风险暂时先不谈。非对称加密算法特别费时间就是因为这一点这种方案就几乎已经不被采纳了。对称加密是比较省时间的。所以在我们实际进行http通信的时候我们根本就不是采用纯对称或者纯非对称纯对称有安全隐患纯非对称有效率问题。 实际中的加密方法 实际中采用的是非对称对称方案 当客户端发来一个请求服务端是有自己的非对称的公钥S和非对称的私钥S^的当客户端一旦分发起请求服务端给客户端进行响应的时候服务端就将自己的公钥给客户端客户端就收到了公钥S接下来客户端形成对称秘钥的私钥X然后客户端用公钥S对私钥X进行加密形成X然后客户端把经过加密的对称秘钥的私钥交给服务端这个世界只有服务端有S^所以只有服务端能用S^对X进行解密然后就得到了X所以server就以安全的方式拿到了客户端发来的对称秘钥的私钥X然后这个阶段完成后因为客户端和服务端都知道了对称加密的X所以他俩就采用对称方案进行数据的加密和解密。 将对称秘钥发送个对方叫做秘钥协商阶段采用非对称算法。 利用对称秘钥传输数据叫做数据通信阶段采用对称加密。 什么叫做安全 不是让别人拿不到就叫做安全而是别人拿到了也没办法处理。只要别人有一点点可能性能侦测到你的数据你就要把这种可能性无限放大这就是安全意识小小的漏洞都要认为它是一个普遍问题。你的数据是随时随地都能被别人抓到的。只要你的数据在网络里跑别人据一定能拿到。现在的问题是数据加密了能解开就是不安全的解不开就是安全的 。 在安全层面解密的成本远远超过了解密后带来的收益就是安全的。 比如别人出价10块让我破解你的信息但是我破解你的信息要花费10000如果我不考虑成本我一定能破解但此时考虑了成本我是不会进行破解的此时就称你的数据是安全的因为我破解了是没有任何意义的。这是以经济角度谈的。如果信息涉及到了国家安全那么它就不是钱能衡量的那么这个时候它的安全级别必须是特别特别强的你要攻克我可能要用一些超级计算机得几百上千年才能运算出来这也就是为什么我们国家搞的一种量子计算机国外非常害怕因为量子计算的运算能力是比现在的二进制计算机的效率高了几万几十万甚至几百万倍所以本来破解一个密码需要1个月现在就可能只需要几秒钟一瞬间就把你建立好的安全全部破解了。 中间人  客户端和服务端之间的数据被中间的某些用户查看阅读甚至篡改这种攻击手法我们称之为中间人。 那么在服务端把公钥给客户端的时候可不可能出现问题呢 在网络环节中随时都有可能存在中间人来偷窥修改我么的数据。服务端在给我们客户端发送自己的公钥S(这也是一段报文或者是一段数据)假设此时出现了个中间人(一台监听设备)这个设备内部也有自己的公钥M私钥M^服务端发送的公钥S是大家随时随地都可以获取的只不过这个中间人做了一个工作它将服务端发送个客户端携带公钥S的报文拿到然后把自己的公钥M替换了服务端的公钥S然后把包含公钥M的这个报文发送给了客户端此时客户端并不知道中间人把服务端发给自己的报文篡改了所以客户端就认为自己收到了一个公钥M然后形成自己的对称加密的私钥X将X通过M进行加密形成M然后客户端就把M发送给服务端可是中间人又把这个M截取到了因为你用的是中间人的公钥M所以中间人就用私钥M^进行解密然后就拿到了客户端和服务端进行通信的对 称加密的私钥X。然后中间人就把这个私钥X保存在自己的系统里然后把解密出的数据私钥X重新用公钥S加密形成S然后把这个S在交给服务端此时服务端和客户端都认为自己交换成功了自己的秘钥可此时中间人也拿到了服务端和客户端通信对称加密的私钥X这样中间人就顺利成章的得到了双方通信的数据。 本质问题客户端无法判定发来的秘钥协商报文是不是从合法的服务方发来的 证书 所以这种方案目前就无解了别人想怎么搞你就怎么搞你所以当人们意识到这种攻击收发会让很多人无所适从所以我们的网络中就出现了一种非常重要的机构CA证书机构。 证书是什么 在生活中我是用人方你怎么证明你是一个大学生呢我们就可以通过学校颁发的学位证进行证明为什么信任各大高校呢因为各大高校本身就有国家教育部在后面支持所以只要一个服务商经过权威机构认证该服务商就是合法的。 比如这个服务端是一个正规的公司的网站这个服务商首先向CA机构申请证书申请证书需要提供企业的基本信息域名公钥。CA机构拿到你的申请给你进行创建证书。 CA机构 1.很权威  2.CA机构有自己的公钥A和私钥A^。 ps:公钥和私钥只是一种算法任何人都可以有一点也不值钱。 创建证书 创建证书的时候要有企业的基本信息{域名公钥(这个公钥与CA机构没关系是服务器发送给客户端的公钥)}这个企业的基本信息就是一段文本。所以我们根据企业信息形成一个数字签名。 回顾下数字签名形成这个企业文本经过hash散列形成数据摘要然后用CA自己的私钥加密形成该公司的数字签名。 我们把公司基本信息公司信息的数字签名统称为CA的证书。然后再把这个证书颁发给企业这个证书里面就包括了公司的基本内容域名公钥公司基本内容的数字签名。 创建证书  CA机构流程 有了证书后请求该怎么做呢 客户端发起一个请求服务端就要把秘钥返回给客户端了以前就是直接把公钥返回给它现在就是把证书信息返回给客户端假如现在中间人就截取了你的证书了但是现在中间人就不能将服务端的公钥替换成自己的公钥了因为域名公钥基本信息都是明文传送所有人都能看到但是人家带了数字签名。当客户端收到证书时它会把文本内容和签名内容拆出来然后使用相同的散列算法(这个算法也可以体现在证书上)对该文本进行形成摘要如果中间人对公钥进行篡改那么形成的摘要就不是服务端发过来的而是新的中间人篡改后的摘要 这样就和解密后的数字签名对不上了。 数字签名中间人改不了吗 是的中间人是改不了数字签名的因为数字签名我们用的是CA机构的私钥进行加密的(当然任何一个人都可以用CA的公钥解密) 中间人是没有CA机构的私钥的CA机构的私钥只有CA机构知道也就是只有CA机构能重新形成对应的数字签名。所以中间人解密出来后因为没有私钥也就没办法进行重新生成数字摘要。          如果中间人也是一个合法的服务方呢 我这个中间人我也向CA机构申请CA机构也给我颁发证书身为中间人的我也就有了合法的证书我直接把服务端的整个证书报文全部全替换掉用成中间方的这样行不行呢 答案也是不行的因为证书的基本信息里有一个域名如果是合法的中间人你的域名一定和客户端请求的域名一定是不一样的正如中间人不能改你的域名不能进行任何篡改一旦改了就会被侦测出来。换言之中间人将证书全部替换后到了客户端客户端进行秘钥解密的时候发现一些列都对但是客户端我原本请求的域名是www.123.com但现在怎么变成了www.zhongjianren.com了目标地址发生变化了我客户端就不相信它了。 所以无论中间人怎么改这个证书它都改不了 我们认证一个证书是对原始内容进行散列因为原始内容和散列算法本来就是公开的我们直接用就可以关键是对数字摘要解密。所以要求客户端必须知道CA机构的公钥信息。 客户端是如何知道CA机构的公钥信息呢 1.一般是内置的相当于你在下载浏览器的时候这些浏览器本身就已经内置了很多的公钥相关的信息。 2.访问网址的时候浏览器可能会提示用户进行安装。但这种出现的后果就需要用户自己承担。大部分情况下我们见到的很多网站很少提示你让你重新安装证书的。99%情况下直接访问网站除非它是不合法的。 我们主流的方案是第一种你的windows操作系统和浏览器都内置了很多的CA机构这个是软件发布的时候厂商就内置好了默认下载就有了涵盖的就是CA机构相关的公钥信息。有了公钥就可以对我们对证书的合法性是否被篡改做些对应的判定。 查看证书 受信任的根证书颁发机构这就是默认设置好的。中间证书颁发机构就相当于受信任的根证书颁发机构 信任 中间证书颁发机构因为我们信任根证书机构只要根证书颁发机构信任的我们也信任。 传输层 再谈端口号 端口号(Port)标识了一个主机上进行通信的不同的应用程序; 比如主机A在通信的时候它的服务器上可能部署了大量的服务我们的HTTP默认绑定的端口是80这个端口不能改变这是服务端口必须是众所周知的。HTTPS默认的端口是443。底层收到的数据它的报文中会通过ip来标定是哪台机器但这台机器上有众多的服务我们就根据端口号进行交付。所以套接字通信的本质其实是进程间通信ip标识唯一的主机端口标识该主机唯一的一个进程。  在TCP/IP协议中, 用 源IP(标识某台主机), 源端口号(标识主机上某个特定的服务), 目的IP, 目的端口号, 协议号 这样一个五元组来标识一个通信(可以通过netstat -n查看)。源IP, 源端口号, 目的IP, 目的端口号就是一对套接字标识互联网中唯一一对进程。实际上在网络通信中协议号没有任何的意义因为协议号就是端口号。比如你是HTTP你的端口号是80就完了我不管你的协议号是多少。eg:端口号范围划分  0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的. 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.认识知名端口号(Well-Know Port Number) 有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号: ssh服务器, 使用22端口 ftp服务器, 使用21端口 telnet服务器, 使用23端口 http服务器, 使用80端口 https服务器, 使用443 执行下面的命令, 可以看到知名端口号 cat /etc/services 我们自己写一个程序使用端口号时, 要避开这些知名端口号 两个问题 1. 一个进程是否可以bind多个端口号? 可以 2. 一个端口号是否可以被多个进程bind? 不能 pidof 在查看服务器的进程id时非常方便. 语法pidof [进程名] 功能通过进程名, 查看进程id netstat netstat是一个用来查看网络状态的重要工具. 语法netstat [选项] 功能查看网络状态 常用选项 n 拒绝显示别名能显示数字的全部转化成数字 l 仅列出有在 Listen (监听) 的服物状态 查普通状态的套接字不带l  p 显示建立相关链接的程序名 t (tcp)仅显示tcp相关选项 u (udp)仅显示udp相关选项 a (all)显示所有选项默认不显示LISTEN相关我们最常用的还是 netstat -nltpUDP协议 UDP协议端格式 应用层用的就是传输层的接口。传输层最简单的协议就是TCP和UDP。TCP和UDP一定是对上提供对应接口的东西让应用层可以直接调用。 UDP的报文是这样的 UDP的报文结构的宽度是0-3116位的源端口16位的目的端口代表的是上层的应用程序它的源端口是什么到了对端之后它的目的端口又是什么双方在通信时源端口和目的端口就表明了我这个报文是上层的应用程序哪一个程序发的以及要发到哪一个程序当中。然后还包括一个16位的UDP长度16位UDP长度指的是整个报文的长度而UDP的报头长度是定长的(8字节)。 对于16位UDP长度的理解 16位的源端口16位的目的端口16位UDP长度16位UDP校验分别都是2个字节分别是UDP报头的4个字段他们组成UDP的报头一共8个字节。 这里的16位UDP长度代表的是UDP整个报文的长度一共是2^1665536字节也就是64k。描述16位UDP长度的这个字段占2字节也就是说以后在填写UDP报头的时候你可以在这里填写一个0~65536的一个数字单位是字节。  UDP如何做到封装和解包的 UDP的封装就是添加上定长的报头当它要解包本质就是将自己的报头和有效载荷做分离所以我们读取UDP定长的报头剩下的就是有效载荷。 UDP如何做到向上交付(分用问题) a.报头和有效载荷分离 b.根据目的端口号交付有效载荷给上层应用 UDP的报文有一个16位的目的端口号代表的是当这个报文被目标主机收到以后会根据16位的目的端口号交付给应用层对应的进程其中会把数据交付给上层程序。 我们写代码的时候为什么需要绑定端口号 就是因为当底层收到了对应的UDP报文它会根据报文的目的端口号把数据转给特定的绑定目的端口号的进程。 端口号为什么是16位 我们之前UDP/TCP 套接字端口号一直是uint_t 16 因为这是协议规定的。 Linux内核是C语言写的请问如何看待udp报头 所谓的报头就是一个结构体 struct udp_hdr{unin32_t src_port:16;unin32_t dst_port:16;unin32_t total:16;unin32_t check:16; } //:后面的数字用来限定成员变量占用的位数。 所以经常说的给udp报文添加一个报头就是拿着结构体定义一个对象把这个对象的数据一填写然后和上层的数据一拷贝形成一个报文然后就可以发了。 UDP的特点 UDP传输的过程类似于寄信. 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;面向数据报: 不能够灵活的控制读写数据的次数和数量;面向数据报 UDP本身报文大小并不大而且它是一个报文一旦整个报文被你全部收到了因为UDP是在底层协议是属于传输层的数据在读取时是应用层在调用系统调用接口把数据读上来。当应用层在读数据的时候整个UDP可能有多个报文而作为应用层要么就不读要读就把完整的一个报文全部读上去。换句话说就是我们之前写的UDP套接字客户端send多少次服务器就必须recv多少次。客户端send的每一个报文服务器在收的时候必须全部收到要么干脆就不收要么收就要全部收到。这种特点就叫做面向数据报这样的UDP报文就是原样发原样收既不拆分也不合并。这个就叫做面向数据报。 应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并; eg:用UDP传输100个字节的数据: 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节; UDP的缓冲区 系统调用接口就是曾经的创建套接字绑定监听等这样的一批接口我们以read/recvwrite/send为例这样的接口参数里面都会涵盖一些缓冲区和缓冲区大小以及文件描述符。 我们以前认为它的数据是直接发送到网络中然后由网络发送到对端主机但这种认识是不全面的这种接口文件角度叫做读写网络角度叫做收发与其说是收发函数不如说是拷贝函数 最终我们的应用层无论是http还是https还是曾经TCP/UDP传字符串的那些基本代码本质上是要将自己发送的数据拷贝到TCP/UDP对应的缓冲区里面TCP具有接受缓冲区和发送缓冲区当你调用read的时候其实你并不是把数据从网络里读上来而是你把数据直接从传输层的TCP的接受缓冲区里拷贝到用户空间当你写的时候其实并不是你把你的数据直接发出去而是把你的数据拷贝到对应的发送缓冲区当中。拷贝完成之后具体该数据什么时候发发多少完全由OS(传输层)控制。 传输层主要解决的就是什么时候发发多少已经可能有协议会解决如果发送失败了会怎么班的问题。说白了传输层更多的给我们提供传输数据的策略UDP提供的策略就是越简单越好。对我们来讲传输层提供的一些传输策略对我们后续保证可靠性各种流量控制滑动窗口这些机制全部会在这一层实现具体点就是在TCP中而UDP几乎没策略有数据直接发。 因为应用层的各种读写接口所以我们就不得不谈缓冲区的问题这个缓冲区存在的价值一方面要能够让传输层能够定制很多发送数据的策略另外一方面它将应用层协议和下层通信细节进行了解耦应用层只需要把数据拷贝个传输层因为传输层属于协议栈协议栈在OS启动就在内存中说白了这块就是将数据从内存中拷贝到内存中效率特别高接下来的数据怎么发是要经网络的说白了就是经过网卡网线去长距离传送的它比较费时间一些所以我们只要正常拷贝具体数据我们拷贝到之后上层立马返回就可以直接进行后续处理了发送的细节就继续由OS帮我们进行把数据发送出去。  UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;换言之只要你的报文交给了OSOS直接就发了所以UDP没有发送缓冲区。UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;也就是说网络把数据读到它会把数据自底向上的交付给UDP如果上层还没有调用read/recv接口UDP会把数据暂存到它自己的缓冲区当中上层读的时候直接从缓冲区里读上来即可。UDP的socket既能读, 也能写, 这个概念叫做 全双工。 UDP全双工 什么叫做一个协议通信时全双工呢 TCP和UDP都是全双工的。 eg:老师上课老师在说学生在听这个就叫做半双工是老师单向的给学生输出然后将来学生和老师聊学生说话给老师的时候老师也就不说话了就等学生说完这就叫做学生给老师输出我们都叫做半双工。我们两个在聊天的时候我说我的我说完了该你说你说完了该我说这个是一种交叉式的工作方式是一种半双工的方式。意思就是说我们两个在正常通信的时候我们两个都可以进行发送消息但是一个发的时候另外一个人就不能在发了我们两个就得等一等彼此这个就叫做半双工。 进程间通信的管道就是最经典的半双工通信因为它只能单向通信。  全双工就有点像两个人在进行吵架你说你的我说我的我再说的时候你也在说甚至你在说的时候我也在说我也在听相当于我们两个在同一个信道当中我们两个可以同时收发这就叫做全双工。 所以UDP中既可以recvfrom又可以sendto可以被同时调用如果我有两个线程一个专门从文件描述符中读一个专门向文件描述符中写这个我们就可以理解成是一种全双工的工作方式。 UDP使用注意事项 我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部). 然而64K在当今的互联网环境下, 是一个非常小的数字. 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装; 基于UDP的应用层协议 NFS: 网络文件系统TFTP: 简单文件传输协议DHCP: 动态主机配置协议比如说你自己接入到你家的WiFi的时候你的手机自动的会获取一个ip地址这个ip地址的获取其实是你的路由器支持DHCP协议它能够自动给入网的主机分配ip地址 BOOTP: 启动协议(用于无盘设备启动)DNS: 域名解析协议当然, 也包括你自己写UDP程序时自定义的应用层协议;  TCP协议  TCP全称为 传输控制协议(Transmission Control Protocol). 人如其名, 要对数据的传输进行一个详细的控制。 在生活中我们做事情无非就是两种事情 1.做决策          2.做执行 eg:公司中做决策的就是老板做执行的就是员工。 TCP会定制各种决策策略我们真正做数据通信的是下两层帮我们去做执行传输层更多的是定一些策略当然策略要被执行也一定要和下层关联起来就好比你的老板要做一件事情就要和手底下的员工进行沟通。 TCP协议段格式  TCP的标准报头长度是20字节一行就是4个字节有5行。TCP也可以携带一些选项这里我们不谈选项我们说的是TCP的标长报头。无论是封装解包还是向上交付我们首先都要将它的报头和有效载荷进行分离。 TCP的标准长度是20个字节。 4位首部长度是4个比特位对应的2进制范围也就是[0000~1111]。这里的首部长度是以4字节为单位的。eg:如果首部长度是1那么报头就应该是1*44如果首部长度是10那么报头长度是10*440字节。所以首部长度不能只看字面值还要乘上基本单位。所以TCP报头最大长度就是1111就是15*460字节。又因为标准报头是20个字节所以选项最多40字节。假设4位首部长度描述的长度是len, len*420所以len就是5所以默认情况下在TCP的4位首部长度中这个字段一般被填充的基本都是0101(5转2进制就是0101)。所以当我们读到一个完整的TCP报文我提取到它的前20个字节从20个字节中在分析出报文长度确定清楚它的报文确实是20个字节然后就把前20个字节拿走了剩下的就是有效载荷。所以我们能够让报头和有效载荷进行分离。6位标志位: URG: 紧急指针是否有效 ACK: 确认号是否有效 PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走 RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段 SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段 FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段 16位窗口大小: 后面再说 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分. 16位紧急指针: 标识哪部分数据是紧急数据; 40字节头部选项: 暂时忽略; TCP报头里有一个4位首部长度能够做到将报头和有效载荷进行分离只要分离了报头还包含源端口号和目的端口号通过目的端口号就可以做到向上交付。 任何协议都要回答两个经典的问题 TCP如何做到封装和解包的 完整报文就是报头有效载荷报头的长度我已经知道了把报头的长度去掉就是数据所以我们可以做到对报文进行解包和封装封装就是添加报头解包就是去掉报头。 TCP如何做到向上交付的(分用问题) 分用就是将有效载荷交付给上层通过TCP的目的端口号就能做到。 总结通过4位首部长度做到解包和封装通过16位目的端口号做到向上交付。16位源端口目的端口表明你的报文从哪个进程来的要发送到哪个进程当中。 确认应答(ACK)机制 TCP叫做保证可靠性 要理解TCP的可靠性必须理解TCP可靠性中最核心的机制基于序号确认应答机制 eg:常年上网课的我们当老师说某某某知识大家听懂了吗当老师说出去这句话以后能不能保证这句话被每个相隔千里之外的学生听到了呢 并不能因为大家经过长距离传输就不得不面对一个问题这个长距离传输的报文在路上丢了怎么办错了怎么办其中对方与我相隔千里之外我是得不到任何反馈的也就是说老师在说这个话的时候并不能确定在屏幕前的同学已经收到了这个消息。 可靠性作为发送方我想知道的是我发出去的数据你有没有收到我把话说了一大堆我说完了可是我并不能确认我说的话你听到了什么时候可以证明老师说的话被学生听明白了呢 就是我们平常熟悉的扣1政策“同学们听懂扣1”当学生给老师反馈的时候(扣1)此时是学生给老师发消息同样的学生也面临着同样的问题学生给老师发的消息也并不能确认老师是否收到了但是老师一收到反馈就能立马意识到他刚刚说的话同学听懂了。          client与server通信时client给server发送消息在server没任何反馈的情况下client是没有办法确认这条报文是否被对方收到了有可能这个报文丢了有可能server的反馈丢了...但是如果server给了client一个响应对于client讲最大的意义不在于自己收到了一个响应而在于client终于确认了他刚刚发给server的消息已经被对方收到了。同样的对应server它也无法确定刚刚的响应是否被client收到了所以client也应给server一个响应这个响应对应server的意义就是确认了自己发给client的报文被对方收到了。 所以确认应答机制是通过应答来保证上一条信息被对方100%收到了但是在双方通信的时候总会遇到最新的一条消息是没有任何应答的最新的消息没有应答我们就无法保证整个通信彻底是可靠的所以TCP不是100%可靠的但是只要一条消息有应答我们就能确认该消息被对方100%收到了。 TCP常规可靠性-确认应答的工作方式 client端在和server端正常通信时client进行发送数据但client无法确定被server收到了因为TCP需要确认应答所以server需要对这个消息进行确认(目前没有携带任何其他数据就是单纯的确认)server发出的确认也不能保证被client收到了但是当client收到了确认client就知道了向server发送的这个数据被对方已经收到了。 如果单向的发client只向server发消息server不断进行确认只要是client收到了确认就能保证自己刚刚发的消息被对方收到了就能保证数据从client到server端的可靠性意思就是说今天就是client向server发消息server对client发送的每一条消息都进行应答虽然server无法确定应答是否被client收到了但是server也不关心只要把确认应答发出去就可以了对与client只要发现我收到确认了我就能100%确定我发的消息被server收到了。反过来server给client发消息做的工作也是一样的。 无论是client给server发消息还是server给client发消息我们只要能保证每一个方向发出去的消息都有对应确认我们就能保证发送的数据被对方可靠的收到了。所以我们能保证client收到确认就能保证它发的数据被server100%收到能保证client到server方向的可靠反过来server到client的可靠性我们也能保证。 client和server在进行常规的数据报文交换通信的时候只要我发消息你必须给我么这个消息发确认只要你给我确认我就能保证这个消息是可靠的被你收到了如果这个确认丢了我就收不到我就认为这个数据丢了。所以可靠性不仅要判断对方100%收到也要判断对方没收到。所以基于确认应答机制双方给给对方都做应答的时候我们就能保证历史数据能被对方可靠的收到可靠性就是100%的。 TCP的其他策略都是基于确认应答基础之上构建出来的。网络里不存在100%可靠的数据通信总有最新的报文没确认应答。但是在单方向角度我发出消息如果我收到应答了我就认为我的上一条消息对方已经收到了我没有收到应答我就认为我的上一条消息丢了。换言之TCP的可靠性体现的是对历史数据的可靠性对于当前最新数据不关心。 确认应答 我们发送的数据在TCP叫做数据段在IP叫做数据报在Mac叫做数据帧今天我们认为发送的都是报文。 如果client发出去了一批报文假设这批有5个报文。只要client收到了各个报文的应答就能知道数据被服务端可靠的收到了。如果发送报文的顺序是1,2,3,4,5接收方收到的顺序一定是1,2,3,4,5,吗 被服务端收到了并不意味着被服务端按顺序收到了。如果我发的这一批报文对方给我都做响应我可能发的是1,2,3,4,5但是在做网络传送的时候有可能1号报文在路由转发的时候它选择的路径比较长2号报文比较快3号报文网络环境差总之网络的环境很复杂所以此时就存在一个问题server端收到的报文是一个乱序的报文。所以client发送数据的顺序是12345但是对方收的时候变成了54321全部乱套了。乱序的数据问题是挺严重的因为TCP要保证可靠性除了要保证被对方收到也要保证按序到达你必须保证client发的是12345服务器收的也必须是12345不能乱序乱序就出问题了一旦乱序就导致业务逻辑出现紊乱所以按序到达也是需要做到的点。 如何保证按序到达呢 TCP报头里面就涵盖了一个叫做32位的序号我们都有序号序号只要编好了到时候被对方收到的时候我们只要按照序号进行升序排序我们就能保证数据全部被对方收到的同时然后保证它按序到达所以32位序号的作用保证按序到达。 如何确认信息和发送信息的对应关系呢 之前确认的意义在于保证发送方发送的数据被对方收到了现在我们的报文是有序号的我现在发送10,11,12对方可能是乱序接收的但是因为有序号就可以升序排序就变的有序了紧接着我收到了3个确认可是我怎么知道这三个确认报文哪一个报文是对应历史上发送出去的这些数据报文的呢也就是确认信息和发送信息对应关系的问题 TCP的报头中涵盖一个确认序号这个确认序号是对历史确认报文的序号1。比如客户端发送的是编号10,11,12的报文那么10号报文对应的确认报文是11,11号对应的就是1212号对应的就是13. 当发送方收到确认应答tcp报文之后可以通过确认序号来辨别是对哪一个报文的确认。当我收到了确认序号为11的确认应答我就知道了11号报文之前的报文我已将全部收到了因为我发的是10,11,12所以10号肯定收到了如果我收到的是12我就认为历史上12号之前的报文我已经全收到了。 准确表述一下确认序号就是对历史报文的序号值1代表的含义以确认序号是13为例就代表13之前的所有的报文我已经全部收到了下次发送请从13号报文开始发送 无论是数据还是应答本质都是发送的一个TCP报文我发的和我收的都是TCP完整的报文可以不携带数据但是一定要具有一个完整的TCP报头 eg:来回通信的都是TCP完整的报文 为什么一个报文里面既有序号又有确认序号 我们双方在数据通信的时候发送的一定是TCP报文对于客户端和服务器来讲我们一定是要填自己的序号的。 我们发现一个报文里面既有序号又有确认序号为什么TCP报头在设计的时候序号和确认序号是两个字段每一个人都要占4个字节 貌似我们只要有一个序号字段就可以了比如服务端发送数据时序号填写10那么服务端应答的时候同样的序号字段填写成11我就可以用一个序号发的时候代表序号确认应答的时候代表确认序号。换言之在通信过程中我们完全可以使用一个序号值就表明刚刚的通信过程了为什么TCP协议在设计的时候序号和确认序号是两个独立的字段呢 根本原因是因为我们刚刚在谈的时候只谈了数据单方向的从客户端到服务器。但是TCP是一个全双工的通信协议(你在给我发消息的同时我也可以给你发消息你在给我进行确认的时候我也可能在给你确认)我们实际上对应的一个客户端在发送数据的时候可能是既有自己要发送数据的序号也有可能这个报文是对对方的确认。 比如你和你爸说我想吃拉面你爸说好的这是单纯的你在给你爸说话你在给你爸说话的时候你爸的给你的应答就叫做好的。这就是单向的你爸给你发消息你爸给你一个应答。还有一种情况你和你爸说我要吃拉面你爸说拉面不好吃我们吃火锅。你爸说的话里面拉面不好吃就是对你刚刚的消息做确认“我们去吃火锅”这句话是既有对你报文的确认又有他自己想给你发的消息。一个确认不是干巴巴的确认它可能还会携带一部分数据。 所以我们的最终结论双方通信的时候一个报文既可能携带要发送的数据也可能携带对历史报文的确认。换言之我可能给你发的消息即是对你上一个报文的确认同时里面可能会携带上我想给你发的数据所以一个报文即可能是对别人的确认又可能携带自己的数据。所以这就是为什么TCP要设置两个序号。 16位窗口大小 单纯的发数据也是有问题的比如客户端疯狂的给服务端发数据但是因为你的机器是发送方对方的机器是接收方接收方机器的状态你并不清楚机器内存有多少内存是接受缓冲区接受数据的能力是多少。就好比有一种饿叫做你妈觉得你饿不管你吃的再多你妈总让你多吃点实际上你早就吃不下了。同样的万一你频繁给对方发送数据导致对方来不及接收这个报文就只能被丢弃TCP虽然有策略保证丢包可以重传但是一个报文千里迢迢经过公网传到目标主机但是它的下场就是直接被丢弃而且还浪费了很多网络资源这种大量发送对方来不及接受进而导致对方把报文直接丢弃的现象我们称之为因为没做流量控制而导致对方丢包的问题虽然并不是大问题但这就是浪费网络资源所以我们必须得修复它所以我们必须保证我向对方发消息报文的总数一定是在对方的可承受范围之内所以我们就要有16位窗口。 TCP协议是自带发送和接收缓冲区的TCP协议内部是会为了方便数据的收和发是会自带接收和发送缓冲区的就是TCP内部malloc了两段内存空间。 TCP为什么要弄两个缓冲区 如果调用send/write直接把数据发送到网络里本质就是要用网卡把数据通过网卡发出去相当于write/send直接调用了网卡接口把数据直接发出去可是网卡也是外设所以直接把数据发送到网卡中就等价于你直接调用printf的时候直接把数据写到显示器里面就等价于你要进行fwrite的时候直接把数据从内存刷新到磁盘里这样的话效率是比较慢的。我们如果带一个中间的缓冲层的话应用层只需要补数据拷贝到发送缓冲区的内存空间里就够了就如同文件把数据拷贝到你的文件的对应的写入缓冲区之中其中上层就可以进行返回了剩下的就是系统的事情了。TCP虽然隶属网络但是它是在OS内部实现的所以它带个缓冲区很正常。 1.提高应用层效率如果有这个缓冲区应用层把数据只要拷贝到发送缓冲区里面应用层就可以直接进行返回了至于这个数据什么时候发怎么发应用层不关心这个就是TCP的事情了。 不管是客户端还是服务端你想把数据发到网络里网络能不能让你发呢对方能不能接受你的数据呢?...像这些问题都属于网络细节如果把这些任务交给应用层应用层是无法知道的但是只有OS中的TCP协议可以知道网络乃至对方状态的明细。所以也就只有TCP协议能处理如何发什么时候发发多少出错了怎么办等细节问题。这样的话题就叫做传输控制协议意思就是你的应用层只需要把数据拷贝到我的缓冲区里应用层就别管了剩下的事情就交给TCP了至于这些通信传输过程中遇到的问题由我来统一控制所以TCP协议才叫做传输控制协议。就如同现实中你发快递到了一个快递点工作告诉我你只需要填一个单子然后你就走吧不用管了至于这个快递什么时候发如何发中间如果有人快递丢了怎么办这些细节问题与你无关所有发送快递的细节有快递公司统一承担这就是传输控制协议。 TCP协议只关心数据如何发出应用层只关心数据如何拷贝进来拷贝完成应用层返回剩下的工作应用层完全不考虑。 2.因为缓冲区的存在可以做到应用层和TCP进行解耦 再次复盘 如果我要发一个数据应用层把数据拷贝进来在我拷贝的时候有可能发送缓冲区里本来就有数据也在发所以当我实际在进行拷贝的时候我就是写入当人家把数据发出去的时候就对应发出这里就相当于一个在写入一个在发出一个在生成一个在消费这种也是用户和内核级别的一个生产消费者模型。同样的如果对方接收的话一定是有人从网络里把数据给他发出去发出去之后应用层只需要把数据读上来就可以至于这个数据怎么收的收多少这些问题应用层也不关心所以这就是发送和接受缓冲区存在的问题。 16位窗口大小 客户端和服务器是相隔千里之外所以当客户端给服务器发送消息的时候客户端自己有发送缓冲区应用层无脑给他拷贝客户端也无脑给对方发因为服务器是有自己的接受缓冲区的所以服务器在接收的时候有可能把缓冲区已经接收满了也就是说当我实际在给对方发消息的时候如果不加任何速率方面的控制其中我给对方发大量的数据可能导致对方来不及接受。TCP应用层拷贝给它的数据拷贝到发送缓冲区里它在自己的发送缓冲区里拿数据然后不断的把数据扔到网络里如果上层就是不断的在发如果TCP没有任何控制策略最终的结果就可能是服务器的接受缓冲区满了(假设上层不接受或者调用read/recv频率低)当服务器的接受缓冲区满了以后在来一个数据那么此时这个数据就被丢弃了。 如果对方来不及接受对方就只能丢弃。对于TCP来讲貌似是没啥影响的因为TCP有超时重传的机制也就是丢包重传只要丢包了我之后就可以给你重传。可是虽然不影响报文传输经过封装路由器转发消耗了网络资源因为server来不及处理导致它被丢弃了这是server的问题报文是没错的全网的所有主机全都用TCP通信的话我们有几十亿台机器一人丢十几条报文我们光在来回传输这些丢弃报文就不知道要浪费多少电力和人力所有我们是不想报文被丢弃的。 所有我们就有一个流量控制。所谓的流量控制和报文中16位窗口大小紧密相关。 server有接受缓冲区客户端给server发消息server是会进行应答的其中server如何让客户端慢一点呢 类比生活一个人蒙住眼睛拿上水壶另一个人拿上水杯你拿水壶给我的水杯倒水我怎么样让你慢一点呢 1.直接和对方说话让他慢一点。但是server给client说慢一点client是不理解的计算机为了方便计算必须量化处理。 2.比如水杯是500毫升你给我倒一次水我是还剩400毫升又倒一次还剩300毫升....还剩0毫升。我看着水杯不断给你通告还剩多少空间你作为倒水的一方你听到还剩多少的时候你根据剩余的数据量就可以动态的调整倒水的速率乃至倒水的策略。 换言之我们此时的server端收到数据要给对方应答发的数据和应答的数据都是TCP报文所以我们可以在应答报文中在报头里面填上我自己的接受缓冲区中剩余空间的大小 eg:client给server发消息server的缓冲区假设是100kb你给我发消息发了1kb我的接受缓冲区剩余的大小还是99我给你的应答报文里携带上还是99kb的空间那么client是不是就知道了server端的接收能力。 我如何把我自己的接受缓冲区中剩余空间的大小通告给你呢 通过TCP报头中的16位窗口大小。这就是我的接受能力知道了我的接收能力你就可以根据我的接收能力来动态的调整你自己发送数据的多少问题。我们的TCP不仅仅是客户端向server发消息server也可能在给客户端发消息甚至是同时进行的server同样要考虑是不是给客户端发的消息过多了来不及接受。双方在通信时都可在报文中携带上自己的接受缓冲区剩余空间的大小所以此时双方就可以动态的进行数据发送。流量控制主要是为了让数据量发送的速度变得合理而不是一味地快或者一味地慢。 如何通过端口号找到目标进程 进程要被OS管理起来就是有一个pcb进程PCB就是一个内核数据结构我们把他想向成一个大链表网络里是有端口号的进程是要和端口号进行绑定的我们要根据目的端口号找到对应的进程其实就是根据一个整数找到一个进程用的就是哈希的策略我根据目的端口做哈希然后就可以找到目标进程的PCB只要目标进程的PCB找到了这个进程的对应打开的网络文件那么网络文件对应的网络缓冲区也就有了报文收到之后把他拷贝到它自己的网络缓冲区里就可以了。 系统中存在很多文件为什么你读取文件的时候这个文件读取到系统之后是读给你的 你创建一个进程打开一个文件这个文件是配套的有缓冲区的其实当网络里来了数据我们根据端口号哈希算法找到目标进程根据目标进程的相关指针数据就可以找到这个进程打开文件对应的缓冲区然后把数据拷贝进去这个时候这个进程就拿到了这个数据。 根据端口号能找到进程只要找到进程就能找到这个进程曾经打开的socket文件socket文件是有缓冲区的然后把数据放在缓冲区里这个数据就属于这个进程了。 6个标记位 实际上有些TCP标准它的标记位不是6个而是8个但是多的2个特别不常用。 TCP协议是面向连接的。TCP socket(就是基于TCP协议在应用层用的接口)要通信的时候需要先connect所谓的面向连接本质就是通信前要先建立连接。 为什么要建立连接呢 是为了保证TCP的可靠性的。 如何建立连接呢 TCP的三次握手。三次握手是一种形象化的表述说人话就是在通信前我们要进行三次数据交换。三次数据交换再说人话就是我们要进行交换三次报文。现在的报文我们暂时不考虑携带的数据只是一个报头。 作为一个server在任何时刻可能有成百上千个client都向server发消息。server首先面临的是面对大量的TCP报文如何区分各个报文的类别 eg:我是一个餐厅的老板我把我的餐厅的生意打理的特别好饭特别好吃服务特别好所以陆陆续续有好多的人来我的餐厅吃饭可是除了到现场吃饭的人外还有很多人点的外卖作为老板我怎么区分这个人是正常来吃饭的客人还是来取餐的外卖小哥呢 一般就是通过他们的穿着外卖小哥穿着很具有特定。不同的人群老板有这不同的处理策略 server就是通过TCP的标志位来进行区分所以这里的6个标志位表征的是不同种类的TCP报文不同种类的TCP报文对应的接收方给他们的处理策略是不一样的。所以如果server收到一个报文标记位填写的是ACK我就认为这个报文是一个确认报文如果是SYN它就是一个连接请求的报文如果是FIN我认为它是断开连接的报文。换句话说不同标记位可以让server做出不同的动作。 ACK标记 假设client和server进行通信时客户端发消息后server端进行响应因为我们已经有了一个确认序号对于这个响应我们就可以填上一个确认序号除此之外我还得表示下我这个报文的类别就好比如果你是一个外卖小哥就应该穿上黄色的或者蓝色的衣服上面必须得把对应的美团外卖饿了么外卖的标志带上所以作为老板一眼就看出这个人是取饭的。确认序号是用来供client确认它发出去的报文有多少被对方收到了我们要表征一下这个报文就是一个确认报文而不是一个连接断开连接请求的报文所以这就要将ACK设置成1ACK就叫做对报文做确认表征报文自己是一个确认报文。几乎在所有的TCP通信的过程中ACK都会被设置。 SYN标记 server端可能会收到一个连接建立的请求请求虽然叫请求但是它也是数据所以也要进行交换server端如何区分发来的报文是请求呢 我们就通过SYN标记位SYN称之为同步标记位也叫作建立连接请求的标记位。也就是说只要client发送一个报文SYN位被置1了就证明这个发来的请求它是一个连接建立的请求。一旦收到了一个SYN的请求之后server和client要进行数据交互此时就要完成3次握手。 建立连接三次握手的过程 三次握手的过程 首先client(建立连接的一方)要先发送SYN(注意这里不是只发了个SYN过去而是发送了一个完整报文报SYN位置1)server端进行确认确认的时候带了SYNACK。紧接着client(主动发起连接建立的一方)再次给对方一个响应。这个就叫做3次握手的过程。换句话说当我们建立连接的时候我们首先需要的是3次握手的过程而SYN的标记位代表的连接建立的请求server端一旦收到SYN也要返回一个同样的SYN标记位以告知客户端我们可以来连接。然后client再给server一个响应。 eg:在学校里你喜欢一个女孩你和她说做我女朋友吧 对方说好啊什么时候开始呢你说就现在。此时我们就以一个较快较短的方式就完成了一次3次握手的阶段。 3次握手的目的就是建立连接我们理解下什么叫是连接 一个client可以向server建立连接10个client也可以向server发起10个建立连接请求...所以在某个时间点server中是可能会存在大量的连接的。server端一旦存在大量的连接。那么server需不需要管理这些连接呢 当然是需要的管理方式就叫做先描述在组织也就意味着server端一定存在描述连接的结构体结构体里填充的就是该连接的各种属性最后把所有的连接以各种数据结构组织起来比如说链表哈希表二叉树.... 建立连接的本质3次握手成功一定要在双方的OS内为维护该连接创建对应的数据结构(这就叫做创建一个连接)所以双方维护连接是有成本的(时间空间)创建对应的数据结构要花时间更要花空间。 为什么是3次握手呢 3次握手我们并不担心第1次丢第2次丢我们担心的是第3次丢因为第一次它有应答第二次它也有应答第三次它没有应答最后一次没有应答就有可能有丢失的风险。不要认为3次握手就必须成功。三次握手指的是以较大概率建立连接的过程。 我们注意到建立连续的线都是斜着向下画的以证明报文除了从左(右)向右(左)迁移之外从上到下也在进行时间的流逝。 我们要进行3次握手client和server都要认为只要3次握手完成连接就建立好了。其中对于client来讲是不是只要最后把ACK发出去client就立马认为连接已经好了还是client发出去的ACK被server收到之后才任务连接已经建立好了 答案就是只要把ACK发出去了client就立马认为连接建立好了因为最后一个ACK根本就没有响应所以client就没有办法得知最后一个ACK是否被server收到了。假设client最后发送的ACK的时间是10:00当然这个ACK有没有被server收到client是不确定的有可能这个ACK就丢了这个时候就是搏一搏单车变摩托。此时当ACK被server收到假设收到的时间是10:02此时server的3次握手才成功。 一般而言双方握手成功是有一个短暂的时间差的。 RST标记位  假如最后的ACK丢失client认为连接已经建立好server认为连接还没有完成那么server就不可能给client发消息但是此时client就开始发送它的消息 一旦发送消息时这个消息经过网络被传送到了server端server端会认为你这个client访问我的8080端口不是应该建立连接吗你怎么连接都没建立好就给我把数据发过来了。此时server就有可能给client发送一个响应回的报文这个报文的标记位携带RSTclient一旦识别到了RSTclient就意识到连接建立失败了client最终就会关闭掉它的连接所以RST是用来重置异常连接的。 第三次的报文丢失只是连接异常的一种情况只要是双方连接出现异常都可以进行reset来进行连接重置所谓的重置就是把双方连接对应的我们曾经维护的连接对应的在双方内存空间的数据清理掉让我们的客户端重新连接。这就叫做RST。 PSH标记位 如果客户端给服务端发消息服务端的接收缓冲区快打满了或者已经打满了然后客户端就想催对方让对方把数据尽快向上交付客户端就可以发送一个报文报文里的PSH标记位置1它的作用就是告知对方尽快将接收缓冲区中的数据尽快向上交付。 如何理解这个让上层尽快将数据取走是怎么个取法 read/recv是用户层在调用我如果是个恶意用户你发的数据我压根就没调用read/recv或者我就干脆不给你读取这样的话神仙来了也没办法。 再者如果你作为一个服务端的程序员有数据你不尽快读走你是一名合格的程序员吗 所以一旦有数据来了我们应该做的就是尽快取走程序员在上层一定是会尽快把数据取走的来不及取走一定的上层来不及读取。 我们现阶段理解告知上层尽快取走数据当你实际在进行数据读取的时候缓冲区里面不是说有数据就能让你读而是说这个缓冲区里面的数据实际是有它的低水位和高水位标记的比如缓冲区是100KB接收到的数据假设超过了5KB上层才能读如果数据超过了80KB你立马就要读了或者上层就不能在写了...OS能做的就是告诉你这个数据可以读了。比如在read的时候不是说来1个字节读一个字节而是来了一批数据OS才让你读因为过度频繁的通知你数据已经好了就会导致你过度频繁的调用read每一次read系统调用就会涉及用户和内核过度频繁切换进而导致效率比较低所以OS还是希望你一次读一批数据而不是一个一个读所以这里就可以在OS层面上告诉你数据已经就绪了。 URG URG是和16紧急指针是搭配使用的。目前因为TCP有按序到达每一个报文什么时候被上层读取到基本是确定的相当于我读完第一个再读第二个读完第二个再读第三个...如果我想让一个数据尽快的被上层读到可以设置URGURG表明该报文中携带了紧急数据需要被优先处理。这个URG只是表明有没有紧急数据99%的报文都是不携带的有一个携带了我们还需要确认这个紧急数据在哪里这个紧急数据就又16位紧急指针指向。 16位紧急指针是什么呢 TCP的报文后面携带的是数据如果你把数据想象成一个字节序列16位指针就会指向对应的位置。比如我送的是abcdefg123456我如果想让对方优先读取的数据假如说是g我们此时的16位指针就可以指向g在报文中的地址这个就叫做16位的紧急指针。ps:TCP的紧急指针只能传输1个字节。如果紧急指针能让你传太多的数据它就破坏了TCP本身按序到达的特性它给你开个紧急指针让你传1个字节已经是仁至义尽了。 eg:send当中的flag参数可以设置为MSG_OOB这个就叫做读取紧急指针 我们把这种紧急数据又称为带外数据意思就是在TCP正常通信的数据流中我们可以插队般的紧急把这个数据获得。  这个带外数据有什么用呢  比如今天我有个服务但是现在这个服务出错了网络各方面状态都好着但是它的服务出错了出错后我在怎么请求都没用就相当于它的服务已经出现问题了。但服务没挂掉如果它的服务里专门有个线程读取带外数据我就可以向我的服务发起一个带外数据的请求最后它给我响应一个带外数据的响应此时它可以给我传输一个状态码比如这个状态码中分别用1,2,3,4表示不同的错误此时这里就可以用带外数据来做。 FIN标记位 一般而言建立连接的一般是client但是断开连接是双方的事情双方随时都有可能客户端可以断开server也可以断开。我们以客户端主动断开为例。 客户端此时要断开连接所以客户端也要发送一个表明自己是断开请求的报文所以就在报文中携带FIN标记位。FIN相当于就是client告诉server我想断开连接。当然实际上断开连接有很多场景比如客户端想断服务端不想服务端想断客户端不想双方都想断因为TCP本身是全双工的只要一方想断开连接就是说我对你没什么好说的了但是有可能server还想向client发因为TCP是全双工的我们也不影响。 四次挥手 现在client向server发送断开连接server同意断开连接就对他进行应答就相当于把client向server通信的信道关闭了假设现在server也要断开我的连接就向client发送断开连接然后client再对这个报文进行ACK至此就称之为4次挥手完成。 eg:类比生活中离婚这就叫做达成一致征得双方同意的本质就叫做达成一致关闭连接的过程用4次挥手本质就是为了以最小的成本达成一致。 以4次挥手的方式达成连接关闭的一致认识。  如何理解序号 发送缓冲区我们可以理解成一个大的数组比如应用层发一个hello我们就把hello按字节为单位依次填写到了我们对应的发送缓冲区里。所以每个字节都天然的带有编号如果你想把0~4这一段报文全部发出去那么我们给这个报文的编号就是这批数据的最大下标4作为我的序号给对方发出去。对方给我响应5的时候我此时就认为4之前的全部数据(包括4)都发送了接下来继续发后面的。不管是文本图片...都是二进制流所以我把所有的数据按字节为单位放在数组里每个数据就天然带来编号。这个编号就是序列号。 TCP是面向字节流的上层的数据交给发送缓冲区在发送缓冲区以及接受缓冲区看来数据全都是基于字节流的也就是它的报文之间是没有明显的边界的也就是全部放在数组里上层去读就可以了。 超时重传机制  数据丢包了保证对方还能收到就只能重传。报头里没有体现任何超时重传的机制TCP保证可靠性有很多是在报头里就直接体现出来了也有一些可靠性机制是没有在报头里体现出来的。因为凡是没有体现出来的直接使用现成的(已经体现出来的这些机制)再加OS本身的一些机制就能完成。 超时重传就需要OS给每个报文设置一个定时器。 超时时间间隔应该是多长 比如把报文发出去1秒钟对方就绝对能收到可是你非得等5秒就会导致主机发送的效率特别低。如果我把超时的时间设置的特别短就有可能导致重复发送报文的情况。 时间间隔网络是变化的网络通信的效率是变化的发送数据得到ACK时间也是浮动的超时重传的时间一定是浮动的 当你把报文发出去了发送方没有收到确认ACK接收方是一定没有收到对应的报文数据吗 不一定 1.这个报文真的丢了 2.应答丢了 但是对于客户端来讲从技术角度是可以识别出这两个问题的但是这特别复杂实际客户端根本不关心是啥原因因为我们有重传。此时唯一带来的问题就是真的丢包情况还好最害怕的是对方已经收到了但是确认应答丢了。此时在重传就可能导致对方收到了重复的数据。当对方收到了重复的数据本身也是不可靠的表现。 我们怎么保证对方收到的数据不是重复的呢 很简单就是因为每个报文都有序号既然你是重传报文那么这个报文在序号上一定没有变化只要没有变化服务器就可以根据序号进行去重所以我们就不担心报文被对方重复收到。           那么, 如果超时的时间如何确定? 最理想的情况下, 找到一个最小的时间, 保证 确认应答一定能在这个时间内返回. 但是这个时间的长短, 随着网络环境的不同, 是有差异的. 如果超时时间设的太长, 会影响整体的重传效率; 如果超时时间设的太短, 有可能会频繁发送重复的包; TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间. Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍. 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传. 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接过了一会对方缓过来了你把连接强制关闭了对方连接还在没关系你给对方会发送reset让对方重新连接。 连接管理机制 在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接。 三次握手就是客户端发送TCP报文SYN位被置1服务端进行SYNACK进行响应客户端在进行ACK客户端只要发出去ACK连接就建立成功服务端收到后连接才建立成功。客户端发送SYN状态就变迁至同步发送SYN_SENT服务器发出SYNACK状态变迁为同步收到SYN_RECV。最后一个ACK丢了就丢了我们就进行超时重传。 三次握手是双方的OS中TCP协议自动完成的用户层完成不参与应用层唯一要注意的是客户端调用connect就是在发起三次握手。直到server的accept返回就一定是3次握手已经成功了。所以在TCP中不要认为用户的发送行为会直接影响tcp的发送逻辑。 为什么是三次握手  在我们两个想建立连接的时候无非就是要确立两件事情 1.对方好着没 2.网络好着没。 eg:你和你的朋友打电话你的第一件事情就是先拨通电话电话拨通之后当你的朋友先接起来“喂”这叫做网络好着呢但是你们两的状态适不适合谈话呢你喝对方说现在方便打电话吗这叫做确认对方好着没。 建立TCP连接确认的就是下面两件事  a.确认双方主机是否健康(对应对方好着没) b.验证全双工三次握手我们是能看到双方都有收发的最小次数(对应网络好着没) 验证全双工就得验证客户端和服务器本身具有数据收和发的能力只有具有数据收和发的能力你将来才可能全双工的起来 所以对于客户端来讲它发送SYN就是证明自己能发数据它收到SYNACK证明客户端能收数据前两次握手就证明客户端是能够收和发的换言之客户端自己发送给服务器端的这条信道是通畅的自己收这个报文的能力是具备的。对于服务器端当他收到了一个SYN就证明它是能从客户端收到消息的当服务器端收到ACK就证明它自己曾经发送的SYNACK已经被客户端收到了就证明服务器端自己也有发数据的能力。换言之客户端和服务器双方就能以最小的成本次数去验证全双工。 假如是1次握手客户端无法验证自己收和发的能力。服务器即便收到这个报文也只能验证自己具有收的能力发的能力无法验证。 假如是2次客户端发个消息服务端给响应。这可以验证客户端收和发的能力。服务器收到一条消息只能验证自己接收数据的能力但是它无法验证自己发出去的消息是否被服务器收到所以就无法验证发数据的能力。 所以3次能行了3次肯定就是验证全双工的最小次数。 当我在验证全双工的时候双方一定能够收和发数据的前提就相当于是对方好着呢要不然我们就不可能收到对方的应答网络好着呢要不然我就不可能收到我们对应的收发的报文。所以我们只要能验证全双工我们也就能同步的去校验对方是健康的主机是没有问题的甚至主机的网络通信的信道都是没有问题的。其中确认双方主机的健康更多的是确认两方面1.主机状态好着没OS有没有挂掉 2.在OS层面上双方的IO状况是健康的。同样的我只要能收发到消息网络的状况也是好的。 为什么4,5,6次握手不行呢 4,5,6次不行是因为它们已经有些多余了我们不是为了建立连接而建立连接我们是为了验证网络状态双方主机的就绪状态验证全双工状态来发起3次握手。3次握手足以完成双方交互状态那么4,5,6次已经不需要了因为过多次的握手就会建立连接的成本。 为什么1,2次握手不行呢 除了上述理由还有些其他理由 1次握手这种情况是绝对不可以的如果只是客户端发一条消息双方的连接就建立好了对于服务器端一旦建立好连接OS是会为了维护连接创建连接对应的数据结构换言之维护连接是有成本的如果只是一次握手客户端只需要向你发送一个报文此时就占据了一个连接的资源如果客户端重复的发送海量报文的时候那么它就很容易让你服务器上的资源很快被消耗完那么客户端给服务端发送SYN请求的时候服务端立马就把连接结构建立好了但是客户端又不和服务器通信客户端来10万次就占据了我10万份资源。所以一次握手是绝对不可以的因为这样的话服务器收到攻击的成本实在是太低了。 2次握手本质上和1次握手是没有区别的客户端发一条消息服务器发一条消息此时连接就建立好了。对于服务端只要收到一个消息只要把这个报文发出服务端就认为自己对应的连接就已经建立好了甚至这个服务器的响应报文客户端压根没收到或者这报文已经丢弃了服务端也认为自己的连接建立好了所以作为客户端我要攻击你这个服务器给你发送大量的SYN服务器发回来的ACK管都不管服务器照样会维护大量的健康连接可是这些连接从来没有人给你在进行后续工作了维护连接是有成本的发一个维护一个发10万个维护10万个我甚至无限给你发都行因为对于客户端是没成本的服务器可是要进行维护连接的这样的话你的服务器随随便便就被别人攻击了。 1次2次握手是极度容易被别人通过发送海量的SYN而消耗完服务器上的连接资源的。维护连接是有成本的比如一个连接5KB对方给你发送100万条就是5个G后果就是服务器端充满大量的连接我们把客户端发送大量SYN的请求叫做SYN洪水。1次或者2次并不能很好的预防SYN洪水问题。 3次握手就可以预防了洪水问题了吗 3次也有问题也不一定能预防。但是3次握手相对而言被攻击的成本会高一些当然实际上为了预防这些攻击我们也有其他的策略。当连接建立好双方为了维护连接是有成本的有了3次握手后给了服务端一定的缓冲策略实际上TCP就可以对有效连接做到甄别。 3次握手也不能彻底解决SYN洪水问题它相较于1次或2次的优点如下 当服务端发送SYN服务端响应SYNACK的时候也就是完成两次握手的时候此时服务端并不认为连接是建立好的。说明如果客户端只给我发送大量的SYN的时候服务器端并不认为连接是建立成功的因为你给我发SYN我给你发SYNACK你又不给我响应(没有最后的ACK)所以服务器端并不会为你维护连接结构体也就意味着服务器端的资源并没有太多的消耗 那么如果客户端发送SYN服务器响应SYNACK客户端在响应ACK我的连接不就建立好了么如果这样的话那么客户端至少得维护下发送的SYN对应返回的SYNACK那么这个连接才能合法的建立。这种情况说白了就是正常的进行3次握手你消耗服务器的资源的同时你也在消耗客户端的资源所以双方是等量的一种消。如果是等量的消耗就意味着普通的小白如果拿着1台2台电脑他是不可能把服务器全部攻击掉的所以就基本杜绝了小面积的个人的去攻击我服务器的可能性。 比如如果我3次握手进行攻击你就等价于是个合法连接如果你是合法连接那么这个连接一旦建立好那么服务器就能把这个连接获取上来你这个客户端的ip端口号我都能拿到那么一旦这个客户端是个恶意用户跟我建立了好多连接此时我在应用层得到了你的ip和端口号“你为什么作为一个普通的客户端就给我建立了上百条连接呢”所以我直接在应用层就可以做一些安全策略比如黑名单我一识别到你的连接是非法的直接就把你这个连接关掉甚至我服务器底层也可以在防火墙层面上用防火墙的接口这个连接来的时候直接就拒绝掉此时传输层和应用层配合共同去阻挡恶意连接的到来。 就相当于对我们来讲如果是3次握手客户端是不能通过只发送大量的SYN来对服务器进行海量攻击的因为只发SYN没有ACK其中我的服务器也就不会为它维护连接。如果正常3次握手攻击至少客户端和服务器是等量成本其二就是服务器可以拿到这个连接的相关信息做各种安全策略。 半连接  实际上只发送SYN的时候服务器也是会维护连接的叫做半连接只不过维护的时间特别短半连接也有自己的安全机制一旦是你发送的请求到我这边的时候我在tcp底层也有相应的安全措施。总之如果是半连接的话服务器的成本是非常非常低的。事实上只发SYN这样的攻击在TCP这里是存在的所以TCP有自己的策略。 事实上通过3次握手这样的攻击也是存在的虽然以一己之力是做不到的 但大部分的恶意攻击其一是为了窃取你的信息其二就是他想用你的资源他不是为了攻击你比如他给你种植了木马病毒这个病毒可能就是有定时的任务比如中午12:00统一向百度发起3次握手他在全网中散播木马他就可能在网络中劫持上万台机器然后在12:00同时发起3次握手发起之后对于服务器讲在某个时间点突然来了一大批请求因为服务器有可能每天的用户量是确定的突然有一天来了大量的请求服务器的硬件配置各方面软件服务跟不上就有可能被搞垮了。别人通过一些恶意方式劫持你的机器此时你的机器就叫做肉机这个肉机就可以定时定点的向特定的服务定向的去发送某些请求。这种情况是真实存在的而且像这种情况是防止不了的因为被攻击的是客户客户向你发起就是正常请求。当然劫持大量机器的成本也是很高的。 为什么是四次挥手 断开连接的本质双方达成连接都应该断开的共识。就是一个通知对方的机制。 四次挥手是协商断开连接的最小次数。你要和对方断开连接你得让对方知道同时对方也要让你知道它同意了。四次挥手更强调功能性只要能够以最小成本把连接断开就行了。 四次挥手的状态变化 当客户端发起FIN的时候它的状态就变迁到FIN_WAIT_1当服务端收到断开连接的请求并发出自己的ACK服务端状态变迁到CLOSE_WAIT然后客户端收到ACK状态变迁为FIN_WAIT_2。此时2次挥手就完成客户端告诉服务器连接它断开了客户端不想和服务端说话了注意这只是断开了单向连接服务端还可以向客户端发。再下来服务端向客户端发送FIN此时服务端状态变迁为LAST_ACK此时客户端收到后再给服务端响应一个ACK此时客户端进入TIME_WAIT状态服务端收到后变为CLOSED。 理解TIME_WAIT状态 现阶段看先断开连接的一方是客户端先动的手经过4次挥手进入TIME_WAIT状态。主动断开连接的一方要进入TIME_WAIT状态。 这个状态一般而言叫做连接有没有被释放对于主动断开连接的一方叫做4次挥手已经完成。可是对于TCPTCP不能立马让你释放资源因为我们无法保证最后一个ACK被对方收到了。最后一个ACK在发的时候是有可能丢失的本来你可以重传一下但是如果没有TIME_WAIT状态立马关闭此时4次挥手没有完成这个连接已经被你释放了也就没人再次发ACK了。所以TIME_WAIT是主动断开连接的一方即便4次挥手完成也不能立马释放自己的连接结构而必须得维持一段时间这个时候所处的状态就叫做TIME_WAIT状态。 如果立马释放掉连接资源万一最后一个ACK对方没有收到就导致服务端认为连接还建立着呢服务端就重复不断的进行LAST_ACK确认它进行LAST_ACK确认就是超时重传它在超时重传期间经过尝试一定的次数如果不行它的连接在断开。这虽然没有问题但是这并不是正常手段的断开连接。所以当我们进行LAST_ACK不断重复的时候它就会过多的消耗服务器端的资源而导致服务器一直询问所以我们为了节省它的资源我们需要主动断开连接的一份进入TIME_WAIT状态。这只是一个非官方的理由因为主动断开连接的一方通常是客户端但这并不是100%比如之前写的HTTP协议断开连接的一方的服务器。 验证主动断开连接的一方要进入TIME_WAIT 验证主动断开连接的一方要进入TIME_WAIT 我今天这个服务器上面的代码压根就没有用我们只是在main函数里创建了一个套接字绑定了一下然后监听了一下一旦我们处于监听状态我们是允许别人向我们建立连接的。 1.我们验证下当调用accept的时候服务端把连接已经建立好了你只是用accept从底层拿连接。  2.服务端想要主动断开连接。                   验证过程 接下来我进行telnet连接(因为博主只有一条服务器只能自己连接自己) 然后我们再去查 红色框我们看到81.70.240.196这个ip通过随机端口号连接我们。此时的连接已经建立我们的套接字并没有accept但是连接已经建立好了说明accept只是把已经建立好的连接拿了上去。 验证主动断开连接的一方要进入TIME_WAIT 现在我们拿上来这个连接 启动服务器并建立连接 断开服务器并观察现象 总结一哈  1.accept只是帮助我们获取新连接在listen的时候底层就已经建立好连接了 2.主动断开连接的一方最终要进入一个TIME_WAIT状态 3.一旦服务器进入TIME_WAIT状态我们的服务是没办法立即进行重启的  为什么会要有TIME_WAITTIME_WAIT通常是多长 MSLMax Segment Life, 报文最大生存时间。意思就是说如果一个报文从左到右或者从右到左比如从左到右一个报文从一点到另一点最大花费的时间叫MSL一个报文通过我自己的收发统计我发现一个报文在发出去之后有1毫秒2毫秒3毫秒大家都是在这个时间段内数据被对方收到了那么其中3毫秒就是最大传送时间也就是只要包含3毫秒在内一个报文就已经能够从A点到B点了。当然具体到了没是另外一回事反正如果没故障一个报文从A点到B点最大时间就是MSL就叫做最大传送时间换一种角度也就是报文的生存时间。 一个报文从一点到另外一点需要的最大时间是MSLTIME_WAIT时间一般等于2倍MSL。MSL的时间不同的系统的规定是不一样的在linux中默认是60s但实际这个时间是变化的。 为什是TIME_WAIT时间一般等于2倍MSL呢 因为有可能在你自己发出去最后一个ACK(断开来连接的要求的时候)曾今在网络里可能还会存在历史没有发完的数据比如说服务端刚发出去一个报文紧接着就发出去一个FIN历史上曾今还有很多发报文可能滞留在网络当中其中对我们来讲我们就需要在最后一次4次挥手ACK等待2倍的MSL的时间 1.尽量保证历史发送的网络数据在网络中消散我发送断开连接的报文如果这个时候把连接断开了可能网络中还存在一些历史的数据并没有被双方收取话说TCP不是按序到达么按照道理来讲发出去的这个ACK它也要保证ACK被按序到达但是所有的按序到达的前提是确认应答最后一个ACK是没有确认的当你发这个ACK时历史上有很多的数据可能还在路上而且这个报文有没有被对方收到你也不确定那么其他很多东西也没法保证所以保证历史的数据在网络中消散这就是2倍MSL等的时机。 为什么是2倍的MSL是因为历史上的数据刚好就在出口路由器上2倍的MSL至少保证数据是一来一回的因为当我们断开连接的时候双方已经进入最后的握手阶段了只要最后一次ACK的时候双方已经不在发数据了历史上的数据也就不会被增多不会在增多历史上的数据最多残留的时间就是2倍的MSL。 再比如有时候在网络上会进行超时重传有时候重传的时候报文并不是真的丢了有可能这个报文没丢而是阻塞在某个路由器上的但是这个数据经过重传这个数据可能早就不需要了其中当你断开连接的时候压根和这个报文没关系了。但我们担心的是当你断开连接立马重新建立连接而这个报文恰好使用的端口信息又一样恰好这个报文又被你的服务器收到了此时这个报文就有可能干扰你的正常数据的业务逻辑。这种问题存在且或多或少的不可避免。所以我们只能设置门槛降低这种情况的发生概率。所以要保证历史上残留数据消散。 2.尽量的保证最后一个ACK被对方收到了 建立连接不上100%成功的断开连接也不是100%断开的因为网络情况实在是太复杂了。归根结底就是最后一个ACK我没有收到或者丢失了其中发出ACK的一端认为握手成功了接受一方认为没有成功。当发送方发送了最后一个ACK刚发出去就立马计时一个报文发出去最多一个MSL我对对方是否收到这个ACK确实不清楚但是如果我在我等的这个时间段内我又收到了一个FIN就证明我曾经发的ACK丢了。因为我虽然无法确定ACK对方是否收到了但是我只要一直收到FIN那么就证明我的ACK对方是没有收到的。只要对方给我再发FIN我就继续ACK。换句话说当我在等上2倍的MSL这个时间段内如果我没有收到FIN我就认为这个ACK被对方收到了没有消息就是最好的消息。 对我们来讲我在TIME_WAIT期间我没有再次收到FIN可能是ACK对方没收到FIN也出现问题了所以这也就是尽量的原因。 为什么会断开服务器后立即重启会bind error  一个服务如果启动后断开之后。当我想立马进行重启时我发现无法立即重启原因就是因为你是连接断开的主动一方。导致主动断开连接的一方进入TIME_WAIT这个连接并没有被释放连接还在就意味着这个端口还被占领着虽然没人用了所以你再去绑定就是一个端口被另外一个进程再去绑定端口号只能被一个进程绑定所以就会出现bind error。 如何解决bind error 当一个主动断开连接的一方进入TIME_WAIT实际上已经把4次挥手做完了无非就是在等网络数据消散和最后一个ACK被对方收到。但是这个端口你别占着因为此时这个端口上不会再有正常的数据发送了(正常就是一些控制端口连接的数据)所以你就继续等。我们同时也是可以把这个端口直接用起来的。所以linux提供了对应的接口。 这个接口就是setsockopt 使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符 这次我们断开后立马启动就成功了。  服务器无法立即重启会有什么危害 比如双十一我的服务器上挂着大量的连接当连接不断增多的时候有可能随时来的一个连接都有可能是压死骆驼的最后一根稻草比如服务器上已经有10万个连接了然后又来了5000个连接最后这5000个连接直接把我的服务器搞崩溃了也就是进程挂掉了进程挂掉是因为这5000个连接到了但是我的服务器曾经还挂着10万个连接呢所以此时对这个10万个连接来讲我的服务器就是主动断开连接的一方在技术角度没啥等一等就可以了在业务角度危害就大了服务器如果无法立即重启1秒就是几千万的流水如果等上60秒损失就大了去了。所以一定要做到可以立即重启。又因为服务器的ip和port一定是众所周知的所以你不能通过换一个端口号的方式重启服务器必须得在原来的端口号重启。 CLOSE_WAIT状态 四次挥手一定是你主动一次对方主动一次所谓的断开连接本质就是我的客户端调用close服务端调用close。当你调用close的时候就是你主动断开连接的时候。1个close就是两次挥手2个close就是4次挥手。如果客户端给我断开连接我在发送ACK这个一来一回是客户端主动断开连接。可是如果我的服务器不断开连接呢(不调用close)如果服务器不调用close就会导致服务器一直处于CLOSE_WAIT状态而让服务器不会在进行后两次握手。 验证CLOSE_WAIT状态 CLOSE_WAIT状态我们可以正常的进行通信但是进行通信的时候我把你的连接拿上来但是我不关你的连接然后你的客户端你自己退。 ps: 我们只看红色框即可。 我们看到服务器的状态就处于CLOSE_WAIT此时服务端没有关闭这个文件描述符没有调用close然后客户端告诉服务器我要跟你断开连接服务器说好的自此以后服务器就不说话了也不关闭连接此时四次挥手的后两次不会执行我们的服务器就会把这个连接一直维护着一直处于CLOSE_WAIT状态客户端早就走了CLOSE_WAIT对服务器的资源是一个非常大的消耗。 CLOSE_WAIT给我们带来的启示 1.一个fd被用完千万不要忘记进行释放fd本质就是数组下标它在linux内核2.6中是32个但是实际上我们可以通过打开linux的一些选项来把他的文件描述符调整成10万个。我们自己用的是生产环境(线上环境)能打开文件描述符的个数就是10万个(这是生产环境必须要有的要不然一个服务器只能打开几个连接就太少了)。 2.fd是有限的假如你今天写的服务都忘记了close只要有一个连接到来就少了一个fd最终就造成了文件描述符泄露。所以你将来在你自己的服务器上发现有大量的CLOSE_WAIT状态一定不是别人的问题而是你的服务器上有问题。 滑动窗口 我们现在已经有了确认应答机制 发一个数据对方给我一个ACK收到ACK之后我再发下一个数据对方在给我ACK这样的话随着时间的推移报文就可以以确认应答的机制被对方收到当然反过来也是可以的。但是这样的一收一发方式有一个致命的问题就是所有的报文发送都是串行的一旦所有报文发送是串行的那么它的效率和性能势必会降到非常非常低实际上TCP是允许我们一次发送多条数据的就可以大大提高我们的效率。 eg:一家快递公司如果每次只能发一个快递那么他发到猴年马月都发不完但是他可以拿着车一次拉一大批过去这样的话就可以把多个报文在路上的时间就进行了重叠进而提高了效率。 所以主机A一次就可以向主机B发大量的数据这也就是TCP向对方发起对应的数据的时候每个报文都需要带序号的原因因为一次你允许我发送多个这多个消息你是按顺序发的B不一定按顺序收。所以序号相当大的好处就是让我们的数据进行按序到达。 如果我们运行一个主机向另一个主机发送大量数据时那么一次给对方多少呢 一次给对方多少由接收方决定一方面我们要提高效率另一方面我们还要保证发送的数据让对方可以按照自己的接收能力而接收。所以我们为了做到这一点我们就引入了滑动窗口。 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值上图的窗口大小就是4000个字节(四个段).。说人话就是将来我们可能有个滑动窗口以前的报文是发送一个收到应答才能发送下一个现在的如上图发送第一个没有应答发送第二个也没有应答这个就叫做在你发出去一个报文没有应答时其他报文也可以发。就是可以直接向对方塞的数据量。 发送前四个段的时候, 不需要等待任何ACK, 直接发送;注意这里不需要ACK不是永远不需要而是当前短期内不需要。把数据发出去之后理论上每个报文都需要应答。只不过我现在可以先不收到确认。你可以晚来但是不能不来。 滑动窗口在哪里是什么 我们的滑动窗口本质是发送缓冲区的一部分。16位窗口大小表明的是接收方中剩余空间的大小。我一次最多可以给对方塞多少数据不是由发送方放决定的而是由接收方决定的所以就是由接收方的接收能力决定的。所以目前滑动窗口的大小是和对方的接收能力有关(今天就认为是对方的接收能力)比如说你自己的缓冲区里面还剩10KB我这个滑动窗口的这部分区域最多一次可以给你发10KB的数据也就是说我可以同时向你发10KB数据这10KB数据可以暂时不收到所谓的应答。 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;滑动窗口有没有可能缩小呢 滑动窗口不是固定的4个8个10个的它是可以浮动的滑动窗口的概念我们叫做可以暂时不用ACK可以直接向对方发的前提条件就是对方有能力可以把这些数据收到所以对我们而言如果再给主机B发消息的时候比如现在的滑动窗口是4KB我给它发了1KB数据它给了我ACK接收方虽然拿了一条数据但是这个数据没有被上层拿走也就意味着接收方的接收能力变少了那么接收方的ACK给发送方的时候通告发送方窗口由4KB减成了3KB。换言之这个时候的滑动窗口只会把左侧移动过去右侧不变。如果对方一直不拿数据最终这个滑动窗口会减成0。 如图 所以滑动窗口是可以不断的减少的甚至也可能为0右侧可以没有任何变化根本原因就在于对方读取数据的时候对方的应用层来不及接受数据所以滑动窗口就会不断在减少。 滑动窗口有没有可能扩大呢(向右移动) 也有可能当对方告诉我它的窗口大小是4KB然后我的滑动窗口就是4KB我一次给他发了4KB 的数据接收方的上层一下子就把当初积累的数据全部拿走了一下子接收方的缓冲区变成了12KB然后接收方给发送发送放通告的窗口大小就是12KB当发送方收到ACK时发送方对它的滑动窗口一瞬间就不是仅仅向右移动了而是移动的同时还扩大所以就相当于可以快速扩大滑动窗口对应的区域。 如果今天我发4KB对方收到4KB然后对方把这个4KB全部读上去了并且给我的1000~2000先确认所以才有了上图窗口整体向右移动的情况。所以滑动窗口最终能要进行大小调整是和对方的接收窗口接收能力强相关的。 滑动窗口可能向左滑动吗 不可能凡是在滑动窗口左侧的都是已经发送已经收到确认的数据          如果出现了丢包, 如何进行重传?  滑动窗口发送1001~5001这么多报文最后ACK确认先确认的是1001~2001后面的2001~3001也会陆陆续续确认但是如果中间的ACK丢了呢 比如你已经发了你没有收到3001的确认但是你收到了2001,4001,5001的确认这该怎么办 对我们来讲如果我没有收到3001我就认为发送方已经把2001~3001丢了但是我收到了5001序号的含义是我已经收到了该序号之前的所有内容。所有我在发送数据时中间有些ACK丢了我没有收到所谓的ACK应答但是我的报文里却收到了5001的ACK那么就其实已经告诉我了不要害怕实际上2001~3001我已经收到了因为我的序号是从5001给你发的。换句话说如果中间报文的ACK丢了我们的窗口直接移动到5001相当于这部分数据已经被全部收到了。所以TCP是允许少量的ACK丢失的。 如果此时我给对方发消息还是1001~50011001~2001数据对方收到了但是2001~3001数据丢了因为对方收到了5001可是2001~3001的数据没了此时对方给我的ACK是什么呢 不要忘了确认序号的含义它表示的是确认序号之前的数据已经全部被收到了此时如果中间发送的数据丢了此时这里的ACK 5001,4001,3001都不能发你只能发送2001。当发送方收到2001的时候它就立马知道了我发的这4个报文中有可能有一些报文已经丢了我试一试在发送一个2001~3001一发完后对方收到了完整的报文ACK就成了5001。 总之当我们进行数据发送的时候因为是滑动窗口只有收到ACK的时候滑动窗口才会右移所以不要担心当我收到ACK的时候中间的ACK没收到但是结尾的ACK收到了只要你收到了最大就认为之前的全部收到了协议就是这么规定的。如果ACK2001收到了后面的3个没收到我就判断是不是后面的丢了我就进入超时重传的环节。换句话说当我没有收到ACK时窗口向右滑动的时候不会越过这几个没收到ACK的字段数据就被暂时保存起来了。滑动窗口右移的过程就是删除数据的过程所以如果我的窗口不向右滑动不越过这个节点不越过这个数据这个数据就一直在发送缓冲区里面它就在等ACK如果等不到ACK就进行超时重传。所以多我们来讲我们曾经说你要把数据超时重传前提条件是你把数据已经发出去了但是你没收到ACK之前你还得把数据保存起来就是在滑动窗口里保存。 再次理解滑动缓冲区 我们曾经说过发送缓冲区可以想象成一个数组。数组中的每一个元素就是一个字节所以上层写数据的时候木就是把字符一个一个填充到数组里面我接下来就是一个一个发了。所以所谓的滑动窗口我们可以把他理解成我定义两个int win_start int win_end 可以认为源码就是这么实现的。滑动窗口右移就相当于start。滑动窗口扩大相当于end。 当我们给对方发了一条消息比如说我们的滑动窗口大小是4KB我给对方一次塞了4KB的数据比如对方接收缓冲区是16KB可是现在只剩4KB了然后我就给他发了一批数据填到了它对应的发送缓冲区里然后接收方的应用层没有把数据取走所以接收方会告诉发送方它的窗口大小是0所以当我收到了对方一个一个的ACK报文的时候我发现接收方给我的窗口大小是0。发送方怎么办呢 当我收到一个确认序号我们的窗口左侧向右移动本质是win_start确认序号所以当不断ACK的时候win_start这个位置不断向后移动最后指向和win_end一样其中当接收方通知窗口是0win_end就不移动此时win_start和win_end就指向同一个位置就证明滑动窗口为0此时就意味着我们不能在进行发生了。当再次ACK时假设接收方的缓冲区变成16KB了所以更新了一个窗口大小16所以win_start不变win_end 对方通告的窗口大小。如此同步的告诉我确认序号和窗口大小那么这个滑动窗口就整体的右移了。 貌似这个缓冲区是线性的那么发送缓冲区不会越界吗 不用担心上层把数据放进来你把数据发出去这就是一个简单的生产消费。而且我们曾经还学过一个东西叫做环形队列所以实际上发送缓冲区是环状的形式组织的那么窗口不断移动的本质就是窗口绕着这个环不断的转圈此时就不存在越界的问题。比如环形队列被我们写满了你调用write是有可能阻塞住的所谓的阻塞住就是环形队列被打满了当然网络中肯定是会被阻塞住的当然也无需担心你把数据从上层拷贝到你的环形队列里然后环形队列的发送缓冲区里面滑动窗口就控制它的发送速度然后当你实际在向对方不断发送数据的时候如果你的发送缓冲区被打满了但是对方接收数据为0你也就不发了当你不发时上层再拷贝数据就拷贝不下来了因为缓冲区被写满了。你上层的read/write就会被阻塞住。 再次总结下丢包的两种情况 情况一: 数据包已经抵达, ACK被丢了这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认;  情况二: 数据包就直接丢了.当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 我想要的是 1001 一样; 如果发送端主机连续三次收到了同样一个 1001 这样的应答, 就会将对应的数据 1001 - 2000 重新发送; 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中这种机制被称为 高速重发控制(也叫 快重传) 超时重传 vs 快重传 实际上TCP里这两种重传机制都是存在的为什么超时重传还存在呢 原因就是快重传是有要求的你必须连续收到3个同样的确认应答。如果我滑动窗口只有两个空间一次支持发送两个报文其中1个丢了我们也只能得到一个ACK所以快重传不能解决所有的问题所以超时重传是给我们兜底的也就是说超时重传必须存在快重传是在保证能重传的前提下为我们提高效率的。 流量控制 接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control); 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 窗口大小 字段, 通过ACK端通知发送端; 窗口大小字段越大, 说明网络的吞吐量越高; 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端; 发送端接受到这个窗口之后, 就会减慢自己的发送速度; 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端 什么时候发送方就知道了接收方的接收能力 取决于对方什么时候给我发送的第一个报文在3次握手的时候双方已经进行交互了所以就是在3次握手期间协商窗口大小。双方就要根据对方的窗口大小来设置自己的滑动窗口的初始值进而调整发送的速率。滑动窗口就是既想提高效率(体现在滑动窗口内的报文可以暂时不要应答立马发)又想保证对方接收。          如果我的接收缓冲区的窗口大小为0怎么办 此时发送方就不发数据了就停下来了因为我们有流量控制。那么发送方什么时候再向接收方发消息呢 理论上是不会再发了因为接收方的窗口大小是0如果接收方的应用层一直不取数据就会导致发送方一直在等所以发送方可以向接收方发送报文携带PSH标记这个报文可以不携带任何数据只是报头发过去之后并不占用接受缓冲区数据的空间。所以发送方发出PSH后接收方意识到对面催了我得赶紧让上层去取数据。 一旦发送方给接收方发了消息接收方要不要应答呢 TCP是确认应答的所以接收方要应答一旦应答了就要在通告自己的窗口大小所以接收方一旦为0 了发送方就可以等一会然后给对方发一个窗口探测的报文这个窗口探测就是携带PSH的普通报头一旦对方应答就会告诉我窗口大小如果此时对方给我通告的窗口大小还是为0那么此时发送方只能定期的轮询式的向对方发送窗口探测。除了发送方主动问对方有没有窗口更新之外接收方也可以主动进行。比如接收方上层拿走数据了腾出来2000字节的空间所以接收方就立马可以向发送方发送一个报文(不携带数据)告诉发送方我的窗口大小更新了你向我发消息吧。所以一旦接收方的窗口大小为0发送方在等着发接收方在等上层取数据等的时候接收方要通告窗口的更新情况。一种就是发送方定期轮询一种是接收方更新了进行主动通知。在TCP中这两种策略都会被使用。 总结  接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息; 那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么? 实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位  拥塞控制 我们之前介绍的全部考虑的是两台主机的问题我们并没有考虑过中间网络的问题。很多时候压根并不是两台主机的问题而是中间网络的问题而导致数据传送出现一些奇怪现象。我们以前的所有机制都是考虑了发送方和接收方的问题。其实TCP也考虑了网络的问题。 如果我给你发了1000个报文我只丢了1~2个作为我来讲我认为这是正常的在正常的情况下要么超时重传要么就快重传如果我发了1000个报文999个报文全部都丢失了作为发送方就是没有收到数据对应的ACK如果此时我所发的数据这么多都丢了那么我应该进行超时重传或者快重传吗 首先当我丢大量报文的时候绝对不是对方主机的问题我发的数据量一定是对方可以接收的数据量对方不可能在自己的机器上把数据直接丢了大概率是在网络中间就把数据丢了。现在的问题就是少量丢包和大量丢包要不要重传的问题。 比如你今天考试参与考试的有300人然后老师阅卷发现有三两个人不及格这当然是正常的作为出题人我也可以认为我出的题特别好挂了课的人就是他们自己的问题。但是如果我阅卷子的时候发现只有三两个人及格了290多个人全部挂了此时就是老师出卷子的水平问题题目出的太难了这时候就是老师的问题。我们发现挂1个和挂200个都是挂有时候就是怪自己有时候就是怪出题人。所以量少和量多是两码事归因的时候就归到了不同人的头上。 同样的今天我给对方发报文丢一两个报文是正常的我给对方重复就行了但是丢了大量的报文我就不应该向对方重发。 背景引入 我们目前学习TCP一直研究的都是一台主机到另外一台主机的情况。可是全世界里的大部分主机用的协议都是TCP也就意味着所有人遇到了TCP中一些问题的话所有主机都要采取相同的策略。比如A主机给B发B给C发....如果每台主机都只丢1~2个报文大家重发就可以。但是如果我发了1000个999个报文都丢了。也就是说如果你发了1000个丢了1~2个可能其他主机也可能是发100个丢1~2个如果你发了1000个900个都丢了那么和你在同一个网段的其他主机就也有可能出现丢失大量报文的情况。 发1000个丢1~2个网络出现问题只是属于偶发性的个别逻辑出问题了但是发1000个丢900个就认为整个网络全部出问题了更重要的是所有的主机都会这么认为如果我们只有快重传或者超市重传所有主机几乎在同一个时候都认为网络有问题了所有主机都准备把自己的900个数据全部发到网络里A主机发B主机也发C主机还发...但是网络已经出现了大面积丢包的问题你还要让所有的主机都进行重传更夸张的是所有主机几乎步调一致的重传那就相当于所有主机进一步把网络冲垮了本来网络缓一缓还能缓过来现在一人给一脚网络就扛不住了。所以一旦识别到有大面积丢包TCP规定立马警惕起来至少规定不要进行重传了你这么认为的时候别的主机也这么认为。换言之少量丢包立即重传大量丢包此时发送端主机就应当等一等让网络缓一缓调整一下所有局域网中的主机都会这么认为此时网络里就不会有新增报文了它就可以让网络自己适应一下自己处理一下。就跟你的电脑卡住了你就不要点来点去而是等一等一样的意思。 像上面这种如果发现大量丢包TCP协议不会立即对数据重传而是等一等这样的机制就叫做拥塞控制。 所以流量控制滑动窗口是为了考虑主机1对1进行数据通信时解决数据通信太快或者太慢的问题。拥塞控制就是控制的网络的一个情况TCP不仅仅考虑了发送和接收方连路上的时间都给你安排好了而且拥塞控制是所有局域网中的主机都要考虑的大家一旦发生网络拥塞大家就有共识大家都要等一等这样就给了网络喘息的时候这就叫做拥塞控制。 所谓的拥塞控制是TCP发现网络拥塞然后尝试去恢复网络状况的一种策略。拥塞控制最重要的是理解不仅仅是你一个人在控制大家都用的是TCP网络溢出影响的是大家大家都会执行拥塞控制这样就形成了一个短暂共识给网络缓冲时间所以在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的。 拥塞窗口 此处引入一个概念程为拥塞窗口 我们实际上在TCP协议里面最重要的窗口有三个滑动窗口接收窗口(报文里面的窗口大小)滑动窗口在我的发送缓冲区里接收窗口指的是对方的接收缓冲区的剩余空间。拥塞窗口是一个对应的描述网络可能会发生拥塞的临界值也就是说拥塞窗口是一个数字。拥塞窗口是用来描述网络状态的一个概念。所以我们的窗口依次对应的是发送方接收方网络。 发送开始的时候, 定义拥塞窗口大小为1; 每次收到一个ACK应答, 拥塞窗口加1; 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口; 拥塞窗口其实就是一个数字。 比如 int win4096;意思就是主机A你如果一次向网络里塞大量的数据到对方的接受缓冲区当中那么你一次塞的数据量超过拥塞窗口4096就有可能引起网络拥塞所以你发送的数据总量要限制在4096以内。限制发送放向当前的网络中发送数据的最大值这个窗口就叫做拥塞窗口。 之前不是说滑动窗口是我向网络里塞数据一次可以塞多少数据而暂时可以不用应答的这样的一个范围吗滑动窗口不是说好的是由对方的窗口大小也就是接收能力决定的吗 是的这都没错这是因为我们之前只考虑主机B不考虑网络但今天TCP是考虑到网络的所以实际上滑动窗口发送的数据量拥塞窗口和对方的窗口大小中的较小值。所以一个主机能向网络中发送的数据总量(防止拥塞一次最多塞多少呢)是拥塞窗口和对方接收缓冲区的接收能力当中的较小值决定的。 所以实际上作为发送方向网络中发消息的时候既要考虑网络拥塞的问题还要考虑对方接收能力的问题也就是发送方既要考虑流量控制的问题也要考虑网络拥塞的问题。 网络拥塞了TCP该怎么办 虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题. 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的。TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;慢启动 只是指初使时慢, 但是增长速度非常快. 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍. 此处引入一个叫做慢启动的阈值 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长当TCP开始启动的时候, 慢启动阈值等于窗口最大值; 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1; 为什么慢启动前期使用指数增长呢 一旦网络拥塞了我们发一个报文确认网络的健康状态对方给我应答了说明现在网络OK了第二次我发2个还是OK其中对我们来讲一旦前面1~2次2~3次发现此时网络已经能够正常通信了能够给我进行ACK了这个时候我们已经不在需要探测网络的健康了而是要尽快的恢复网络的状态。 什么叫慢启动呢 类比生活在旧社会有一些佃农(丧失土地的农民)只能通过租赁地主的土地来存活租赁的话就必须付房租或者付所谓的地租有时候碰到灾年荒年就没有钱去偿还。现在有一个地主地主就跑过去找可怜的佃农老张就说“你欠我的钱什么时候还呢”老张说“我实在是没钱付不起地租”。地主又说“这样把你欠我的钱不用还了今天你给我1粒米第二个给我2粒米第三天给我4粒米依次类推每一天都是前一天的两倍然后你给我一个月这样的话地租我就不要了”。佃农老张一听挺划算的就欣然同意了同意之后就正常履行偿还的约定但是当老张还到10几20天的时候就发现不对劲了发现越靠后面就越还不起了。当初地主提出这样的要求的时候老张为什么同意呢因为老张看中的是前期还的少后期的帐他算不来他可能就同意了。 这种还米粒的方式就是指数级增长指数级增长最明显的特点就是前期慢一旦过了某个时间点它就变得非常快所以指数级增长发送报文探测就叫做慢启动慢启动就是前期慢而一旦前期慢了。也符合我们前期需要发送少量报文的需求可一旦前面的3~4次经过指数级探测发现都有应答说明网络已经就绪转准备好了我们不应该再慢下去而是快速的恢复过来。所以指数增长前期慢后期快即保证前期不要把网络压垮又保证了检测网络没有问题我们就应该尽快让网络状况进行恢复这就是采用指数级增长的根本原因。这就叫慢启动策略。 可是如果我们一直指数增长下去这个窗口大小就可以在短期之内一瞬间上升的非常大那么这个拥塞窗口就没有意义了所以只要通信过程在一直正常进行着我们就可以在指数增长的一定程度就不要让他指数增长了而是让他由指数变成线性增长这个从指数到线性增长的过程就叫做指数到线性的一个临界值也就是线性增长的阈值。 窗口大小即便是线性增长它也在不断的增长增长的过程是不断的以动态的方式在尝试网络发生拥塞的阈值问题。也就是网络是动态的它的拥塞窗口大小是不一定的它用指数级增长进行前期探测中期恢复后期用来探测下一次的窗口大小有多大。然后一旦发生拥塞此时我们就立马重新开始慢启动。 比如刚开始拥塞窗口大小24我一次可向网络里发24个报文一旦我发现有很多个报文丢了我立马进行慢启动把发送的24立马降到1然后重新开始指数增长在我重新开始指数增长时我在24这个点发送网络拥塞的同时也探测到了当前次网络拥塞的阈值问题发到24网络就发生问题了。然后我们重新调整指数到线性增长的阈值这个新的阈值就成为网络里拥塞窗口的一半(指数增长到线性增长它是在一半处进行切换的)。所以当我们发生网络拥塞我立马在进行慢启动指数级前期探测中期恢复然后到了旧的阈值的一半时切换成线性增长在继续探测当前次新的网络拥塞时它的一个对应阈值情况一旦再次拥塞。继续回复到最开始再次执行以上流程。不断进行重复。                      网络发生拥塞在主机看来是必然的因为我一直在指数增长而我这个必然是故意为之的因为网络是变化的它什么时候拥塞时是不可预测的所以你只能通过不断的去尝试检测出当前次的拥塞用当前的拥塞来指导接下来的行为。所以对我们来讲就是一旦发生拥塞就回到慢启动指数增长快速恢复恢复之后正常通信通信时一旦发生拥塞在循环。这就是网络发生数据的真实情况。 我们最终的结论所谓的拥塞窗口是被尝试出来的是不断的经过指数增长线性增长尝试出来的然后一旦发生拥塞此时我们就执行慢启动。这个过程就叫做拥塞控制。实际上这个拥塞控制就是一个策略(就是用两个临界值约束我们的发送行为的是由TCP自己控制的)。  那网络的拥塞窗口变来变去的是不是会造成网络的数据量一会升一会降 不用担心正常通信的时候传输轮次是可以非常多的当你增长到一定程度的时候拥塞窗口不断增大可是你这个主机发送数据的总量不是只看拥塞窗口的还有对方的接收能力当对方的接接收能力比较稳定你发送数据量以对方接收能力做处理的话此时网络状况也就不会出现丢包问题只要不出现丢包问题网络就可以一直放数据拥塞窗口大小也会一直在增长。另外网络发送拥塞时随时随地都有可能发生的。在TCP看来如果不发生网络拥塞的话拥塞窗口大小也可以不用变化。这个拥塞窗口就是被探测出来的结果因为网络是变化的。            延迟应答 如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小. 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K; 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了; 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来; 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M; 一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率; 那么所有的包都可以延迟应答么? 肯定也不是; 比如我这个报文的超时时间特别短那么这个延迟就可能出问题不是所有的报文都可以延迟应答的。比如我处理数据的速度本来就慢上层取数据的时间非常慢那么延迟应答的作用就不明显如果上层取数据特别快那么延迟应答的效率就特别高。 假设今天适用于延迟应答延迟应答有哪些策略呢 数量限制: 每隔N个包就应答一次; 时间限制: 超过最大延迟时间就应答一次; 具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms; eg每两个报文给对方一次ACK  捎带应答  本来我给你发个消息你要立马给我ACK可是我在给你ACK的时候只是一个裸的报文 (也就是TCP报头没有数据)实际上我也可以给你ACK的同时给这个报文携带上数据也就是你想给我发数据我也想给你发数据。 我给你发数据的时候我把我的ACK位置为1表示对你报文的确认同时我这个报文还携带了有效数据这就叫做捎带应答。 捎带应答实际上就已经是我们TCP最真实的通信场景了。意思就是说我们可不仅仅是发一个你给我应答发一个你给我应答后来我们升级成了一次发很多你给我应答现在我们又知道了你一次给我发很多我给你每个报文或者是延迟应答的报文进行确认确认的同时我也给你的确认报文里面可以在携带所谓的有效数据。所以我们一般在TCP的通信场景里面TCP的所有报文是既有可能对某台主机的数据只做确认又有可能既有确认又携带上数据。正常的通信场景下主机A和B大部分情况下是既有ACK又携带数据除非是单向的主机A只给主机B发消息那么主机B就只能ACK没有数据或者是在某些特殊情况下不携带数据比如说握手和挥手的时候此时就是纯的ACK否则大部分情况下都会携带ACK和数据。这就是为什么很多网络教材中会说几乎所有报文的ACK都被置1了因为你给我发报文的时候同时也是上一个报文的确认。类比生活着理解捎带应答比如说我问你吃了吗我首先确认的是我这句话你听到了。你说“嗯”。然后你可能也想同步的跟我说话“那你吃了吗”所以我们就可以把“嗯”和“那你吃了吗”压缩成一句话就是“吃了吗”“嗯那你吃了吗” 此时对我们来讲当我收到“嗯那你吃了吗”的时候我识别到“嗯”我就认为这是我的ACK“那你吃了吗”就是你问我的。所以我给你发的消息就是既有确认又有我给你发的消息。重新认识3次握手 3次握手实际上应该是4次握手。因为主机A向主机B发送SYN的时候主机B应该做的其实是ACK这个意思就是说主机A问主机B你能不能给我建立一下连接呢主机B说好的。因为TCP是全双工的所以主机B也要给主机A发SYN主机A给给B发ACK意思就是说主机B问主机A我能不能和你建立连接呢主机A说好的。所以3次握手本质上是4次握手和4次挥手一样是为了让双方完成共识的。我们看见的是3次握手实际上就是因为主机B将ACK和SYN捎带应答了。一次用一个报文表达了两个含义。面向字节流 什么叫做字节流 所谓的文件流本质上就是数据流或者字节流。 什么叫做流呢  创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区; 调用write时, 数据会先写入发送缓冲区中; 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出; 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去; 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区; 然后应用程序可以调用read从接收缓冲区拿数据; 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工 由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如: UDP是你要发10个报文对方必须收10个不能收9个或者8个我发10个你就必须收10个报文和报文之间的收发是严格匹配的这叫做用户数据报。对于TCP实际上因为缓冲区的存在应用层拷贝下的数据可能被当做10次发送了也可能应用层拷贝了10次的数据被TCP一次就发走了此时这一种发次数和取次数根本毫无关系或者不是一一匹配的这种特性就叫做字节流。写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节; 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次; 字节流说白了就是数据之间不像UDP报文和报文之间必须有明显的格式发了一个报文对方必须收完整的报文就如同生活中的寄信。而流呢意思就是数据本身并不需要边界你要写就往里面扔。你要读就直接向里面拿。 比如:家里都有水龙头你可以去拿水杯接水也可以去拿水桶接水可以拿脸盆去接你完全是按照你的要求去从水管里面把水拿出来但是你知不知道这个水是怎么来的呢这个水是水电站的工作人员一桶一桶给你打上来的还是电机抽上来的早上抽的还是晚上抽的这水在抽的时候是10ml抽还是10L抽....你完全不关心你只需要知道水龙头一拧开就有水(数据)你需要多少水(数据)你就拿多少水(数据)。这个数据拿多少怎么拿完全由上层决定。这个就叫做字节流。 回忆http http请求是以行位单位陈列的但是这仅仅是你在应用层这么认为一旦自己发送的时候到了TCP的缓冲区里实际上是把http请求拷贝到发送缓冲区里面了当你在把这些数据拷贝到发送缓冲区里面的时候你调用send/write内核有没有把数据发出去呢 答案是不一定有可能你调用send/write 你一直在拷贝可是网络发生了拥塞它并不发所以你把数据拷贝下来此时这个时候你的http请求全部是以字节流的形式在TCP的发送缓冲区里面存在。接下来TCP发送的时候TCP并不会说这个是请求行这个是第一个属性...TCP连管都不管只要现在能发500个字节了就把前500个字节发出去这500个可能是第一行可能是前3行可能是前3行第4行的部分内容...总之TCP完全不关心你的协议是按行发还是怎么样只关心我能发多少字节我就是按字节发。此时对于TCP来讲这个缓冲区就叫做字节流。同样的接收方在收到这个数据时也不会认为这是http协议你发过来的不完整我不收了而是发送方发多少我就收多少之后接受缓冲区里的数据是按照什么特定方式被读取就是由应用层决定。在TCP双方就如同流水一般直接把数据从一端倒到另外一端。至于上层想要拿杯子接受桶接收都是应用层的事。反正TCP只要保证应用层只要读我就有数据你打开水龙头我就有数据这就可以了这就叫做字节流。 为什么打开文件叫做文件流 OS不知道你这个文件是什么文件里的内容保存的是视频还是音频还是代码...OS完全不关心这个文件里有什么是你关心的所以你打开一个.txt就喜欢用记事本打开打开一个视频永远是播放器打开因为只有用户知道这个文件是干啥的在OS看来我就认为这个文件里全部放的都是字节流也就是OS对文件内容不做任何解释我只关心你要读几个字节我给你读上来就行。至于你应用层想怎么解释这些个字节由你的应用层去定。所以曾经读写文件都叫做打开流因为这是OS的视角。用户的视角就叫做打开配置文件打开源代码。 粘包问题 我们发送给对方http的时候呢在接受缓冲区里可能有很多报文报文和报文都挨着按照正常的协议请求你把content-length读到正文读了下一次读就是下一个报文的开始。可是如果content-length填错了呢此时上层要么多读要么少读一定会导致这个报文要么一部分被丢了要么一部分被别人读走了。这就叫做粘包问题。所以单纯的TCP的存在是会存在粘包问题的所以TCP要不要解决这个问题呢 答案是TCP根本没有能力去解决这个问题因为TCP是面向字节流的解决粘包问题是应用层要解决的应用层要定制协议然后根据协议从TCP当中将数据进行取走。  我们以http为例如果我们要读取这一个数据我们就不能简单粗暴的去做如下操作 此时我们就简单粗暴的把一次读取到的内容全部当成一个完整请求这是不正确的。今天我们读的时候就可以换成这种方式 这样的话就能正常把一个数据读上来了虽然这样还有问题但是就能说明粘包问题了。报头读完在根据content-length读取正文一个报文就读完了下次在继续重复这样的操作就能解决粘包问题了能解决就是因为应用层协议给我们规定好了。 所以协议不仅仅规定每个字段是什么含义它还要能规定我们双方通信时如何正确读取的问题所以解决粘包问题就是TCP的报文我们约定好采用定长字段比如我每次给你发报文都发1024个字节这样的话就不会存在粘包问题了。还有就是特殊字符就能区分一个报文是否结束假设我给你发的所有文字都以空行作为分隔符所以你读的时候就按行读读取到一行就是我要给你说的话。当然也可以像http一样做描述字段比如先发4字节就代表了后面的长度然后长度表明是100字节我就给你发100字节最后先读4字节在根据4字节决定你下来要读多少这就叫做协议。 所以协议不仅仅像http这般告诉我们每个字段的含义还有一个隐形的协议就是协议的报文也是存在格式的协议所有报文的格式都是为了方便读取在今天看来是为了避免数据粘包问题。 因为TCP是面向字节流的所以它的报文和报文之间是没有边界的数据一旦放进缓冲区里大家就揉在一起了同一个东西在不同的视角看待方式是不一样的所以因为TCP是字节流的所以TCP收到数据之后数据本身的解释权不由TCP解释TCP只是个发数据的有数据就发数据报文里面是什么东西不是TCP该关心的所以粘包问题是由应用层解决的。HTTP协议报文的请求格式就是为了方便对方读取也就是为了解决报文和报文之间的粘包问题。 生活中例子比如你妈蒸包子刚蒸出来包子就是粘在一起的如果刚出炉直接上手拿一个包子就有可能这个包子就带着其他包子上来了要么这个包子就拿到了一半甚至你只拿了个皮。这就是粘包。所以一般爸爸妈妈的做法是蒸出来之后先不拿而是把包子进行分开包子和包子之间分开包子和底之间分开分开之后在晾一下你在吃。此时把包子和包子分开就叫做分离包子和包子的边界。从而让你下一次拿的时候不会因为一个包子而影响另外一个包子这就叫做解决粘包问题。  TCP异常情况  对我们来讲如果此时我们曾经创建好的连接如果出现了问题该怎么办呢 比如进程终止了曾经建立好的连接会怎么样呢 所谓的连接双方通信在应用层就是文件。PS: linux和windows版的套接字编程只有3行代码不一样无非就是头文件包含引入socket库还有打开一个对应的套接字资源这三步不一样其他的几乎全部一样因为大家用的都是TCP协议所以OS的接口也肯定是一样的(这可不是由OS决定的是由TCP/IP协议去统一规定的)。所以你想写一个linux和windows通信的程序成本也很低。 在应用层连接就是文件描述符就是文件所以如果我已经建立好连接突然连接终止了甚至这个终止是被我kill掉的或者是自己挂掉的那么进程终止就会释放文件描述符和正常关闭没什么区别。文件的生命周期是随进程的意思就是你打开一个文件如果你的进程突然异常或者正常崩溃了其中你的文件描述符自动就关闭了所以你不用担心文件描述符被关闭的问题网络也是一样。网络通信的时候打开的是文件也是以文件的方式来进行操作的所以一旦进程终止了实际上OS会自动回收这个进程打开的文件资源文件描述符也会自动关这是文件。文件的生命周期是随进程的在网络里实际上也是类似的所以一旦你的进程出现了终止或者崩溃OS被关掉在底层对应的就是它会自动4次挥手。所以进程一旦终止底层会自动进行4次挥手比如说客户端连上服务器然后直接把服务器给退出此时连接还在但是服务器没了所以服务器的连接状态由listen直接就变成CLOSE_WAIT如果进程终止底层没有进行4次挥手怎么可能进入CLOSE_WAIT呢。连接一旦建立好进程终止这个连接在底层OS自动4次挥手这也是通信细节。 如果我在电脑上建立好大量的连接突然我的机器直接重启了会怎么样呢 当你的电脑进行重启时如果有些进程是打开的你就会发现windows就会提醒你这个文件正要被关闭请问是否要进行保存你点击是就给你保存了然后进行关机。所以一个进程要关机时要把用户进程先关掉。所以当你重启前电脑上建立着大量的连接TCP协议在你机器底层就会先进行各种4次挥手4次挥手把连接断开之后然后在完成正常的关机动作。 如果机器掉电或者网络断开了会怎么样呢 这个时候对端没有办法知道你的网络状态因为这种情况属于OS直接挂掉了就是一瞬间的事4次挥手是要花很长时间的一瞬间就挂掉了它连发送断开连接的请求都没时间。你就没办法和对方正常通信了交互没有了随以对方也就不可能知道了所以此时你突然掉电或者网络断开对方是不可能知道你的信息的对方不可能知道你的信息客户端该咋样咋样服务端就只能超时一段时间就比如这个连接怎么挂上我长时间不给我反馈呢它还会定期问一问你还在不在。只要客户端确认就认为你还在。再比如我是个服务你是个连接连接早没了一旦服务端询问客户端客户端就发现你怎么还给我发消息我们的连接不是早就关闭了吗此时客户端就可以给服务器发送RST报文服务器立马就意识到连接造没了此时就把连接RST就是把连接释放了。  RST标志位就是用来处理这些特殊情况像进程终止网线断开机器掉电这种情况你压根就不知道包括路由器出问题了...所以网络里一定要存在对连接重置的一个标志位就是RST。 eg:如我们玩一些在线对战游戏我不想让官方扣我的信誉积分我就不采取逃跑的方式而是采用拔网线的方式。把网线一拔系统就会判断掉线了你的客户端就不会给服务器发任何数据。 所以网络出问题服务器是不知道你出现什么问题所以你不能扣我分这个是不可抗拒的因素。如果你不想打游戏直接强制退出你用的就是客户端退出就是客户端在给服务器发消息服务器就可以收集你退出的行为它就知道你是逃跑的。但如果拔了网线连接断开的数据都发不过去所以正常情况下你自己的退出信息也发不出去。所以当你不想玩游戏了你直接一拔网线然后在退出客户端一般服务器不怎么强的服务就判定你是网线掉了也就不扣你的分了。  另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.TCP小结 为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能. 可靠性: 校验和 序列号(按序到达) 确认应答 超时重发 连接管理 流量控制 拥塞控制 提高性能: 滑动窗口 快速重传 延迟应答 捎带应答 其他: 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)基于TCP应用层协议 HTTP HTTPS SSH Telnet FTP SMTP 当然, 也包括你自己写TCP程序时自定义的应用层协议 TCP/UDP对比 TCP和UDP在传输领域就是两个极端要么非常可靠要么就不关心可靠性。一旦两种技术比较极端就意味着它的特点非常的明显世界上没有绝对好和绝对坏的事情。特征明显更容易被人选中。 TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;比如请求http协议请求网页请求上传下载数据包括ssh登录linux因为这些数据一个也不能丢。就比如你用linux命令(如果是UDP)你输入ls对方收到了l完全就是糟糕的用户体验。UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输(直播)等. 另外UDP可以用于广播;比如直播udp把报文发送到服务端服务端收到之后就把直播者的图形画面声音统一广播给大家就相当于把直播者的声音图形数据打上udp报文然后把报文不断的经过直播软件的服务端直播平台的服务端收到直播者的报文然后把报文做一个转发每个想看直播的人都得登录直播软件的客户端这就是看直播的人拿着自己的客户端和直播软件通信直播者自己的客户端和直播软件通信直播者的客户端是上传端观看直播的客户端是下载端所以此时就可以不断的进行直播了。再比如看视频画质选择自动就是UDP。 看直播延迟是怎么做到的呢 直播者的数据是直接上传到了直播软件的平台中观看直播的人看到的直播内容是直播软件把直播者曾经上传的数据给推送过去延迟就是推送的晚一些推送的晚一些还要推送就是它把直播者可能有3~5秒的数据可能缓存了起来缓存一下再推送给大家这就是有延迟的一个原因。 为什么要这么干呢 如果今天它上传上去的数据直接就立马推给你对服务端的配置要求是特别高的因为实时性要求就高了你可以简单的理解成它必须把数据上传上来的同时也必须立马把数据发送出去而且也没有办法对直播者说的内容有相关方面的侦测比如说的话可能是一些违法违纪的内容缓存过后就可以有充足的时间对直播者说的内容做进行文本识别。而且带一个缓存可以让直播平台的容错率更高比如如果今天不缓存我给你发送的消息必须全部都得立马转过来如果今天在直播平台的人直播的人太多那么这个平台就忙不过来了但是如果今天有缓存我只负责把数据上传上去然后我不着急给你推送我就可以给每个人维护一点缓存数据这样的话平台就可以更从容一些直播平台压力比较大就可以慢慢的推送数据直播平台压力比较小就可以快一点推送数据这也就是每次看直播它的延迟情况都是不一样的。 网络通信都不允许丢包吗 并不是所有的网络通信都不允许丢包的以直播为例选择UDP哪里网络不好了就动态调整发送数据量就可以做到比较高效稳定。所以如果在用户角度我如果用TCP我这里是高清的你必须也收到高清的如果网络状况有差别别人观看正常你观看就十分卡顿这样的产品体验反而不好UDP就是你网络好看到的就是高清的网络不好看到的模糊点。虽然会有观看体验的起伏但是不会卡顿这就是UDP。归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定.用UDP实现可靠传输 参考TCP的可靠性机制, 在应用层实现类似的逻辑;我想使用UDP实现可靠性一定是结合场景去谈的要不然我直接用TCP可以了我只是想实现一个轻量化的可以引入部分的TCP策略。比如说QQ聊天用UDP实现可靠性我们用UDP就是存在丢包问题实际上我们只需要保证数据能进行确认应答能够进行按序到达能够进行重传就可以了。又不是大文本大内容就是一些很小的报文。例如: 引入序列号, 保证数据顺序; 引入确认应答, 确保对端收到了数据; 引入超时重传, 如果隔一段时间没有应答, 就重发数据; TCP 相关实验 理解 listen 的第二个参数 Sock.hpp #pragma once#includeiostream #includestring #includecstring #includecstdlib #includesys/socket.h #includenetinet/in.h #includearpa/inet.h #includeunistd.husing namespace std; class Sock { public:static int Socket(){int sock socket(AF_INET, SOCK_STREAM, 0);if (sock 0){cerr socket error endl;exit(2); //直接终止进程}int opt 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); return sock;}static void Bind(int sock,uint16_t port){struct sockaddr_in local;local.sin_family AF_INET;local.sin_port htons(port);local.sin_addr.s_addr INADDR_ANY;if (bind(sock, (struct sockaddr *)local, sizeof(local)) 0){cerrbind error!endl;exit(3);}}static void Listen(int sock){if (listen(sock, 5) 0){cerr listen error ! endl;exit(4);}}static int Accept(int sock){struct sockaddr_in peer; //对端的信息socklen_t len sizeof(peer);int fd accept(sock, (struct sockaddr *)peer, len);if (fd 0){return fd;}return -1;}static void Connect(int sock, std::string ip, uint16_t port){struct sockaddr_in server;memset(server, 0, sizeof(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){cout Connect Success! endl;}else{cout Connect Failed! endl;exit(5);}} };之前我们的listen第二个参数一直是5现在我们将5改成1。 Http.cc #includeSock.hpp #includepthread.h #includesys/types.h #includesys/stat.h #includeunistd.h #includefstreamvoid Usage(std::string proc) {std::cout Usage: proc port std::endl; }int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}uint16_t port atoi(argv[1]);int listen_sock Sock::Socket();Sock::Bind(listen_sock, port);Sock::Listen(listen_sock); //只允许别人连我我不获取它for ( ; ; ){sleep(1);} }一启动服务 二建立第1个连接   三建立第2个连接   四建立第3个连接  五建立第4个连接从此次开始就不会在有新的连接 上层不进行accept底层是可以把连接建立好的一旦把连接建立好上层accept就可以直接把连接取走底层给我建立好连接让我去accept的个数是受限制的不是所有连我的连接都可能会ESTABLISHED建立成功 因为我们的套接字代码没有accept所有底层建立好若干个ESTABLISHED再来的就叫做SYN_RECV(这个状态是只要客户端连上我给我发了SYN我的状态就叫做SYN_RECV我也会维持这个状态)意思就是说它当前并不认为3次握手完成了而是不在继续进行3次握手了相当于我们服务端因为某种原因而限制了我们只能建立2个已经3次握手成功的连接。一旦太多了服务器就不让你连了而是只让你维持一个SYN_RECV状态3次握手没有完成也不给你完成。其中这里我们限制底层在任何一个时刻最多能够进行建立连接成功的个数就叫做listen的第二个参数。 listen的第二个参数1描述的就是在TCP层建立正常连接的个数。注意这个建立正常连接的个数不是说只能在服务器端维护几个连接。你在服务器端可以accept随便拿。但是只要一个连接已将被建立好且没有被拿走此时它的个数就由listen的第二个参数进行维护。所以我们在底层维护好的这一个能够被上层随时读走的这个一个东西我们就称为全连接队列accpetd队列用来保存处于established状态但是应用层没有调用accept取走的请求而全连接队列的长度会受到 listen 第二个参数的影响. 全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了. 这个队列的长度通过上述实验可知, 是 listen 的第二个参数 1.为什么要进行1呢 因为这个队列的长度它最少是个1如果是0的话你就不用进行维护了listen的第二个参数又是一个整数如果用户传0我们就可以通过1的方式至少保证它是1。 别人在连接的时候你可以通过这1次至少可以让服务冗余出一个被上层读的一个连接。 但我也可以让这个参数限制为至少是1但是这样的话你得让用户知道而且listen的第二个参数是会受TCP协议影响的默认与你的设置有关也有其他策略保证队列长度。 我们还能发现如果在有新的连接来它不是ESTABLISHED而是SYN_RECV也就是客户端只给我发了个SYN服务端收到SYN后不给客户端应答了暂时把他吊着一旦上层连接被取走一旦全连接空了我在把他完成3次握手连接建立好在放进全连接队列里面。也就说还有一个半链接队列用来保存处于SYN_SENT和SYN_RECV状态的请求说人话就是这个半连接队列用来维护一些处于3次握手过程之中的一些连接。对我们来讲只有你握手成功了你才有可能进入全连接队列。即便是你3次握手成功了它也不一定会把你的建立成功的连接放到你的全连接队列里。这个半连接队列一旦握手成功它内部还有一些安全策略重点是为了主要能够保证我们的连接是安全和健康的。 半连接队列是如何移到全连接队列的 半连接队列在进行握手的时候会有一些随机数策略保证3次握手来自同一个客户端请求或者来自同一个客户端的合法请求只有当你安全认证通过或者安全认证策略通过这个时候你建立好的连接才会放到全连接队列里。 那我作为一个服务器有人不攻击我的全连接队列而是攻击我的半连接队列怎么办 因为SYN洪水我只给你发SYN的话其中大量的连接此时都处于半连接状态当然你是半连接队列你是有长度的这个半连接队列的长度完全是由OS去进行设置的(这个算法很复杂主要是为了考虑安全性的问题)全连接队列长度由listen第二个参数指定。比如对于半连接队列我今天给你发SYN服务端就处于SYN_RECV状态此后我客户端就不给服务端发任何ACK了即便服务端给客户端发SYNACK我客户端也不应答就相当于半连接来的请求就在你这个半连接队列上挂着挂着之后我发送大量的SYN之后最后这个半连接队列就会被打满然后你的服务端最后在发SYNACK客户端也不响应你最后这个半连接队列就被客户端占着这个客户端也不走服务端你只能是超时把它给关掉可是你一关掉又一个客户端立马就连你了依次循序始终占着你的半连接队列就会导致正常客户半连接队列连不上那么全连接队列也就进不来。所以恶意分子照样可以通过攻击我的半连接队列来攻击我的TCP请求。 类比生活就好比我是一个餐厅老板我的竞争对手雇了一群大爷大妈来我的餐厅他们也不吃饭就是坐在那我一赶他走就躺下了正常来的客人来了也没地方坐所以他们就占着我的资源也不应答我所以就起到了攻击我的效果。 TCP针对这种攻击也有自己的策略在这个半连接队列里还有一个队列你们必须先连接那个队列只有那个连接队列审核通过了你们才能连接半连接队列。其实在握手期间服务器还有一大套的算法比如用生成随机数的方式进行验证意识到你是正常客户端时候才会把你放到来接队列里。    为什么要维护队列为什么这个队列不能太长为什么这个队列不能没有 我们今天谈论的是全连接队列。 我们以海底捞为例海底捞里面会有很多人在吃饭在海底捞门口有一些对应的过道过道里面陆陆续续也会有新的人进来想去海底捞里吃饭比如说你和你的朋友过去了工作人员看到你过来了就会和你说先生您好要过来吃饭吗你说是的。服务员就告诉你不好意思目前店里面已经坐满了如果你要吃饭就得等一等可是等待的时候不能让人家站着如果让客人站在等的话人家不可能一等就等半个小时客人直接就走了所以一般海底捞门口就会在自己的门口摆上很多桌椅对应的工作人员就会给你和你的朋友一个吃饭的号码让你和你的朋友去休息区等。 假设海底捞门口就没有桌椅当你和你朋友到的时候服务员告诉你们要进行等待可是你看到都没一个进行休息等待的地方所以大概率就都走了虽然陆陆续续来了很多人想在这吃放但是因为都要站着等所以很多人就都走了。其中恰好有1~2桌客人吃完饭了恰好目前又没有新的客人来所以就导致海底捞里面的桌椅被空上了10~20分钟。假设每桌平均消费500这样的场景每天被复现了5次复现了5张桌子出现了20分钟的空档期假设平均20分钟一张桌子能赚100块那么这样的话每天就少挣5次就是500块如果这个店有1000家分布全国每一个店少赚500块1000个店1天就少赚50万...仅仅是这一个小细节没有做到位里面客人吃完饭走了导致里面的桌子没有被充分利用进而导致海底捞这个企业每年可能少赚很多钱。所以老板就规定必须让门口摆上桌椅板凳设置休息区此时客人排队的概率就大大增加了。 为什么要维护门口的桌椅板凳让客人可以等待 此时当有客人离桌的时候服务员就可以立马让在外面等的客人立马填充进来就可以保证海底捞里面所有的就餐桌椅始终是被100%利用的这样的话也就不会造成海底捞内部各个桌椅资源的浪费。 作为海底捞的员工给客人发放取餐号码的时候就是在给你们排队谁先谁后让你们去等就是让客人坐在休息区去等有人愿意等同样有人也不愿意等。所以门口的桌椅板凳(其实就是队列)最大的意义是当海底捞内部满了的时候我门一旦有人离开就可以立马把在外部等待的客人接进来。这样做就可以保证海底捞内部始终是资源被100%利用的这就是队列存在的意义。如果这个队列被坐满了再来客人呢此时你只能让这批客人流失了没有办法。 那么队列又这样的好处的话我为什么不把这个队列搞的多一些就相当于我把门口的桌椅板凳摆上非常多这样可以是可以但是一旦队列太长也就丧失了队列的意义队列太长的话你得考虑客户的耐心一旦客户发现有成百上千的人在进行排对轮到他的时候都到了半夜1:00这个客人肯定不吃了所以在排队的话门口的队列太长是没有意义的。而且桌椅板凳都要钱再者为什么老板不把海底捞服务的范围扩大而是要扩大休息区呢。 所以这个队列不能没有就是因为如果没有队列就会有种风险当客人离桌时这个资源不能立马被使用。 这个队列不能太长是因为维护队列是要有成本的如果你把队列维护的过长那么对应尾部若干的等待的客户是没有意义的因为等待的时间太久了客户体验非常不好这部分客户最终肯定会流失。与其把队列维护的很长倒不如把维护队列的成本砍掉嫁接到服务上让服务能够提供更多的桌椅板凳。 这个海底捞就相当于是我们自己写的网络服务器提供某种网络服务一张张桌椅板凳就是文件描述符或者是内存资源然后这个在门口叫号的服务员就是listen套接字在海底捞门口排队的队列就相当于是全连接队列。当有新连接到了的时候为什么要维护全连接这个全连接不能没有就是因为有可能上层的服务太忙了已经将服务打满了来不及接收新的客户我们只能让客户暂时在我们的底层先将队列维护好处于ESTABLISHED状态当上层调用accept的时候就是把这个客户唤入到我们的服务内部。所以这个队列必须有如果没有就可能导致内部服务资源没有被充分利用。又因为维护队列是有成本的维护长队列当然可以但是客户一旦连接上你长时间没反应他觉得太慢了所以就直接把把网页或者连接关闭了此时你维护也没有意义倒不如你维护一个短队列把不维护长队列的资源节省出来供服务去使用这样就可以让服务以较高的效率给用户提供服务。所以我们维护的全连接队列是一个短队列。
http://www.ho-use.cn/article/10821048.html

相关文章:

  • 手机网站关键词优化西安网站设计建设公司
  • 网站都有什么功能wordpress 登陆验证码
  • 网站开发属于什么会计科目国外一些建筑公司网站
  • 高端网站制作多少钱做同款的网站
  • 北京网站建设 知乎网站建设的要点是什么
  • 地方门户网站赚钱吗河北省最新任免
  • 如何查看一个网站是否备案设计logo素材
  • 福州企业免费建站企业展示型网站程序
  • 上饶网站制作深圳网站建设好不好
  • 网站添加百度商桥医院门户网站建设规划
  • 网站内容是什么浦东网站建设价格
  • 消防网站模板wordpress时间
  • c 做网站看什么书邢台123信息港
  • 个人网站做什么内容好关键词研究工具
  • 百度给做网站公司苏州自助模板建站
  • 上海网站 建设wordpress调整logo大小
  • 网站客户留言网站运营者是做啥工作的
  • 做兼职一般去哪个网站WordPress副标题不显示
  • 昆明做网站建设的公司排名ss永久免费服务器
  • 建设公司网站的背景意义重庆建筑信息网官网
  • 美橙互联 送网站运输网站建设
  • 定制网站建设服务平台外包app开发价格表
  • wordpress nginx安装怎么做公司网站seo
  • 做网站千篇一律地方网站做哪些内容
  • 搭积木建网站软件郑州+高端网站建设
  • 开平 做一网站安卓android下载安装
  • aspx网站做app138企业邮箱登录
  • 石家庄建设路网站网站策划与制作
  • 辽宁省建设厅科技中心网站中国廉洁建设网是什么正规网站吗
  • 做网站维护费是怎么算的seo标题优化步骤