10min速通Socket
- 套接字
- 简介
- 数据传输基本模型
- 1.TCP/IP模型
- 2.UDP模型
- 套接字类型
- 套接字(Socket)编程
- Socket 的连接
- 1.连接概述
- (1)基本概念
- (2)连接状态
- (3)连接队列
- 2.建立连接
- 3.关闭连接
- socket 编程接口介绍
- 数据的传输
- 1. 阻塞与非阻塞
- 2. I/O复用
- 数据的传输路径
- 数据报文收发的总体流程
- 1. 发送报文
- 2. 接收报文
- 整理工具:
- 参考文献:
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如:
UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.1
他们都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。
就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。
套接字
Socket是进程间通信的一种抽象,提供了一套API接口,对网络传输层一套具体的进程提供了抽象接口的调用
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现, socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭).
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
注意:其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。
进程间通信机制( s o c k e t I P C ) 进程间通信机制(socket IPC) 进程间通信机制(socketIPC)
openEuler通过Socket接口为用户程序提供网络服务。本节通过Socket介绍数据传输的基本模型. 2
套接字存在于进程与协议栈之间,为应用程序提供API调用、远程TCP、IP通信同住机内Unix通信、应用程序与内核之间的netlink之类的,一个socket对象。
简介
套接字(Socket)是计算机网络中进行数据通信的端点,它提供了一种在不同计算机或同一台计算机的不同进程之间进行数据交换的机制。在操作系统中,套接字是网络通信的基础,通过套接字可以实现进程间的通信(IPC, Inter-Process Communication)以及不同计算机之间的网络通信。
数据传输基本模型
套接字通常基于两种基本的数据传输模型:面向连接的传输(TCP,Transmission Control Protocol)和无连接的传输(UDP,User Datagram Protocol)。
1.TCP/IP模型
TCP/IP模型是一个四层模型,它包括应用层、传输层、网络层和链路层。
-
应用层:负责处理用户的应用程序,如Web浏览器、FTP客户端等。应用层协议定义了应用程序如何通过套接字接口使用网络服务。常见的应用层协议有HTTP、FTP、SMTP等。
-
传输层:负责数据的可靠传输。TCP是传输层的主要协议,它提供了面向连接的、可靠的、基于字节流的传输服务。TCP通过三次握手建立连接,并通过序列号确保数据的顺序性和可靠性。
-
网络层:负责将数据包从源地址路由到目的地址。IP(Internet Protocol)是网络层的主要协议,它定义了数据包的结构,并提供了地址和路由功能。
-
链路层:负责数据在物理介质(如以太网、Wi-Fi等)上的传输。链路层协议(如Ethernet、PPP等)定义了如何在物理网络上发送和接收数据。
2.UDP模型
UDP是另一种重要的传输层协议,与TCP不同,UDP提供的是无连接的、不可靠的数据传输服务。UDP数据包包含应用程序数据、源端口、目的端口和UDP长度等信息。由于UDP不提供可靠性保证,它通常用于对实时性要求较高或能够容忍偶尔丢包的场景,如视频流、VoIP等。
套接字类型
在openEuler或其他类Unix系统中,套接字通常分为三种类型:
-
流套接字(SOCK_STREAM):提供面向连接的、可靠的、基于字节流的传输服务,通常用于TCP协议。
-
数据报套接字(SOCK_DGRAM):提供无连接的、不可靠的、基于数据报的传输服务,通常用于UDP协议。
-
原始套接字(SOCK_RAW):允许直接访问底层协议,通常用于开发新的网络协议或进行网络调试。
套接字(Socket)编程
套接字编程通常涉及以下几个步骤:
- 创建套接字:使用
socket()
函数创建一个套接字,并指定协议族(如AF_INET用于IPv4协议)、套接字类型和协议。
创建Socket:
- 使用系统调用(如`socket()`函数)创建一个新的Socket描述符。
- 指定协议族(如`AF_INET`表示IPv4协议)、Socket类型(如`SOCK_STREAM`表示流式Socket)以及协议(通常为0,表示使用默认协议)。
- 绑定地址和端口:使用
bind()
函数将套接字绑定到一个本地地址和端口上,以便其他进程或计算机可以通过该地址和端口访问该套接字。
绑定地址:
- 使用`bind()`函数将Socket与本地地址和端口号绑定。
- 这样,当远程主机尝试连接时,系统就知道将连接路由到这个Socket。
- 监听和接受连接(对于服务器端):使用
listen()
函数使套接字进入监听状态,并使用accept()
函数接受客户端的连接请求。
监听连接:
- 对于服务端Socket,使用`listen()`函数将其置于监听状态。
- 这将允许远程主机发起连接请求。
- 连接和发送/接收数据(对于客户端):使用
connect()
函数连接到服务器端的套接字,并使用send()
或write()
函数发送数据,使用recv()
或read()
函数接收数据。
接受连接:
- 当有远程主机发起连接请求时,服务端使用`accept()`函数接受连接。
- `accept()`会创建一个新的Socket描述符,用于与远程主机通信。
- 原来的Socket(监听Socket)继续处于监听状态,等待新的连接请求。
- 关闭套接字:使用
close()
或shutdown()
函数关闭套接字,释放相关资源。
通过这些步骤,可以在openEuler或其他类Unix系统中进行基于套接字的网络通信编程。
Socket 的连接
更准确来说是,流式Socket连接的相关内容
1.连接概述
基本概念
在系统场景中系统一般提供三种类型的Socket:也就是
-
流式Socket(Stream Socket):
- 基于TCP(Transmission Control Protocol,传输控制协议)的Socket。
- 提供面向连接的、可靠的、基于字节流的传输服务。
- 数据在发送和接收时保持顺序,无重复,并且无丢失。
- 需要三次握手建立连接,四次挥手断开连接。
-
数据报Socket(Datagram Socket):
- 基于UDP(User Datagram Protocol,用户数据报协议)的Socket。
- 提供无连接的、不可靠的、基于数据报的服务。
- 数据在发送和接收时可能不保持顺序,也可能出现重复或丢失。
- 不需要建立和维护连接,适用于对实时性要求较高,但数据可靠性要求不高的场景。
-
原始Socket(Raw Socket):
- 允许应用程序直接操作底层协议,绕过内核协议栈。
- 开发者可以自定义协议头,直接构造数据包。
- 这类Socket在常规应用中较少使用,因为它需要对网络协议有深入的了解。
- 流式Socket(Stream Socket)基于TCP,要三次握手的那个,可靠的字节流。
- 数据报Socket(Dategram Socket)基于UDP,基于数据报的非可靠数据传输服务
- 原始Socket(Raw Socket)绕过内核协议栈,填充各级协议头直接构造数据包,常规应用不使用。
TCP通信需要先建立虚拟链路(通信双方的一个连接,connection),TCP/IP通讯下,Socket采用四元组(源IP、源端口、目的IP、目的端口)标识(identity)
i
d
e
n
t
i
t
y
identity
identity
(1)基本概念
同一时间只能处理一个连接
openEuler提供了两种列缓存连接请求,分别为半连接队列和连接队列,当服务端建立具体的请求时,半连接队列和连接队列在其中起缓存作用。
第一次的握手,
(2)连接状态
连接状态
//源文件:include/net/tcp_states.h
enum{
TCP_ESTABLISHED = 1;
TCP_SYN_SENT,
TCP_SYN_RECV,
TCP_FIN_WAIT1,
TCP_FIN_WAIT2,
TCP_CLOSE,
TCP_CLOSE_WAIT,
TCP_LAST_ACK,
TCP_LISENCE,
...
};
//连接状态
TCP连接状态由tcp_states.h中定义的枚举类型表示。以下是一些常见的TCP连接状态:
#TCP_ESTABLISHED:连接已经建立,数据可以传输。
#TCP_SYN_SENT:连接正在建立中,服务器已发送SYN报文,等待客户端的SYN-ACK报文。
TCP_SYN_RECV:连接正在建立中,服务器已收到客户端的SYN报文,并发送了SYN-ACK报文,等待客户端的ACK报文。
#TCP_FIN_WAIT1:连接正在关闭中,应用程序已调用close()函数,等待对方发送FIN报文。
TCP_FIN_WAIT2:连接正在关闭中,已收到对方的FIN报文,并发送了ACK报文,等待对方确认。
TCP_CLOSE:连接已关闭,无数据传输。
#TCP_CLOSE_WAIT:连接正在关闭中,对方已发送FIN报文,等待应用程序关闭连接。
TCP_LAST_ACK:连接正在关闭中,已发送最后的ACK报文,等待对方确认。
(3)连接队列
2.建立连接
- 数据传输:
- 一旦连接建立,就可以使用
send()
或write()
函数发送数据,以及使用recv()
或read()
函数接收数据。 - 数据传输是双向的,可以在两个Socket之间自由流动。
- 一旦连接建立,就可以使用
TCP连接的建立通常通过三次握手来完成:
1. **SYN(SYN=1, seq=x)**:客户端发送一个SYN报文给服务器,并进入SYN_SENT状态,等待服务器的确认。
2. **SYN-ACK(SYN=1, ACK=1, seq=y, ack=x+1)**:服务器收到SYN报文后,发送一个SYN-ACK报文给客户端,并进入SYN_RECV状态,等待客户端的确认。
3. **ACK(ACK=1, seq=x+1, ack=y+1)**:客户端收到SYN-ACK报文后,发送一个ACK报文给服务器,并进入ESTABLISHED状态,表示连接已建立。服务器收到ACK报文后,也进入ESTABLISHED状态。
3.关闭连接
- 关闭连接:
- 当数据传输完成后,使用
close()
函数关闭Socket。 - 对于服务端,可能需要显式关闭监听Socket以停止接受新的连接。
- 当数据传输完成后,使用
TCP连接的关闭通常通过四次挥手来完成:
1. **FIN(FIN=1, seq=u)**:应用程序调用`close()`函数关闭连接,客户端发送一个FIN报文给服务器,并进入FIN_WAIT_1状态,等待服务器的确认。
2. **ACK(ACK=1, seq=v, ack=u+1)**:服务器收到FIN报文后,发送一个ACK报文给客户端,并进入CLOSE_WAIT状态,表示已知道客户端要关闭连接。
3. **FIN(FIN=1, seq=w)**:当服务器准备好关闭连接时,发送一个FIN报文给客户端,并进入LAST_ACK状态,等待客户端的确认。
4. **ACK(ACK=1, seq=u+1, ack=w+1)**:客户端收到FIN报文后,发送一个ACK报文给服务器,并进入TIME_WAIT状态,等待足够的时间以确保服务器收到ACK报文。服务器收到ACK报文后,关闭连接。
上图为四次挥手的示意图
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
对应函数接口如图:
socket 编程接口介绍
下面介绍socket 编程中使用到的一些接口函数。使用 socket 接口需要在我们的应用程序
代码中包含两个头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
1. socket()函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数类似于 open()函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符,通常把这个文件描述符称为 socket 描述符(socket descriptor),这个 socket 描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
该函数包括 3 个参数,如下所示:
d
o
m
a
i
n
domain
domain
参数 domain 用于指定一个通信域;这将选择将用于通信的协议族。
对于 TCP/IP 协议来说,通常选择 AF_INET 就可以了,当然如果你的 IP 协议的版本支持 IPv6,那么可以选择 AF_INET6。
t
y
p
e
type
type
参数 type 指定套接字的类型,当前支持的类型有:
p
r
o
t
o
c
o
l
protocol
protocol
参数 protocol 通常设置为 0
,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为 SOCK_STREAM 的默认协议是传输控制协议 (Transmission Control Protocol,TCP 协议)
在 AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP。
调用 socket()
与调用 open()
函数很类似,调用成功情况下,均会返回用于文件 I/O 的文件描述符,只不过对于 socket()来说,其返回的文件描述符一般称为 socket 描述符。当不再需要该文件描述符时,可调用close()
函数来关闭套接字,释放相应的资源。
如果 socket() 函数调用失败,则会返回-1,并且会设置 errno 变量以指示错误类型。
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打开套接字
if (0 > socket_fd) {
perror("socket error");
exit(-1);
}
......
......
close(socket_fd); //关闭套接字
2. bind()函数
bind()函数原型如下所示:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数用于将一个 IP 地址或端口号与一个套接字进行绑定(将套接字与地址进行关联)。将一个客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。一般来讲,会将一个服务器的套接字绑定到一个众所周知的地址—即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址(注意这里说的地址包括 IP 地址和端口号)。因为对于客户端来说,它与服务器进行通信,首先需要知道服务器的 IP 地址以及对应的端口号,所以通常服务器的 IP 地址以及端口号都是众所周知的。
调用 bind()函数将参数 sockfd 指定的套接字与一个地址 addr 进行绑定,成功返回 0,失败情况下返回-1,并设置 errno 以提示错误原因。
参数 addr 是一个指针,指向一个 struct sockaddr 类型变量,如下所示:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
第二个成员 sa_data 是一个 char 类型数组,一共 14 个字节,在这 14 个字节中就包括了 IP 地址、端口号等信息,这个结构对用户并不友好,它把这些信息都封装在了 sa_data 数组中,这样使得用户是无法对sa_data 数组进行赋值。事实上,这是一个通用的 socket 地址结构体。
一般我们在使用的时候都会使用 struct sockaddr_in 结构体,sockaddr_in 和 sockaddr 是并列的结构(占用的空间是一样的),指向 sockaddr_in 的结构体的指针也可以指向 sockadd 的结构体,并代替它,而且sockaddr_in 结构对用户将更加友好,在使用的时候进行类型转换就可以了。该结构体内容如下所示:
struct sockaddr_in {
sa_family_t sin_family; /* 协议族 */
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP 地址 */
unsigned char sin_zero[8];
};
这个结构体的第一个字段是与 sockaddr 结构体是一致的,而剩下的字段就是 sa_data 数组连续的 14 字节信息里面的内容,只不过从新定义了成员变量而已,sin_port 字段是我们需要填写的端口号信息,sin_addr字段是我们需要填写的 IP 地址信息,剩下 sin_zero 区域的 8 字节保留未用。
最后一个参数 addrlen 指定了 addr 所指向的结构体对应的字节长度。
使用示例
struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零
//填充变量
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);
//将地址与套接字进行关联、绑定
bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
Tips: 代码中的 htons 和 htonl 并不是函数,只是一个宏定义,主要的作用在于为了避免大小端的问题,需要这些宏需要在我们的应用程序代码中包含头文件<netinet/in.h>。
Tips:bind()函数并不是总是需要调用的,只有用户进程想与一个具体的 IP 地址或端口号相关联的时候才需要调用这个函数。如果用户进程没有这个必要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择,通常在客户端应用程序中会这样做。
3. listen()函数
listen()函数只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函数在一般在 bind()函数之后调用,在 accept()函数之前调用,它的函数原型是:
int listen(int sockfd, int backlog);
无法在一个已经连接的套接字(即已经成功执行 connect()的套接字或由 accept()调用返回的套接字)上执行 listen()。
参数 backlog 用来描述 sockfd 的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为 TCP 连接是一个过程,由于同时尝试连接的用户过多,使得服务器进程无法快速地完成所有的连接请求,那怎么办呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限,这个 backlog 参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。
- accept()函数
服务器调用 listen()函数之后,就会进入到监听状态,等待客户端的连接请求,使用 accept()函数获取客户端的连接请求并建立连接。函数原型如下所示:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
① 调用 socket()函数打开套接字;
② 调用 bind()函数将套接字与一个端口号以及 IP 地址进行绑定;
③ 调用 listen()函数让服务器进程进入监听状态,监听客户端的连接请求;
④ 调用 accept()函数处理到来的连接请求。
accept()函数通常只用于服务器应用程序中,如果调用 accept()函数时,并没有客户端请求连接(等待连接队列中也没有等待连接的请求),此时 accept()会进入阻塞状态,直到有客户端连接请求到达为止。当有客户端连接请求到达时,accept()函数与远程客户端之间建立连接,accept()函数返回一个新的套接字。这个套接字与 socket()函数返回的套接字并不同,socket()函数返回的是服务器的套接字(以服务器为例),而accept()函数返回的套接字连接到调用 connect()的客户端,服务器通过该套接字与客户端进行数据交互,譬如向客户端发送数据、或从客户端接收数据。
所以,理解 accept()函数的关键点在于它会创建一个新的套接字,其实这个新的套接字就是与执行
connect()(客户端调用 connect()向服务器发起连接请求)的客户端之间建立了连接,这个套接字代表了服务器与客户端的一个连接。如果 accept()函数执行出错,将会返回-1,并会设置 errno 以指示错误原因。
参数 addr 是一个传出参数,参数 addr 用来返回已连接的客户端的 IP 地址与端口号等这些信息。
参数addrlen 应设置为 addr 所指向的对象的字节长度,如果我们对客户端的 IP 地址与端口号这些信息不感兴趣,可以把 arrd 和 addrlen 均置为空指针 NULL。
5. connect()函数
connect()函数原型如下所示:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
该函数用于客户端应用程序中,客户端调用 connect()函数将套接字 sockfd 与远程服务器进行连接,参数 addr 指定了待连接的服务器的 IP 地址以及端口号等信息,参数 addrlen 指定了 addr 指向的 struct sockaddr对象的字节大小。
客户端通过 connect()函数请求与服务器建立连接,对于 TCP 连接来说,调用该函数将发生 TCP 连接的握手过程,并最终建立一个 TCP 连接,而对于 UDP 协议来说,调用这个函数只是在 sockfd 中记录服务器IP 地址与端口号,而不发送任何数据。
函数调用成功则返回 0,失败返回-1,并设置 errno 以指示错误原因。
- 发送和接收函数
一旦客户端与服务器建立好连接之后,我们就可以通过套接字描述符来收发数据了(对于客户端使用socket()返回的套接字描述符,而对于服务器来说,需要使用 accept()返回的套接字描述符),这与我们读写普通文件是差不多的操作,譬如可以调用 read()或 recv()函数读取网络数据,调用 write()或 send()函数发送数据。
read()函数
read()函数大家都很熟悉了,通过 read()函数从一个文件描述符中读取指定字节大小的数据并放入到指定的缓冲区中,read()调用成功将返回读取到的字节数,此返回值受文件剩余字节数限制,当返回值小于指定的字节数时并不意味着错误;这可能是因为当前可读取的字节数小于指定的字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数据,或者 read()函数被信号中断等),出错返回-1 并设置 errno,如果在调 read 之前已到达文件末尾,则这次 read 返回 0。
套接字描述符也是文件描述符,所以使用 read()函数读取网络数据时,read()函数的参数 fd 就是对应的套接字描述符。
recv()函数
recv()函数原型如下所示:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
不论是客户端还是服务器都可以通过 revc()函数读取网络数据,它与 read()函数的功能是相似的。参数sockfd 指定套接字描述符,参数 buf 指向了一个数据接收缓冲区,参数 len 指定了读取数据的字节大小,参数 flags 可以指定一些标志用于控制如何接收数据。
函数 recv()与 read()很相似,但是 recv()可以通过指定 flags 标志来控制如何接收数据3
数据的传输
在网络编程中,数据的传输是核心功能之一。数据传输涉及到许多概念和技术,包括阻塞
与非阻塞I/O、I/O复用
等。这些概念和技术对于理解和优化网络性能至关重要。
1. 阻塞与非阻塞
阻塞I/O:在阻塞I/O模型中,当应用程序发起一个读或写请求时,如果数据还没有准备好,应用程序将被阻塞,直到数据准备好为止。在这个过程中,应用程序无法执行其他任务。这种模型简单易懂,但对于需要同时处理多个I/O操作的应用来说,效率较低。
非阻塞I/O:在非阻塞I/O模型中,应用程序发起读或写请求时,如果数据没有准备好,操作会立即返回一个错误,而不是阻塞应用程序。这样,应用程序可以继续执行其他任务,直到数据准备好为止。非阻塞I/O提高了应用程序的响应性,但需要开发者自己管理多个I/O操作的轮询和调度,增加了编程的复杂性。
2. I/O复用
I/O复用:I/O复用技术允许单个线程同时处理多个I/O操作。通过复用描述符集合,线程可以在多个描述符之间进行切换,从而实现对多个I/O操作的并行处理。I/O复用技术包括select、poll和epoll等。
select:是最早的I/O复用技术之一。它允许应用程序监视多个文件描述符的状态变化。当某个文件描述符的状态发生变化时,select会通知应用程序,从而进行相应的读或写操作。但是,select在处理大量文件描述符时性能较低,因为它采用轮询的方式来检查每个文件描述符的状态。
poll:poll是select的改进版本,它解决了select在处理大量文件描述符时性能低下的问题。poll使用一个链表来存储文件描述符,而不是使用位图,这使得它在处理大量文件描述符时更加高效。
epoll:epoll是Linux特有的I/O复用技术,它采用事件驱动的方式来实现对多个I/O操作的并行处理。epoll通过一个红黑树来管理文件描述符,并使用一个事件表来存储触发的事件。当某个文件描述符的状态发生变化时,epoll会通知应用程序,并将事件添加到事件表中。这样,应用程序可以一次性处理多个触发的事件,提高了处理效率。
在实际应用中,开发者需要根据具体场景和需求选择合适的I/O模型和复用技术。对于需要处理大量并发连接的应用,非阻塞I/O和I/O复用技术通常是更好的选择。而对于简单的、不需要处理大量并发连接的应用,阻塞I/O模型可能更加适合。
数据的传输路径
数据报文收发的总体流程
在计算机网络中,数据的传输路径涉及多个层次和组件,从物理层到应用层。数据报文(或称数据包)的收发遵循一个总体流程,包括发送报文和接收报文两个主要阶段。
1. 发送报文
发送报文的过程通常涉及以下步骤:
应用层处理:
- 应用程序生成要发送的数据。
- 应用程序使用适当的协议(如HTTP、FTP、SMTP等)对数据进行封装,添加必要的头部信息(如目标地址、端口号等)。
传输层处理:
- 传输层(通常是TCP或UDP)接收来自应用层的数据段,并添加传输层头部信息(如序列号、窗口大小、校验和等)。
- 如果是TCP连接,传输层负责将数据分割成适当大小的数据段,并处理流量控制和拥塞控制。
网络层处理:
- 网络层(通常是IP层)接收来自传输层的数据包,并添加网络层头部信息(如源IP地址、目标IP地址等)。
- 路由器根据数据包中的IP地址信息进行路由选择,将数据包转发到下一个目标地址。
数据链路层处理:
- 数据链路层(如以太网)将网络层传来的数据包封装成帧,添加帧头部和尾部(如MAC地址、帧类型等)。
- 帧通过物理介质(如网线、无线信号等)传输到相邻的节点。
物理层传输:
- 物理层负责在物理介质上传输比特流。这涉及到电信号、光信号或无线信号的传输。
2. 接收报文
接收报文的过程是发送过程的逆向操作,通常涉及以下步骤:
物理层接收:
- 物理层接收到来自物理介质的比特流,并将其转换为数据链路层可以理解的信号。
数据链路层处理:
- 数据链路层从接收到的信号中提取帧,验证帧的完整性(如CRC校验)。
- 如果帧有效,数据链路层将其传递给网络层。
网络层处理:
- 网络层从帧中提取数据包,并根据数据包中的IP地址信息进行路由处理。
- 如果数据包的目标地址与本地节点匹配,网络层将其传递给传输层。
传输层处理:
- 传输层(TCP或UDP)接收数据包,并验证其完整性和正确性(如序列号、校验和等)。
- 如果是TCP连接,传输层负责重新排序收到的数据段,并进行流量控制和拥塞控制。
应用层处理:
- 应用层从传输层接收数据,并去除协议头部信息。
- 应用程序处理接收到的数据,并根据需要执行相应的操作(如显示网页、保存文件等)。
在实际的网络通信中,发送和接收报文的过程可能涉及多个中间节点(如路由器、交换机等)的转发和处理。此外,为了保证数据传输的可靠性和效率,网络协议栈中的各个层次通常会使用各种算法和机制(如差错控制、流量控制、拥塞控制等)来优化数据传输过程。4
整理工具:
- 绘图工具:VISO
- 截图工具:Photor
- 文本工具:typora
- 书签工具:pocket
参考文献:
Linux 的 SOCKET 编程详解,hguisu ↩︎
《openEuler操作系统(第2版)》,任炬、张尧学 ↩︎
Socket 编程基础,比特冬哥 ↩︎
Socket 编程详解:从基本概念到实例应用(TCP|UDP C语言实例详解) ,二进制coder ↩︎