C++网络聊天室

单线程模式

客户端基于poll监听标准输入和socket, 服务端基于poll监听socket请求和socket上传来的信息。

Client

#include<poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<arpa/inet.h>
const int BUF_SIZE = 1024;

void set_pfd(pollfd& pfd, int fd, int event, int revent) {
	pfd.fd = fd;
	pfd.events = event;
	pfd.revents = revent;
}

int main() {
	int port;
	char ip[40];
	printf("Input port num and ip address !\n");
	scanf("%d %s", &port, ip);
	sockaddr_in server_address;
	bzero(&server_address, sizeof(server_address));
	server_address.sin_family = AF_INET;
	server_address.sin_port = htons(port);
	inet_pton(AF_INET, ip, &server_address.sin_addr);

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	assert(sockfd >= 0);

	int re = connect(sockfd, (sockaddr*)&server_address, sizeof(server_address));
	if (re < 0) {
		printf("Connect failure\n");
		close(sockfd);
		return -1;
	}

	pollfd fds[2];
	set_pfd(fds[0], 0, POLLIN, 0);
	set_pfd(fds[1], sockfd, POLLIN | POLLRDHUP, 0);

	int pipefd[2];
	re = pipe(pipefd);
	assert(re != -1);

	char* read_buf[BUF_SIZE];

	printf("Begin !\n");
	while (true) {
		re = poll(fds, 2, -1);
		if (re < 0) {
			printf("poll failure\n");
			break;
		}

		if (fds[1].revents & POLLIN) {
			memset(read_buf, '\0', BUF_SIZE);
			recv(sockfd, read_buf, BUF_SIZE - 1, 0);
			printf("recv msg : %s\n", read_buf);
		}
		else if (fds[1].revents & POLLRDHUP) {
			printf("Server close the connection\n");
			break;
		}

		if (fds[0].revents & POLLIN) {
			// 使用零拷贝方法,将标准输入的信息输入到socket上发送到服务端
			splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
			splice(pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
		}
	}

	close(sockfd);
	return 0;
}

Server

#include<poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
const int BUF_SIZE = 1024;
const int MAX_USER_NUM = 10;
const int MAX_FDS = 65535;
// 现有游客数计数器
int user_count = 0;
struct user
{
	sockaddr_in addr;
	char* write;
	char read[BUF_SIZE];
};

void set_pfd(pollfd& pfd, int fd, int event, int revent);
int setnonblocking(int fd);
int main(int argc, char **argv) {
	if (argc <= 1) {
		printf("请输入端口号!\n");
		return -1;
	}
	int port = atoi(argv[1]);

	sockaddr_in address;
	bzero(&address, sizeof(address));
	address.sin_family = AF_INET;
	address.sin_port = htons(port);
	address.sin_addr.s_addr = htonl(INADDR_ANY);

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	assert(sockfd >= 0);
	int ret = bind(sockfd, (sockaddr*)&address, sizeof(address));
	assert(ret != -1);

	ret = listen(sockfd, MAX_USER_NUM);
	assert(ret != -1);

	user* users = new user[MAX_FDS];
	pollfd fds[MAX_USER_NUM + 1];
	// 初始化pollfd文件描述符
	set_pfd(fds[0], sockfd, POLLIN | POLLERR, 0);
	for (int i = 1; i <= MAX_USER_NUM; ++i) {
		set_pfd(fds[i], -1, 0, 0);
	}

	while (true) {
		// 阻塞模式
		ret = poll(fds, MAX_USER_NUM + 1, -1);
		if (ret < 0) {
			printf("poll failure\n");
			break;
		}
		for (int i = 0; i <= MAX_USER_NUM; ++i) {
			
			if (fds[i].fd == sockfd && (fds[i].revents & POLLIN)) {
				// 新的连接建立请求
				sockaddr_in client_addr;
				socklen_t client_addr_len = sizeof(client_addr);
				int connfd = accept(sockfd, (sockaddr*)&client_addr, &client_addr_len);
				if (connfd < 0) {
					printf("Accept failure, errno is %d", errno);
					continue;
				}
				// 超过最大服务用户数
				if (user_count > MAX_USER_NUM) {
					const char* info = "max connect num\n";
					printf(info);
					send(connfd, info, strlen(info), 0);
					close(connfd);
					continue;
				}
				// 可以建立新的服务
				user_count++;
				users[connfd].addr = client_addr;
				setnonblocking(connfd);
				// 设置新连接的用户的connfd文件描述符,监听输入、错误、断开连接事件。
				set_pfd(fds[user_count], connfd, POLLIN | POLLERR | POLLRDHUP, 0);
				printf("新的连接到达,connfd号是%d, 现有%d个用户\n", connfd, user_count);
			}
			else if (fds[i].revents & POLLIN) {
				int connfd = fds[i].fd;
				// 读取信息到相应connfd的read buffer区
				memset(users[connfd].read, '\0', BUF_SIZE);
				ret = recv(connfd, users[connfd].read, BUF_SIZE - 1, 0);
				printf("Get %d bytes info : %s from %d\n", ret, users[connfd].read, connfd);
				if (ret < 0) {
					// 读取操作出错,关闭连接
					// 关闭方法:将最后一个用户信息置换到当前位置,使用户数和指针i减一
					if (errno != EAGAIN) {
						users[connfd] = users[fds[user_count].fd];
						fds[i] = fds[user_count];
						i--;
						user_count--;
						close(connfd);
					}
				}
				else if (ret == 0) {
					// 空信息,不必转发
				}
				else {
					// 接收到了数据,通过其他socket准备写数据
					for (int j = 1; j <= user_count; ++j) {
						if (fds[j].fd == connfd) {
							// 不必给自己转发
							continue;
						}
						fds[j].events |= ~POLLIN;
						fds[j].events |= POLLOUT;
						users[fds[j].fd].write = users[connfd].read;
					}
				}
			}
			else if (fds[i].revents & POLLOUT) {
				int connfd = fds[i].fd;
				if (!users[connfd].write) {
					continue;
				}
				ret = send(connfd, users[connfd].write, strlen(users[connfd].write), 0);
				users[connfd].write = NULL;
				// 重新注册事件
				fds[i].events |= ~POLLOUT;
				fds[i].events |= POLLIN;
			}
			else if (fds[i].revents & POLLRDHUP) {
				// close 
				int connfd = fds[i].fd;
				close(fds[i].fd);
				users[connfd] = users[fds[user_count].fd];
				fds[i] = fds[user_count];
				i--, user_count--;
				printf("%d left\n", connfd);
			}
			else if (fds[i].revents & POLLERR) {
				printf("Get an error from %d", fds[i].fd);
				char errors[BUF_SIZE];
				memset(errors, '\0', sizeof(errors));
				socklen_t len = sizeof(errors);
				if (getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, errors, &len) < 0) {
					printf("Get socket option failure\n");
				}
				continue;
			}
		}
	}
	delete[] users;
	close(sockfd);
	return 0;
}

