套接字可选项和I/O缓冲大小
套接字多种可选项
之前的程序创建好套接字之后是直接使用的,此时通过默认的套接字特性进行数据通信。
示例有时需要特别操作套接字特性,就要更改一些可选项。
套接字可选项分层:
IPPROTO_IP层可选项是IP协议相关事项
IPPROTO_TCP层是TCP协议相关的事项
SOL_SOCKET层是套接字相关的通用可选项
函数getsockopt & setsockopt
getsockopt函数读取套接字可选项:
setsockopt更改可选项:
getsockopt()调用方法及其作用:
/* getsockopt函数的调用方法
* 用协议层SOL_SOCKET,名为SO_TYPE的可选项查看套接字类型(TCP或UDP) */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
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); //生成TCP套接字
udp_sock = socket(PF_INET,SOCK_DGRAM,0); //生成UDP套接字
printf("SOCK_STREAM: %d \n",SOCK_STREAM);
printf("SOCK_DGRAM : %d \n",SOCK_DGRAM);
//输出创建套接字时传入的SOCK_STREAM,SOCK_DGRAM.
/* getsockopt()获取套接字信息
* TCP套接字将获得SOCK_STREAM常数值1,UDP套接字将获得SOCK_DGRAM常数值2 */
state = getsockopt(tcp_sock,SOL_SOCKET,SO_TYPE,(void*)&sock_type,&optlen); //SO_TYPE表示查看套接字的类型
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;
}
运行结果:./sock_type
可选项SO_SNDBUF & SO_RCVBUF
创建套接字将同时生成I/O缓冲。SO_RCVBUF是输入缓冲大小可选项。SO_SNDBUF是输出缓冲大小可选项。
用这两个可选项既可以读取当前I/O缓冲大小,也可以进行更改。
通过以下示例读取创建套接字时默认I/O缓冲大小:
/* 读取I/O缓冲大小 */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
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); //SO_SNDBUF代表读取输出(发送)缓冲区
if (state)
error_handling("getsockopt() error!");
len = sizeof(rcv_buf);
state = getsockopt(sock,SOL_SOCKET,SO_RCVBUF,(void*)&rcv_buf,&len); //SO_RCVBUF代表读取输入(接收)缓冲区
if (state)
error_handling("getsockopt() error!");
printf("Input buffer size: %d \n",rcv_buf);
printf("Output buffer size: %d \n",snd_buf);
return 0;
}
运行结果: ./get_buf
下例setsockopt()函数改变I/O缓冲大小:
/* setsockopt()设置套接字一些特性
* 本例设置I/O缓冲大小
* */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
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));
//设置输入缓冲长度为rcv_buf:3
if (state)
error_handling("setsockopt() error!");
state = setsockopt(sock,SOL_SOCKET,SO_SNDBUF,(void*)&snd_buf,sizeof(snd_buf));
//设置输出缓冲长度为snd_buf:3
if (state)
error_handling("setsockopt() error!");
len = sizeof(snd_buf);
state = getsockopt(sock,SOL_SOCKET,SO_SNDBUF,(void*)&snd_buf,&snd_buf);
if (state)
error_handling("getsockopt() error!");
len = sizeof(rcv_buf);
state = getsockopt(sock,SOL_SOCKET,SO_RCVBUF,(void*)&rcv_buf,&rcv_buf);
if (state)
error_handling("getsockopt() error!");
printf("Input buffer size : %d \n",rcv_buf);
printf("Output buffer size : %d \n",snd_buf);
return 0;
}
运行结果:
(并不完全按照我们的要求进行)
SO_REUSEADDR
可选项SO_REUSEADDR及其相关的Time_wait状态很重要!
发生地址分配错误(Binding Error)
/* 回声服务器端 */
#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)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
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,(void*)&option,optlen); //使套接字在Time-wait状态下的端口号可以重新分配给新的套接字
*/
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)) == -1)
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); //1是标准输出,将message中的消息输出
}
close(clnt_sock);
close(serv_sock);
return 0;
}
//在客户端控制台输入Q或者通过CTRL+C终止程序
(调用close函数,向服务器端发送FIN消息并经过四次握手过程)
通常都是由客户端先请求断开连接,这种情况重新运行服务器端不成问题。
若是在服务器端控制台输入CTRL+C,强制关闭服务器端(模拟服务器端向客户端发送FIN消息),服务器端重新运行会产生问题,用同一端口运行服务器端,将输出bind() error的消息。
上述两种情况唯一的区别是谁先传输了FIN消息,结果却迥然不同,原因何在呢?
Time-wait状态
重温四次握手过程:
假设A是服务器端,主机A向B发送FIN信息相当于在服务器端控制台输入CTRL+C。
套接字经过四次握手过程后并非立即删除,而是要经过一段时间的Time-wait状态。套接字处在Time-wait状态时,相应端口是正在使用的状态。因此bind()调用过程中当然会发生错误。
先端开连接的(先发送FIN消息)的主机才经过Time-wait状态。
提示:
不管是服务器端还是客户端都会有Time-wait状态。先端开连接的套接字必然会经过Time-wait过程。因为客户端套接字的端口号是任意指定的,与服务器端不同,客户端每次运行程序时都会动态分配端口号,因此无需过多关注Time-wait过程 (所以上例中从客户端结束,再运行服务器端分配同样端口号不会bind() error!)
Time-wait状态的作用:
四次握手过程中, 假设A向B发送完最后一条ACK消息后立即消除套接字:
若主机A向主机B传输最后一条ACK消息(SEQ 7501,ACK 5001)在传递途中丢失,未能传给主机B。主机B没收到确认信号,会认为之前发送的FIN消息(SEQ 7501, ACK 5001)未能抵达主机A,继而重传。若此时A已是完全终止的状态,则主机B永远无法收到从主机A最后传来的ACK消息。相反,若主机A的套接字处于Time-wait状态,则会向主机B重传最后的ACK消息。
基于这些考虑,先传输FIN消息的主机应经过Time-wait过程。
地址再分配
Time-wait有时并不那么方便。若系统发生故障从而紧急停止的情况,需要尽快重启服务器端以提供服务,但因处于Time-wait状态而必须等几分钟。因此,TIme-wait状态也有缺点。
四次握手不得不延长Time-wait过程的情况
若最后的数据丢失,B会重传FIN消息,收到FIN消息的主机A将重启Time-wait计时器。因此,若网络不理想,Time-wait状态将持续。
解决方案:
在套接字的可选项中更改SO_REUSEADDR的状态。调整该参数,可将Time-wait状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR的默认值是0,意味着无法分配Time-wait状态下的套接字端口号。将此值该为1。(如上示例代码,改动在注释中)
optlen = sizeof(option);
option = TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);
此时,处于服务器端变为可随时运行的状态。
TCP_NODELAY
TCP_NODELAY可选项属于IPPROTO_TCP协议层 (头文件是arpa/inet.h)
而要使用TCP_NODELAY可选项,必须包含头文件(netinet/tcp/h)
Nagle算法
Nagle算法防止因数据包过多而发生网络过载。应用与TCP层。是否使用的差异如下图:
结论:只有收到前一条数据的ACK消息时,Nagle算法才发送下一数据!
TCP套接字默认使用Nagle算法交换数据,最大限度地进行缓冲,直到收到ACK。
左侧:为了发送Nagle,将其传递到缓冲区。因为头字符N之前没有其他数据(没有需接收的ACK),因此立即传输。等待收到了N的ACK消息(等待过程中agle填入输出缓冲),收到ACK后,将输出缓冲中的agle装入一个数据包发送。 整个过程只用了4个数据包。
右侧:N到e字符依次传到输出缓冲,发送过程与ACK接收与否无关。数据到达缓冲后立即被发送出uq。整个过程用了10个数据包。 因此不使用Nagle算法将对网络流量产生负面影响。
因此,为了提高网络传输效率,必须使用Nagle算法。
提示:上图过程分析是极端情况,实际程序中将字符传给输出缓冲时并不是逐字传递的。
有些情况下Nagle算法也不适用:传输大文件数据时,将文件数据传入输出缓冲不会花太多时间,因此,即便不使用Nagle算法,也会装满输出缓冲时传输数据包。不使用Nagle算法反而可以无需等待ACK连续传输。
禁用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算法可以提高传输速率。但如果无条件放弃使用Nagle算法,会增加过多的网络流量,反而影响传输。因此,是否使用Nagle算法应再三斟酌。