基于linuxC结合epoll + TCP 服务器客户端 + 数据库实现一个注册登录功能

发布于:2025-03-25 ⋅ 阅读:(26) ⋅ 点赞:(0)

1. 整体功能概述

        实现了一个简单的用户注册和登录系统,采用客户端 - 服务器(C/S)架构。

        客户端可以选择注册或登录操作,将用户名和密码发送给服务器,服务器接收请求后处理并返回相应的结果给客户端。

        服务器使用 SQLite 数据库来存储用户信息。

2. 客户端功能实现分析

  • 功能:客户端程序,允许用户选择注册或登录操作,输入用户名和密码,将请求发送给服务器,并接收服务器的响应结果。
  • 输入:用户选择(1 表示注册,2 表示登录)、用户名、密码。
  • 输出:服务器返回的处理结果(如注册成功、用户名已存在、登录成功等)。

1.1. 详细步骤

1.1.1. 包含必要的头文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

        这些头文件提供了标准输入输出、内存管理、字符串处理、套接字编程和 IP 地址转换等功能。

1.1.2. 定义协议包结构体

#define BUF_SIZE 1024

typedef struct {
    uint8_t op;         // 操作码,1表示注册,2表示登录
    uint16_t data_len;  // 数据长度(网络字节序)
    char data[BUF_SIZE];// 存储用户名、密码等数据
} ProtocolPack;

        该结构体用于封装客户端和服务器之间传输的协议包,包含操作码、数据长度和数据内容。

1.1.3. 创建套接字并连接服务器

int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_port = htons(8888)
};
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));

        使用 socket 函数创建一个 TCP 套接字,设置服务器地址和端口,然后使用 connect 函数连接到服务器。

1.1.4. 获取用户输入

int choice;
printf("1. 注册\n2. 登录\n选择: ");
scanf("%d", &choice);

char username[32], password[32];
printf("用户名: ");
scanf("%31s", username);
printf("密码: ");
scanf("%31s", password);

        提示用户选择操作类型(注册或登录),并输入用户名和密码。

1.1.5. 构建协议包并发送

ProtocolPack pack = {0};
pack.op = (uint8_t)choice;
sprintf(pack.data, "%s %s", username, password);
pack.data_len = htons(strlen(pack.data));
send(sock, &pack, sizeof(ProtocolPack), 0);

        将用户选择的操作码、用户名和密码封装到协议包中,并使用 send 函数发送给服务器。

1.1.6. 接收服务器响应并输出结果

ProtocolPack resp_pack;
recv(sock, &resp_pack, sizeof(ProtocolPack), 0);
printf("结果: %s\n", resp_pack.data);

        使用 recv 函数接收服务器的响应协议包,并输出结果。

1.1.7. 关闭套接字

close(sock);

3.服务器功能实现分析

  • 功能:服务器程序,监听客户端的连接请求,接收客户端发送的注册或登录请求,处理请求并将结果返回给客户端。使用 SQLite 数据库存储用户信息。
  • 输入:客户端发送的协议包,包含操作码、用户名和密码。
  • 输出:处理结果(如注册成功、用户名已存在、登录成功等),封装在协议包中返回给客户端。

3.1. 详细步骤

3.1.1. 包含必要的头文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sqlite3.h>

        这些头文件提供了标准输入输出、内存管理、字符串处理、套接字编程、事件驱动 I/O 和 SQLite 数据库操作等功能。

3.1.2. 定义常量和协议包结构体

#define MAX_EVENTS 100
#define BUF_SIZE 1024

typedef struct {
    uint8_t op;         // 操作码,1表示注册,2表示登录
    uint16_t data_len;  // 数据长度(网络字节序)
    char data[BUF_SIZE];// 存储用户名、密码等数据
} ProtocolPack;

    MAX_EVENTS 定义了 epoll_wait 函数返回的最大事件数量,BUF_SIZE 定义了协议包数据部分的最大缓冲区大小。

3.1.3. 初始化数据库

sqlite3* init_db() {
    sqlite3* db;
    int rc = sqlite3_open("user.db", &db);
    if (rc) {
        fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
        exit(1);
    }
    const char* sql = "CREATE TABLE IF NOT EXISTS users ("
                      "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                      "username TEXT UNIQUE NOT NULL,"
                      "password TEXT NOT NULL)";
    char* errmsg;
    rc = sqlite3_exec(db, sql, 0, 0, &errmsg);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "SQL error: %s\n", errmsg);
        sqlite3_free(errmsg);
    }
    return db;
}

        打开或创建一个名为 user.db 的 SQLite 数据库,并创建一个名为 users 的表,用于存储用户信息。

3.1.4. 处理用户注册逻辑

