TCP相关知识整理

一、概要

TCP相关知识,参考C语言中文网中内容。这里给出一个最基本的demo代码。

二、整体架构流程

整体框架的流程如图所示,参考链接: Socket编写流程
在这里插入图片描述

三、技术名词解释

  1. 什么是socket?
    Socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。Socket本质上是一个抽象层,它是一组用于网络通信的API,包括了一系列的函数和数据结构,它提供了一种标准的网络编程接口,使得应用程序可以在网络中进行数据传输。

四、基础TCP的demo

4.1. server流程:

  1. socket()函数创建套接字
    (1)在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字:
// socket创建套节字
int socket(int af, int type, int protocol); 

(1)af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

(注意:127.0.0.1 是特殊的IP地址,表示本机地址)

(2)type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)

(3)protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

本demo使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:

// 创建 socket 描述符
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)		// 自动设置面向连接套接字与 无连接的套接字。  连接套接字只有tcp,无连接套节字只有udp
    {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
  1. bind() 函数
    bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址。

struct sockaddr_in{    
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型    
    uint16_t        sin_port;     //16位的端口号    
    struct in_addr  sin_addr;     //32位IP地址    
    char            sin_zero[8];  //不使用,一般用0填充
};

  1. listen() accept() 函数
    对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。
    listen函数的原型为:
int listen(int sock, int backlog);  //Linux

// TODO somaxconn

if (listen(server_fd, 3) < 0)
    {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

  当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定。

accept() 函数
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。其原型为:

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux
// 接受连接请求并处理
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0)
    {
        perror("accept failed");
        exit(EXIT_FAILURE);
    }
  1. fcntl控制文件描述符
    在使用 fcntl 函数时,您需要包含 <fcntl.h> 头文件。
    fcntl(file control)是一个用于对文件描述符进行控制的函数。它提供了一些操作选项来执行各种特定的操作,如修改文件状态标志、非阻塞 I/O 设置、文件锁定等。
// 设置非阻塞模式读
	int flags = fcntl(new_socket, F_GETFL, 0);
	flags |= O_NONBLOCK;
	fcntl(new_socket, F_SETFL, 0);  // 标志位用来设置  read的阻塞模式
  1. write()/read() 函数
    Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。
send() 函数相对于 write() 函数多了一个参数用于指定发送数据的标志。

ssize_t write(int fd, const void *buf, size_t nbytes);
ssize_t read(int fd, void *buf, size_t nbytes);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

server实现的收发:

// 从客户端接收数据
    valread = read(new_socket, buffer, BUFFER_SIZE);
	if (valread == -1) {
		printf("===========No value======\n");
		if (errno == EAGAIN || errno == EWOULDBLOCK) {
			// 处理非阻塞情况下的逻辑
			printf("===========当前为非阻塞模式======\n");
		} else {
			// 处理其他错误
		}
	} else if (valread == 0) {
		// 连接关闭
		printf("===========连接关闭======\n");
	} else {
		// 读取到数据,进行相应操作
		printf("===========读到数据======\n");
	}
    printf("%s\n", buffer);

    // 向客户端发送响应消息
    write(new_socket, hello, strlen(hello));```

4.2.  client流程:
1. socket函数如上创建套接字
```javascript
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
  1. connect函数用来建立连接
    原型为:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 
// 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
        perror("connection failed");
        exit(EXIT_FAILURE);
    }
  1. write&read函数
// 向服务器发送消息
    write(sock, hello, strlen(hello));
    printf("Hello message sent\n");

    // 从服务器接收响应消息
    valread = read(sock, buffer, BUFFER_SIZE);
    printf("%s\n", buffer);

五、socket的学习

5.1 socket是什么?套节字是什么?

什么是 socket?
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式
UNIX/Linux 中的 socket 是什么?
UNIX/Linux 中的一切都是文件!
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
a. 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
b. 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
一个文件描述符只是一个和打开的文件相关联的整数。它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接,网络连接也是一个文件。
注意:在tcp框架中,原始创建的socket的fd文件描述符和后面accept返回的文件描述符的不同。 原始创建的文件描述符用于监听client的请求,后面accept所返回的文件描述符代表client与server的链接。
Window 系统中的 socket 是什么?
Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。如果涉及 Windows 平台通常将使用“句柄”,如果涉及 Linux 平台则通常使用“描述符”。

5.2 套接字有哪些类型?socket有哪些类型?

  套接字类型一般采用Internet类型。常用的套接字有两种:1.流格式套接字(SOCK_STREAM)。面向连接的套接字;2. 数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”。
(一)、流格式套接字
SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
特点:
a. 数据在传输过程中不会消失;
b. 数据是按照顺序传输的;
c. 数据的发送和接收不是同步的。
  套接字类型一般采用Internet类型。常用的套接字有两种:1.流格式套接为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。
  流格式套接流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。

