套接字可选项进而I/O缓冲大小

我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解这些特性并根据实际需要进行更改也十分重要。之前我们写的程序在创建好套接字后都是未经特别操作就直接使用,此时通过默认的套接字特性进行数据通信。之前的示例比较简单,无需特别操作套接字特性,但有时的确需要更改,表1-1列出一部分套接字可选项

表1-1   可设置套接字的多种可选选项 
协议层 选项名 说明 读取 设置
SOL_SOCKET          SO_SNDBUF  发送缓冲区大小  O  O
 SO_RCVBUF  接收缓冲区大小  O  O
 SO_REUSEADDR  是否启用地址再分配,主要原理是操作关闭套接字的Time-wait时间等待的开启和关闭  O  O
 SO_KEEPALIVE  开启套接字保活机制  O  O
 SO_BROADCAST  允许或禁止发送广播数据  O  O
 SO_DONTROUTE  打开或关闭路由查找功能  O  O
 SO_OOBINLINE  该数据字节并不放入套接字接收缓冲区,而是被放入该连接的一个独立的单字节带外缓冲区  O  O
 SO_ERROR  获得套接字错误  O  X
 SO_TYPE  获得套接字类型(这个只能获取,不能设置)  O  X
 IPPROTO_IP      IP_TOS  设定该字段的值,以区分不同服务的优先级  O
 IP_TTL  设置主机发送数据包的生存时间
 IP_MULTICAST_TTL  生存时间(Time To Live),组播传送距离
 IP_MULTICAST_LOOP  禁止组播数据回送
 IP_MULTICAST_IF  取默认接口或默认设置
 IPPROTO_TCP    TCP_KEEPALIVE  TCP保活机制开启下,设置保活包空闲发送时间间隔  O
 TCP_NODELAY  不使用Nagle算法  O O
 TCP_MAXSEG  TCP最大数据段的大小

从表1-1可以看出,套接字可选项是分层的。IPPROTO_IP层可选项是IP协议相关事项,IPPROTO_TCP层可选项是TCP协议相关的事项,SOL_SOCKET层是套接字相关的通用可选项。

getsockopt和setsockopt

我们几乎可以针对表1-1中的所有可选项进行读取(Get)和设置(Set),可选项的读取和设置通过如下两个函数完成:

#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);//成功时返回0,失败时返回-1

  

  • sock:用于查看选项套接字文件描述符
  • level:要查看的可选项的协议层
  • optname:要查看的可选项名
  • optval:保存查看结果的缓冲地址值
  • optlen:向第四个参数optval传递的缓冲大小,调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数

上述函数用于读取套接字可选项,并不难,接下来介绍更改可选项时调用的函数

#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);//成功时返回0,失败时返回-1

  

  • sock:用于更改可选项的套接字文件描述符
  • level:要更改的可选项的协议层
  • optname:要更改的可选项名
  • optval:保存要更改的选项信息的缓冲地址值
  • optlen:向第四个参数optval传递的可选项信息的字节数

接下来介绍这些函数的调用方法,我们先介绍getsockopt函数的调用方法,setsockopt函数的调用方法将在其他的示例中给出。下面示例用协议层为SOL_SOCKET、名为SO_TYPE的可选项查看套接字类型(TCP或UDP)

