网络并发处理

引入

本文将讨论网络并发处理中的两个重要组成部分: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处理机制。

  1. 创建epoll实例:通过epoll_create创建一个epoll实例。

  2. 添加文件描述符:通过epoll_ctl将需要关注的文件描述符添加到epoll实例中,并设置关注的事件类型。

  3. 等待事件:通过epoll_wait等待文件描述符上的事件发生,此时程序会被阻塞。

  4. 处理事件:当文件描述符上的事件发生时,epoll_wait返回,程序开始执行注册的回调函数,处理相应的事件。

  5. 重复等待:重复步骤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. 1-2行是头文件的保护宏定义,防止头文件的重复包含;
  2. 3-11行是导入必须的头文件,包含了一些基本的系统调用,网络编程相关的函数,以及 epoll相关的函数和数据结构;
  3. 13-19行是定义了一个简单的链表结构ListNode,以及一个链表结构List,并创建了一个全局的链表实例clientsList,用于存储连接到服务器的客户端的信息;
  4. 20-28行是一些常量定义,包括服务器的IP地址和端口号、epoll实例的大小、缓冲区大小、欢迎信息、聊天消息格式、退出指令、注意事项信息以及用户名长度;
  5. 29-31行定义了三个函数,setNonBlocking是将套接字设置为非阻塞模式、addFd是向epoll实例中添加文件描述符、sendBroadcastMessage是向所有连接的客户端广播消息;
  6. 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实例中添加文件描述符、以及向所有连接的客户端广播消息。

  1. setNonBlocking函数:通过fcntl函数将指定的套接字文件描述符设置为非阻塞模式。它使用位运算和O_NONBLOCK标志,将原来的文件描述符标志位与O_NONBLOCK进行OR操作,从而设置非阻塞模式。
  2. addFd函数:用于向epoll实例中添加文件描述符,并根据参数enableEt决定是否使用边缘触发模式(ET)。它首先设置struct epoll_event结构体,然后通过epoll_ctl函数将文件描述符添加到epoll实例中。如果enableEt为真,表示启用边缘触发模式,将 EPOLLET加入关注的事件类型中。
  3. 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;
}

 这段代码实现了聊天室的服务端。

  1. 4-7行通过结构体 serverAddr 初始化服务器的地址,包括地址族(IPv4)、端口号(使用预定义的 SERVER_PORT)和IP地址(使用预定义的 SERVER_IP)
  2. 8-10行创建一个监听套接字listener,并检查是否创建成功。如果创建失败,打印错误信息并退出程序;
  3. 11-14行将监听套接字绑定到指定的地址和端口,如果绑定失败,打印错误信息并退出程序;
  4. 15-17行启动监听模式,允许最多5个连接处于等待状态。如果监听失败,打印错误信息并退出程序。
  5. 18-20创建epoll实例,将监听套接字添加到epoll实例中,启用边缘触发模式。如果创建epoll实例失败,打印错误信息并退出程序;
  6. 从while部分开始进入事件处理循环;
  7. while中的for循环遍历发生的事件,如果事件对应的文件描述符是监听套接字,表示有新的客户端连接,执行相应的处理。否则,表示已连接的客户端有消息到达,执行消息广播操作;
  8. for循环中if体首先接受新的客户端连接,并获取客户端的地址信息,如果连接失败,打印错误信息并退出程序;
  9. 之后,将新客户端添加到epoll实例和链表;
  10. 向新客户端发送欢迎消息;
  11. 最后处理已连接客户端的消息广播,关闭监听套接字和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. 第1部分通过结构体serverAddr初始化服务器的地址,包括地址族(IPv4)、端口号(使用预定义的 SERVER_PORT)和IP地址(使用预定义的 SERVER_IP);
  2. 第2部分创建套接字并连接到服务器。如果创建套接字或连接失败,打印错误信息并退出程序;
  3. 第3部分创建一个管道pipeFd,用于父子进程之间的通信;
  4. 第4部分创建epoll实例,将套接字和管道的读端添加到epoll实例中,启用边缘触发模式。如果创建epoll实例失败,打印错误信息并退出程序;
  5. 第5部分创建子进程并进行聊天,子进程负责从标准输入读取用户输入,并通过管道发送给父进程;父进程负责监听套接字和管道,处理收到的消息;
  6. 第6部分子进程读取用户输入并发送给父进程,子进程通过标准输入获取用户输入,如果用户输入为 "exit",则退出聊天室,否则将用户输入写入管道;
  7. 第7部分父进程监听套接字和管道,处理收到的消息,如果事件对应的文件描述符是套接字,表示从服务端接收到消息,处理服务端的消息;如果事件对应的文件描述符是管道的读端,表示从管道接收到消息,处理从子进程传来的用户输入,然后将消息发送给服务端;
  8. 第8部分处理从服务端接收到的消息,处理从服务器接收到的消息,如果返回值为 0,表示服务器关闭连接,关闭套接字并退出聊天室。否则,打印接收到的消息;
  9. 第9部分处理从管道接收到的消息,如果返回值为 0,表示子进程已经关闭,退出聊天室。否则,将消息发送给服务器;
  10. 最后,关闭套接字和管道,如果是父进程,关闭管道的读端和套接字,如果是子进程,关闭管道的写端。