void set_pfd(pollfd& pfd, int fd, int event, int revent) {
	pfd.fd = fd;
	pfd.events = event;
	pfd.revents = revent;
}

int setnonblocking(int fd) {
	int oldopt = fcntl(fd, F_GETFL);
	int newopt = oldopt | O_NONBLOCK;
	fcntl(fd, F_SETFL, newopt);
	return oldopt;
}

多进程版服务器

#include<sys/epoll.h>
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/shm.h>
#include<signal.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<sys/wait.h>
/*
用到了socketpair:
1. 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
2. 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
3. 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述副sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。
*/
const int USER_LIMIT = 5;
const int BUFFER_SIZE = 1024;
const int FD_LIMIT = 65535;
const int MAX_EVENT_NUM = 1024;
const int PROCESS_LIMIT = 65536;

struct client_data {
	int fd; // socket文件描述符
	pid_t pid; // 处理这个连接的子进程pid
	int pipefd[2]; // 和父进程通信的管道
	sockaddr_in addr; // socket地址
};

static const char* shm_name = "/my_shm";
int sig_pipefd[2];
int epollfd;
int sockfd;
int shmfd;
char* share_mem = NULL;
// 客户连接数组,进程用客户连接的编号(connfd)来索引这个数组
client_data* users = NULL;
// 子进程和客户连接的映射关系,用进程的PID来索引这个数组,可获得该进程所处理的客户连接编号
int* sub_process = NULL;
// 当前服务的客户数目
int user_cnt = 0;

