深入解析 epoll 并实现聊天室

深入解析 epoll 并实现聊天室

1.epoll简介

在Linux系统下,epoll 是一种高效的 I/O 事件通知机制,通常用于并发编程和网络编程。它允许程序监视多个文件描述符(File Descriptor,简称 FD)上的 I/O 事件,并在这些文件描述符就绪时通知应用程序,从而实现高效的 I/O 多路复用。

epoll 能够处理大量的并发连接而不会消耗太多系统资源,因为它采用了事件驱动的模式,只有当 I/O 事件准备就绪时才会通知应用程序,而不是像传统的阻塞式 I/O 那样需要不断地轮询检查文件描述符状态。

​ 在通信领域,特别是在构建聊天室等即时通讯应用程序时,epoll 在管理和处理大量并发连接方面显得非常重要。epoll 是用于管理大量文件描述符的事件机制,特别适用于需要高效处理大量并发连接的网络应用程序。epoll 基于事件驱动,利用内核维护的事件表来监视文件描述符。应用程序使用 epoll_ctl() 向内核注册需要监视的文件描述符,并设置所关注的事件类型。内核在文件描述符上有事件发生时通知应用程序,通过 epoll_wait() 返回就绪文件描述符及事件类型,应用程序进行相应处理。

1.1. epoll 的优势
  • 高效性: 在大量并发连接时,epoll 比传统的 selectpoll 更高效,因为它只关注活跃的文件描述符,避免了遍历所有文件描述符的开销。
  • 边缘触发模式: 支持边缘触发模式,只在状态变化时通知应用程序,避免了频繁的通知和处理。
  • 避免遍历:selectpoll 不同,epoll 不需要遍历所有文件描述符,因此在处理大规模并发连接时效率更高。
1.2. 创建 Socket 和 epoll 实例

​ 当将 epoll 与 Socket 编程结合使用时,可以实现高效的 I/O 多路复用,用于管理大量并发连接。以下是将 epoll 与 Socket 编程结合的基本步骤:

创建 epoll 实例

使用 epoll_create() 函数创建一个 epoll 实例。

int epoll_fd = epoll_create(EPOLL_SIZE);
将 Socket 添加到 epoll 实例中

使用 epoll_ctl() 函数将需要监听的 Socket 文件描述符添加到 epoll 实例中。

struct epoll_event event;
event.events = EPOLLIN; // 设置事件类型为可读
event.data.fd = server_socket; // 监听服务器端 Socket

if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) < 0) {
    perror("epoll_ctl failed");
    exit(EXIT_FAILURE);
}
等待事件发生

使用 epoll_wait() 函数等待就绪事件发生。

struct epoll_event events[MAX_EVENTS]; // 存放就绪事件的数组

int ready_fds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (ready_fds < 0) {
    perror("epoll_wait failed");
    exit(EXIT_FAILURE);
}
处理就绪事件

针对每一个就绪事件,执行相应的操作,例如接受连接、处理数据等。

for (int i = 0; i < ready_fds; ++i) {
    if (events[i].data.fd == server_socket) {
        // 处理新连接
        // accept()...
    } else {
        // 处理已连接的 Socket 数据
        // recv() / send()...
    }
}
关闭 epoll 实例

在程序结束时,关闭 epoll 实例对应的文件描述符,释放资源。

close(epoll_fd);

epoll 在 Socket 编程中的应用,通过 epoll_ctl() 将需要监听的 Socket 添加到 epoll 实例中,并使用 epoll_wait() 等待就绪事件。这样可以实现高效的事件驱动 I/O 处理,有效管理和处理大量并发连接,提高网络应用程序的性能。
在一个聊天室应用中,epoll 可以有效地处理大量连接,同时实现即时通讯。例如,当有新消息到达时,服务器可以通过 epoll 发现哪些连接已经准备好接收数据,从而实现高效的消息分发和即时通信。

2.所用知识及分析

​ Socket 编程是一种计算机网络通信的编程技术,允许应用程序在不同计算机之间进行数据传输和通信。它是一种基于套接字(socket)的编程模型,在网络通信中被广泛应用。以下是与 Socket 编程相关的理论知识:

