SO_RCVTIMEO, SO_SNDTIMEO介紹
套接字選項SO_RCVTIMEO: 用來設置socket接收數據的超時時間;
套接字選項SO_SNDTIMEO: 用來設置socket發送數據的超時時間;
比如,一般情況下,調用accept/connect/send/recv, 進程會阻塞,但是如果對端異常,進行可能無法正常退出等待。如何讓這些調用自動定時退出?
可以使用諸如alarm定時器、I/O復用設置定時器,還可以使用socket編程里函數級別的socket套接字選項SO_RCVTIMEO和SO_SNDTIMEO,僅針對與數據接收和發送相關,而無需設置專門的信號捕獲函數。
能夠作用的系統調用包括:send、sendmsg、recv、recvmsg、accept、connect。
- EAGAIN通常和EWOULDBLOCK是同一個值;
- SO_RCVTIMEO, SO_SNDTIMEO不要求系統調用對應fd是非阻塞(nonblocking)的,但是使用了該套接字選項的sock fd,會成為nonblocking(即使之前是blocking)的。參見man手冊ERRORS EAGAIN/EWOULDBLOCK的描述;
示例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 # 服務器端超時未輸入數據,客戶端程序運行結束
寫超時
[mapan@localhost sockOption]$ ls
client.cpp makefile server.cpp
[mapan@localhost sockOption]$ cat client.cpp
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define MAXLINE 4096
int main(int argc,char **argv)
{
int connfd,ret;
char sendbuf[400000]={0};
struct sockaddr_in servaddr;
if(argc != 2)
{
printf("error\n");
}
connfd=socket(AF_INET,SOCK_STREAM,0);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(6666);
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
connect(connfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
struct timeval stTimeValStruct;
stTimeValStruct.tv_sec=5;
stTimeValStruct.tv_usec=0;
setsockopt(connfd,SOL_SOCKET,SO_SNDTIMEO,&stTimeValStruct,sizeof(stTimeValStruct));
while(1)
{
ret= write(connfd,sendbuf,sizeof(sendbuf));
printf("ret=%d\n",ret);
}
close(connfd);
return 0;
}
[mapan@localhost sockOption]$ cat server.cpp
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define MAXLINE 4096
int main()
{
int listenfd,acceptfd;
struct sockaddr_in servaddr;
listenfd=socket(AF_INET,SOCK_STREAM,0);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(6666);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
listen(listenfd,10);
acceptfd=accept(listenfd,(struct sockaddr *)NULL,NULL);
getchar();
close(acceptfd);
close(listenfd);
return 0;
}
[mapan@localhost sockOption]$ cat makefile
all:server client
server.o:server.cpp
g++ -c server.cpp
client.o:client.cpp
g++ -c client.cpp
server:server.o
g++ -o server server.o
client:client.o
g++ -o client client.o
clean:
rm -f server client *.o
[mapan@localhost sockOption]$
編譯并運行,客戶端需要新打開一個窗口執行。
[mapan@localhost sockOption]$ make
g++ -c server.cpp
g++ -o server server.o
g++ -c client.cpp
g++ -o client client.o
[mapan@localhost sockOption]$ ./server
[mapan@localhost sockOption]$ ./client 127.0.0.1
ret=400000
ret=400000
ret=400000
ret=254012
ret=-1
ret=-1
^C
[mapan@localhost sockOption]$
再看看看接收緩沖區和發送緩沖區:
socket緩沖區配置
cat /proc/sys/net/ipv4/tcp_wmem
4096 65535 33554432
65535 : tcp 發送緩沖區的默認值
cat /proc/sys/net/ipv4/tcp_rmem
4096 65535 16777216
65535 :tcp接收緩沖區的默認值
cat /proc/sys/net/core/wmem_max
33554432
33554432: tcp 或 udp 發送緩沖區最大可設置值的一半。
tcp 或udp收發緩沖區最小值
tcp 或udp接收緩沖區的最小值為 256 bytes,由內核的宏決定;
tcp 或udp發送緩沖區的最小值為 2048 bytes,由內核的宏決定
ss -nt | grep 6666