添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Linux协议栈posix接口浅析

Linux协议栈posix接口浅析

Linux协议栈与posix api的关系

众所周知, Linux内核协议栈 Linux内核的网络管理模块的具象化表达 实例,用户空间如果想使用 Linux内核 提供的网络服务就需要使用内核提供的一系列网络相关的 posix接口 (操作系统 原语 ,或者说原生接口)。实际上,当前posix网络接口(套接字接口)的API是基于4.4 BSD套接字接口的。尽管这些年套接字接口有些细微的变化, 但是当前的套接字接口与20世纪80年代早期4.2BSD所引入的接口很类似。

以TCP通信举例,我们想开发一套基于C/S通信架构的服务,需要获取一个tcp的socket,该socket本质上是一个文件描述符,也可以说是一个内核中tcb(tcp control block)的映射。当我们在应用层调用connect,send等posix相关的api(系统调用)时,内核就会操作tcb的传输内存,按照tcp协议的标准,进行握手建立连接并发送用户数据。

本文旨在通过介绍这些posix api,分析用户态接口表达与深层内核操作的联系,帮助我们更好的理解linux内核网络模块。

客户端&通用posix接口

客户端通常有以下常用接口,在服务端相同的功能实现上,poxis提供的是同一套接口,因此服务端部分仅介绍其独有的posix api;

socket

套接字是通信端点的抽象。正如使用文件描述符访问文件, 应用程序用套接字描述符访问套接字. 套接字描述符在unix/linux系统中被当作是一种文件描述符。事实上, 许多处理文件描述符的函数(如read和l write)可以用于处理套接字描述符。

@domin		:域,用于确定通信的特性,可以使用AF_xx宏,AF就是只address family,地址族
				AF_INET :ipv4因特网域
				AF_INET6:ipv6因特网域
				AF_UNIX :UNIX域
				AF_UPSPEC:未指定
@type		:确定套接字的类型, 进一步确定通信特征
				SOCK_DGRAM		:固定长度的、无连接的、不可靠的报文传递
				SOCK_RAW		:IP协议的数据报接口〈在POSIX1中为可选)
				SOCK_SEQPACKET	:固定长度的、有序的、可靠的、面向连接的报文传递
				SOCK_STREAM		:有序的、可靠的、双向的、而向连接的字节流