2.1 套接字(Socket)

​ 套接字是一种通信端点,通过网络进行进程间通信的一种机制。它是网络通信的基础,用于在网络上发送和接收数据。

a.套接字类型:
  • 流式套接字(Stream Socket): 使用 TCP 协议,在网络中实现可靠的双向通信。TCP 提供面向连接的、可靠的数据流传输。
  • 数据报套接字(Datagram Socket): 使用 UDP 协议,以数据包形式在网络上传输数据。UDP 提供无连接的、不可靠的数据传输。
b. 套接字编程基本步骤
  • 创建套接字: 使用 socket() 函数创建一个套接字,指定协议族(如 AF_INET 表示 IPv4 地址族)、套接字类型(如 SOCK_STREAM 表示流式套接字)和协议(通常为 0)。

  • 绑定地址和端口: 使用 bind() 函数将套接字和特定的 IP 地址及端口号绑定。

  • 监听连接请求(仅适用于服务器): 对于服务器端,使用 listen() 函数开始监听来自客户端的连接请求。

  • 连接到服务器(仅适用于客户端): 对于客户端,使用 connect() 函数连接到服务器的套接字。

  • 发送和接收数据: 使用 send()recv() 函数发送和接收数据,分别用于发送和接收数据流或数据报。

  • 关闭套接字: 使用 close()closesocket() 函数关闭套接字,释放资源。

2.2 TCP 和 UDP
  • TCP(Transmission Control Protocol):
    • 提供可靠的、面向连接的数据传输。
    • 通过三次握手建立连接,确保数据可靠性和顺序传输。
    • 适用于需要可靠传输和数据完整性的应用,如文件传输、网页访问等。
  • UDP(User Datagram Protocol):
    • 提供不可靠的、无连接的数据传输。
    • 无需建立连接,直接发送数据报。
    • 适用于实时性要求高的应用,如视频流、音频通话等。
  • TCP 数据传输流程:
    1. 建立连接:客户端和服务器通过三次握手建立连接。
    2. 数据传输:数据通过 TCP 连接传输,保证可靠性和顺序性。
    3. 断开连接:通过四次挥手断开连接。
  • UDP 数据传输流程:
    1. 直接发送数据包,无需建立连接。
    2. 传输不保证可靠性,可能会有数据丢失或乱序。
    3. 适用于实时性要求高、对数据完整性要求不高的场景。

​ Socket 编程是实现网络通信的重要手段,通过使用合适的协议和函数,可以实现可靠的数据传输和通信。 TCP 提供可靠的数据传输,适用于需要数据完整性和可靠性的场景,而 UDP 则适用于对实时性要求较高、对数据完整性要求不高的场景。

2.3 多线程编程

​ 在 Linux 下,epoll 是一种高效的 I/O 事件通知机制,常用于实现高性能的网络服务器。epoll 支持非阻塞 I/O 和多路复用,能够有效处理大量连接和 I/O 操作,其结合多线程可以进一步提升性能。 epoll 多线程相关的理论知识:

  • epoll 是 Linux 下的多路复用机制之一,与 selectpoll 相比,epoll 在大规模连接时具有更好的性能和扩展性。

  • ​ 多线程可以进一步提高 I/O 并发处理能力,配合 epoll 实现多个线程同时处理大量的 I/O 事件。

  • ​ 可以将多个文件描述符分配给不同的线程处理,以实现更高的并发处理能力。

  • ​ 在多线程环境下,每个线程都可以拥有自己的 epoll 实例。

  • ​ 文件描述符可以按照某种规则(如哈希、轮询等)分配给不同的 epoll 实例或不同的线程。

  • ​ 每个线程使用独立的事件循环,调用 epoll_wait() 等待事件的发生,并处理所监视的文件描述符上的 I/O 事件。

  • epoll 机制下,I/O 操作是非阻塞的,可以避免因为 I/O 操作阻塞导致的性能下降。

    epoll 在多线程环境下能够提高系统的并发处理能力和性能,但需要注意线程安全性和同步机制的设计,以及合理地分配文件描述符给不同的线程,避免负载不均和性能下降的情况发生。

