【Linux系统与网络编程】14:SocketSelect

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:

  1. 当server服务端先于client客户端退出时,客户端无法收到服务端退出的通知。
  2. 如果客户端client建立连接却又什么都不做时,占用服务端的文件描述符浪费资源(在select中设置timeout)。
  3. 在客户端close之后,maxfd需要发生变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值