基于 C 语言的 QQ 聊天室(TCP + 多线程 + SQLite3)
项目功能
基础功能: 登录、注册、添加好友、私聊、创建群聊、群聊
扩展功能: 删除好友、注销账号、好友在线状态、群管理(拉人/踢人)、VIP 特权、邮件通知等
功能介绍:
模拟QQ聊天
客户端:
登录界面:
1、登录
2、注册 //将用户注册账户/密码信息存储在数据里
3、注销
服务器:
转发信息:主要实现私聊和群聊的功能,只要通过服务器
进行数据传输,最好都先用json在客户端打包,然后在服务器中解包
处理信息:将从客户端发来的数据存储到数据库等操作
主界面:
1、添加好友 //是否需要同意
2、查看我的好友 //显示好友当前状态,是否在线
3、与好友私聊 //离线的信息怎么处理
4、删除好友 //单删
5、创建群聊
6、开始群聊 //禁言
7、拉好友进群 //群主和管理员的权限
8、踢人 //群主和管理员的权限
9、查看群中好友 //显示群主和管理员的权限、群员的权限
10、注销账号
11、公共朋友圈 //朋友圈//小游戏
推荐使用TCP通信来实现,因为TCP只需要将客户端的fd存储在数据库,那么我们就可以随时想要客户端的fd然后给其发送信息。
==========================================================
1、画架构图--->实现什么功能
2、设置结构体
3、设置协议
4、设置函数
===================================================
客户端:
>显示界面
printf("1、登录 2、注册 3、退出 4、注销");
单独写函数化界面
Switch(){
case 1:
login
break
case 2:
zhuce
break
case 3:
logout
break
case 4:
zhuxiao
break
}
//用户名:
密码:
...........
send到服务器中---》数据存入数据库
==================================================================
服务器:
1、并发服务器 考虑使用--->进程?线程?
2、接受客户端的数据
3、数据的转发
数据库:
1>打开数据库
注册---写入
登录---读取并匹配
2>可以添加相关功能
3>关闭数据库
参考:
服务器可以使用多路复用取创建线程,每一个线程都是单独和一个客户端进行通信
客户端登录完成后,创建线程,专门用来接受服务器发送给客户端的信息
数据库:需要多个table
用户总表:储存注册的用户和密码,登录后将他的fd也放进去,下线后再次登录就更新。
好友表:每当一个用户注册就创建一个好友表,里面储存该用户的好友名
群聊总表:存储所有被创建的群聊名
群聊成员总表:存储群聊里面的成员名
=====================================================================
1. 系统架构设计
🔹 服务器端
监听客户端连接
多线程处理每个客户端
使用 SQLite3 存储用户/好友/群信息
解析 JSON 数据包
转发私聊/群聊消息
🔹 客户端
建立 TCP 连接
JSON 格式封装数据
交互界面(菜单选择私聊/群聊等)
子线程接收服务器消息
🔹 1.1 架构图
+--------------------+ +------------------+
| 客户端 | TCP/IP | 服务器 |
+--------------------+ <------> +------------------+
| 1. 登录/注册 | | 1. 处理用户请求 |
| 2. 添加好友 | | 2. 保存数据到DB |
| 3. 私聊/群聊 | | 3. 发送/转发消息 |
| 4. 退出/注销 | | 4. 维护在线状态 |
+--------------------+ +------------------+
聊天室实现流程图:(实现框架)
实现聊天室的登陆、注册流程图
注册:
登陆:
以上实现的编程思路逻辑为:
注册:
1、服务器要先启动,监听客户端的连接;
2、客户端启动,首先连接服务器,并显示登陆、注册界面;
3、服务器接收到客户端连接后,会创建一个子线程专门用于于客户端的通信;
4、选择注册后,提示输入用户名、密码,封装注册信息到结构体变量msg中,并发送该信令给服务器;
5、服务器接收到客户端注册信息后,进入注册处理流程;
6、注册功能:首先在数据库表中查找该用户名是否存在,如果不存在则使flage值为1,并且在数据库表中将该用户名密码保存到数据库中,并返回注册成功的信令;
否则使flage值为-1,并返回 错误信息;
7、客户端接收到服务器注册处理指令后,会打印提示信息,并显示步骤2的菜单。
登陆:
1、服务器要先启动,监听客户端的连接;
2、客户端启动,首先连接服务器,并显示登陆、注册界面;
3、服务器接收到客户端连接后,会创建一个子线程专门用于于客户端的通信;
4、选择登陆后,提示输入用户名、密码,封装登陆信息到结构体变量msg中,并发送该信令给服务器;
5、服务器接收到客户端注册信息后,进入登陆处理流程;
6、登陆功能:首先查找该用户名、密码是否在数据库表中存在匹配项,找到返回对应的下标,并将于该客户端相连接的套接字保存到对应的条目中,返回登陆成功信息给客户端;如果没有找到,则返回-1,并返回错误信息给客户端;
7、客户端接收到服务器注册处理指令后,会打印提示信息,并设置客户端在线的标记login_f 为1,此时会显示 聊天功能对应的菜单。
客户端与服务端:私聊
私聊流程:
1、客户端从菜单选择私聊功能;
2、输入要聊天的对象和聊天信息;
3、发送聊天信息给服务器;
4、服务器的子线程收到私聊数据之后,进入私聊流程;
5、向指定用户发送该私聊信息;
客户端与服务端:公聊
公聊流程:
1、客户端从菜单选择公聊功能;
2、输入要聊天信息;
3、回车发送聊天信息;
4、服务器的子线程收到公聊数据之后,进入公聊流程;
5、查找所有在线用户,向所有的在线用户发送该公聊信息;
6、客户端进入聊天后会创建一个子线程,该子线程会循环接收所有服务器发送的数据信息。
🔹 1.2 技术选型
组件 | 技术方案 |
---|---|
通信协议 | TCP(稳定可靠) |
数据格式 | JSON(使用 cJSON 解析) |
服务器模型 | I/O 多路复用(select 或 epoll)+ 线程池 |
数据库 | SQLite3(轻量级) |
加密 | 密码 SHA-256 哈希存储 |
2. 数据库设计
🔹 2.1 用户表(users)
字段 | 类型 | 说明 |
---|---|---|
id | INTEGER PRIMARY KEY | 用户 ID |
username | TEXT UNIQUE | 用户名 |
password | TEXT | SHA-256 哈希存储 |
status | INTEGER | 0=离线, 1=在线 |
fd | INTEGER | 记录 socket 连接 |
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
status INTEGER DEFAULT 0,
fd INTEGER DEFAULT -1
);
🔹 2.2 好友表(friends)
记录用户之间的好友关系。
字段 | 类型 | 说明 |
---|---|---|
user_id | INTEGER | 用户 ID |
friend_id | INTEGER | 好友 ID |
CREATE TABLE friends (
user_id INTEGER,
friend_id INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (friend_id) REFERENCES users(id)
);
🔹 2.3 群聊表(groups)
记录群聊信息。
字段 | 类型 | 说明 |
---|---|---|
group_id | INTEGER PRIMARY KEY | 群 ID |
group_name | TEXT UNIQUE | 群名称 |
owner_id | INTEGER | 群主 ID |
CREATE TABLE groups (
group_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_name TEXT UNIQUE NOT NULL,
owner_id INTEGER NOT NULL,
FOREIGN KEY (owner_id) REFERENCES users(id)
);
🔹 2.4 群成员表(group_members)
记录群聊成员及身份。
字段 | 类型 | 说明 |
---|---|---|
group_id | INTEGER | 群 ID |
user_id | INTEGER | 用户 ID |
role | TEXT | owner/admin/member |
CREATE TABLE group_members (
group_id INTEGER,
user_id INTEGER,
role TEXT CHECK(role IN ('owner', 'admin', 'member')) DEFAULT 'member',
FOREIGN KEY (group_id) REFERENCES groups(group_id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
3. 通信协议(TCP)
🔹 3.1 数据包格式(JSON)
{
"type": "login",
"username": "Alice",
"password": "123456"
}
🔹 3.2 主要消息类型
操作 | type 值 | 附加字段 |
---|---|---|
登录 | “login” | username, password |
注册 | “register” | username, password |
私聊 | “private_chat” | from, to, message |
群聊 | “group_chat” | group_id, from, message |
添加好友 | “add_friend” | from, to |
删除好友 | “delete_friend” | from, to |
4. 服务器端实现
🔹 4.1 服务器初始化
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <pthread.h>
#define PORT 8080
#define MAX_CLIENTS 100
void *handle_client(void *sockfd) {
int client_sock = *(int *)sockfd;
char buffer[1024];
while (1) {
memset(buffer, 0, sizeof(buffer));
int bytes = recv(client_sock, buffer, sizeof(buffer), 0);
if (bytes <= 0) {
printf("客户端断开连接\n");
break;
}
printf("收到消息: %s\n", buffer);
send(client_sock, "消息收到", strlen("消息收到"), 0);
}
close(client_sock);
return NULL;
}
int main() {
int server_sock, client_sock;
struct sockaddr_in server_addr, client_addr;
socklen_t client_size = sizeof(client_addr);
server_sock = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_sock, MAX_CLIENTS);
printf("服务器启动, 监听端口 %d...\n", PORT);
while (1) {
client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_size);
printf("新客户端连接: %d\n", client_sock);
pthread_t thread;
pthread_create(&thread, NULL, handle_client, &client_sock);
pthread_detach(thread);
}
close(server_sock);
return 0;
}
5. 客户端实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
char buffer[1024];
while (1) {
printf("输入消息: ");
fgets(buffer, sizeof(buffer), stdin);
send(sock, buffer, strlen(buffer), 0);
memset(buffer, 0, sizeof(buffer));
recv(sock, buffer, sizeof(buffer), 0);
printf("服务器: %s\n", buffer);
}
close(sock);
return 0;
}
5. 线程池优化(使用 epoll 处理多个客户端)
#include <sys/epoll.h>
int epfd = epoll_create(1);
struct epoll_event event, events[MAX_CLIENTS];
event.events = EPOLLIN;
event.data.fd = server_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, &event);
while (1) {
int n = epoll_wait(epfd, events, MAX_CLIENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == server_sock) {
int client_sock = accept(server_sock, NULL, NULL);
event.data.fd = client_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &event);
} else {
char buffer[1024];
recv(events[i].data.fd, buffer, sizeof(buffer), 0);
printf("收到: %s\n", buffer);
}
}
}
以上为项目结构 和 模块化的编程思想,下面提供完整代码:
✅ 使用 TCP + JSON 进行消息传输;
✅ 基于 SQLite3 存储用户、好友、群聊信息;
✅ 服务器采用 多线程 + I/O 多路复用(epoll) 支持高并发。
客户端(client)
头文件集合
my.h
#ifndef __MY_H__
#define __MY_H__
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<poll.h>
#include<stdbool.h>
#include<json/json.h>
#include<sqlite3.h>
#include<sys/time.h>
#include<signal.h>
#include<semaphore.h>
#include<pthread.h>
#include<errno.h>
typedef struct msg{
char usr_name[20];
char passwd[20];
char qq_group[20];
char buf[100];
int qq;
int cmd;
}msg_q,*msg_p;
extern void struct_init(msg_p* qt);
extern void json_init(msg_p * p);
extern const char * json_pack(msg_p p);
extern msg_p json_unpack(const char * buf1);
extern int user_insert(int fd);
extern int login_qq(int fd);
extern int add_friend(int fd);
extern int sql_into(msg_p qt);
extern int del_friend(int fd);
extern int sql_del(int qq);
extern int tuichu_qq(int fd);
extern int siliao_friend(int fd);
extern int group_fd(int fd);
extern int group_creat(msg_p qt);
extern int add_f_gp(int fd);
extern int table_show();
extern int fun1(void* arg,int col,char** str,char** name);
extern int qunliao(int fd);
extern int del_g_f(int fd);
extern int chakan();
extern int chakan_gf(int fd);
extern int qun_lib();
#endif
json.h
#ifndef _JSON_H_
#define _JSON_H_
#include <stdio.h>
#include <string.h>
#include <json/json.h>
#include <stdlib.h>
typedef struct msg{
char usr_name[20];
char passwd[20];
char qq_group[20];
char buf[100];
int qq;
int cmd;
}msg_q,*msg_p;
extern void json_init(msg_p * p);
extern const char *json_pack(msg_p p);
extern msg_p json_unpack( const char *buf1);
#endif
主函数
client.c
#include"my.h"
int fd;
extern int sdnum;
int main(int argc, char const *argv[]) //./client server_ip server_port
{
if (argc!=3)
{
printf("usage : %s <server_ip> <server_port>\n",argv[0]);//传参:服务器ip + 端口号
exit(1);
}
msg_p qq;
msg_p qq1;
json_init(&qq);
json_init(&qq1);
const char* auf;
char buf[521];
fd=socket(AF_INET,SOCK_STREAM,0);
if (fd<0)
{
perror("socket");
exit(1);
}
int ret;
struct sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(atoi(argv[2]));
server_addr.sin_addr.s_addr=inet_addr(argv[1]);
ret=connect(fd,(struct sockaddr *)&server_addr,sizeof(server_addr));
if (ret<0)
{
perror("connect");
exit(1);
}
/*
fd_set myset;
while(1)
{
FD_ZERO(&myset);
FD_SET(0,&myset);
FD_SET(fd,&myset);
select(fd+1, &myset, NULL,NULL,NULL);
if(FD_ISSET(0,&myset)){*/
end:
while(1)
{
fd_set myset;
while(1)
{
FD_ZERO(&myset);
FD_SET(0,&myset);
printf("-------------\n");
printf("----1.注册---\n");
printf("----2.登录---\n");
printf("----3.退出---\n");
printf("-------------\n");
struct_init(&qq);
printf("please chose:\n");
select(fd+1, &myset, NULL,NULL,NULL);
scanf("%d",&qq->cmd);
if(FD_ISSET(0,&myset)){
switch(qq->cmd){
case 1:
user_insert(fd);//注册
bzero(buf,sizeof(buf));
struct_init(&qq1);
recv(fd,buf,sizeof(buf),0);
qq1=json_unpack(buf);
if(qq1->cmd==1){
printf("注册成功\n");
printf("你的QQ号为:%d\n",qq1->qq);
}else
printf("注册失败\n");
break;
case 2:
login_qq(fd);//登录
bzero(buf,sizeof(buf));
struct_init(&qq1);
recv(fd,buf,sizeof(buf),0);
qq1=json_unpack(buf);
if(qq1->cmd==1){
printf("正在登录...\n");
sleep(2);
}else{
printf("帐号或密码错误\n");
break;
}
// menu();
while(1)
{
fd_set myset;
while(1)
{
begin:
FD_ZERO(&myset);
FD_SET(0,&myset);
FD_SET(fd,&myset);
int qq_num;
int i;
//sleep(5);
system("clear");
printf("4.添加好友\n");
printf("5.删除好友\n");
printf("6.与好友私聊\n");
printf("7.创建群聊\n");
printf("8.拉好友进群\n");
printf("9.开始群聊\n");
printf("10.查看我的好友\n");
printf("11.查看群中的好友\n");
printf("12.退出登录\n");
printf("请输入数字选择相应操作\n");
select(fd+1, &myset, NULL,NULL,NULL);
if(FD_ISSET(fd,&myset))
{
do{
bzero(buf,sizeof(buf));
ret=recv(fd,buf,sizeof(buf),0);
struct_init(&qq1);
qq1=json_unpack(buf);
printf("%s\n",qq1->buf);
if(qq1->cmd==1000)
{
goto begin;
}
}while(qq1->cmd!=0);
sleep(3);
break;
}
scanf("%d",&i);
if(FD_ISSET(0,&myset)){
switch(i){
case 4:
add_friend(fd);//添加好友
sleep(2);
break;
case 5:
printf("请输入你要删除好友的QQ:");
scanf("%d",&qq_num);
//del_friend(fd);
bzero(buf,sizeof(buf));
struct_init(&qq1);
qq1->cmd=5;
qq1->qq=qq_num;
auf=json_pack(qq1);
send(fd,auf,strlen(auf),0);
recv(fd,buf,sizeof(buf),0);
qq1=json_unpack(buf);
if(qq1->cmd==1)
{
sql_del(qq_num);
printf("删除成功\n");
sleep(2);
break;