2.4 字符串处理
  • 使用字符串处理函数如 fgets()strcmp()strlen() 处理用户输入的消息和对消息进行判断。
  • 通过字符串比较检查是否为退出指令。
  • 使用字符串处理函数对用户输入的消息进行处理和格式化。
2.5 系统函数和错误处理
  • 使用函数如 exit()printf()closesocket()WSACleanup() 等进行程序的管理和错误处理。
  • 使用 WSAGetLastError() 获取套接字错误信息。

3.基于 epoll 的聊天室服务端实现

3.1. createServerSocket() 函数
// 创建服务器 Socket
int createServerSocket(int port) {
    int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddress.sin_port = htons(port);

    int bind_result = bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
    if (bind_result < 0) {
        perror("Error binding socket");
        exit(EXIT_FAILURE);
    }

    int listen_result = listen(serverSocket, SOMAXCONN);
    if (listen_result < 0) {
        perror("Error listening on socket");
        exit(EXIT_FAILURE);
    }

    return serverSocket;
}

createServerSocket() 函数用于创建服务器的 Socket。首先使用 socket() 函数创建一个 TCP Socket。然后设置服务器地址信息,包括 IP 地址和端口号。使用 bind() 将 Socket 与指定地址和端口绑定。listen() 函数用于启动监听,开始接受客户端连接请求。如果出现错误,函数会打印错误信息并退出程序。

3.2. createEpollInstance() 函数
// 创建 epoll 实例
int createEpollInstance() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) {
        perror("Error creating epoll instance");
        exit(EXIT_FAILURE);
    }
    return epoll_fd;
}

createEpollInstance() 函数用于创建 epoll 实例。使用 epoll_create1() 函数创建一个 epoll 实例。如果创建失败,函数会打印错误信息并退出程序。

3.3. addSocketToEpoll() 函数
// 将 Socket 添加到 epoll 实例中
void addSocketToEpoll(int epoll_fd, int socket_fd) {
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = socket_fd;
    int epoll_ctl_result = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);
    if (epoll_ctl_result < 0) {
        perror("Error adding socket to epoll");
        exit(EXIT_FAILURE);
    }
}

addSocketToEpoll() 函数用于将指定的 Socket 添加到 epoll 实例中进行监听。首先,设置 epoll_event 结构体 event,指定所关注的事件类型为可读。使用 epoll_ctl() 函数将指定的 Socket 添加到 epoll 实例中。如果添加失败,函数会打印错误信息并退出程序。

3.4. acceptClientConnection() 函数
// 接受客户端连接
int acceptClientConnection(int serverSocket) {
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen = sizeof(clientAddr);
    int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
    if (clientSocket < 0) {
        perror("Error accepting connection");
        exit(EXIT_FAILURE);
    }
    return clientSocket;
}

acceptClientConnection() 函数用于接受客户端的连接请求,并返回客户端的套接字描述符。使用 accept() 函数从 serverSocket 中接受连接,并返回与客户端通信的新的 Socket。如果出现错误,函数会打印错误信息并退出程序。

3.5. main() 函数
int main() {
    int serverSocket = createServerSocket(9090);
    int epoll_fd = createEpollInstance();
    addSocketToEpoll(epoll_fd, serverSocket);
    std::map<int, ClientInfo> clients;
    while (true) {
        struct epoll_event events[MAX_EVENTS];
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_events < 0) {
            perror("Error in epoll_wait");
            break;
        }
        for (int i = 0; i < num_events; ++i) {
            int current_fd = events[i].data.fd;
            // 消息处理
        }
    }
    close(epoll_fd);
    close(serverSocket);
    return 0;
}

main() 函数是整个程序的主入口,包含了整个服务端的主逻辑。首先创建服务器 Socket,并添加到 epoll 实例中进行监听。进入一个无限循环,使用 epoll_wait() 监听事件。对每个就绪事件进行处理,接受新连接或处理已连接的客户端的消息通信。在程序退出之前,关闭了 epoll 实例和服务器 Socket。

