UNIX网络编程(UNP) 第七章学习笔记

概述

我们可以通过若干个方法来修改套接字的选项,包括getsockopt和setsockopt函数,fcntl函数可以用于将套接字设置为非阻塞套接字或者信号驱动套接字以及修改套接字属主的方法

int     setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
int     getsockopt(int sockfd, int level, int optname, void * optval, socklen_t * optlen);

sockfd必须是一个已经打开的套接字,level指定系统中解释选项的代码或者为通用,或者为特定于协议,optval在set中会作为传入值,在get中作为传出值,optlen在set中是传入值,在get中是值-结果参数

套接字的状态

有些时候,一些套接字选项的设置需要考虑到时序,比方说keep_alive什么

因此有一些套接字选项会继承自监听套接字(否则在连接完成到accept过程都没有设置)

包括有SO_DEBUG, SO_DONTROUTE, SO_KEEPALIVE, SO_LINGER, SO_OOBINLINE, SO_RCVBUF, SO_RCVLOWAT, SO_SNDBUF, SO_SNDLOWAT

对于这些选项,我们只需要配置在监听套接字上就可以

通用套接字

SO_BROADCAST套接字选项

只适用于UDP,用于配置是否禁止还是开启广播消息的能力,在udp发布广播数据报之前必须配置该选项

SO_DEBUG套接字选项

仅支持TCP,设置之后,TCP会在所有的发送、接收分组上保留详细跟踪信息,被保存在内核的某个环形缓冲区中,可以使用trpt程序检查

SO_DONTROUTE套接字选项

规定绕过正常的路由机制。也就是说设置之后,将绕过路由表中网关的表项,在send,sendmsg,sendto中设置MSG_DONTROUTE有同样的效果

SO_ERROR套接字选项

该套接字不能配置只能通过SO_ERROR获取

如果套接字发生了错误,内核就会配置该套接字选项so_error为标准的Unix Exxx,而这会立刻通知到正在阻塞的select调用以及配置了信号驱动的套接字(会发送SIGIO信号)

read系统调用如果没有读到任何数据且SO_ERROR不为0,则会返回-1,errno设置为so_error配置值,之后so_error复位为0,如果so_error不为0但是有数据,那么会返回数据

write被调用的时候如果so_error不为0,那么返回-1,errno设置为so_error配置值,之后so_error复位为0

SO_KEEPALIVE套接字选项

设置SO_KEEPALIVE之后,如果两个小时套接字任意方向都没有数据交换,那么会发送一个存活确认分组,对此我们可能有四种结果,当然如果你仔细分析,你会发现这四个结果跟正常一个请求发送得到的结果很像:

  • 获取到正常响应,服务器不会收到任何反馈,2小时后继续发送分组
  • 对端已经重启了进程,收到RST报文,关闭套接字,设置为ECONNRESET。
  • 没有收到响应,内核会每隔75s发送一次分组,总共九次没有得到回应之后,关闭套接字,设置为ETIMEOUT
  • 如果收到ICMP报文,则关闭套接字,设置为EHOSTUNREACH,该情景发生在网络崩溃或者中间路由器感知到了主机的崩溃

准确来说,该选项的作用在于检测主机的崩溃或者不可达,因为如果仅仅是进程崩溃的话,我们会收到FIN报文的,之所以说或者不可达,是因为存在一种可能是对端主机正常,但是路由崩溃了一段时间。

在CS框架下,一般来说比较常用与服务端,因为服务端相对来说比较稳定且可知,而客户端的话有可能因为种种原因没有切断连接,于是连接一直处于半开连接的状态,SO_KEEPALIVE可以提供一种解决方法

对端状态感知方法

上面提到了如果对面进程崩溃,我们会收到一个FIN报文来感知,我们不妨仔细分析对端可能的情景以及我们可有的感知方法