@protocol	:通常是0, 表示为给定的域和套接字类型选择默认协议。
				如果domin是AF_INET,type是SOCK_STREAM,protocol就是tcp
				如果domin是AF_INET,type是SOCK_DGRAM,protocol就是udp
				您也可以指定其值,但是需要符合协议标准。
				IPPROTO_IP:IPv4网际协议
				IPPROTO_IPV6:IPv6网际协议(在POSX.1 中为可选
				IPPROTO_ICMP:因特网控制报文协议(Internet Control Message Protocol)
				IPPROTO_RAW:原始IP数据包协议(在POSIX.1中为可选〉
				IPPROTO_TCP:传输控制协议
				IPPROTO_UDP:用户数据报协议(User Datagram Protocol)
int socket (int domain, int type, int protocol);
//返回值: 若成功, 返回文件〈套接字〉描述符. 若出错, 返回-1

调用 socket 与调用open相类似。在两种情况下,均可获得用于IO的文件描述符。当不再需要该文件描述符时,调用close 来关闭对文件或套字的访问,并且释放该描述符以便重新使用。

虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。例如,lseek不能以套接字描述符为参数,因为套接字不支持文件偏移量的概念。

同时,调用socket后,内核为新的套接字分配一个唯一的文件描述符,并返回该文件描述符给应用程序。同时分配数据结构,内核在内存中为该套接字分配相应的数据结构(如sock结构体),并将其与该文件描述符相关联。这些数据结构包含有关套接字状态、传输控制块、缓冲区等信息。

shutdown

套接字通信是双向的。可以采用 shutdown 函数来禁止一个套接字的 IO。

@sockfd	:被操作的socket fd
@how	:如何关闭
			SHUT_RD:无法从套接字读取数据
			SHUT_WR:无法使用套接字发送数据
			SHUT_RDWR:则既无法读取数据,又无法发送数据
#include <sys/socket,h>
int shutdown (int sockfd,int how);
//返回值:若成功,返回 0;若出错,返回-1

这里也要引出一个问题,既然**close()**能够关闭一个套接字,为何还使用 shutdown 呢?这里有若干理由。

首先,只有最后一个活动引用关闭时,close 才释放网络端点。这意味着如果复制一个套接字(如采用 dup),要直到关闭了最后一个引用它的文件描述符才会释放这个套接字。而 shutdown 允许使一个套接字处于不活动状态,和引用它的文件描述符数目无关。

其次,有时可以很方便地关闭套接字双向传输中的一个方向。例如,如果想让所通信的进程能够确定数据传输何时结束,可以关闭该套接字的写端,然而通过该套接字读端仍可以继续接收数据。

但是使用close()关闭socket在相当多的历史工程中得到了应用,为了工程的稳定性,有时我们又不得不选择close;

setsockopt

套接字机制提供套接字选项setsockopt接口来控制套接字行为。

@sockfd:socket
@level:参数 level 标识了选项应用的协议。如果选项是通用的套接字层次选项,则 level 设置成SOL_SOCKET。
否则,level 设置成控制这个选项的协议编号。对于TCP 选项,level是IPPROTO_TCP对于IP,level是IPPROTO_IP。
@option:通用套接字层次选项,详见下图
@val:根据选项的不同指向一个数据结构或者一个整数。一些选项是 on/off开关。如果整数非0,则启用选项。如果整数为0,则禁止选项。
@len:参数len指定了val 指向的对象的大小。
int setsockopt(int sockfd, int level, int option, const void *val,socklen_t len);
//返回值:若成功,返回0:若出错,返回-1

无独有偶,我们也可以使用getsockopt接口获取套接字状态。

bind

该接口用于绑定地址和端口号,如果应用程序调用bind函数绑定了地址和端口号,则内核将相应地设置套接字的本地IP地址和端口号。否则,系统会自动分配一个可用的未绑定端口号。

int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
//返回值:若成功,返回 0:若出错,返回-1

对于使用的地址有以下一些限制。

  • 在进程正在运行的计算机上,指定的地址必须有效;不能指定一个其他机器的地址。 地址必须和创建套接字时的地址族所支持的格式相匹配。
  • 地址中的端口号必须不小于1024,除非该进程具有相应的特权(即超级用户)。一般只能将一个套接字端点绑定到一个给定地址上,尽管有些协议允许多重绑定。对于因特网域,如果指定IP地址为 INADDR_ANY (<netinet/in.h>中定义的),套接字端点可以被绑定到所有的系统网络接口上。这意味着可以接收这个系统所安装的任何一个网卡的数据包。

相关视频推荐

tcpip,accept,11个状态,细枝末节的秘密,还有哪些你不知道

c++八股文重点,网络的posix api实现原理

100行代码,开启tcp/ip协议栈实现之路,准备好linux环境

免费学习地址:Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

需要C/C++ Linux服务器架构师学习资料加qun 579733396 获取(资料包括 C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),免费分享

connect

@sockfd:发起连接使用的socket
@addr:发起连接的设备信息结构体指针
@len:结构体大小
int connect(int sockfd,const struct sockaddr *addr, socklen_t len);
//返回值:若成功,返回 0;若出错,返回-1

connect用于发起连接,在addr中填充好连接信息后,就可以通过该接口向服务器发起三次握手建立连接。大致过程如下:

当应用程序调用connect()函数时,内核会根据指定的目标地址和端口号创建一个TCP套接字,并向远程主机发送SYN包以发起连接请求。此时开始第一次握手。
如果远程主机接收到了SYN包并愿意建立连接,则它会向本地主机发送一个ACK包作为应答,并同时发送自己的SYN包以确认连接请求。此时开始第二次握手。
当本地主机接收到了来自远程主机的SYN和ACK包后,会向远程主机发送一个ACK包以确认连接已经建立。此时完成了第三次握手。

在 connect 中指定的地址是我们想与之通信的服务器地址。如果 sockfd 没有绑定到一个地址,connect会给调用者绑定一个默认地址。

当尝试连接服务器时,出于一些原因,连接可能会失败。要想一个连接请求成功,要连接的计算机必须是开启的,并且正在运行,服务器必须绑定到一个想与之连接的地址上,并且服务器的等待连接队列要有足够的空间(详见下文listen) 。因此,应用程序必须能够处理connect 返回的错误,这些错误可能是由一些瞬时条件引起的。

send

既然一个 套接字端点 表示为一个 文件描述符 ,那么只要建立连接,就可以使用 read和 write来通过套接字通信。在套接字描述符上使用 read 和 write 是非常有意义的,因为这意味着可以将套接字描述符传递给那些原先为处理本地文件而设计的函数。而且还可以安排将套接字描述符传递给子进程,而该子进程执行的程序并不了解套接字。

尽管可以通过 read 和 write 交换数据,但这就是这两个函数所能做的一切。如果想指定选项,从多个客户端接收数据包,或者发送带外数据,就需要使用6(三个发送函数,三个配套的接收函数) 个特定函数。

ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
//返回值:若成功,返回发送的字节数:若出错,返回-1

flag的定义如下图所示:

值得一提的是,send即使返回了发送成功的字节数,也只能代表数据已经无误的拷贝到了内核协议栈的发送缓冲区队列,或者理解为发送到了网络驱动程序上,并不能代表接收端已经接收到了发送的字节数。

ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,const struct sockaddr *destaddr,  socklen_t destlen);
//返回值:若成功,返回发送的字节数:若出错,返回-1

sendto与send类似,但是他可以在一个无连接的套接字上指定一个确定的目标地址。

ssize t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//返回值:若成功,返回发送的字节数;若出错,返回-1

该接口可以指定多重缓冲区传输数据,类似于writev函数。

recv

当从套接字上接收数据时,可以直接从socket上read数据。与read类似,接收接口有以下api可供选择:

ssize_t recv(int socfd, void *buf, size_t nbytes, int flags);
//返回值:返回数据的字节长度:若无可用数据或对等方已经按序结束,返回0:若出错,返回-1

flags的详细定义可看下图:

ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,struct sockaddr *restrict addr, socklen_t *restrict addrlen);
//返回值:返回数据的字节长度:若无可用数据或对等方已经按序结束,返回0:若出错,返回-1