sock_type.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message); int main(int argc, char *argv[])
{
int tcp_sock, udp_sock;
int sock_type;
socklen_t optlen;
int state; optlen = sizeof(sock_type);
tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
printf("SOCK_STREAM: %d \n", SOCK_STREAM);
printf("SOCK_DGRAM: %d \n", SOCK_DGRAM); state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
if (state)
error_handling("getsockopt() error!");
printf("Socket type one: %d \n", sock_type); state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
if (state)
error_handling("getsockopt() error!");
printf("Socket type two: %d \n", sock_type);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

    

  • 第15、16行:分别生成TCP、UDP套接字
  • 第17、18行:输出创建TCP、UDP套接字时传入的SOCK_STREAM、SOCK_DGRAM
  • 第20、25行:获取套接字类型信息,如果是TCP套接字,将获得SOCK_STREAM常数值1;如果是UDP套接字,则获得SOCK_DGRAM的常数值2

编译sock_type.c并运行

# gcc sock_type.c -o sock_type
# ./sock_type
SOCK_STREAM: 1
SOCK_DGRAM: 2
Socket type one: 1
Socket type two: 2

  

上述示例给出了调用getsockopt函数查看套接字信息的方法,另外,用于验证套接字类型的SO_TYPE是典型的只读可选项,即套接字类型只能在创建时决定,以后不能再更改

SO_SNDBUF和SO_RCVBUF

前面介绍过,创建套接字将同时生成I/O缓冲,SO_RCVBUF是输入缓冲大小相关可选项,SO_SNDBUF是输出缓冲大小相关可选项。用这两个可选项可以读取和修改当前I/O缓冲大小。通过下面的示例读取创建套接字时默认的I/O缓冲大大小

get_buf.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
int snd_buf, rcv_buf, state;
socklen_t len; sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
if (state)
error_handling("getsockopt() error"); len = sizeof(rcv_buf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
if (state)
error_handling("getsockopt() error"); printf("Input buffer size: %d \n", rcv_buf);
printf("Outupt buffer size: %d \n", snd_buf);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

编译get_buf.c并运行

# gcc get_buf.c -o get_buf
# ./get_buf
Input buffer size: 87380
Outupt buffer size: 16384

  

这是我系统的运行结果,不同系统可能默认的输入缓冲和输出缓冲有所差异,接下来,我们通过程序修改I/O缓冲大小

set_buf.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
int snd_buf = 1024 * 3, rcv_buf = 1024 * 3;
int state;
socklen_t len; sock = socket(PF_INET, SOCK_STREAM, 0);
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));
if (state)
error_handling("setsockopt() error!"); state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
if (state)
error_handling("setsockopt() error!"); len = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
if (state)
error_handling("getsockopt() error!"); len = sizeof(rcv_buf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
if (state)
error_handling("getsockopt() error!"); printf("Input buffer size: %d \n", rcv_buf);
printf("Output buffer size: %d \n", snd_buf);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

  • 第15、19行:I/O缓冲大小更改为3M字节
  • 第24、29行:为了验证I/O缓冲的更改,读取缓冲大小

编译set_buf.c并运行

# gcc set_buf.c -o set_buf
# ./set_buf
Input buffer size: 6144
Output buffer size: 6144

  

输出结果和我们预想的完全不同,但也算合理,缓冲大小的设置需谨慎,因此不会完全按照我们的要求进行,只是通过setsockopt函数向系统传递我们的要求。如果把输出缓冲设置为0并如实反映这种设置,TCP协议将如何进行?如果要实现流控制和错误发生时的重传机制,至少要有一些缓冲空间吧?上述示例虽没有完全按照我们的要求设置缓冲大小,但也大致反映出可以通过setsockopt函数设置缓冲大小

SO_REUSEADDR

学习SO_REUSEADDR之前,应先理解好Time-wait状态,我们看完下面的示例在了解后面的内容

reuseadr_eserver.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define TRUE 1
#define FALSE 0
void error_handling(char *message); int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[30];
int option, str_len;
socklen_t optlen, clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr; if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
} serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error");
/*
optlen=sizeof(option);
option=TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
*/ memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)))
error_handling("bind() error "); if (listen(serv_sock, 5) == -1)
error_handling("listen error");
clnt_adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); while ((str_len = read(clnt_sock, message, sizeof(message))) != 0)
{
write(clnt_sock, message, str_len);
write(1, message, str_len);
}
close(clnt_sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

此示例是之前已实现多次的回声服务端,可以结合TCP/IP网络编程之基于TCP的服务端/客户端(一)这一章中的回声客户端运行。下面运行该示例,第30到32行应保持注释状态,可通过在客户端控制台输入Q或CTRL+C终止程序。也就是说,让客户端先通知服务端终止程序,在客户端控制台输入Q消息时调用close函数,向服务端发送FIN消息并经过四次握手过程。当然,输入CTRL+C时也会向服务端发送FIN消息。强制终止程序时,由操作系统关闭文件及套接字,此过程相当于调用close函数,也会向服务端发送FIN消息

服务端和客户端在已建立连接的状态下,向服务端控制台输入CTRL+C,即强制关闭服务端,这主要模拟了服务端向客户端发送FIN消息。但如果以这种方式终止程序,那么服务端重新运行将产生问题,如果用同一端口号重新运行服务端,将输出"bind() error"消息,并无法再次运行,需要等到两三分钟后才可重新运行服务端

上述两种终止运行的方式唯一的区别在于是谁先输出FIN消息,但结果迥然不同,原因何在呢?

Time-wait状态

这里需要对四次握手有很好的理解,如果还有疑问请看TCP/IP网络编程之基于TCP的服务端/客户端(二)这一章

图1-1   Time-wait状态下的套接字

图1-1中主机A是服务端,因为主机A向主机B发送FIN消息,故可以想象服务端在控制台输入CTRL+C。但问题是,套接字经过四次握手过程后并非立即消除,而是要经过一段时间的Time-wait状态。当然,只有先断开连接(即先发送FIN消息)的主机才经过Time-wait状态。因此,若服务端先断开连接,则无法立即重新运行。套接字处在Time-wait过程时,相应端口是正在使用的状态

刚才说过,先断开连接的主机的套接字,都会经过一段时间的Time-wait状态,因此,客户端或者服务端都有可能经历Time-wait状态,要看是谁先断开连接。但是客户端的套接字即便处在Time-wait状态也不要紧,因为客户端套接字的端口号是任意指定的,与服务端不同,客户端每次运行程序都动态分配端口号,因此无需太在意客户端的Time-wait状态

那么到底为什么会有Time-wait状态呢?图1-1中假设主机A向主机B传输ACK消息(SEQ 5001、ACK 7502)后立即消除套接字,但最后这条ACK消息在传递途中丢失,未能传给主机B。这时会发生什么?主机B会认为之前自己发送的FIN消息(SEQ 7501、ACK 5001)未能抵达主机A,继而试图重传。但此时主机A已是完全终止的状态,主机B永远无法收到从主机A最后传来的ACK消息。相反,若主机A的套接字处于Time-wait状态,则会向主机B重传最后的ACK消息,主机B也可以正常终止。基于这些考虑,先传输FIN消息的主机应经过Time-wait过程

地址再分配

Time-wait看似很重要,但不一定讨人喜欢,考虑一下系统发生故障从而紧急停止的情况,这时候需要尽快重启服务端以提供服务,但因Time-wait状态而必须等待几分钟。因此,Time-wait并非只有优点,而且有些情况下可能引起更大的问题。图1-2演示了四次握手不得不延长Time-wait过程的情况

图1-2   重启Time-wait计时器

图1-2所示,在主机A的四次握手过程中,如果最后数据丢失,主机B会认为主机A未能收到自己发送的FIN消息,因此重传。此时,收到FIN消息的主机A将重启Time-wait计时器。因此,如果网络状况不理想,Time-wait状态将持续

解决方案就是在套接字的可选项中更改SO_REUSEADDR的状态,适当调整该参数,可将Time-wait状态下的套接字端口重新分配给新的套接字。SO_REUSEADDR的默认值为0(假),这就意味着无法分配Time-wait状态下的套接字端口号,因此需要将这个值改成1(真)。具体做法已在示例reuseadr_eserver.c中给出,就是那段被注释的代码

optlen=sizeof(option);
option=TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);

  