情形 对端进程崩溃 对端主机崩溃 对端主机不可达
本端TCP正在发送数据 对端发送FIN报文,select条件可以感知到读条件。发送分组会收到RST报文,套接字关闭,如果继续(向套接字)写入,会发出SIGPIPE信号以及EPIPE错误 没有收到回复,重复尝试之后触发ETIMEOUT 错误,套接字关闭 tcp超时,套接字会触发EHOSTUNREACH错误
本端TCP正在主动接收数据 对端发送FIN报文,本端收到一个EOF(可能在业务逻辑上属于过早) 我们会(可能通过超时)停止接受数据 我们会(可能通过超时)停止接受数据
连接空闲,保持存活选项开启 对端发送FIN报文,select条件可以感知到读条件。 两小时后发送存活检测分组,然后设置为ETIMEOUT 两小时后发送存活检测分组,然后设置为EHOSTUNREACH
连接空闲,未开启保持存活 对端发送FIN报文,select条件可以感知到读条件。
SO_LINGER套接字选项

从之前可知,当我们调用close的时候会立刻返回,但是发送缓冲区的数据会先发完最后发送一个fin报文

我们可以通过SO_LINGER选项来改变这种行为,比方说我们可以丢掉所有发送缓冲区的数据并发送一个RST报文从而跳过time_wait阶段来复用端口(并不推荐,因为time_wait本身是为了避免出现幽灵分组问题才提出来的),或者我们可以让close稍微等一下(从而我们可以收到剩余发送的确认)

具体来说,SO_LINGER定义的行为是由struct linger决定的

/*
 * Structure used for manipulating linger option.
 */
struct  linger {
	int     l_onoff;                /* option on/off */
	int     l_linger;               /* linger time */
};

期中l_onoff如果是0,那么SO_LINGER相当于没有设置,还是按照默认行为

如果l_onoff不为0,而l_linger为0,就是我们提到的放弃所有发送缓冲区中的数据,发送RST报文,这个最多用在一些一直停留在close_wait(对端一直不发fin报文)的连接中

如果l_onoff不为0且l_linger也不为0,这个时候close就不会立刻返回,而是等待l_linger指定的时间或者收到了所有剩余数据的ack为止,如果是因为指定时间到的话,close会返回EWOULDBLOCK错误,同时剩下的发送缓冲区数据都会丢失

三种确认对端收到分组的方法

虽然我们描述了SO_LINGER的行为,但是可能有些人不太理解,这个东西作用在哪里呢?答案是“收到确认”,在默认的close行为中,close会直接返回,我们是没有办法确认对端的确已经收到了所有剩余数据,更不用说如何确认对端确实使用到了数据,要知道,即便对端的确收到数据 并发送了ack,对端进程也有可能在未读取接收缓冲区的数据的时候就崩溃了。

通过设置SO_LINGER,如果我们确实返回且没有错误,那么我们可以完成对端的确收到了数据的确认,但是问题是,这就够了吗?

我们可以发现,至少有两个问题是SO_LINGER没有解决的

  1. SO_LINGER是通过设置一个时间来等待,但是这样做的问题是,如果到了时间仍然没有收到响应,一种可能的确是对端没有收到数据而正常返回错误,但是也存在着可能是一切正常,只不过SO_LINGER设置的值太过于小,确认没有来得及到本端
  2. SO_LINGER顶多只能确认对端收到了数据,但是却没有办法确认对端成功使用了数据

于是我们可以考虑到第二种做法:我们用写shutdown的方式发送FIN报文通知关闭,然后阻塞调用read方法来确认对端的确已经使用了数据

第三种方法是”应用级ACK“,说白了,就是双方约定好,比方说A发送完数据,就阻塞等待读取一个字节,然后B收到信息之后,就发送一个字节,通过这种约定来确认

若干种关闭套接字的方式以及影响
函数 说明
shutdown,SHUT_RD 套接字发送缓冲区数据被丢弃,所有到来的分组都被确认然后丢弃,不能在该套接字上读取
shutdown,SHUT_WR 不能再调用发送请求,可以读取,所有发送缓冲区的数据都会被发送并跟着发送FIN序列
close,l_onoff=0 该套接字上不能在读写,如果引用计数为0,那么接收缓冲区的数据丢弃,发送缓冲区的数据被发送,跟着发送FIN序列
close,l_onoff=1,l_linger=0 套接字不能读写,如果引用计数为0,则接收、发送缓冲区数据都丢弃,套接字发送RST报文并且直接跳过time_wait状态进入closed状态
close,l_onoff=1,l_linger!=0 套接字不能读写,如果引用计数为0,则发送缓冲区数据都丢弃,所有发送缓冲区的数据都会被发送并跟着发送FIN序列,如果在确认之前(也就是变成closed状态前)延滞时间到了,那么就会返回EWOULDBLOCK错误
SO_OOBINLINE套接字选项