(二)、数据报格式套接字
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
特点:
a. 强调快速传输而非传输顺序;
b. 传输的数据可能丢失也可能损毁;
c. 限制每次传输的数据大小;
d. 数据的发送和接收是同步的。
数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。采用udp协议(用户数据报格式)。

5.3 面向连接和无连接的套接字到底有什么区别

一个简化的网络模型:
H1 ~ H6 表示计算机,A~E 表示路由器,发送端发送的数据必须经过路由器的转发才能到达接收端。
在这里插入图片描述
假设 H1 要发送若干个数据包给 H6,那么有多条路径可以选择,比如:
路径①:H1 --> A --> C --> E --> H6
路径②:H1 --> A --> B --> E --> H6
路径③:H1 --> A --> B --> D --> E --> H6
路径④:H1 --> A --> B --> C --> E --> H6
路径⑤:H1 --> A --> C --> B --> D --> E --> H6
(一) 、面向无连接的套接字
对于无连接的套接字,每个数据包可以选择不同的路径,比如第一个数据包选择路径④,第二个数据包选择路径①,第三个数据包选择路径②……当然,它们也可以选择相同的路径,那也只不过是巧合而已。
(二)、面向连接的套接字
面向连接的套接字在正式通信之前要先确定一条路径,没有特殊情况的话,以后就固定地使用这条路径来传递数据包了。当然,路径被破坏的话,比如某个路由器断电了,那么会重新建立路径。
在这里插入图片描述
 &emsp面向连接的套接字会比无连接的套接字多出很多数据包,因为发送端每发送一个数据包,接收端就会返回一个数据包。此外,建立连接和断开连接的过程也会传递很多数据包。
  有连接的数据包比无连接大很多,这意味着更大的负载和更大的带宽。许多即时聊天软件采用 UDP 协议(无连接套接字),与此有莫大的关系。
适用场景:两种套接字的特点决定了它们的应用场景,有些服务对可靠性要求比较高,必须数据包能够完整无误地送达,那就得选择有连接的套接字(TCP 服务),比如 HTTP、FTP 等;而另一些服务,并不需要那么高的可靠性,效率和实时才是它们所关心的,那就可以选择无连接的套接字(UDP 服务),比如 DNS、即时聊天工具等。

5.4 OSI网络七层模型简明教程

OSI 是 Open System Interconnection 的缩写,译为“开放式系统互联”。
OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
OSI 只是存在于概念和理论上的一种模型,它的缺点是分层太多,增加了网络工作的复杂性,所以没有大规模应用。
TCP/IP模型对齐进行了简化,分为了4层从下到上分别是接口层、网络层、传输层和应用层,
在这里插入图片描述
网络模型简而言之是进行数据封装的。
网络通信的流程说明:我们平常使用的程序(或者说软件)一般都是通过应用层来访问网络的,程序产生的数据会一层一层地往下传输,直到最后的网络接口层,就通过网线发送到互联网上去了。数据每往下走一层,就会被这一层的协议增加一层包装,等到发送到互联网上时,已经比原始数据多了四层包装。整个数据封装的过程就像俄罗斯套娃。
当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。

5.6 IP、mac和端口号

  1. IP地址
    一个IPV4资源有限,不能一台计算机一个ip。一台计算机一个 IP 地址是不现实的,往往是一个局域网才拥有一个 IP 地址。

  2. MAC地址
    一个局域网往往才能拥有一个独立的 IP;换句话说,IP 地址只能定位到一个局域网。真正能唯一标识一台计算机的是 MAC 地址

  3. 端口号
    端口号用于标识网络通信中不同的服务。在计算机网络中,每个应用程序都需要使用一个唯一的端口号来与其他应用程序进行通信。通过使用不同的端口号,网络应用程序可以在同一台计算机上同时运行而不会相互干扰。

5.7 socket缓冲区及阻塞模式

(一). socket缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。
在这里插入图片描述
缓冲区特性如下:
2. I/O缓冲区在每个TCP套接字中单独存在;
3. I/O缓冲区在创建套接字时自动生成;
4. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
5. 关闭套接字将丢失输入缓冲区中的数据。

(二). 阻塞模式
对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:

  1. 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。

  2. 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。

  3. 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。

  4. 直到所有数据被写入缓冲区 write()/send() 才能返回。
    当使用 read()/recv() 读取数据时:

  5. 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。

  6. 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。

  7. 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

5.8 TCP协议的粘包问题(数据的无边界性)

说明:TCP的client向server发送数据,但是server在recv之前会做10秒的延迟,等待10秒后,server接收到的数据会是client发送至缓冲区的数据,因为server认为它们是连续的数据流,然后server再返回给client。client由于一直没有recv数据会阻塞,直到server发送数据给client之后程序运行结束。

5.9 TCP三次握手

