目录
netstat
pidof
xargs
UDP协议
TCP协议
setsockopt函数
确认应答机制
超时重传机制
流量控制
滑动窗口
拥塞控制
延迟应答
捎带应答
面向字节流
粘包问题
在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信
0- 1023:知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议,他们的端口号都是固定的
1024 - 65535:操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的
知名端口号:
ssh服务器,:使用22端口
ftp服务器:使用21端口
telnet服务器:使用23端口
http服务器:使用80端口
https服务器:使用443
netstat
netstat是一个用来查看网络状态的重要工具
常用选项:
n:拒绝显示别名,能显示数字的全部转化成数字
I:仅列出有在Listen (监听)的服務状态
p:显示建立相关链接的程序名
t :(tcp)仅显示tcp相关选项
u :(udp)仅显示udp相关选项
a :(all)显示所有选项,默认不显示LISTEN相关
后面使用时最常用的就是netstat -nltp / -natp
pidof
在查看服务器的进程id时非常方便
用法:pidof [进程名]
功能:通过进程名,查看进程id
xargs
xargs是将标准输入的内容,转化成为命令行参数
例如我们前面所学的kill命令,kill -9 [pid]可以杀死这个进程,这里的pid是按照命令行参数的方式传给kill的,而管道则是标准输入的方式,下面举个例子:
我们前面实现的http的demo代码,在左边窗口运行HttpServer,右边窗口grep查看,可以看到是pid为22461的进程
按照前面的使用方式,再grep查出来PID后,再kill -9 PID
但是我们今天学习了pidof和xargs后,可以直接如下图所示的方式:
pidof HttpServer | xargs kill -9
即pidof HttpServer找到进程的pid,通过管道的方式标准输入,再通过xargs将标准输入的内容,转化成为命令行参数,所以就可以执行kill命令了
UDP协议
几乎任何协议都要首先解决两个问题: a. 如何分离(封装) b.如何交付
如何分离:
UDP采用的是定长报头的策略,其中有16位的源端口号、16位的目的端口号、16位的UDP长度、16位的UDP检验和
报头标准长度8字节,所以可以轻松将报头和有效载荷分离
如何交付:
根据报头中的16位端口号,进行向上交付,因为进程bind了端口号
所以我们在应用层编写代码的时候,每一次写端口号的时候,都喜欢uint16_t,因为协议用的端口号是16位的
udp如何正确的提取整个完整报文的呢?
udp有固定长度的报头,即16位udp长度,再16位udp长度-8就是有效载荷的长度
因此UDP是具有将报文一个一个正确接收的能力的
16位UDP长度,,表示整个数据报(UDP首部+UDP数据)的最大长度
如果校验和出错, 就会直接丢弃
理解报文本身
报头在内核中其实就是一个结构体:(位段)
struct udp_hdr
{
uint32_t src_port:16;
uint32_t dest_port:16;
uint32_t udp_len:16;
uint32_t udp_check:16;
}
UDP特点:不可靠、无连接、面向数据报
不可靠:(不可靠是中性词,不含贬义)没有确认机制,,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息
无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接
面向数据报:不能够灵活的控制读写数据的次数和数量,应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致,如果缓冲区满了,再到达的UDP数据就会被丢弃
UDP的socket既能读,也能写,这个概念叫做全双工
需要注意的是:UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是2^16=64K(包含UDP首部)
然而64K在当今的互联网环境下,是一个非常小的数字,如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装
基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
TCP协议
TCP全称为 "传输控制协议
TCP报文本身结构:
首先解决两个问题: a. 如何分离(封装) b.如何交付
如何交付:
TCP报头也有16位的源端口号和16位的目的端口号,所以可以进行向上交付
如何分离:
在TCP报头中包含了4位首部长度,0000~1111,即0~15,又因为单位是4字节,所以报头大小就是0*4 ~ 15*4也就是0~60字节
又因为最开始的基础长度就是20字节(可能会有选项),所以报头的长度范围就是20~60字节,而这个首部的值假设为x,最小就为x*4==20,x==5,最大为x*4==60,x==15,所以x取值就是5~15,转换为二进制就是[0101, 1111]
因此解包顺序如下:
1.提取20字节标准报头
2.根据标准报头,提取4位首部长度,如果* 4 =20,就读取完成,若大于20,执行下面步骤3
3.读取[提取4位首部长度* 4- 20]字节数据,即为选项
4.读完了报头,剩下的都是有效载荷
注意:TCP是没有整个报文的大小的(面向字节流)
读取的TCP报头在内核中其实就是一个struct tcp_hdr的结构体对象,TCP报头也是一个位段结构
可靠性的理解
并不存在100%可靠的协议
在网络中,发送数据的双方,都无法保证自己作为最新发送数据的一方,发送出去的数据能够被对方收到
但是在局部上,我们能做到100%可靠
我所发出去的所有的消息,只要有匹配的应答,那么我就能保证,我刚刚发出去的消息对方一定收到了
TCP协议的确认应答机制:但是只要一个报文收到了对应的应答,就能保证我发出的数据对方收到了
序号与确定序号
client一次可能向服务端发送多个报文,就有一个问题,发送的顺序,不一定是接受顺序
那么client如何确认,哪一个应答,对应哪一个请求呢?
其实每一个请求都有一个序号,而对应的应答就有一个确认序号,确认序号一般是序号+1,即序号1000,则确认序号是1001
确认序号表示:确认序号对应的数字,之前的所有的报文已经全部收到了,告诉对方下次发送时,从确认序号指明的序号发送
总结:
1.将请求和应答进行一一对应
2.确认序号表示的含义:确认序号之前的数据已经全部收到
3.允许部分确认丢失,或者不给应答(由2得出的结论,例如收到了2001,1001不论收没收到应答,都默认收到了)
4.为什么要有两个字段数字,不能只使用一个序号呢?
因为任何通信的一方,工作方式都是全双工的,在发送确认的时候,也可能携带新的数据,例如server端既想给对方确认(要有确认序号),又想同时给对方发送它的消息(要有序号)
5.序号为1000 2000 3000发送过去变为了 2000 1000 3000,乱序了,因为任何一方都会收到报文,报文中会携带序号,所以可以对序号进行排序,就可以解决乱序的问题了
TCP中有发送缓冲区和接收缓冲区,我们前面所使用的send并不是直接发送到网络中,而是发送到发送缓冲区中,如何发送、什么时候发送、丢失了怎么办等问题,都是由TCP决定的
同样recv也不是从网络中读取的,而是从接收缓冲区中读取的
因为TCP有发送和接受缓冲区,所以如果我们有client和server, 我们就有了两对接受和发送缓冲区,一方发送是由该方的发送缓冲区向对方的接收缓冲区中发送,而自己的接收缓冲区并不影响,所以TCP就能够实现全双工通信
16位窗口大小
client发送数据,既不能太快,也不能太慢,那么如何保证发送方发送数据,不要太快,或者太慢了呢?
需要进行流量控制,接收方需要向发送方同步自己的接收能力,由于发送方发送的数据是发送到接收方的接收缓冲区的,所以接收方的接收能力是由接收缓冲区的剩余空间的大小决定的
而报头中的16位窗口大小存储的就是接收缓冲区的剩余空间的大小,这里的16位窗口大小填的是接收方自己的剩余空间
由于双方通信是存在两对接受和发送缓冲区的,所以就可以实现两个方向的流量控制
6个标记位
6个标记位:1bit位是表示某种含义的
那么为什么需要多个标记位呢?
因为服务端会受到大量的不同的报文,而标记位是可以标记报文类型的
各个标记位的含义:
SYN:该报文是一个连接请求报文
FIN:该报文是一个断开连接请求的报文
ACK:确认应答标志位,凡是该报文具有应答特征,该标志位都会被设置为1
大部分网络报文ACK都是被设置为1的;也有例外,例如第一个连接请求报文
RST:reset,连接重置
在连接建立后,双方通信过程中,有可能连接建立异常,导致对方不知情,反而继续发消息,我们要关闭掉对方连接,让对方重新建立连接
PSH:push,督促对方尽快将数据进行向上交付
当发送方发现接收方的16位窗口大小为0,即接收缓冲区为0时,就发送PSH标记位,表明督促对方尽快将数据进行向上交付
URG:紧急标志位,需要配合报头中的16位紧急指针使用
16位紧急指针表示特定的一个数据,在有效载荷位置中的偏移量,在指向位置只有一个字节的数据是紧急数据,一般用于服务器的异常检测
三次握手
建立连接时需要三次握手,断开连接需要四次挥手
因为有大量的client将来可能连接server,所以server端一定会存在大量的连接,OS要不要管理这些连接呢?当然要:先描述,在组织
所谓的连接:本质其实就是内核的一种数据结构类型,建立连接成功的时候,就是在内存中创建对应的连接对象,再对多个连接对象进行某种数据结构的组织
维护连接是有成本的(内存+ cpu)
再具体说明三次握手四次挥手时,需要注意的是发送的不是标记位,而是整个报文
下面是三次握手的示例图:
其中我们发现建立连接时双方连接的线是斜的,这表明是时间线,即接收方收到报文的时间一定是小于发送方的
下面思考一个问题:是不是三次握手一定要保证成功?
不一定,因为前两次都有应答,而最新发送的ACK,发送出去后,不能保证对方有没有收到
当client端第三次将ACK报文发出,自己的状态就改为了ESTABLISHED,表示连接建立
但并不能保证这个ACKserver端一定能够收到,可能会丢失
为什么要3次握手?
1. server可以嫁接同等的成本给client
例如如果只进行一次握手,那么client端每次只需要发送SYN,并不关心server端是否建立成功连接,client端可以连续发送大量的SYN,自己没有受到什么影响,反而会导致服务器出现异常,这种情况被叫做SYN洪水;
同理可得,如果是两次握手,client端同样可以连续发送大量的SYN,也根本不理会server端发送的SYN+ACK,可以选择直接丢弃,这时同样有SYN洪水的问题,
所以选择三次握手,当client端发送SYN,server端发送SYN+ACK后,client端还需要建立连接才能发送ACK报文,此时如果发送失败,server端没有受到任何影响,反而是client端成本增加
2.验证全双工
在client端和server端都可以验证全双工,因为第一次client端发送报文,可以验证自己是否能够发送,而发送是否成功可以由server端给client端发送的ACK报文判断,此时就可以保证client端数据既能发出又能被接收
而server端在第一次收到client端发送的SYN数据后,表示自己可以接收消息,接收信道是正常的,所以也要保证自己能够发送数据,即向client端发送SYN+ACK,那么如何证明自己可以发送成功呢,可以通过是否能够成功收到client端第三次发送的ACK判断
四次挥手
四次挥手示例图:
四次挥手是因为连接断开时双方的事情,需要征求双方的同意
四次挥手也有可能是三次挥手,因为server端的ACK和FIN标记位也有可能同时放在一个报文中,给client端发送过去
如果我们发现服务器具有大量的close_ wait状态的连接的时候,原因是什么呢?
应用层服务器写的有bug,忘了关闭对应连接的sockfd(文件描述符),所以服务器一直无法发送FIN
虽然4次挥手已经完成,但是主动断开连接的一方要维持一段时间的TIME_WAIT状态,才能变为CLOSED状态,在TIME_WAIT状态下,连接其实已经结束,但是地址信息ip, port依旧是被占用的
所以这就出现了之前绑定一个端口的进程,该进程结束后可能暂时无法继续绑定,而这在实际情况中是非常严重的事故,如果一个服务器在人流量非常大的时间挂掉了,几分钟之内无法重新连接,会造成非常大的经济损失,所以在创建套接字时,需要调用setsockopt函数,此时如果出现服务区崩溃退出的情况,就可以立即重启了,不会需要等待一段时间才能重新绑定ip、port:
setsockopt函数
所以在Sock.hpp中,在创建套接字函数Socket中,需要加入如下语句:
这样就能够地址复用,让对应的端口号和ip地址,可以在TIEE_WAIT状态期间,被服务端立刻绑定
上面解决了TIME_WAIT状态如何地址复用,那么为什么要有这个TIME_WAIT状态呢?
TIME_WAIT状态需要等2MSL的时间,才变为CLOSED状态,其中MSL就是指报文从一端到另一端所需的最大时间
TIME_WAIT状态等待的这2MSL的时间,可以尽量保证历史数据从网络中消散,尽量避免历史数据影响新建立的连接
也可以尽量保证最后一个ACK报文能够被对方收到
确认应答机制
我们理解TCP的发送缓冲区:我们可以将TCP的发送缓冲区看做一个char sendbuffer[N]这样的数组,所以就相当于天然拥有了下标,我们把TCP的发送缓冲区当做了char类型的数组,而一个char类型的大小就是1字节,所以这就叫做面向字节流
TCP将每个字节的数据都进行了编号,即为序列号,当我们想发某一段数据时,我们把这一段数据拿出来,这段数据最后一个元素的下标就作为它的序号
每一个ACK都带有对应的确认序号,确认序号一般是序号+1,意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发
超时重传机制
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 主机A就断定发送的数据丢包了,就会进行重发
那么这里就会有两种情况,第一种就是上述所说的,报文丢包了;第二种是发出去的报文被对方收到了,但是对方发过来的应答丢包了
如果是第二种情况,带来的后果就是主机B有可能收到主机A发来的两份甚至更多的一样的数据,那么这时主机B就需要对数据进行去重
主机B收到了重复报文,可以通过序号进行去重
那么超时重传的超时时间应该如何设置?
如果超时时间设的太长, 会影响整体的重传效率
如果超时时间设的太短, 有可能会频繁发送重复的包
但是这个时间的长短, 随着网络环境的不同, 是有差异的,
网络好的时候,超时时间应该短一些,网络不好的时候,超时时间应该长一些
所以超时时间不是固定的
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
Linux中超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍
如果重发一次之后, 仍然得不到应答, 等待 2*500ms后再进行重传
如果仍然得不到应答, 等待 4*500ms 进行重传;依次类推, 以指数形式递增
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 会强制关闭连接
流量控制
主机A给主机B发消息,会根据主机B的接收能力,来动态调整自己发送的数据量,这种机制就叫做流量控制
接收端将自己可以接收的缓冲区大小放入TCP 首部中的 "16位窗口大小" 字段, 通过ACK端通知发送端
窗口大小字段越大, 说明网络的吞吐量越高
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端
发送端接受到这个窗口之后, 就会减慢自己的发送速度
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
因为是全双工的,所以流量控制同样是双向的
在进行流量控制的时候,第一次发送数据我们如何得知对方的接收能力呢?我们所要的对方的接收能力(即接收缓冲区中剩余空间的大小,如何得知呢?)
交换报文,第一次发送数据 并不是 第一次交换报文
三次握手的时候,交换双方的接收缓冲区的大小,TCP报头中有16位的窗口大小,也就是接收缓冲区的大小
滑动窗口
像上面所说的超时重传、流量控制等机制都是为了保证可靠性,而这里的滑动窗口则是为了提高网络数据发送效率的机制
确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答;收到ACK后再发送下一个数据段
这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间较长的时候
既然上述这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)
什么滑动窗口呢?如下图所示:
在发送缓冲区中,可以直接发送并且暂时不需要收到应答的数据所在的区间,称之为滑动窗口
滑动窗口在自己的发送缓冲区中,属于自己的发送缓冲区中的一部分
确认应答过的数据,就可以从缓冲区删掉
滑动窗口的本质:
sender方,可以一次性向对方推送数据的上限
滑动窗口也必须有上限,即由对方的接收能力决定(目前认知)
滑动窗口:
1.既想给对方推送更多数据
2.又想要保证对方来得及接收
由于我们将缓冲区理解为char类型的数组,所以滑动窗口本质就是指针或者下标
即int win_ start, win end
win_ start和win end更新时:
win_ start = 收到的应答报文中的确认序号
win end = win_ start + 收到的应答报文中的窗口大小
1.滑动窗口必须向右移动吗?
不一定,因为如果在给对方发送数据时,对方上层一直不取数据,只会导致滑动窗口左侧不断向右移动,而右侧可以不动,象征着对方的接收能力不断减少
2.滑动窗口可以为0吗?
可以,滑动窗口为0即为win_ start == win end,当我给对方发送数据,对方上层一直不取走数据,导致接收能力越来越差,而窗口也就越来越小,所以到最后就会导致滑动窗口为0了
3.如果没有收到开始的报文的应答,而是收到中间的应答,这种情况可能吗?影响结果吗?
这种情况是有可能,并不影响结果。因为有可能前面的应答丢了,接收到后面的应答了,不影响结果是因为,接收到的应答表示,该确认序号前的所有数据都收到了,所以即使没有收到前面的应答, 也默认前面的数据都收到了
4.超时重传,背后的含义就是没有收到应答的时候,数据必须被暂时保存起来
5.滑动窗口如果一直向右滑动,越界问题?
不存在的,因为TCP的发送缓冲区是环状结构的,会进行取模操作,不会出现越界的问题
当1001~2000这一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001" 一样
如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送
这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中
这种机制被称为 "高速重发控制"(也叫 "快重传")
快重传既然又快,又能重传,为什么我们还要超时重传呢?
因为快重传是有条件的,需要连续三次收到了同样的应答,就会重新发送,但是也有些情况不会连续收到三个一样的应答,所以就需要用到超时重传了
因此他们两个不是对立的,而是协作的
拥塞控制
上面学的这些机制,例如超时重传、快重传、流量控制、链接管理、滑动窗口、去重、按序到达、序号机制、确认应答机制,都解决的是端到端的可靠性问题
但是数据不仅仅是在端到端之间通信,还需要经过网络,还需要考虑网络的健康状态
虽然TCP有滑动窗口,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的
TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
拥塞窗口:单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值
所以滑动窗口的大小 = min(拥塞窗口,对方窗口大小[接收能力])
发送开始的时候,首先定义拥塞窗口大小为1;每次收到一个ACK应答, 拥塞窗口加1(实际发送的数据是:1、2、4、8按照指数级别增长的)
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
拥塞窗口增长速度, 是指数级别的, "慢启动" 只是指初始时慢, 但是增长速度非常快,为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍
此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
为何拥塞之后,前期是指数增长?
指数前期慢,后期非常快
一旦拥塞:
前期要让网络有一个缓一缓的机会,所以前期需要数据量少,速度慢一点
中后期,网络恢复了之后,尽快恢复通信的过程,否则可能会影响通信效率
又想解决网络拥塞问题,又想尽快恢复双方通信的效率,所以选择指数增长
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案
延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小
假设接收端缓冲区为1M,一次收到了500K的数据,如果立刻应答, 返回的窗口就是500K,但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来; 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高
我们的目标是在保证网络不拥塞的情况下尽量提高传输效率,那么所有的包都可以延迟应答么?
那肯定也不是:数量限制: 每隔N个包就应答一次; 时间限制: 超过最大延迟时间就应答一次
具体的数量和超时时间,不同的操作系统也有差异,一般N取2, 超时时间取200ms
捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的,意味着客户端给服务器发送一个数据时, 服务器也会给客户端回一个数据,那么这个时候ACK就可以搭顺风车,和服务器回应的数据一起发回给客户端
面向字节流
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可 以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP程序的读和写不需要一一匹配,例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次 read一个字节, 重复100次
对字节流中的数据进行解释是需要应用层自主完成的
粘包问题
因为TCP不关心字节流数据的格式,所以当我们进行上层任意读取时,极有可能对多个报文出现多读或少读,这样的情况称之为数据报的粘包问题
那么如何避免粘包问题呢?
归根结底就是一句话, 明确两个包之间的边界
对于定长的包, 保证每次都按固定大小读取即可,例如Request结构, 是固定大小的, 那么就从缓冲 区从头开始按sizeof(Request)依次读取即可
对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置
对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)
对于UDP协议来说, 是否也存在粘包问题呢?
很明显并不存在,因为UDP有标准报头定长的,和16位UDP长度,所以当收到报文时,去掉报头剩下的就是有效载荷,就能够保证读到的就是完整报文
基于TCP应用层协议:文章来源:https://www.toymoban.com/news/detail-854923.html
HTTP、HTTPS、SSH、Telnet、FTP、SMTP文章来源地址https://www.toymoban.com/news/detail-854923.html
到了这里,关于网络:TCP、UDP协议的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!