int handle_register(sqlite3* db, const char* username, const char* password) {
    const char* sql = "INSERT INTO users (username, password) VALUES (?, ?)";
    sqlite3_stmt* stmt;
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
    if (rc != SQLITE_OK) return -1;

    sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, password, -1, SQLITE_STATIC);

    rc = sqlite3_step(stmt);
    if (rc == SQLITE_DONE) {
        sqlite3_finalize(stmt);
        return 0; // 注册成功
    }
    if (rc == SQLITE_CONSTRAINT_UNIQUE) {
        sqlite3_finalize(stmt);
        return 1; // 用户名已存在
    }
    sqlite3_finalize(stmt);
    return -1;
}

        将用户的用户名和密码插入到 users 表中,处理可能的错误情况,如 SQL 执行失败或用户名已存在。

3.1.5. 处理用户登录逻辑

int handle_login(sqlite3* db, const char* username, const char* password) {
    const char* sql = "SELECT COUNT(*) FROM users WHERE username=? AND password=?";
    sqlite3_stmt* stmt;
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
    if (rc != SQLITE_OK) return -1;

    sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, password, -1, SQLITE_STATIC);

    rc = sqlite3_step(stmt);
    if (rc == SQLITE_ROW) {
        int count = sqlite3_column_int(stmt, 0);
        sqlite3_finalize(stmt);
        return count == 1 ? 0 : -1; // 登录成功返回0,否则失败
    }
    sqlite3_finalize(stmt);
    return -1;
}

        检查用户的用户名和密码是否匹配数据库中的记录,返回登录结果。

3.1.6. 创建监听套接字并绑定地址

int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_port = htons(8888),
    .sin_addr.s_addr = INADDR_ANY
};
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 5);

        使用 socket 函数创建一个 TCP 监听套接字,绑定到指定的地址和端口,然后开始监听客户端连接。

3.1.7. 创建 epoll 实例并监听事件

int epoll_fd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);

        使用 epoll_create1 函数创建一个 epoll 实例,将监听套接字加入到 epoll 监控列表中,监听读事件。

3.1.8. 事件循环处理

while (1) {
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == listen_fd) { // 新客户端连接
            int client_fd = accept(listen_fd, NULL, NULL);
            ev.events = EPOLLIN;
            ev.data.fd = client_fd;
            epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
        } else { // 处理客户端数据
            int client_fd = events[i].data.fd;
            ProtocolPack pack;
            if (recv(client_fd, &pack, sizeof(ProtocolPack), 0) <= 0) {
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                close(client_fd);
                continue;
            }

            char username[32] = {0}, password[32] = {0};
            sscanf(pack.data, "%31s %31s", username, password);

            ProtocolPack resp_pack = {0};
            if (pack.op == 1) { // 处理注册请求
                int result = handle_register(db, username, password);
                if (result == 0) strcpy(resp_pack.data, "REGISTER_SUCCESS");
                else if (result == 1) strcpy(resp_pack.data, "USER_EXISTS");
                else strcpy(resp_pack.data, "REGISTER_FAIL");
            } else if (pack.op == 2) { // 处理登录请求
                int result = handle_login(db, username, password);
                if (result == 0) strcpy(resp_pack.data, "LOGIN_SUCCESS");
                else strcpy(resp_pack.data, "LOGIN_FAIL");
            }
            resp_pack.op = pack.op;
            resp_pack.data_len = htons(strlen(resp_pack.data));
            send(client_fd, &resp_pack, sizeof(ProtocolPack), 0);
        }
    }
}

        使用 epoll_wait 函数等待事件发生,处理新客户端连接和客户端数据。根据客户端发送的操作码调用相应的处理函数,并将结果封装在协议包中返回给客户端。

3.1.9. 关闭监听套接字和数据库连接

close(listen_fd);
sqlite3_close(db);

3.2.编译运行步骤

  1. 编译服务器
    gcc server.c -o server -lsqlite3
    
  2. 编译客户端
    gcc client.c -o client
    
  3. 运行服务器
    ./server
    
  4. 新开终端运行客户端
    ./client

3.3. 功能实现结果展示

3.4. 源码

3.4.1. 服务器端代码(server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sqlite3.h>

#define MAX_EVENTS 100
#define BUF_SIZE 1024

// 定义通信协议包结构体
typedef struct {
    uint8_t op;         // 操作码,1表示注册,2表示登录
    uint16_t data_len;  // 数据长度(网络字节序)
    char data[BUF_SIZE];// 存储用户名、密码等数据
} ProtocolPack;

// 初始化数据库
sqlite3* init_db() {
    sqlite3* db;
    // 打开/创建数据库文件 "user.db"
    int rc = sqlite3_open("user.db", &db);
    if (rc) {
        fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
        exit(1);
    }
    // 创建用户表(若不存在)
    const char* sql = "CREATE TABLE IF NOT EXISTS users ("
                      "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                      "username TEXT UNIQUE NOT NULL,"
                      "password TEXT NOT NULL)";
    char* errmsg;
    // 执行建表SQL
    rc = sqlite3_exec(db, sql, 0, 0, &errmsg);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "SQL error: %s\n", errmsg);
        sqlite3_free(errmsg);
    }
    return db;
}