实现效果

编译可执行文件

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 的头文件,其中包含了一些宏定义、结构体定义以及函数声明。这些内容主要用于实现一个简单的进程间通信系统,通过消息队列进行客户端和服务器之间的通信。

  1. 宏定义:防止头文件重复包含;
  2. 包含一系列系统调用和标准库的头文件,为后续使用提供必要的函数和类型定义;
  3. 定义一些常量和标志:消息类型、队列ID、最大客户端数量、客户端名称最大长度等;
  4. 消息缓冲区结构体定义:定义了一个结构体message_buffer用于存储消息的各个字段,包括消息类型、客户端进程ID、客户端名称、目标名称以及消息文本;
  5. 全局变量和数组定义:定义了一系列全局变量,用于存储程序运行时的状态和数据。其中包括标志位、客户端数量、消息队列ID、进程ID、消息缓冲区以及存储客户端PID和名称的数组;
  6. 函数声明:声明了一系列函数,用于执行各种与消息队列相关的操作。函数的具体实现可能在其他源文件中定义。

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);
}

这段代码实现了一系列用于消息队列通信的函数,这些函数用于存储客户端信息、发送公共或私有消息、读取客户端数据等。下面是每个函数的详细解释:

  1. Open_Public_Chat_Queue函数:通过msgget函数打开或创建一个消息队列,并返回队列的标识符,如果操作失败,输出错误信息并退出程序;
  2. Store_Client_Info函数:将客户端信息存储到全局数组中,包括客户端PID和名称,输出记录客户端存储成功的信息和当前客户端数量;
  3. Delete_Client_Info函数:在客户端退出时,从客户端信息数组中删除相应的客户端,用于清理无效的客户端信息;
  4. Send_Public_Or_Private_Message函数:根据消息的目标类型,向相应的消息队列发送消息,处理新客户端进入和退出聊天室的情况,并向所有客户端发送相应的广播消息;
  5. Read_Client_Data函数:从服务器接收消息队列读取消息,显示消息的本地时间和内容;
  6. Display_Local_Time函数:获取当前本地时间,并以格式化的方式输出
  7. Private_Chat_Filter_By_Name函数:根据客户端发送的消息判断消息类型,提取私聊消息的目标客户端名称和消息内容;
  8. Write_Client_Data函数:向公共消息队列写入客户端数据,当客户端发送退出命令时,将相应信息写入消息队列,并停止子进程(如果有);
  9. 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;
}

 这段代码实现了一个简单的服务端程序,使用消息队列进行客户端之间的通信。

  1. 初始化全局变量:初始化客户端数量和新客户端标志。
  2. 注册信号处理函数:注册信号处理函数,用于处理服务器收到的 SIGINT 信号,一般是通过键盘输入 Ctrl+C 触发。
  3. 打开公共消息队列:使用 Open_Public_Chat_Queue 函数打开或创建公共消息队列,并获取队列的标识符。
  4. 主循环:在主循环中使用 msgrcv 函数等待接收客户端发送的消息。当接收到消息时,根据消息类型进行相应的处理。
  5. 处理新客户端消息:如果接收到的消息类型为新客户端消息,设置新客户端标志,然后调用 Store_Client_Info 函数存储客户端信息。
  6. 处理客户端退出消息:如果接收到的消息类型为客户端退出消息,关闭该客户端的私有消息队列,从存储的客户端信息中删除,并更新客户端数量和退出标志。
  7. 发送公共或私有消息:调用 Send_Public_Or_Private_Message 函数处理并发送公共或私有消息。
  8. 处理没有消息的情况:如果没有消息,检查错误码是否为 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;
}

这段代码实现了一个简单的客户端程序,用于与服务器进行消息队列通信。

  1. 获取服务器的进程ID:获取当前客户端程序的进程ID,并将其存储到全局变量 server_pid 中;
  2. 输入用户名:提示用户输入用户名,将用户名存储到客户端发送消息的数据结构中;
  3. 设置客户端信息并发送新客户端消息给服务器:设置客户端信息,包括进程ID、消息类型为新客户端消息,并将消息发送给服务器;
  4. 读取并显示服务器发送的消息:调用Read_Client_Data函数,读取并显示服务器发送的消息。这里主要是为了显示欢迎消息;
  5. 创建子进程:使用 fork 创建一个子进程,以便实现同时读取用户输入和接收服务器消息的功能;
  6. 父进程:在父进程中,使用fgets从标准输入读取用户输入的消息。设置消息类型为默认消息类型,客户端进程ID为当前进程ID。调用Private_Chat_Filter_By_Name函数处理消息,将消息发送给服务器;
  7. 子进程(客户端接收消息):在子进程中,不断调用Read_Client_Data函数,用于接收和显示服务器发送的消息;
  8. 返回:完成程序执行,返回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和消息队列来实现并发网络应用程序,并体会到了并发编程的重要性和挑战性。

最后,我要感谢本门课程的孟老师。孟老师的教学方法生动有趣,深入浅出,风格轻松,让我们能够更好地理解和掌握并发网络编程的知识。您还给予了我们很多实践的机会,让我们能够通过实际的项目来应用所学的知识。感谢您的辛勤付出和耐心指导,让我们在本门课程中取得了很大的进步。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值