getchat c语言如何实现?

99ANYc3cd6
预计阅读时长 40 分钟
位置: 首页 C语言 正文

我们将从最基础的模型开始,逐步构建一个功能更完善的版本。

getchat c语言
(图片来源网络,侵删)

核心概念

要实现两个程序之间的实时通信,最直接的方法是使用 网络套接字,我们将采用 客户端/服务器 架构:

  1. 服务器:一个程序,它在一个特定的 IP 地址和端口上“监听”连接请求,它负责接收来自客户端的消息,并将消息广播给所有已连接的客户端。
  2. 客户端:另一个程序,它知道服务器的 IP 地址和端口,主动连接到服务器,客户端可以发送消息到服务器,并接收来自服务器的其他客户端的消息。

对于 C 语言,我们将使用 Berkeley Sockets (BSD Sockets) API,它在几乎所有现代操作系统(如 Linux, macOS, Windows)上都有支持。


第一部分:最简单的模型(一对一聊天)

这个模型中,一个服务器只接受一个客户端的连接,服务器和客户端可以互相发送消息。

服务器端代码 (server.c)

这个服务器会等待一个客户端连接,然后进入一个循环,不断地从客户端接收消息并打印到自己的终端,同时从自己的终端读取并发送给客户端。

getchat c语言
(图片来源网络,侵删)
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int server_fd, client_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字文件描述符
    // AF_INET: IPv4
    // SOCK_STREAM: TCP
    // 0: 自动选择协议
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置套接字选项,允许地址重用
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    // 3. 绑定套接字到端口和地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    address.sin_port = htons(PORT);        // 将端口号从主机字节序转为网络字节序
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 4. 开始监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 5. 接受一个客户端连接
    if ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
    // 6. 通信循环
    while (1) {
        // 接收来自客户端的消息
        int valread = read(client_socket, buffer, BUFFER_SIZE);
        if (valread <= 0) {
            // valread == 0 表示客户端断开连接
            // valread < 0 表示发生错误
            printf("Client disconnected.\n");
            break;
        }
        printf("Client: %s", buffer);
        // 如果收到 "exit",则退出
        if (strncmp(buffer, "exit", 4) == 0) {
            break;
        }
        // 清空缓冲区
        memset(buffer, 0, BUFFER_SIZE);
        // 从服务器终端读取要发送的消息
        printf("Server: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        // 发送消息给客户端
        send(client_socket, buffer, strlen(buffer), 0);
        // 如果服务器输入 "exit",则退出
        if (strncmp(buffer, "exit", 4) == 0) {
            break;
        }
    }
    // 7. 关闭套接字
    close(client_socket);
    close(server_fd);
    return 0;
}

客户端代码 (client.c)

这个客户端连接到服务器,然后进入一个循环,可以发送消息到服务器,并接收来自服务器的消息。

// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main(int argc, char const *argv[]) {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    char server_ip[INET_ADDRSTRLEN];
    // 如果没有提供服务器IP,则默认为本地回环地址 127.0.0.1
    if (argc < 2) {
        strcpy(server_ip, "127.0.0.1");
    } else {
        strcpy(server_ip, argv[1]);
    }
    // 1. 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    // 2. 设置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将IP地址从字符串转换为网络地址格式
    if (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }
    // 3. 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    printf("Connected to server %s:%d\n", server_ip, PORT);
    // 4. 通信循环
    while (1) {
        // 从客户端终端读取要发送的消息
        printf("You: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        // 发送消息到服务器
        send(sock, buffer, strlen(buffer), 0);
        // 如果输入 "exit",则退出
        if (strncmp(buffer, "exit", 4) == 0) {
            break;
        }
        // 清空缓冲区
        memset(buffer, 0, BUFFER_SIZE);
        // 接收来自服务器的消息
        int valread = read(sock, buffer, BUFFER_SIZE);
        if (valread <= 0) {
            printf("Server disconnected.\n");
            break;
        }
        printf("Server: %s", buffer);
    }
    // 5. 关闭套接字
    close(sock);
    return 0;
}

如何编译和运行

  1. 编译: 你需要使用 C 编译器(如 gcc)来编译这两个文件,在 Linux 或 macOS 上,链接网络库:

    # 编译服务器
    gcc server.c -o server
    # 编译客户端
    gcc client.c -o client
  2. 运行

    • 第一步:在一个终端窗口中运行服务器:

      getchat c语言
      (图片来源网络,侵删)
      ./server

      你会看到 "Server listening on port 8080..." 的提示。

    • 第二步:在另一个终端窗口中运行客户端,如果你的服务器在同一台机器上,使用 0.0.1

      ./client 127.0.0.1

      你会看到 "Connected to server..." 的提示。

    • 第三步:现在你可以在两个终端里互相发送消息了,输入一行文字按回车,另一端就会收到,输入 exit 可以退出程序。