// 处理注册逻辑
int handle_register(sqlite3* db, const char* username, const char* password) {
    const char* sql = "INSERT INTO users (username, password) VALUES (?, ?)";
    sqlite3_stmt* stmt;
    // 准备SQL语句
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
    if (rc != SQLITE_OK) return -1;
    
    // 绑定用户名和密码参数
    sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, password, -1, SQLITE_STATIC);
    
    // 执行插入操作
    rc = sqlite3_step(stmt);
    if (rc == SQLITE_DONE) {
        sqlite3_finalize(stmt);
        return 0; // 注册成功
    }
    if (rc == SQLITE_CONSTRAINT_UNIQUE) {
        sqlite3_finalize(stmt);
        return 1; // 用户名已存在
    }
    sqlite3_finalize(stmt);
    return -1;
}

// 处理登录逻辑
int handle_login(sqlite3* db, const char* username, const char* password) {
    const char* sql = "SELECT COUNT(*) FROM users WHERE username=? AND password=?";
    sqlite3_stmt* stmt;
    // 准备SQL语句
    int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
    if (rc != SQLITE_OK) return -1;
    
    // 绑定用户名和密码参数
    sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, password, -1, SQLITE_STATIC);
    
    // 执行查询操作
    rc = sqlite3_step(stmt);
    if (rc == SQLITE_ROW) {
        int count = sqlite3_column_int(stmt, 0);
        sqlite3_finalize(stmt);
        return count == 1 ? 0 : -1; // 登录成功返回0,否则失败
    }
    sqlite3_finalize(stmt);
    return -1;
}

int main() {
    sqlite3* db = init_db(); // 初始化数据库
    
    // 创建监听套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8888),
        .sin_addr.s_addr = INADDR_ANY
    };
    // 绑定地址
    bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
    // 开始监听
    listen(listen_fd, 5);
    
    // 创建epoll实例
    int epoll_fd = epoll_create1(0);
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    // 将监听套接字加入epoll监控
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
    
    while (1) {
        // 等待事件发生
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == listen_fd) { // 新客户端连接
                int client_fd = accept(listen_fd, NULL, NULL);
                ev.events = EPOLLIN;
                ev.data.fd = client_fd;
                // 将客户端套接字加入epoll监控
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
            } else { // 处理客户端数据
                int client_fd = events[i].data.fd;
                ProtocolPack pack;
                // 接收协议包
                if (recv(client_fd, &pack, sizeof(ProtocolPack), 0) <= 0) {
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
                    close(client_fd);
                    continue;
                }
                
                char username[32] = {0}, password[32] = {0};
                // 解析数据内容
                sscanf(pack.data, "%31s %31s", username, password);
                
                ProtocolPack resp_pack = {0};
                if (pack.op == 1) { // 处理注册请求
                    int result = handle_register(db, username, password);
                    if (result == 0) strcpy(resp_pack.data, "REGISTER_SUCCESS");
                    else if (result == 1) strcpy(resp_pack.data, "USER_EXISTS");
                    else strcpy(resp_pack.data, "REGISTER_FAIL");
                } else if (pack.op == 2) { // 处理登录请求
                    int result = handle_login(db, username, password);
                    if (result == 0) strcpy(resp_pack.data, "LOGIN_SUCCESS");
                    else strcpy(resp_pack.data, "LOGIN_FAIL");
                }
                resp_pack.op = pack.op;
                resp_pack.data_len = htons(strlen(resp_pack.data));
                // 发送响应协议包
                send(client_fd, &resp_pack, sizeof(ProtocolPack), 0);
            }
        }
    }
    close(listen_fd);
    sqlite3_close(db);
    return 0;
}

3.4.2. 客户端代码(client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 1024

// 定义通信协议包结构体
typedef struct {
    uint8_t op;         // 操作码,1表示注册,2表示登录
    uint16_t data_len;  // 数据长度(网络字节序)
    char data[BUF_SIZE];// 存储用户名、密码等数据
} ProtocolPack;

int main() {
    // 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8888)
    };
    // 设置服务器IP
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    // 连接服务器
    connect(sock, (struct sockaddr*)&addr, sizeof(addr));
    
    ProtocolPack pack = {0};
    int choice;
    printf("1. 注册\n2. 登录\n选择: ");
    scanf("%d", &choice);
    
    char username[32], password[32];
    printf("用户名: ");
    scanf("%31s", username);
    printf("密码: ");
    scanf("%31s", password);
    
    pack.op = (uint8_t)choice;
    // 构建数据内容
    sprintf(pack.data, "%s %s", username, password);
    pack.data_len = htons(strlen(pack.data));
    
    // 发送协议包
    send(sock, &pack, sizeof(ProtocolPack), 0);
    
    ProtocolPack resp_pack;
    // 接收响应协议包
    recv(sock, &resp_pack, sizeof(ProtocolPack), 0);
    printf("结果: %s\n", resp_pack.data);
    
    close(sock);
    return 0;
}