3.6服务端完整代码
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <map>

const int MAX_EVENTS = 1024;

struct ClientInfo {
    int clientSocket;
    std::string name;
};

int createServerSocket(int port) {
    int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddress.sin_port = htons(port);

    int bind_result = bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
    if (bind_result < 0) {
        perror("Error binding socket");
        exit(EXIT_FAILURE);
    }

    int listen_result = listen(serverSocket, SOMAXCONN);
    if (listen_result < 0) {
        perror("Error listening on socket");
        exit(EXIT_FAILURE);
    }

    return serverSocket;
}

int createEpollInstance() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) {
        perror("Error creating epoll instance");
        exit(EXIT_FAILURE);
    }

    return epoll_fd;
}

void addSocketToEpoll(int epoll_fd, int socket_fd) {
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = socket_fd;

    int epoll_ctl_result = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);
    if (epoll_ctl_result < 0) {
        perror("Error adding socket to epoll");
        exit(EXIT_FAILURE);
    }
}

int acceptClientConnection(int serverSocket) {
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen = sizeof(clientAddr);
    int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
    if (clientSocket < 0) {
        perror("Error accepting connection");
        exit(EXIT_FAILURE);
    }

    return clientSocket;
}

int main() {
    int serverSocket = createServerSocket(9090);
    int epoll_fd = createEpollInstance();

    addSocketToEpoll(epoll_fd, serverSocket);

    std::map<int, ClientInfo> clients;

    while (true) {
        struct epoll_event events[MAX_EVENTS];
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_events < 0) {
            perror("Error in epoll_wait");
            break;
        }

        for (int i = 0; i < num_events; ++i) {
            int current_fd = events[i].data.fd;

            if (current_fd == serverSocket) {
                int clientSocket = acceptClientConnection(serverSocket);
                addSocketToEpoll(epoll_fd, clientSocket);

                ClientInfo client;
                client.clientSocket = clientSocket;
                client.name = "";
                clients[clientSocket] = client;
            }
            else {
                char buffer[1024];
                int bytes_received = read(current_fd, buffer, sizeof(buffer));
                if (bytes_received < 0) {
                    perror("Error reading from client");
                    close(current_fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, current_fd, 0);
                    clients.erase(current_fd);
                }
                else if (bytes_received == 0) {
                    close(current_fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, current_fd, 0);
                    clients.erase(current_fd);
                }
                else {
                    std::string msg(buffer, bytes_received);
                    if (clients[current_fd].name.empty()) {
                        clients[current_fd].name = msg;
                    }
                    else {
                        std::string name = clients[current_fd].name;
                        for (auto& client : clients) {
                            if (client.first != current_fd) {
                                std::string message = "[" + name + "]: " + msg;
                                write(client.first, message.c_str(), message.size());
                            }
                        }
                    }
                }
            }
        }
    }

    close(epoll_fd);
    close(serverSocket);

    return 0;
}

4.基于socket的windows客户端代码实现

4.1. InitializeSocket()
void InitializeSocket() {
    // Winsock 初始化
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;
    wVersionRequested = MAKEWORD(2, 2);
    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) {
        exit(-1);
    }
    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
        WSACleanup();
        exit(-1);
    }
    // 创建套接字
    hSocket = socket(AF_INET, SOCK_STREAM, 0);

    if (hSocket == INVALID_SOCKET) {
        printf("套接字创建失败\n");
        WSACleanup();
        exit(-1);
    }
}

InitializeSocket() 函数主要负责 Winsock 的初始化和套接字的创建。使用 WSAStartup() 初始化 Winsock,检查是否成功,若失败则退出程序。创建 TCP 套接字 (SOCK_STREAM)。如果套接字创建失败,则输出错误信息并退出程序。

