套接字的多种可选项
具体源码可以参考我的GitHub
1. 套接字可选项和I/O缓冲大小
我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解这些特性并根据实际需要进行更改也很重要.
1.1 套接字的多种可选项
可选项是分层的。
协议层 | 选项名 | 读取 | 设置 |
---|---|---|---|
SOL_SOCKET | SO_SNDBUF | O | O |
SOL_SOCKET | SO_RCVBUF | O | O |
SOL_SOCKET | SO_REUSEADDR | O | O |
SOL_SOCKET | SO_KEEPALIVE | O | O |
SOL_SOCKET | SO_BROADCAST | O | O |
SOL_SOCKET | SO_DONTROUTE | O | O |
SOL_SOCKET | SO_OOBINLINE | O | O |
SOL_SOCKET | SO_ERROR | O | X |
SOL_SOCKET | SO_TYPE | O | X |
IPPROTO_IP | IP_TOS | O | O |
IPPROTO_IP | IP_TTL | O | O |
IPPROTO_IP | IP_MULTICAST_TTL | O | O |
IPPROTO_IP | IP_MULTICAST_LOOP | O | O |
IPPROTO_IP | IP_MULTICAST_IF | O | O |
IPPROTO_TCP | TCP_KEEPALIVE | O | O |
IPPROTO_TCP | TCP_NODELAY | O | O |
IPPROTO_TCP | TCP_MAXSEG | O | O |
大致常用的协议层分三类
SOL_SOCKET层:是套接字的相关通用可选项
IPPROTO_IP层:是IP协议相关事项
IPPROTO_TCP层:是TCP协议相关事项
1.2 常用操作
有以下两个操作Get读取 and 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
optval 保存要修改的选项信息缓冲地址值
optlen 传递可选项信息的字节数
其他同上
调用getsockopt函数的事例如下:
- [sokc_type]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main() {
int TCP_sock, UDP_sock;
int sock_type;
socklen_t optlen;
int state;
TCP_sock = socket(PF_INET, SOCK_STREAM, 0);
UDP_sock = socket(PF_INET, SOCK_DGRAM, 0);
printf("SOCK_STREAM: %d\n SOCK_DGRAM: %d\n", SOCK_STREAM, SOCK_DGRAM);
printf("PF_INET: %d\n", PF_INET);
state = getsockopt(TCP_sock, SOL_SOCKET, SO_TYPE, (void*) &sock_type, &optlen);
if (state)
error("getsockopt() error");
printf("TCP_sock type : %d \n optlen: %d\n", sock_type, optlen);
state = getsockopt(UDP_sock, SOL_SOCKET, SO_TYPE, (void*) &sock_type, &optlen);
if (state)
error("getsockopt() error");
printf("UDP_sock type : %d \n optlen: %d\n", sock_type, optlen);
close(TCP_sock);
close(UDP_sock);
return 0;
}
- 注意事项: 运行有时候会发生错误,多运行几次即可。
- 套接字类型只能在创建决定,后期不能更改。
编译运行:
gcc sock_type.c -o sock_type
./sock_type
运行结果:
SOCK_STREAM: 1
SOCK_DGRAM: 2
PF_INET: 2
TCP_sock type : 1
optlen: 4
UDP_sock type : 2
optlen: 4
1.3 I/O缓冲大小
生成套接字的时候将同时生成I/O缓冲。
SO_RCVBUF是输入缓冲大小可选项,SO_SNDBUF是输出缓冲大小可选项
读取与更改事例
读取
- [get_buf.c]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main() {
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("getsockopt() error");
len = sizeof(rcv_buf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
if (state)
error("getsockopt() error");
printf("INPUT buffer: %d\n", rcv_buf);
printf("OUTput buffer: %d\n", snd_buf);
close(sock);
return 0;
}
编译运行:
gcc get_buf.c -o get_buf
./get_buf
运行结果:
INPUT buffer: 131072
OUTPUT buffer: 16384
这是我的输入输出缓冲大小
更改
- [set_buf.c]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]) {
int sock, state;
int snd_buf = 1024*3;
int rcv_buf = 1024*3;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(rcv_buf);
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));
if (state)
error("setsockopt() 1 error");
len = sizeof(snd_buf);
state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
if (state)
error("setsockopt() 2 error");
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
if (state)
error("getsockopt() 1 error");
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
if (state)
error("getsockopt() 2 error");
printf("INPUT :%d\n", rcv_buf);
printf("OUTPUT :%d\n", snd_buf);
close(sock);
return 0;
}
编译运行:
gcc set_buf.c -o set_buf
./set_buf
运行结果:
INPUT : 6144
OUTPUT : 6144
运行结果跟预想不一样很正常,因为缓冲大小设置需要很谨慎,不会完全按我们的要求去改,会自动保证一点空间。
2. SO_REUSEADDR
更改可选项SO_REUSEADDR的状态,可以决定在Time-wait状态下的套接字端口号是否能分配给新的套接字。
默认值为0意味着此状态下不能分配给新的套接字,改成1即为可以分配。
2.1 什么是Time-wait状态
在套接字断开连接时,会经历四次握手无论是调用close() 还是 Ctrl +c
先断开连接的会先发送FIN消息收到的会开启time-wait状态为了等待判断自己的ACK是否发送到了,因为如果自己的没法送到就关闭套接字了,另一端超时会误认为FIN未发送到一直重发,所以就是为了等待FIN没有重发来判断自己的发送成功,所以一般等待时间在3分钟左右。
实际上,不论是服务端还是客户端,都要经过一段时间的 Time-wait 过程。先断开连接的套接字必然会经过 Time-wait 过程,但是由于客户端套接字的端口是任意制定的,所以无需过多关注 Time-wait 状态.
这也就是为什么断开连接后立即运行同个断开会发生地址分配失败的原因。
2.2 更改可选项SO_REUSEADDR
来判断关闭后同个端口立即运行新的套接字。
- [reuseadr_eserver.c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define mx 1024
void error_handling(char *message);
int main(int argc, char* argv[]) {
int ser_sock, cli_sock;
struct sockaddr_in ser_adr, cli_adr;
socklen_t cli_adr_sz;
int state, str_len;
char message[mx];
ser_sock = socket(PF_INET, SOCK_STREAM, 0);
if (ser_sock == -1)
error_handling("socket() error");
int option;
int optlen = sizeof(option);
option = 1;
state = setsockopt(ser_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);
if (state)
error_handling("setsockopt() error");
memset(&ser_adr, 0, sizeof(ser_adr));
ser_adr.sin_family = AF_INET;
ser_adr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_adr.sin_port = htons(atoi(argv[1]));
if (bind(ser_sock, (struct sockaddr*)&ser_adr, sizeof(ser_adr)) == -1)
error_handling("bind() error");
if (listen(ser_sock, 5) == -1)
error_handling("listen() error");
cli_adr_sz = sizeof(cli_adr);
cli_sock = accept(ser_sock, (struct sockaddr*)&cli_adr, &cli_adr_sz);
if (cli_sock == -1)
error_handling("socket() error_cli");
while ((str_len = read(cli_sock, message, sizeof(message))) != 0) {
write(cli_sock, message, str_len);
// write(1, message, str_len);
}
// shutdown(ser_sock, SHUT_WR);
close(cli_sock);
close(ser_sock);
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
编译运行:
gcc reuseadr_eserver.c -o reuseadr_eserver
./reuseadr_eserver 9190
^c
./reuseadr_eserver 9190
运行结果:
无报错
现象:客户端在服务端关闭以后还能用一下才会关闭,服务端关闭后还可以在客户端关闭之前重连。
3. TCP_NODELAY
设置Nagle算法使用状态。
3.1 什么是Nagle算法
Nagle 算法就是只有在接收到前一个数据的回复ACK后才会发送下一个数据,使用与发送短消息时可以提高网络传输效率。
因为它能在等待的时候将跟多的数据传入缓冲区下次发送更大的数据,从而减少数据包的使用,每个数据包都会有头信息,减少使用也就能减少网络流量。
这只适用于小文件传输,而大文件传输的传入输出缓冲不会花费太多时间,也会在装满数据缓冲包时进行传输,所以不会增加数据包的使用,因此,如果不用Nagle算法能够大大提高效率。
3.2 禁用Nagle算法
只需将TCP_NODELAY 改为1 (真)即可
int opt_val = 1
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*) &opt_val, sizeof(opt_val)); // 设置
socklen_t opt_len = sizeof(opt_val);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*) &opt_val, &opt_len ); //查看