小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1 网络超时检测
1.1 网络信息检索函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
功能:获取一个套接字的选项
参数:
sockfd:文件描述符
level:协议层次,每一个协议层次对应都有选项名称
查看选项名称使用man 7 层次名,例如man 7 socket
SOL_SOCKET 套接字层次
IPPROTO_TCP TCP层次
IPPROTO_IP IP层次
optname:选项的名称,确定选项名称前需要确定层次
套接字层次:
SO_BROADCAST 是否允许发送广播
SO_RCVBUF 获取接收缓冲区大小
SO_SNDBUF 获取发送缓冲区大小
SO_REUSEADDR 设置端口复用
SO_RCVTIMEO 获取接收超时时间
SO_SNDTIMEO 获取发送超时时间
optval:获取到的选项的值
optlen:optval的大小
返回值:
成功:0
失败:-1
1.1.1 使用getsockopt获取接收和发送缓冲区大小
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
int sockfd;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//使用getsockopt函数获取套接字的选项
int nbuf;
socklen_t lenbuf = sizeof(nbuf);
if(getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &nbuf, &lenbuf) == -1)
{
ERRLOG("getsockopt error");
}
printf("send buf:%dk\n", nbuf / 1024);
if(getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &nbuf, &lenbuf) == -1)
{
ERRLOG("getsockopt error");
}
printf("recv buf:%dk\n", nbuf / 1024);
return 0;
}
1.1.2 使用setsockopt设置端口复用
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
功能:设置一个套接字的选项
参数:
sockfd:文件描述符
level:协议层次
SOL_SOCKET 套接字层次
optname:选项的名称
SO_REUSEADDR 是否允许端口复用
optval:要设置的值
1 允许
0 不允许
optlen:optval的大小
返回值:
成功:0
失败:-1
//使用setsockopt函数设置端口复用
int on = 1;
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
{
ERRLOG("setsockopt error");
}
1.2 网络超时检测相关概念
- 阻塞:以读函数为例,缓冲区中如果有数据,则函数正常执行,如果没有数据,会一直等待;
- 非阻塞:以读函数为例,缓冲区中如果有数据,则函数正常执行,如果没有数据,则立即错误返回;
- 超时检测:他介于阻塞和非阻塞之间,设定一定的时间,在时间范围内如果缓冲区中没有数据,则函数一直阻塞等待,当时间到的时候还没有数据,则函数立即编程非阻塞的形式。
1.3 网络超时检测的方法
- 方法1:使用setsockopt函数实现网络超时检测
- 方法2:使用select函数实现网络超时检测
- 方法3:使用alarm闹钟实现网络超时检测
2 使用setsockopt函数实现网络超时检测
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
功能:设置一个套接字的选项
参数:
sockfd:文件描述符
level:协议层次
SOL_SOCKET 套接字层次
optname:选项的名称
SO_RCVTIMEO 设置接收超时时间
optval:要设置的值
struct timeval {
long tv_sec; 秒
long tv_usec; 微秒
};
optlen:optval的大小
返回值:
成功:0
失败:-1
2.1 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
ssize_t bytes;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//使用setsockopt函数设置端口复用
int on = 1;
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
{
ERRLOG("setsockopt error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用setsockopt实现网络超时检测
//场景:5秒内如果没有客户端连接,则打印连接超时,
// 如果客户端与服务器通信是,5秒内客户端没有说话,
// 则强制与客户端断开
struct timeval mytime;
mytime.tv_sec = 5;
mytime.tv_usec = 0;
if(setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &mytime, sizeof(mytime)) == -1)
{
ERRLOG("setsockopt error");
}
NEXT:
//第五步:阻塞等待客户端的连接
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
//printf("errno = %d\n", errno);
if(errno == 11)
{
printf("连接超时...\n");
goto NEXT;
}
else
{
ERRLOG("accept error");
}
}
//打印客户端的信息
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
//进行通信
while(1)
{
if((bytes = recv(acceptfd, buf, N, 0)) == -1)
{
if(errno == 11)
{
printf("客户端存在挂机行为,强制断开...\n");
close(acceptfd);
goto NEXT;
}
else
{
ERRLOG("recv error");
}
}
else if(bytes == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
goto NEXT;
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
goto NEXT;
}
printf("%s-%d:%s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), buf);
strcat(buf, "^_^");
//如果发送放关闭,当send执行第二次的时候会结束当前进程
//是因为第二次执行send的时候产生了SIGPIPE信号,这个信号默认对当前进程
//的处理方式是结束进程
if(send(acceptfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
}
return 0;
}
2.2 客户端
//TCP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("connect error");
}
//进行通信
char buf[N] = {0};
ssize_t bytes;
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(send(sockfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端退出了\n");
exit(0);
}
memset(buf, 0, N);
if((bytes = recv(sockfd, buf, N, 0)) == -1)
{
ERRLOG("recv error");
}
else if(bytes == 0)
{
printf("长时间不说话,那就结束吧\n");
exit(0);
}
printf("服务器:%s\n", buf);
}
return 0;
}
3 使用select函数实现网络超时检测
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:允许一个程序操作多个文件描述符,阻塞等待文件描述符
准备就绪,如果有文件描述符准备就绪,函数立即返回,
执行相应的IO操作
参数:
nfds:最大的文件描述符加1
readfds:保存读操作文件描述符的集合
writefds:保存写操作文件描述符的集合
exceptfds:保存其他或者异常的文件描述符的集合
timeout:超时
struct timeval {
long tv_sec; 秒
long tv_usec; 微秒
};
参数设置为NULL 阻塞
结构体中的成员设置为0 非阻塞
返回值:
成功:准备就绪的文件描述符的个数
失败:-1
清空集合set
void FD_ZERO(fd_set *set);
将文件描述符fd添加到集合set中
void FD_SET(int fd, fd_set *set);
将文件描述符fd从集合set中移除
void FD_CLR(int fd, fd_set *set);
判断文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set);
返回值:
存在:1
不存在:0
3.1 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
//注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
//如果是自己主机的客户端服务器测试,可以使用127网段的
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用select实现网络超时检测
//select函数在超时检测中相对用的比较多,只要是阻塞函数都可以通过
//select设置超时,所以一般要设置某一个阻塞函数的超时,就在函数基
//础上使用select即可
int ret;
//第一步:创建一个保存要操作的文件描述符集合并清空
fd_set readfds;
FD_ZERO(&readfds);
int maxfd = sockfd;
struct timeval mytime;
while(1)
{
//select函数返回之后,会将设置的超时时间清零,所以需要每次都设置
mytime.tv_sec = 5;
mytime.tv_usec = 0;
//第二步:将要操作的文件描述符添加到集合中
FD_SET(0, &readfds);
FD_SET(sockfd, &readfds);
//第三步:调用select函数,阻塞等待文件描述符准备就绪
if((ret = select(maxfd+1, &readfds, NULL, NULL, &mytime)) == -1)
{
ERRLOG("select error");
}
//select函数超时,函数不会报错,而是立即返回,此时的select函数的
//返回值为0
else if(ret == 0)
{
printf("超时了...\n");
}
else
{
if(FD_ISSET(0, &readfds) == 1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
printf("buf = %s\n", buf);
}
if(FD_ISSET(sockfd, &readfds) == 1)
{
if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("accept error");
}
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
}
}
return 0;
}
3.2 客户端
//TCP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("connect error");
}
return 0;
}
4 使用alarm闹钟实现网络超时检测
4.1 相关概念
信号默认对进程的处理方式:
- 忽略,信号产生之后对进程任何影响
- 终止进程,信号产生之后使得进程结束
- 停止进程,当信号产生之后当前进程变成停止态
- 让停止的进程继续运行,当信号产生之后停止的进程会在后台执行
信号人为处理方式:
- 忽略
- 默认
- 捕捉
使用alarm闹钟实现网络超时检测
当使用alarm闹钟函数设置一定的时候后,代码继续向下运行,当时间达到,会产生SIGALRM信号:
- 如果这个信号按照默认的方式处理,就会让整个进程结束
- 如果这个信号按照捕捉的方式处理,当信号产生之后,会立即执行信号处理函数,当信号处理函数执行完毕之后,会继续回到之前的代码运行,将这种信号的属性称之为自重启属性
- 如果要实现超时检测,需要关闭这个自重启属性,如果关闭了自重启属性,当执行完信号处理函数之后,代码不会继续执行之前的代码,而是错误返回,然后继续运行
使用sigaction函数设置信号的属性
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
功能:获取或者设置一个信号的行为
参数:
signum:信号,除了SIGKILL和SIGSTOP
act:新的行为,通过这个参数设置新的行为
struct sigaction {
void (*sa_handler)(int); 信号处理函数
SIG_DFL 默认
SIG_IGN 忽略
自己定义的信号处理函数 捕捉
void (*sa_sigaction)(int, siginfo_t *, void *); 信号处理函数
sigset_t sa_mask; 关于阻塞的掩码
int sa_flags; 标志位
SA_RESTART 自重启属性
void (*sa_restorer)(void); 没有用
};
oldact:之前的行为,通过这个参数获取之前的行为
返回值:
成功:0
失败:-1
4.2.1 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
void SignalHandler(int sig)
{
}
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
ssize_t bytes;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//使用setsockopt函数设置端口复用
int on = 1;
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
{
ERRLOG("setsockopt error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用alarm闹钟实现网络超时检测
//当使用alarm闹钟函数设置一定的时候后,代码继续向下运行,当时间达到,会产生SIGALRM信号,
//如果这个信号按照默认的方式处理,就会让整个进程结束
//如果这个信号按照捕捉的方式处理,当信号产生之后,会立即执行信号处理函数,当信号处理函数
//执行完毕之后,会继续回到之前的代码运行,将这种信号的属性称之为自重启属性
//如果要实现超时检测,需要关闭这个自重启属性,如果关闭了自重启属性,当执行完信号处理函数
//之后,代码不会继续执行之前的代码,而是错误返回,然后继续运行
//使用sigaction函数设置信号的行为
//信号的自重启属性是信号的一个标志位,所以要执行读改写三步
//第一步:读,获取之前的标志位
struct sigaction act;
if(sigaction(SIGALRM, NULL, &act) == -1)
{
ERRLOG("sigaction error");
}
//第二步:改,修改属性
act.sa_handler = SignalHandler;
act.sa_flags = act.sa_flags & (~SA_RESTART);
//第三步:设置新的属性
if(sigaction(SIGALRM, &act, NULL) == -1)
{
ERRLOG("sigaction error");
}
NEXT:
alarm(5);
//第五步:阻塞等待客户端的连接
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
//printf("errno = %d\n", errno);
if(errno == 4)
{
printf("连接超时...\n");
goto NEXT;
}
else
{
ERRLOG("accept error");
}
}
//打印客户端的信息
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
//进行通信
while(1)
{
alarm(5);
if((bytes = recv(acceptfd, buf, N, 0)) == -1)
{
if(errno == 4)
{
printf("客户端存在挂机行为,强制断开...\n");
close(acceptfd);
goto NEXT;
}
else
{
ERRLOG("recv error");
}
}
else if(bytes == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
goto NEXT;
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
goto NEXT;
}
printf("%s-%d:%s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), buf);
strcat(buf, "^_^");
//如果发送放关闭,当send执行第二次的时候会结束当前进程
//是因为第二次执行send的时候产生了SIGPIPE信号,这个信号默认对当前进程
//的处理方式是结束进程
if(send(acceptfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
}
return 0;
}
4.2.2 客户端
//TCP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("connect error");
}
//进行通信
char buf[N] = {0};
ssize_t bytes;
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(send(sockfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端退出了\n");
exit(0);
}
memset(buf, 0, N);
if((bytes = recv(sockfd, buf, N, 0)) == -1)
{
ERRLOG("recv error");
}
else if(bytes == 0)
{
printf("长时间不说话,那就结束吧\n");
exit(0);
}
printf("服务器:%s\n", buf);
}
return 0;
}