目录
3.可以显示好友信息。好友名称 + 好友 ip + port
5.成员信息保存链表中。并加载在服务端与客户端本地文件中(两份).
(后两个功能设置为root用户专属)(su root + password)
8.上线提醒功能。某人上线了,所有人都知道。服务器给所有人发送信息.
9.用户标识符功能(name: **** time), 发送方本人无需标识。
10.信号功能signal。(crtl + c 时会结束音乐播放,避免孤儿进程)
11.音乐播放功能。多进程技术服务端播放bgm,server和client都可以控制bgm的暂停和播放及切歌。
12.切换背景功能。服务端和客户端可切换随机背景,也可恢复background。
前言:项目简述
简述:TCP(即传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。套接字 socket一种文件类。类型:1流式套接字 SOCK_STREAM 2数据报套接字 3原始套接字。tcp采用的数据流套接字,udp采用的数据报套接字。由于udp传输数据时会丢包,故采用tcp协议搭建聊天室。
主要功能及实现
1.登录界面进度条动态加载
通过移动光标的位置,不断填补和覆盖数组中内容,数字百分比,加上延时,达到进度条的效果
自定义进度条的长度,跟printf中长度要同步,百分比跟加载进度同步,直至100%。
#include "process.h"
const char* lable = "|/-\\";
void processbar(int rate , int Rate)
{
if(rate <0 || rate >TOP )//合理性判断
{
return;
}
if(rate == 0 )//当比率为0的时候将数组全置为'\0'
{
memset(bar,'%', sizeof(bar)); //置为EOF更为直观但是会导致第二行的末尾多一个~ 后面发现不是EOF的原因
bar[sizeof(bar)]='\0'; //解决如上的问题 successed 虽然不是很严谨,越界了
}
int len = strlen(lable);
if(i!=0) //i只有等于0才为false
{
i--;
printf("Downloading");
printf(" ");
printf("["LIGHT_PURPLE"%-40s"NONE"]""[%d%%][%c]\r", bar, Rate, lable[rate%len]); //xx s字符串长度
usleep(10000);
fflush(stdout); //清空输出缓冲区
}
else
{
if(j>5)
{
i=j;
j=0;
}
else
{
j++;
printf("Downloading >_<");
printf("["LIGHT_PURPLE"%-40s"NONE"]""[%d%%][%c]\r", bar, Rate, lable[rate%len]); //xx s字符串长度
usleep(10000);
fflush(stdout); //清空输出缓冲区
}
}
/* 用于不点点
// printf("Downloading ...");
// printf("["LIGHT_PURPLE"%-100s"NONE"]""[%d%%][%c]\r", bar, Rate, lable[rate%len]); //xx s字符串长度
// usleep(50000);
// fflush(stdout); //清空输出缓冲区
*/
bar[rate++] = STYLE;
if(rate < TOP)
{
bar[rate] = BODY;
}
}
void Download(callback_t ct)
{
int total = (10*TOP);//假设总共要下载 个MB 6 改成这样我随便定进度条长度
int cur = 0;//当前下载的
int Rate = 0;
int rate = 0;
//printf("i=%d\n",i); //测试用,检查i出错原因
while(cur <= total)
{
rate = cur*TOP/total;
Rate = cur*100/total;
ct(rate,Rate);
usleep(100000);//模拟下载花费时间
cur += (total/TOP);//循环下载了一部分,更新进度
}
printf("Download Success! ^_^");
putchar(10);
}
2.循环服务器。循环登录功能。
while(1)中循环,accept阻塞并等待客户端,bind成功后创建线程执行交互操作,使用for循环判断存储cid的数组中空的数组内容并填充,避免了用户退出再登录时产生的冲突。
3.多用户登录功能。并发服务器。使用多线程技术.连接
while (1)
{
// 主线程 阻塞
cid = accept(sid, (struct sockaddr *)&caddr, &size);
for (int i = 0; i < MAX; i++) // 遍历一遍 不会调试会发现出错 退出后空数组cid没有填补
{
if (cidnum[i] == 0)
{
cidnum[i] = cid;
cidflg++;
break;
}
}
strcpy(ipp, (inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ip, sizeof(ip)))); // ip 和 port
port = ntohs(caddr.sin_port);
// printf("a user is login and he is :%d\n", cid);
printf("a user is login\n");
// 输出对面的信息 : 对面是客户端
// printf("client ip:%s client port:%d\n", ipp,port);
time(&tim);
stim = ctime(&tim); // 更新时间
strcpy(name, (namecmp(port, ip, linklist, cid))); // 比对用户 显示Ip 和 端口号 及用户信息 并且将用户插入链表
// printf("name: %s\n",name); //测试用
// showlinklist(linklist); // 调试用, 观察是否将用户插入链表中
if (strcmp(name, "hhh") != 0)
{
strcpy(sbuf, name);
// 群发代码 时间戳加进来;
strcat(sbuf, "上线");
strcat(sbuf, "\t\t");
strcat(sbuf, stim);
fprintf(fp, "%s\n", sbuf); // 写入文件中
fflush(fp);
strcpy(ciddd1.str, sbuf);
bzero(sbuf, sizeof(sbuf)); // 使用后记得扫尾
}
else
{
strcpy(sbuf, "未知用户上线");
strcat(sbuf, "\t\t");
strcat(sbuf, stim);
fprintf(fp, "%s\n", sbuf); // 写入文件中
fflush(fp);
strcpy(ciddd1.str, sbuf);
bzero(sbuf, sizeof(sbuf));
}
ciddd1.cidd = cid;
strcpy(ciddd1.name, name);
// strcpy((char *)(ciddd1.linklist),(char *)linklist); //强转失败 额直接传
ciddd1.linklist = linklist; // 传链表头
printf("当前%d个用户\n", cidflg);
if (cid != -1)
{
pthread_create(&tid1, NULL, pthread_fun1, &ciddd1);
pthread_detach(tid1);
}
}
printf("服务器退出\n");
close(sid);
fclose(fp);
wait(NULL);
return 0;
}
4.可以显示好友信息。好友名称 + 好友 ip + port
使用链表保存了上线成员的信息,增加使用的尾插法,链表实现增删改查功能,并且可以实时展示成员信息,比使用数组存储成员方便许多。
#include "header.h"
#include "lianbiao.h"
#include "name.h"
user_t * create_linklist(void)//创建头结点的函数 user_t* linklist =create_linklist();
{
//申请空间
user_t *head=(user_t*)malloc(sizeof(user_t));
if(head == NULL)
{
perror("nalloc error\n");
return NULL;
}
//填写数据
head->age = 0;
strcpy(head->name,"user");
strcpy(head->sex,"sex");
//指针问题
head->next = NULL;
return head;
}
int insert_tlinklist(user_t * head,user_t user)
{
//1参数判断
if(head == NULL)
{
perror("linklist is error\n");
return -1;
}
//2申请空间+数据填写
user_t* newnode=(user_t*)malloc(sizeof(user_t));
if(newnode ==NULL)
{
perror("malloc error/n");
return -2;
}
//找尾巴 头连尾
user_t*tail=head;
while(tail->next != NULL)
{
tail=tail->next;
}
//赋值语句
newnode->age = user.age;
newnode->cid = user.cid;
strcpy(newnode->name,user.name);
strcpy(newnode->sex,user.sex);
//3修改指针
newnode->next = NULL;
tail->next = newnode;
return 0;
}
//打印结点数据
void* showlinklist(user_t*head)
{
name_t userss[10]={0};
if(head==NULL || head->next==NULL)
{
perror("暂时没有用户上线 \n");
return (void*)0;
}
int i = 0;
user_t *temp=head ->next;
while(temp!=NULL)
{
userss[i].age = temp->age;
strcpy(userss[i].name,temp->name);
strcpy(userss[i].sex,temp->sex);
printf("name:%s\tsex:%s\tage:%d\t\n",temp->name,temp->sex,temp->age);
temp=temp->next;
i++;
}
name_t *p = userss;
return p;
}
int delete_ilinklist(user_t * head,char *sname)
{
//1.判断参数
if(head == NULL || head -> next ==NULL || sname == NULL)
{
printf("parameters error or user is null\n");
return -999;
}
//遍历
user_t* before = head;
user_t* temp = head->next;
//判断出界
while(temp != NULL )
{
if(strcmp(sname,temp->name)==0) //不相等时继续执行
break; //相等时跳出
else
{
before = temp;
temp= temp ->next;
}
}
//3判断跳出循环情况
if(temp == NULL)
{
printf("没有找到此人\n");
return -999;
}
else{
//temps才是目标节点
user_t* temps = temp;
before->next =temps -> next;
//返回数据的保存
char *ret = temps->name;
int ccid = temps -> cid;
//释放
printf("%s已从列表中清除\n",ret);//要放在free前面才生效 因为ret是指针指向temps->name
free(temps);
return ccid; //不能这样传 XXX 首选ret 被释放掉了,即使没被释放,由于返回的是整型,
//return *ret; // 取的是字符串的首地址,所以只会返回字符串的第一个数的ascii值。
}
}
int seek_ilinklist(user_t * head,char *sname)
{
//1.判断参数
if(head == NULL || head -> next ==NULL || sname == NULL)
{
printf("parameters error or user is null\n");
return -999;
}
//遍历
user_t* temp = head->next;
//判断出界
while(temp != NULL )
{
if(strcmp(sname,temp->name)==0) //不相等时继续执行
break; //相等时跳出
else
{
temp= temp ->next;
}
}
//3判断跳出循环情况
if(temp == NULL)
{
printf("没有找到此人\n");
return -999;
}
else{
//temps才是目标节点
//返回数据的保存
char *ret = temp->name;
int ccid = temp -> cid;
return ccid; //不能这样传 XXX 首选ret 被释放掉了,即使没被释放,由于返回的是整型,
//return *ret; // 取的是字符串的首地址,所以只会返回字符串的第一个数的ascii值。
}
}
5.保存聊天信息 + 保存日志内容。保存到文件中。
使用文件保存服务端发送过来的消息,以及在线成员数据
buffsize为宏定义,我定义的1024,一次可最多接收1MB数据,注意发送端与接收端要同步
发送端:
char buffer[BUFFER_SIZE];
FILE *file;
long file_size;
name_t *userr = (name_t *)(showlinklist(linklistt)); // userr接住
if (userr == NULL)
{
printf("成员为空\n"); // 其实按道理不用判的,但是暂时没有踢掉成员
// close(file);
continue; // 跳过本次循环
}
// 打开要发送的文件
file = fopen("./user.txt", "w+"); // 每次读写都冲刷一次
if (!file)
{
perror("File open failed");
}
// 接住后写入到本地文件中
for (int i = 0; i < cidflg; i++)
{
fprintf(file, "name:%s age:%d sex:%s\n", (*(userr + i)).name, (*(userr + i)).age, (*(userr + i)).sex);
fflush(file);
}
// 获取文件大小
fseek(file, 0, SEEK_END);
file_size = ftell(file); // 文件指针位置就是大小
rewind(file);
// 发送文件大小
if (send(cidn, &file_size, sizeof(file_size), 0) == -1)
{
perror("File size send failed");
}
// 逐块读取文件内容并发送
while (!feof(file))
{
size_t bytesRead = fread(buffer, 1, BUFFER_SIZE, file);
if (send(cidn, buffer, bytesRead, 0) == -1)
{
perror("File send failed");
}
}
printf("File sent successfully\n");
// 关闭文件
fclose(file);
接收端:
FILE *file;
long file_size;
// 接收文件大小
if (recv(cidnn, &file_size, sizeof(file_size), 0) == -1)
{
perror("File size receive failed");
}
// 创建要接收的文件
file = fopen("./recv.txt", "w+");
if (!file)
{
perror("File creation failed");
}
// 逐块接收文件内容并写入文件 每次接收的bytesRead个字节
long bytesReceived = 0;
while (bytesReceived < file_size)
{
size_t bytesRead = recv(cidnn, buffer, BUFFER_SIZE, 0);
if (bytesRead == -1)
{
perror("File receive failed");
}
bytesReceived += bytesRead;
fwrite(buffer, 1, bytesRead, file);
}
printf("File received successfully\n");
// 关闭文件
fclose(file);
6.成员信息保存链表中。并加载在服务端与客户端本地文件中(两份).
通过上述链表给出代码中,查找结构体数组中对应的成员信息,show后用结构体指针接住然后循环打印保存在本地文件然后发送服务端,服务端再接收链表中内容,同样的方法保存在本地文件中。一式两份,文件打开类型为w+,每次show时清空文件中内容再写入。
注意不能传指针,不管是链表指针还是文件指针亦或是结构体指针,都是不行的,会发生段错误。
printf("\n");
char buffer[BUFFER_SIZE];
FILE *file;
long file_size;
name_t *userr = (name_t *)(showlinklist(linklisttt)); // userr接住
if (userr == NULL)
{
printf("成员为空\n"); // 其实按道理不用判的,但是暂时没有踢掉成员
// close(file);
continue; // 跳过本次循环
}
// 打开要发送的文件
file = fopen("./user.txt", "w+"); // 每次读写都冲刷一次
if (!file)
{
perror("File open failed");
}
// 接住后写入到本地文件中
for (int i = 0; i < cidflg; i++)
{
fprintf(file, "name:%s age:%d sex:%s\n", (*(userr + i)).name, (*(userr + i)).age, (*(userr + i)).sex);
fflush(file);
}
// 获取文件大小
fseek(file, 0, SEEK_END);
file_size = ftell(file); // 文件指针位置就是大小
rewind(file);
// 发送文件大小
if (send(cidn, &file_size, sizeof(file_size), 0) == -1)
{
perror("File size send failed");
}
// 逐块读取文件内容并发送
while (!feof(file))
{
size_t bytesRead = fread(buffer, 1, BUFFER_SIZE, file);
if (send(cidn, buffer, bytesRead, 0) == -1)
{
perror("File send failed");
}
}
printf("File sent successfully\n");
// 关闭文件
fclose(file);
}
7.群聊功能。转发功能。私聊功能。单独与好友聊天。
(后两个功能设置为root用户专属)(su root + password)
这里我采用的双线程模式,在主线程下分有子线程,然后在子线程下又分出一个子线程,实现了
超级用户的效果,输入su root后,需要输入正确的密码,输入错会提示错误。进入root模式后可以
踢人和私聊功能。注意printf输出时清空缓冲区,否则数据会堵死在缓冲区内无法输出。
8.上线提醒功能。某人上线了,所有人都知道。服务器给所有人发送信息.
关于上线功能,使用的自己定义的结构体数组进行比对,比对登录用户的端口号,输出user的ip和
port,以及user的所有信息,由于时间有限,这里我只定义了10个用户的信息,然后其他端口号登录过来客户都被我归为无名氏系列,并在其后加上数字来加以区分,具体代码如下。
定义的指针函数,这样返回的是一个指针,用于返回登录用户的姓名,作用。
void * namecmp(int port,char *ip,user_t *head,int cid)
{
char *name = "hhh";
char yes[32] = "";
for (int i = 0 ;i < MAX ; i++)
{
if (port == (10000+i)) //根据端口值 已知用户
// if ((port%10)==i) //根据端口尾数 不妥
{
printf("欢迎%s上线 ip:%s port:%d\n",user[i].name,ip,port);
printf("name:%s age:%d sex:%s \n",user[i].name,user[i].age,user[i].sex);
strcpy(users.name,user[i].name);
strcpy(users.sex,user[i].sex);
users.cid=cid;
users.age = user[i].age;
insert_tlinklist(head,users); //尾插
strcpy(yes,user[i].name);
bzero(users.name,sizeof(users.name));
name = yes;
return name;
}
}
sx++;
char sxx =(sx + '0') ; //i to c
strcat(users.name,"无名氏");
strncat(users.name,&sxx,strlen(&sxx)-1);
strcpy(users.sex,"NULL");
users.age = 0;
insert_tlinklist(head,users); //尾插
strcpy(yes,users.name);
bzero(users.name,sizeof(users.name));
name = yes;
printf("未知用户上线\n");
// printf("return:%s\n",name);//测试用
return name;
}
9.用户标识符功能(name: **** time), 发送方本人无需标识。
这个没啥需要讲述的,就是连接用户名和其所说的内容加上说话时的时间。
用strcat函数即可实现。
10.信号功能signal。(crtl + c 时会结束音乐播放,避免孤儿进程)
除了线程部分一个逻辑问题卡了四五个小时外,这里的音乐播放从构思到实现也卡了近两个小时。使用mpg123播放器,在王者那篇文章中有讲用法。这次使用,换了一种用法,上次没有学多进程,霸王硬上弓用了多进程播放音乐,execl函数族,好在没出大问题。这次使用时发现一处问题,当强制终止服务端程序时,音乐播放会变成孤儿进程。而且是只有当循环播放音乐时会如此,单次播放音乐不会。后来解决方法是使用信号功能函数signal,在杀死父进程前捕捉到SIGINT信号,然后调用kill将子进程杀死后恢复默认功能。
也可以使用ctrl + z 暂停进程,但是我发现难以捕捉到此信号,有一些bug,由于不是很重要所以没有去深究了。
11.音乐播放功能。多进程技术服务端播放bgm,server和client都可以控制bgm的暂停和播放及切歌。
由于每次都是用的终端命令来播放音乐,觉得很枯燥无味,所以这次特意去问了chat老师哈哈哈,然后通过脚本实现了音乐的播放功能。具体如下,注意路径改用自己音乐存放路径
#!/bin/bash
# 设置音乐文件目录
MUSIC_DIR="/home/chenyi/hhh/x系统编程/project/music"
# 找到音乐目录下的所有mp3文件
music_files=$(find "$MUSIC_DIR" -type f -name "*.mp3")
# 随机选择一个音乐文件
random_music=$(echo "$music_files" | shuf -n 1)
# 播放随机选择的音乐文件
mpg123 "$random_music"
12.切换背景功能。服务端和客户端可切换随机背景,也可恢复background。
想玩一些花里胡哨的功能,然后除了进度条外,还增加了背景切换的功能,由于只是展示作用,所以我只选了8种颜色来充当背景色,计算并设置RGB值,获得自己想要的颜色,大家可以直接去查找自己喜欢的颜色然后修改代码即可,然后通过随机数种子,循环展示完所有背景色后选出随机的一种颜色来充当背景色。具体代码实现如下所示。
#include "header.h"
#define t1 150000
int background()
{
int n = 0;
srand((unsigned)time(NULL));
n = rand() % 8;
for(int i=0;i<4;i++)
{
// 改变背景颜色为粉色
system("clear");
printf("\e]11;rgb:ff00/de00/de00\e\\");
fflush(stdout);
usleep(t1);
//白色
printf("\e]11;rgb:ff00/ffff/ff00\e\\");
fflush(stdout);
usleep(t1);
//黄色
printf("\e]11;rgb:ff00/ff00/0000\e\\");
fflush(stdout);
usleep(t1);
//红色
printf("\e]11;rgb:ff00/0000/0000\e\\");
fflush(stdout);
usleep(t1);
//紫色
printf("\e]11;rgb:9b00/3000/ff00\e\\");
fflush(stdout);
usleep(t1);
//棕色
printf("\e]11;rgb:cd00/b300/8b00\e\\");
fflush(stdout);
usleep(t1);
//水蓝色
printf("\e]11;rgb:9700/ff00/ff00\e\\");
fflush(stdout);
usleep(t1);
//浅灰色
printf("\e]11;rgb:6600/8b00/8b00\e\\");
fflush(stdout);
usleep(t1);
}
switch (n)
{
case 0: // 改变背景颜色为粉色
system("clear");
printf("\e]11;rgb:ff00/de00/de00\e\\");
fflush(stdout);
break;
case 1: // 白色
printf("\e]11;rgb:ff00/ffff/ff00\e\\");
fflush(stdout);
break;
case 2: // 黄色
printf("\e]11;rgb:ff00/ff00/0000\e\\");
fflush(stdout);
break;
case 3: // 红色
printf("\e]11;rgb:ff00/0000/0000\e\\");
fflush(stdout);
break;
case 4: // 紫色
printf("\e]11;rgb:9b00/3000/ff00\e\\");
fflush(stdout);
break;
case 5: // 棕色
printf("\e]11;rgb:cd00/b300/8b00\e\\");
fflush(stdout);
break;
case 6: // 水蓝色
printf("\e]11;rgb:9700/ff00/ff00\e\\");
fflush(stdout);
break;
case 7: // 浅灰色
printf("\e]11;rgb:6600/8b00/8b00\e\\");
fflush(stdout);
break;
default:
printf("\e]11;rgb:0000/0000/0000\e\\");
fflush(stdout);
break;
}
return 0;
}
int bbackground(void)
{
// 恢复原来的背景颜色 黑色
// printf("\e]11;rgb:0000/0000/0000\e\\");
// 恢复原来的背景颜色 白色
printf("\e]11;rgb:ff00/ff00/ff00\e\\");
fflush(stdout);
return 0;
}
总结: 项目总结
通过本次项目,加强了自己的思维逻辑,当几十个报错蜂拥而上,却能应付自如,增加了心理承受能力哈哈哈,开玩笑的。但当通过几十分钟几个小时,甚至一晚上的思考,将问题解决后的喜悦感是平时所感受不到的。勤能补拙,相信只要我们付出努力,所有的问题都会迎刃而解,方法总比困难多,共勉!