TCP/IP网络编程之套接字的多种可选项
套接字可选项进而I/O缓冲大小
我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解这些特性并根据实际需要进行更改也十分重要。之前我们写的程序在创建好套接字后都是未经特别操作就直接使用,此时通过默认的套接字特性进行数据通信。之前的示例比较简单,无需特别操作套接字特性,但有时的确需要更改,表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 | O |
IP_TTL | 设置主机发送数据包的生存时间 | O | O | |
IP_MULTICAST_TTL | 生存时间(Time To Live),组播传送距离 | O | O | |
IP_MULTICAST_LOOP | 禁止组播数据回送 | O | O | |
IP_MULTICAST_IF | 取默认接口或默认设置 | O | O | |
IPPROTO_TCP | TCP_KEEPALIVE | TCP保活机制开启下,设置保活包空闲发送时间间隔 | O | O |
TCP_NODELAY | 不使用Nagle算法 | O | O | |
TCP_MAXSEG | TCP最大数据段的大小 | O | O |
从表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网络编程之套接字的多种可选项的更多相关文章
- TCP/IP网络编程之套接字类型与协议设置
套接字与协议 如果相隔很远的两人要进行通话,必须先决定对话方式.如果一方使用电话,另一方也必须使用电话,而不是书信.可以说,电话就是两人对话的协议.协议是对话中使用的通信规则,扩展到计算机领域可整理为 ...
- TCP/IP网络编程之套接字与标准I/O
标准I/O函数 标准标准I/O函数有两个优点: 标准I/O函数具有良好的移植性 标准I/O函数可以利用缓冲提高性能 关于移植性无需过多解释,不仅是I/O函数,所有标准函数都具有良好的移植性.因为,为了 ...
- TCP/IP网络编程之多播与广播
多播 多播方式的数据传输是基于UDP完成的,因此,与UDP服务端/客户端的实现非常接近.区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机.换言之,采用多播方式时 ...
- 【TCP/IP网络编程】:01理解网络编程和套接字
1.网络编程和套接字 网络编程与C语言中的printf函数和scanf函数以及文件的输入输出类似,本质上也是一种基于I/O的编程方法.之所以这么说,是因为网络编程大多是基于套接字(socket,网络数 ...
- 【TCP/IP网络编程】:09套接字的多种可选项
本篇文章主要介绍了套接字的几个常用配置选项,包括SO_SNDBUF & SO_RCVBUF.SO_REUSEADDR及TCP_NODELAY等. 套接字可选项和I/O缓冲大小 前文关于套接字的 ...
- TCP/IP网络编程之网络编程和套接字
网络编程和套接字 网络编程又称为套接字编程,就是编写一段程序,使得两台连网的计算机彼此之间可以交换数据.那么,这两台计算机用什么传输数据呢?首先,需要物理连接,将一台台独立的计算机通过物理线路连接在一 ...
- UNIX网络编程——原始套接字(dos攻击)
原始套接字(SOCK_RAW).应用原始套接字,我们可以编写出由TCP和UDP套接字不能够实现的功能. 注意原始套接字只能够由有 root权限的人创建. 可以参考前面的博客<<UNIX网络 ...
- 《TCP/IP网络编程》
<TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...
- TCP/IP网络编程系列之四(初级)
TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...
随机推荐
- ElasticSearch:华为云搜索CSS 之POC操作记录
2019/03/06 09:00 ES文档官方:https://support.huaweicloud.com/usermanual-es/es_01_0024.html 华为云区域:华北北京1 ES ...
- hibernate课程 初探单表映射2-3 session简介
hibernate流程: 1 配置对象Configurateion 读取 hibernate.cfg.xml 2 会话工厂SessionFactory 读取 user.hbm.xml(创建销毁相当耗费 ...
- 牛客NOIP提高组(三)题解
心路历程 预计得分:$30 + 0 + 0 = 30$ 实际得分:$0+0+0= 0$ T1算概率的时候没模爆long long了... A 我敢打赌这不是noip难度... 考虑算一个位置的概率,若 ...
- codevs 原创抄袭题 5969 [AK]刻录光盘
题目描述 Description • 在FJOI2010夏令营快要结束的时候,很多营员提出来要把整个夏令营期间的资料刻录成一张光盘给大家,以便大家回去后继续学习.组委会觉得这个主意不错!可是组委会一时 ...
- new Date(str)返回的时间结果在移动端比PC端快了8小时
最近开发过程中,后端传过来一个“2018-03-15T17:53:19.6307928”字符串,需要将字符串转换成“2018-03-15 17:53”的格式展示出来.首先我使用了var time=n ...
- EBS应用重启
重启系统应用 cd $ADMIN_SCRIPTS_HOME ./adstpall.sh apps/apps ./adstrtal.sh apps/apps 在重启应用时,可能会出现并发管理器未启动的情 ...
- nsight 中出现method could not be resolved 报错
解决的方法就是现在编译选项中取消该报错. 项目右键->属性->c/c++常规->Code Analysis,选择"Use project settings" 中 ...
- iOS - 协议实现的例子
在实际开发中,协议的应用非常广泛,以下是实际应用的例子. 1.协议的定义: myProtocolDelegate.h // // myProtocolDelegate.h // zlwPlayerAp ...
- [Hack] 搭建渗透测试实验环境
安装虚拟机镜像,镜像如下: Kali-Linux-2016.1-vm-amd64(https://www.kali.org/) Metasploitable2-Linux(https://source ...
- HDU1043 八数码(BFS + 打表)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1043 , 康托展开 + BFS + 打表. 经典八数码问题,传说此题不做人生不完整,关于八数码的八境界 ...