bool stop_child = false;

int setnonblocking(int fd);
int addfd(int epoll, int fd);
void sig_handler(int sig);
int addsig(int sig, void(*handler)(int), bool restart = true);
int del_resource();
void term_child(int sig);
int run_child(int idx, client_data* users, char* share_mem);

int main(int argv, char** argc) {
	if (argv <= 1) {
		printf("Should input port\n");
	}
	int port = atoi(argc[1]);
	sockaddr_in address;
	bzero(&address, sizeof(address));
	address.sin_family = AF_INET;
	address.sin_port = htons(port);
	address.sin_addr.s_addr = htonl(INADDR_ANY);
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	assert(sockfd != -1);

	// int port = 19983;
	// sockaddr_in address;
	// bzero(&address, sizeof(address));
	// address.sin_family = AF_INET;
	// address.sin_port = htons(port);
	// address.sin_addr.s_addr = htonl(INADDR_ANY);
	// sockfd = socket(AF_INET, SOCK_STREAM, 0);
	// assert(sockfd != -1);


	int ret = bind(sockfd, (sockaddr*)&address, sizeof(address));
	assert(ret != -1);

	ret = listen(sockfd, 5);
	assert(ret != -1);

	user_cnt = 0;
	users = new client_data[USER_LIMIT + 1];
	sub_process = new int[PROCESS_LIMIT];
	for (int i = 0; i < PROCESS_LIMIT; i++) {
		sub_process[i] = -1;
	}
	epoll_event events[MAX_EVENT_NUM];

	epollfd = epoll_create(5);
	assert(epollfd != -1);
	// 监听sockfd,检测是否有新的连接请求建立
	addfd(epollfd, sockfd);

	ret = socketpair(AF_UNIX, SOCK_STREAM, 0, sig_pipefd);
	assert(ret != -1);
	setnonblocking(sig_pipefd[1]);
	// 注册有信号就绪事件
	addfd(epollfd, sig_pipefd[0]);

	// 注册信号以及响应函数
	addsig(SIGCHLD, sig_handler, true);
	addsig(SIGTERM, sig_handler, true);
	addsig(SIGINT, sig_handler, true);
	addsig(SIGPIPE, SIG_IGN, true);
	bool stop_server = false;
	bool terminate = false;

	// 创建共享内存,作为所有客户socket的读缓存
	// O_CREAT : 如果对象不存在(shm_name),则创建一个
	shmfd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
	assert(shmfd != -1);
	// ftruncate: 将指定的文件修改为指定的大小
	ret = ftruncate(shmfd, USER_LIMIT * BUFFER_SIZE);
	assert(ret != -1);
	share_mem = (char*)mmap(NULL, USER_LIMIT * BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shmfd, 0);
	assert(share_mem != MAP_FAILED);

	while (!stop_server) {
		int num = epoll_wait(epollfd, events, MAX_EVENT_NUM, -1);
		if (num < 0 && errno != EAGAIN) {
			printf("EPOLL FAILURE\n");
			break;
		}

		for (int i = 0; i < num; i++) {
			int eventfd = events[i].data.fd;
			if (eventfd == sockfd) {
				// 有新的连接请求
				sockaddr_in client_addr;
				socklen_t client_len = sizeof(client_addr);
				int connfd = accept(sockfd, (sockaddr*)&client_addr, &client_len);
				if (connfd < 0) {
					printf("accept errno %d", errno);
					continue;
				}
				if (user_cnt >= USER_LIMIT) {
					// 超过用户上限
					const char* info = "Too many connections\n";
					printf(info);
					send(connfd, info, sizeof(info), 0);
					close(connfd);
				}
				users[user_cnt].addr = client_addr;
				users[user_cnt].fd = connfd;
				// 在主进程和子进程间创建管道
				ret = socketpair(AF_UNIX, SOCK_STREAM, 0, users[user_cnt].pipefd);
				assert(ret != -1);
				pid_t pid = fork();
				if (pid < 0) {
					close(connfd);
					continue;
				}
				else if (pid == 0) {
					// 在子进程中,关闭子进程继承父进程的fd
					close(epollfd);
					close(sockfd);
					// 关闭子进程管道的读端
					close(users[user_cnt].pipefd[0]);
					close(sig_pipefd[0]);
					close(sig_pipefd[1]);
					run_child(user_cnt, users, share_mem);
					// 结束服务了,释放对mmap
					munmap((void*)share_mem, USER_LIMIT * BUFFER_SIZE);
					exit(0);
				}
				else {
					close(users[user_cnt].pipefd[1]);
					close(connfd);
					// 注册读就绪事件
					addfd(epollfd, users[user_cnt].pipefd[0]);
					users[user_cnt].pid = pid;
					sub_process[pid % PROCESS_LIMIT] = user_cnt;
					user_cnt++;
				}
			}
			else if (eventfd == sig_pipefd[0] && events[i].events & EPOLLIN) {
				// 有信号
				int sig;
				char signals[1024];
				ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
				if (ret == -1 || ret == 0) {
					continue;
				}
				else {
					for (char c : signals) {
						switch (c)
						{
							// 表示有子进程退出,某个客户端关闭了连接
						case SIGCHLD:
							pid_t pid;
							int stat;
							// 非阻塞模式
							while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
								// 获得用户的编号
								int del_user = sub_process[pid % PROCESS_LIMIT];
								sub_process[pid % PROCESS_LIMIT] = -1;
								if (user_cnt < 0 || del_user > USER_LIMIT) {
									continue;
								}
								// 释放资源
								epoll_ctl(epollfd, EPOLL_CTL_DEL, users[del_user].pipefd[0], 0);
								close(users[del_user].pipefd[0]);
								user_cnt--;
								// 将最后一个用户交换到被删除用户的位置,达到删除目的
								users[del_user] = users[user_cnt];
								sub_process[users[del_user].pid % PROCESS_LIMIT] = del_user;
							}
							if (terminate && user_cnt == 0) {
								stop_server = true;
								break;
							}
							break;
						case SIGTERM:
						case SIGINT:
							printf("Stop the Server\n");
							if (user_cnt == 0) {
								stop_server = true;
								break;
							}
							for (int j = 0; j < user_cnt; j++) {
								// 发送给子进程退出信号,子进程退出后会发送SIGCHLD信号
								// 然后在case SIGCHLD 中会释放子进程的资源,所以不必在这里释放
								int childfd = users[j].fd;
								kill(childfd, SIGTERM);
							}
							terminate = true;
							break;
						default:
							break;
						}
					}
				}
			}
			else if (events[i].events & EPOLLIN) {
				// 子进程对父进程写入数据(对应管道读就绪)
				int child = 0;
				// 这里eventfd是events[i].data.fd, 对应的是users[事件对应用户].pipefd[0]
				// 即读端口
				printf("Read data from child process\n");
				ret = recv(eventfd, (char*)&child, sizeof(child), 0);
				if (ret == -1 || ret == 0) {
					continue;
				}
				else {
					// 向其他子进程发送消息,通知客户端有数据要写
					for (int j = 0; j < user_cnt; j++) {
						if (users[j].pipefd[0] == eventfd) {
							continue;
						}
						printf("Send msg to other child process pipe\n");
						// 主进程往pipe[0]写,子进程从pipe[1]读。socketpair是全双工的
						// 通知其他子进程,哪个子进程有数写入了,发送子进程user编号
						send(users[j].pipefd[0], (char*)&child, sizeof(child), 0);
					}
				}
			}
		}
	}
	del_resource();
	return 0;
}