TCP建立连接时要传输三个数据包,俗称三次握手。三次握手成功后说明客户端与服务端建立了tcp连接。
额外补充:在客户端和服务器之间建立TCP连接时,可以通过一些网络诊断工具来检查连接是否成功建立。例如,可以使用ping命令来测试网络连接是否正常,也可以使用telnet命令来测试TCP连接是否正常。在telnet命令中,可以输入telnet <服务器IP地址> <端口号>来测试是否能够成功连接到服务器。如果连接成功,则表示三次握手已经完成,可以进行数据传输。
TCP数据报结构
在这里插入图片描述
带阴影的几个字段需要重点说明一下:

  1. 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。

  2. 确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。

  3. 标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:
    URG:紧急指针(urgent pointer)有效。
    ACK:确认序号有效。
    PSH:接收方应该尽快将这个报文交给应用层。
    RST:重置连接。
    SYN:建立一个新连接。
    FIN:断开一个连接。
    连接建立的过程:
    在这里插入图片描述
    这里三次握手的过程,不再详说,总之client以及server内部各自会生成并且记录下这个seq,而Ack用来确认是否为上个Seq+1,来保证数据的确实是发送成功了。客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求…

5.10 TCP传输的过程

建立连接后,传输数据的过程如下图:
在这里插入图片描述
如下的公式确认 Ack 号:
Ack号 = Seq号 + 传递的字节数 + 1
原因:为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。ACK按照传输的字节数增量传输的方式确认传输的字节数是否有丢失。
在这里插入图片描述
  上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。
为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。
a. 重传超时时间(RTO, Retransmission Time Out)
这个值太大了会导致不必要的等待,太小会导致不必要的重传,理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。
b. 重传次数
TCP数据包重传次数根据系统设置的不同而有所区别。有些系统,一个数据包只会被重传3次,如果重传3次后还未收到该数据包的 ACK 确认,就不再尝试重传。但有些要求很高的业务系统,会不断地重传丢失的数据包,以尽最大可能保证业务数据的正常交互。

5.11 TCP四次握手断开链接

如下图为TCP四次挥手的过程:
在这里插入图片描述
问题1:第一次挥手和第三次挥手为什么要挥手2次?
第一次挥手:client不再发送数据。第二次挥手:server知道client不会发送数据了。
第三次挥手:server不发送数据了。第四次挥手:client知道server不发送数据了,并且让server关闭连接
问题2:客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?
作用1超时重传。客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。
作用2:在TIME_WAIT状态下,客户端只是为了确保连接的可靠关闭,并防止出现重复。

5.12 如何优雅地断开TCP连接

close()/closesocket()和shutdown()的区别。
  shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
  调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
  默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

5.13 网络数据传输时的大小端问题

CPU 向内存保存数据的方式有两种:
大端序(Big Endian):高位字节存放到低位地址(高位字节在前)。
小端序(Little Endian):高位字节存放到高位地址(低位字节在前)。
不同 CPU 保存和解析数据的方式不同(主流的 Intel 系列 CPU 为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
常见的网络字节转换函数有
htons():host to network short,将 short 类型数据从主机字节序转换为网络字节序。
ntohs():network to host short,将 short 类型数据从网络字节序转换为主机字节序。
htonl():host to network long,将 long 类型数据从主机字节序转换为网络字节序。
ntohl():network to host long,将 long 类型数据从网络字节序转换为主机字节序。
inet_addr() 除了将字符串转换为 32 位整数,同时还进行网络字节序转换。

小结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,以下是计算机网络基础知识整理: 1. 计算机网络的定义和组成部分 计算机网络是指将多台计算机通过通信线路互相连接起来,实现数据交换和资源共享的系统。它主要由硬件、软件和协议三部分组成,其中硬件包括通信设备和网络设备,软件包括操作系统和网络协议栈,协议包括物理层、数据链路层、网络层、传输层和应用层。 2. OSI七层模型和TCP/IP四层模型 OSI七层模型是由国际标准化组织(ISO)发布的一种网络协议分层模型,包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。而TCP/IP四层模型包括网络接口层、网络层、传输层和应用层,是目前互联网最常用的协议分层模型。 3. IP地址和子网掩码 IP地址是指网络中每个主机或路由器的标识符,它由32位二进制数组成,通常用点分十进制表示。而子网掩码则是用来区分网络地址和主机地址的一种地址掩码,它也由32位二进制数组成,通常用点分十进制表示。 4. 路由和路由器 路由是指在计算机网络中,数据包从源地址到目的地址的传输路径。而路由器则是一种网络设备,它可以根据路由表中的路由信息,将数据包从一个网络传输到另一个网络。 5. 网络拓扑结构 网络拓扑结构是指计算机网络中各设备之间的物理连接方式。常见的网络拓扑结构包括总线型、星型、环型、树型和网状型等。 6. 网络安全 网络安全是指保护计算机网络不受非法侵入、破坏和窃取的一种技术和管理措施。它包括对网络设备、数据和用户进行安全防护、网络监测和漏洞修复等方面。 以上是计算机网络基础知识的简单整理,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值