TCP_NODELAY

Nagle算法是为防止因数据包过多而发生网络过载,该算法应用于TCP层,非常简单,其使用与否会导致图1-3所示的差异

图1-3   Nagle算法

图1-3展示了通过Nagle算法发送字符串“Nagle”和未使用Nagle算法的差别,可以得到一条结论:只有收到前一条数据的ACK消息时,Nagle算法才发送下一条数据。TCP套接字默认使用Nagle算法交换数据,因此最大限度地进行缓冲,直到收到ACK消息。图1-3左侧正是这种情况,为了发送“Nagle”字符串,将其传递到输出缓冲,这时头字符“N”之前没有其他数据(没有需接收的ACK),因此立即传输。之后开始等待字符“N”的ACK消息,等待过程中,剩下的“agle”填入输出缓冲。接下来,收到字符“N”的ACK消息后,将输出缓冲的“agle”装入一个数据包发送。也就是说,共需传递四个数据包以输出一个字符串

接下来分析未使用Nagle算法时发送字符串“Nagle”的过程,假设字符“N”到“e”依序传输到输出缓冲,此时的发送过程与ACK接收与否无关,因此数据到达数据缓冲后立即被发送出去,从图1-3右侧可以看到,发送字符串“Nagle”共需十个数据包。由此可知,不使用Nagle算法将对网络流量产生负面影响。即使只传输一个字节,其头信息都有可能几十个字节,因此,为了提高网络传输效率,必须使用Nagle算法

Nagle算法并不是什么时候都适用,根据传输数据的特性,网络流量未受太大影响时,不使用Nagle算法要比使用它时传输速度快,最典型的是“传输大文件数据”。将文件数据传入输出缓冲不会花太多时间,因此即便不使用Nagle算法,也会在装满输出缓冲时传输数据包,这不仅不会增加数据包的数量,反而会在无需等待ACK的前提下连续传输,因此可以大大提高传输速度

一般情况下,不适用Nagle算法可以提高传输速度,但如果无条件放弃使用Nagle算法,就会增加过多的网络流量,反而会影响传输。因此,未准确判断数据特性时不应禁用Nagle算法

禁用Nagle算法

刚才说过的“大数据文件”应禁用Nagle算法,换言之,如果有必要,就应禁用Nagle算法。Nagle算法使用与否在于网络流量上差别不大,使用Nagle算法的传输速度更慢。禁用方法很简单,从下面代码可以看出,只需将套接字可选项TCP_NODELAY改为1(真)即可

int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));

  

通过TCP_NODELAY的值查看Nagle算法的设置状态

int opt_val;
socklen_t opt_len;
opt_len = sizeof(opt_val);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, opt_len);

  