int setnonblocking(int fd) {
	int oldopt = fcntl(fd, F_GETFL);
	int newopt = oldopt | O_NONBLOCK;
	fcntl(fd, F_SETFL, newopt);
	return oldopt;
}

int addfd(int epoll, int fd) {
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN | EPOLLET;
	epoll_ctl(epoll, EPOLL_CTL_ADD, fd, &event);
	setnonblocking(fd);
}

void sig_handler(int sig) {
	int save_err = errno;
	int msg = sig;
	// 使用管道传输信号
	send(sig_pipefd[1], (char*)&msg, 1, 0);
	errno = save_err;
}


int del_resource() {
	close(sockfd);
	close(sig_pipefd[0]);
	close(sig_pipefd[1]);
	close(epollfd);
	shm_unlink(shm_name);
	delete[]users;
	delete[] sub_process;
}

void term_child(int sig) {
	// 每个子进程的stop_child都是独立的,所以修改后就可终止当前子进程了
	stop_child = true;
}
int run_child(int idx, client_data* users, char* share_mem) {
	int connfd = users[idx].fd;
	// 子进程里使用pipefd[1]
	int pipefd = users[idx].pipefd[1];
	epoll_event events[MAX_EVENT_NUM];
	// 错误!不能直接使用父进程的epoll!
	//addfd(epollfd, pipefd);
	//addfd(epollfd, connfd);
	int child_epollfd = epoll_create(5);
	assert(child_epollfd != -1);
	addfd(child_epollfd, pipefd);
	addfd(child_epollfd, connfd);
	int ret;
	// 终止的信号,接收父进程里的终止信号
	addsig(SIGTERM, term_child, false);
	while (!stop_child) {
		int num = epoll_wait(child_epollfd, events, MAX_EVENT_NUM, -1);
		if (num < 0) {
			printf("Child epoll failure\n");
			break;
		}
		for (int i = 0; i < num; i++) {
			int eventfd = events[i].data.fd;
			if (eventfd == connfd && events[i].events & EPOLLIN) {
				// 对应的客户端发来消息
				ret = recv(connfd, share_mem + idx * BUFFER_SIZE, BUFFER_SIZE - 1, 0);
				if ((ret < 0 && errno != EAGAIN) || ret == 0) {
					printf("Close client %d\n", users[idx].fd);
					stop_child = true;
				}
				else {
					// 告诉主进程哪个子进程有消息读入了,主进程再通知其他子进程
					send(pipefd, (char*)&idx, sizeof(idx), 0);
				}

			}
			else if (eventfd == pipefd && events[i].events & EPOLLIN) {
				// 有其他子进程接收到消息了,将其发送给客户端
				int other_idx = 0;
				ret = recv(pipefd, (char*)&other_idx, sizeof(other_idx), 0);
				if ((ret < 0 && errno != EAGAIN) || ret == 0) {
					printf("Close client %d\n", users[idx].fd);
					stop_child = true;
				}
				else {
					send(connfd, share_mem + other_idx * BUFFER_SIZE, BUFFER_SIZE - 1, 0);
				}
			}
			else {
				continue;
			}
		}
	}
	close(connfd);
	close(pipefd);
	close(child_epollfd);
	return 0;
}

int addsig(int sig, void(*handler)(int), bool restart) {
	struct sigaction sa;
	memset(&sa, '\0', sizeof(sa));
	sa.sa_handler = handler;
	if (restart) {
		sa.sa_flags |= SA_RESTART;
	}
	sigfillset(&sa.sa_mask);
	// 使用sigaction注册信号
	assert(sigaction(sig, &sa, NULL) != -1);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值