L-Chat(2)——M3

0 阅读5分钟

一、修改架构

image.png

1.1 引入dispatch

#include <stdio.h>
#include "dispatcher.h"
#include "service/auth_service.h"
#include "service/chat_service.h"

void dispatch_message(Conn *conn, uint16_t type, void *payload) {
    switch (type) {
        case MSG_TYPE_LOGIN_REQ:
            handle_login(conn, payload);
            break;
        case MSG_TYPE_CHAT_REQ:
            handle_chat(conn, payload);
            break;
        
        default:
            printf(">>> [Dispatcher] Unknown type: %d\n", type);
            break;
    }
}

void dispatch_message()置入void handle_client_message()中,来识别请求和分配各自的业务,各自的业务再对应到service下的各种服务

如目前:

  • MSG_TYPE_LOGIN_REQ对应登录业务auth_service.c
  • MSG_TYPE_CHAT_REQ对应聊天业务chat_service.c

auth_service.c

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include "auth_service.h"

void handle_login(Conn *conn, void *payload) {
    static uint32_t auto_increment_uid = 1001; // 静态变量,函数退出后值依然保留
    uint32_t login_uid = auto_increment_uid++;

    LoginRequest *req = (LoginRequest *)payload;
    printf(">>> [AuthService] User: %s logging in...\n", req->username);

    // 构造响应包(逻辑与之前一致)
    char send_buf[1024];
    AppHeader *resp_header = (AppHeader *)send_buf;
    LoginResponse *resp_body = (LoginResponse *)(send_buf + sizeof(AppHeader));

    // 
    // uint32_t login_uid = 1001; // 先模拟一个uid 实际工程中应该是从数据库查询后得到的真实 UID
    resp_body->uid = htonl(login_uid);
    resp_body->result = 1; // 1 表示成功
    strcpy(resp_body->msg, "Login Success (M3 Architecture)");

    // 在返回成功响应前,将该连接与 UID 绑定到全局路由表中
    user_bind(login_uid, conn); 
    printf(">>> [Auth] User %d bound to fd %d\n", login_uid, conn->fd);
    // ------------------

    resp_header->magic = htonl(PROTOCOL_MAGIC);
    resp_header->version = htons(1);
    resp_header->type = htons(MSG_TYPE_LOGIN_RESP);
    resp_header->length = htonl(sizeof(LoginResponse));

    send(conn->fd, send_buf, sizeof(AppHeader) + sizeof(LoginResponse), 0);
}

chat_service.c

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include "chat_service.h"
#include "../connection.h"

void handle_chat(Conn *conn, void *payload) {
    // 1. 解析负载
    ChatMsg *msg = (ChatMsg *)payload;
    uint32_t from_id = ntohl(msg->from_uid);
    uint32_t to_id = ntohl(msg->to_uid);
    uint8_t type = msg->chat_type;

    // 获取当前包的总长度,以此计算正文 content 的长度
    // 技巧:回溯包头获取 length 字段
    AppHeader *header = (AppHeader *)((char *)payload - sizeof(AppHeader));
    uint32_t total_payload_len = ntohl(header->length);
    uint32_t content_len = total_payload_len - sizeof(ChatMsg);

    printf(">>> [ChatService] From:%u To:%u Type:%d Len:%u\n", 
           from_id, to_id, type, content_len);

    if (type == 0) { // 私聊逻辑
        // 在全局用户映射表中查找目标用户
        Conn *target_conn = g_user_map[to_id];
        
        if (target_conn && target_conn->is_auth) {
            // 直接将整个二进制包(Header + Body)转发给目标 fd
            send(target_conn->fd, (char *)header, sizeof(AppHeader) + total_payload_len, 0);
            printf("    Success: Forwarded to fd %d\n", target_conn->fd);
        } else {
            printf("    Failed: Target user %u is offline\n", to_id);
            // TODO: 这里可以给发送者回发一个“对方不在线”的消息包
        }
    } 
    else if (type == 1) { // 群聊逻辑 (简单广播版)
        // 遍历所有在线连接,广播该消息
        for (int i = 0; i < 1024; i++) {
            if (g_conns[i] && g_conns[i]->is_auth && g_conns[i]->fd != conn->fd) {
                send(g_conns[i]->fd, (char *)header, sizeof(AppHeader) + total_payload_len, 0);
            }
        }
        printf("    Success: Broadcasted to all online users\n");
    }
}

1.2用户映射

connection.c新增:

//g_conns[fd] 是通过文件描述符找连接,但聊天是通过 uid 找人。
// 增加绑定函数,将用户的uid和conn绑定起来
void user_bind(uint32_t uid, Conn* conn) {
    if (uid < 10000) {
        g_user_map[uid] = conn;
        conn->uid = uid;
        conn->is_auth = 1;
    }
}
void user_unbind(uint32_t uid) {
    if (uid < 10000) g_user_map[uid] = NULL;
}

  1. auth_service 验证用户名密码成功后,立即调用 user_bind(uid, conn),将该 Conn 指针填入数组对应位置
  2. chat_service 收到私聊请求时,提取目标 to_uid,直接从 g_user_map[to_uid] 获取目标连接的 fd
  3. 当客户端异常断开或主动退出时,conn_delete 调用 user_unbind,将映射关系抹除

1.3 客户端 Pthread 多线程

client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include "../include/protocol.h"

#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUFFER_SIZE 65536

uint32_t my_uid = 0;

// 接收线程:专门负责从服务器读数据并打印
void* recv_thread_func(void* arg) {
    int sockfd = *(int*)arg;
    char buffer[BUFFER_SIZE];

    while (1) {
        // 1. 先读包头
        int n = recv(sockfd, buffer, sizeof(AppHeader), 0);
        if (n <= 0) break;

        AppHeader* header = (AppHeader*)buffer;
        uint16_t type = ntohs(header->type);
        uint32_t len = ntohl(header->length);

        // 2. 读负载
        if (len > 0) {
            recv(sockfd, buffer + sizeof(AppHeader), len, 0);
        }

        // 3. 根据类型处理展示
        if (type == MSG_TYPE_LOGIN_RESP) {
            LoginResponse* resp = (LoginResponse*)(buffer + sizeof(AppHeader));
            my_uid = ntohl(resp->uid);
            printf("\n[System] %s! My UID is: %u\n", resp->msg, my_uid);
        } 
        else if (type == MSG_TYPE_CHAT_REQ) {
            ChatMsg* msg = (ChatMsg*)(buffer + sizeof(AppHeader));
            printf("\n[Message] From %u: %s\n", ntohl(msg->from_uid), msg->content);
        }
        printf("L-Chat > "); // 保持提示符
        fflush(stdout);
    }
    printf("\n[System] Disconnected from server.\n");
    exit(0);
}

int main() {
    int sockfd;
    struct sockaddr_in addr;
    pthread_t tid;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &addr.sin_addr);

    if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Connect failed");
        return -1;
    }

    // 1. 启动接收线程
    pthread_create(&tid, NULL, recv_thread_func, &sockfd);

    // 2. 发送登录请求 (模拟 admin/123456)
    LoginRequest login = {"admin", "123456"};
    AppHeader header = {htonl(PROTOCOL_MAGIC), htons(1), htons(MSG_TYPE_LOGIN_REQ), 0, htonl(sizeof(login))};
    send(sockfd, &header, sizeof(header), 0);
    send(sockfd, &login, sizeof(login), 0);

    sleep(1); // 等待登录成功并获取 UID

    // 3. 主循环:用户输入聊天内容
    printf("--- Welcome to L-Chat ---\n");
    printf("Format: <to_uid> <content>\n");
    
    while (1) {
        uint32_t to_uid;
        char content[512];
        printf("L-Chat > ");
        scanf("%u %s", &to_uid, content);

        uint32_t payload_len = sizeof(ChatMsg) + strlen(content) + 1;
        char* send_buf = malloc(sizeof(AppHeader) + payload_len);

        // 填充 Header
        AppHeader* h = (AppHeader*)send_buf;
        h->magic = htonl(PROTOCOL_MAGIC);
        h->type = htons(MSG_TYPE_CHAT_REQ);
        h->length = htonl(payload_len);

        // 填充 ChatMsg
        ChatMsg* m = (ChatMsg*)(send_buf + sizeof(AppHeader));
        m->from_uid = htonl(my_uid);
        m->to_uid = htonl(to_uid);
        m->chat_type = (to_uid > 5000) ? 1 : 0; // 假设5000以上是群
        strcpy(m->content, content);

        send(sockfd, send_buf, sizeof(AppHeader) + payload_len, 0);
        free(send_buf);
    }

    close(sockfd);
    return 0;
}
  1. 在传统的单线程客户端中,scanf(等待键盘)和 recv(等待网络)都是阻塞操作。在单线程下,它们是互斥的,所以将客户端拆分为两个并行的执行流
  2. 主线程:解析输入的命令、封装、发到服务器
  3. 接收线程:由 pthread_create 启动,独立运行在后台,一直执行 recv 操作,一旦服务器转发消息过来,它立即解析并直接打印在屏幕上

二、测试结果

一个服务器

image.png 三个客户端:1001、1002、1003

  1. 1001给1002发送Hello_Friend!
  2. 1001给9999uid发送Hi_Everyone!,uid大于5000转化为群聊
  3. 1002给1003发送abcdefg

d1496fa8f6730e287845afdf3b5138d2.png

76ad171adf9d9d53925769d525825441.png

38979fb7429a18fa84e8638c3e75fc05.png