netstat命令->查看网络状态
![Lesson12 udp&&tcp协议_第1张图片](http://img.e-com-net.com/image/info8/239b18bf7c534ee9a9f9310f33157f0f.jpg)
- n 拒绝显示别名,能显示数字的全部转化成数字
- l 仅列出有在 Listen (监听) 的服務状态
- p 显示建立相关链接的程序名
- t (tcp)仅显示tcp相关选项
- u (udp)仅显示udp相关选项
- a (all)显示所有选项,默认不显示LISTEN相关
pidof命令->查看进程id
![](http://img.e-com-net.com/image/info8/eef9df082fd34c53a4397ce3a3563ba8.jpg)
![Lesson12 udp&&tcp协议_第2张图片](http://img.e-com-net.com/image/info8/2670954f531b463e81510ba50feff6e4.jpg)
UDP协议
UDP协议端格式
![Lesson12 udp&&tcp协议_第3张图片](http://img.e-com-net.com/image/info8/33535bc5854a4e19a80d4cc1d733cacc.jpg)
1. 如何分离(封装)
2.如何交付
- 根据报头中的16位端口号,进行向上交付(进程bind了端口号)
udp是如何正确的提取整个完整的报头
- 固定长度的报头 -> 16位udp长度,这也解释了在应用层写端口号的时候,类型是uint16_t类型的
3.理解udp报文本身
![Lesson12 udp&&tcp协议_第4张图片](http://img.e-com-net.com/image/info8/4d6775c105864cf49e4f9b1408817721.jpg)
4. sendto/recvfrom/write/read/recv/send ...io类接口
![Lesson12 udp&&tcp协议_第5张图片](http://img.e-com-net.com/image/info8/e6612a9005534df489bd96d9c45165e6.jpg)
5.udp 全双工 && 半双工
- udp是全双工的
- 全双工: 一边读,一边写(可以同时发生)
- 半双工: 只能一边读,或者一边写(不可以同时发生)
TCP协议
TCP协议端格式
![Lesson12 udp&&tcp协议_第6张图片](http://img.e-com-net.com/image/info8/47ca3e0d92e14118a0f2523d6ec3dc21.jpg)
- 4为首部长度虽然是4个bit位,但是单位是字节
- 0000->1111 => 0 -> 15,即范围是0->60字节
- 报头的范围: [20,60]
- x * 4 = 20 推出 x = 5->0101
- x * 4 = 60 推出 x = 15->1111
1. TCP是如何交付的
- 根据报头中的16位端口号,进行向上交付(进程bind了端口号)
2. TCP是如何解包的
-
先提取20字节
-
根据标准报头,提取4位首部长度 * 4 = 20 - done
-
读取[提取4位首部长度 * 4 - 20]字节数据,选项
-
读完了报头,剩下的都是有效载荷
3.理解可靠性
![Lesson12 udp&&tcp协议_第7张图片](http://img.e-com-net.com/image/info8/17c819327d1149a986ea5a5dd7584f86.jpg)
- 在网络中不存在100%可靠的协议,无论那台主机都无法保证自己作为最新发送数据的一方
发送出去的数据都能被对方收到
- 但是在局部上能做到100%可靠,只有有匹配应答,就能保证刚发出去的消息,对方收到了
- TCP协议的确认机制: 只要一个报文收到了对应的应答,就能保证我发出的数据对方收到了
4.序号和确认序号
![Lesson12 udp&&tcp协议_第8张图片](http://img.e-com-net.com/image/info8/7b865ad1e1524542954b2d38b97149f9.png)
- client一次可能向服务器发送多个报头,就会有一个问题,发送的顺序,不一定是接收顺序
- 序号和确认序号,就是解决这个问题的
序号和确认序号:
- 将请求和应答进行一一对应
- 确认序号,表示的含义:确认序号之前的数据已经全部收到的
- 允许部分确认丢失,或者不给应答
- 为什么要有两个字段数字
- 任何通信的一方,工作方式都是全双工的,在发送确认的时候,也可能携带新的数据
- 1000 2000 3000 -> 2000 1000 3000,发送和接收的顺序不一定一样,因为任何一方都会收到报文,但报文中会携带序号(可以排序)
TCP的接收缓冲区和发送缓冲区
![Lesson12 udp&&tcp协议_第9张图片](http://img.e-com-net.com/image/info8/980e09cffd584561883f506db3b4cafd.jpg)
- tcp有发送和接收缓冲区,当我们有client和server,我们就要有两对接收和发送缓冲区
TCP的6个标记位
![Lesson12 udp&&tcp协议_第10张图片](http://img.e-com-net.com/image/info8/3f77627686c14fbfa2d8da840632b1d5.jpg)
- TCP的6个标记位都是标记报文类型的
- SYN: 该报文是一个链接请求报文
- FIN: 该报文是一个断开链接请求的报文
- ACK: 确认应答标志位,
- 凡是该报文具有应答特征,该标志位都会被设置成1,大部分网络报文ACK都是被设置为1的
除了一个链接请求报文
- RST: reset,连接重置
- PSH: PUSH,督促对方尽快将数据进行向上交付
- URG: 紧急标志位,16位紧急指针
1.理解TCP的连接
- 因为有大量的client将来可能会链接server,所以server端一定会存在大量的连接
- OS会通过先描述,再组织的方式管理这些连接
- 所谓的连接:本质其实就是内核的一种数据结构类型,建立连接成功的时候,就是在内存中创建对应的连接对象,再对多个连接对象进行某种数据结构的组织
- 维护连接是有成本的(内存+cpu)
2. 理解TCP的三次握手
![Lesson12 udp&&tcp协议_第11张图片](http://img.e-com-net.com/image/info8/f47fe8e74466447e8aa481d4048e69de.jpg)
为什么需要三次握手
- 站在client端,他认为只要发送了SYN给server,就建立了连接
- 站在server端,他认为只要发送了SYN给client,就建立了连接
- 但实际上需要收到对方的应答,才算是建立的连接,
- 所以1次肯定不行,偶数次也不行,3次就刚好可以(客户端和服务端都会建立链接)
- 这3次握手也能验证全双工,
3. 理解四次挥手
![Lesson12 udp&&tcp协议_第12张图片](http://img.e-com-net.com/image/info8/b9eeaaa1b3f24896a9b1316700216b58.jpg)
- 如果我们发生服务器中具有大量的close_wait状态的连接
- 有可能是因为应用层写的时候有bug(没有关闭对应的连接sockfd)
- 为什么会有TIME_WAIT状态,而不是直接就是CLOSED状态
- 虽然4次挥手已经完成,但是主动断开连接的一方要维持一定的TIME_WAIT状态,
- 在该状态下,连接其实已经结束了,但是地址信息ip和port依旧是被占用的,等待上面其他的发送结束(等待时间也是不确定的)
setsockopt函数
![Lesson12 udp&&tcp协议_第13张图片](http://img.e-com-net.com/image/info8/174901f2ac8b4b09bbafd558a5e0b88e.jpg)
![Lesson12 udp&&tcp协议_第14张图片](http://img.e-com-net.com/image/info8/23a5372b79634beaad7a4cd24c377197.jpg)
- 4次挥手结束后TIME_WAIT到CLOSED状态的时间太长,该函数会缩短这个时间
确认应答(ACK)机制
![Lesson12 udp&&tcp协议_第15张图片](http://img.e-com-net.com/image/info8/f51f7745e5f346b08cd9d3bef4574645.jpg)
TCP将每个字节的数据都进行了编号. 即为序列号
![Lesson12 udp&&tcp协议_第16张图片](http://img.e-com-net.com/image/info8/c155e7fa316548b3bba79c2894471df7.jpg)
- 每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
超时重传机制
![Lesson12 udp&&tcp协议_第17张图片](http://img.e-com-net.com/image/info8/e936ddf1220b4eac8d46382e804e9441.jpg)
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
但是 , 主机 A 未收到 B 发来的确认应答 , 也可能是因为 ACK 丢失了
![Lesson12 udp&&tcp协议_第18张图片](http://img.e-com-net.com/image/info8/b53c5bcb8d894409b756ec56a8fc20dd.jpg)
- 主机B会收到很多重复数据. 那么TCP协议通过序列号就能够识别出那些包是重复的包, 并且把重复的丢弃掉.达到去重的效果
超时时间的影响
- 这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
解决方案
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时间, 他都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
连接管理机制
![Lesson12 udp&&tcp协议_第19张图片](http://img.e-com-net.com/image/info8/f47fe8e74466447e8aa481d4048e69de.jpg)
![Lesson12 udp&&tcp协议_第20张图片](http://img.e-com-net.com/image/info8/b9eeaaa1b3f24896a9b1316700216b58.jpg)
滑动窗口
确认应答策略 ,
对每一个发送的数据段 , 都要给一个 ACK 确认应答 . 收到 ACK 后再发送下一个数据段 .
这样做有一个比较大的缺点, 就是 性能较差 . 尤其是数据往返的 时间较长 的时候
![Lesson12 udp&&tcp协议_第21张图片](http://img.e-com-net.com/image/info8/d9316e30668d438dae2307b6deecb220.jpg)
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值.
上图的窗口大小就是4000个字节(四个段).
- 发送前四个段的时候, 不需要等待任何ACK, 直接发送;
![Lesson12 udp&&tcp协议_第22张图片](http://img.e-com-net.com/image/info8/4c513ca6a69743ec814e38fec5a35964.jpg)
![Lesson12 udp&&tcp协议_第23张图片](http://img.e-com-net.com/image/info8/c004e6d4da5643fd9f0e8e1a4ddc199e.jpg)
- 滑动窗口: 在自己的发送缓冲区中,属于自己的发送缓冲区中的一部分
- 一边想要给对方推更多的数据,另一边又想要对方来得及接收
- 滑动窗口的上限 == 对方的接收能力
- 本质: 让sender方,可以一次性发送对方接收数据的上限
滑动窗口的进一步理解
![Lesson12 udp&&tcp协议_第24张图片](http://img.e-com-net.com/image/info8/2be7c72022924e25930357ee3ab55ad1.jpg)
快重传 vs 超时重传
- 快重传是有条件的,
- 这两个不是对立的,而是相互协作的
拥塞控制
- 虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据.
但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
- 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵.
在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
- TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
- 拥塞窗口: 单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值
- 滑动窗口的大小 = min(拥塞窗口,对方窗口大小(接收能力))
拥塞窗口变化大小
![Lesson12 udp&&tcp协议_第25张图片](http://img.e-com-net.com/image/info8/412e709eb42a42c1bef4298ee0c0ef32.jpg)
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
拥塞窗口变化原因
- 它是指数级别增长,特点: 前期慢,后期快
- 在网络拥塞时
- 前期要让网络有一个缓一缓的机会
- 中后期,网络恢复之后,需要尽快恢复通信 -> 避免影响通信效率
拥塞控制的总结
- 少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
- 当TCP通信开始后, 网络吞吐量会逐渐上升;
- 随着网络发生拥堵, 吞吐量会立刻下降;
- 拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方,
但是又要避免给网络造成太大压力的折中方案.
延迟应答
![Lesson12 udp&&tcp协议_第26张图片](http://img.e-com-net.com/image/info8/172c02a35e8d43b89339729d36b429de.jpg)
- 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
延迟应答的时间
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次
捎带应答
面向字节流
创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
-
调用 write 时, 数据会先写入发送缓冲区中;
-
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
-
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
-
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用 read 从接收缓冲区拿数据;
-
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
-
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
-
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
粘包问题
TCP异常情况
-
进程终止: 进程终止会释放文件描述符 , 仍然可以发送 FIN. 和正常关闭没有什么区别 .
机器重启 : 和进程终止的情况相同 .
-
机器掉电/网线断开 : 接收端认为连接还在 , 一旦接收端有写入操作 , 接收端发现连接已经不在了
就会进行 reset .即使没有写入操作, TCP自己也内置了一个保活定时器 ,
会定期询问对方是否还在 . 如果对方不在 , 也会把连接释放 .
TCP小结
要保证可靠性, 同时又尽可能的提高性能
可靠性:
提高性能:
TCP/UDP对比
理解 listen 的第二个参数
演示
Sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class Sock
{
private:
// listen的第二个参数,意义:底层全连接队列的长度 = listen的第二个参数+1
const static int gbacklog = 1;
public:
Sock() {}
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
exit(2);
}
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return listensock;
}
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(3);
}
}
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
exit(4);
}
}
// 一般经验
// const std::string &: 输入型参数
// std::string *: 输出型参数
// std::string &: 输入输出型参数
int Accept(int listensock, std::string *ip, uint16_t *port)
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
return -1;
}
if(port) *port = ntohs(src.sin_port);
if(ip) *ip = inet_ntoa(src.sin_addr);
return servicesock;
}
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
else return false;
}
~Sock() {}
};
main.cc
#include "Sock.hpp"
int main()
{
Sock sock;
int listensock = sock.Socket();
sock.Bind(listensock, 8080);
sock.Listen(listensock);
while(true)
{
sleep(1);
}
}
Makefile
TcpServer:main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f TcpServer
![Lesson12 udp&&tcp协议_第31张图片](http://img.e-com-net.com/image/info8/6bd373ae45114e3d8c5e132b9d710801.gif)