4.2. ConnectToServer()
void ConnectToServer() {
    // 服务器地址设定
    SOCKADDR_IN serverAddress;
    memset(&serverAddress, 0, sizeof(serverAddress));
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(服务器端口号);
    inet_pton(AF_INET, "服务器ip", &serverAddress.sin_addr);
    // 连接服务器
    if (connect(hSocket, (SOCKADDR*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
        printf("连接错误 : %d\n", WSAGetLastError());
        closesocket(hSocket);
        WSACleanup();
        exit(-1);
    }
}

ConnectToServer() 函数用于连接服务器。设置服务器地址信息。使用 connect() 连接指定 IP 地址和端口号的服务器。如果连接出现错误,则输出错误信息并退出程序。

4.3. GetUsernameAndSend()
void GetUsernameAndSend() {
    printf("epoll实现聊天室,请输入你的用户名:\n");
    fgets(username, BUF_SIZE, stdin);
    username[strlen(username) - 1] = '\0';
    send(hSocket, username, strlen(username), 0);
}

GetUsernameAndSend() 函数用于获取用户输入的用户名并发送给服务器。打印提示信息要求输入用户名。使用 fgets() 获取用户输入的用户名,并发送给服务器端。

4.4. SendMessageThread()ReceiveMessageThread()
DWORD WINAPI SendMessageThread(void* arg) {
    while (1) {
        fgets(messageBuffer, BUF_SIZE, stdin);
        if (strcmp(messageBuffer, "quit\n") == 0 || strcmp(messageBuffer, "QUIT\n") == 0) {
            closesocket(hSocket);
            exit(0);
        }
        send(hSocket, messageBuffer, strlen(messageBuffer), 0);
    }
    return 0;
}

SendMessageThread() 函数是一个线程函数,通过 CreateThread() 创建一个新线程用于发送消息。该函数使用无限循环 while (1),持续接收用户在控制台输入的消息。使用 fgets() 从标准输入中获取用户输入的消息,并存储在 messageBuffer 中。如果用户输入了 “quit” 或 “QUIT”,则关闭套接字并退出程序。否则,使用 send() 函数将消息发送给服务器。

DWORD WINAPI ReceiveMessageThread(void* arg) {
    while (1) {
        int receivedLen = recv(hSocket, messageBuffer, sizeof(messageBuffer) - 1, 0);
        if (receivedLen <= 0) {
            closesocket(hSocket);
            exit(0);
        }
        messageBuffer[receivedLen] = '\0';
        if (messageBuffer[0] == '[') {
            printf("%s", messageBuffer);
        }
        else {
            printf("[%s]: %s\n", username, messageBuffer);
        }
    }
    return 0;
}

ReceiveMessageThread() 函数是另一个线程函数,用于接收服务器发送的消息。同样使用无限循环 while (1),持续接收服务器发送的消息。使用 recv() 函数从套接字接收消息,并将其存储在 messageBuffer 中。如果接收的消息长度小于等于 0,则关闭套接字并退出程序。接收到消息后,首先将其设为字符串结束符,并检查消息内容的开头字符。

这两个函数构成了聊天客户端的消息发送和接收功能。SendMessageThread() 负责接收用户输入并发送消息,而 ReceiveMessageThread() 负责接收服务器发来的消息并在控制台显示。

4.5. StartChat()
void StartChat() {
    HANDLE sendThread = CreateThread(NULL, 0, SendMessageThread, NULL, 0, NULL);
    HANDLE receiveThread = CreateThread(NULL, 0, ReceiveMessageThread, NULL, 0, NULL);
    WaitForSingleObject(sendThread, INFINITE);
    WaitForSingleObject(receiveThread, INFINITE);
}

StartChat() 函数是聊天开始的函数,用于创建发送消息和接收消息的线程并等待其结束。

4.6客户端完整代码
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <Windows.h>
#include <stdio.h>
#pragma comment(lib,"Ws2_32.lib")
#define BUF_SIZE 1024
char username[BUF_SIZE];
char messageBuffer[BUF_SIZE];
SOCKET hSocket;
void InitializeSocket() {
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;
    wVersionRequested = MAKEWORD(2, 2);
    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) {
        exit(-1);
    }
    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
        WSACleanup();
        exit(-1);
    }
    hSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET) {
        printf("套接字创建失败\n");
        WSACleanup();
        exit(-1);
    }
}