与recv不同的是,recvfrom可以根据出参addr的内容获取发送者的五元组信息;

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//返回值:返回数据的字节长度;若无可用数据或对等方已经按序结束,返回 0;若出错,返回-1

recvmsg用msghdr 结构指定接收数据的输入缓冲区。可以设置参数flags来改变recvmsg 的默认行为。返回时,msghdr 结构中的msg_flags 字段被设为所接收数据的各种特征。

close

该接口用于关闭套接字,当应用程序调用close函数关闭套接字时,内核会释放该套接字所占用的资源,并将其从内核数据结构中移除。
接口也很简单,直接close(sockfd)即可。

服务端posix接口

listen

int listen(int sockfd,int backlog);
//返回值:若成功,返回 0;若出错,返回-1

服务端调用 listen 宣告它接受连接请求,其参数backlog提供了一个提示,提示该进程所要入队的未完成连接的请求数量。为什么说是提示呢,因为根据Linux内核的不断迭代,实际上该socket可以容纳的未连接请求远远不止传入的这个数目,其数量可以自适应增加,但不会超过其上限:<sys/socket.h>中的SOMAXCONN;

accept

一旦服务端调用了listen让主动套接字变被动监听套接字,就可以使用accept函数获得连接请求并建立连接;

@sockfd:服务端socket,该socket仅用于接收连接请求,获取的新socket的fd与之无关。
@addr:一个指向struct sockaddr类型变量的指针,用于存储客户端地址信息。
@len:一个指向socklen_t类型变量的指针,表示addr结构体长度。
int accept(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict len);
//返回值:若成功,返回文件 (套接字) 描述符;若出错,返回-1

当服务器程序调用accept()时,如果此时有客户端发送了连接请求,那么它将立即从内核中已经建立的全连接队列中,寻找该请求的 五元组 信息。如果确认该客户端已经处于全连接队列,那么就返回新分配给该客户端所用的套接字文件描述符。

此时服务器进程就可以利用该文件描述符与该客户进程进行通信了。同时,在accept()函数中会填充传入参数中对应结构体(如addr)的内容,以获得远程主机地址信息等相关信息。

需要注意以下几点:

当没有连接请求到达时,accept()函数会一直阻塞等待,并且只有在监听套接字上有连接请求时,它才会返回新的套接字描述符,可以将socket设置为非阻塞从而马上获得获取结果。
由于accept()函数是阻塞式函数,因此可能会导致服务器程序被挂起,为了避免这种情况的出现,可以通过使用多线程或者多路复用等技术来实现同时处理多个客户端请求。