如果正在使用Nagle算法,opt_val变量中会保存0,如果已禁用Nagle算法,则保存1

TCP/IP网络编程之套接字的多种可选项的更多相关文章

  1. TCP/IP网络编程之套接字类型与协议设置

    套接字与协议 如果相隔很远的两人要进行通话,必须先决定对话方式.如果一方使用电话,另一方也必须使用电话,而不是书信.可以说,电话就是两人对话的协议.协议是对话中使用的通信规则,扩展到计算机领域可整理为 ...

  2. TCP/IP网络编程之套接字与标准I/O

    标准I/O函数 标准标准I/O函数有两个优点: 标准I/O函数具有良好的移植性 标准I/O函数可以利用缓冲提高性能 关于移植性无需过多解释,不仅是I/O函数,所有标准函数都具有良好的移植性.因为,为了 ...

  3. TCP/IP网络编程之多播与广播

    多播 多播方式的数据传输是基于UDP完成的,因此,与UDP服务端/客户端的实现非常接近.区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机.换言之,采用多播方式时 ...

  4. 【TCP/IP网络编程】:01理解网络编程和套接字

    1.网络编程和套接字 网络编程与C语言中的printf函数和scanf函数以及文件的输入输出类似,本质上也是一种基于I/O的编程方法.之所以这么说,是因为网络编程大多是基于套接字(socket,网络数 ...

  5. 【TCP/IP网络编程】:09套接字的多种可选项

    本篇文章主要介绍了套接字的几个常用配置选项,包括SO_SNDBUF & SO_RCVBUF.SO_REUSEADDR及TCP_NODELAY等. 套接字可选项和I/O缓冲大小 前文关于套接字的 ...

  6. TCP/IP网络编程之网络编程和套接字

    网络编程和套接字 网络编程又称为套接字编程,就是编写一段程序,使得两台连网的计算机彼此之间可以交换数据.那么,这两台计算机用什么传输数据呢?首先,需要物理连接,将一台台独立的计算机通过物理线路连接在一 ...

  7. UNIX网络编程——原始套接字(dos攻击)

    原始套接字(SOCK_RAW).应用原始套接字,我们可以编写出由TCP和UDP套接字不能够实现的功能. 注意原始套接字只能够由有 root权限的人创建. 可以参考前面的博客<<UNIX网络 ...

  8. 《TCP/IP网络编程》

    <TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...

  9. TCP/IP网络编程系列之四(初级)

    TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...

随机推荐

  1. vue安装及环境搭建

    vue项目在pycharm里运行需要安装一个插件,打开settings,找到plugins,里面搜索vue.js,点击安装. vue安装 先安装node.js npm install -g @vue/ ...

  2. It does not do to dwell on dreams and forget to live.

    It does not do to dwell on dreams and forget to live.不要过于依赖梦想,却忘了生活.

  3. Mybatis介绍(一)

    这里介绍的mybatis比较简单, 我做为一个初学者, 记录下个人在学习中方法, 如果那里出错, 希望读者朋友们见谅. 首先这里介绍一下我们下面用的表结构: author表是保存了作者的个人信息, 因 ...

  4. TP5.0:的安装与配置

    在网址中输入:localhost/安装TP5的文件夹/public/ 入口文件位置:public/index.php: 最新版本中,新建的文件夹是没有模型和视图的,需要自行添加没有的文件: 添加前: ...

  5. ABAP Netweaver和Cloud Foundry上的环境变量Environment Variable

    Netweaver 更准确的说应该是系统变量:结构体sy 设一个断点,调试器里看这些字段的值就能知道每个字段是用来做什么的. sy-dbsys sy-sysid sy-opsys sy-saprl s ...

  6. World Wind Java开发之七——读取本地栅格文件(影像+高程)构建三维场景(转)

    http://blog.csdn.net/giser_whu/article/details/41679515 首先,看下本篇博客要达到的效果图: 下面逐步分析如何加载影像及高程文件. 1.World ...

  7. IPC Gateway 设计

    1. IPC Gateway对外提供的功能: IPC的register/request/reply/notification服务. 2. IPC Gatew的实现原理: 各个具体的服务注册自己的回调函 ...

  8. 在RichTextBox控件中替换文本文字

    实现效果: 知识运用: RichTextBox控件的SelectedText属性 实现代码: private void button1_Click(object sender, EventArgs e ...

  9. python_78_软件目录结构规范

    一定要看http://www.cnblogs.com/alex3714/articles/5765046.html #print(__file__)#打印的是文件的相对路径 import os pri ...

  10. sessionStorage 和 localStorage

    html5 中的 web Storage 包括了两种存储方式:sessionStorage 和 localStorage. sessionStorage 用于本地存储一个会话(session)中的数据 ...