【TCP/IP网络编程】:09套接字的多种可选项
本篇文章主要介绍了套接字的几个常用配置选项,包括SO_SNDBUF & SO_RCVBUF、SO_REUSEADDR及TCP_NODELAY等。
套接字可选项和I/O缓冲大小
前文关于套接字的描述仅仅是使用其默认套接字特性来进行数据通信,这对于简单的使用场景来说似乎是可以的,然而实际工作场景中的确需要配置相关套接字选项来满足一些特殊需求。下图所示是一些常用的套接字可选配置选项。
一些常用套接字可配置选项
从图中可以看出,套接字可选项是分层的。IPPROTO_IP层可选项是IP协议相关事项,IPPROTO_TCP层可选项是TCP协议相关事项,SOL_SOCKET层是套接字相关的通用可选项。
getsockopt & setsockopt
针对上文所描述的套接字可选项,可分别通过getsockopt函数和setsockopt函数来进行读取(Get)和设置(Set)(有些选项可能仅支持一种操作)。
- #include <sys/socket.h>
- //Get option
- int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
- -> 成功时返回0,失败时返回-
- //Set option
- int setsockopt(int sock, int level, int optname, void *optval, socklen_t optlen);
- -> 成功时返回0,失败时返回-
下面示例源码给出了getsockopt函数的使用方法,同时也展示了只读套接字选项SO_TYPE的作用(套接字类型只能在创建时决定,之后不能再更改)。
- #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, );
- udp_sock=socket(PF_INET, SOCK_DGRAM, );
- 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 ;
- }
- void error_handling(char *message)
- {
- fputs(message, stderr);
- fputc('\n', stderr);
- exit();
- }
sock_type
运行结果
SO_SNDBUF & SO_RCVBUF
前文中我们提到套接字的输入输出缓冲区,而SO_SNDBUF 和SO_RCVBUF便是与套接字缓冲区大小相关的两个可选项。通过这两个选项我们可以获取当前套接字的输入输出缓冲区大小,抑或设置相应缓冲区的大小。如下是这两个选项使用的相关示例代码。
- #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, );
- 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 ;
- }
- void error_handling(char *message)
- {
- fputs(message, stderr);
- fputc('\n', stderr);
- exit();
- }
get_buf
- #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=*;
- int state;
- socklen_t len;
- sock=socket(PF_INET, SOCK_STREAM, );
- 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 ;
- }
- void error_handling(char *message)
- {
- fputs(message, stderr);
- fputc('\n', stderr);
- exit();
- }
- /*
- root@com:/home/swyoon/tcpip# gcc get_buf.c -o getbuf
- root@com:/home/swyoon/tcpip# gcc set_buf.c -o setbuf
- root@com:/home/swyoon/tcpip# ./setbuf
- Input buffer size: 2000
- Output buffer size: 2048
- */
set_buf
运行结果
从运行结果可以看出,对于缓冲大小的设置并非完全生效。实际上这些设置只是传递了我们的要求,而最终的生效值操作系统会根据当前环境做出设置,不过配置值的大小趋势和我们期望的一致。
SO_REUSEADDR
发生地址绑定错误(Binding Error)
回顾之前的文章“【TCP/IP网络编程】:04基于TCP的服务器端/客户端”,我们介绍了回声服务器端/客户端的实现。其中服务器端代码稍作改变如下。
- #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[];
- int option, str_len;
- socklen_t optlen, clnt_adr_sz;
- struct sockaddr_in serv_adr, clnt_adr;
- if(argc!=) {
- printf("Usage : %s <port>\n", argv[]);
- exit();
- }
- serv_sock=socket(PF_INET, SOCK_STREAM, );
- if(serv_sock==-)
- error_handling("socket() error");
- /*
- optlen=sizeof(option);
- option=TRUE;
- setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
- */
- memset(&serv_adr, , 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[]));
- if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)))
- error_handling("bind() error ");
- if(listen(serv_sock, )==-)
- 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)))!= )
- {
- write(clnt_sock, message, str_len);
- write(, message, str_len);
- }
- close(clnt_sock);
- return ;
- }
- void error_handling(char *message)
- {
- fputs(message, stderr);
- fputc('\n', stderr);
- exit();
- }
reuseaddr_server
客户端通过输入“Q”消息,或是通过CTRL+C终止程序,两种方式客户端都会执行close函数向服务器端传递EOF消息结束标志。服务器端收到EOF消息,也可以正常退出程序。现在考虑另一种情况,如果服务器端和客户端在已建立连接的状态下,向服务器端执行CTRL+C终止程序,会发生什么?
这种情况,服务器端会主动向客户端发送FIN消息断开连接并退出程序。此时,如果再次以相同端口号启动服务器端则会发生错误(bind()报错:“Address already in use”),通常需要等待1~4分钟才能再次运行服务器端。客户端主动发送FIN消息断开连接,不影响客户端或服务器端的再次运行;而服务器端主动发送FIN消息断开连接,则会影响服务器端的再次运行,为什么会出现这种现象呢?
TIME_WAIT状态
TIME_WAIT状态下的套接字
上图展示的就是前文有提到过的四次握手断开连接的过程。从图中可以看出,主动断开连接的主机(先发送FIN消息)会经过TIME_WAIT的状态,持续时间为2MSL(Maximum Segment Lifetime,最长分节生命期,30s或2min)。而处于TIME_WAIT状态时,相应的端口号是正在使用状态,因此,若服务器端先断开连接则无法立即重新运行。与服务器端不同,客户端由于每次运行都会动态分配端口号,因此不受TIME_WAIT状态的影响。
原来是TIME_WAIT的作怪,导致主动断开连接的服务器端不能立即以相同的端口号重新运行。既然对服务器端有这种影响,那为什么要有TIME_WAIT状态呢?(以下描述主要摘录自UNP,以客户端先发送FIN消息断开连接为例)
TIME_WAIT状态的存在有两个理由:
- 可靠地实现TCP全双工连接的终止
- 允许老的重复分节在网络中消逝
第一个理由可以假设上述四次握手过程最终的ACK丢失了来解释。主机B将重新发送它的最终那个FIN,因此主机A必须维护状态信息,以允许它重新发送最终那个ACK。如果主机A不维护状态信息,它将以一个RST(另外一种类型的TCP分节)消息来响应,该分节将被主机B解释为一个错误消息。如果TCP打算执行所有必要的工作以彻底终止某个连接上两个方向的数据流(即全双工关闭),那么它必须正确处理连接终止序列4个分节中任何一个分节丢失的情况。本例也说明了为什么执行主动关闭的那一端需要处于TIME_WAIT状态,因为它可能不得不重传最终那个ACK。
为理解存在TIME_WAIT状态的第二个理由,我们假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口之间建立另一个连接。后一个连接称为前一个连接的化身(incarnation),因为它们的IP地址和端口号都相同。TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。通过实施这个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了(单向传输一个分节的最长生命周期是MSL,TIME-WAIT状态的2MSL是考虑了一次双向信息交互的最长时间。比如最后的ACK丢失后,来自对端重发的FIN消息也会在2MSL内消逝)。
地址再分配
从上文的描述来看,TIME_WAIT状态在可靠通信过程中似乎起到了重要的作用,但它也有其自身的缺点。比如下图的情况,收到FIN消息的主机A发送ACK消息至主机B并启动Time-wait定时器,如果网络状态不好致使ACK消息不断丢失,则TIME-WAIT状态可能一直持续下去。
重启Time-wait定时器
另一种情况,考虑正在工作中的服务器突然故障停机而需要快速重启,这时由于TIME_WAIT状态则必须等几分钟,也会带来严重的影响(时间就是money)。
针对以上TIME_WAIT状态所带来的影响,可以通过配置可选项SO_REUSEADDR来解决。默认情况下,SO_REUSEADDR选项处于关闭状态(值为0,假),即无法分配处于TIME_WAIT状态下套接字端口。因此,我们需要将该选项置为1(真)即可。
- int opt_val = ;
- setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val));
SO_REUSEADDR可选项有效解决了以上问题,UNP中也有这么一句描述“所有TCP服务器都应该指定本套接字选项,以允许服务器在这种情形下被重新启动”。同时我们也应该意识到SO_REUSEADDR其实无视了TIME_WAIT状态的一些作用,此时如果收到一些不期望的数据(旧连接的分片)可能会导致服务程序混乱,不过这种可能性极低。
TCP_NODELAY
Nagle算法
Nagle算法的出现是为了防止因数据包过多而导致的网络过载,它应用于TCP层,其作用如下图所示。
Nagle算法
不难看出,只有收到ACK消息,Nagle算法才会发送下一数据。TCP套接字默认使用Nagle算法,因此可以最大限度地进行缓冲,直到收到ACK。上图的演示中,使用Nagle算法发送一个字符串消息需要传递4个数据包,而不使用Nagle算法则需要传递10个数据包,对网络流量(Traffic,网络负载或混杂程度)产生了较大的影响。当然,上图的演示只是一种极端的情况(特定场景下,字符串中的字符需要间隔一定的时间来传输至缓冲区),实际程序中将字符串传输至缓冲区并非逐个字符进行的。
根据数据传输的特性,网络流量未受太大影响时,不使用Nagle算法反而更快。典型的场景就是“传输大文件数据”,此时即使不使用Nagle算法,也会在填满缓冲区时传输数据。这种情况并没有增加数据包的数量,反而由于无需等待ACK而可以连续传输,大大提高了传输速度。禁止Nagle算法的方法如下。
- int opt_val = ;
- setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt_val, sizeof(opt_val));
是否使用Nagle算法,需要根据使用与否对网络流量影响的差别大小确定。通常情况,不使用Nagle算法确实可以获得更快的传输速度。但为了保证网络流量,在未准确判断数据特性时不应该禁止Nagle算法。
【TCP/IP网络编程】:09套接字的多种可选项的更多相关文章
- TCP/IP网络编程之套接字的多种可选项
套接字可选项进而I/O缓冲大小 我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性.但是,理解这些特性并根据实际需要进行更改也十分重要.之前我们写的程序在创建好套接字后都是未经特别操 ...
- 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网络编程之网络编程和套接字
网络编程和套接字 网络编程又称为套接字编程,就是编写一段程序,使得两台连网的计算机彼此之间可以交换数据.那么,这两台计算机用什么传输数据呢?首先,需要物理连接,将一台台独立的计算机通过物理线路连接在一 ...
- 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套接字是面向连接的,因此又称为基于流的 ...
随机推荐
- vs code python保存时pylint提示"Unable to import 'flask'"
在配置vscode python开发环境时,编写如下代码并保存时,会提示Unable to import 'flask' from flask import Flask app = Flask(__n ...
- 洛谷P1164 小A点菜
//求方案数 定义状态f[i][j] 用前i件物品恰好放够体积为j的背包 方案数 #include<bits/stdc++.h> using namespace std; ; ; int ...
- 1x1卷积
你可能会想为什么有人会用1x1卷积,因为它关注的不是一块像素,而是一个像素,图1 图1 我们看看传统的卷积,它基本上是运行在一个小块图像上的小分类器,但仅仅是个线性分类器.图2 图2 如果你在中间加一 ...
- @noi.ac - 492@ casino
目录 @description@ @solution@ @solution@ @part - 1@ @part - 2@ @part - 3@ @accepted code@ @details@ @d ...
- 【codeforces 520C】DNA Alignment
[题目链接]:http://codeforces.com/contest/520/problem/C [题意] 给你一个函数; 它的作用是评估两个字符串的相似程度; 评估的时候: 保持一个字符串不动, ...
- Laravel获取所有的数据库表及结构
遇到一个需求,需要修改数据库中所有包含email的字段的表,要把里面的长度改为128位.Laravel获取所有的表,然后循环判断表里面有没有email这个字段.代码如下: use Illuminate ...
- 用winrar和zip命令拷贝目录结构
linux系统下使用zip命令 zip -r source.zip source -x *.php -x *.html # 压缩source目录,排除里面的php和html文件 windows系统下使 ...
- java线程与进程的比较
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元:而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任 ...
- [转]JavaWeb工程 目录结构
今天为此事纠结了一段时间. 以下是mavaen推荐的项目目录. ├── pom.xml └── src ├── main │ ├── java │ │ └── my ...
- Scrapy项目注意事项