IO多路复用实现TCP客户端与并发服务器
IO多路复用 :select函数
c
功能:
阻塞函数,让内核监测集合中是否有文件描述符准备就绪,若准备就绪则解除阻塞;
当函数解除阻塞后,集合中会只剩下产生事件的文件描述符;例如:
0号准备就绪,则集合中只剩下0号
sfd准备就绪,则集合中只能下sfd;
0和sfd均准备就绪,则0和sfd均存在
若不将数据从触发事件的文件描述符对应的空间中取出,此时该文件描述符一直处于就绪状态。
原型:
#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);
参数:
int nfds:需要填充三个集合中最大的文件描述符编号+1;
fd_set *readfds, fd_set *writefds,
fd_set *exceptfds:读集合,写,其他集合,
若集合不使用,填NULL; 一般只用读集合;
struct timeval *timeout:设置超时时间;
1. 若不想设置超时时间,填NULL,则当前函数会一直阻塞,直到集合中有文件描述符准备就绪。
2. 设置超时时间; 若时间到后依然没有事件产生,则该函数解除阻塞,且返回失败情况。
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:
>0, 成功返回成功触发事件的文件描述符个数;
=0, 超时了;
失败,返回-1,更新errno;
操作集合的函数 select 函数中集合的函数
c
void FD_CLR(int fd, fd_set *set); //将fd从集合中删除
int FD_ISSET(int fd, fd_set *set); //判断fd是否在集合中
void FD_SET(int fd, fd_set *set); //将fd添加到结合中
void FD_ZERO(fd_set *set); //清空集合
TCP并发服务器 code
c
#include <my_head.h>
#define PORT 8888 // 端口号的网络字节序,1024~49151
#define IP "192.168.101.63" // 本机IP,ifconfig
int keyboardEvents(void);
int client_connect(int sfd, struct sockaddr_in *pCin, fd_set *preadfds, int *pmaxfd);
int deal_client_message(int newfd, struct sockaddr_in pCin[], fd_set *preadfds, int *pmaxfd);
int main(int argc, const char *argv[])
{
// 创建流式套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("socket create success sfd=%d\n", sfd);
// 允许端口快速复用
int reuse = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
{
ERR_MSG("setsockopt");
return -1;
}
printf("允许端口快速复用成功\n");
// 填充地址信息结构体给bind函数绑定使用;
// 真实的地址信息结构体根据地址族指定,AF_INET:man 7 ip
struct sockaddr_in sin;
sin.sin_family = AF_INET; // 必须填AF_INET;
sin.sin_port = htons(PORT); // 端口号的网络字节序,1024~49151
sin.sin_addr.s_addr = inet_addr(IP); // 本机IP的网络字节序,ifconfig
// 绑定服务器的地址信息---》必须绑定
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
ERR_MSG("bind");
return -1;
}
printf("bind success\n");
// 将套接字转换成被动监听状态
if (listen(sfd, 128) < 0)
{
ERR_MSG("listen");
return -1;
}
printf("listen success\n");
// 创建一个读集合
fd_set readfds, tempfds;
// 由于fd_set是一个结构体,里面有一个整形数组,若不初始化
// 则可能随机到合法的但是不需要监测的文件描述符
// 所以必须初始化
FD_ZERO(&readfds); // 清空集合
// 将需要的文件描述符添加到集合中
FD_SET(0, &readfds);
FD_SET(sfd, &readfds);
int maxfd = sfd; // 存储集合中最大的文件描述符
struct sockaddr_in saveCin[1024 - 3]; // 将客户端的信息另存
char buf[128] = "";
ssize_t res = 0;
int newfd = -1;
int s_res = 0;
while (1)
{
tempfds = readfds;
// 调用IO多路复用函数,阻塞监测集合
s_res = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
if (s_res < 0)
{
ERR_MSG("select");
return -1;
}
else if (0 == s_res)
{
printf("time out...\n");
break;
}
printf("__%d__\n", __LINE__);
// 能运行到当前位置,则代表集合中有文件描述符准备就绪
// 判断哪个文件描述符准备就绪,走对应处理函数即可
// 当函数解除阻塞后,集合中会只剩下产生事件的文件描述符;例如:
// 0号准备就绪,则集合中只剩下0号
// sfd准备就绪,则集合中只能下sfd;
// 0和sfd均准备就绪,则0和sfd均存在
// 所以只需要遍历集合中剩下哪个文件描述符,就代表哪个文件描述符准备就绪
for (int i = 0; i <= maxfd; i++)
{
if (FD_ISSET(i, &tempfds) == 0) // 若i所代表的文件描述符不存在,则遍历下一个文件描述符
continue;
// 能运行到当前位置,说明i所代表的文件描述符在集合中。
// 判断i所代表的文件描述符是什么(0, sfd , 其他newfd),走对应处理函数即可
if (0 == i) // 说明0在集合中
{
printf("触发键盘输入事件\n");
keyboardEvents();
}
else if (sfd == i) // 说明sfd在集合中
{
printf("触发客户端连接事件\n");
client_connect(sfd, saveCin, &readfds, &maxfd);
}
else
{
printf("触发客户端交互事件\n");
deal_client_message(i, saveCin, &readfds, &maxfd);
}
}
}
// 关闭套接字
close(sfd);
return 0;
}
int deal_client_message(int newfd, struct sockaddr_in pCin[], fd_set *preadfds, int *pmaxfd)
{
ssize_t res = -1;
char buf[128] = "";
// 接收数据
res = recv(newfd, buf, sizeof(buf), 0);
if (res < 0)
{
ERR_MSG("recv");
return -1;
}
else if (0 == res)
{
printf("[%s:%d] newfd=%d 客户端下线__%d__\n",
inet_ntoa(pCin[newfd - 3].sin_addr), ntohs(pCin[newfd - 3].sin_port), newfd, __LINE__);
close(newfd); // 关闭文件描述符
FD_CLR(newfd, preadfds); // 剔除该文件描述符
// 更新maxfd, 从当前最大的文件描述符开始判断是否在集合中
// 若在,则该文件描述符就是最大文件描述符
for (; *pmaxfd >= 0; *pmaxfd--)
if (FD_ISSET(*pmaxfd, preadfds))
break;
// while(!FD_ISSET(*pmaxfd, preadfds) && *pmaxfd-->=0);
return 0;
}
printf("[%s:%d] newfd=%d : %s __%d__\n",
inet_ntoa(pCin[newfd - 3].sin_addr), ntohs(pCin[newfd - 3].sin_port), newfd, buf, __LINE__);
// 发送数据
strcat(buf, "*_*");
if (send(newfd, buf, sizeof(buf), 0) < 0)
{
ERR_MSG("send");
return -1;
}
printf("send success\n");
return 0;
}
int client_connect(int sfd, struct sockaddr_in *pCin, fd_set *preadfds, int *pmaxfd)
{
struct sockaddr_in cin; // 存储客户端的地址信息
socklen_t addrlen = sizeof(cin);
int newfd;
newfd = accept(sfd, (struct sockaddr *)&cin, &addrlen);
if (newfd < 0)
{
ERR_MSG("accept");
return -1;
}
printf("[%s:%d] newfd=%d 客户端连接成功__%d__\n",
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, __LINE__);
pCin[newfd - 3] = cin; // 将客户端信息另存
FD_SET(newfd, preadfds); // 将newfd添加到集合中
*pmaxfd = *pmaxfd > newfd ? *pmaxfd : newfd; // 更新maxfd
return 0;
}
int keyboardEvents(void)
{
char buf[128] = "";
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = 0;
printf(":%s\n", buf);
return 0;
}
TCP 客户端 code
c
#include <my_head.h>
#define SERVER_IP "192.168.101.63"
#define SERVER_PORT 8888
int main(int argc, const char *argv[])
{
// 创建流式套接字
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd < 0)
{
ERR_MSG("socket");
return -1;
}
// 可以不用绑定
// 连接connect
// 填充信息
struct sockaddr_in server_info;
server_info.sin_addr.s_addr = inet_addr(SERVER_IP);
server_info.sin_family = AF_INET;
server_info.sin_port = htons(SERVER_PORT);
socklen_t server_len = sizeof(server_info);
if (connect(client_fd, (struct sockaddr *)&server_info, server_len) < 0)
{
ERR_MSG("connect");
return -1;
}
// 创建一个读集合
fd_set read_sets, temp_sets;
// 将读集合清空
FD_ZERO(&read_sets);
// 将需要用到的文件描述符添加到读集合中
FD_SET(0, &read_sets);
FD_SET(client_fd, &read_sets);
// 设一个最大文件描述符变量
int max_fd = client_fd;
int res_select = -1;
char buff[128];
while (1)
{
// 使用工具人
temp_sets = read_sets;
// 调用IO多路复用函数
res_select = select(max_fd + 1, &temp_sets, NULL, NULL, NULL);
if (res_select < 0)
{
ERR_MSG("select");
return -1;
}
else if (0 == res_select)
{
printf("超时中。。。\n");
break;
}
// printf("__%d__\n", __LINE__);
for (int i = 0; i <= max_fd; i++)
{
if (!FD_ISSET(i, &temp_sets))
continue;
if (0 == i)
{
printf("触发读事件\n");
bzero(buff, sizeof(buff));
fgets(buff, sizeof(buff), stdin);
buff[strlen(buff) - 1] = 0;
printf("输入事件完成\n");
// 输入完成后,需要对服务器进行发送消息
send(client_fd, buff, sizeof(buff), 0);
}
}
}
return 0;
}