基于TCP协议的QQ聊天室

目录

前言:项目简述

主要功能及实现

1.登录界面进度条动态加载

2.循环服务器。循环登录功能。

3.可以显示好友信息。好友名称 + 好友 ip + port

4.保存聊天信息 + 保存日志内容。保存到文件中。

5.成员信息保存链表中。并加载在服务端与客户端本地文件中(两份).

6.多用户登录功能。并发服务器。使用多线程技术.

7.群聊功能。转发功能。私聊功能。单独与好友聊天。

(后两个功能设置为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;
}

总结: 项目总结

通过本次项目,加强了自己的思维逻辑,当几十个报错蜂拥而上,却能应付自如,增加了心理承受能力哈哈哈,开玩笑的。但当通过几十分钟几个小时,甚至一晚上的思考,将问题解决后的喜悦感是平时所感受不到的。勤能补拙,相信只要我们付出努力,所有的问题都会迎刃而解,方法总比困难多,共勉!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值