安丘做网站的公司,柳林网站建设,商城app有哪些,app公司的组织结构目录
一再谈端口号
1端口号范围划分
2两个问题
3理解进程与端口号的关系
二UDP协议
1格式
2特点
3进一步理解
3.1关于UDP报头
3.2关于报文
4基于UDP的应用层协议
三TCP协议
1格式
2TCP基本通信
2.1关于可靠性
2.2TCP通信模式
3超时重传
4连接管理
4.1建立…目录
一再谈端口号
1端口号范围划分
2两个问题
3理解进程与端口号的关系
二UDP协议
1格式
2特点
3进一步理解
3.1关于UDP报头
3.2关于报文
4基于UDP的应用层协议
三TCP协议
1格式
2TCP基本通信
2.1关于可靠性
2.2TCP通信模式
3超时重传
4连接管理
4.1建立连接
4.2断开连接
5再次理解
5.1建立连接为什么要三次握手
5.2重新理解四次挥手
5.3验证状态
CLOSE_WAIT
TIME_WAIT
6流量控制
7滑动窗口
7.1两个问题
7.2丢包问题
编辑 7.3关于滑动窗口
8拥塞控制
9延迟应答
10捎带应答
11TCP异常问题
12基于 TCP 应用层协议
13TCP小结
四UDP与TCP
1两者的面向问题
UDP面向数据报
TCP面向字节流
2粘包问题
对于UDP来说没有粘包问题
对于TCP来说存在粘包问题
解决粘包问题
扩展理解
3两者的对比
五TCP全连接队列
1listen的第二个参数
2理解全连接队列
3内核层里socket和连接
4使用TCP dump抓包
4.1安装 tcpdump
4.2使用
捕获所有网络接口上传输的 TCP 报文
捕获指定网络接口上的 TCP 报文
捕获特定源或目的 IP 地址的 TCP 报文
捕获特定端口的 TCP 报文
保存捕获的数据包到文件
4.3代码观察现象 一再谈端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序 在 TCP/IP 协议中, 用 源 IP, 源端口号, 目的 IP, 目的端口号, 协议号 这样一个五元组来标识一个通信(可以通过 netstat -n 查看) 1端口号范围划分
0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号 有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号: 服务器端口号ftp21ssh22telnet23http80https443 执行下面的命令, 可以看到知名端口号:
vim /etc/services
我们自己写一个程序使用端口号时, 要避开这些知名端口号
2两个问题
1. 一个进程是否可以 bind 多个端口号? 2. 一个端口号是否可以被多个进程 bind? 端口号是用来标识主机内进程的唯一性所以一个端口号是不能被多个进程bind的 而一个进程可以bind多个端口号(但要避开知名端口号) 3理解进程与端口号的关系 端口号通过传输层要与进程进行配对时通常做法 会通过一个hash表(储存着bind进程与端口号的映射)找到对应的进程 但如果bind进程映射的端口号不是唯一的还有其它bind进程也与该端口号进行关联 端口号在进行配对时就不确定是哪个进程了(hash冲突)所以这样就说明了端口号是不能bind多个进程的 二UDP协议
1格式 理解协议都要来认识它们之间的共性 1如何解包 a提取前8个字节(报头大小) b读取UDP长度用它-8字节看看等不等于0等于0说明没数据 不等于0说明数据大小UPD长度-8字节剩下的都是数据 2.如何分用 报文中有16位目的端口号当主机收到UDP报文时读取源端口后进行查表(bind的进程对应的端口号)找到指定进程再填写端口号就能进行通信了 2特点
UDP传输的过程类似送信的过程 无连接: 知道对端的 IP 和端口号就直接进行传输, 不需要建立连接;不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;面向数据报: 不能够灵活的控制读写数据的次数和数量; 3进一步理解
3.1关于UDP报头
UDP报头在内核中以结构体的方式存在 我们在使用UDP协议时进行反序列化让结构体转成字符串再以网络序列发送出去对方在接收时也是按照这个结构体来进行反序列化这个过程是在内核中规定好了的 如果我们自己在应用层能不能实现类似结构体进行序列化与反序列化 可以的在前面的网络版本计算器中我们就实现过了但是不推荐 双方在进行通信时设备可能完全不一样编写语言不同大小端内存对齐... 而在操作系统内核中能这么干双方内核都是C语言写的不会引进其它新的结构体字段网络序列基本是固定的只要考虑好大小端问题就OK了 3.2关于报文
在OS中可能在某一时间段内有很多的报文一些有可能要向上交付一些要向下交付一些可能发生失败要进行丢弃...而OS要对这些报文进行管理如何管理先描述在组织
描述报文的结构体struct sk_buff 在协议栈中往下交付时其实传的是sk_buff指针*head往左移:添加报文(封包) 往上交付时指针*head往右移删除报文(解包) 4基于UDP的应用层协议
NFS网络文件系统TFTP简单文件传输协议DHCP动态主机配置协议BOOTP启动协议(用于无盘设备启动)DNS域名解析协议
三TCP协议
TCP 全称为 传输控制协议(Transmission Control Protocol)对数据的传输进行一个详细的控制
1格式 学习协议首先要理解 a如何解包 在TCP报头中有4位首部长度单位4字节范围[0,15] -换成字节范围[0,60] 如果报头大小为20字节那么4位首部长度为0101(20/45) 1.提取20字节(报头大小) 2.读取4位首部长度用它 - 报头大小看看等不等于0如果为0则没有选项:剩下的都是数据 不为0则有选项(选项首部长度-报头大小)(假设选项大小为16)提取16字节:剩下的都是数据 b如何分用 发送TCP前我们要先填16位目的端口(发送给谁)对方收到后报头里的16位源端口代表谁发送的读取它并把它往上交付给指定的进程便能进行通信了 2TCP基本通信
2.1关于可靠性
举一个例子当你给朋友发微信你吃了吗过了一会朋友回你还没吃朋友进行了回复就说明他一定是送到了我发给他的消息否则就不会无缘无故会发信息说还没吃
在tcp中也是如此 当client发送报文给server后server有应答那我们就认为serer一定是收到了报文 (但总会有最新的消息还没有应答这个我们不保证:对方收到了应答就说明历史数据被对方收到了
对于clinet来讲 有没有可能收不到应答的情况是有可能的(有可能是发出去的数据丢失也可能是server的应答丢失)但这种情况我们都认为报文丢失了 收到应答100%就认为server收到报文反之报文丢失(后面说)这便是TCP协议的可靠性 TCP也是全双工的那么server到client(反过来)也是保证可靠性的思路与上面类似
2.2TCP通信模式
TCP共有2种通信模式
第一种一个报文一个报文的发送类似你去快递站一个快递拿回家拆完后又去快递站拿 另一种是(在某个时间段)同时发送多个报文同时送到多个应答报文(TCP常用的通信方式) 确认应答中1001数字是填在应答报文的确认序号里即代表1001之前的数据已经全部收到 下次发从1001开始(不一定是1001,还要加上数据长度~) 那这时有人要问了把发送过来的报文里的32位序号修改成1001不就行了吗?为什么要有两个序号 要记住TCP时全双工的server也有可能给client发报文(数据)此时历史数据也要给client发应答报文难道server要发两个报文 server可以在一个报文里填上两个序号来代表我既要给你发数据也要对临时数据作应答这种方式叫做捎带应答提高了发送效率 我们知道发送数据本质是发送到对方的发送缓冲区中我们从逻辑上把缓冲区中的字节流看成是数组的形式(实际比较复杂)那么序列号的填写工作不就是转换成最后一个值的数字下标吗 而在TCP中client发送的报文请求每次可能是不同的有建立连接的发送数据的断开连接的应答的应答发送数据的server怎么知道收到的报文是什么请求(类型)呢 通过报头里的保留(6位)的标记位来进行判断ack标记为1说明该报文时应答报文该报文里有数据说明该报文类型是应答 发送数据类型 3超时重传
共有两种情况主机A发送数据时丢包和主机B应答时丢包这时就会触发TCP的超时重传机制在特定的时间间隔内重新发送数据 但在下面的情况中主机B收到的两个重复报文怎么办 TCP判定两个报文序号一样会去重 那如果主机B收到了很多报文要对报文进行判别那个是先到那个是后到的又怎么办 通过序号的大小进行排序保证按序到达 既然丢包后会超时重传那特定的时间间隔是多久呢 这就要取决于网络状态了网络好时间间隔短网络差时间间隔长在500ms整数倍浮动 如果一直丢包一直重传TCP就会给我们断开连接帮助我们控制成本 4连接管理
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接
但我们在平时写TCP代码是只有listenacceptconnect根本就没有这种感觉这其实是OS在内部帮助我们在做~ 4.1建立连接
以故事引入主题 建立连接这件事就好比男女确定男女关系一样男做我女朋友好吗女好啊那你做我男朋友好吗 男好啊 我们在前面说了TCP只保证历史数据被对方收到了不保证最新的数据被对方收到那么最后一次握手时client给serverACK时不是就可能出现丢包的情况吗怎么解决这个问题 我们先来想下一个问题建立连接一定要成功吗不一定吧 建立连接的本质是在赌最后一个ACK对方一定收到了 如果在最后一次出现丢包了server是不知道这回事的它会以为连接以及建立好了接下来就给server发送数据但server收到数据后在想连接不是还没建立好吗怎么这么快就给我发消息哦我明白了出现丢包了所以server会给client发送报头中把reset标记位置1来告诉server连接异常要进行释放重新建立连接三次握手 4.2断开连接
还是先以故事引入主题 断开连接这件事就好比男女要分手类似女我们分手吧 男好啊 男那我也要给你分手 女好那我们从此就不是一路人了 client给serverFINserverACK这就表明client要给你发的数据已经发完了我要给你断开连接server也给clientFINclientACKserver此时双方之间就完全断开了链接四次挥手完成
但server也可以选择不给clinetFIN你发完了但我要给你发的数据还没完server每给client发时虽然在client那边认为我已经跟server断开连接的但还是会给serverACK应答怎么理解 client给serverFIN说明client要给server发送的用户数据已经发完了关闭sockfd清理发送缓冲区断开‘连接’了但实际上TCP为了配合对方发送ACK连接没彻底断开server端还是能给clinet发送数据的 此时client收到server的数据怎么读上来
之前使用close(sockfd)表面读写端都断开了如果用 shutdown(sockfd,SHUT WR) 我只关闭写端还能正常读数据
5再次理解
在OS中我们会有很多连接有请求的连接正在通信的连接请求断开的连接怎么多连接OS怎么管理
先描述在组织建立内核结构体进行管理
这也说明了一件事建立连接是有成本时间空间的
在上面我们讲了三次握手那一次两次握手行不行呢 一次握手与两次握手会带来server安全问题SYN洪水 一次握手就建立连接的话client可以给server发送大量SYNserver建立连接时需要成本的每个连接占一点资源有可能会让server崩溃 两次握手也同理(server给clientACK client可以不处理)那如果这样的话三次握手也是可能造成SYN洪水的啊
但三次握手相对而言比一次两次来说更好些最后一次ACK时client发的clientOS内部也要管理连接(需要成本)持续攻击client也会崩溃
5.1建立连接为什么要三次握手
1验证全双工验证网络的连通性 (距离问题) client给serverSYNserverACK说明clinet给server发的消息 server能收到 server给clientSYNclientACK说明server给clinet发的消息 clinet能收到 这样既能说明双方网络连通也说明了client能发和收server能收和发 2建立双方通信的共识意愿意见问题 client给serverSYNserverACK说明server同意与clinet进行连接 server给clientSYNclinetACK说明clinet同意与server进行连接 而server给client的SYN和ACK实际在握手时采用的是捎带应答(一次搞定效果是一样的)
这些问题都是一次握手两次握手无法来验证的
5.2重新理解四次挥手
与为什么三次握手的理由类似也是要知道双方的距离与意见问题至于为什么是四次而不是三次主要是为了处理我们上面的特殊情况client要断开连接而server不想断开的情形
5.3验证状态
CLOSE_WAIT
被动断开的一方会在第一次挥手完成时处于CLOSE_WAIT
将http写的代码中把请求处理好后不关闭sockfd就能看到现象
//TcpServer.hpp
#pragma once
#include functional
#include Socket.hpp
#include Log.hpp
#include InetAddr.hppusing namespace socket_ns;static const int gport 8888;using service_io_t std::functionstd::string(std::string);class TcpServer
{
public:TcpServer(service_io_t server, uint16_t port gport): _server(server), _port(port), _listensockfd(std::make_sharedTcpSocket()), _isrunning(false){_listensockfd-Tcp_ServerSocket(_port); // socket bind listen}void Start(){_isrunning true;while (_isrunning){InetAddr client;SockPtr newsock _listensockfd-AcceptSocket(client);if (newsock nullptr)continue; // 断开连接// 进行服务// version 2 -- 多线程 -- 不能关fd -- 共享pthread_t pid;PthreadDate *date new PthreadDate(newsock, this, client);pthread_create(pid, nullptr, Excute, date);}_isrunning false;}struct PthreadDate{SockPtr _sockfd;TcpServer *_self;InetAddr _addr;PthreadDate(SockPtr sockfd, TcpServer *self, const InetAddr addr): _sockfd(sockfd),_self(self),_addr(addr){}};static void *Excute(void *args){pthread_detach(pthread_self());PthreadDate *date static_castPthreadDate *(args);std::string requeststr;date-_sockfd-Recv(requeststr);std::string reponsestrdate-_self-_server(requeststr); // 进行回调date-_sockfd-Send(reponsestr);//date-_sockfd-Close(); // 不关闭 sockfddelete date;return nullptr;}private:service_io_t _server;uint16_t _port;SockPtr _listensockfd;bool _isrunning;
};
用指令查
netstat -natp 如何我们的服务器卡顿查一下是不是存在大量的CLOSE_WAIT状态是的话把sockfd关闭 TIME_WAIT
主动断开的一方会在第四挥手完成时等待一定的时长
如果我们想看到TIME_WAIT状态先让server退在让client退在server就能看到现象 平时在运行server时如果server退了想再次重启时我们会发现bind失败要换端口号才能解决原因就是进程连接退了但OS所维护的连接没退还在TIME_WAIT中端口号被占用
使用setsockopt设置创建的sockfd就能解决TIME_WAIT问题解决历史遗留问题
void ReuseAddr()
{int opt 1;::setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));
}
至于TIME_WAIT的时间它2*MSL(Maximum Segment Lifetime)最大报文生成时间
可以通过指令查这个时间 cat /proc/sys/net/ipv4/tcp_fin_timeout
那TIME_WAIT有什么好处为什么是2MSLMSL RTO(超时重传时间) 我们都知道但一方把数据发送给对方时我们在某个时间间隔没有进行ACK时就要进行重传当对方没收到该报文有可能是网络差报文正处在某个路由器阻塞着这么说这个报文的生存时间明显要大于重传时间长时间下来在网络里可能存在着很多积攒下来的报文四次挥手后在clinet看来是断开连接的实际上在OS中并没有断开在等待着网络中历史报文收到并清除防止后面我们在进行连接时(用相同的端口号)进行一定干扰 有了这个TIME_WAIT时间在第四次挥手时如果ACK丢了对方会持续想我们发送FIN来提醒我们丢包了好让我们进行ACK应答 6流量控制
在发送报文时有时会遇到发送方发送过去的报文接收方的接收缓冲区满了会对收到的报文进行丢弃这合理吗
有人可能会说报文被丢弃了TCP保证可靠性不是有超时重传吗重传下不就好了
这虽然有道理但我把报文经过CPU资源流量等花费千里迢迢到达目的地接收方只是因为接收缓冲区满了就丢弃了它有没有什么错所以我们要根据接收方的接收能力 那什么是接收方的接收能力-- 接收方接收缓冲区剩余空间的大小 那我作为发送方怎么知道 发送报文时接收方不是要给我们ACK吗除了在报头里吧ACK标志位置1它还要更新16位窗口大小填写是自己的接收能力对方送到后就能动态调整发送数据的大小啦 接收方接收缓冲区满了我们就要把发的速度由快变慢但有没有可能接收方缓冲区很大发送方
发送还太慢呢 这时完全有可能的这时接收方上层已经嗷嗷待哺了发送方发送速度还这么慢这时完全不合理的 所以流量控制不仅能控制变慢也能控制变快 现在两台主机都知道对方的接收能力了可以根据剩余空间大小做调整但如果发送方首次给接收方发送报文应该发多大 有人可能会说首次不清楚的情况下发小一点不就可以吗 但关键是要发多少100字节200字节那如果接收方接收缓冲区故意设置得很小呢 首次给对方发报文这个说法不怎么准确在那期间我们双方进行连接时要进行三次握手这不就是给对方发送报文吗发送报文除了把SYN置1/ACK置1自己的属性数据(如流量大小)也要填写后发送告知对方也就是说在三次握手期间双方就已经交换了双方的接收能力 既然双方都知道了接收能力也就能根据对方实际情况发送合理的数据了这时我们把它推导到极端接收方剩余空间为0发送方要怎么办接收方不为0了接收方又要怎么办 接收方缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测空报文看看接收方的情况当接收方窗口不为0时会主动给发送方发送窗口更新通知空报文发送方我能够接收数据了快来给我发送消息吧 在这里如果接收方窗口不更新发送方一直窗口探测的结果一直是0这该怎么办 那么这时发送方就会将要发送的报文中把PSH标记位置1让接收方尽快处理数据把接收缓冲区的数据尽快取走 那么现在还有最后一个标记位URG没说现在就来说说它的作用 URG紧急指针是否有效 16位紧急指针标识那部分数据是紧急指针 紧急指针是数据中偏移量数组下标的数据只有一个字节
紧急指针下的数据一般是我们规定好的0:正常 1暂停 2取消常用于上传资源过程中途取消
如果没有紧急指针那么我们想取消的这个需求等前面数据传输后才能取消这样就很费资源
过程:读到报文中URG置1(紧急指针有效) - 紧急指针偏移量下的数据 - 根据数据进行处理动作 在写代码过程中要想进行发送接收紧急数据时就要用到recv,send参数中的falgsMSG_OOB 要想实现我们可以启用两个线程一个正常通信一个负责紧急数据处理但这个实用性不高(如果要更换需求,整个代码可能会受影响)一般推荐用ftp协议(两个端口分开处理后续改动不大 7滑动窗口
前面我们讲了流量控制和超时重传通过两个问题来引出滑动窗口 1流量控制发送方如何根据对方接收能力来发送数据 流量控制是通过滑动窗口来实现的 2超时重传在超时重传时间内已经发送的报文不能丢弃它保存在哪里 保存在滑动窗口中 先不着急理解这些我们先来看看TCP的两种通信模式一种是发送一个报文确认应答后再发送报文我们说这种效率不高所以有了第二种方式在一个时间段内发送多个报文同时会收到多个应答在这里我们发送的多个报文的前提下是对方要来得及接收的所以在发送方这里我们要来规定出一个概念 滑动窗口在滑动窗口内的数据可以直接发送暂时不需要收到应答 滑动窗口就相当于发送缓冲区中的其中一块缓冲区 左边是已发送已确认数据自己是暂时不用收到应答直接发送右边是未发送未确认数据 (暂时)所以目前可以得出滑动窗口大小 接收方的接收能力
7.1两个问题
滑动窗口只能向右滑动吗可以向左滑动吗 以目前来说好像是只能向右滑不能向左滑因为往右滑动时左边的数据是已发送已应答了没必要再次发重复数据 滑动窗口是一直不变的吗可以变大吗可以变小吗可以为零吗 窗口大小(暂时)代表着接收方的接收能力当本次发送报文时接收方给我ACK的窗口大小变大滑动窗口不就变大了变小也类似如果接收方不处理接收缓冲区的数据ACK的窗口大小肯定会越来越小甚至最后变0也就是滑动窗口为零 之前我们说了我们可以把发送缓冲区连接成 char outbuffer[N] 数组那里序号就是数组下标
而滑动窗口本质上就是两个下标指针[int win_startint win_end]来控制大小 当接收方ACK报文时更新下标指针win_start ack_seqwin_end win_start win 要想指针所指的范围变大win_end变大的速度大于win_start也就是更新窗口大小变大 要想指针所指的范围变小甚至变成零win_start变大的速度大于win_end甚至win_start win_end也就是接收方不处理接收缓冲区的数据窗口大小变小(最后变为零) 7.2丢包问题
关于丢包问题共有3种(1种)情况
最左侧报文丢失 比如1001~2000的报文丢失而后面的报文发送成功那么主机B ACK应答时确认序号全是1001代表1001之前的数据主机B已经收到TCP识别主机A收到3个以上ACK应答就会认为最左侧丢失触发快重传机制进行补发 有了快重传这么高效率的机制超时重传是不是显得有点累赘了 一点都不会如果双方通信接近末期了主机A收到了2个相同的ACK确认应答快重传没触发此时就由超时重传来接管也就是说快重传是在超时重传的基础上提高效率而超时重传是在为快重传没法触发时兜底的 那如果不是最左侧报文丢失而是ACK应答丢失了呢 这种我们就看最新的ACK应答来判断那个报文丢失从而进行补发最新丢了看次新的... 总结 a由于确认序号约束滑动窗口的wit_start不动 b快重传 | 超时重传进行补发 中间报文丢失因为中间报文丢失而最左侧报文发送成功并收到了应答win_start向右移动此时中间报文变成最左侧报文 最右侧报文丢失前面的报文发送成功并收到应答win_start ack_seq此时的最右侧报文就又变成了最左侧报文 7.3关于滑动窗口
滑动窗口向右滑动过程中会不会发生越界 不会我们在逻辑上把发送缓冲区理解成数组但物理上它是环形队列不会出现越界情况 滑动窗口左边的数据已经是已发送已应答了要把它们删除吗
不用滑动窗口向右滑动也就把左边数据默认已经是没用数据随时可被新数据所覆盖~
8拥塞控制
以上的TCP策略我们只考虑发送方到接收方的问题不考虑网络问题那如果此时通信出现网络拥塞了如何识别出来是网络的问题
(上帝视角)当server给client发送1000个报文时有999个报文被对方收到只有一个出现丢包这时server会认为很正常重新对丢包报文进行补发但如果是999个报文出现丢包server就会认为出现严重的网络拥塞了停止发送报文 这时可能会有人说了不就是网络拥塞了吗重新补发999个报文不就好了为什么还要停止发送报文呢网络通信你不要以为只有你们(server和clinet)在通信还有其它server和client也在通信继续补发不是更加加剧网络拥塞吗 解决网络拥塞的意义在于多个使用同一个网络通信的主机有拥塞避免的共识
TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据; 在这里我们要引入拥塞窗口int变量这个概念前面讲了滑动窗口 应答窗口 这个说法是暂时的实际上滑动窗口 min(应答窗口拥塞窗口) 网络好的情况用应答窗口网卡情况用拥塞窗口使得滑动窗口的大小最终一定是小于等于对方接收能力的
发送开始的时候, 定义拥塞窗口大小为1;每次收到一个 ACK 应答, 拥塞窗口加1;
由于网络状态是浮动的那么也就说明拥塞窗口大小也必然是浮动的那主机应该这样得知拥塞窗口的大小是多大
经过多轮尝试发送1,2,.4...来确定拥塞窗口的大小 多轮尝试慢启动发送数据呈现指数级增长(2^n)前期慢后期快增幅大减小网络发送让网络恢复(前期慢)网络一旦恢复了我们的通信过程也要快速恢复起来(后期快) 而拥塞窗口大小时刻都在发送变化为了不增长的那么快, 不能使拥塞窗口单纯的加倍 此处引入慢启动的阈值 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长 线性增长探测到网络拥塞了更新出拥塞窗口大小线性增长更新的大小较准确了下次传输从1开始重新进行探测拥塞窗口大小新的ssthresh值是上次更新的拥塞窗口的一半 那有没有可能状态一直稳定探测一直线性增长
这是有可能的但在不同的系统中拥塞窗口大小一般是有上限的而网络状态一定在某个时刻会出现些许波动不可能一直是流畅的状态毕竟万事无绝对嘛
9延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小
• 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;
• 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
• 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也 能处理过来;
• 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的 窗口大小就是 1M; (网络健康状态下)窗口越大, 网络吞吐量就越大, 传输效率就越高 如果延迟应答后上层还是未处理接收缓冲区的数据ACK的窗口大小不还是跟之前的一样(在这种情况下)延不延迟好像没必要吧 延迟应答是保证数据能在短时间内被上层处理好给对方ACK告诉对方我能接收更多的数据如果短时间内未被处理慢ACK与快ACK是不影响通信的而慢ACK有概率触发数据被上层处理提高传输效率 那么所有的包都可以延迟应答么? 肯定也不是
• 数量限制: 每隔 N 个包就应答一次;• 时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; Linux一般 N 取 2, 超时时间取 200ms;
10捎带应答 在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 一发一收 的. 意味着客户端给服务器说了 How are you, 服务器也会给客户端回一个 Fine, thank you; 那么这个时候 ACK 就可以搭顺风车, 和服务器回应的 Fine, thank you 一起回给客户端 在建立连接时第三次握手client给server发ACK时就可以捎带应答(前两次不行)在给serverACK的同时携带数据因为给serverACK时client认为连接已经是建立好了的 11TCP异常问题
进程终止(机器重启)进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别
机器掉电/网线断开server认为连接还在, 一旦server有写入操作, client没反应server就会发现连接已经不在了, 就会进行 reset即使没有写入操作, TCP 自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放这种做法叫做保活机制
给client定期发消息检测连接是否存在的做法也可以在应用层中体现例如前面的socket编程中给client echo_server 信息就是其中一种
12基于 TCP 应用层协议
• HTTP• HTTPS• SSH• Telnet• FTP• SMTP
13TCP小结
为什么 TCP 这么复杂? 因为既要保证可靠性, 也要提高性能
可靠性:
• 校验和• 序列号(按序到达)• 确认应答• 超时重传• 连接管理(三次握手四次挥手)• 流量控制• 拥塞控制
提高性能:
• 滑动窗口• 快速重传• 延迟应答• 捎带应答
其他:• 定时器(超时重传定时器, 保活定时器, TIME_WAIT 定时器等)
四UDP与TCP
1两者的面向问题
UDP面向数据报 应用层交给 UDP 多长的报文, UDP 原样发送, 既不会拆分, 也不会合并 用 UDP 传输 100 个字节的数据:
• 发送端调用一次 sendto, 发送 100 个字节, 接收端也必须调用对应的一次recvfrom接收100字节
• UDP 没有真正意义上的 发送缓冲区调用 sendto 会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
• UDP 具有接收缓冲区但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致; 如果缓冲区满了, 再到达的 UDP 数据就会被丢弃; 注意UDP 协议首部中有一个 16 位的最大长度也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部)然而 64K 在当今的互联网环境下, 是一个非常小的数字.如果我们需要传输的数据超过 64K就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装; TCP面向字节流
创建一个 TCP 的socket的同时会在内核中创建一个发送缓冲区和一个接收缓冲区;
• 调用 write 时, 数据会写入发送缓冲区中;
• 如果发送的字节数太长, 会自动被拆分成多个 TCP 的数据包发出;
• 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
• 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
由于缓冲区的存在, TCP的读和写不需要一一匹配, 例如:• 写100 个字节数据时,可以调用一次write写100 个字节,也可以调用 100 次write, 每次写一个字节; 这里的字节流与我们在之前学习的语言级别的字节流是一样的 2粘包问题
这个问题就如同北方城市蒸包子蒸好的包子之间如果没有距离你去拿一个包子时会无从下手可能拿上来是一个半包子也可能是半个包子...
而粘包问题本质上是解决两个包之间的边界问题
对于UDP来说没有粘包问题
• 对于 UDP, 如果还没有上层交付数据, UDP 的报文长度仍然在同时, UDP 是一个一个把数据交付给应用层. 就有很明确的数据边界
• 站在应用层的角度recvform读取字节一定是对方发送的字节数不可能出现少读多读的情况
这样读到的一直是完整的报文
对于TCP来说存在粘包问题
• 在 TCP 的协议头中, 没有如同 UDP 一样的 报文长度 这样的字段, 但是有一个序号这样的字段
• 站在传输层的角度, TCP 是一个一个报文过来的. 按照序号排好序放在缓冲区中保证按需到达
• 站在应用层的角度, 看到的是字节流在缓冲区中
• 这样就不知道从哪个部分开始到哪个部分, 是一个完整的报文
解决粘包问题
• 对于定长的包, 保证每次都按固定大小读取即可;requestreponse结构
• 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段添加包长度作为包的报头
• 对于变长的包, 还可以在包和包之间使用明确的分隔符例如用\r\n表示包的结束位置
扩展理解
在之前学习文件时我们往文件里写入各种数据结构intfloatchar类型...将文件里的数据给读上来时会发现很难进行解析我们会这样 因为打开文件时就是面向字节流的语言层学习文件时少了粘包问题与反序列化的相关知识所以会发现往文件里写好写读就不好读粘包问题要解决
在之前的序列与反序列化中我们用json进行序列化成字符串在encode(添加报头用分隔符隔开)后往网络里写
现在变成往文件里写要把文件读上来不就是当时写client的逻辑decode(读取有效载荷后删除报文)后进行反序列化得到我们想要的数据
而在未来我们要学习的数据库redis,mysqt,mongodb...)把数据存到文件里再把数据读上来本质上是序列化与反序列化自己实现的协议
3两者的对比
我们说了 TCP 是可靠连接, 那么是不是 TCP 一定就优于 UDP 呢? TCP 和 UDP 之间的优点和缺点, 不能简单, 绝对的进行比较
• TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;• UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传输等. 另外 UDP 可以用于广播;
对于要使用哪个协议还用通过具体场景与具体需求去判定
五TCP全连接队列
1listen的第二个参数
正常server与client通信的代码把server的accept部分给注释掉(修改backlog)还能不能连接
//tcpserver.cc
#include iostream
#include string
#include cerrno
#include cstring
#include cstdlib
#include memory
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include sys/wait.h
#include unistd.hconst static int default_backlog 1;//修改成1enum
{Usage_Err 1,Socket_Err,Bind_Err,Listen_Err
};#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)class TcpServer
{
public:TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 都是固定套路void Init(){// 1. 创建socket, file fd, 本质是文件_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){exit(0);}int opt 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, opt, sizeof(opt));// 2. 填充本地网络信息并bindstruct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr htonl(INADDR_ANY);// 2.1 bindif (bind(_listensock, CONV(local), sizeof(local)) ! 0){exit(Bind_Err);}// 3. 设置socket为监听状态tcp特有的if (listen(_listensock, default_backlog) ! 0){exit(Listen_Err);}}void ProcessConnection(int sockfd, struct sockaddr_in peer){uint16_t clientport ntohs(peer.sin_port);std::string clientip inet_ntoa(peer.sin_addr);std::string prefix clientip : std::to_string(clientport);std::cout get a new connection, info is : prefix std::endl;while (true){char inbuffer[1024];ssize_t s ::read(sockfd, inbuffer, sizeof(inbuffer)-1);if(s 0){inbuffer[s] 0;std::cout prefix # inbuffer std::endl;std::string echo inbuffer;echo [tcp server echo message];write(sockfd, echo.c_str(), echo.size());}else{std::cout prefix client quit std::endl;break;}}}void Start(){_isrunning true;while (_isrunning){// 4. 获取连接//struct sockaddr_in peer;//socklen_t len sizeof(peer);//int sockfd accept(_listensock, CONV(peer), len);//if (sockfd 0)//{// continue;//}ProcessConnection(sockfd, peer);}}~TcpServer(){}private:uint16_t _port;int _listensock; // TODObool _isrunning;
};using namespace std;void Usage(std::string proc)
{std::cout Usage : \n\t proc local_port\n std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{if (argc ! 2){Usage(argv[0]);return Usage_Err;}uint16_t port stoi(argv[1]);std::unique_ptrTcpServer tsvr make_uniqueTcpServer(port);tsvr-Init();tsvr-Start();return 0;
}//client.cc
#include iostream
#include string
#include unistd.h
#include sys/socket.h
#include sys/types.h
#include arpa/inet.h
#include netinet/in.hint main(int argc, char **argv)
{if (argc ! 3){std::cerr \nUsage: argv[0] serverip serverport\n std::endl;return 1;}std::string serverip argv[1];uint16_t serverport std::stoi(argv[2]);int clientSocket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket 0){std::cerr socket failed std::endl;return 1;}sockaddr_in serverAddr;serverAddr.sin_family AF_INET;serverAddr.sin_port htons(serverport); // 替换为服务器端口serverAddr.sin_addr.s_addr inet_addr(serverip.c_str()); // 替换为服务器IP地址int result connect(clientSocket, (struct sockaddr *)serverAddr, sizeof(serverAddr));if (result 0){std::cerr connect failed std::endl;::close(clientSocket);return 1;}while (true){std::string message;std::cout Please Enter ;std::getline(std::cin, message);if (message.empty())continue;send(clientSocket, message.c_str(), message.size(), 0);char buffer[1024] {0};int bytesReceived recv(clientSocket, buffer, sizeof(buffer) - 1, 0);if (bytesReceived 0){buffer[bytesReceived] \0; // 确保字符串以 null 结尾std::cout Received from server: buffer std::endl;}else{std::cerr recv failed std::endl;}}::close(clientSocket);return 0;
}server端与client端启动时现象 我们所用的公网IP是云服务器厂商虚拟出来的机器真实的IP是内网IP但内网IP不便于进行公网访问但使用公网IP时内网IP会路由到公网IP里远程使用时用不了内网IP所以在对方看来就用公网IP来标识连接对方身份 再启动多个client端时 listen的第二个参数的作用全连接队列中已经建立三次握手成功的连接个数 backlog 1 在服务器来不及对连接进行accept时底层的TCP会允许用户进行三次握手但建立连接的个数不能太多 最大是backlog 1 2理解全连接队列 (内核中)在传输层TCP中建立accept_queue队列维护的来不及处理的连接当有客户端来进行连接三次握手时在队列后面进行链接结构体内包含着各种基本信息当应用层进行accept获取连接时实际上是获取建立连接的结构体连接的本质是内核中的一种数据结构 在上面我们模拟的是应用层accept给注释掉(accept非常忙来不及进行accept)此时accept_queue队列的长度最大backlog 1
为什么全连接队列不能为空也不能太长 a应用层要用全连接队列中去拿新到来的连接加入到全连接队列中这不就是生产者消费者模型吗 b队列为空会增加服务器的闲置率减少给用户提供服务的效率与体验 c队列太长会浪费资源空间使得用户体验不好新到来的用户连接等待时间长 3内核层里socket和连接
当我们创建出一个套接字的时候在struct file* fd array[]申请一个空间(3号数组下标listen_socket)返回给上层服务(进程)先要为我们创建出一个file对象这个file对象在底层创建出socket再通过type创建出tcp_scok(udp_sock)对象(连接)然后我们就能通过file对象的private data找到socket再通过socket里的sock(多态的方式)访问到tcp_scok(udp_sock)里的所有数据
进行accept时在struct file* fd array[]申请一个空间(4号数组下标普通socket)返回给上层来进行IO系统先创建出file和socket对象通过指针连接起来三次握手通过socket里的sock找到tcp_sockudp_sock这样在上层就能通过普通socket进行通信啦
内核代码证明file与sock的联系 关于连接
全连接队列与链表管理报文示意图 4使用TCP dump抓包
TCPDump 是一款强大的网络分析工具 主要用于捕获和分析网络上传输的数据包。
4.1安装 tcpdump
tcpdump 通常已经预装在大多数 Linux 发行版中。如果没有安装可以使用包管理器进行安装。 例如 Ubuntu 可以使用以下命令安装
sudo apt-get install tcpdump
在 Red Hat 或 CentOS 系统中 可以使用以下命令
sudo yum install tcpdump
4.2使用
捕获所有网络接口上传输的 TCP 报文
sudo tcpdump -i any tcp -i any 指定捕获所有网络接口上的数据包tcp 指定捕获 TCP 协议的数据包。i 可以理解为 interface 捕获指定网络接口上的 TCP 报文
$ ifconfig
eth0: flags4163UP,BROADCAST,RUNNING,MULTICAST mtu 1500
inet 172.18.45.153 netmask 255.255.192.0 broadcast
172.18.63.255
inet6 fe80::216:3eff:fe03:959b prefixlen 64 scopeid
0x20link
ether 00:16:3e:03:95:9b txqueuelen 1000 (Ethernet)
RX packets 34367847 bytes 9360264363 (9.3 GB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 34274797 bytes 6954263329 (6.9 GB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
$ sudo tcpdump -i eth0 tcp
捕获特定源或目的 IP 地址的 TCP 报文
捕获特定源IP地址
sudo tcpdump src host IP地址 and tcp
捕获目的 IP 地址
sudo tcpdump dst host IP地址 and tcp
两种都捕获
sudo tcpdump src host IP地址 and dst host IP地址 and tcp
捕获特定端口的 TCP 报文
sudo tcpdump port 端口号 and tcp
保存捕获的数据包到文件
sudo tcpdump -i eth0 port 端口号 -w data.pcap
从文件中读取数据分析
tcpdump -r data.pcap
4.3代码观察现象
//tcpclient.cc
#include iostream
#include string
#include unistd.h
#include sys/socket.h
#include sys/types.h
#include arpa/inet.h
#include netinet/in.hint main(int argc, char **argv)
{if (argc ! 3){std::cerr \nUsage: argv[0] serverip serverport\n std::endl;return 1;}std::string serverip argv[1];uint16_t serverport std::stoi(argv[2]);int clientSocket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket 0){std::cerr socket failed std::endl;return 1;}sockaddr_in serverAddr;serverAddr.sin_family AF_INET;serverAddr.sin_port htons(serverport); // 替换为服务器端口serverAddr.sin_addr.s_addr inet_addr(serverip.c_str()); // 替换为服务器IP地址int result connect(clientSocket, (struct sockaddr *)serverAddr, sizeof(serverAddr));if (result 0){std::cerr connect failed std::endl;::close(clientSocket);return 1;}while (true){std::string message;std::cout Please Enter ;std::getline(std::cin, message);if (message.empty())continue;send(clientSocket, message.c_str(), message.size(), 0);char buffer[1024] {0};int bytesReceived recv(clientSocket, buffer, sizeof(buffer) - 1, 0);if (bytesReceived 0){buffer[bytesReceived] \0; // 确保字符串以 null 结尾std::cout Received from server: buffer std::endl;}else{std::cerr recv failed std::endl;}}::close(clientSocket);return 0;
}
//tcpserver.cc
#include iostream
#include string
#include cerrno
#include cstring
#include cstdlib
#include memory
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include sys/wait.h
#include unistd.hconst static int default_backlog 6;enum
{Usage_Err 1,Socket_Err,Bind_Err,Listen_Err
};#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)class TcpServer
{
public:TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 都是固定套路void Init(){// 1. 创建socket, file fd, 本质是文件_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){exit(0);}int opt 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, opt, sizeof(opt));// 2. 填充本地网络信息并bindstruct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr htonl(INADDR_ANY);// 2.1 bindif (bind(_listensock, CONV(local), sizeof(local)) ! 0){exit(Bind_Err);}// 3. 设置socket为监听状态tcp特有的if (listen(_listensock, default_backlog) ! 0){exit(Listen_Err);}}void ProcessConnection(int sockfd, struct sockaddr_in peer){uint16_t clientport ntohs(peer.sin_port);std::string clientip inet_ntoa(peer.sin_addr);std::string prefix clientip : std::to_string(clientport);std::cout get a new connection, info is : prefix std::endl;while (true){char inbuffer[1024];ssize_t s ::read(sockfd, inbuffer, sizeof(inbuffer)-1);if(s 0){inbuffer[s] 0;std::cout prefix # inbuffer std::endl;std::string echo inbuffer;echo [tcp server echo message];write(sockfd, echo.c_str(), echo.size());}else{std::cout prefix client quit std::endl;break;}}}void Start(){_isrunning true;while (_isrunning){// 4. 获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int sockfd accept(_listensock, CONV(peer), len);if (sockfd 0){continue;}ProcessConnection(sockfd, peer);sleep(1);//才能看到四次挥手close(sockfd);}}~TcpServer(){}private:uint16_t _port;int _listensock; // TODObool _isrunning;
};using namespace std;void Usage(std::string proc)
{std::cout Usage : \n\t proc local_port\n std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{if (argc ! 2){Usage(argv[0]);return Usage_Err;}uint16_t port stoi(argv[1]);std::unique_ptrTcpServer tsvr make_uniqueTcpServer(port);tsvr-Init();tsvr-Start();return 0;
}现象 以上便是 TCP 协议的全部内容有错误欢迎指正最后感谢您的观看