下图为TCP协议段格式:
TCP的报头前两项和UDP一样(交付过程也类似UDP),根据16位目的端口号(源端口号),可以将数据向上交付给进行(进程绑定了端口号)。
根据上面的内容,引出一个疑问,我们知道UDP是面向数据报的,协议段中有整个报文的大小,那么对于TCP面向字节流,协议段中有没有整个报文或是有效载荷的大小呢
我们知道TCP是可靠的,而UDP是不可靠的,可靠就是好,不可靠就是坏吗?
那么存不存在 100%可靠性的协议呢? —— 不存在!
再次举一个例子:
我们知道,TCP是全双工 的:通信双方可以同时进行接收和发送数据。
对于 序号 与 确认序号:
两台主机通信的时候,显然 发送方 应确保数据的发送不能过快 / 过慢,如何保证发送放发送数据的速度适中呢?
通信双方同时向对方发送自身的接收能力,也是构成全双工的一点。
标记位:1bit表示的某种含义
如上图所示,实际的连接过程中,会有大量的client连接server,自然服务器端会有大量的连接,操作系统如何管理?
连接管理机制主要包括 三次握手和四次挥手、以及客户端与服务端的状态变化。
三次握手的过程可以由下图解释:
不妨考虑一下,为什么TCP协议规定了三次握手,一次、两次、四次…有什么问题吗?
而TCP所采用的三次握手的机制较好的解决了这一问题:
显然TCP的三次握手:
而在保证了客户端服务器通信功能完善的情况下,采用四次握手自然会造成:
额外的通信开销:四次握手需要额外的一轮通信,相比于三次握手,会增加一定的通信开销和时间延迟。
复杂性增加:四次握手会增加协议的复杂性,使得实现和管理起来更加繁琐,容易引入错误。
性能影响:每增加一轮握手,都会增加连接建立的时间延迟,可能对某些应用场景的性能产生影响。
且TCP的三次握手在大多数情况下已经被证明是足够可靠和安全的。
下图为四次挥手的过程:
TIME_WAIT
和CLOSE_WAIT
分别表示连接已关闭但仍在等待一段时间以确保任何延迟数据包到达或确认。
TIME_WAIT:
TIME_WAIT
状态。TIME_WAIT
状态下,该端口将等待一段时间(通常是2倍的MSL,最长生存时间,保证历史数据从网络中消散),确保在网络中所有的数据包都已经到达目的地,而对端已经收到并确认了最后一个ACK。TIME_WAIT
状态的连接不能再接收新的数据包,但仍然能够发送和接收一些控制报文(如RST报文)。CLOSE_WAIT:
CLOSE_WAIT
状态。CLOSE_WAIT
状态表示本地端已经收到远程端发送的FIN报文,并关闭了连接,但仍然需要等待本地应用程序处理完所有的数据后才能关闭套接字。CLOSE_WAIT
状态持续过长时间,可能会导致资源泄漏或连接耗尽的问题。连接建立(三次握手):
SYN_SENT
)。SYN_RECEIVED
)。ESTABLISHED
)。连接断开(四次挥手):
FIN_WAIT1
)。CLOSE_WAIT
)。LAST_ACK
)。TIME_WAIT
)。服务器收到ACK后进入CLOSED
状态,客户端在等待一段时间后也进入CLOSED
状态。如何理解TCP的发送缓冲区,可以将其看作一个char sendbuffer[NUM]
的数组
所以对于TCP,收到了重复报文,可以通过序号进行去重。
我们来看下图:
对于上面的情况,当主机A发送给主机B数据,且在特定的时间间隔未收到确认应答(网络拥堵、丢包等),就会重传数据。
实际上,未收到确认应答,也可能是由于ACK(应答)丢失:
对于超时重传,超时时间应该如何设置?
流量控制(Flow Control)用于确保发送端发送的数据不会超过接收端的处理能力,从而避免数据丢失、拥塞和重传等问题。
TCP的流量控制机制主要通过 滑动窗口 实现。
首先我们介绍流量控制的相关事项和步骤,后面对滑动窗口进行深度了解:
首先通过下图理解滑动窗口的作用:将发送缓冲区分成三部分。
根据上面的图,引出一个问题:滑动窗口在哪里?
滑动窗口的本质是什么?
滑动窗口是什么结构?内部结构是什么样的?来看下图:
滑动窗口滑动的过程:
我们知道,两台主机通信过程中,丢失了中间的ACK问题不大,只要有后面的确认应答(ACK)即可,但丢失了数据包(报文)该怎么办呢?:
上图问快重传的过程,触发条件如图中文字表示:
通过上面的内容我们知道,有了滑动窗口,TCP可以效可靠的发送大量的数据. 但是如果在通信开始阶段就发送大量的数据, 仍然可能引发问题.
两台主机在通信时,传输效率以及可靠性,需要考虑主机自身的各种因素,而网络更是不可忽略的一大因素:
从宏观角度看,网络并非只有我们考虑的两台主机使用,网络上有很多的计算机,可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能使网络状况更加糟糕的。
TCP引入 慢启动 机制, 先发少量的数据探路, 了解了当前的网络拥堵状态后, 再决定按照多大的速度传输数据。
(上图非自制)
少量的丢包, 仅仅触发超时重传,大量的丢包,我们就认为网络拥塞。
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 实际是TCP协议尽量快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
我们知道:滑动窗口的大小与拥塞窗口和对方的接收能力有关,如果每次接收端收到报文后就立刻发送ACK,此时发送端接收到的窗口就比较小:
延迟应答的步骤:
延迟方式
窗口越大,传输效率就越高,我们的目的是在网络不拥堵的前提下提高效率,有两种延迟的方式:
我们知道:TCP是全双工的
这个概念是比较好理解的,根据文字理解就好。
当我们 创建一个TCP的socket, 同时会在内核中创建一个 发送缓冲区 和一个 接收缓冲区
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
那么如何避免粘包问题?—— 明确两个包的界限
思考:UDP会不会出现粘包问题?
文章上面都介绍了哪些内容?可以看看下面TCP的总结,顺便思考自己是否理清了其内容。
可靠性
提高性能
其他
许多 基于TCP的应用层协议 被广泛用于各种网络应用中。这些协议利用TCP的可靠性和连接性来实现各种功能。
HTTP(超文本传输协议):用于在Web服务器和客户端之间传输超文本文档,支持可靠的数据传输和连接性。
SMTP(简单邮件传输协议):用于在邮件服务器之间或邮件客户端与服务器之间传输电子邮件消息。
POP3(邮局协议版本3):用于从邮件服务器上获取电子邮件消息。
IMAP(互联网消息访问协议):类似于POP3,也是用于从邮件服务器上获取电子邮件消息,但提供了更丰富的功能,如在服务器上管理邮件的状态。
FTP(文件传输协议):用于在客户端和服务器之间传输文件。
Telnet(远程终端协议):允许用户通过网络连接到远程主机并执行命令。
SSH(安全外壳协议):用于通过加密的方式在网络上安全地连接到远程主机。
首先提出一个问题: 我们知道 TCP是可靠连接,而TCP是不可靠连接,但可靠与否并非直接决定好坏,什么时候使用哪一种连接方式是要分情况讨论的:
TCP用于可靠传输和顺序交付的情形, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高,可靠性要求不那么高 的通信领域, 如 音频、视频传输;
根据以上的内容,我们进行一个思考,在我们编写TCP编程的相关代码时, 使用accept函数时,accept会不会参与到三次握手的过程?
答: 不需要参与,先建立好连接后,accept直接获取建立好的连接。
根据上面的分析,可以得到结论: 不需要调用accept就可以建立连接
如果上层来不及调用accept,且对端来了大量连接,怎么办 提前建立好连接?
答:服务器本身要维护一个连接队列,用于存储客户端的连接请求,其中与listen的第二个参数有关。
我们首先通过代码验证上面的结论: “不需要调用accept就可以建立连接”
写一个简单的套接字代码:
Sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
using std::string;
class Sock
{
private:
// 将listen的第二个参数设为1
const static int gbacklog = 1; // 监听队列的最大数量
public:
Sock() {}
~Sock() {}
int Socket() // 创建监听socket
{
int listenSock = socket(AF_INET, SOCK_STREAM, 0);
if(listenSock < 0) // 失败
{
// logMessage(FATAL, "socket error");
exit(2);
}
// logMessage(NORMAL, "create socket successfully");
return listenSock;
}
void Bind(int sock, uint16_t port, 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);
// local.sin_addr.s_addr = inet_addr(ip.c_str());
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
// logMessage(FATAL, "bind error | %d : %s", errno, strerror(errno));
exit(3);
}
}
void Listen(int sock)
{
if(listen(sock, gbacklog) < 0)
{
// logMessage(FATAL, "listen error | %d : %s", errno, strerror(errno));
exit(4);
}
// logMessage(NORMAL, "init server successfully");
}
int Accept(int sock, string* ip, uint16_t* port)
{
// 接收连接请求
struct sockaddr_in src;
socklen_t len = sizeof(src);
int serviceSock = accept(sock, (struct sockaddr*)&src, &len);
if(serviceSock < 0)
{
// logMessage(FATAL, "accept error | %d : %s", errno, strerror(errno));
exit(5);
}
if(port) *port = ntohs(src.sin_port);
if(ip) *ip = inet_ntoa(src.sin_addr);
return serviceSock;
}
// 连接
bool Connect(int sock, const 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)
{
// logMessage(NORMAL, "connect to %s successfully", server_ip.c_str());
return true;
}
else
return false;
}
// 主动关闭连接
void Close(int sock)
{ }
};
main.cc
#include "Sock.hpp"
// 不进行accept
int main()
{
// 创建一个Socket对象
Sock sock;
// 创建一个监听Socket,并且绑定到8080端口
int listenSock = sock.Socket();
sock.Bind(listenSock, 8080);
// 仅创建一个监听状态的套接字
// 开始监听
sock.Listen(listenSock);
while(true)
{
sleep(1);
}
return 0;
}
当开启进程后,此时发现进程进入了 监听状态
[usr@folder]:# ./TcpServer
[usr@folder]# netstat -nltp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 128/./TcpServer
此时如果我们 用其他主机对当前主机进行telnet
命令连接:
[root@iZ0jl4dw0m30ln5nvqiew5Z verifyListenParameters]# netstat -ntp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 1 172.22.99.98:8080 8.130.140.119:23124 ESTABLISHED
tcp 0 1 172.22.99.98:8080 8.130.140.119:56191 ESTABLISHED
如果此时再开一个连接,会出现:
[root@iZ0jl4dw0m30ln5nvqiew5Z verifyListenParameters]# netstat -ntp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 1 172.22.99.98:8080 8.130.140.119:23124 ESTABLISHED
tcp 0 1 172.22.99.98:8080 8.130.140.119:56191 ESTABLISHED
tcp 0 1 172.22.99.98:8080 8.130.140.119:44191 SYN_RECV
此时第三个连接的状态为SYN_RECV
,我们知道SYN_RECV
表示已经收到连接,但暂时不处理。
因为: Linux内核协议栈为一个tcp连接管理使用两个队列:
SYN_SENT
和SYN_RECV
状态的请求)established
状态,但是应用层没有调用accept
取走的请求)而全连接队列的长度会受到 listen 第二个参数的影响。
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了。
listen
的第二个参数的意义 :
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- stra.cn 版权所有 赣ICP备2024042791号-4
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务