第九章:套接字的多种可选项
之前的套接字都是用的同一些参数并没有进行一些更改,这里介绍套接字具有的多种特性。
同时更细致得观察套接字内部。
9.1 套接字可选项和I/O缓冲大小
套接字编程过程中,关注的重点往往是数据通信过程,而套接字的不同特性也是十分重要的。
9.1.1 套接字多种可选项
之前的实例在创建套接字时几乎使用的都是默认参数,没有进行更改,但其实是可以更改的,下面的图标中展示了其中一部分套接字可选项。
上表中可以看出,套接字可选项是分层的。
IPPROTO_IP 层是IP协议相关事项。
IPPROTO_TCP层TCP协议相关的事项。
SOL_SOCKET层是套接字相关的通用可选项。
实际上可选项比上表多很多,但是不是都要背下来的,用的时候去查就行啦,用的多就记住了。下面挑选一些进行讲解。
9.1.2 getsockopt & setsockopt
我们几乎可以针对上表中的所有选项进行读取(get)和设置(set)操作。
可选择项的读取和设置通过下面两个函数完成。
第一个getsockopt
函数,用于读取套接字可选项。
#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传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。
(state1 = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
)
太抽象太抽象,来个例子。
下面的这个例子中,用协议层为SOL_SOCKET、名为SO_TYPE的可选项查看套接字类型(TCP或UDP)(说了也迷迷糊糊的。。。。看下面)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char* message);
int main(int argc,char *argv[])
{
// define some variables
int tcp_sock, udp_scok;
int sock_type;
socklen_t optlen;
int state1, state2;
//define socket
optlen = sizeof(sock_type);
tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
udp_scok = socket(PF_INET, SOCK_DGRAM, 0);
// what is the keyword?
printf("SOCK_STREAM : %d\n", SOCK_STREAM);
printf("SOCK_DGRAM: %d\n", SOCK_DGRAM);
state1 = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
if(state1){
error_handling("getsockopt() error");
}
printf("Socket type one : %d \n", sock_type);
state2 = getsockopt(udp_scok, SOL_SOCKET, SO_TYPE, (void*)&sock_type,&optlen);
if(state2){
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);
}
下面是测试结果
用于验证套接字类型的SO_TYPE是典型的只读可选项,套接字类型只能在创建时决定,不能再更改
上面是读取套接字可选项的函数,下面是更改可选项时调用的函数。
#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 传递的可选项信息的 字节数。(与第四个配套的,但是改了结果不会变)
这部分的实例和后面再一起。
总结一下上面的参数含义
sock:将要被设置或者获取选项的套接字。
level:选项所在的协议层。
optname:需要访问的选项名。
optval:对于getsockopt(),指向 存放get得到的值 的地址。对于setsockopt(),指向存放 想要修改的新数值 的地址 。
optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),设置的。
9.1.3 SO_SNDBUF & SORCVBUF(关于IO缓冲的可选项)
第五章中介绍了 创建套接字的同时会生成 I/O缓冲。
SO_RCVBUF是输入缓冲大小相关可选项,SO_SNDBUF是输出缓冲大小相关可选项。
这两个选项既可以读取当前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_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);
}
结果非常简单,输入缓冲大小 87380字节,输出缓冲大小 32767 字节
下面是更改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,state;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(snd_buf);
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);
}
上面代码中,我自己尝试了修改参数值(这里没有列出),发现setsockopt
函数中 optval
和 optlen
竟然可以不匹配。 尝试修改之后,发现决定性的参数还是 optval
,这个数字决定了我们修改后的 缓冲的大小。
9.2 SO_REUSEADDR(重用地址)
本节主要讲解SO_REUSEADDR及其相关的 Time-wait 状态。
9.2.1 发生地址分配错误(Binding Error)
我们先从下面这段代码中理解一下 什么是Time-wait 状态。
下面这段代码并不是全新的代码,套路都差不多,其实就是从回声服务器端的代码修改来的。 请认真再看一次。
reuseaddr_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
#define BUF_SIZE 1024
void error_handling(char* message);
int main(int argc, char* argv[])
{
int ser_sock , client_sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_addr, client_addr;
socklen_t client_addr_size;
if(argc != 2){
printf("usage : %s<port> \n", argv[0]);
exit(1);
}
ser_sock = socket(PF_INET, SOCK_STREAM, 0);
if(ser_sock == -1){
error_handling("sock() error");
}
/*
optlen = sizeof(option);
option = TRUE;
setsockopt(ser_sock, SOL_SOCKET,SO_REUSEADDR, (void*)& option , optlen);
*/
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(ser_sock, (struct sockaddr*)& serv_addr,sizeof(serv_addr)) == -1){
error_handling("bind() error");
}
if(listen(ser_sock,5) == -1){
error_handling("listen error");
}
client_addr_size = sizeof(client_addr);
client_sock = accept(ser_sock, (struct sockaddr*)&client_addr, &client_addr_size);
if(client_sock == -1){
error_handling("accept error");
}
else{
printf("connected ok!\n");
}
while((str_len = read(client_sock, message, BUF_SIZE)) != 0)
{
write(client_sock, message, str_len);
//write(1, message, str_len);
}
close(client_sock); // 在for中 如果while接收到 客户端不在发送内容,就会关闭这个客户端套接字连接
close(ser_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char* message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc != 3){
printf("Usage %s <IP> <port>\n", argv[0]);
exit(1);
}
// 为客户端分配套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1){
error_handling("socket () error ");
}
// 初始化服务器端的地址信息 这个信息是从main函数的参数中读取的
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]); //字符串形式的 本地字节序转为网络字节序
serv_adr.sin_port = htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1){
error_handling("connect() error");
}
else{
puts("Connected...... ");
}
while(1)
{
fputs("Input message(Q to quit):", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")){
break;
}
write(sock, message, strlen(message));
/*
* fd:显示数据接受对象的文件描述 buf:要保存的数据的缓冲地址值 nbytes:要接收数据的最大字节数
* ssize_t read(int fd, void* buf,size_t nbytes);
* 成功时返回接收的字节数,(但遇到文件结尾则返回0),失败返回-1
*/
str_len = read(sock,message,BUF_SIZE - 1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
测试结果见 第四章图片。
这里和以前可能没什么区别,但是我们尝试一下,如果先用 ctrl+c断开服务器的连接,再断开客户端,会发生什么?
当我们再次启用服务器端 的时候,如果我们采用相同的端口号, 就会报错bind() error
。
这是为什么呢?
这样的顺序就还是模拟了服务器端向客户端发送 FIN 消息的情景。 如果以这种方式终止程序 ,大约在3 分钟之后,才能使用相同的端口号进行运行服务器端。
9.2.2 Time-wait 状态
下图为 之前说过的四次握手的流程图。
其中 A为服务器端, B为客户机端。
图中的顺序是 A向B发送 FIN消息,相当于在服务器端控制台输入了 ctrl + c 。
从图中可以看到,套接字经历了4次握手之后,没有立刻清除。而是经过了一段 Time-wait 状态。
只有先断开连接的(先发送FIN消息的)主机才会经过 Time-wait状态
因此套接字没有清除,所以端口号还是被使用的状态。当然如果再次使用这个端口号就会报错。
注意: 所有先断开连接的套接字都会经历Time-wait 过程。但是客户端不需要考虑,因为每次运行程序时都会动态分配端口号,因此无需过多关注Time-wait状态。
那为什么要有这个 Time-wait 状态呢??
假设上图中A向B传输的最后一条消息ACK肚子,B会一直重复发送上一条FIN消息,然而这时A已经断了,B永远无法接收到最后一条ACK; 如果A处于 Time-wait状态,就会重新向B传输最后的消息, 主机B也就是可以正常终止。
9.2.3 地址再分配
标题的意思是,我们想要把Time-wait状态取消,让这个地址下的端口号可以重新使用。
因为在系统发生故障时,需要尽快开启服务器,Time-wait 的时间可能造成很大的损失。
下图是一个延长Time-wait过程的情况。
如上图,如果最后的数据丢失,B认为A未能收到自己的FIN消息, 重新传输会让A重新开启 Time-wait 计时器,因此如果网络不理想,这个状态可能还会继续持续下去。。。
解决方法就是 用 setsockopt
函数,控制可选项,改变 SO_REUSEADDR 的状态,调整改参数,能使 Time-wait状态下的套接字端口号中心分配给新的套接字。
参数 SO_REUSEADDR 的默认为:0
我们给改为: 1
就ok啦~
代码在上面服务器代码中 给出了, 只要去掉注释~
int option = TURE;
int optlen = sizeof(option);
setsockopt(ser_sock, SOL_SOCKET, SO_REUSEADDR, (void*)& option,optlen);
9.3 TCP_NODELAY(NO delay)
9.3.1 Nagle算法
TCP套接字默认使用Nagle算法。
先看图
使用Nagle算法: 只有收到前一个数据的 ACK消息时,Nagle算法才发送下一个数据。
未使用Nagle算法:数据到达输出缓冲后将立即被发送出去,上图是极端情况,产生了10个数据包。
不使用会对网络流量产生负面影响。为了提高网络传输效率,要使用Nagle算法。
但并非什么时候都是用,根据传输数据的特性,网络流量未受太大影响时,不用Nagle算法传输速度要更快。
最典型的是“传输大文件数据”, 文件数据传入输出缓冲很快,即使不使用nagle算法,也会在装满输出缓冲时传输数据包。
(从这句话我们可以看出,Nagle算法的目的是为了减少通信带来的数据包增加的情况,但是如果像上面这样的情况下,同样可以瞬间填满输出缓冲,进而直接传输数据包,也不会花费什么数据包的通信流量。)
综上: 一般情况下,不选择Nagle算法可以提高传输速度,但是为准去判断数据特性时,不应该禁用。
9.3.2 禁用Nagle算法
只要将套接字选项 TCP_NODELAY 改为1 即可
int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, &opt_len);
同样也可以查看~
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,如果禁用了 则保存1