《Linux 网络架构:基于 TCP 协议的多人谈天体系搭建详解》

[复制链接]
发表于 2025-10-12 06:12:19 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
一、体系概述

        本体系是一个基于 TCP 协议的多人谈天体系,由一个服务器和多个客户端构成。客户端可以毗连到服务器,向服务器发送消息,服务器吸取到消息后将其转发给其他客户端,实现多人之间的及时谈天。体系使用 C 语言编写,使用了 Unix 体系的网络编程接口和多线程、I/O 多路复用等技能。
二、文件布局


  • server.c:服务器端步调,负责监听客户端毗连、吸取客户端消息并将消息转发给其他客户端。
  • client1.c:客户端步调,使用 poll 函数实现 I/O 多路复用,同时处置处罚用户输入和服务器消息。
  • client2.c:客户端步调,使用多线程技能,一个线程负责吸取服务器消息,另一个线程负责处置处罚用户输入。
三、代码具体分析

1. 数据包布局体

        在三个文件中都界说了雷同的数据包布局体 Packet,用于在客户端和服务器之间传输数据。
  1. typedef struct {
  2.     int type;  // 0 for message, 1 for disconnect
  3.     char data[BUFFER_SIZE];
  4. } Packet;
复制代码

  • type:数据包范例,0 表现消息,1 表现断开毗连。
  • data:数据包携带的数据,最大长度为 BUFFER_SIZE。
2. 服务器端步调(server.c)

2.1 重要变量


  • server_fd:服务器套接字文件形貌符。
  • client_fd:客户端套接字文件形貌符。
  • max_fd:记载最大的文件形貌符,用于 select 函数。
  • activity:记载 select 函数返回的运动文件形貌符数目。
  • valread:记载从客户端读取的数据长度。
  • server_addr:存储服务器的所在信息。
  • client_addr:存储客户端的所在信息。
  • client_sockets:数组用于存储全部客户端的套接字文件形貌符。
  • readfds:文件形貌符聚集,用于 select 函数监听可读变乱。
2.2 重要步调


  • 创建套接字:使用 socket 函数创建一个 TCP 套接字。
  • 绑定所在:使用 bind 函数将套接字绑定到指定的所在和端口。
  • 监听毗连:使用 listen 函数开始监听客户端毗连。
  • 循环处置处罚:使用 select 函数监听服务器套接字和客户端套接字的可读变乱。

    • 若服务器套接字有可读变乱,阐明有新的客户端毗连哀求,使用 accept 函数担当毗连。
    • 若客户端套接字有可读变乱,从客户端读取数据包,根据数据包范例举行相应处置处罚。

      • 若数据包范例为消息,将消息转发给其他客户端。
      • 若数据包范例为断开毗连或读取到的数据长度为 0,阐明客户端断开毗连,关闭客户端套接字。
                     
           
3. 客户端步调(client1.c)

3.1 重要变量


  • client_fd:客户端套接字文件形貌符。
  • server_addr:存储服务器的所在信息。
  • packet:用于存储要发送或吸取的数据包。
  • fds:数组用于存储要监听的文件形貌符及其变乱。
3.2 重要步调


  • 创建套接字:使用 socket 函数创建一个 TCP 套接字。
  • 毗连服务器:使用 connect 函数毗连到服务器。
  • 初始化 poll 布局体:监听标准输入和客户端套接字的可读变乱。
  • 循环处置处罚:使用 poll 函数监听文件形貌符集会合的可读变乱。

    • 若标准输入有可读变乱,从标准输入读取数据,设置数据包范例为消息,发送给服务器。
    • 若客户端套接字有可读变乱,从服务器读取数据包,根据数据包范例举行相应处置处罚。

      • 若数据包范例为消息,输出吸取到的消息。
      • 若数据包范例为断开毗连或读取数据失败,阐明服务器断开毗连,关闭客户端套接字,跳出循环。
                     
           
4. 客户端步调(client2.c)

4.1 重要变量


  • client_fd:客户端套接字文件形貌符。
  • server_addr:存储服务器的所在信息。
  • packet:用于存储要发送或吸取的数据包。
  • thread_id:存储线程的标识符。
4.2 重要步调


  • 创建套接字:使用 socket 函数创建一个 TCP 套接字。
  • 毗连服务器:使用 connect 函数毗连到服务器。
  • 创建线程:创建一个线程来吸取服务器发送的消息。
  • 循环处置处罚:在主线程中,从标准输入读取数据,设置数据包范例为消息,发送给服务器。
  • 线程函数:在子线程中,一连吸取服务器消息,根据数据包范例举行相应处置处罚。

    • 若数据包范例为消息,输出吸取到的消息。
    • 若数据包范例为断开毗连或读取数据失败,阐明服务器断开毗连,关闭客户端套接字,退出步调。
           
