一、 利用操作系统提供的系统调用、标准C库函数和POSIX 线程库pthread 函数设计并实现多人即时聊天软件。并利用学过的知识深入分析问题
1.1. 守护进程命名规则:chatserver_学生姓名拼音首字母缩写,例如,张三的守护进程命名为:chatserver_zs。
1.2. 服务器配置文件设置以下信息:
1.2.1. 众所周知的命名管道
a) REG_FIFO :命名为“学生姓名拼音首字母缩写+register”
b) LOGIN_FIFO :命名为“学生姓名拼音首字母缩写+login”
c) MSG_FIFO :命名为“学生姓名拼音首字母缩写+sendmsg”
d) LOGOUT_FIFO :命名为“学生姓名拼音首字母缩写+logout”
1.2.2. 同时在线用户数最大值 MAX_ONLINE_USERS,该值为 4。同一用户同一时刻只允许在一个终端登录一次 MAX_LOG_PERUSER,该值为 1。<br>
1.2.3. 用户日志文件存放目录 LOGFILES:/var/log/chat-logs/
1.3. 服务器首先读取配置文件,通过配置文件约束行为。
1.3.1. 根据配置文件创建众所周知的命名管道,采用多路复用技术
(select()、epoll()或 poll())监听并读取以上信息,分别启动线程处理用户的注册请求、登录请求、聊天请求和退出请求,并将结果返回给用
户,并做日志记录。服务器要求运行期间忽略 SIGCHLD、SIGTERM 等信号。
1.3.2. 为每个注册成功的用户建立独立日志文件,统一放在 1.2.3 设置的用户日志文件存放目录 LOGFILES 下。日志文件收集用户注册事件(用户名,注册,时间)、登录事件(用户名,登录,时间)、退出事件(用户名,退出,时间)、成功发送的信息(发送者,接收者,时间,true),未成功发送的信息(发送者,接收者,时间,false),日志文件只有超级用户可以读、写。同组用户和其他用户一律取消读、写、运行等权限。如果有未成功发送的信息,当接收者重新登录时服务器主动推送这些信息,并写相应日志文件(发送者,接收者,时间,true)。为安全起见,未成功发送的信息(发送者,接收者,信息,时间)应该存在特定的地方,更安全地保护起来(难点)。
请在答卷完成以下内容:
1) (10 分)请截图显示守护进程相关的进程信息、公共 FIFO 相关的信息,并解释各特征字段含义,截图显示命名管道创建、打开的代码。
2) (10 分)请截图显示配置文件内容及服务器端相应的处理代码,服务器忽略 SIGCHLD、SIGTERM 信号的代码
3) (10 分)请截图显示日志文件生成代码及有效日志文件属性及其内容,包括日志文件存放路径、权限,注册、登录、退出、成功发送消息和未成功发送消息事件记录的日志信息,未成功发送信息的管理等(难点)。
4) (10 分)展示多路复用监听请求的代码、解释运行过程 select()、epoll()或 poll()的参数文件描述符集合的变化。特别提醒,代码质量将作为评分点。
用户注册时提供用户名和密码,不允许不同用户使用同一用户名注册。客户端将为每个用户建立一个以用户名为名字的专用命名管道,用户A 发送给用户 B 的信息首先发送给服务器,然后服务器通过 B 的专用命名管道发送给用户 B。守护进程各线程通过共享内存共同维护和使用一个在线用户列表,包含用户名、密码及其私有命名管道。请注意其中的互斥访问和线程安全,请截图并讨论你的线程安全实现。
1) 截图用户注册的线程函数,截屏显示以下过程:启动 6 个终端,分别用以下用户名依次注册:学生姓名拼音_1、学生姓名拼音_2、 学生姓名拼音_3、最后学生姓名拼音_4、学生姓名拼音_5、学生姓名拼音_1。守护进程将注册不成功的信息返回给相应的用户。(8 分)。
2) 截图客户端登录线程函数,截屏显示以下登录过程:启动 6 个终端,分别用以下用户名登录: 学生姓名拼音_1、学生姓名拼音_2、 学生姓名拼音_3、学生姓名拼音_4、学生姓名拼音_5、学生姓名拼音_3。守护进程将成功登录的用户信息及所有在线的总人数及用户名显示给所有的用户,不成功的信息仅返回给不成功的用户端。(12 分)名为“学生姓名拼音_2”的用户退出,守护进程将其退出的信息及剩余在线用户的信息显示给剩余的用户。保持名为“学生姓名拼音_2”用户的退出状态做 3)(4 分)。
3) 截图用户消息发送过程,一对一发送:“学生姓名拼音_1”的用户给“学生姓名拼音_2”的用户发送信息“How are you,学生姓名拼音_2 ”,一对多发送:“学生姓名拼音_1”的用户给“学生姓名拼音_2”、“学生姓名拼音_3”的用户发送信息“Hi,let’s play badminton? ”,截屏三个用户的收发信息情况,包括成功和不成功的情况(可能较难)。最后名为“学生姓名拼音_2”的用户登录系统,收到前面的信息,展示发送的时间。(12分)。
4) 截图讨论主线程收集新线程退出状态与否的代码实现,并讨论其优点和缺点(4 分)
系统参考架构图见图 2,请结合代码介绍线程池的管理。
a) 在配置文件里新增线程池规模参数 POOLSIZE;值为 4;
b) 服务器启动时根据配置文件新建 POOLSIZE 个线程,运行期接收请求时分派线程,请求处理完后回收线程用于再分派。请给出线程池管理数据结构设计、线程池线程状态管理算法设计和实现等,并附上相应的运 行 展 示 。 线 程 池 中 线 程 被 分 派 和 回 收 的 时 间 记 录 在 日 志/var/log/chat-logs/threads-log。截图展示日志信息。
二、 (难)openEuler 开源实习官网,领取一项任务并完成,贡献社区后提交任务报告:(https://www.openeuler.org/zh/internship/)
PPID为1,则说明该进程是守护进程:
这四个黄色的文件就是公共FIFO:
命名管道名称宏定义在头文件info.h中:
命名管道创建:
命名管道的打开:
info.h:
Chatserver_wyx(服务器端)配置:
依次是 参数,主函数内的守护进程创建
日志文件权限:
注册:
登录:
退出:
成功发送消息:
未成功发送消息:
未成功发送消息会给客户端的用户提示:
Server端:
fd_set readFds即为文件描述符集合,FD_ZERO初始化文件描述符集合,FD_SET需要监听的管道的文件描述符。select阻塞监听,直到有管道有内容可读时,对应文件描述符集合的元素被置标记,往下执行,多个if来确定哪个文件描述符被标记,然后创建线程进行相应操作,主程序初始化后接着监听。
Client端:
1~5注册是类似的
再次注册1号会注册失败,并显示原因:
2) 截图客户端登录线程函数,启动6个终端用分别注册以下6个用户名,并截图展示运行细节(12分)
学生姓名拼音_1
学生姓名拼音_2
学生姓名拼音_3
学生姓名拼音_4
学生姓名拼音_5
学生姓名拼音_3。
学生姓名拼音_2用户退出系统。(4分)
server端的客户端登录线程函数:
登录方法:
随着其他用户登录或退出,已登录的1号会得到消息:
3号示例:
2号:
终端6登录失败(先检测了在线人数,所以显示在线人满了):
3) 截图客户端发送信息函数,并用以下发送信息序列显示系统运行信息(12分)
“学生姓名拼音_1”的用户给“学生姓名拼音_2”的用户发送信息“How are you”,消息被挂起;
“学生姓名拼音_1”的用户给“学生姓名拼音_2”、“学生姓名拼音_3”的用户发送信息“Hi,let’s play badminton? ”,截屏三个用户的收发信息情况;
学生姓名拼音_2登录后收到信息 发:
3号:
3. 基于线程池的多用户聊天服务器设计与实现(20分)(难点,温馨提示:数据结构设计、算法设计及编码将作为评分点。)
(
请图文并茂展示线程池管理的代码和相应的运行结果,包括配置文件、线程池数据结构、管理算法(线程分派和回收)、线程分派和回收日志信息等。
)
特别提醒:代码质量和是否按时提交是本题评分点之一,请结合程序的正确性、线程的安全性、用户信息的安全性等讨论,迟交的同学本题分数直接减分,减分范围0~20分,以确保总评成绩不为A+为准。即如果该生整体成绩为A,虽然迟交,不扣分。如果该学生平时成绩基本满分,期末答卷也接近满分,但迟交,则本题将扣至该生不拿A+为止。
线程数组,任务队列,以及锁。
线程池初始化时,会建立设定数目个线程并加入到线程数组中,这些线程都执行死循环的worker函数。
没有任务时,都在条件等待,直到新任务加入到任务队列时会发送condition信号,等待线程中的一个就会从任务队列中拿出这个任务然后执行。
(对于线程池执行分配的方法,这里用了多态(纯虚函数),只需将希望执行的方法放到继承Task类实现的execute方法中,该调用时穿Task*即可。)
只需加入特定的flag,并且在响应的代码片加入if判断退出即可。 这里是用类内的bool stop来判断
#pragma once
#include <bits/stdc++.h>
using namespace std;
#define POOLSIZE 4
class Task {
public:
virtual void execute() = 0;
};
// class Task1 : public Task {
// public:
// void execute() override {
// // 执行任务1的具体操作
// }
// };
class ThreadPool{
public:
ThreadPool(size_t numThreads = POOLSIZE);
~ThreadPool();
void enqueue(Task* task);
private:
static void* worker(void*arg);
std::vector<pthread_t> workers;
std::queue<Task*> tasks;
pthread_mutex_t queueMutex;
pthread_cond_t condition;
bool stop;
};
ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
pthread_mutex_init(&queueMutex, nullptr);
pthread_cond_init(&condition, nullptr);
for (size_t i = 0; i < numThreads; ++i) {
pthread_t thread;
pthread_create(&thread, nullptr, worker, this);
workers.push_back(thread);
}
}
ThreadPool::~ThreadPool() {
{
pthread_mutex_lock(&queueMutex);
stop = true;
pthread_cond_broadcast(&condition);
pthread_mutex_unlock(&queueMutex);
}
for (pthread_t& worker : workers) {
pthread_join(worker, nullptr);
}
pthread_mutex_destroy(&queueMutex);
pthread_cond_destroy(&condition);
}
void ThreadPool::enqueue(Task* task) {
pthread_mutex_lock(&queueMutex);
tasks.push(task);
pthread_cond_signal(&condition);
pthread_mutex_unlock(&queueMutex);
}
void* ThreadPool::worker(void* arg) {
ThreadPool* pool = static_cast<ThreadPool*>(arg);
while (true) {
Task* task;
{
pthread_mutex_lock(&pool->queueMutex);
while (!pool->stop && pool->tasks.empty()) {
pthread_cond_wait(&pool->condition, &pool->queueMutex);
}
if (pool->stop && pool->tasks.empty()) {
pthread_mutex_unlock(&pool->queueMutex);
break;
}
task = pool->tasks.front();
pool->tasks.pop();
pthread_mutex_unlock(&pool->queueMutex);
}
task->execute(); // 执行任务
delete task; // 任务完成后删除
}
return nullptr;
}