目录
在Linux系统中,IO多路复用是一种机制,它允许一个进程能够监视多个文件描述符(sockets、pipes等)的可读、可写和异常等事件。这样,一个进程就能够同时等待多个IO操作,而不需要创建多个线程来处理每个IO操作。
常见的IO多路复用函数包括
select
、poll
、epoll
等。这些函数允许程序员编写高效的IO多路复用代码,从而使得单个进程能够同时处理多个IO事件,提高系统的并发性能。使用IO多路复用的好处在于,它可以避免创建大量的线程或进程来处理IO事件,从而减少了系统资源的消耗,并且降低了上下文切换的开销。这对于高性能的网络服务器等应用是非常重要的。
编写程序实现多个Client之间通过Server来传递消息从而实现client间的通信功能。要求如下:
服务器创建3个众所周知的命名管道FIFO_1, FIFO_2, FIFO_3, 分别接收用户的注册请求、登录请求和聊天请求,服务器等待任一管道来的请求,收到时立即响应;不同用户不允许使用同一用户名注册,并设置密码。
每个用户都建立一个以自己用户名为名字的命名管道,用户A发送给用户B的信息发送给服务器,然后服务器通过B的专用FIFO发送给用户B。
基本思路是用select函数实现IO多路复用,使得单个线程能够同时处理多个IO事件。
服务器能够处理多个用户的注册请求、登录请求和聊天请求,不同用户之间可以通过服务器进行通信。
我们编写的代码文件有实现服务器的mutiplexingServer.c,实现客户端的mutiplexingClient.c,以及通信配置的mutiplexing.h。
首先是配置通信的头文件mutiplexing.h,在这里我们定义了服务器众所周知的三个命名管道和规定了服务器可容纳用户的数目。
User结构体存储用户的命名管道信息以及用户名和密码。
Chat结构体存储聊天信息,包括目标用户的命名管道信息和发送者的命名管道信息,还有要发送的聊天信息。
还有一个Response结构体用来存储服务器返回客户端的响应信息,由于操作不一定总是客户端所期望的,所以我们除了正常的响应信息外,还存储了一个ok值,当ok值不为0时,说明操作异常。
接下来我们来看服务器的实现。
首先定义了一个全局变量userNumber来记录当前用户的数量,还有一个users数组存储所有用户的信息。
然后创建三个众所周知的命名管道文件并打开。
指定检查这三个文件描述符并获取最大的文件描述符。
关键用select监听这些文件描述符。
然后发现哪个文件描述符准备好可读的时候就调用我们写好的处理函数来处理。
下面逐个讲解我们写的处理函数。
首先是处理注册请求的函数。
我们拿到用户名判断现有的用户组中是否有相同的用户名,如果有,那么我们向客户端返回该用户名已经存在的信息。
如果没有用户名与它相同,那么我们把这个用户的信息加到现有的用户组里面,并向客户端返回注册成功的消息,并且在服务器端打印该用户注册的信息。
然后看登录处理函数。
首先看看当前用户组里面有没有这个用户,找到的话就比较密码是否相同,相同就向客户端发送登录成功的信息,并在服务器端打印该用户登录的信息;密码不同就发送密码错误的信息;如果没有找到这个用户就发送用户名错误的信息。
最后看聊天处理函数。
首先判断该用户要发生的目标用户存不存在,存在的话就向目标用户发送聊天信息,并向发送者反馈信息发送成功,如果目标用户不存在,向发送者反馈该用户不存在。
再看客户端实现。
主函数是创建命名管道,然后进入功能页面显示函数。
在主功能页面,我们首先提示用户输入r表示注册,输入l表示登录,输入q表示退出。如果是输入l或者r,即注册或者登录,我们就收集用户名和密码然后跳转到相应的请求发送函数,如果收到其他字符则给出输入错误信息并重新展示主功能页面。
然后我们看注册请求发送函数。
打开众所周知的注册命名管道,向其写入我们收集的用户名和密码,等待服务器响应后打印响应信息并返回主页面,因为不管注册的结果如何,都需要返回主页面进行下一步的操作。
然后是登录请求函数。
同样我们打开众所周知的登录命名管道,向其写入收集的用户名和密码,如果登录成功,那么进入聊天页面,否则返回主页面。
最后看聊天请求函数。
先展示功能,提示用户按下r表示接收消息,按下s表示发送消息,按下q表示退出当前页面。
如果是发送消息,那么需要输入发送目标用户的用户名已经要发送的消息并打印服务器返回的发送结果。
如果是接收消息,就从自己的命名管道中读取数据并打印。
如果用户按下的其他按键,那么提示用户按错了,重新展示聊天页面。
现在我们来展示运行结果。
首先编译运行服务器,可以看到服务器已经启动。
再编译运行一个客户端,可以看到功能展示页面。
然后我们输入r注册,输入用户名yemaolin和密码2021155015,可以看到服务器返回的注册成功的信息。
然后我们输入l登录,输入刚刚注册的用户名和密码进行登录,可以看到登录成功。
然后我们再开一个客户端注册一个game101,密码是OpenGL并登录。
然后我们用game101给yemaolin发消息hello, I am game101。
然后我们用yemaolin接收消息。
然后yemaolin再给game101发一条消息I love C++。
同样game101可以收到yemaolin发来的消息。
最后我们可以看一下服务器端打印的消息,这展示了用户的状态。
圆满结束IO多路复用。
这个过程还是比较难顶的,但是结果还是比较美好的。
首先不得不说,IO多路复用真的是美妙。我大二曾经用Java写过多个客户端的聊天程序,但是是用的多线程实现的。如今居然可以用单线程实现多用户访问服务器,真是神奇。当然多路复用只能让服务器同时处理多个用户的请求,轮到客户端本身发送和接受消息的时候,在同一时间,客户端只能选择发送消息或者是接收消息。如果要同时接收和发送的话,那估计只有多线程可以实现了。
这次使用select实现的IO多路复用聊天程序除了能够处理正常的用户注册、用户登录和用户间通信之外,还对一些异常情况做了处理,例如用户重复注册,登录用户名错误或者是密码错误,已经发送消息时目标用户不存在等情况做了处理,让我们的程序更加健壮。
除此之外,为了之后的功能拓展以及便于修改,我们将每个功能模块化,并将一些基本的配置消息单独写在一个头文件,这为我们之后的进一步完善做了比较坚实的基础。
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include "mutiplexing.h"
- #include <sys/select.h>
-
- int userNumber = 0; // 当前用户量
- User users[UserCapacity];
-
- void registerHandler(User*user) {
- char message[BuffSize];
- int ok = 0;
- for (int i = 0; i < userNumber; i++) {
- if (strcmp(user->username, users[i].username) == 0) {
- sprintf(message, "The username has already exited!");
- ok = -1;
- break;
- }
- }
- if (ok == 0) {
- strcpy(users[userNumber].username, user->username);
- strcpy(users[userNumber].password, user->password);
- sprintf(message, "Register succeed!");
- printf("User %s register\n",user->username);
- userNumber++;
- }
- int fd = open(user->fifo, O_RDWR | O_NONBLOCK);
- write(fd, message, BuffSize);
- }
-
- void loginHandler(User*user) {
- Response response;
- response.ok = -1;
- for (int i = 0; i < userNumber; i++) {
- if (strcmp(user->username, users[i].username) == 0) {
- if (strcmp(user->password, users[i].password) == 0) {
- strcpy(users[i].fifo, user->fifo);
- sprintf(response.message, "Login succeed!");
- printf("User %s login\n",user->username);
- response.ok = 0;
- break;
- } else {
- sprintf(response.message, "Wrong password!");
- response.ok=-2;
- break;
- }
- }
- }
- if (response.ok == -1) {
- sprintf(response.message, "Wrong username!");
- }
- int fd = open(user->fifo, O_RDWR | O_NONBLOCK);
- write(fd, &response, sizeof(Response));
- }
-
- void chatHandler(Chat*chat) {
- char message[BuffSize];
- int ok = -1;
- for (int i = 0; i < userNumber; i++) {
- if (strcmp(chat->targetUser, users[i].username) == 0) {
- int fd = open(users[i].fifo, O_RDWR | O_NONBLOCK);
- write(fd, chat->message, BuffSize);
- sprintf(message, "Send succeed!");
- ok = 0;
- break;
- }
- }
- if (ok == -1) {
- sprintf(message, "User %s does not exit!", chat->targetUser);
- }
- int fd = open(chat->fifo, O_RDWR | O_NONBLOCK);
- write(fd, message, BuffSize);
- }
-
- int main() {
- fd_set fds, read_fds; // 文件描述符集合
- int max_fd;
- int register_fd, login_fd, chat_fd;
- // 创建或打开FIFO文件
- mkfifo(Register_FIFO, 0777);
- mkfifo(Login_FIFO, 0777);
- mkfifo(Chat_FIFO, 0777);
- // 打开FIFO文件 O_RDWR 可读写 O_NONBLOCK 非阻塞
- register_fd = open(Register_FIFO, O_RDWR | O_NONBLOCK);
- login_fd = open(Login_FIFO, O_RDWR | O_NONBLOCK);
- chat_fd = open(Chat_FIFO, O_RDWR | O_NONBLOCK);
- // 指定要检查的文件描述符
- FD_ZERO(&fds);
- FD_SET(register_fd, &fds);
- FD_SET(login_fd, &fds);
- FD_SET(chat_fd, &fds);
- // 获取最大文件描述符
- max_fd = register_fd > login_fd ? register_fd : login_fd;
- max_fd = max_fd > chat_fd ? max_fd : chat_fd;
- printf("MutiplexingServer Listening\n");
- while (1) {
- read_fds = fds;
- User registerData;
- User loginData;
- Chat chatData;
- // 使用select监听文件描述符
- if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) == -1) {
- perror("select");
- exit(EXIT_FAILURE);
- }
- // 检查哪些文件描述符已经准备好
- if (FD_ISSET(register_fd, &read_fds)) {
- // 处理注册
- read(register_fd, ®isterData, sizeof(User));
- registerHandler(®isterData);
- }
- if (FD_ISSET(login_fd, &read_fds)) {
- // 处理登录
- read(login_fd, &loginData, sizeof(User));
- loginHandler(&loginData);
- }
- if (FD_ISSET(chat_fd, &read_fds)) {
- // 处理聊天
- read(chat_fd, &chatData, sizeof(Chat));
- chatHandler(&chatData);
- }
- }
- }
- #include<unistd.h>
- #include<stdio.h>
- #include <stdlib.h>
- #include<string.h>
- #include<sys/stat.h>
- #include<fcntl.h>
- #include"mutiplexing.h"
-
- void showPage(User*user);
-
- void registerClient(User*user){
- int register_fd = open(Register_FIFO, O_RDWR | O_NONBLOCK);
- write(register_fd, user, sizeof(User));
- int fd=open(user->fifo,O_RDWR | O_NONBLOCK);
- char message[BuffSize];
- while(1){
- int result= read(fd,message,BuffSize);
- if(result>0){
- printf("%s\n",message);
- break;
- }
- }
- showPage(user);
- }
- void chatClient(User*user){
- char what;
- printf("Chat Page:\npress r for receive | s for send | q for quit\n");
- while((what=getchar())=='\n'){}
- if (what == 'q') {
- exit(0);
- } else if(what =='s') { // 发送信息
- Chat chat;
- strcpy(chat.fifo,user->fifo);
- printf("send username: ");
- scanf("%s",chat.targetUser);
- getchar();
- printf("send data: ");
- scanf("%[^\n]",chat.message);
- int chat_fd=open(Chat_FIFO,O_RDWR | O_NONBLOCK);
- write(chat_fd,&chat,sizeof(Chat));
- // 等待服务器响应
- int fd=open(user->fifo,O_RDWR | O_NONBLOCK);
- char message[BuffSize];
- while(1){
- int result= read(fd,message,BuffSize);
- if(result>0){
- printf("%s\n",message);
- break;
- }
- }
- } else if(what=='r'){ //接收消息
- int fd=open(user->fifo,O_RDWR | O_NONBLOCK);
- char message[BuffSize];
- while(1){
- int result= read(fd,message,BuffSize);
- if(result>0){
- printf("%s\n",message);
- break;
- }
- }
- }else{
- printf("Wrong press key, please try again!\n");
- }
- chatClient(user);
- }
- void loginClient(User*user){
- int login_fd = open(Login_FIFO, O_RDWR | O_NONBLOCK);
- write(login_fd, user, sizeof(User));
- int fd=open(user->fifo,O_RDWR | O_NONBLOCK);
- Response response;
- while(1){
- int result= read(fd,&response,sizeof(Response));
- if(result>0){
- printf("%s\n",response.message);
- break;
- }
- }
- if(response.ok!=0){ // 登录失败
- showPage(user);
- }else{
- chatClient(user);
- }
- }
-
- void showPage(User*user){
- char what;
- printf("Chat Client:\npress r for register | l for login | q for quit\n");
- while((what=getchar())=='\n'){}
- if (what == 'q') {
- exit(0);
- } else if(what=='r'||what=='l') {
- printf("username: ");
- scanf("%s", user->username);
- printf("password: ");
- scanf("%s", user->password);
- }else{
- printf("Wrong press key, please try again!\n");
- showPage(user);
- }
- if (what == 'r') {
- registerClient(user);
- }else if(what=='l'){
- loginClient(user);
- }
- }
- int main() {
- User user;
- sprintf(user.fifo, "client_fifo/client_fifo%d", getpid());
- mkfifo(user.fifo, 0777);
- showPage(&user);
- exit(0);
- }
- #ifndef SYSTEMPROGRAM_MESSAGE_H
- #define SYSTEMPROGRAM_MESSAGE_H
- #define Register_FIFO "register_fifo" // 注册
- #define Login_FIFO "login_fifo" // 登录
- #define Chat_FIFO "chat_fifo" // 聊天
- #define BuffSize 100
- #define UserCapacity 10 // 可容纳用户量
- typedef struct {
- char fifo[BuffSize]; //client's FIFO name
- char username[20];
- char password[20];
- } User;
- typedef struct {
- char fifo[BuffSize]; //client's FIFO name
- char targetUser[20]; // 向谁发送信息
- char message[BuffSize];
- } Chat;
- typedef struct{
- int ok;
- char message[BuffSize];
- }Response;
- #endif //SYSTEMPROGRAM_MESSAGE_H