四、编译和运行

4.1 编译
  1. gcc server.c -o server
  2. gcc client1.c -o client1
  3. gcc client2.c -o client2 -lpthread
复制代码
4.2 运行


  • 启动服务器:
  1. ./server
复制代码

  • 启动客户端:
  1. ./client1
  2. ./client2
复制代码
4.3运行效果展示


五、源码

5.1服务器端步调(server.c)
  1. #include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/select.h>#include <sys/time.h>#include <sys/types.h>#include <fcntl.h>#include <errno.h>// 界说服务器监听的端标语#define PORT 8080// 界说数据缓冲区的巨细#define BUFFER_SIZE 1024// 界说服务器允许的最大客户端毗连数#define MAX_CLIENTS 10/** * 界说数据包布局体,用于在服务器和客户端之间传输数据 * type 数据包范例,0 表现消息,1 表现断开毗连 * data 数据包携带的数据 */
  2. typedef struct {
  3.     int type;  // 0 for message, 1 for disconnect
  4.     char data[BUFFER_SIZE];
  5. } Packet;/** * 主函数,服务器步调的入口点 * @return 步调的退出状态码,0 表现正常退出 */int main() {    // server_fd 为服务器套接字文件形貌符,client_fd 为客户端套接字文件形貌符    // max_fd 记载最大的文件形貌符,用于 select 函数    // activity 记载 select 函数返回的运动文件形貌符数目    // valread 记载从客户端读取的数据长度    int server_fd, client_fd, max_fd, activity, valread;    // server_addr 存储服务器的所在信息,client_addr 存储客户端的所在信息    struct sockaddr_in server_addr, client_addr;    // client_len 存储客户端所在布局体的长度    socklen_t client_len = sizeof(client_addr);    // packet 用于存储从客户端吸取的数据包    Packet packet;    // client_sockets 数组用于存储全部客户端的套接字文件形貌符    int client_sockets[MAX_CLIENTS] = {0};    // readfds 是一个文件形貌符聚集,用于 select 函数监听可读变乱    fd_set readfds;    // 创建服务器套接字,使用 IPv4 所在族和 TCP 协议    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {        // 若套接字创建失败,输堕落误信息并退出步调        perror("socket failed");        exit(EXIT_FAILURE);    }    // 设置服务器所在信息    server_addr.sin_family = AF_INET;    // 监听全部可用的网络接口    server_addr.sin_addr.s_addr = INADDR_ANY;    // 将端标语从主机字节序转换为网络字节序    server_addr.sin_port = htons(PORT);    // 将服务器套接字绑定到指定的所在和端口    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {        // 若绑定失败,输堕落误信息,关闭套接字并退出步调        perror("bind failed");        close(server_fd);        exit(EXIT_FAILURE);    }    // 开始监听客户端毗连,允许的最大毗连哀求队列长度为 3    if (listen(server_fd, 3) < 0) {        // 若监听失败,输堕落误信息,关闭套接字并退出步调        perror("listen");        close(server_fd);        exit(EXIT_FAILURE);    }    // 输出服务器启动信息,表现监听的端标语    printf("Server started on port %d\n", PORT);    // 进入无穷循环,一连处置处罚客户端毗连和数据    while (1) {        // 清空文件形貌符聚集        FD_ZERO(&readfds);        // 将服务器套接字添加到文件形貌符集会合,监听其可读变乱        FD_SET(server_fd, &readfds);        // 初始化最大文件形貌符为服务器套接字文件形貌符        max_fd = server_fd;        // 遍历客户端套接字数组        for (int i = 0; i < MAX_CLIENTS; i++) {            // 获取当前客户端的套接字文件形貌符            int sd = client_sockets[i];            if (sd > 0) {                // 若该客户端套接字有效,将其添加到文件形貌符集会合                FD_SET(sd, &readfds);            }            if (sd > max_fd) {                // 更新最大文件形貌符                max_fd = sd;            }        }        // 调用 select 函数监听文件形貌符集会合的可读变乱,无超时时间        activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);        if ((activity < 0) && (errno != EINTR)) {            // 若 select 函数调用失败且不是被信号停止,输堕落误信息            perror("select error");        }        if (FD_ISSET(server_fd, &readfds)) {            // 若服务器套接字有可读变乱,阐明有新的客户端毗连哀求            if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len)) < 0) {                // 若担当毗连失败,输堕落误信息并继续循环                perror("accept");                continue;            }            // 输出新客户端毗连的信息,包罗套接字文件形貌符、IP 所在和端标语            printf("New connection, socket fd is %d, ip is : %s, port : %d\n",                   client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));            // 遍历客户端套接字数组,找到一个空闲位置存储新客户端的套接字文件形貌符            for (int i = 0; i < MAX_CLIENTS; i++) {                if (client_sockets[i] == 0) {                    client_sockets[i] = client_fd;                    break;                }            }        }        // 遍历客户端套接字数组,查抄每个客户端是否有可读变乱        for (int i = 0; i < MAX_CLIENTS; i++) {            int sd = client_sockets[i];            if (FD_ISSET(sd, &readfds)) {                // 从客户端读取数据包                valread = read(sd, &packet, sizeof(Packet));                if (valread == 0) {                    // 若读取到的数据长度为 0,阐明客户端断开毗连                    getpeername(sd, (struct sockaddr*)&client_addr, &client_len);                    // 输出客户端断开毗连的信息,包罗 IP 所在和端标语                    printf("Host disconnected, ip %s, port %d\n",                           inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));                    // 关闭客户端套接字                    close(sd);                    // 将该位置的客户端套接字文件形貌符置为 0,表现空闲                    client_sockets[i] = 0;                } else {                    if (packet.type == 0) {                        // 若数据包范例为消息,输出吸取到的消息                        printf("Received from client %d: %s", sd, packet.data);                        // 将消息转发给其他客户端                        for (int j = 0; j < MAX_CLIENTS; j++) {                            if (client_sockets[j] != sd && client_sockets[j] != 0) {                                // 发送数据包给其他客户端                                send(client_sockets[j], &packet, sizeof(Packet), 0);                            }                        }                    } else if (packet.type == 1) {                        // 若数据包范例为断开毗连,输出客户端断开毗连的信息                        printf("Client %d disconnected\n", sd);                        // 关闭客户端套接字                        close(sd);                        // 将该位置的客户端套接字文件形貌符置为 0,表现空闲                        client_sockets[i] = 0;                    }                }            }        }    }    return 0;}
复制代码
5.2客户端步调(client1.c)
  1. #include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <poll.h>#include <errno.h>// 界说服务器监听的端标语#define PORT 8080// 界说数据缓冲区的巨细#define BUFFER_SIZE 1024/** * 界说数据包布局体,用于在客户端和服务器之间传输数据 * type 数据包范例,0 表现消息,1 表现断开毗连 * data 数据包携带的数据 */
  2. typedef struct {
  3.     int type;  // 0 for message, 1 for disconnect
  4.     char data[BUFFER_SIZE];
  5. } Packet;/** * 主函数,客户端步调的入口点 * @return 步调的退出状态码,0 表现正常退出 */int main() {    // client_fd 为客户端套接字文件形貌符    int client_fd;    // server_addr 存储服务器的所在信息    struct sockaddr_in server_addr;    // packet 用于存储要发送或吸取的数据包    Packet packet;    // fds 数组用于存储要监听的文件形貌符及其变乱    struct pollfd fds[2];    // 创建客户端套接字,使用 IPv4 所在族和 TCP 协议    if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {        // 若套接字创建失败,输堕落误信息并退出步调        perror("socket failed");        exit(EXIT_FAILURE);    }    // 设置服务器所在信息    server_addr.sin_family = AF_INET;    // 将端标语从主机字节序转换为网络字节序    server_addr.sin_port = htons(PORT);    // 将服务器的 IP 所在转换为网络字节序    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");    // 毗连到服务器    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {        // 若毗连失败,输堕落误信息,关闭套接字并退出步调        perror("connect");        close(client_fd);        exit(EXIT_FAILURE);    }    // 输出毗连乐成的信息    printf("Connected to server\n");    // 初始化 poll 布局体    // 监听标准输入的可读变乱    fds[0].fd = STDIN_FILENO;    fds[0].events = POLLIN;    // 监听客户端套接字的可读变乱    fds[1].fd = client_fd;    fds[1].events = POLLIN;    // 进入无穷循环,一连处置处罚输入和服务器消息    while (1) {        // 调用 poll 函数监听文件形貌符集会合的可读变乱,无超时时间        int activity = poll(fds, 2, -1);        if ((activity < 0) && (errno != EINTR)) {            // 若 poll 函数调用失败且不是被信号停止,输堕落误信息            perror("poll error");        }        if (fds[0].revents & POLLIN) {            // 若标准输入有可读变乱            // 清空数据包的数据部门            memset(packet.data, 0, BUFFER_SIZE);            if (fgets(packet.data, BUFFER_SIZE, stdin) != NULL) {                // 若乐成从标准输入读取数据                // 设置数据包范例为消息                packet.type = 0;                // 发送数据包给服务器                send(client_fd, &packet, sizeof(Packet), 0);            }        }        if (fds[1].revents & POLLIN) {            // 若客户端套接字有可读变乱            // 清空数据包            memset(&packet, 0, sizeof(Packet));            if (read(client_fd, &packet, sizeof(Packet)) > 0) {                // 若乐成从服务器读取数据                if (packet.type == 0) {                    // 若数据包范例为消息,输出吸取到的消息                    printf("Received from server: %s", packet.data);                } else if (packet.type == 1) {                    // 若数据包范例为断开毗连,输出服务器断开毗连的信息                    printf("Server disconnected\n");                    // 关闭客户端套接字                    close(client_fd);                    // 跳出循环                    break;                }            } else {                // 若读取数据失败,阐明服务器断开毗连                printf("Server disconnected\n");                // 关闭客户端套接字                close(client_fd);                // 跳出循环                break;            }        }    }    // 关闭客户端套接字    close(client_fd);    return 0;}
复制代码
5.3客户端步调(client2.c)
  1. // 包罗标准输入输出库,用于使用 printf、perror 等函数举行输入输出操纵#include <stdio.h>// 包罗标准库,提供 exit 等函数用于步调退出等操纵#include <stdlib.h>// 包罗字符串处置处罚库,提供 memset、strlen 等字符串操纵函数#include <string.h>// 包罗 Unix 标准库,提供 close、read、write 等体系调用函数#include <unistd.h>// 包罗网络所在转换库,提供 inet_ntoa、htons 等网络所在转换函数#include <arpa/inet.h>// 包罗线程相干的头文件,用于创建和管理线程#include <pthread.h>// 界说服务器监听的端标语#define PORT 8080// 界说数据缓冲区的巨细#define BUFFER_SIZE 1024/** * 界说数据包布局体,用于在客户端和服务器之间传输数据 * type 数据包范例,0 表现消息,1 表现断开毗连 * data 数据包携带的数据 */
  2. typedef struct {
  3.     int type;  // 0 for message, 1 for disconnect
  4.     char data[BUFFER_SIZE];
  5. } Packet;// 全局变量,存储客户端套接字文件形貌符int client_fd;/** * 线程函数,用于吸取服务器发送的消息 * arg 线程函数的参数,此处未使用 * @return 线程返回值,此处为 NULL */void *receive_messages(void *arg) {    // 界说数据包变量,用于存储从服务器吸取的数据包    Packet packet;    // 进入无穷循环,一连吸取服务器消息    while (1) {        // 清空数据包        memset(&packet, 0, sizeof(Packet));        if (read(client_fd, &packet, sizeof(Packet)) > 0) {            // 若乐成从服务器读取数据            if (packet.type == 0) {                // 若数据包范例为消息,输出吸取到的消息                printf("Received from server: %s", packet.data);            } else if (packet.type == 1) {                // 若数据包范例为断开毗连,输出服务器断开毗连的信息                printf("Server disconnected\n");                // 关闭客户端套接字                close(client_fd);                // 退出步调,返回失败状态                exit(EXIT_FAILURE);            }        } else {            // 若读取数据失败,阐明服务器断开毗连            printf("Server disconnected\n");            // 关闭客户端套接字            close(client_fd);            // 退出步调,返回失败状态            exit(EXIT_FAILURE);        }    }    return NULL;}/** * 主函数,客户端步调的入口点 * @return 步调的退出状态码,0 表现正常退出 */int main() {    // server_addr 存储服务器的所在信息    struct sockaddr_in server_addr;    // packet 用于存储要发送的数据包    Packet packet;    // thread_id 存储线程的标识符    pthread_t thread_id;    // 创建客户端套接字,使用 IPv4 所在族和 TCP 协议    if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {        // 若套接字创建失败,输堕落误信息并退出步调        perror("socket failed");        exit(EXIT_FAILURE);    }    // 设置服务器所在信息    server_addr.sin_family = AF_INET;    // 将端标语从主机字节序转换为网络字节序    server_addr.sin_port = htons(PORT);    // 将服务器的 IP 所在转换为网络字节序    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");    // 毗连到服务器    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {        // 若毗连失败,输堕落误信息,关闭套接字并退出步调        perror("connect");        close(client_fd);        exit(EXIT_FAILURE);    }    // 输出毗连乐成的信息    printf("Connected to server\n");    // 创建线程来吸取消息    if (pthread_create(&thread_id, NULL, receive_messages, NULL) != 0) {        // 若线程创建失败,输堕落误信息,关闭套接字并退出步调        perror("pthread_create");        close(client_fd);        exit(EXIT_FAILURE);    }    // 进入无穷循环,一连从标准输入读取数据并发送给服务器    while (1) {        // 清空数据包的数据部门        memset(packet.data, 0, BUFFER_SIZE);        if (fgets(packet.data, BUFFER_SIZE, stdin) != NULL) {            // 若乐成从标准输入读取数据            // 设置数据包范例为消息            packet.type = 0;            // 发送数据包给服务器            send(client_fd, &packet, sizeof(Packet), 0);        }    }    // 关闭客户端套接字    close(client_fd);    return 0;}
复制代码
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表