SocketSelect
除了使用了多进程与多线程实现server对client的聊天程序,还可使用IO多路复用技术select实现多客户端连接服务器
将Socket多线程实现的聊天程序改为使用IO多路复用技术中的select解决:
select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:当前进程所打开文件描述符中最大的+1
- readfds:监测可读状态的文件描述符集合
- writefds:监测可写状态的文件描述符集合
- exceptfds:监测其他状态的文件描述符集合
- timeout:文件描述符监测的最大时间
- return:成功则返回三个文件描述符集合中的总数量,如果timeout expires则直接返回0,如果出错则返回-1并设置相应的errno
sourcecode
//server.c
#include "head.h"
#include "common.h"
#define MAXUSER 100
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[]) {
//./a.out -p port
//1.命令行解析
if (argc != 3) {
fprintf(stderr, "Usage : %s -p port", argv[0]);
exit(1);
}
int opt;
int port;
while ((opt = getopt(argc, argv, "p:")) != -1) {
switch (opt) {
case 'p':
port = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -p port\n", argv[0]);
exit(1);
}
}
//2.创建socket与select参数准备
int maxfd = 0;//nfds
int server_listen;//创建监听套接字文件描述符
int fd[MAXUSER + 5] = {0};//文件描述符数组
if ((server_listen = socketCreate(port)) < 0) handle_error("socketCreate");
fd_set rfds;
FD_SET(server_listen, &rfds);//将server_listen添加到rfds文件描述符集合中
maxfd = server_listen;//更新最大的文件描述符为server_listen
fd[server_listen] = server_listen;//将server_listen文件描述符也添加到fd数组中
//accept循环的接受客户端对server的连接
while (1) {
//3.将rfds集合清空并将fd数组中的所有文件描述符添加到rfds集合中
FD_ZERO(&rfds);//文件描述符集合清空
for (int i = 0; i < MAXUSER; ++i) {//如果文件描述符大于0则加入到rfds文件描述符集合中
if (fd[i] > 0) FD_SET(fd[i], &rfds);
}
//4.调用select监测文件描述符的动向
int ret = select(maxfd + 1, &rfds, NULL, NULL, NULL);
if (ret < 0) handle_error("select");
int newfd;
struct sockaddr_in client;
socklen_t len = sizeof(client);
//5.利用server_listen文件描述符进行accept操作
if (FD_ISSET(server_listen, &rfds)) {//判断server_listen是否在rfds文件描述符集合中
if ((newfd = accept(server_listen, (struct sockaddr *)&client, &len)) < 0) handle_error("accept");
if (newfd >= MAXUSER) {//太多客户端创建的文件描述符!服务器主动关闭某些!
close(newfd);
printf("too many users try to connect!\n");
break;
}
if (newfd > maxfd) maxfd = newfd;//在文件描述符添加操作结束后,尝试更新参数maxfd
fd[newfd] = newfd;
printf("<accept> %s:%d: accept a client!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
}
//6.如果在rfds集合中有已经准备好的可读的文件描述符 则进行recv并读取打印信息, 如果是监听状态则继续for
for (int i = 0; i < MAXUSER; ++i) {
if (fd[i] == server_listen) continue;
if (fd[i] > 0 && FD_ISSET(fd[i], &rfds)) {
char buff[1024] = {0};
ssize_t rsize = recv(fd[i], buff, sizeof(buff), 0);
if (rsize <= 0) {//客户端断开连接进行四次挥手
close(fd[i]);
fd[i] = 0;
printf("<server> : %s:%d has left!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
} else {//将读入的内容进行输出
//send(fd[i], buff, strlen(buff)/*rsize*/, 0);将收到的数据发回客户端
printf("<receive> %s:%d: %s\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buff);
break;
}
}
}
}
return 0;
}
//client.c
#include "head.h"
#include "common.h"
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int sockfd;
//ctrl+c信号处理
void closeSock(int signum) {
send(sockfd, "I am leaving...", 27, 0);
close(sockfd);//关闭客户端文件描述符
exit(0);
}
int main(int argc, char *argv[]) {
//./a.out ip port
if (argc != 3) {
fprintf(stderr, "Usage : %s ip port\n", argv[0]);
exit(1);
}
signal(SIGINT, closeSock);
//1.建立连接connect
if ((sockfd = socketConnect(argv[1], atoi(argv[2]))) < 0) handle_error("socketConnect");
printf("connect sccuess!\n");
//2.send发送数据
while (1) {//循环发送消息
char buff[1024] = {0};
scanf("%[^\n]s", buff);//输入可含空格的字符串
getchar();//吞掉一个换行
//只要向文件描述符中写入 tcp服务就会帮助发送消息
//With a zero flags argument, send is equivalent to write(2).
if (strlen(buff)) send(sockfd, buff, sizeof(buff), 0);
}
return 0;
}
//common.h
#ifndef _COMMON_H
#define _COMMON_H
int socketCreate(int port);
int socketConnect(const char *ip, int port);
#endif
//common.c
#include "head.h"
int socketCreate(int port) {
//1.创建套接字
int sockfd;
struct sockaddr_in addr;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);//主机字节序转换为网络字节序
addr.sin_addr.s_addr = inet_addr("0.0.0.0");//将网络字节序转化为主机字节序 0.0.0.0表示不关注消息传来的地址
//2.bind绑定套接字与结构体信息 listen
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) return -1;
if (listen(sockfd, 20) < 0) return -1;
return sockfd;
}
int socketConnect(const char *ip, int port) {
int sockfd;
//1.客户端打开一个socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1;
//2.定义结构体用于绑定端口号、ip地址(存放服务端的具体信息)
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);//端口号
server.sin_addr.s_addr = inet_addr(ip);//ip地址
//3.建立连接connection
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0) return -1;
return sockfd;
}
最终结果:
bug
程序中遗留的一些bug:
- 当server服务端先于client客户端退出时,客户端无法收到服务端退出的通知。
- 如果客户端client建立连接却又什么都不做时,占用服务端的文件描述符浪费资源(在select中设置timeout)。
- 在客户端close之后,maxfd需要发生变化。