void ConnectToServer() {
    SOCKADDR_IN serverAddress;
    memset(&serverAddress, 0, sizeof(serverAddress));
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(9090);
    inet_pton(AF_INET, "172.26.145.237", &serverAddress.sin_addr);
    if (connect(hSocket, (SOCKADDR*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
        printf("连接错误 : %d\n", WSAGetLastError());
        closesocket(hSocket);
        WSACleanup();
        exit(-1);
    }
}
void GetUsernameAndSend() {
    printf("epoll实现聊天室,请输入你的用户名:\n");
    fgets(username, BUF_SIZE, stdin);
    username[strlen(username) - 1] = '\0';
    send(hSocket, username, strlen(username), 0);
}
DWORD WINAPI SendMessageThread(void* arg) {
    while (1) {
        fgets(messageBuffer, BUF_SIZE, stdin);
        if (strcmp(messageBuffer, "quit\n") == 0 || strcmp(messageBuffer, "QUIT\n") == 0) {
            closesocket(hSocket);
            exit(0);
        }
        send(hSocket, messageBuffer, strlen(messageBuffer), 0);
    }
    return 0;
}
DWORD WINAPI ReceiveMessageThread(void* arg) {
    while (1) {
        int receivedLen = recv(hSocket, messageBuffer, sizeof(messageBuffer) - 1, 0);
        if (receivedLen <= 0) {
            closesocket(hSocket);
            exit(0);
        }
        messageBuffer[receivedLen] = '\0';
        if (messageBuffer[0] == '[') {
            printf("%s", messageBuffer);
        }
        else {
            printf("[%s]: %s\n", username, messageBuffer);
        }
    }
    return 0;
}
void StartChat() {
    HANDLE sendThread = CreateThread(NULL, 0, SendMessageThread, NULL, 0, NULL);
    HANDLE receiveThread = CreateThread(NULL, 0, ReceiveMessageThread, NULL, 0, NULL);
    WaitForSingleObject(sendThread, INFINITE);
    WaitForSingleObject(receiveThread, INFINITE);
}
int main() {
    InitializeSocket();
    ConnectToServer();
    GetUsernameAndSend();
    StartChat();
    closesocket(hSocket);
    WSACleanup();
    return 0;
}

5.聊天室结果展示

在这里插入图片描述
成功实现聊天功能!
项目仓库地址

6.实验总结与结课感悟

​ 开发聊天室涉及多个技术方面,其中 Socket 编程是关键。使用 Socket API 实现了服务器与客户端之间的实时通信,允许多个用户连接并交换消息。通过 Epoll(Linux)技术实现多路复用,提高服务器性能和处理效率。架构设计遵循 Client-Server 模型,服务器负责接收和分发消息,实现消息广播以供群聊使用。
​ 最后,感谢孟宁老师的课程,让我对网络通信有了更深刻的理解。在他的课上,我学到了许多关键概念。在课堂上对WebSocket、Socket API、Linux 内核网络协议栈以及 gRPC 等内容的进行了深入的学习。
​ 本次课程,我学到了 关于 WebSocket 的重要知识,包括如何建立和管理 WebSocket 连接,以及实现实时通信的基本原理。另外,在 Socket API 和 Linux 内核网络协议栈的学习中,老师以深入浅出的方式讲解了网络通信的基本原理和操作,使我对套接字、协议栈等概念有了更清晰的认识。这些知识让我更加熟悉网络通信的工作流程,为未来的网络应用开发打下了坚实的基础。并且了解了 gRPC,这是一个高性能的远程过程调用框架,通过gRPC可以通过protobuf来定义接口来实现通讯。对 gRPC 的学习让我理解了其优势和适用场景,为将来设计和开发分布式系统提供了重要的启示。
​ 特别感谢老师的耐心教导和深入浅出的讲解,这些知识将成为我未来学习和实践的宝贵财富,对我的职业发展有着重要的指导作用。

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值