第二部分:改进模型(多客户端聊天室)

上面的程序只能支持两个人聊天,一个真正的 "getchat" 应该允许多个客户端加入一个公共聊天室,这需要服务器能够处理多个连接。

主要修改点(服务器端)

  1. 使用 select 系统调用select 是一种 I/O 多路复用技术,它允许我们监视多个文件描述符(套接字),看它们中的任何一个是否准备好进行读、写或异常处理,这样,服务器就可以在一个循环中处理所有客户端的连接,而不需要为每个客户端创建一个新线程。

  2. 管理客户端列表:服务器需要维护一个列表,记录所有已连接的客户端套接字。

服务器端代码 (multi_server.c)

// multi_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_CLIENTS 10
int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    char *welcome_message = "Welcome to the chat room!\n";
    // 用于 select 的文件描述符集合
    fd_set read_fds;
    int max_sd;
    // 存储所有客户端套接字的数组
    int client_sockets[MAX_CLIENTS];
    for (int i = 0; i < MAX_CLIENTS; i++) {
        client_sockets[i] = 0;
    }
    // 1. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 2. 绑定套接字
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 3. 监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Chat room server listening on port %d...\n", PORT);
    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&read_fds);
        // 添加主套接字到集合
        FD_SET(server_fd, &read_fds);
        max_sd = server_fd;
        // 添加所有客户端套接字到集合
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];
            if (sd > 0) {
                FD_SET(sd, &read_fds);
            }
            if (sd > max_sd) {
                max_sd = sd;
            }
        }
        // 等待活动套接字
        // select 会阻塞,直到有套接字准备好或超时
        if (select(max_sd + 1, &read_fds, NULL, NULL, NULL) < 0) {
            perror("select error");
            exit(EXIT_FAILURE);
        }
        // 检查是否有新的连接请求
        if (FD_ISSET(server_fd, &read_fds)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            // 将新的客户端套接字添加到数组
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = new_socket;
                    printf("New connection from %s:%d, socket fd is %d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port), new_socket);
                    send(new_socket, welcome_message, strlen(welcome_message), 0);
                    break;
                }
            }
        }
        // 检查所有客户端是否有数据可读
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];
            if (FD_ISSET(sd, &read_fds)) {
                int valread = read(sd, buffer, BUFFER_SIZE);
                // 客户端断开连接
                if (valread == 0) {
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    printf("Client disconnected, socket fd %d\n", sd);
                    close(sd);
                    client_sockets[i] = 0;
                } 
                // 收到客户端消息
                else {
                    // 广播消息给所有其他客户端
                    for (int j = 0; j < MAX_CLIENTS; j++) {
                        int target_sd = client_sockets[j];
                        if (target_sd != 0 && target_sd != sd) {
                            send(target_sd, buffer, valread, 0);
                        }
                    }
                }
                memset(buffer, 0, BUFFER_SIZE);
            }
        }
    }
    // 清理资源(理论上循环不会退出到这里)
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (client_sockets[i] != 0) {
            close(client_sockets[i]);
        }
    }
    close(server_fd);
    return 0;
}

客户端代码 (client.c)

客户端代码不需要改变,它仍然使用之前编写的 client.c

如何编译和运行多客户端版本

  1. 编译服务器

    gcc multi_server.c -o multi_server
  2. 运行

    • 在一个终端运行服务器:
      ./multi_server
    • 打开两个或更多新的终端,分别运行客户端:
      ./client 127.0.0.1
    • 你在任何一个客户端输入的消息,都会被广播到所有其他已连接的客户端,这实现了一个简单的聊天室功能。
特性 一对一模型 (server.c) 多客户端模型 (multi_server.c)
架构 简单的客户端/服务器 客户端/服务器,带连接管理
核心技术 accept, read, send select (I/O多路复用)
客户端管理 只处理一个连接 维护一个客户端套接字数组
消息处理 点对点通信 服务器广播(聊天室)
适用场景 两个特定程序通信 多人在线聊天室

从“一对一”到“多客户端”的演进,是网络编程中的一个重要里程碑。select 是实现这一目标的关键,更高级的实现可能会使用 pollepoll (Linux特有) 来获得更好的性能,但 select 的原理是相通的,希望这个详细的教程能帮助你理解如何用 C 语言构建一个聊天程序!

-- 展开阅读全文 --
头像
c语言 form(
« 上一篇 04-22
织梦管理员密码忘了咋改?
下一篇 » 04-22

相关文章

取消
微信二维码
支付宝二维码

目录[+]