使用select实现IO多路转接
相关知识
TCP的状态转换
TCP的状态转换:在进行通信的时候进程所处的状态会随着通信的状态发生改变
在一个正常通信的模型中,服务器和客户端的状态变化如下L
客户端:CLOSED->(第一次握手后)SYN_SENT->(第二次握手)ESTABLISTHED->(第一次挥手)FIN_WAIT_2->(第二次挥手)FIN_WAIT_2->(第三四次挥手)TIME_WAIT->(延时2msl)CLOSED
服务器:CLOSED->(被动接受连接请求)LISTEN->(收到SYN,发送SYN,ACK)SYN_RCVD->(收到ACK)ESTABLISTHED->(收到FIN,发送ACK)CLOSED_WAIT->(关闭发送FIN)LAST_ACK->(收到ACK)->CLOSED
当发送和接受端的状态都变为ESTABLISTHED的时候两端才能够正常通信,在通信过程中,两端的状态不会发生改变,在挥手的过程中,谁先关闭连接,谁的状态先发生改变
一个MSL的时长大概在30s
半关闭状态和实现函数:
半关闭状态:
A给B发送是FIN(A调用了close函数), 但是B没有给A发送FIN(B没有调用close)
A断开了与B的连接, B没有断开与A的连接
特点:A不能给B发送数据, A可以收B发送的数据
B可以给A发送数据
实现函数:int shutdown(int sockfd, int how);
sockfd: 要半关闭的一方对应的文件描述符
通信的文件描述符
how:所要关闭的功能
SHUT_RD - 0 - 读
SHUT_WR - 1 - 写
SHUT_RDWR - 2 - 读写
查看网络相关状态信息
命令: netstat
参数:
-a (all)显示所有选项,默认不显示LISTEN相关
-p 显示建立相关链接的程序名
-n 拒绝显示别名,能显示数字的全部转化成数字。
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-l 仅列出有在 Listen (监听) 的服务状态
端口占用问题:在客户端断开连接后再次进行连接时候会失败,原因是该端口在连接断开后需要等待2MSL才进行释放,为了解决该问题,需要进行端口重用
常用的用途:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
设置方法
int opt = 1;
setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR,(const void *)&opt, sizeof(opt));
I/O的多路转接
当一台服务器需要处理多个客户端的连接请求,为了优化效率提高速度,将一些连接和通信的请求处理交给内核去完成。
未使用I/O多路转接下的处理方法
1.多进程线程
主进程线程用于监听连接请求,处于阻塞状态,当接收到请求后创建新的进程或者线程处理连接
2非阻塞,忙轮寻
在已经建立连接的客户端中,每隔一定时间向客户端发送信号,确认客户端下一次将要传输数据的时间,根据时间来安排处理顺序
优点: 提高了程序的执行效率
缺点: 需要占用更多的cpu和系统资源
使用IO多路转接技术 select/poll/epoll
1.select/poll
在服务器程序中,委托内核调用select()函数检测已建立连接的客户端中,哪些将要与自身进行通信,选择这些将要通信的进行处理
select/poll的缺点:
select/poll只能计算出将要通信的个数,但具体是哪个客户端需要自己去遍历
2.epoll
实现的思路和select/poll大致类似,但能够显示通信请求的具体信息
I/O多路转接的大致实现思路
1.先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该列表中
2.调用一个函数,监听该表中的文件描述符,知道这些描述符表中的一个进行I/O操作时,该函数才返回
–该函数为阻塞函数
–函数对文件件描述符的检测操作是由内核完成的
3.在返回时,他告诉进程有多少描述符要进行I/O操作
select()函数原型
int select(int nfds,fd_setwritefds,fd_setexceptfds,struct timeval*timeout);
函数参数:
nfds: 要检测的文件描述中最大的fd+1 (最大值为1024)
readfds: 读集合
writefds: 写集合
exceptfds: 异常集合
timeout:
NULL: 永久阻塞
当检测到fd变化的时候返回
struct timeval a;
a.tv_sec = 10;
a.tv_usec = 0;
返回值
文件描述符集类型: fd_set rdset;
文件描述符操作函数:
全部清空
void FD_ZERO(fd_set *set);
从集合中删除某一项
void FD_CLR(int fd, fd_set *set);
将某个文件描述符添加到集合
void FD_SET(int fd, fd_set *set);
判断某个文件描述符是否在集合中
int FD_ISSET(int fd, fd_set *set);
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 最大的文件描述符
int maxfd = lfd;
// 文件描述符读集合
fd_set reads, temp;
// init
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
// 委托内核做IO检测
temp = reads;
int ret = select(maxfd+1, &temp, NULL, NULL, NULL);
if(ret == -1)
{
perror("select error");
exit(1);
}
// 客户端发起了新的连接
if(FD_ISSET(lfd, &temp))
{
// 接受连接请求 - accept不阻塞
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
char ip[64];
printf("new client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
// 将cfd加入到待检测的读集合中 - 下一次就可以检测到了
FD_SET(cfd, &reads);
// 更新最大的文件描述符
maxfd = maxfd < cfd ? cfd : maxfd;
}
// 已经连接的客户端有数据到达
for(int i=lfd+1; i<=maxfd; ++i)
{
if(FD_ISSET(i, &temp))
{
char buf[1024] = {0};
int len = recv(i, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("客户端已经断开了连接\n");
close(i);
// 从读集合中删除
FD_CLR(i, &reads);
}
else
{
printf("recv buf: %s\n", buf);
send(i, buf, strlen(buf)+1, 0);
}
}
}
}
close(lfd);
return 0;
}
IO多路转接poll实现:
函数原型:
int poll(struct pollfd*fd,nfds_t nfds,int timeout);
函数参数:
pollfd --数组地址
nfds --数组的最大长度,数组中最后一个使用的元素下标加一)
timeout:等待模式
-1:永久阻塞
0:调用完成立即返回
大于0:等待的时长 毫秒
返回值:io发送辩护的文件描述符的个数
pollfd结构体:
struct pollfd
{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
}