文章目录
概叙
理论
如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么setsockopt
、getsockopt
就是专门用来读取和设置socket文件描述符属性的方法
#include <sys/socket.h>
/*
* 参数: sockfd: 指向一个打开的套接字描述符
* level: 指定系统中解释选项的代码或者通用套接字,或者指定要操作哪个协议的选项(比如IPV4,IPV6,TCP, SCTP)
* optname:指定选项的名字
* optval、optlen:被操作选项的值和长度
* setsockopt从*optval中取得选项待设置的新值
* getsockopt则把已获取的选项当前值放到*optval中。
* 返回值: 成功0,出错-1并设置error
*/
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
int getsockopt(int socket, int level, int option_name,
void *restrict optval, socklen_t *restrict option_len);
下面汇总了所有可以由setsockopt
和getsockopt
设置的选项。其中“数据类型”给出了指针optval必须指向的每个选项的数据类型,比如linger{}
表示struct linger
实践:当前系统支持哪些socket选项
检查上表选项是否得到支持,如果有,则输出默认值
#include <netinet/tcp.h> /* for TCP_xxx defines */
#include <stddef.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unp.h>
//在Union类型中枚举每一个getsockopt的每个可能的返回值
union val {
int i_val;
long l_val;
struct linger linger_val;
struct timeval timeval_val;
} val;
// 为用户输出给定套接字选项的4个函数定义原型
static char *sock_str_flag(union val *, int);
static char *sock_str_int(union val *, int);
static char *sock_str_linger(union val *, int);
static char *sock_str_timeval(union val *, int);
// sock_opts结构包含给每个套接字选项调用getsockopt并输出其当前值所需要的信息
// sock_opts数组里面的每个元素代表一个套接字选项
struct sock_opts {
const char *opt_str;
int opt_level;
int opt_name;
char *(*opt_val_str)(union val *, int);
} sock_opts[] = {
{ "SO_BROADCAST", SOL_SOCKET, SO_BROADCAST, sock_str_flag },
{ "SO_DEBUG", SOL_SOCKET, SO_DEBUG, sock_str_flag },
{ "SO_DONTROUTE", SOL_SOCKET, SO_DONTROUTE, sock_str_flag },
{ "SO_ERROR", SOL_SOCKET, SO_ERROR, sock_str_int },
{ "SO_KEEPALIVE", SOL_SOCKET, SO_KEEPALIVE, sock_str_flag },
{ "SO_LINGER", SOL_SOCKET, SO_LINGER, sock_str_linger },
{ "SO_OOBINLINE", SOL_SOCKET, SO_OOBINLINE, sock_str_flag },
{ "SO_RCVBUF", SOL_SOCKET, SO_RCVBUF, sock_str_int },
{ "SO_SNDBUF", SOL_SOCKET, SO_SNDBUF, sock_str_int },
{ "SO_RCVLOWAT", SOL_SOCKET, SO_RCVLOWAT, sock_str_int },
{ "SO_SNDLOWAT", SOL_SOCKET, SO_SNDLOWAT, sock_str_int },
{ "SO_RCVTIMEO", SOL_SOCKET, SO_RCVTIMEO, sock_str_timeval },
{ "SO_SNDTIMEO", SOL_SOCKET, SO_SNDTIMEO, sock_str_timeval },
{ "SO_REUSEADDR", SOL_SOCKET, SO_REUSEADDR, sock_str_flag },
#ifdef SO_REUSEPORT
{ "SO_REUSEPORT", SOL_SOCKET, SO_REUSEPORT, sock_str_flag },
#else
{ "SO_REUSEPORT", 0, 0, NULL },
#endif
{ "SO_TYPE", SOL_SOCKET, SO_TYPE, sock_str_int },
// { "SO_USELOOPBACK", SOL_SOCKET, SO_USELOOPBACK, sock_str_flag },
{ "IP_TOS", IPPROTO_IP, IP_TOS, sock_str_int },
{ "IP_TTL", IPPROTO_IP, IP_TTL, sock_str_int },
#ifdef IPV6_DONTFRAG
{ "IPV6_DONTFRAG", IPPROTO_IPV6,IPV6_DONTFRAG, sock_str_flag },
#else
{ "IPV6_DONTFRAG", 0, 0, NULL },
#endif
#ifdef IPV6_UNICAST_HOPS
{ "IPV6_UNICAST_HOPS", IPPROTO_IPV6,IPV6_UNICAST_HOPS,sock_str_int },
#else
{ "IPV6_UNICAST_HOPS", 0, 0, NULL },
#endif
#ifdef IPV6_V6ONLY
{ "IPV6_V6ONLY", IPPROTO_IPV6,IPV6_V6ONLY, sock_str_flag },
#else
{ "IPV6_V6ONLY", 0, 0, NULL },
#endif
{ "TCP_MAXSEG", IPPROTO_TCP,TCP_MAXSEG, sock_str_int },
{ "TCP_NODELAY", IPPROTO_TCP,TCP_NODELAY, sock_str_flag },
#ifdef SCTP_AUTOCLOSE
{ "SCTP_AUTOCLOSE", IPPROTO_SCTP,SCTP_AUTOCLOSE,sock_str_int },
#else
{ "SCTP_AUTOCLOSE", 0, 0, NULL },
#endif
#ifdef SCTP_MAXBURST
{ "SCTP_MAXBURST", IPPROTO_SCTP,SCTP_MAXBURST, sock_str_int },
#else
{ "SCTP_MAXBURST", 0, 0, NULL },
#endif
#ifdef SCTP_MAXSEG
{ "SCTP_MAXSEG", IPPROTO_SCTP,SCTP_MAXSEG, sock_str_int },
#else
{ "SCTP_MAXSEG", 0, 0, NULL },
#endif
#ifdef SCTP_NODELAY
{ "SCTP_NODELAY", IPPROTO_SCTP,SCTP_NODELAY, sock_str_flag },
#else
{ "SCTP_NODELAY", 0, 0, NULL },
#endif
{ NULL, 0, 0, NULL }
};
/* *INDENT-ON* */
/* end checkopts1 */
/* include checkopts2 */
int main(int argc, char **argv)
{
int fd;
socklen_t len;
struct sock_opts *ptr;
for (ptr = sock_opts; ptr->opt_str != NULL; ptr++) {
printf("%s: ", ptr->opt_str);
if (ptr->opt_val_str == NULL)
printf("(undefined)\n");
else {
switch(ptr->opt_level) {
case SOL_SOCKET:
case IPPROTO_IP:
case IPPROTO_TCP:
fd = Socket(AF_INET, SOCK_STREAM, 0);
break;
#ifdef IPV6
case IPPROTO_IPV6:
fd = Socket(AF_INET6, SOCK_STREAM, 0);
break;
#endif
#ifdef IPPROTO_SCTP
case IPPROTO_SCTP:
fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
break;
#endif
default:
err_quit("Can't create fd for level %d\n", ptr->opt_level);
}
len = sizeof(val);
if (getsockopt(fd, ptr->opt_level, ptr->opt_name,&val, &len) == -1) {
err_ret("getsockopt error");
} else {
printf("default = %s\n", (*ptr->opt_val_str)(&val, len));
}
close(fd);
}
}
exit(0);
}
static char strres[128];
static char *sock_str_int(union val *ptr, int len)
{
if (len != sizeof(int))
snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);
else
snprintf(strres, sizeof(strres), "%d", ptr->i_val);
return(strres);
}
static char *sock_str_flag(union val *ptr, int len)
{
if (len != sizeof(int))
snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);
else
snprintf(strres, sizeof(strres),"%s", (ptr->i_val == 0) ? "off" : "on");
return(strres);
}
static char *sock_str_linger(union val *ptr, int len)
{
struct linger *lptr = &ptr->linger_val;
if (len != sizeof(struct linger))
snprintf(strres, sizeof(strres),
"size (%d) not sizeof(struct linger)", len);
else
snprintf(strres, sizeof(strres), "l_onoff = %d, l_linger = %d",
lptr->l_onoff, lptr->l_linger);
return(strres);
}
static char *sock_str_timeval(union val *ptr, int len)
{
struct timeval *tvptr = &ptr->timeval_val;
if (len != sizeof(struct timeval))
snprintf(strres, sizeof(strres),
"size (%d) not sizeof(struct timeval)", len);
else
snprintf(strres, sizeof(strres), "%d sec, %d usec",
tvptr->tv_sec, tvptr->tv_usec);
return(strres);
}
通用套接字选项
TCP_DEFER_ACCEPT
Linux 提供的一个特殊 setsockopt , 在 accept 的 socket 上面,只有当实际收到了数据,才唤醒正在 accept 的进程,可以减少一些无聊的上下文切换
- 经过测试发现,设置TCP_DEFER_ACCEPT选项后,服务器受到一个CONNECT请求后,操作系统不会Accept,也不会创建IO句柄。操作系统应该在若干秒,(但肯定远远大于上面设置的1s) 后,会释放相关的链接。但没有同时关闭相应的端口,所以客户端会一直以为处于链接状态。如果Connect后面马上有后续的发送数据,那么服务器会调用Accept接收这个链接端口。
- 感觉了一下,这个端口设置对于CONNECT链接上来而又什么都不干的攻击方式处理很有效。我们原来的代码都是先允许链接,然后再进行超时处理,比他这个有点Out了。不过这个选项可能会导致定位某些问题麻烦。
/**
* 设置监听套接字的延迟接收功能,即当客户端连接上有数据时才将该连接返回
* 给应用,目前该功能仅支持 Linux
* @param fd {ACL_SOCKET} 套接字
* @param timeout {int} 如果客户端连接在规定的时间内未发来数据,也将该连接返回
* 给应用
*/
void acl_tcp_defer_accept(ACL_SOCKET fd, int timeout){
#ifdef TCP_DEFER_ACCEPT
if (timeout < 0)
timeout = 0;
if (setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT,
&timeout, sizeof(timeout)) < 0)
{
acl_msg_error("%s: setsockopt(TCP_DEFER_ACCEPT): %s",
__FUNCTION__ , strerror(errno));
}
#endif
}
可以通过setsockopt来设置defer accept:setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &timeout, sizeof(timeout)) < 0
- 其中,timeout为超时时间,内核会把此时间转换为最大重传次数。
- 单位是秒,注意如果打开这个功能,kernel 在 val 秒之内还没有收到数据,不会继续唤醒进程,而是直接丢弃连接。
在不使用此选项的情况下,TCP三次握手建立连接的过程为:
- Client -> Server:SYN
- Server -> Client: SYN + ACK,Server端的连接为SYN_RECV状态
- Client -> Server: ACK,Client和Server端的连接均为ESTABLISHED
- 使用此选项时,若Client和Server完成三次握手,
- 但Client并没有数据发来,这时Server并不会把连接的状态置为ESTABLISHED,而是会忽略最后一次ACK,保持SYN_RECV状态不变,应用也就没有机会accept这个连接,
- 这样可以避免Client建立连接却不发送请求,导致Server的资源浪费,
- 尤其对于像Apache这样的应用,接收到连接之后会一直阻塞等待Client的请求数据,直到超时,严重影响服务的性能和稳定性。
然,不同的内核版本对此选项的支持却大不一样,比如 2.6.18 和 2.6.32的行为就大不相同,对比内核源码:
- 三次握手完成后,判断是否丢弃后续重传的ACK的条件不一样
- 2.6.18:丢弃ack的条件是设置了defer accept选项:
- 2.6.32:丢弃ack的条件是重传ack的次数小于在设置defer accept时指定的值(内核会根据timeout参数的值计算最大重传次数):
- 三次握手完成后,若一直没有请求数据到来,重传行为不一样
- 2.6.18:重传SYN+ACK,若Client及时响应ACK但没有数据到来,Server就会定时重传SYN+ACK,保持连接状态SYN_RECV不变。
- 2.6.32:实际上并不会多次重传SYN+ACK,虽然中间会定时计算是否需要重传,但只会重传超时时间之内的最后一次SYN+ACK,看下面代码,注意注释:
- 三次握手完成后,若一直没有请求数据到来,Accept行为不一样
- 2.6.18:由于Server端连接状态保持为SYN_RECV,超过最大重传次数后,Server会删掉SYN_RECV状态的连接,应用也就没有机会Accept这个新连接。Client看到的状态仍然是ESTABLISHED,但此连接并没有占用Server的资源。
- 2.6.32:若Client响应最后一次重传的SYN+ACK,由于中间会定时计算是否需要重传并对重传次数计数(req->restrans++),因此上面代码中丢弃ACK的条件就不成立,Server就会把连接置为ESTABLISHED,交给应用Accept这个新连接准备读取数据。下面的代码显示虽然中间不重传SYN+ACK,但仍然计数:
结论:- 2.6.18内核:若三次握手完成后Client没有数据发来,Server的连接状态一直是SYN_RECV,不会Accept新连接,直到把SYN_RECN状态的连接删除,但Client仍然显示ESTABLISHED。
- 2.6.32内核:若三次握手完成后Client没有数据发来,Server的连接状态最终会变成ESTABLISHED,会Accept新连接,Client和Server连接的状态均为ESTABLISHED。
至于2.6.18之后从哪个版本开始变成了2.6.32的行为,没有考证,2.6.32及之后的版本都是同样的处理方式。
此外,当三次握手完成,且没有数据到来,Server的连接状态处于SYN_RECV状态时,如果Client发送FIN,则Server会响应ack及后续的FIN,正常终止连接;在2.6.18上,若Server端已超时,SYN_RECV状态的连接被移除后,Client发送FIN时,Server响应RST。
相关参数:
- net.ipv4.tcp_synack_retries:重传SYN+ACK的次数
- net.ipv4.tcp_syn_retries:重传SYN的次数
- setsockopt中的timeout:超时时间,计算时在这几个参数中具有高优先级
TCP_NODELAY
糊涂窗口综合征:是指当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小; 极端情况下,有效载荷可能只有1个字节;传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象。
如果发送端为产生数据很慢的应用程序服务(典型的有telnet应用),例如,一次产生一个字节。这个应用程序一次将一个字节的数据写入发送端的TCP的缓存。如果发送端的TCP没有特定的指令,它就产生只包括一个字节数据的报文段。结果有很多41字节的IP数据报就在互连网中传来传去。
解决的方法是防止发送端的TCP逐个字节地发送数据。必须强迫发送端的TCP收集数据,然后用一个更大的数据块来发送。发送端的TCP要等待多长时间呢?如果它等待过长,它就会使整个的过程产生较长的时延。如果它的等待时间不够长,它就可能发送较小的报文段。Nagle找到了一个很好的解决方法,发明了Nagle算法。
用TCP_NODELAY选项可以禁止Negale 算法.
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <rpc/types.h>
# define ACL_SOCKET int
# define ACL_SOCKET_INVALID (int) -1
#define error printf
#include <sys/un.h>
typedef union {
struct sockaddr_storage ss;
struct sockaddr_in6 in6;
struct sockaddr_in in;
struct sockaddr_un un;
struct sockaddr sa;
} ACL_SOCKADDR;
int main() {
return 0;
}
/**
* 取得套接字的类型:是网络套接字还是域套接字
* @param fd {ACL_SOCKET} 网络套接字
* @return {int} -1: 表示出错或输入非法或非套接字; >= 0 表示成功获得套接字
* 类型,返回值有 AF_INET、AF_INET6 或 AF_UNIX
*/
int acl_getsocktype(ACL_SOCKET fd)
{
ACL_SOCKADDR addr;
struct sockaddr *sa = (struct sockaddr*) &addr;
socklen_t len = sizeof(addr);
if (fd == ACL_SOCKET_INVALID) {
return -1;
}
if (getsockname(fd, sa, &len) == -1) {
return -1;
}
if (sa->sa_family == AF_UNIX) {
return AF_UNIX;
}
if (sa->sa_family == AF_INET || sa->sa_family == AF_INET6) {
return sa->sa_family;
}
return -1;
}
/**
* 设置 TCP 套接字的 nodelay 功能
* @param fd {ACL_SOCKET} 套接字
* @param onoff {int} 1 表示打开,0 表示关闭
*/
void acl_tcp_nodelay(ACL_SOCKET fd, int onoff){
const char *myname = "acl_tcp_nodelay";
int on = onoff ? 1 : 0;
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return;
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY,
(char *) &on, sizeof(on)) < 0) {
error("%s(%d): set nodelay error(%s), onoff(%d)",
myname, __LINE__, strerror(errno), onoff);
}
}
/**
* 获得 TCP 套接字是否设置了 nodelay 选项
* @param fd {ACL_SOCKET} 套接字
* @return {int} 1 表示打开,0 表示关闭
*/
int acl_get_tcp_nodelay(ACL_SOCKET fd) {
const char *myname = "acl_get_tcp_nodelay";
socklen_t len;
int on = 0;
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return 0;
len = (socklen_t) sizeof(on);
if (getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char*) &on, &len) < 0) {
error("%s(%d): getsockopt error: %s, fd: %d",
myname, __LINE__, strerror(errno), fd);
return -1;
}
return on;
}
/**
* 打开 TCP 套接字的 nodelay 功能
* @param fd {ACL_SOCKET} 套接字
*/
void acl_tcp_set_nodelay(ACL_SOCKET fd)
{
acl_tcp_nodelay(fd, 1);
}
TCP_FASTOPEN
TCP建立连接需要三次握手,这个大家都知道。但是三次握手会导致传输效率下降,尤其是HTTP这种短连接的协议,虽然HTTP有keep-alive来让一些请求频繁的HTTP提高性能,避免了一些三次握手的次数,但是还是希望能绕过三次握手提高效率,或者说在三次握手的同时就把数据传输的事情给做了。TCP Fast Open(简称TFO)就是来干这样的事情的
首先我们回顾一下三次握手的过程:
这里客户端在最后ACK的时候,完全可以将想要发送的第一条数据也一起带过去,这是TFO做的其中一个优化方案。
然后TFO还参考了HTTP登录态的流程,采用cookie的方案,让客户知道某个客户端之前已经登陆过了,那么它发过来的数据就可以直接接收了,不需要一开始必须三次握手再发送数据
当客户端第一次连接服务端时,是没有Cookie的,所以会发送一个空的Cookie,意味着要请求Cookie,如下图:
这样服务端就会将Cookie通过SYN+ACK的路径返回给客户端,客户端保存后,将发送的数据三次握手的最后一步ACK同时发送给服务端。
当客户端断开连接,下一次请求同一个服务端的时候,会带上之前存储的Cookie和要发送的数据,在SYN的路径上一起发送给服务端,如下图:
这样之后每次握手的时候还同时发送了数据信息,将数据传输提前了。服务端只要验证了Cookie,就会将发送的数据接收,否则会丢弃并且再通过SYN+ACK路径返回一个新的Cookie,这种情况一般是Cookie过期导致的。
TFO是需要开启的,开启参数在:
/proc/sys/net/ipv4/tcp_fastopen
0:关闭
1:作为客户端使用Fast Open功能,默认值
2:作为服务端使用Fast Open功能
3:无论是客户端还是服务端都使用Fast Open功能
并且如果之前的代码没有做这方面的处理,也是不能使用的,从上面的流程图就能看到,客户端是在连接的过程就发送数据,但是之前客户端都是先调用connect成功后,才用send发送数据的。
服务端需要对listen的socket设置如下选项:
//需要的头文件
#include <netinet/tcp.h>
int qlen = 5; //fast open 队列
setsockopt(m_listen_socket, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
客户端则直接使用sendto方法进行连接和发送数据,示例代码如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main(){
struct sockaddr_in serv_addr;
struct hostent *server;
const char *data = "Hello, tcp fast open";
int data_len = strlen(data);
int sfd = socket(AF_INET, SOCK_STREAM, 0);
server = gethostbyname("10.104.1.149");
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr,
(char *)&serv_addr.sin_addr.s_addr,
server->h_length);
serv_addr.sin_port = htons(5556);
int len = sendto(sfd, data, data_len, MSG_FASTOPEN/*MSG_FASTOPEN*/,
(struct sockaddr *) &serv_addr, sizeof(serv_addr));
if(errno != 0){
printf("error: %s\n", strerror(errno));
}
char buf[100] = {0};
recv(sfd, buf, 100, 0);
printf("%s\n", buf);
close(sfd);
}
经过试验,客户端存储的Cookie是跟服务端的IP绑定的,而不是跟进程或端口绑定。当客户端程序发送到同一个IP但是不同端口的进程时,使用的是同一个Cookie,而且服务端也认证成功。
SO_REUSEADDR、SO_REUSEPORT
1、SO_REUSEADDR
重用处于TIME_WAIT
的socket
SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的socket,才可以重复绑定使用
。server程序总是应该在调用bind()之前设置SO_REUSEADDR套接字选项。TCP,先调用close()的一方会进入TIME_WAIT状态- 另外一个方法:通过修改内核参数
/proc/sys/net/ipv4/tcp_tw_recycle
来快速回收被关闭的socket,从而使得TCP连接根据不进入TIME_WAIT
状态,进而运行应用程序立即重用本地的socket地址
SO_REUSEADDR提供如下四个功能:
- SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知的端口,即使以前建立的将此端口用作他们的本地端口的连接仍旧存在。这通常是重启监听服务器时出现,如果不设置此项,则bind出错
- 假如一个systemd托管的service异常退出了,留下了TIME_WAIT状态的socket,那么systemd将会尝试重启这个service。但是因为端口被占用,会导致启动失败,造成两分钟的服务空档期,systemd也可能在这期间放弃重启服务。
- 但是在设置了SO_REUSEADDR以后,处于TIME_WAIT状态的地址也可以被绑定,就杜绝了这个问题。因为TIME_WAIT其实本身就是半死状态,虽然这样重用TIME_WAIT可能会造成不可预料的副作用,但是在现实中问题很少发生,所以也忽略了它的副作用。
- SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
- SO_REUSEADDR允许单个进程捆绑在同一端口的多个套接字上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
- SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)
2、SO_REUSEPORT端口重用
Linux kernel 3.9之前的版本,一个ip+port组合,只能被监听bind一次。这样在多核环境下,往往只能有一个线程(或者进程)是listener,在高并发情况下,往往这就是性能瓶颈。于是Linux kernel 3.9之后,Linux推出了端口重用SO_REUSEPORT选项。
SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
-
允许多个套接字 bind()/listen() 同一个TCP/UDP端口
- 每一个线程拥有自己的服务器套接字
- 在服务器套接字上没有了锁的竞争
-
内核层面实现负载均衡
-
安全层面,监听同一个端口的套接字只能位于同一个用户下面
其核心的实现主要有三点:
- 扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport。
- 修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口
- 修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 和端口的多个 sock 之间均衡选择。
有了SO_RESUEPORT后,每个进程可以自己创建socket、bind、listen、accept相同的地址和端口,各自是独立平等的。让多进程监听同一个端口,各个进程中accept socket fd不一样,有新连接建立时,内核只会唤醒一个进程来accept,并且保证唤醒的均衡性。
4、使用这两个套接字选项的定义
- 在所有的TCP服务器上,在调用bind之前设置SO_REUSEADDR选项
- 当编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置SO_REUSEADDR选项,并将本组的多播地址作为本地IP地址捆绑。
SO_REUSEADDR选项的本质?
这个套接字选项通知内核:
- 如果端口忙,但是TCP状态位于TIME_WAIT ,可以重用端口。
- 如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。
一个套接字由相关五元组构成,协议、本地地址、本地端口、远程地址、远程端口。SO_REUSEADDR 仅仅表示可以重用本地本地地址、本地端口,整个相关五元组还是唯一确定的。所以,重启后的服务程序有可能收到非期望数据。必须慎重使用SO_REUSEADDR 选项。
SO_REUSEPORT和SO_REUSEADDR
从字面意思理解,SO_REUSEPORT是端口重用,SO_REUSEADDR是地址重用。两者的区别:
(1)SO_REUSEPORT是允许多个socket绑定到同一个ip+port上。SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的socket,才可以重复绑定使用。
(2)两者使用场景完全不同。SO_REUSEADDR这个套接字选项通知内核,如果端口忙,但TCP状态位于TIME_WAIT,可以重用端口。这个一般用于当你的程序停止后想立即重启的时候,如果没有设定这个选项,会报错EADDRINUSE,需要等到TIME_WAIT结束才能重新绑定到同一个ip+port上。而SO_REUSEPORT用于多核环境下,允许多个线程或者进程绑定和监听同一个ip+port,无论UDP、TCP(以及TCP是什么状态)。
(3)对于多播,两者意义相同。
为什么要引入SO_REUSEPORT:
1、惊群效应:简单来说就是多个进程或者线程等待同一个事件,当事件发生时,所有进程和线程都会被内核唤醒。唤醒之后通常只有一个进程获得了该事件并进行处理,其他进程发现获取事件失败之后又继续进入了等待状态,在一定长度上降低了性能。
2、为什么惊群效应会降低系列性能?
- 多线程/多进程的唤醒,会进行上下文切换。频繁的上下文切换带来的一个问题是数据将频繁的在寄存器与运行队列中流转。极端情况下,时间更多的小号在进程/线程的调度上,而不是执行
- 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
3、常见的惊群问题
在 Linux 下,我们常见的惊群效应发生于我们使用 accept 以及我们 select 、poll 或 epoll 等系统提供的 API 来处理我们的网络链接。
(1) accept 惊群:
首先我们用一个流程图来复习下我们传统的 accept 使用方式
服务器
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define SERVER_ADDRESS "0.0.0.0"
#define SERVER_PORT 10086
#define WORKER_COUNT 4
int worker_process(int listenfd, int i) {
while (1) {
printf("I am work %d, my pid is %d, begin to accept connections \n", i,
getpid());
struct sockaddr_in client_info;
socklen_t client_info_len = sizeof(client_info);
int connection =
accept(listenfd, (struct sockaddr *)&client_info, &client_info_len);
if (connection != -1) {
printf("worker %d accept success\n", i);
printf("ip :%s\t", inet_ntoa(client_info.sin_addr));
printf("port: %d \n", client_info.sin_port);
} else {
printf("worker %d accept failed", i);
}
close(connection);
}
return 0;
}
int main() {
int i = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_ADDRESS, &address.sin_addr);
address.sin_port = htons(SERVER_PORT);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
int ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
ret = listen(listenfd, 5);
for (i = 0; i < WORKER_COUNT; i++) {
printf("Create worker %d\n", i + 1);
pid_t pid = fork();
/*child process */
if (pid == 0) {
worker_process(listenfd, i);
}
if (pid < 0) {
printf("fork error");
}
}
/*wait child process*/
int status;
wait(&status);
return 0;
}
客户端
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include<arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SA struct sockaddr
#define SERV_PORT 10086
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
exit(0);
}
为什么这里没有出现我们想要的现象(一个进程 accept 成功,三个进程 accept 失败)?原因在于在 Linux 2.6 之后,Accept 的惊群问题从内核上被处理了
(2) select/poll/epoll 惊群:
我们以 epoll 为例,我们来看看传统的工作模式
服务器:
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define SERVER_ADDRESS "0.0.0.0"
#define SERVER_PORT 10086
#define WORKER_COUNT 4
#define MAXEVENTS 64
static int create_and_bind_socket() {
int fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_ADDRESS, &server_address.sin_addr);
server_address.sin_port = htons(SERVER_PORT);
bind(fd, (struct sockaddr *)&server_address, sizeof(server_address));
return fd;
}
static int make_non_blocking_socket(int sfd) {
int flags, s;
flags = fcntl(sfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl error");
return -1;
}
flags |= O_NONBLOCK;
s = fcntl(sfd, F_SETFL, flags);
if (s == -1) {
perror("fcntl set error");
return -1;
}
return 0;
}
int worker_process(int listenfd, int epoll_fd, struct epoll_event *events,
int k) {
while (1) {
int n, i;
n = epoll_wait(epoll_fd, events, MAXEVENTS, -1);
printf("Worker %d pid is %d get value from epoll_wait\n", k, getpid());
for (i = 0; i < n; i++) {
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) ||
(!(events[i].events & EPOLLIN))) {
printf("%d\n", i);
fprintf(stderr, "epoll err\n");
close(events[i].data.fd);
continue;
} else if (listenfd == events[i].data.fd) {
struct sockaddr in_addr;
socklen_t in_len;
int in_fd;
in_len = sizeof(in_addr);
in_fd = accept(listenfd, &in_addr, &in_len);
if (in_fd == -1) {
printf("worker %d accept failed\n", k);
break;
}
printf("worker %d accept success\n", k);
close(in_fd);
}
}
}
return 0;
}
int main() {
int listen_fd, s, i;
int epoll_fd;
struct epoll_event event;
struct epoll_event *events;
listen_fd = create_and_bind_socket();
if (listen_fd == -1) {
abort();
}
s = make_non_blocking_socket(listen_fd);
if (s == -1) {
abort();
}
s = listen(listen_fd, SOMAXCONN);
if (s == -1) {
abort();
}
epoll_fd = epoll_create(MAXEVENTS);
if (epoll_fd == -1) {
abort();
}
event.data.fd = listen_fd;
event.events = EPOLLIN;
s = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event);
if (s == -1) {
abort();
}
events = calloc(MAXEVENTS, sizeof(event));
for ( i = 0; i < WORKER_COUNT; i++) {
printf("create worker %d\n", i);
int pid = fork();
if (pid == 0) {
worker_process(listen_fd, epoll_fd, events, i);
}
}
int status;
wait(&status);
free(events);
close(listen_fd);
return EXIT_SUCCESS;
}
客户端同上
怎么使用?
1、封装
# define ACL_SOCKET int
# define ACL_SOCKET_INVALID (int) -1
ACL_SOCKET acl_inet_bind(const struct addrinfo *res, unsigned flag){
ACL_SOCKET fd;
int on;
fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd == ACL_SOCKET_INVALID) {
acl_msg_error("%s(%d): create socket %s",
__FILE__, __LINE__, acl_last_serror());
return ACL_SOCKET_INVALID;
}
on = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
(const void *) &on, sizeof(on)) < 0) {
acl_msg_warn("%s(%d): setsockopt(SO_REUSEADDR): %s",
__FILE__, __LINE__, acl_last_serror());
}
on = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT,
(const void *) &on, sizeof(on)) < 0)
acl_msg_warn("%s(%d): setsocket(SO_REUSEPORT): %s",
__FILE__, __LINE__, acl_last_serror());
if (bind(fd, res->ai_addr, res->ai_addrlen) < 0) {
close(fd);
acl_msg_error("%s(%d): bind error %s",
__FILE__, __LINE__, acl_last_serror());
return ACL_SOCKET_INVALID;
}
return fd;
}
SO_ACCEPTCONN
- SO_ACCEPTCONN: 该套接字是否是监听套接字(listen)
#include <sys/socket.h>
/**
* 检查套接字:是监听套接字还是网络套接字(已连接套接字)
* @param fd {ACL_SOCKET} 套接字句柄
* @return {int} 返回 -1 表示该句柄非套接字,1 为监听套接字,0 为非监听套接字
*/
# define ACL_SOCKET int
int acl_check_socket(ACL_SOCKET fd)
{
int val, ret;
socklen_t len = sizeof(val);
ret = getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, (void*) &val, &len);
if (ret == -1) {
return -1;
} else if (val) {
return 1;
} else {
return 0;
}
}
/**
* 判断套接字是否为监听套接字
* @param fd {ACL_SOCKET} 套接字句柄
* @return {int} 返回值 0 表示非监听套接字,非 0 表示为监听套接字
*/
int acl_is_listening_socket(ACL_SOCKET fd)
{
return acl_check_socket(fd) == 1;
}
SO_SNDBUF和SO_RCVBUF
理论
含义
- SO_SNDBUF:TCP发送缓冲区的容量上限;
- SO_RCVBUF:TCP接受缓冲区的容量上限;
注意:缓冲区的上限不能无限大,如果超过内核设置的上限值,则以内核设置值为准
$ sysctl -a
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_wmem = 8192 65536 16777216
net.ipv4.tcp_mem = 8388608 12582912 16777216
与实际使用内存的关系?
- SO_SNDBUF和SO_RCVBUF只是规定了读写缓冲区大小的上限,在实际使用未达到上限前,SO_SNDBUF和SO_RCVBUF是不起作用的。
- 一个TCP连接占用的内存相当于读写缓冲区实际占用内存大小之和。
- 当我们用
setsockopt
来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个值。这是为了确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞。- 接收缓冲区最小值一般是256
- 发送缓冲区最小值一般是2048
- 我们也可以强制修改内存参数
/proc/sys/net/ipv4/tcp_rmem
和/proc/sys/net/ipv4/tcp_wmem
来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制
与滑动窗口的关系?
接收缓存区和接收滑动窗口关系
接收缓存区包含了滑动窗口,即接收缓存区大小>= 滑动窗口大小。接受缓冲区的数据主要分为两部分:
- 接受滑动窗口内的无序的TCP报文;
- 有序的,应用还未读取的数据(占用比例:1/(2^tcp_adv_win_scale),默认tcp_adv_win_scale配置为2);
因此,当接受缓冲区上限固定后,如果应用程序读取数据的速率过慢,接收滑动窗口会缩小,从而通知连接的对端降低发送速度,避免无谓的网络传输。
发送缓存区和发送滑动窗口关系
发送缓存区包含了发送滑动窗口,即发送缓存区大小>= 发送滑动窗口大小。发送缓冲区的数据主要分为两部分:
- 发送窗口内的数据:已发送还未确认的数据;
- 应用写入的数据;
缓冲区大小预估
一般以BDP来设置最大接收窗口,BDP叫做带宽时延积,也就是带宽与网络时延的乘积。因为BDP就表示了网络承载能力,最大接收窗口就表示了网络承载能力内可以不经确认发出的报文。如下图所示:
根据接受窗口大小的占比1-1/(2^tcp_adv_win_scale),计算出缓冲区大小上限;
举例:例如若我们的带宽为2Gbps,时延为10ms,那么带宽时延积BDP则为2G/8 * 0.01=2.5MB,所以这样的网络中可以设最大接收窗口为2.5MB,当tcp_adv_win_scale=2时最大读缓存可以设为4/3*2.5MB=3.3MB。
1. 获取接收缓冲区的大小
#include <stdlib.h>
#include <stdio.h>
#include <getopt.h>
#include <zconf.h>
#include <sys/socket.h>
int main(int argc, char *argv[])
{
int fd, val;
socklen_t len;
char strres[128];
len = sizeof(val);
fd = socket(AF_INET, SOCK_STREAM, 0);
if(getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &val, &len) == -1)
{
printf("getsockopt error");
exit(0);
}
else
{
if(len != sizeof(int))
snprintf(strres, sizeof(strres), "sizeof (%d) not sizeof(int)", len);
else
snprintf(strres, sizeof(strres), "%d", val);
printf("SO_RCVBUF default = %s\n", strres); // 87380
}
close(fd);
exit(0);
}
2. 封装
//
// Created by oceanstar on 2021/9/7.
//
#include <cerrno>
#include <cstring>
#include <msg/acl_msg.h>
#include <net/acl_getsocktype.h>
#include "acl_tcp_ctl.h"
namespace oceanstar{
/**
* 设置 TCP 套接字的写缓冲区大小
* @param fd {ACL_SOCKET} 套接字
* @param size {int} 缓冲区设置大小
*/
void acl_tcp_set_sndbuf(ACL_SOCKET fd, int size){
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return;
if(setsockopt(fd, SOL_SOCKET, SO_SNDBUF, (char *)&size, sizeof(size)) < 0){
acl_msg_error("(%s:%d): FD %d, SIZE %d: %s\n",
__LINE__, __FUNCTION__ , fd, size, strerror(errno));
}
}
/**
* 获取 TCP 套接字的写缓冲区大小
* @param fd {ACL_SOCKET} 套接字
* @return {int} 缓冲区大小
*/
int acl_tcp_get_sndbuf(ACL_SOCKET fd){
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return 0;
int size;
socklen_t len;
len = (socklen_t) sizeof(size);
if (getsockopt(fd, SOL_SOCKET, SO_SNDBUF, (char *) &size, &len) < 0) {
acl_msg_error("%s(%d): size(%d), getsockopt error(%s)",
__LINE__, __LINE__, size, strerror(errno));
return -1;
}
return size;
}
/**
* 设置 TCP 套接字的读缓冲区大小
* @param fd {ACL_SOCKET} 套接字
* @param size {int} 缓冲区设置大小
*/
void acl_tcp_set_rcvbuf(ACL_SOCKET fd, int size){
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return ;
if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF,
(char *) &size, sizeof(size)) < 0) {
acl_msg_error("%s(%d): size(%d), setsockopt error(%s)",
__FILE__, __LINE__, size, strerror(errno));
}
}
/**
* 获取 TCP 套接字的读缓冲区大小
* @param fd {ACL_SOCKET} 套接字
* @return {int} 缓冲区大小
*/
int acl_tcp_get_rcvbuf(ACL_SOCKET fd){
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return 0;
int size;
socklen_t len;
len = (socklen_t)sizeof(size);
if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF,
(char *) &size, &len) < 0)
{
acl_msg_error("%s(%d): size(%d), getsockopt error(%s)",
__FILE__, __LINE__, size, strerror(errno));
return -1;
}
return size;
}
}
SO_RCVLOWAT、SO_SNDLOWAT
SO_RCVLOWAT、SO_SNDLOWAT
分别表示TCP接收缓存和发送缓冲区的低水位标记。他们一般被IO复用系统调用用来判断socket
是否可读可写:
- 当TCP接收缓冲区中可读数据的总数大于其低水位标记时,IO复用系统调用将通知应用程序可以从对应的
socket
上读取数据; - 当TCP发送缓冲区中空闲空间的大小大于其低水位标记时,IO复用系统调用将通知应用程序可以往对应的
socket
上写数据
默认情况下,TCP接收/发送缓冲区的低水位标记均为1
SO_BROADCAST套接字选项
- 本选项开启或禁止进程发送广播消息的能力。只有数据报套接字支持广播,并且还必须是在支持广播消息的网络上(例如以太网,令牌环网等)。
- 我们不可能在点对点链路上进行广播,也不可能在基于连接的传输协议(例如TCP和SCTP)之上进行广播。
SO_DEBUG 套接字选项
- 本选项仅由TCP支持。当给一个TCP套接字开启本选项时,内核将为TCP在该套接字发送和接受的所有分组保留详细跟踪信息。这些信息保存在内核的某个环形缓冲区中,并可使用trpt程序进行检查。
SO_DONTROUTE套接字选项
SO_ERROR套接字选项
当一个套接字上发生错误时,源自Berkeley的内核中的协议模块将该套接字的名为so_error
的变量设置为标准的Unix Exxx值中的一个。我们称它为该套接字的待处理错误。
内核能以下面两种方式之一立即通知进程这个错误:
- 如果进程阻塞在对该套接字的
select
调用上,那么无论是检查可读条件还是可写条件,select
均返回并设置其中一个或者所有两个条件 - 如果进程使用信号驱动式IO模型,那么给进程或者进程组一个
SIGIO
信号。
进程然后可以通过访问SO_ERROR
套接字选项获取so_error
的值。由getsockopt
返回的整数值就是该套接字的待处理错误。so_error
随后由内核复位为0
-
当进程调用
read
:- 没有数据返回时,那么
read
返回-1而且errno
被设置为so_error
的值(非0值),so_error
设复位为0 - 如果由数据等待被读取,那么
read
返回读取到的数据。
- 没有数据返回时,那么
-
如果在进程调用
write
时so_error
为非0值,那么wriet
返回-1而且被errno
被设为so_error
的值(非0值),so_error
设复位为0
SO_KEEPALIVE套接字选项
SO_KEEPALIVE 保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。
设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方 发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节.它会导致以下三种情况:
1、对方接收一切正常:以期望的ACK响应,应用程序无动作(因为一切正常),又经过没有动静的2小时后,TCP将发出另一个探测分节。
2、对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET
,套接 口本身则被关闭。
3、对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。在发出第一个探测分节11分钟15秒后若仍无响应就放弃。
- 如果该套接字没有任何响应,套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭。
- 如果该套接字收到ICMP响应,ICMP错误是“host unreachable(主机不可达)”,这种情况下待处理错误被置为 EHOSTUNREACH。
SO_LINGER
- 作用: 指定close函数对 面对连接的协议(TCP、SCTP,但不是UDP)如何操作
- 说明:在默认情况下,当调用close关闭socket的使用,close会立即返回,但是,如果
send buffer
中还有数据,系统会试着先把send buffer中的数据发送出去,然后close才返回。SO_LINGER
选项则是用来修改这种默认操作的
SO_LINGER
要求在用户进程和内核间传递如下结构:
#include <sys/socket.h>
struct linger {
int l_onoff //0=off, nonzero=on(开关)
int l_linger //linger time(延迟时间)
}
(1)如果l_onoff
=0,则关闭这个选项。采用默认操作
(2)如果l_onoff
!=0 l_linger =0
:
- close调用立即返回
- TCP模块将通过发送RST(复位)分组而不是用正常的FIN|ACK|FIN|ACK四个分组来关闭该连接。
- 至于发送缓冲区中如果有未发送完的数据,则丢弃。
- 主动关闭一方的TCP状态则跳过TIMEWAIT,直接进入CLOSED。
网上很多人想利用这一点来解决服务器上出现大量的TIMEWAIT状态的socket的问题,但是,这并不是一个好主意,这种关闭方式的用途并不在这儿,实际用途在于服务器在应用层的需求。
(3)如果l_onoff
!=0 l_linger >0
:
此时close的行为取决于两个条件:
- 被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据
- 被关闭的socket是阻塞的还是非阻塞的。
- 如果是阻塞的,close将等待
l_linger
时间,直到TCP模块发送完残留数据并得到对方的确认。如果这段时间内TCP模块没有确认,那么close返回将返回-1并设置errno为EWOULDBLOCK
- 如果是非阻塞的,close立即返回,我们通过返回值和error来判断残留数据是否已经发送完毕
- 如果是阻塞的,close将等待
/**
* 设置 TCP 套接字的 SO_LINGER 选项
* @param fd {ACL_SOCKET} 套接字
* @param onoff {int} 是否启用 SO_LINGER 选项
* @param timeout {int} 当SO_LINGER打开时,取消 timed_wait 的时间,单位为秒
*/
void acl_tcp_so_linger(ACL_SOCKET fd, int onoff, int timeout){
struct linger l;
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return;
l.l_onoff = onoff ? 1 : 0;
l.l_linger = timeout >= 0 ? timeout : 0;
if (setsockopt(fd, SOL_SOCKET, SO_LINGER, (char *) &l, sizeof(l)) < 0) {
acl_msg_error("%s(%d): setsockopt(SO_LINGER) error(%s),"
" onoff(%d), timeout(%d)", __FUNCTION__ , __LINE__,
strerror(errno), onoff, timeout);
}
}
/**
* 获得 TCP 套接字的 linger 值
* @param fd {ACL_SOCKET} 套接字
* @return {int} 返回 -1 表示未设置 linger 选项或内部出错,>= 0 表示设置了
* linger 选项且该值表示套接字关闭后该 TCP 连接在内核中维持 TIME_WAIT 状态
* 的逗留时间(秒)
*/
int acl_get_tcp_solinger(ACL_SOCKET fd){
int n = acl_getsocktype(fd);
if (n != AF_INET && n != AF_INET6)
return -1;
struct linger l;
socklen_t len = (socklen_t) sizeof(l);
memset(&l, 0, sizeof(l));
if (getsockopt(fd, SOL_SOCKET, SO_LINGER, (char*) &l, &len) < 0) {
acl_msg_error("%s(%d): getsockopt error: %s, fd: %d",
__FILE__, __LINE__, strerror(errno), fd);
return -1;
}
return l.l_linger == 0 ? -1 : l.l_linger;
}
SO_ERROR
当一个套接字上发生错误时,内核协议中的协议模块将此套接字的名为so_error的变量设为标准的Unix Exxx值中的一个,我们称它为该套接字上的待处理错误(pending error)
内核能够以下面两种方式之一立即通知进程这个错误:
-
如果进程阻塞在对该套接字的select调用上,那么无论是检查可读还是可写条件,select均返回并设置其中一个或者所有两个条件
-
如果进程使用信号驱动式IO模型,那就给进程或者进程组产生一个SIGIO信号
进程然后可以通过访问SO_ERROR套接字选项获取so_error的值。由getsockopt返回的整数值就是该套接字的待处理错误。so_error随后由内核复位为0.
-
当进程调用read且没有数据返回时,如果so_error为非0值,那么read返回-1且errno被置为so_error的值。so_error随后被复位为0。如果该套接字上有数据在排队等待读取,那么read返回那些数据而不是返回错误条件。
-
如果在进程调用write时so_error为非0值,那么write返回-1且errno被设为so_error的值。so_error随后被复位为0。
这是一个可以获取但不能设置的套接字选项。
xxxxx
wait_write(fe);
len = sizeof(err);
ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (char *) &err, &len);
if (ret == 0 && err == 0) {
SOCK_ADDR saddr;
struct sockaddr *sa = (struct sockaddr*) &saddr;
socklen_t n = sizeof(saddr);
if (getpeername(sockfd, sa, &n) == 0) {
return 0;
}
return -1;
}
return -1;
SO_RCVTIMEO, SO_SNDTIMEO
- 套接字选项SO_RCVTIMEO: 用来设置socket接收数据的超时时间;
- 套接字选项SO_SNDTIMEO: 用来设置socket发送数据的超时时间;
问:一般情况下,调用accept/connect/send/secv,进程会阻塞,但是如果对端异常,进程可能无法正常退出等待。如何让这些调用自动定时退出
答:可以使用比如alarm定时器、IO复用设置定时器,还可以使用socket编程里函数级别的socket套接字选项SO_RCVTIMEO和SO_SNDTIMEO,仅针对于数据接收和发送相关,而无需设置专门的信号捕获函数。
能够作用的系统调用包括:send、sendmsg、recv、recvmsg、accept、connect。
系统调用 | 有效选项 | 系统调用超时后的行为 |
---|---|---|
send | SO_SNDTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
sendmsg | SO_SNDTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
recv | SO_RCVTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
recvmsg | SO_RCVTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
accept | SO_RCVTIMEO | 返回-1,设置errno为EAGAIN或EWOULDBLOCK |
connect | SO_SNDTIMEO | 返回-1,设置errno为EINPROGRESS |
注意:
- EAGAIN通常和EWOULDBLOCK是同一个值;
- SO_RCVTIMEO, SO_SNDTIMEO不要求系统调用对应fd是非阻塞(nonblocking)的,但是使用了该套接字选项的sock fd,会成为nonblocking(即使之前是blocking)的
示例1:设置connect超时时间
根据系统调用accept的返回值,以及errno判断超时时间是否已到,从而决定是否开始处理超时定时任务。
客户端程序:超时连接服务器
/**
* 客户端程序
* 连接服务器,超时报错、返回
* build:
* $ gcc timeout_connect.c
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
/* 超时连接 */
int timeout_connect (const char *ip, int port, int time)
{
int ret = 0;
struct sockaddr_in servaddr;
printf("client start...\n");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &servaddr.sin_addr);
servaddr.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
/* 通过选项SO_RCVTIMEO和SO_SNDTIMEO设置的超时时间的类型时timeval, 和select系统调用的超时参数类型相同 */
struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof(timeout);
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
if (ret == -1) {
perror("setsockopt error");
return -1;
}
if ((ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) < 0) {
/* 超时对于errno 为EINPROGRESS. 下面条件如果成立,就可以处理定时任务了 */
if (errno == EINPROGRESS) {
perror("connecting timeout, process timeout logic");
return -1;
}
perror("error occur when connecting to server\n");
}
return sockfd;
}
int main(int argc, char *argv[])
{
if (argc <= 2) {
printf("usage: %s ip_address port_number\n", argv[0]);
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
printf("connect %s:%d...\n", ip, port);
int sockfd = timeout_connect(ip, port, 10);
if (sockfd < 0) {
perror("timeout_connect error");
return 1;
}
return 0;
}
运行结果(随意输入一个服务器IP、端口):
$ ./timeout_connect 192.168.0.105 8000
connect 192.168.0.105:8000...
client start...
connecting timeout, process timeout logic: Operation now in progress
timeout_connect error: Operation now in progress
可以看到,本来阻塞的connect调用,10秒后返回-1,并且errno设置为EINPROGRESS。
示例2:超时接收(服务器数据)
服务端
监听本地任意IP地址,端口8001
从键盘输入一行数据,就发送给用户;如果没有数据,就阻塞。
/**
* 服务器程序
* 示例:超时接收服务器数据,超时时间例程中设置为10秒
* 编译: $ gcc timeout_recv_server.c -o server
* 运行方式:
* $ ./server
* 默认监听端口8001(根据实际情况修改)
* 服务器功能:从键盘接收用户输入,每接收一行就向客户输出一行。如果没有用户输入,
* 则阻塞。
* 客户端需要跟服务器安装在同一网段上,为了测试方便,就直接都安装到同一机器上
*/
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <signal.h>
int sockfd = -1;
void sig_func(int sig_no)
{
if (sig_no == SIGINT || sig_no == SIGTERM) {
if (sockfd >= 0) {
close(sockfd);
}
exit(1);
}
}
int main()
{
struct sockaddr_in servaddr, cliaddr;
int listenfd;
signal(SIGINT, sig_func);
printf("server start...\n");
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket error");
exit(1);
}
sockfd = listenfd;
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
perror("setsocketopt error");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
// servaddr.sin_addr.s_addr = INADDR_ANY;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(8001);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind error");
exit(1);
}
if (listen(listenfd, 5) < 0) {
perror("listen error");
exit(1);
}
char buf[1024];
socklen_t clilen = sizeof(cliaddr);
int connfd;
if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {
perror("accept error");
exit(1);
}
printf("input a line string: \n");
int nbytes;
while (fgets(buf, sizeof(buf), stdin)) {
nbytes = send(connfd, buf, strlen(buf), 0);
if (nbytes < 0) {
perror("send error");
break;
}
else if (nbytes == 0) {
}
printf("send: %s\n", buf);
}
close(connfd);
close(listenfd);
return 0;
}
客户端
设置10秒超时,接收服务器数据。
客户端10秒以内,接收到服务器数据,则直接打印;超过10秒,就报错退出。
/**
* 客户端程序
* 示例:超时接收服务器数据,超时时间例程中设置为10秒
* 编译: $ gcc timeout_recv_client.c -o client
* 运行方式:
* 如本地运行(对应服务器实际监听的IP地址和端口号) $ ./client 127.0.0.1 8001
*/
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
int timeout_recv(int fd, char *buf, int len, int nsec)
{
struct timeval timeout;
timeout.tv_sec = nsec;
timeout.tv_usec = 0;
printf("timeout_recv called, timeout %d seconds\n", nsec);
if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
perror("setsockopt error");
exit(1);
}
int n = recv(fd, buf, len, 0);
return n;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
printf("usage: %s <ip address> <port>\n", argv[0]);
}
char *ip = argv[1];
uint16_t port = atoi(argv[2]);
printf("client start..\n");
printf("connect to %s:%d\n", ip, port);
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket error");
exit(1);
}
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &servaddr.sin_addr);
servaddr.sin_port = htons(port);
int connfd;
if ((connfd = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) < 0) {
perror("connect error");
exit(1);
}
printf("success to connect server %s:%d\n", ip, port);
printf("wait for server's response\n");
char buf[100];
while (1) {
int nread;
nread = timeout_recv(sockfd, buf, sizeof(buf), 10);
if (nread < 0) {
perror("timeout_recv error");
exit(1);
}
else if (nread == 0) {
shutdown(sockfd, SHUT_RDWR);
break;
}
write(STDOUT_FILENO, buf, nread);
}
return 0;
}
客户端运行结果:
可以看到,超过10秒后,客户端自动退出程序,而不再阻塞在recv。
$ ./client 127.0.0.1 8001
client start..
connect to 127.0.0.1:8001
success to connect server 127.0.0.1:8001
wait for server's response
timeout_recv called, timeout 10 seconds
hello # 服务器端用户输入数据
timeout_recv called, timeout 10 seconds
nihao # 服务器端用户输入数据
timeout_recv called, timeout 10 seconds
timeout_recv error: Resource temporarily unavailable # 服务器端超时未输入数据,客户端程序运行结束