项目背景
基于 TCP/IP 的网络多人聊天室是一种旨在提供实时的文字交流平台,使多个用户能够在网络上进行即时聊天和互动的应用程序。它通过利用 TCP/IP 协议栈中的传输控制协议(TCP)和 Internet 协议(IP)来实现消息的传输和网络通信。
该网络多人聊天室是基于传输层TCP协议由服务端模块与客户端模块组成。服务器端负责接收和转发消息,而客户端负责与服务器建立连接并发送/接收消息。TCP协议是一种一对一连接协议,它在客户端和服务端之间建立可靠的通信连接,确保数据的可靠性和有序性。因此,客户端只能与服务端进行一对一连接通信,但可通过服务器与其他客户端完成一对多通信。
- 服务端作为该网络聊天室的核心组件,负责接受来自客户端的连接请求,维护客户端的连接状态,并处理消息的转发。
- 服务器端通常使用单线程或多线程的模型来处理客户端的连接和消息。每个客户端连接会被分配一个独立的线程,负责处理该客户端的消息收发。
- 服务器端需要维护一个用户列表或用户数据库,用于管理在线用户和其相关信息,如用户名、IP 地址等。服务器端还负责处理系统通知、用户加入/退出聊天室等特殊事件的处理。
- 客户端是用户使用的界面,允许用户输入消息并发送给服务器,同时接收其他用户发送的消息。
- 客户端可以由线程组成,这种方式通常称为多线程编程。多线程编程是一种并发编程的方式,允许在一个程序中同时执行多个线程,每个线程都可以执行客户端要求的任务,并且多个线程可以共享一个进程的资源,可以提高系统资源利用率并节约内存空间。
- 客户端需要与服务器建立连接,并通过套接字(Socket)与服务器进行通信。客户端通常提供用户注册、登录、选择聊天室、显示在线用户列表等功能。客户端还负责将用户输入的消息发送给服务器,并将接收到的消息显示在用户界面上。
软件开发平台
- Ubuntu20.04操作系统
- 基于ssh网络远程连接Linux系统的VScode代码编辑器
- 开发语言:C语言
功能分析
服务器功能
-
登录界面
- 循环服务器。循环登录功能。
- 可以显示好友信息。好友名称 + 好友 ip + port
- 保存聊天信息 + 保存日志内容。//保存到文件中。或者是链表中。或者数据库中。
- 多用户登录功能。并发服务器。使用多线程技术,多进程技术。
- 群聊功能。转发功能。私聊功能。单独与好友聊天。
- 上线提醒功能。某人上线了,所有人都知道。服务器给所有人发送信息。
-
用户标识符功能:jack> mike: linkda= //字符串连接函数。
- 任务排斥,其他任务抢占现象:线程互斥锁 条件变量 信号量。进程文件锁,进程信号量。
-
信号功能。信号注册。signal sigaction
- 接收文件。//接受文件名 + 以这个名字命名一个文件 + 接受文件内容。
客户端功能
- 登录成功以后:欢迎信息。
- 信号注册功能。ctrl + c : 显示当前时间。
- 发送文件。
- 互斥锁。文件锁 信号量。
- 自定义功能。广告功能,QQ秀功能。群通知功能。匿名聊天功能。时间戳功能。踢人功能。敏感词屏蔽。
聊天室系统服务端、客户端模块运行流程图
- 服务端运行流程图
- 客户端运行流程图
客户端模块相关代码
-
用户登录
//定义用户信息节点
typedef struct userdata{
void *data;
struct userdata *next;
}user_t;
//data指针 数据指向
typedef struct logdata{
char username[16];
char userpasswd[16];
int user_cid;
}log_t;
log_t *login(char ip[],unsigned int port)
{
FILE *fp = fopen("user_data.txt","a+");
fseek(fp,0,SEEK_SET);
log_t *logdata = (log_t *)malloc(sizeof(log_t *));
// 1.存用户名
printf("\tWelcome to the chat room\n\n");
printf("username:");
scanf("%s",logdata->username);
fprintf(fp,"username:%s\n",logdata->username);
// 2.存密码
printf("password:");
scanf("%s",logdata->userpasswd);
fprintf(fp,"password:%s\n",logdata->userpasswd);
// 存储用户信息 IP + Port
fprintf(fp,"IP:%s\n",ip);
fprintf(fp,"Port:%d\n",port);
// 关闭文件
fclose(fp);
return logdata;
}
void get_time(FILE * fp)
{
time_t currentTime = time(NULL);
struct tm *localTime = localtime(¤tTime);
fprintf(fp,"time: %d-%02d-%02d %02d:%02d:%02d\n\n",
localTime->tm_year + 1900,
localTime->tm_mon + 1,
localTime->tm_mday,
localTime->tm_hour,
localTime->tm_min,
localTime->tm_sec);
}
void sigfun(int signo)
{
// ctrl + c
if(signo == SIGINT)
{
time_t currentTime = time(NULL);
struct tm *localTime = localtime(¤tTime);
printf("time: %d-%02d-%02d %02d:%02d:%02d\n\n",
localTime->tm_year + 1900,
localTime->tm_mon + 1,
localTime->tm_mday,
localTime->tm_hour,
localTime->tm_min,
localTime->tm_sec);
}
}
-
具体实现
-
#include "cJSON.h" int file_pass_flag = 0; void file_pass(int signo) { // ctrl + z if(signo == SIGTSTP) { // printf("file_pass_flag = %d\n",file_pass_flag); // file_pass_flag = 21; // printf("file_pass_flag = %d\n",file_pass_flag); system("sl"); } } int main(int argc, char const *argv[]) { if(argc < 5) { perror("usage:./client + s_IP + Port + c_IP + Port\n"); return -1; } // !-------------select准备部分------------------ // 输入文件描述符集合 fd_set rset = {0}; // 设定超时时间 struct timeval tm = {0}; // 有数据变化的文件描述符个数 int count = 0; char bufh[128] = ""; // !--------------------------------------------- // 已经登录 char tcname[16] = ""; // strcpy(tcname,login((char *)argv[1],atoi(argv[2]))); log_t *users_data = login((char *)argv[1],atoi(argv[2])); if (strcmp(users_data->username,"")) { printf("\nusersname:%s---欢迎\n",users_data->username); signal(SIGINT,sigfun); } strcpy(tcname,users_data->username); //建立客户端套接字 int cid = socket(AF_INET,SOCK_STREAM,0); printf("cid = %d\n",cid); //connect struct sockaddr_in caddr = {0}; caddr.sin_family = AF_INET; caddr.sin_port = htons(atoi(argv[2])); caddr.sin_addr.s_addr = inet_addr(argv[1]); if(connect(cid,(struct sockaddr*)&caddr,sizeof(caddr)) < 0 ) { perror("connect error\n"); return -3; } printf("connect successful\n"); // !看看是否成功接收到客户端登录name printf("---%s---\n",tcname); // !在这里初始化一个接收服务器信息的地址 char rbuf[512] = ""; char bufv[128] = ""; struct sockaddr_in rAddr = {0}; rAddr.sin_family = AF_INET; rAddr.sin_addr.s_addr = inet_addr(argv[3]); rAddr.sin_port = htons(atoi(argv[4])); // 接收条件 cJSON *json_res = NULL; cJSON *json_name = NULL; cJSON *json_code = NULL; cJSON *json_info = NULL; cJSON *json_sign = NULL; cJSON *fuhao = NULL; cJSON *json_ip = NULL; cJSON *json_port = NULL; // !私聊对象 cJSON *json_chat_name = NULL; // 先发送自己的客户端用户名给服务器 send(cid,(char *)tcname,sizeof(tcname),0); char bufs[128] = ""; int len = 0 ; //------------------json的数据封装。------------------------------// cJSON * json_pointer = NULL; //创建一个链表数据对象。 json_pointer = cJSON_CreateObject(); //1添加字符串类型到节点当中 姓名 char cname[16] = ""; cJSON_AddStringToObject(json_pointer,"name",tcname); //2 添加整型数据 密码 char cpasswd[16] = "1221"; // 匹配密码 FILE *cfp = fopen("user_data.txt","r+"); // fscanf(cfp,"password:%s\n",cpasswd); cJSON_AddStringToObject(json_pointer,"code",cpasswd); //3 添加字符串类型到节点当中 信息 //cJSON_AddStringToObject(json_pointer,"info","hello linux"); //4 添加字符串类型到节点当中 签名 cJSON_AddStringToObject(json_pointer,"sign","Fear of violence"); //添加字符串类型到节点当中 cJSON_AddStringToObject(json_pointer,"fuhao",">"); // 添加IP字符串到节点中 char cIP[16] = ""; strcpy(cIP,argv[3]); printf("cIP = %s\n",cIP); cJSON_AddStringToObject(json_pointer,"IP",cIP); // 添加端口号到节点中 unsigned int cPort = htons(atoi(argv[4])); cJSON_AddNumberToObject(json_pointer,"Port",cPort); //数据整理 char * str = NULL; char chat_name[16] = ""; //循环读写 while(1) { bzero(chat_name,sizeof(chat_name)); bzero(bufs,sizeof(bufs)); bzero(bufv,sizeof(bufv)); // !select循环读写设置部分 FD_SET(cid,&rset); FD_SET(STDIN_FILENO,&rset); tm.tv_sec = 2; count = select(cid+1,&rset,NULL,NULL,&tm); signal(SIGTSTP,file_pass); // 键盘有动作,说明客户端要发数据 if (FD_ISSET(STDIN_FILENO, &rset)) { // 选择聊天对象 read(STDIN_FILENO, chat_name, sizeof(chat_name) - 1); if (file_pass_flag == 0) { // 从键盘读数据再写给对面 read(STDIN_FILENO, bufs, sizeof(bufs) - 1); // 增加私聊功能 printf("chat:%s",chat_name); cJSON_AddStringToObject(json_pointer, "chat_name", chat_name); // 聊天信息部分 cJSON_AddStringToObject(json_pointer, "info", bufs); // jason格式转换成字符串格式 str = cJSON_Print(json_pointer); FILE * fp = fopen("chat_logs.txt","a+"); fprintf(fp,"%s-->%sinfo:%s",tcname,chat_name,bufs); get_time(fp); send(cid, str, strlen(str), 0); // 发送过去之后,进行删除节点。 if (!strncmp(bufs, "quit", 4)) { cJSON_DeleteItemFromObject(json_pointer, "info"); break; } cJSON_DeleteItemFromObject(json_pointer, "info"); cJSON_DeleteItemFromObject(json_pointer,"chat_name"); printf("提示:客户端发送信息时,先输入聊天对象,再输入聊天内容\n"); fclose(fp); } if (file_pass_flag == 21) { char filename[16] = ""; printf("输入一个文件名:"); scanf("%s",filename); strncpy(filename,filename,strlen(filename)-1); FILE * fp = fopen(filename,"w+"); printf("请输入文件内容:"); // 清空键盘缓存区域 fflush(stdin); // fputs(); } signal(SIGTSTP,SIG_DFL); } if (FD_ISSET(cid,&rset)) { printf("服务器正在发送信息~\n"); bzero(rbuf, sizeof(rbuf)); // 接收服务器发出数据 len = recv(cid, rbuf, sizeof(rbuf), 0); // 解析rbuf json_res = cJSON_Parse(rbuf); // 姓名 json_name = cJSON_GetObjectItem(json_res, "name"); // 密码 json_code = cJSON_GetObjectItem(json_res, "code"); // 信息 json_info = cJSON_GetObjectItem(json_res, "info"); // 签名 json_sign = cJSON_GetObjectItem(json_res, "sign"); // 符号 fuhao = cJSON_GetObjectItem(json_res, "fuhao"); // IP json_ip = cJSON_GetObjectItem(json_res, "IP"); // Port json_port = cJSON_GetObjectItem(json_res, "Port"); // !聊天对象 json_chat_name = cJSON_GetObjectItem(json_res, "chat_name"); printf("来自:%s的信息_:%s",json_name->valuestring,json_info->valuestring); printf("%s %s的个性签名:%s\n",fuhao->valuestring,json_name->valuestring,json_sign->valuestring); printf("IP:%s\tPort:%d\n",json_ip->valuestring,json_port->valueint); } } shutdown(cid,SHUT_RDWR); fclose(cfp); return 0; }
服务端模块相关代码
-
功能实现
#include "cJSON.h"
//静态 cid ;
char bufs[512] = "";
char bufv[128] = "";
int len = 0 ;
// 建立链表,在服务器中建立链表,方便使用
user_t *head = NULL;
void * pthread_fun(void * arg)
{
//cid是从主线程传过来的。
int pcid = *(int *)arg;
// !-------------select准备部分------------------
// 输入文件描述符集合
fd_set rset = {0};
// 设定超时时间
struct timeval tm = {0};
// 有数据变化的文件描述符个数
int count = 0;
char bufh[128] = "";
// !---------------------------------------------
// 已经在主线程中创建链表了并使用头插法插入新的客户端链表
// !------------为了通过name转发而做准备,准备一个单链表--------------
user_t *temp = head->next;
char sname[16] = "";
bzero(sname,sizeof(sname));
recv(pcid,(char *)sname,sizeof(sname),0);
while(temp!=NULL)
{
if (((log_t *)(temp->data))->user_cid == pcid)
{
memcpy(((char *)(((log_t *)(temp->data))->username)),sname,16);
printf("%s is online\n",(char *)(((log_t *)(temp->data))->username));
break;
}
temp = temp->next;
}
// !-----------------------------------------------------------------
//清空bufs
bzero(bufs,sizeof(bufs));
cJSON * json_res = NULL;
cJSON * json_name = NULL;
cJSON * json_code = NULL;
cJSON * json_info = NULL;
cJSON * json_sign = NULL;
cJSON * fuhao = NULL;
cJSON * json_ip = NULL;
cJSON * json_port = NULL;
// !私聊对象
cJSON * json_chat_name = NULL;
while(1)
{
// !select循环读写设置部分
FD_SET(pcid,&rset);
FD_SET(STDIN_FILENO,&rset);
tm.tv_sec = 10;
count = select(pcid+1,&rset,NULL,NULL,&tm);
//cid 要传递过来
bzero(bufs,sizeof(bufs));
// !接收
len = recv(pcid,bufs,sizeof(bufs),0);
//解析bufs
json_res = cJSON_Parse(bufs);
// 姓名
json_name = cJSON_GetObjectItem(json_res,"name");
// 密码
json_code = cJSON_GetObjectItem(json_res,"code");
// 信息
json_info = cJSON_GetObjectItem(json_res,"info");
// 签名
json_sign = cJSON_GetObjectItem(json_res,"sign");
// 符号
fuhao = cJSON_GetObjectItem(json_res,"fuhao");
// IP
json_ip = cJSON_GetObjectItem(json_res,"IP");
// Port
json_port = cJSON_GetObjectItem(json_res,"Port");
// !聊天对象
json_chat_name = cJSON_GetObjectItem(json_res,"chat_name");
temp = head->next;
int send_num = 1;
int send_jud = 0;
while (temp!=NULL)
{
// 判断是否私发给服务器
if (!strncmp(json_chat_name->valuestring,"server",6)&&send_num--)
{
printf("来自:%s的信息_:%s",json_name->valuestring,json_info->valuestring);
printf("%s %s的个性签名:%s\n",fuhao->valuestring,json_name->valuestring,json_sign->valuestring);
printf("IP:%s\tPort:%d\n",json_ip->valuestring,json_port->valueint);
printf("chat_name:%s",json_chat_name->valuestring);
send_num = 0;
send_jud = 1;
}
// 通过服务器转发给其他用户 --- 比较name
if (!strncmp(json_chat_name->valuestring,((log_t *)temp->data)->username,strlen(json_chat_name->valuestring)-1))
{
// 经过测试,确实存在转发,并且转发的字节数是对的
send_jud = send(((log_t *)temp->data)->user_cid,bufs,strlen(bufs),0);
}
// 群发
if (!strncmp(json_chat_name->valuestring,"all",3))
{
user_t *temp_all = head->next;
while(temp_all!=NULL)
{
if (strncmp(((log_t *)(temp_all->data))->username,json_name->valuestring,strlen(json_name->valuestring)))
{
send_jud = send(((log_t *)temp->data)->user_cid,bufs,strlen(bufs),0);
}
temp_all = temp_all->next;
}
if (send_num)
{
// 服务器显示的部分
printf("来自:%s的信息_:%s",json_name->valuestring,json_info->valuestring);
printf("%s %s的个性签名:%s\n",fuhao->valuestring,json_name->valuestring,json_sign->valuestring);
printf("IP:%s\tPort:%d\n",json_ip->valuestring,json_port->valueint);
send_num = 0;
}
}
temp = temp->next;
}
if(!send_jud)
{
printf("不存在该用户:%s",json_chat_name->valuestring);
}
send_jud = 0;
if (!strncmp(json_info->valuestring,"quit",4))
{
printf("来自IP:%s的客户端正在退出~\n",json_ip->valuestring);
break;
}
temp = head->next;
}
//线程退出 - cid关闭是不是一件事?
//shutdown(pcid,SHUT_RDWR);
close(pcid);
// ! pcid = -1;
delete_vlinklist(head,pcid);
printf("来自IP:%s的客户端退出完毕~\n",json_ip->valuestring);
//线程退出
pthread_exit((void *)0);
}
//多用户登录 - 并发服务器 - 线程方法
int main(int argc, char const *argv[])
{
if(argc < 3)
{
perror("usage:./server + ip + port\n");
return -1;
}
head = create_linklist();
//.建立套接字
int sid = socket(AF_INET,SOCK_STREAM,0);
printf("sid = %d\n",sid);
//实际地址结构体
struct sockaddr_in saddr = {0};
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr(argv[1]);
saddr.sin_port = htons(atoi(argv[2]));
if(bind(sid,(struct sockaddr*)&saddr,sizeof(saddr))<0)
{
perror("bind error\n");
return -2;
}
printf("bind successful\n");
//3.监听
listen(sid,10);
//4.循环连接
char bufs[128] = "";
int len = 0 ;
pthread_t tid = 0 ;
//只要初始化一次,在全局数据区
static int cid = -1;
while (1)
{
//主线程。 链接。
cid = accept(sid,NULL,NULL);
if(cid != -1)
{
// !这段说明与建立客户端连接成功
log_t *log_data = (log_t*)malloc(sizeof(log_t));
log_data->user_cid = cid;
head = insert_hlinklist(head,log_data);
//把线程设定成分离属性 - 线程执行完毕以后,自动释放占用的空间。
pthread_create(&tid,NULL,pthread_fun,&cid);
pthread_detach(tid);
printf("有新用户登录 cid = %d\n",cid);
}
}
// 销毁链表
destroy_linklist(&head);
close(sid);
return 0;
}
- 服务端应用数据结构
//1.创建一个单链表 user_t * create_linklist() { user_t * head =(user_t *)malloc(sizeof(user_t)); if(head ==NULL) { perror("create error\n"); return NULL; } head->data = NULL; head->next = NULL; return head; } //2.头插法插入结点 user_t * insert_hlinklist(user_t * head, log_t * log_data) { //数据判断 if(log_data == NULL||head==NULL) { perror("parameter error !\n"); return (user_t *)-1; } //申请空间 user_t * newnode = (user_t *)malloc(sizeof(user_t)); if(newnode==NULL) { perror("newnode create error!\n"); return (user_t *)-2; } newnode->data = (log_t *)malloc(sizeof(log_t)); memcpy(((log_t *)(newnode->data)),log_data,sizeof(log_t)); newnode->next = head->next; head->next = newnode; return head; } //根据部分信息输出全部信息 typedef int (*cmpfun_t)(void *data1,void *data2); //1.比对函数 int cmpname(void *data1,void *data2) { log_t* newdata1 = (log_t*)data1; log_t* newdata2 = (log_t*)data2; return strcmp(newdata1->username,newdata2->username); } user_t * Search_linklist(user_t * head,void *value,cmpfun_t cmpfun) { //1.参数判断 if(head == NULL || head->next == NULL || value == NULL || cmpfun == NULL) { perror("ERROR!\n"); return (user_t*)-1; } //2. user_t * temp = head->next; while (temp != NULL) { if (!cmpfun(temp->data,value)) { return temp; } temp = temp->next; } return NULL; } // 按cid查找并且删除 节点 user_t * delete_vlinklist(user_t*head,unsigned int cid) { //1.参数判断 if (head == NULL || head->next==NULL) { /* 用perror也可以*/ printf("head is null or linklist is illegal\n"); return (user_t *)-1; } //2.遍历 user_t*temp = head->next,*before = head; while (temp != NULL &&(((log_t *)(temp->data))->user_cid!=cid)) { //before先移动 before = temp; //temp再移动 temp = temp->next; } //出界判断 if (temp == NULL) { perror("this value is not in this linklist\n"); return (user_t *)-1; } //3.修改指针 before->next = temp->next; free(temp); temp = NULL; return head; } //删除所有结点的数据并销毁链表 int destroy_linklist(user_t **head) { if(*head == NULL) { perror("linklist is not exits\n"); return -1; } //先取出元素 再取出成员 user_t*temp =(*head)->next ,*before = NULL; (*head) ->next = NULL; while( temp != NULL) { before = temp; temp = temp->next; free(before); } free(*head); *head = NULL; return 0 ; }
在该项目的信息传输过程中应用了CJSON,用于解析JSON数据、构建JSON对象和数组,以及将JSON数据列化为字符串,使数据在网络传输后能够方便地被另一端解析和处理。
心得体会
- 这个项目让我有机会亲自实践多线程编程方法,并且我在这个过程中取得了一些实践成果。学会了如何在两个不同的主机之间进行通信,学习到了如何利用不同的信号功能来提高代码的可读性。
- 在网络编程方面,我学会了使用套接字(socket)进行网络连接,发送和接收数据。这让我对网络通信的原理有了更深入的理解,并且能够编写简单的网络应用程序来实现数据交换。
- 我学习到了如何使用信号来增强代码的可读性。通过合理地使用信号,我可以在代码中引入清晰的逻辑和结构。例如,我可以使用信号来处理异常情况或处理特定的事件等。