略 #todo

SO_RCVBUF和SO_SNDBUF选项
含义与作用

这两个顾名思义就是发送缓冲区大小和接收缓冲区大小了,期中接收缓冲区大小直接决定了建立连接的时候约定的窗口大小,根据TCP的流量控制,对端不会发送超过通告窗口大小的数据,如果的确有,那么本端就会直接丢弃;但是对于UDP来说,由于没有流量控制,也就是无法控制对端发送的数据大小,从而可能导致数据报因为窗口小而被直接丢弃

设置的时机

回顾三次握手,窗口大小的信息其实在syn报文中已经携带,因此我们不可能对一个已连接套接字设置该选项(想象下,此时三次握手已经完成了),因此对于客户端来说,该选项的设置发生在connect调用之前,对于服务端来说,发生在listen调用之前

设置大小的约束条件
  1. 由于快速恢复要求连续发送三个相同的确认,因此要求接收端至少能容纳四个分组(比方说j,j+2,j+3,j+4触发了三个相同的确认),才能激活快速恢复算法, 所以要求缓冲区大小至少为MSS的四倍
  2. 一般要求是整数倍(或者是偶数倍),多余的并不能得到使用
  3. 窗口大小会影响性能,我们可以用带宽(单位bit/s)乘以延迟(单位s)从而计算得到带宽延迟积,其实际含义有点像是满带宽下发送端最大未确认的数据大小,如果窗口大小小于该值,则不能满带宽运行
SO_RCVLOWAT和SO_SNDLOWAT套接字选项

用于让select判断可读和可写,默认情况下,SO_RCVLOWAT是1,SO_SNDLOWAT是2048

UDP虽然也有低水位的概念,但是由于UDP套接字的发送缓冲区大小不会改变(因为udp不复制从而占用缓冲区)所以只要设定的发送缓冲区大小大于发送低水位,那么UDP就一定是可写的

SO_RCVTIMEO和SO_SNDTIMEO套接字选项

该套接字选项设置的时候使用的是timeval结构体(跟select参数一致),可以指定我们读取和发送的超时值,默认是0s和0μs,这种情况下禁止超时(一直阻塞等待)

SO_REUSEADDR和SO_REUSEPORT套接字选项

SO_REUSEADDR允许我们重用地址和端口,他可以应用于以下的场景

  1. 假设说有一个服务器在遇到新的连接请求的时候通过派生子进程来处理,那么如果中途停止重启(此时子进程仍然在服务),如果不设定SO_REUSEADDR,bind会因为子进程占用端口而失败,通过在socket和bind之间设置SO_REUSEADDR选项,我们可以避免该问题,所有的tcp服务器都应该指定该选项

  2. SO_REUSEADDR允许我们在同一端口启动同一服务器的多个实例,只要我们制定的IP地址不一样。

    我们可以通过指定该选项来启动若干个服务器,并将目的地址不一致的请求分发到不同的服务器中,并且我们可以指定其中之一是通配地址,从而处理所有不被其他目的IP地址包括的请求。如果不指定该选项,bind操作是会失败的

    注意在一些操作系统中,为了避免恶意覆盖(比方说一个关键服务监听通配地址,一个恶意程序则监听特定地址来覆盖前者),这种情况下,通配地址的服务器要在最后设置。

  3. SO_REUSEADDR允许单一进程绑定同一端口到多个套接字上

    这常常用于UDP中从而允许那些不支持IP_RECVDSTADDR套接字选项的系统上的UDP可以获知目标IP地址

  4. 多播

SO_TYPE套接字选项

返回套接字类型,如SOCK_STREAM或者SOCK_DGRAM等,一般由继承套接字的进程使用

SO_USELOOPBACK套接字选项

仅用于路由域,是唯一一个默认打开的套接字选项

IPv4套接字选项

注意这些套接字选项由IPv4处理,级别(第二个参数)是IPPROTO_IP

IP_HDRINCL套接字选项

一般来说,IP首部是内核构造的,但是启用该选项的话,将由应用程序构造自己的IP首部,当然也有一些例外,具体看书

