一、修改架构
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;
}
- 当
auth_service验证用户名密码成功后,立即调用user_bind(uid, conn),将该Conn指针填入数组对应位置 - 当
chat_service收到私聊请求时,提取目标to_uid,直接从g_user_map[to_uid]获取目标连接的fd。 - 当客户端异常断开或主动退出时,
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;
}
- 在传统的单线程客户端中,
scanf(等待键盘)和recv(等待网络)都是阻塞操作。在单线程下,它们是互斥的,所以将客户端拆分为两个并行的执行流 - 主线程:解析输入的命令、封装、发到服务器
- 接收线程:由
pthread_create启动,独立运行在后台,一直执行recv操作,一旦服务器转发消息过来,它立即解析并直接打印在屏幕上
二、测试结果
一个服务器
三个客户端:1001、1002、1003
- 1001给1002发送Hello_Friend!
- 1001给9999uid发送Hi_Everyone!,uid大于5000转化为群聊
- 1002给1003发送abcdefg