引入
本文将讨论网络并发处理中的两个重要组成部分:epoll和消息队列。
epoll是一种在Linux系统中提供的高效I/O复用机制,在具体实现上epoll有表现得更加灵活、高效,极大地提升了网络应用的性能。而消息队列,则是一种异步通信的重要方式,通过解耦发送者和接收者之间的关系,提高了系统的可扩展性和灵活性。
下面将以多人聊天室为例,详细介绍epoll和消息队列的原理和具体实现。
epoll并发实现
原理概述
重要概念
- 事件表(Event table):epoll使用一个事件表来存储所有的文件描述符和它们的事件。这个事件表是一个红黑树(Red-Black Tree)数据结构,用于高效地插入、删除和查找操作;
- 事件结构(Event structure):epoll使用一个结构体来表示每个事件,其中包含文件描述符、关注的事件类型(读、写、错误等)以及用户自定义的数据;
- 回调机制:与传统的同步I/O模型不同,epoll是异步的。当一个文件描述符上的事件发生时,内核不会阻塞程序的执行,而是调用事先注册的回调函数(callback)来处理事件;
主要的调用
- epoll_create:创建一个epoll实例,返回一个文件描述符,用于标识这个实例。
int epoll_create(int size);
- epoll_ctl:用于向epoll实例中添加或删除文件描述符,并设置关注的事件类型。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epfd:epoll 实例的文件描述符。
// op:操作类型,可以是 EPOLL_CTL_ADD(添加新的文件描述符)、EPOLL_CTL_MOD(修改已有文件描述符的关注事件)、EPOLL_CTL_DEL(删除文件描述符)。
// fd:要添加、修改或删除的文件描述符。
// event:struct epoll_event 结构,描述关注的事件类型。
- epoll_wait:等待文件描述符上的事件发生,返回发生事件的文件描述符列表。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// epfd:epoll 实例的文件描述符。
// events:用于存储事件的数组。
// maxevents:最多返回的事件数量。
// timeout:超时时间,设置为 -1 表示无限等待,0 表示立即返回,大于 0 表示等待的毫秒数。
工作流程
epoll
通过非阻塞的方式处理大量的文件描述符,避免了传统同步模型中的轮询开销,提供了高性能的I/O处理机制。
创建
epoll
实例:通过epoll_create
创建一个epoll
实例。添加文件描述符:通过
epoll_ctl
将需要关注的文件描述符添加到epoll
实例中,并设置关注的事件类型。等待事件:通过
epoll_wait
等待文件描述符上的事件发生,此时程序会被阻塞。处理事件:当文件描述符上的事件发生时,
epoll_wait
返回,程序开始执行注册的回调函数,处理相应的事件。重复等待:重复步骤3和4,实现对多个文件描述符的高效管理。
代码解析
下面是一个基于epoll的多人网络聊天室的实现,具体包括四部分:epoll.h、epoll.c、server.c和client.c。下面将对这四个部分进行详细解释。
-
epoll.h
#ifndef UTILITY_H_INCLUDED
#define UTILITY_H_INCLUDED
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
struct ListNode {
int data;
struct ListNode* next;
};
struct List {
struct ListNode* head;
};
struct List clientsList;
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define EPOLL_SIZE 5000
#define BUF_SIZE 0xFFFF
#define SERVER_WELCOME "欢迎来到聊天室,%d!您的聊天ID是:客户端 #%d"
#define SERVER_MESSAGE "%d 说 >> %s"
#define EXIT "EXIT"
#define CAUTION "聊天室只能容纳一个人!"
#define USERNAME_LEN 20
int setNonBlocking(int sockfd);
void addFd(int epollfd, int fd, int enableEt);
int sendBroadcastMessage(int clientfd);
#endif // UTILITY_H_INCLUDED
这段代码是一个简单的聊天室服务器的工具函数库,定义了一些常量、数据结构以及一些实用的功能函数。
- 1-2行是头文件的保护宏定义,防止头文件的重复包含;
- 3-11行是导入必须的头文件,包含了一些基本的系统调用,网络编程相关的函数,以及
epoll
相关的函数和数据结构;- 13-19行是定义了一个简单的链表结构
ListNode
,以及一个链表结构List
,并创建了一个全局的链表实例clientsList,
用于存储连接到服务器的客户端的信息;- 20-28行是一些常量定义,包括服务器的IP地址和端口号、
epoll
实例的大小、缓冲区大小、欢迎信息、聊天消息格式、退出指令、注意事项信息以及用户名长度;- 29-31行定义了三个函数,setNonBlocking是将套接字设置为非阻塞模式、addFd是向epoll实例中添加文件描述符、sendBroadcastMessage是向所有连接的客户端广播消息;
- 32行是头文件的结束,关闭头文件保护宏定义。
-
epoll.c
#include "epoll.h"
int setNonBlocking(int sockfd)
{
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0) | O_NONBLOCK);
return 0;
}
void addFd(int epollfd, int fd, int enableEt)
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;
if (enableEt)
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
setNonBlocking(fd);
}
int sendBroadcastMessage(int clientfd)
{
char buf[BUF_SIZE], message[BUF_SIZE];
bzero(buf, BUF_SIZE);
bzero(message, BUF_SIZE);
printf("从客户端读取(客户端ID = %d)\n", clientfd);
int len = recv(clientfd, buf, BUF_SIZE, 0);
if (len <= 0)
{
close(clientfd);
struct ListNode* current = clientsList.head;
struct ListNode* previous = NULL;
while (current != NULL && current->data != clientfd) {
previous = current;
current = current->next;
}
if (previous == NULL) {
clientsList.head = current->next;
} else {
previous->next = current->next;
}
free(current);
printf("客户端ID = %d 关闭。\n现在聊天室里有 %d 个客户端。\n", clientfd, clientsList.head == NULL ? 0 : 1);
}
else
{
if (clientsList.head == NULL) {
send(clientfd, CAUTION, strlen(CAUTION), 0);
return len;
}
sprintf(message, SERVER_MESSAGE, clientfd, buf);
struct ListNode* current = clientsList.head;
while (current != NULL) {
if (current->data != clientfd) {
if (send(current->data, message, BUF_SIZE, 0) < 0) { perror("error"); exit(-1); }
}
current = current->next;
}
}
return len;
}
这段代码是一个简单的
epoll
使用案例,实现了一些基本的功能,主要包括设置非阻塞模式、向epoll
实例中添加文件描述符、以及向所有连接的客户端广播消息。
setNonBlocking
函数:通过fcntl
函数将指定的套接字文件描述符设置为非阻塞模式。它使用位运算和O_NONBLOCK
标志,将原来的文件描述符标志位与O_NONBLOCK
进行OR操作,从而设置非阻塞模式。addFd
函数:用于向epoll
实例中添加文件描述符,并根据参数enableEt
决定是否使用边缘触发模式(ET)。它首先设置struct epoll_event
结构体,然后通过epoll_ctl
函数将文件描述符添加到epoll
实例中。如果enableEt
为真,表示启用边缘触发模式,将EPOLLET
加入关注的事件类型中。sendBroadcastMessage
函数:用于从指定客户端接收消息,并根据接收到的消息处理不同的情况。如果接收到的消息长度小于等于0,表示客户端关闭连接,需要在链表中移除该客户端,并释放相关资源。如果消息长度大于0,则表示接收到了有效的消息,将消息格式化为服务器消息格式,然后通过链表中的其他客户端的文件描述符,向所有其他客户端广播消息。
-
server.c
#include "epoll.h"
int main(int argc, char* argv[])
{
struct sockaddr_in serverAddr;
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
int listener = socket(PF_INET, SOCK_STREAM, 0);
if (listener < 0) { perror("listener"); exit(-1); }
printf("监听套接字创建成功\n");
if (bind(listener, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
perror("绑定错误");
exit(-1);
}
int ret = listen(listener, 5);
if (ret < 0) { perror("监听错误"); exit(-1); }
printf("开始监听:%s\n", SERVER_IP);
int epfd = epoll_create(EPOLL_SIZE);
if (epfd < 0) { perror("epfd 错误"); exit(-1); }
printf("epoll 创建成功,epollfd = %d\n", epfd);
static struct epoll_event events[EPOLL_SIZE];
addFd(epfd, listener, 1);
while (1)
{
int epollEventsCount = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (epollEventsCount < 0) {
perror("epoll 失败");
break;
}
printf("epoll_events_count = %d\n", epollEventsCount);
for (int i = 0; i < epollEventsCount; ++i)
{
int sockfd = events[i].data.fd;
if (sockfd == listener)
{
struct sockaddr_in clientAddress;
socklen_t clientAddrLength = sizeof(struct sockaddr_in);
int clientfd = accept(listener, (struct sockaddr*)&clientAddress, &clientAddrLength);
printf("客户端连接来自:%s : % d(IP : 端口),clientfd = %d \n",
inet_ntoa(clientAddress.sin_addr),
ntohs(clientAddress.sin_port),
clientfd);
addFd(epfd, clientfd, 1);
struct ListNode* newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
newNode->data = clientfd;
newNode->next = clientsList.head;
clientsList.head = newNode;
printf("将新的 clientfd = %d 添加到 epoll\n", clientfd);
printf("现在聊天室里有 %d 个客户端\n", clientsList.head == NULL ? 0 : 1);
printf("欢迎消息\n");
char message[BUF_SIZE];
bzero(message, BUF_SIZE);
sprintf(message, SERVER_WELCOME, clientfd);
int ret = send(clientfd, message, BUF_SIZE, 0);
if (ret < 0) { perror("发送错误"); exit(-1); }
}
else
{
int ret = sendBroadcastMessage(sockfd);
if (ret < 0) { perror("错误"); exit(-1); }
}
}
}
close(listener);
close(epfd);
return 0;
}
这段代码实现了聊天室的服务端。
- 4-7行通过结构体
serverAddr
初始化服务器的地址,包括地址族(IPv4)、端口号(使用预定义的SERVER_PORT)
和IP地址(使用预定义的SERVER_IP)
;- 8-10行创建一个监听套接字
listener
,并检查是否创建成功。如果创建失败,打印错误信息并退出程序;- 11-14行将监听套接字绑定到指定的地址和端口,如果绑定失败,打印错误信息并退出程序;
- 15-17行启动监听模式,允许最多5个连接处于等待状态。如果监听失败,打印错误信息并退出程序。
- 18-20创建
epoll
实例,将监听套接字添加到epoll
实例中,启用边缘触发模式。如果创建epoll
实例失败,打印错误信息并退出程序;- 从while部分开始进入事件处理循环;
- while中的for循环遍历发生的事件,如果事件对应的文件描述符是监听套接字,表示有新的客户端连接,执行相应的处理。否则,表示已连接的客户端有消息到达,执行消息广播操作;
- for循环中if体首先接受新的客户端连接,并获取客户端的地址信息,如果连接失败,打印错误信息并退出程序;
- 之后,将新客户端添加到epoll实例和链表;
- 向新客户端发送欢迎消息;
- 最后处理已连接客户端的消息广播,关闭监听套接字和
epoll
实例,释放资源。
-
client.c
#include "epoll.h"
int main(int argc, char* argv[])
{
struct sockaddr_in serverAddr;
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
int sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock < 0) { perror("套接字错误"); exit(-1); }
if (connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
perror("连接错误");
exit(-1);
}
int pipeFd[2];
if (pipe(pipeFd) < 0) { perror("管道错误"); exit(-1); }
int epfd = epoll_create(EPOLL_SIZE);
if (epfd < 0) { perror("epfd 错误"); exit(-1); }
static struct epoll_event events[2];
addFd(epfd, sock, 1);
addFd(epfd, pipeFd[0], 1);
int isClientWork = 1;
char message[BUF_SIZE];
int pid = fork();
if (pid < 0) { perror("fork 错误"); exit(-1); }
else if (pid == 0)
{
close(pipeFd[0]);
printf("请输入 'exit' 退出聊天室\n");
while (isClientWork) {
bzero(&message, BUF_SIZE);
fgets(message, BUF_SIZE, stdin);
if (strncasecmp(message, EXIT, strlen(EXIT)) == 0) {
isClientWork = 0;
}
else {
if (write(pipeFd[1], message, strlen(message) - 1) < 0)
{
perror("fork 错误");
exit(-1);
}
}
}
}
else
{
close(pipeFd[1]);
while (isClientWork) {
int epollEventsCount = epoll_wait(epfd, events, 2, -1);
for (int i = 0; i < epollEventsCount; ++i)
{
bzero(&message, BUF_SIZE);
if (events[i].data.fd == sock)
{
int ret = recv(sock, message, BUF_SIZE, 0);
if (ret == 0) {
printf("服务器关闭连接:%d\n", sock);
close(sock);
isClientWork = 0;
}
else printf("%s\n", message);
}
else {
int ret = read(events[i].data.fd, message, BUF_SIZE);
if (ret == 0) isClientWork = 0;
else {
send(sock, message, BUF_SIZE, 0);
}
}
}
}
}
if (pid) {
close(pipeFd[0]);
close(sock);
}
else {
close(pipeFd[1]);
}
return 0;
}
这段代码实现了聊天室的客户端。
- 第1部分通过结构体
serverAddr
初始化服务器的地址,包括地址族(IPv4)、端口号(使用预定义的SERVER_PORT)
和IP地址(使用预定义的SERVER_IP);
- 第2部分创建套接字并连接到服务器。如果创建套接字或连接失败,打印错误信息并退出程序;
- 第3部分创建一个管道
pipeFd
,用于父子进程之间的通信;- 第4部分创建
epoll
实例,将套接字和管道的读端添加到epoll
实例中,启用边缘触发模式。如果创建epoll
实例失败,打印错误信息并退出程序;- 第5部分创建子进程并进行聊天,子进程负责从标准输入读取用户输入,并通过管道发送给父进程;父进程负责监听套接字和管道,处理收到的消息;
- 第6部分子进程读取用户输入并发送给父进程,子进程通过标准输入获取用户输入,如果用户输入为 "exit",则退出聊天室,否则将用户输入写入管道;
- 第7部分父进程监听套接字和管道,处理收到的消息,如果事件对应的文件描述符是套接字,表示从服务端接收到消息,处理服务端的消息;如果事件对应的文件描述符是管道的读端,表示从管道接收到消息,处理从子进程传来的用户输入,然后将消息发送给服务端;
- 第8部分处理从服务端接收到的消息,处理从服务器接收到的消息,如果返回值为 0,表示服务器关闭连接,关闭套接字并退出聊天室。否则,打印接收到的消息;
- 第9部分处理从管道接收到的消息,如果返回值为 0,表示子进程已经关闭,退出聊天室。否则,将消息发送给服务器;
- 最后,关闭套接字和管道,如果是父进程,关闭管道的读端和套接字,如果是子进程,关闭管道的写端。
实现效果
编译可执行文件
make
启动服务端
/.server
启动客户端
打开不同的终端,启动多个客户端。
./client
首先启动第一个client,clinet端显示和server端显示如下:
下面启动第二个client,clinet端显示和server端显示如下:
消息通信
退出程序
消息队列并发实现
原理概述
重要概念
- 消息队列:是一个存储消息的容器,它允许一个进程将消息发送到队列,而另一个进程则可以从队列中接收消息;
- 消息:是进程之间传递的数据单元,可以是任意形式的结构体,包含发送者和接收者需要交换的信息。
- 队列标识符:消息队列由一个唯一的标识符来标识,进程可以使用这个标识符来打开特定的消息队列。
工作流程
创建消息队列
在使用消息队列之前,进程需要先创建一个消息队列。在C语言中,可以使用msgget
函数来创建或获取一个消息队列。
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
// key是一个用于标识消息队列的键值,msgflg是消息队列的标志,可以用来指定创建新队列还是获取已存在的队列。
发送消息
进程通过调用msgsnd
函数向消息队列发送消息。
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// msqid是消息队列标识符,msgp是指向消息的指针,msgsz是消息的大小,msgflg是发送消息的标志。
接收消息
另一个进程通过调用msgrcv
函数从消息队列接收消息。
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// msqid是消息队列标识符,msgp是指向用于存储消息的缓冲区的指针,msgsz是消息缓冲区的大小,msgtyp是要接收的消息类型,msgflg是接收消息的标志。
控制消息队列
可以使用msgctl
函数来对消息队列进行控制,包括删除消息队列等操作。
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// msqid是消息队列标识符,cmd是要执行的操作,buf是一个指向msqid_ds结构体的指针,用于传递或接收消息队列的状态信息。
使用注意事项
- 消息队列是系统范围内的资源,因此需要小心管理,确保在不再需要时及时删除。
- 消息的格式和大小需要事先约定好,以便发送和接收方能够正确解析消息。
- 错误处理是必要的,例如,在创建消息队列或发送/接收消息时可能会发生错误,需要适当地处理这些错误。
代码解析
下面是一个基于消息队列的多人网络聊天室的实现,具体包括四部分:msg_queue.h、msg_queue.c、server.c和client.c。下面将对这四个部分进行详细解释。
-
msg_queue.h
#ifndef __MODIFIED_MYMESSENGER__
#define __MODIFIED_MYMESSENGER__
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#define IS_SERVER_PARENT 1
#define PUBLIC_CHAT_QUEUE_ID 10
#define MAX_CLIENT_COUNT 6
#define MAX_CLIENT_NAME_LENGTH 30
#define DEFAULT_MESSAGE_TYPE 1
#define IS_NEW_CLIENT_MESSAGE_TYPE 2
#define CLIENT_QUIT_MESSAGE_TYPE 3
#define CLIENT_QUIT_COMMAND "quit\n"
#define BROADCAST_MESSAGE_MARKER "Broadcast"
#define PRIVATE_MESSAGE_HEADER "@"
struct message_buffer
{
long mtype;
int client_pid;
char client_name[MAX_CLIENT_NAME_LENGTH];
char target_name[MAX_CLIENT_NAME_LENGTH];
char mtext[100];
};
int Open_Public_Chat_Queue(int queue_id);
void Store_Client_Info(void);
void Delete_Client_Info(void);
void Send_Public_Or_Private_Message(void);
void Read_Client_Data(void);
void Display_Local_Time(void);
void Private_Chat_Filter_By_Name(char *client_message);
void Write_Client_Data(int process, int child_pid);
void Server_Signal_Handler(int num);
int New_Client_Flag;
int Quit_Flag;
int Client_Count;
int Public_Chat_Queue, Private_Chat_Queue;
int server_pid;
struct message_buffer Client_to_Server, Server_to_Client;
int Client_PID_Storage[MAX_CLIENT_COUNT];
char Client_Name_Storage[MAX_CLIENT_COUNT][MAX_CLIENT_NAME_LENGTH];
#endif //__MODIFIED_MYMESSENGER__
这段代码定义了一个名为
msg_queue.h
的头文件,其中包含了一些宏定义、结构体定义以及函数声明。这些内容主要用于实现一个简单的进程间通信系统,通过消息队列进行客户端和服务器之间的通信。
- 宏定义:防止头文件重复包含;
- 包含一系列系统调用和标准库的头文件,为后续使用提供必要的函数和类型定义;
- 定义一些常量和标志:消息类型、队列ID、最大客户端数量、客户端名称最大长度等;
- 消息缓冲区结构体定义:定义了一个结构体
message_buffer
用于存储消息的各个字段,包括消息类型、客户端进程ID、客户端名称、目标名称以及消息文本;- 全局变量和数组定义:定义了一系列全局变量,用于存储程序运行时的状态和数据。其中包括标志位、客户端数量、消息队列ID、进程ID、消息缓冲区以及存储客户端PID和名称的数组;
- 函数声明:声明了一系列函数,用于执行各种与消息队列相关的操作。函数的具体实现可能在其他源文件中定义。
msg_queue.c
#include "msg_queue.h"
int Open_Public_Chat_Queue(int queue_id)
{
int temp_queue;
temp_queue = msgget(queue_id, IPC_CREAT | 0666);
if (temp_queue == -1)
{
perror("Open_Public_Chat_Queue");
exit(1);
}
return temp_queue;
}
void Store_Client_Info(void)
{
Client_PID_Storage[Client_Count] = Client_to_Server.client_pid;
strcpy(Client_Name_Storage[Client_Count], Client_to_Server.client_name);
printf("Client %d has been recorded\n", Client_PID_Storage[Client_Count]);
Client_Count++;
printf("Client Count is: %d\n", Client_Count);
}
void Delete_Client_Info(void)
{
int local;
int count;
for (local = 0; local < Client_Count; local++)
{
if (Client_to_Server.client_pid == Client_PID_Storage[local])
break;
}
for (count = local; count < Client_Count; count++)
{
Client_PID_Storage[count] = Client_PID_Storage[count + 1];
strcpy(Client_Name_Storage[count], Client_Name_Storage[count + 1]);
}
}
void Send_Public_Or_Private_Message(void)
{
int count;
int target_count;
int source_count;
printf("Client Target is: %s\n", Client_to_Server.target_name);
Server_to_Client.mtype = DEFAULT_MESSAGE_TYPE;
if (strcmp(Client_to_Server.target_name, BROADCAST_MESSAGE_MARKER) != 0)
{
for (target_count = 0; target_count < Client_Count; target_count++)
{
if (strcmp(Client_to_Server.target_name, Client_Name_Storage[target_count]) == 0)
break;
}
if (target_count >= Client_Count)
{
printf("[Warning] Target Client not found!\n");
return;
}
for (source_count = 0; source_count < Client_Count; source_count++)
{
if (Client_to_Server.client_pid == Client_PID_Storage[source_count])
break;
}
printf("Private Chat Queue Pid : %d\n", Client_PID_Storage[target_count]);
Private_Chat_Queue = Open_Public_Chat_Queue(Client_PID_Storage[target_count]);
Server_to_Client.client_pid = Client_to_Server.client_pid;
sprintf(Server_to_Client.mtext, "[%s] said to you: ", Client_Name_Storage[source_count]);
strcat(Server_to_Client.mtext, Client_to_Server.mtext);
if ((msgsnd(Private_Chat_Queue, &Server_to_Client, sizeof(Server_to_Client), IPC_NOWAIT)) != -1)
printf("Write message to Client_%d Success!\n", Client_PID_Storage[target_count]);
}
else
{
if (New_Client_Flag)
{
sprintf(Server_to_Client.mtext, "[%s] entered the chat room\n", Client_to_Server.client_name);
New_Client_Flag = 0;
}
else if (Quit_Flag)
{
sprintf(Server_to_Client.mtext, "[%s] exited the chat room\n", Client_to_Server.client_name);
Quit_Flag = 0;
}
else
{
sprintf(Server_to_Client.mtext, "[%s] said: ", Client_to_Server.client_name);
strcat(Server_to_Client.mtext, Client_to_Server.mtext);
}
for (count = 0; count < Client_Count; count++)
{
Private_Chat_Queue = Open_Public_Chat_Queue(Client_PID_Storage[count]);
Server_to_Client.client_pid = Client_to_Server.client_pid;
if ((msgsnd(Private_Chat_Queue, &Server_to_Client, sizeof(Server_to_Client), IPC_NOWAIT)) != -1)
printf("Write message to Client_%d Success!\n", Client_PID_Storage[count]);
usleep(100000);
}
}
printf("\n");
}
void Read_Client_Data(void)
{
Private_Chat_Queue = Open_Public_Chat_Queue(server_pid);
if (msgrcv(Private_Chat_Queue, &Server_to_Client, sizeof(Server_to_Client), DEFAULT_MESSAGE_TYPE, MSG_NOERROR | IPC_NOWAIT) != -1)
{
Display_Local_Time();
printf("%s\n", Server_to_Client.mtext);
}
}
void Display_Local_Time(void)
{
time_t tmpcal_ptr;
struct tm *tmp_ptr = NULL;
time(&tmpcal_ptr);
tmp_ptr = localtime(&tmpcal_ptr);
printf("%d-%02d-%02d ", (1900 + tmp_ptr->tm_year), (1 + tmp_ptr->tm_mon), tmp_ptr->tm_mday);
printf("%02d:%02d:%02d\n", tmp_ptr->tm_hour, tmp_ptr->tm_min, tmp_ptr->tm_sec);
}
void Private_Chat_Filter_By_Name(char *client_message)
{
int offset;
char buffer[2];
char client_name_buffer[MAX_CLIENT_NAME_LENGTH] = {0};
if (strncmp(client_message, PRIVATE_MESSAGE_HEADER, strlen(PRIVATE_MESSAGE_HEADER)) == 0)
{
printf("\n");
offset = strlen(PRIVATE_MESSAGE_HEADER);
while (*(client_message + offset) == ' ')
offset += 1;
while (offset < strlen(client_message) && (*(client_message + offset) != ' '))
{
sprintf(buffer, "%c", *(client_message + offset));
strcat(client_name_buffer, buffer);
offset++;
}
while (*(client_message + offset) == ' ')
offset += 1;
strcpy(Client_to_Server.mtext, (client_message + offset));
strcpy(Client_to_Server.target_name, client_name_buffer);
}
else
{
strcpy(Client_to_Server.target_name, BROADCAST_MESSAGE_MARKER);
strcpy(Client_to_Server.mtext, client_message);
}
}
void Write_Client_Data(int process, int child_pid)
{
Public_Chat_Queue = Open_Public_Chat_Queue(PUBLIC_CHAT_QUEUE_ID);
if (strcmp(Client_to_Server.mtext, CLIENT_QUIT_COMMAND) == 0)
Client_to_Server.mtype = CLIENT_QUIT_MESSAGE_TYPE;
if ((msgsnd(Public_Chat_Queue, &Client_to_Server, sizeof(Client_to_Server), IPC_NOWAIT)) == -1)
printf("Failed to write client data\n");
usleep(200000);
if (Client_to_Server.mtype == CLIENT_QUIT_MESSAGE_TYPE)
{
printf("Client_%d exit\n", Client_to_Server.client_pid);
if (process)
{
kill(child_pid, SIGSTOP);
usleep(1000);
printf("Child process has been stopped\n");
}
exit(0);
}
}
void Server_Signal_Handler(int num)
{
printf("\nServer is exiting...\n");
msgctl(Public_Chat_Queue, IPC_RMID, NULL);
printf("Removed Public_Chat_Queue:%d\nSee you again\n", Public_Chat_Queue);
exit(0);
}
这段代码实现了一系列用于消息队列通信的函数,这些函数用于存储客户端信息、发送公共或私有消息、读取客户端数据等。下面是每个函数的详细解释:
- Open_Public_Chat_Queue函数:通过msgget函数打开或创建一个消息队列,并返回队列的标识符,如果操作失败,输出错误信息并退出程序;
- Store_Client_Info函数:将客户端信息存储到全局数组中,包括客户端PID和名称,输出记录客户端存储成功的信息和当前客户端数量;
- Delete_Client_Info函数:在客户端退出时,从客户端信息数组中删除相应的客户端,用于清理无效的客户端信息;
- Send_Public_Or_Private_Message函数:根据消息的目标类型,向相应的消息队列发送消息,处理新客户端进入和退出聊天室的情况,并向所有客户端发送相应的广播消息;
- Read_Client_Data函数:从服务器接收消息队列读取消息,显示消息的本地时间和内容;
- Display_Local_Time函数:获取当前本地时间,并以格式化的方式输出
- Private_Chat_Filter_By_Name函数:根据客户端发送的消息判断消息类型,提取私聊消息的目标客户端名称和消息内容;
- Write_Client_Data函数:向公共消息队列写入客户端数据,当客户端发送退出命令时,将相应信息写入消息队列,并停止子进程(如果有);
- Server_Signal_Handler函数:处理服务器接收到的信号,用于清理并退出程序,移除公共消息队列。
server.c
#include "msg_queue.h"
int main()
{
Client_Count = 0;
New_Client_Flag = 0;
if (signal(SIGINT, &Server_Signal_Handler) == SIG_ERR)
{
perror("signal");
exit(1);
}
Public_Chat_Queue = Open_Public_Chat_Queue(PUBLIC_CHAT_QUEUE_ID);
while (1)
{
if (msgrcv(Public_Chat_Queue, &Client_to_Server, sizeof(Client_to_Server), 0, MSG_NOERROR | IPC_NOWAIT) != -1)
{
printf("Client Pid: %d\n", Client_to_Server.client_pid);
printf("Client Message: %s", Client_to_Server.mtext);
if (Client_to_Server.mtype == IS_NEW_CLIENT_MESSAGE_TYPE)
{
printf("New client!\n");
New_Client_Flag = 1;
Store_Client_Info();
}
if (Client_to_Server.mtype == CLIENT_QUIT_MESSAGE_TYPE)
{
msgctl(Client_to_Server.client_pid, IPC_RMID, NULL);
Delete_Client_Info();
Client_Count--;
Quit_Flag = 1;
printf("Closed Client_%d Private Chat Queue\n", Client_to_Server.client_pid);
printf("Client Count: %d\n\n", Client_Count);
}
Send_Public_Or_Private_Message();
}
else
{
if (errno != ENOMSG)
{
perror("msgrcv");
exit(1);
}
}
}
return 0;
}
这段代码实现了一个简单的服务端程序,使用消息队列进行客户端之间的通信。
- 初始化全局变量:初始化客户端数量和新客户端标志。
- 注册信号处理函数:注册信号处理函数,用于处理服务器收到的 SIGINT 信号,一般是通过键盘输入 Ctrl+C 触发。
- 打开公共消息队列:使用 Open_Public_Chat_Queue 函数打开或创建公共消息队列,并获取队列的标识符。
- 主循环:在主循环中使用 msgrcv 函数等待接收客户端发送的消息。当接收到消息时,根据消息类型进行相应的处理。
- 处理新客户端消息:如果接收到的消息类型为新客户端消息,设置新客户端标志,然后调用 Store_Client_Info 函数存储客户端信息。
- 处理客户端退出消息:如果接收到的消息类型为客户端退出消息,关闭该客户端的私有消息队列,从存储的客户端信息中删除,并更新客户端数量和退出标志。
- 发送公共或私有消息:调用 Send_Public_Or_Private_Message 函数处理并发送公共或私有消息。
- 处理没有消息的情况:如果没有消息,检查错误码是否为 ENOMSG,如果不是则输出错误信息并退出。
client.c
#include "msg_queue.h"
int main()
{
pid_t pid;
server_pid = getpid();
printf("Enter your username: ");
scanf("%s", Client_to_Server.client_name);
getchar();
Client_to_Server.client_pid = server_pid;
Client_to_Server.mtype = IS_NEW_CLIENT_MESSAGE_TYPE;
strcpy(Client_to_Server.target_name, BROADCAST_MESSAGE_MARKER);
Write_Client_Data(!IS_SERVER_PARENT, -1);
Read_Client_Data();
if ((pid = fork()) < 0)
{
perror("fork");
exit(1);
}
else if (pid > 0)
{
while (1)
{
fgets(Client_to_Server.mtext, 60, stdin);
Client_to_Server.mtype = DEFAULT_MESSAGE_TYPE;
Client_to_Server.client_pid = getpid();
Private_Chat_Filter_By_Name(Client_to_Server.mtext);
Write_Client_Data(IS_SERVER_PARENT, pid);
}
}
else
{
while (1)
{
Read_Client_Data();
}
}
return 0;
}
这段代码实现了一个简单的客户端程序,用于与服务器进行消息队列通信。
- 获取服务器的进程ID:获取当前客户端程序的进程ID,并将其存储到全局变量 server_pid 中;
- 输入用户名:提示用户输入用户名,将用户名存储到客户端发送消息的数据结构中;
- 设置客户端信息并发送新客户端消息给服务器:设置客户端信息,包括进程ID、消息类型为新客户端消息,并将消息发送给服务器;
- 读取并显示服务器发送的消息:调用Read_Client_Data函数,读取并显示服务器发送的消息。这里主要是为了显示欢迎消息;
- 创建子进程:使用 fork 创建一个子进程,以便实现同时读取用户输入和接收服务器消息的功能;
- 父进程:在父进程中,使用fgets从标准输入读取用户输入的消息。设置消息类型为默认消息类型,客户端进程ID为当前进程ID。调用Private_Chat_Filter_By_Name函数处理消息,将消息发送给服务器;
- 子进程(客户端接收消息):在子进程中,不断调用Read_Client_Data函数,用于接收和显示服务器发送的消息;
- 返回:完成程序执行,返回0表示成功执行。
实现效果
编译可执行文件
启动服务端
启动客户端
首先启动第一个client,clinet端显示和server端显示如下:
下面启动第二个client,clinet端显示和server端显示如下:
消息通信
源码地址
本项目的源文件已经上传至Gitee仓库,地址为net: 网络程序课程作业源码。其中包括了所有的头文件、C语言代码文件和Makefile文件。由于Python代码的简洁性和可移植性,我又实现了上述功能的Python版本。该项目已经在Ubuntu20.04上进行了测试,只需要安装gcc、make即可运行成功运行。
总结
在本本中,我们探讨了并发网络编程的主题,并通过实现聊天室应用程序来加深对并发编程的理解,分别使用了epoll和消息队列两种技术来实现聊天室的功能。
首先使用epoll实现了聊天室。epoll是一种高效的I/O复用技术,它可以同时监听多个文件描述符,并在有事件发生时通知程序进行处理。在聊天室中,我们使用epoll来监听客户端的连接请求和接收到的消息。通过使用epoll,我们能够高效地处理大量的并发连接,提高了聊天室的性能和响应速度。
之后,我们还使用消息队列实现了聊天室。消息队列是一种进程间通信的方式,它允许不同的进程之间发送和接收消息。在聊天室中,我们使用消息队列来传递用户发送的消息。通过使用消息队列,我们能够实现不同客户端之间的实时通信,并且能够处理并发的消息发送和接收。
通过实现这两个聊天室应用程序,我们对并发网络编程有了更深入的了解。并发网络编程是一个复杂而重要的领域,它涉及到多线程、多进程、I/O复用和进程间通信等概念和技术。通过本次大作业的实践,我学会了如何使用epoll和消息队列来实现并发网络应用程序,并体会到了并发编程的重要性和挑战性。
最后,我要感谢本门课程的孟老师。孟老师的教学方法生动有趣,深入浅出,风格轻松,让我们能够更好地理解和掌握并发网络编程的知识。您还给予了我们很多实践的机会,让我们能够通过实际的项目来应用所学的知识。感谢您的辛勤付出和耐心指导,让我们在本门课程中取得了很大的进步。