IP_OPTIONS套接字选项

允许我们在首部中设置IP选项,要求我们熟悉IP首部中的IP选项的格式

IP_RECVDSTADDR套接字选项

启用该套接字后,UDP数据报的目的ip地址将由recvmsg函数作为辅助数据返回

IP_RECVIF套接字选项

启用该套接字后,UDP数据报的接收接口索引将由recvmsg函数作为辅助数据返回

IP_TOS套接字选项

略过

IP_TTL套接字选项

允许我们设置默认TTL值,tcp和udp套接字默认是64,原始套接字默认值是255

TCP套接字选项

注意这些套接字选项由TCP处理,级别(第二个参数)是IPPROTO_TCP

TCP_MAXSEG套接字选项

该选项允许我们获取或者设置最大分组的大小,在建立未连接之前,我们得到的是(未收到MSS选项前的)默认大小,在连接建立后,我们得到的通常是对端使用SYN分节通告的MSS大小,如果我们启用了时间戳选项,那么该返回值会略大于实际分组大小,因为时间戳选项占用12字节

在支持MTU路径发现下,该值可能会因为对端路径变动而发生改变

该值在一些系统内可以设置,一般来说,默认是只能减小而不能增大

TCP_NODELAY套接字选项

开启该选项就会禁用Nagle算法,默认情况下nagle算法是启动的

nagle算法的诉求是减少小分组,做法是:如果当前有未确认的数据,那么小分组不能被发送,直到现有数据被确认。利用该算法,TCP可以防止一个连接在任何时刻有多个小分组待确认。

该算法通常与ack延滞算法一并使用,启用ack延滞算法之后,tcp在收到数据后不会立刻发送ACK,而是等待一小段时间(50-200ms),然后在发送,TCO预期这小段时间里面,会有数据发送回对端,从而ACK可以捎带(减少一个分组)

ACK延滞算法和Nagle算法的混合对于一些不会在相反方向发送数据(从而携带ack)的程序来说,会感受到较大的时延,因为客户TCP需要等待延迟的确认,所以客户TCP可以禁止Nagle算法

另一种情形是客户发送若干个小片数据来完成一个逻辑请求。比方说用户A先发送了四字节的请求类型和396字节的请求数据,在nagle算法下,第二个请求需要等待第一个请求的响应,服务器因为仅仅只有请求类型无法开展工作因此不会有数据往回发,因此ACK会被拖延

对此的解决方法有

  • 使用writev方法而不是write方法(从而只产生一个分组)
  • 将数据复制到同一个缓冲区,然后调用一次write
  • 关闭nagle并且连续调用两次,这是不可取且有损的方法,一般不考虑

fcntl函数

fcntl可以改变套接字属性

函数定义
int     fcntl(int fd, int cmd, ...);
函数使用
  • 设置非阻塞

    当然我们不能简单粗暴的通过设置fcntl(fd,F_SETFL,O_NONBLOCK)来设置非阻塞式I/O,这种做法会直接去掉其他文件状态标志

    典型的做法应该是

    int flags;
    if ((flags=fcntl(fd,F_GETFL,0))<0){
    	err_sys("F_GETFL error");
    }
    flags |=O_NONBLOCK;//通过这种方式来确保不丢失
    // flags &= ~O_NONBLOCK;//用于关闭
    if (fcntl(fd,F_SETFL,flags)<0){
        err_sys("something wrong");
    }
    
  • 设置信号驱动

    我们可以用F_SETFL设置O_ASYNC文件状态标志,设置后套接字一旦状态改变,内核就会发送一个SIGIO信号

  • 设置SIGIO和SIGURG信号的属主(进程或者进程组)

    我们可以使用F_SETOWN命令,用F_GETOWN来获取

    注意SIGIO和SIGURG只有在设定属主之后才会产生,设置时候,如果是正值,表示是进程ID,如果是负值,则绝对值大小表示进程组ID

    F_GETOWN设置后fcntl返回值表示上述概念

    进程和进程组的区别在于,信号时只发送给一个进程,还是进程组的所有进程

    新建的socket没有属主,但是连接套接字(和很多套接字选项一样)可以继承监听套接字的属主

发布了31 篇原创文章 · 获赞 32 · 访问量 743

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/103565318