文章目录
1 概述
1.1聊天室设计内容
设计一个聊天系统,实现一下内容:
-
用户管理
用户注册、登录、修改密码 -
聊天室管理
用户登录、创建聊天室、设置聊天室密码、用户可以加入聊天室、退出聊天室 -
聊天管理
在同一聊天室里,用户所发送的消息每位在线用户都可以收到,也可以单独给某位在线用户发消息;给所有在线用户群发消息 -
系统管理
显示所有在线用户;显示所有聊天室;可以查询聊天室在线用户信息;提供命令帮助,让用户了解命令的格式例如:
send user1 message1 /*表示给用户user1发送消息message1等*/
2 系统设计
2.1系统功能设计
网络聊天室系统分为服务器和客户端,客户端主要进行命令的发送,以及接收从服务器发送过来的信息。服务器主要进行指令的解析,然后根据解析出来的指令执行相关的函数,将执行的结果返回给客户端。
2.1.1用户管理
用户管理要完成的用户的注册、登录和密码修改。在服务器程序未启动时,已注册的用户信息存储在文件中。服务器程序启动之后把用户的用户名和密码加载到结构体数组中,当用户登录时,依次遍历数组,将数组中的用户名和用户输入的用户名进行比较,若用户名相同,继续比较密码,若密码相同,则登录成功,否则登录失败。若在数组中没找到用户输入的用户名,则告知用户该账号不存在。
用户注册时,引导用户输入用户名和密码,将用户输入的用户名和密码存入结构体数组中,若成功执行,则告知用户注册成功。
当用户想要修改密码时,需要输入更改密码命令和你想要更改的密码,然后从在结构体数组中查询该用户,找到之后将其旧密码修改为新密码,并提示用户密码修改成功。
当服务器程序退出运行时,将数组中的用户名和密码已“w+”的模式(即先清除文件里的内容,再写入新的内容)写入文件,这样就可以保证每次新注册的用户的信息都存入文件中。用一句话来总结用户管理就是,当服务器程序未运行时,用户信息存在文件中;服务器程序运行时,将用户信息加载到内存;当服务器程序关闭时,再将用户信息从内存写入到文件中。
2.1.2聊天室管理
聊天室存于结构体数组中。用户登录之后便可以创建聊天室,创建聊天室时,用户需要输入聊天室的名字和密码,然后依次遍历聊天室结构体数组,当找到一个空闲的聊天室时,便将用户输入的名字和密码赋给该聊天室,并且还要将创建者加入到该聊天室的成员中。
用户加入聊天室时,需要输入聊天室的名字和聊天室的密码。程序先判断用户是否已经加入了聊天室,如果用户已经加入了聊天室,则提醒用户已经加入了聊天室,不能再加入另外一个聊天室。如果用户还未加入聊天室,则遍历整个聊天室结构体数组,将用户输入的聊天室名和数组中的聊天室名进行比较,若相同继续比较密码,若密码不行同,则告知用户密码不正确,若密码相同,则把用户加入到当前聊天室的成员中,告知用户加入聊天室成功。
当用户退出聊天室时,遍历整个聊天室结构体数组,找到用户加入的聊天室,然后将用户从该聊天室中删除,告知用户成功退出聊天室。
2.1.3聊天管理
用户发送消息的情况有三种,分别为1.发送给聊天室;3.群发信息,所有人都能收到;2.发送给私人(私聊)。用户发送信息都用到了“send”命令,可以根据命令后的选项确定用户要发送的是哪种类型的信息。
- 聊天室消息
发送聊天室消息“send”后面跟“-chatroom”选项。当用户发送聊天室消息时,先查询用户在哪个聊天室中,若没找到,则告知用户还未加入聊天室。若找到用户所在的聊天室,则用一个循环向聊天室中的所有成员发送消息。 - 群发消息
群发消息后跟“send”后面跟“-all”选型。当用户发送群发消息时,只需要用一个循环,向所有在线的用户消息即可。 - 私聊
发送私聊信息,“send”后面跟你要发送的用户的用户名。当用户发送私聊信息时,先查询被发送用户是否存在或在线,若被发送用户不存在或不在线,则提醒用户发送失败。若被发送用户存在且在线,则将信息发送过去,告知用户发送成功。
2.1.4系统管理
显示在线用户信息、显示所有聊天室信息、显示所有聊天室在线用户信息,都用到了“ls”命令,根据命令后的选型确定用户要显示的是什么。
- 显示在线用户信息
显示在线用户信息在“ls”后面跟“-users”选项。显示在线用户信息只需要直接遍历在线用户结构体数组,把查询到的用户返回给客户端显示出来即可。 - 显示所有聊天室信息
显示所有聊天室信息在“ls”后面跟“-chatrooms”选项。显示所有聊天室信息只需要遍历聊天室结构体数组,找出可用的聊天室,并把查询结果返回给客户端。 - 显示所有聊天室在线用户信息
显示所有聊天室在线用户信息在“ls”后面跟“-inrmusr”选项。显示所有聊天室在线用户信息,需要先从聊天室结构体数组中找到要查询的聊天室,然后遍历该聊天室中的用户,将用户信息返回给客户端显示。
帮助信息分为两部分,一部分为用户开始运行客户端程序时提供的帮助信息,有注册、登录、显示帮助信息、退出四条帮助信息;另一部分帮助信息需要输入“help”命令获取,它包含了系统需要用到的所有命令的使用帮助信息。“help”命令获取帮助信息不需要从服务器获取,直接在客户端调用函数来显示。
2.2系统数据结构设计
-
struct user结构体
struct user { char username[20]; //用户名 char password[20]; //用户密码 };
该结构体用来存储从文件中加载到内存中的所有用户信息,无论用户是否在线,都把用户存在该结构体中。该结构体包含用户的用户名和密码。
-
struct user_socket结构体
struct user_socket { char username[20]; int socketfd; int status; //标识是否在线 0:在线 -1:下线 };
该结构体用来存储在线用户,“username”是用户名,“socketfd”是服务器与客户端建立连接时,服务器存储套接字文件描述符数组的下标,“status”标识用户是否在线,初始时为-1。
-
struct chatroom结构体
struct chatroom { char name[20]; char passwd[20]; int user[10]; //聊天室成员 int status; //标识是否正在使用 0:使用中 -1:销毁 };
该结构体用来存储聊天室,“name”字符数组为聊天室名,“passwd”为聊天室密码,“user”数组存放加入聊天室的成员,存入的是内容与结构体struct user_socket中“socketfd”相同,“status”标识该聊天室是否在使用中,初始时为-1。
-
int connfd[]套接字描述符数组
该数组为一个整型数组,里面存的是客户端和服务器建立连接时的套接字,要访问某一个套接字直接使用数组下标访问。初始时为-1。
2.3系统主要函数设计
2.3.1客户端
-
int main()
客户端主程序中主要完成设置服务器地址,创建客户端流式套接字,打印登录前的帮助信息,之后程序分为两个线程,一个线程去执行客户端的发送函数,另一个线程继续在主函数中完成接收消息的任务。
-
void snd()
该函数由一个线程执行,作用是接收客户端的输入,若是接受到的输入是“help”或“quit”,则直接调用get_hep()函数或是退出客户端程序;若是其他命令,则直接发给服务器处理。
2.3.2服务器
-
int main()
服务器主程序主要完成服务器地址的设置,创建服务器流式套接字,将地址结构和套接字绑定,设置侦听端口。然后创建一个线程,该线程接受“quit”命令,用于退出服务器程序。最后接收客户端连接,针对每一个套接字建立一个线程,对当前套接字的消息进行处理。
-
void init()
完成相应结构体数组和变量的初始化,将用户从文件中读入内存。结构体数组主要由三个,分别为:
struct user users[MAXMEM]; //记录所有用户 struct user_socket online_users[MAXMEM]; //记录在线用户 struct chatroom chatrooms[MAXROOM]; //记录聊天室
初始时要把online_users的status、chatroomd的status以及chatroom里的user数组全部初始化为-1。
-
void save_users()
将内存中的的所有用户信息从新写入到文件中。
-
void quit()
该函数由一个线程执行,当服务器输入“quit”命令时,执行该函数,主要调用save_users()函数,将所有用户写入文件,关闭套接字。
-
void rcv_snd(int n)
在客户端与服务器建立连接后,针对每一个套接字建立一个线程来执行该函数,该函数完成服务器的接收和发送功能。
参数 含义 n 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 例如:建立的套接字为
connfd[i] = accept(listenfd, (struct sockaddr *)&cli_addr, &len);
则传入的参数为rcv_snd(i);
该函数主要分为两个循环。第一个循环在用户登录前执行,主要完成用户的登录或者注册,当用户登录成功后,跳出该循环去执行第二个循环。第二个循环接收从客户端发来的命令,并将命令解析,根据解析后的命令决定执行的函数,最后将执行的结果写回给客户端。
-
int user_login(int n)
该函数在客户端输入“login”命令后执行,完成用户的登录。
参数 含义 n 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 返回值为0或-1,登录成功返回0,失败返回-1。
函数执行时,提示用户输入用户名和密码,然后在users[]数组中查询该用户的用户名,若未找到,则告知“Account does not exist.”,返回-1;若找到用户名,则比较密码是否一致,若密码不一致,则告知“Wrong password.”返回-1;若密码一致,登录成功,告知用户“Login successfully.”,返回0。
-
void register_user(int n)
该函数在客户端输入“register”命令后执行,完成用户注册。
参数 含义 n 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 函数执行时,提示用户输入要注册的用户名和密码,然后验证要注册的用户名是否存在,若用户名已存在,则告知用户“The username already exists.”,函数结束执行;若用户名不存在,则将用户名和密码存入users[]数组中,同时告知用户“Account created successfully.”。
-
void change_passwd(int sfd, char *passwd)
该函数在用户输入命令“chgpsw xxx”后执行,完成用户密码的修改。“xxx”想要修改为的密码,作为参数传入该函数。
参数 含义 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 passwd 想要修改为的密码 -
void send_private_msg(char *username, char *data, int sfd)
该函数在用户输入命令“send username xxx”后执行,完成聊天系统的私聊功能。“username”为信息接收人的用户名,“xxx”为发送消息的内容。
参数 含义 username 信息接收人的用户名 data 发送消息的内容 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 函数执行时遍历在线用户数组,查询被发送用户的用户名,若没找到用户,告知客户端“User is not online or user does not exist.”;若找到用户,则把消息发给该用户,同时将“Sent successfully”返回给客户端。
-
void send_all_msg(char *msg, int sfd)
该函数在用户输入命令“send -all xxx”后执行,完成用户信息的群发功能,“xxx”为发送的消息的内容。
参数 含义 msg 发送的消息的内容 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void send_chatroom_msg(char *msg, int sfd)
该函数在用户输入命令“send -chatroom xxx”后执行,完成用户消息在聊天室内的发送,“xxx”为发送的消息的内容。
参数 含义 msg 发送的消息的内容 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void get_online_users(int sfd)
该函数在用户输入命令“ls -users”后执行,完成查询所有在线用户的功能。
参数 含义 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void get_online_chatrooms(int sfd)
该函数在用户输入命令“ls -chatrooms”后执行,完成查询所有已创建房间的功能。
参数 含义 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void get_inroom_users(int sfd)
该函数在用户输入命令“ls -inrmusr”后执行,完成查询用户所加入聊天室的所有成员信息。
参数 含义 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void create_chatroom(char *name, char *passwd, int sfd)
该函数在用户输入命令“create chatroom passwd”后执行,完成创建一个叫做“chatroom”密码为“passwd”的聊天室。
参数 含义 name 聊天室的名字 passwd 聊天室的密码 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void join_chatroom(char *name, char *passwd, int sfd)
该函数在用户输入命令“join chatroom passwd”后执行,完成加入一个叫做“chatroom”加入密码为“passwd”的聊天室。
参数 含义 name 聊天室的名字 passwd 聊天室的加入密码 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void exit_chatroom(int sfd)
该函数在用户输入命令“exit”后执行,完成退出聊天室的功能。
参数 含义 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void quit_client(int n)
该函数在用户输入命令“quit”后执行,表示用户要退出客户端程序,服务器要关闭与该用户的套接字,同时修改该用户的状态为下线,退出与该用户连接时创建的线程。
参数 含义 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标 -
void invalid_command(int sfd)
该函数在客户端输入无效命令时执行,向客户端返回“Invalid command.”的提示信息。
参数 含义 sfd 建立服务器与客户端时存入connfd[]数组的套接字的对应下标
3 系统实现
3.1 开发环境配置
系统环境:Ubuntu 18.04
gcc版本:7.5.0
编辑器:visual studio code version 1.45
visual studio code配置:
-
launch.json
{ "version": "0.2.0", "configurations": [ { "name": "gcc - 生成和调试活动文件", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "MIMode": "gdb", "preLaunchTask": "build", "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "preLaunchTask": "C/C++: gcc build active file", "miDebuggerPath": "/usr/bin/gdb" } ] }
-
tasks.json
{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "gcc", "args": [ "${file}", "-o", "${fileBasenameNoExtension}", "-lpthread" ] } ] }
因为pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以编译的时候需要加上-lpthread。
3.2 功能模块的程序流程图及运行界面
3.2.1功能模块流程图
-
用户管理
-
聊天室管理
-
聊天管理
-
系统管理
3.2.2运行界面
-
登录界面
-
登录成功
-
登录失败
-
查看帮助信息
-
查看所有在线用户
-
发送群聊消息
-
发送私聊信息
strong向trump发送“我爱中国!”私聊信息,trump成功收到私聊信息。 -
私聊信息发送失败
strong向不在线的用户elito发送消息。 -
创建聊天室
strong创建聊天室test,trump创建聊天室America。 -
查看所有在使用中聊天室
-
加入聊天室失败
pony加入聊天室test时聊天室密码输入错误。 -
成功加入聊天室
pony成功加入聊天室test。 -
查看聊天室成员
pony成功加入了strong创建的聊天室test。 -
发送聊天室消息
strong发送了一条群聊信息,群里只有strong和pony,所以只有pony和strong接收到了群聊信息。 -
退出聊天室
pony退出聊天室test。 -
聊天室外不能查看聊天室成员
pony退出了聊天室,所以不能查看聊天室成员信息。 -
退出聊天室后不能再接收到聊天室消息
pony退出了聊天室test,所以不能收到strong在聊天室test发出的聊天室消息。 -
不能同时加入两个聊天室
trump已经加入了聊天室America,所以不能再加入聊天室test。
3.3 关键代码分析说明
-
以为聊天室的客户端和服务器连接是不能断开的,除非有一方程序停止运行,所以客户端和服务器之间的连接采用TCP协议。
/*服务器*/ struct sockaddr_in serv_addr, cli_addr; int i; time_t timenow; pthread_t thread; char buff[BUFFSIZE]; printf("Running...\nEnter command \"quit\" to exit server.\n\n"); bzero(&serv_addr, sizeof(struct sockaddr_in)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); listenfd = socket(AF_INET, SOCK_STREAM, 0); // 建立流式套接字 if (listenfd < 0) { perror("fail to socket"); exit(-1); } if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("fail to bind"); exit(-2); } listen(listenfd, MAXMEM);
/*客户端*/ struct sockaddr_in serv_addr; // struct sockaddr_in char buf[BUFFSIZE], temp[BUFFSIZE]; // 初始化服务端地址结构 bzero(&serv_addr, sizeof(struct sockaddr_in)); // bzero 清零 serv_addr.sin_family = AF_INET; // sin_family AF_INET serv_addr.sin_port = htons(PORT); // sin_port htons(PORT) inet_pton(HOST_IP, &serv_addr.sin_addr); // inet_pton // 创建客户端套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); // socket 创建流式套接字 if (sockfd < 0) { perror("fail to socket"); exit(-1); } // 与服务器建立连接 printf("connecting... \n"); if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("fail to connect"); exit(-2); }
-
服务器对待每一个来自客户端的连接的处理方式是:服务器先从connfd[]中找到一个空位,然后再从侦听队列中选取一个加入到该空位,创建于客户端的连接。每一个连接创建一个单独的线程去处理。
while (1) { int len; for (i = 0; i < MAXMEM; i++) { if (connfd[i] == -1) break; } // accept 从listen接受的连接队列中取得一个连接 connfd[i] = accept(listenfd, (struct sockaddr *)&cli_addr, &len); if (connfd[i] < 0) { perror("fail to accept."); } // 针对当前套接字创建一个线程,对当前套接字的消息进行处理 pthread_create(malloc(sizeof(pthread_t)), NULL, (void *)(&rcv_snd), (void *)i); }
-
服务器中最重要的函数是rcv_snd(),它接收来自客户端的命令,并将命令解析,根据不同的命令执行不同的功能,最后将结果返回给客户端。
该函数有两个循环,第一个循环是在用户登录前执行,用于引导用户登录和注册,一旦当用户登录成功,遍退出该循环,接着执行第二个循环。
第二个循环主要完成用户登录后,对用户发来的命令进行解析,解析命令用到了函数ssanf(),它能根据设置的格式将一个字符串中空格隔开的字符串分离出来,具体用法为:
sscanf(temp, "%s %s %[^\n]", command, arg1, arg2);
temp位为用户发来的命令,它是一整个字符串;command、arg1、arg2都是字符串,用来接收从temp分离出来的字符串。
例如:当temp=“send pony have a nice day!”,解析后command=“send”,arg1=“pony”,arg2=“have a nice day!”;当temp=“ls -users"时,解析后command=“ls”,arg1=”-users",arg2=""。
然后再根据解析出的命令执行不同的功能。
void rcv_snd(int n) { ssize_t len; int i; char mytime[32], buf[BUFFSIZE]; char temp[BUFFSIZE]; char command[20], arg1[20], arg2[BUFFSIZE]; time_t timenow; while (1) { len = read(connfd[n], buf, BUFFSIZE); if (len > 0) { buf[len - 1] = '\0'; // 去除换行符 if (strcmp(buf, "login") == 0) { //登录成功时退出该循环 if (user_login(n) == 0) { break; } } else if (strcmp(buf, "register") == 0) { register_user(n); } else if (strcmp(buf, "quit") == 0) { quit_client(n); } } } while (1) { if ((len = read(connfd[n], temp, BUFFSIZE)) > 0) { temp[len - 1] = '\0'; sscanf(temp, "%s %s %[^\n]", command, arg1, arg2); //解析命令 /*根据解析出的命令执行不同的函数*/ if (strcmp(command, "send") == 0 && strcmp(arg1, "-all") == 0) { send_all_msg(arg2, n); } else if (strcmp(command, "send") == 0 && strcmp(arg1,"-chatroom")==0) { send_chatroom_msg(arg2, n); } else if (strcmp(command, "send") == 0) { send_private_msg(arg1, arg2, n); } else if (strcmp(command, "quit") == 0) { quit_client(n); } else if (strcmp(command, "chgpsw") == 0) { change_passwd(n, arg1); } else if (strcmp(command, "create") == 0) { create_chatroom(arg1, arg2, n); } else if (strcmp(command, "join") == 0) { join_chatroom(arg1, arg2, n); } else if (strcmp(command, "ls") == 0 && strcmp(arg1, "-chatrooms")==0) { get_online_chatrooms(n); } else if (strcmp(command, "ls") == 0 && strcmp(arg1, "-users") == 0) { get_online_users(n); } else if (strcmp(command, "ls") == 0 && strcmp(arg1, "-inrmusr") == 0) { get_inroom_users(n); } else if (strcmp(command, "exit") == 0) { exit_chatroom(n); } else { invalid_command(n); } } } }
-
在服务器退出服务,执行quit()函数时,一定要把内存总的用户信息以“w+”模式从新写入文件,这样才能保障数据的一致性。
/*将用户保存到文件*/ void save_users() { int i; char buf[20]; FILE *fp = NULL; fp = fopen("users.txt", "w+"); for (i = 0; i < user_count; i++) { strcpy(buf, users[i].username); strcat(buf, "\n"); fprintf(fp, buf); strcpy(buf, users[i].password); strcat(buf, "\n"); fprintf(fp, buf); } fclose(fp); }
4 程序调试分析
-
从文件读出数据时数据缺失,于用户输入的注册时输入的用户名和密码不一致。
一开始设计时存入文件的数据没有转行,每条数据用一个特殊字符结尾,然后读的时候读到特殊符号就停止,将该条数据读出,再读下一条数据,直到文件结尾。然而这样在读出数据时很容易出错,并且代码繁琐。
解决方法:一条数据占一行,写入文件时加入’\n’,读出数据时用fscanf(),忽略结尾的’\n’,这样就能得到正确的数据,并且代码简化了不少。
//写入数据 strcpy(buf, users[i].username); strcat(buf, "\n"); //加上换行符 fprintf(fp, buf); strcpy(buf, users[i].password); strcat(buf, "\n"); fprintf(fp, buf); //读出数据 while (fscanf(fp, "%s", buf) != EOF) { strcpy(users[user_count].username, buf); fscanf(fp, "%s", buf); //忽略结尾的'\n' strcpy(users[user_count].password, buf); user_count++; }
-
当一个用户登录系统,然后下线,调用查看在线用户信息时,仍然能看到该用户的信息。
问题来源:结构体struct user_socket没有标识用户是否在线的标志,若一个用户退出聊天系统,只要还存在于struct user_socket数组中,就可以认为他还是在线的。
解决方法:在结构体struct user_socket添加一个状态status,标识用户是否在线。
struct user_socket { char username[20]; int socketfd; int status; //标识是否在线 0:在线 -1:下线 };
-
在用户加入聊天室时,同时比较聊天室名、聊天室密码和用户输入的聊天室名、聊天室密码,但是这种比较方法在某些情况下会造成bug,破坏系统运行。
解决方法:加入聊天室时正确的比较逻辑是先比较聊天室名是否一样,若聊天室名一样再进一步比较聊天室密码。
菜鸡之作,避免不了纰漏和疏忽,各位大佬若有任何问题,欢迎在评论区讨论
参考文献
[1] 宋敬彬 《Linux网络编程》,清华大学出版社,2014
[2] 菜鸟教程 ,C语言教程 https://www.runoob.com/cprogramming/c-tutorial.html
附:源代码
- client.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define BUFFSIZE 128
#define HOST_IP "192.168.159.3"
#define PORT 8888
int sockfd;
void snd();
void get_help();
- client.c
#include "client.h"
int main()
{
pthread_t thread; // pthread_t 线程,gcc编译时需加上-lpthread
struct sockaddr_in serv_addr; // struct sockaddr_in
char buf[BUFFSIZE], temp[BUFFSIZE];
// 初始化服务端地址结构
bzero(&serv_addr, sizeof(struct sockaddr_in)); // bzero 清零
serv_addr.sin_family = AF_INET; // sin_family AF_INET
serv_addr.sin_port = htons(PORT); // sin_port htons(PORT)
inet_pton(HOST_IP, &serv_addr.sin_addr); // inet_pton
// 创建客户端套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0); // socket 创建套接字
if (sockfd < 0)
{
perror("fail to socket");
exit(-1);
}
// 与服务器建立连接
printf("connecting... \n");
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
perror("fail to connect");
exit(-2);
}
printf("Enter \"login\" to login\n");
printf("Enter \"register\" to create an account\n");
printf("Enter \"quit\" to quit\n");
printf("Enter \"help\" to get more help\n\n");
/* === 从此处开始 程序分做两个线程 === */
// 创建发送消息的线程,调用发送消息的函数snd
pthread_create(&thread, NULL, (void *)(&snd), NULL); // pthread_create
// 接收消息的线程
while (1)
{
int len;
if ((len = read(sockfd, buf, BUFFSIZE)) > 0) // read 读取通信套接字
{
write(1, buf, len); //1:标准输出
printf("\n");
}
}
return 0;
}
/*发送消息的函数*/
void snd()
{
char buf[BUFFSIZE];
while (1)
{
fgets(buf, BUFFSIZE, stdin);
if (strcmp(buf, "help\n") == 0)
{
get_help();
continue;
}
if (strcmp(buf, "\n") != 0)
write(sockfd, buf, strlen(buf));
if (strcmp(buf, "quit\n") == 0) // 注意此处的\n
exit(0);
}
}
/*获取帮助信息*/
void get_help()
{
printf("Commands introduction:\n");
printf("\t'ls -users':\t\tShow all online users\n");
printf("\t'ls -chatrooms':\tShow all chat rooms\n");
printf("\t'ls -inrmusr'\t\tShow all online users in chat room you joined\n");
printf("\t'send username msg':\tSend a message to the user named 'username' msg:the content of the message\n");
printf("\t'join chatroom passwd':\tJoin in a chat room named 'chatroom' with password 'passwd'\n");
printf("\t'create chatrname passwd':\tCreate a chat room named 'chatrname' with password 'passwd'\n");
printf("\t'chgpsw passwd':\t\tChange your password to 'passwd'\n");
printf("\t'send -chatroom msg':\tSend a message to the chat room\n");
printf("\t'exit':\t\t\tExit the chat room you joined\n");
printf("\t'send -all msg':\tSend a message to all online users\n");
printf("\t'login':\t\tLogin chat system\n");
printf("\t'register':\t\tCreate an account\n");
printf("\t'quit':\t\t\tExit chat system\n");
printf("\t'help':\t\t\tGet more help information\n\n");
}
- server.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*存储用户*/
struct user
{
char username[20];
char password[20];
};
/*存储用户及其用户套接字文件描述符*/
struct user_socket
{
char username[20];
int socketfd;
int status; //标识是否在线 0:在线 -1:下线
};
/*存储聊天室*/
struct chatroom
{
char name[20];
char passwd[20];
int user[10]; //加入聊天室的人数
int status; //标识是否还存在 0:存在 -1:销毁
};
#define PORT 8888
#define MAXMEM 20
#define MAXROOM 5
#define BUFFSIZE 256
int user_count; //记录总的用户数
int chatroom_count; //记录聊天室个数
int listenfd, connfd[MAXMEM];
struct user users[MAXMEM]; //记录所有用户
struct user_socket online_users[MAXMEM]; //记录在线用户
struct chatroom chatrooms[MAXROOM]; //记录聊天室
void init();
void quit();
void save_users();
void register_user(int n);
void rcv_snd(int p);
void quit_client(int n);
int user_login(int n);
void get_help();
void send_private_msg(char *username, char *data, int sfd);
void send_all_msg(char *msg, int sfd);
void get_online_users(int sfd);
void send_chatroom_msg(char *msg, int sfd);
void create_chatroom(char *name, char *passwd, int sfd);
void join_chatroom(char *name, char *passwd, int sfd);
void get_online_chatrooms(int sfd);
void change_passwd(int sfd, char *passwd);
void get_inroom_users(int sfd);
void exit_chatroom(int sfd);
void invalid_command(int sfd);
- server.c
#include "server.h"
int main()
{
init();
struct sockaddr_in serv_addr, cli_addr;
int i;
time_t timenow;
pthread_t thread;
char buff[BUFFSIZE];
printf("Running...\nEnter command \"quit\" to exit server.\n\n");
bzero(&serv_addr, sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
{
perror("fail to socket");
exit(-1);
}
if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
perror("fail to bind");
exit(-2);
}
listen(listenfd, MAXMEM);
// 创建一个线程,对服务器程序进行管理,调用quit函数
pthread_create(&thread, NULL, (void *)(quit), NULL);
// 将套接字描述符数组初始化为-1,表示空闲
for (i = 0; i < MAXMEM; i++)
connfd[i] = -1;
while (1)
{
int len;
for (i = 0; i < MAXMEM; i++)
{
if (connfd[i] == -1)
break;
}
// accept 从listen接受的连接队列中取得一个连接
connfd[i] = accept(listenfd, (struct sockaddr *)&cli_addr, &len);
if (connfd[i] < 0)
{
perror("fail to accept.");
}
timenow = time(NULL);
printf("%.24s\n\tconnect from: %s, port %d\n",
ctime(&timenow), inet_ntop(AF_INET, &(cli_addr.sin_addr), buff, BUFFSIZE),
ntohs(cli_addr.sin_port));
// 针对当前套接字创建一个线程,对当前套接字的消息进行处理
pthread_create(malloc(sizeof(pthread_t)), NULL, (void *)(&rcv_snd), (void *)i);
}
return 0;
}
/*服务器接收和发送函数*/
void rcv_snd(int n)
{
ssize_t len;
int i;
char mytime[32], buf[BUFFSIZE];
char temp[BUFFSIZE];
char command[20], arg1[20], arg2[BUFFSIZE];
time_t timenow;
while (1)
{
len = read(connfd[n], buf, BUFFSIZE);
if (len > 0)
{
buf[len - 1] = '\0'; // 去除换行符
if (strcmp(buf, "login") == 0)
{
//登录成功时退出该循环
if (user_login(n) == 0)
{
break;
}
}
else if (strcmp(buf, "register") == 0)
{
register_user(n);
}
else if (strcmp(buf, "quit") == 0)
{
quit_client(n);
}
}
}
while (1)
{
if ((len = read(connfd[n], temp, BUFFSIZE)) > 0)
{
temp[len - 1] = '\0';
sscanf(temp, "%s %s %[^\n]", command, arg1, arg2); //解析命令
/*根据解析出的命令执行不同的函数*/
if (strcmp(command, "send") == 0 && strcmp(arg1, "-all") == 0)
{
send_all_msg(arg2, n);
}
else if (strcmp(command, "send") == 0 && strcmp(arg1, "-chatroom") == 0)
{
send_chatroom_msg(arg2, n);
}
else if (strcmp(command, "send") == 0)
{
send_private_msg(arg1, arg2, n);
}
else if (strcmp(command, "quit") == 0)
{
quit_client(n);
}
else if (strcmp(command, "chgpsw") == 0)
{
change_passwd(n, arg1);
}
else if (strcmp(command, "create") == 0)
{
create_chatroom(arg1, arg2, n);
}
else if (strcmp(command, "join") == 0)
{
join_chatroom(arg1, arg2, n);
}
else if (strcmp(command, "ls") == 0 && strcmp(arg1, "-chatrooms") == 0)
{
get_online_chatrooms(n);
}
else if (strcmp(command, "ls") == 0 && strcmp(arg1, "-users") == 0)
{
get_online_users(n);
}
else if (strcmp(command, "ls") == 0 && strcmp(arg1, "-inrmusr") == 0)
{
get_inroom_users(n);
}
else if (strcmp(command, "exit") == 0)
{
exit_chatroom(n);
}
else
{
invalid_command(n);
}
}
}
}
/*初始化*/
void init()
{
int i, j;
user_count = 0;
chatroom_count = 0;
for (i = 0; i < MAXMEM; i++)
{
online_users[i].status = -1;
}
for (i = 0; i < MAXROOM; i++)
{
chatrooms[i].status = -1;
for (j = 0; j < 10; j++)
{
chatrooms[i].user[j] = -1;
}
}
char buf[20];
FILE *fp = NULL;
fp = fopen("users.txt", "r");
//从文件中读取用户
while (fscanf(fp, "%s", buf) != EOF)
{
strcpy(users[user_count].username, buf);
fscanf(fp, "%s", buf);
strcpy(users[user_count].password, buf);
user_count++;
}
fclose(fp);
}
/*将用户保存到文件*/
void save_users()
{
int i;
char buf[20];
FILE *fp = NULL;
fp = fopen("users.txt", "w+");
for (i = 0; i < user_count; i++)
{
strcpy(buf, users[i].username);
strcat(buf, "\n");
fprintf(fp, buf);
strcpy(buf, users[i].password);
strcat(buf, "\n");
fprintf(fp, buf);
}
fclose(fp);
}
/*服务器处理用户退出*/
void quit_client(int n)
{
int ret, i;
close(connfd[n]);
connfd[n] = -1;
for (i = 0; i < MAXMEM; i++)
{
if (n == online_users[i].socketfd)
{
online_users[i].status = -1;
}
}
pthread_exit(&ret);
}
/*用户登录*/
int user_login(int n)
{
int len, i, j;
char buf[BUFFSIZE], username[20], password[20];
sprintf(buf, "your username: ");
write(connfd[n], buf, strlen(buf) + 1);
len = read(connfd[n], username, 20);
if (len > 0)
{
username[len - 1] = '\0'; // 去除换行符
}
sprintf(buf, "your password: ");
write(connfd[n], buf, strlen(buf) + 1);
len = read(connfd[n], password, 20);
if (len > 0)
{
password[len - 1] = '\0'; // 去除换行符
}
for (i = 0; i < MAXMEM; i++)
{
if (strcmp(username, users[i].username) == 0)
{
if (strcmp(password, users[i].password) == 0)
{
sprintf(buf, "Login successfully.\n\n");
write(connfd[n], buf, strlen(buf + 1));
for (j = 0; j < MAXMEM; j++)
{
if (online_users[j].status == -1)
break;
}
strcpy(online_users[j].username, username);
online_users[j].socketfd = n;
online_users[j].status = 0;
return 0;
}
else
{
sprintf(buf, "Wrong password.\n\n");
write(connfd[n], buf, strlen(buf + 1));
return -1;
}
}
}
sprintf(buf, "Account does not exist.\n\n");
write(connfd[n], buf, strlen(buf + 1));
return -1;
}
/*用户注册*/
void register_user(int n)
{
int len, i;
char buf[BUFFSIZE], username[20], password[20];
sprintf(buf, "your username: ");
write(connfd[n], buf, strlen(buf) + 1);
len = read(connfd[n], username, 20);
if (len > 0)
{
username[len - 1] = '\0'; // 去除换行符
}
sprintf(buf, "your password: ");
write(connfd[n], buf, strlen(buf) + 1);
len = read(connfd[n], password, 20);
if (len > 0)
{
password[len - 1] = '\0'; // 去除换行符
}
for (i = 0; i < MAXMEM; i++)
{
if (strcmp(users[i].username, username) == 0)
{
strcpy(buf, "The username already exists.\n\n");
write(connfd[n], buf, strlen(buf) + 1);
return;
}
}
strcpy(users[user_count].username, username);
strcpy(users[user_count].password, password);
user_count++;
sprintf(buf, "Account created successfully.\n\n");
write(connfd[n], buf, strlen(buf) + 1);
}
/*用户发送私聊信息*/
void send_private_msg(char *username, char *data, int sfd)
{
int i, j;
time_t now;
char send_man[20];
char buf[BUFFSIZE], nowtime[20], temp[30];
now = time(NULL);
time(&now);
struct tm *tempTime = localtime(&now);
strftime(nowtime, 20, "[%H:%M:%S]", tempTime);
for (j = 0; j < MAXMEM; j++)
{
if (sfd == online_users[j].socketfd)
{
strcpy(send_man, online_users[j].username);
break;
}
}
for (i = 0; i < MAXMEM; i++)
{
if (strcmp(username, online_users[i].username) == 0)
{
strcpy(buf, nowtime);
strcat(buf, "\t");
strcat(buf, "from ");
strcat(buf, send_man);
strcat(buf, ":\n");
strcat(buf, data);
strcat(buf, "\n");
write(connfd[online_users[i].socketfd], buf, strlen(buf) + 1);
strcpy(temp, "Sent successfully.\n");
write(connfd[sfd], temp, strlen(temp) + 1);
return;
}
}
strcpy(buf, "User is not online or user does not exist.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
return;
}
/*用户群发信息给所有用户*/
void send_all_msg(char *msg, int sfd)
{
int i;
char buf[BUFFSIZE], nowtime[20], send_man[20], temp[30];
time_t now;
time(&now);
struct tm *tempTime = localtime(&now);
strftime(nowtime, 20, "[%H:%M:%S]", tempTime);
for (i = 0; i < MAXMEM; i++)
{
if (sfd == online_users[i].socketfd)
{
strcpy(send_man, online_users[i].username);
break;
}
}
strcpy(buf, nowtime);
strcat(buf, "\t");
strcat(buf, "from ");
strcat(buf, send_man);
strcat(buf, "(goup-sent):\n");
strcat(buf, msg);
strcat(buf, "\n");
for (i = 0; i < MAXMEM; i++)
{
if (connfd[i] != -1 && i != sfd)
{
write(connfd[i], buf, strlen(buf) + 1);
}
}
strcpy(temp, "Sent successfully\n");
write(connfd[sfd], temp, strlen(temp) + 1);
}
/*获取所有在线用户信息*/
void get_online_users(int sfd)
{
int i;
char buf[BUFFSIZE], nowtime[20];
time_t now;
time(&now);
struct tm *tempTime = localtime(&now);
strftime(nowtime, 20, "[%H:%M:%S]", tempTime);
strcpy(buf, nowtime);
strcat(buf, "\t");
strcat(buf, "All online user(s):\n");
for (i = 0; i < MAXMEM; i++)
{
if (online_users[i].status == 0)
{
strcat(buf, "\t");
strcat(buf, online_users[i].username);
strcat(buf, "\n");
}
}
write(connfd[sfd], buf, strlen(buf) + 1);
}
/*向聊天室发送信息*/
void send_chatroom_msg(char *msg, int sfd)
{
int i, j, k;
int flag;
flag = -1;
char buf[BUFFSIZE], nowtime[20];
time_t now;
time(&now);
struct tm *tempTime = localtime(&now);
strftime(nowtime, 20, "[%H:%M:%S]", tempTime);
for (i = 0; i < MAXROOM; i++)
{
if (chatrooms[i].status == 0)
{
for (j = 0; j < 10; j++)
{
if (chatrooms[i].user[j] == sfd)
{
flag = 0;
break;
}
}
}
if (flag == 0)
{
break;
}
}
if (flag == -1)
{
strcpy(buf, "You have not joined the chat room.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
}
else
{
for (k = 0; k < MAXMEM; k++)
{
if (online_users[k].status == 0 && online_users[k].socketfd == sfd)
break;
}
strcpy(buf, nowtime);
strcat(buf, "\tchatroom ");
strcat(buf, chatrooms[i].name);
strcat(buf, ":\nfrom ");
strcat(buf, online_users[k].username);
strcat(buf, ":\t");
strcat(buf, msg);
strcat(buf, "\n");
for (k = 0; k < 10; k++)
{
if (chatrooms[i].user[k] != -1)
{
write(connfd[chatrooms[i].user[k]], buf, strlen(buf) + 1);
}
}
}
}
/*创建聊天室*/
void create_chatroom(char *name, char *passwd, int sfd)
{
int i, j;
char buf[BUFFSIZE];
for (i = 0; i < MAXROOM; i++)
{
if (chatrooms[i].status == -1)
break;
}
strcpy(chatrooms[i].name, name);
strcpy(chatrooms[i].passwd, passwd);
chatrooms[i].status = 0;
for (j = 0; j < 10; j++)
{
if (chatrooms[i].user[j] == -1)
break;
}
chatrooms[i].user[j] = sfd;
strcpy(buf, "Successfully created chat room.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
}
/*加入聊天室*/
void join_chatroom(char *name, char *passwd, int sfd)
{
int i, j;
int room, flag;
char buf[BUFFSIZE];
flag = -1;
for (i = 0; i < MAXROOM; i++)
{
for (j = 0; j < 10; j++)
{
if (chatrooms[i].user[j] == sfd)
{
room = i;
flag = 0;
}
}
}
if (flag == 0)
{
strcpy(buf, "You have joined the chat room ");
strcat(buf, chatrooms[room].name);
strcat(buf, ".\n");
write(connfd[sfd], buf, strlen(buf) + 1);
}
else
{
for (i = 0; i < MAXROOM; i++)
{
if (chatrooms[i].status != -1)
{
if (strcmp(chatrooms[i].name, name) == 0)
{
if (strcmp(chatrooms[i].passwd, passwd) == 0)
{
for (j = 0; j < 10; j++)
{
if (chatrooms[i].user[j] == -1)
{
break;
}
}
chatrooms[i].user[j] = sfd;
strcpy(buf, "Successfully joined the chat room.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
return;
}
else
{
strcpy(buf, "Incorrect chat room password.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
return;
}
}
}
}
}
}
/*获取所有已创建的聊天室的信息*/
void get_online_chatrooms(int sfd)
{
int i;
char buf[BUFFSIZE], nowtime[20];
time_t now;
time(&now);
struct tm *tempTime = localtime(&now);
strftime(nowtime, 20, "[%H:%M:%S]", tempTime);
strcpy(buf, nowtime);
strcat(buf, "\tAll online chat room(s):\n");
for (i = 0; i < MAXROOM; i++)
{
if (chatrooms[i].status == 0)
{
strcat(buf, "\t");
strcat(buf, chatrooms[i].name);
strcat(buf, "\n");
}
}
write(connfd[sfd], buf, strlen(buf) + 1);
}
/*修改密码*/
void change_passwd(int sfd, char *passwd)
{
int i, j;
char buf[BUFFSIZE], name[20];
for (j = 0; j < MAXMEM; j++)
{
if (sfd == online_users[j].socketfd)
{
strcpy(name, online_users[j].username);
break;
}
}
for (i = 0; i < MAXMEM; i++)
{
if (strcmp(name, users[i].username) == 0)
{
strcpy(users[i].password, passwd);
strcpy(buf, "Password has been updated.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
break;
}
}
}
/*查询所有加入某聊天室的用户*/
void get_inroom_users(int sfd)
{
int i, j;
int room, flag; //room记录查询查询发起人所在的房间,flag标识用户是否加入房间
flag = -1;
char buf[BUFFSIZE], nowtime[20];
time_t now;
time(&now);
struct tm *tempTime = localtime(&now);
strftime(nowtime, 20, "[%H:%M:%S]", tempTime);
for (i = 0; i < MAXROOM; i++)
{
for (j = 0; j < 10; j++)
{
if (chatrooms[i].user[j] == sfd)
{
room = i;
flag = 0;
}
}
}
if (flag == -1)
{
strcpy(buf, "Sorry, you have not joined the chat room.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
}
else
{
strcpy(buf, nowtime);
strcat(buf, "\tAll users in the ");
strcat(buf, chatrooms[room].name);
strcat(buf, ":\n");
for (i = 0; i < 10; i++)
{
if (chatrooms[room].user[i] >= 0)
for (j = 0; j < MAXMEM; j++)
{
if (online_users[j].status != -1 && (chatrooms[room].user[i] == online_users[j].socketfd))
{
strcat(buf, "\t");
strcat(buf, online_users[j].username);
strcat(buf, "\n");
}
}
}
write(connfd[sfd], buf, strlen(buf) + 1);
}
}
/*退出聊天室*/
void exit_chatroom(int sfd)
{
int i, j;
int room, flag;
flag = -1;
char buf[BUFFSIZE];
for (i = 0; i < MAXROOM; i++)
{
if (chatrooms[i].status == 0)
{
for (j = 0; j < 10; j++)
{
if (chatrooms[i].user[j] == sfd)
{
chatrooms[i].user[j] = -1;
room = i;
flag = 0;
break;
}
}
}
if (flag == 0)
break;
}
if (flag == -1)
{
strcpy(buf, "You have not joined the chat room.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
}
else
{
strcpy(buf, "Successfully quit chat room ");
strcat(buf, chatrooms[room].name);
strcat(buf, ".\n");
write(connfd[sfd], buf, strlen(buf) + 1);
}
}
/*服务器推出*/
void quit()
{
int i;
char msg[10];
while (1)
{
scanf("%s", msg); // scanf 不同于fgets, 它不会读入最后输入的换行符
if (strcmp(msg, "quit") == 0)
{
save_users();
printf("Byebye... \n");
close(listenfd);
exit(0);
}
}
}
/*用户输入无效命令*/
void invalid_command(int sfd)
{
char buf[BUFFSIZE];
strcpy(buf, "Invalid command.\n");
write(connfd[sfd], buf, strlen(buf) + 1);
}
{
strcat(buf, “\t”);
strcat(buf, online_users[j].username);
strcat(buf, “\n”);
}
}
}
write(connfd[sfd], buf, strlen(buf) + 1);
}
}
/退出聊天室/
void exit_chatroom(int sfd)
{
int i, j;
int room, flag;
flag = -1;
char buf[BUFFSIZE];
for (i = 0; i < MAXROOM; i++)
{
if (chatrooms[i].status == 0)
{
for (j = 0; j < 10; j++)
{
if (chatrooms[i].user[j] == sfd)
{
chatrooms[i].user[j] = -1;
room = i;
flag = 0;
break;
}
}
}
if (flag == 0)
break;
}
if (flag == -1)
{
strcpy(buf, “You have not joined the chat room.\n”);
write(connfd[sfd], buf, strlen(buf) + 1);
}
else
{
strcpy(buf, "Successfully quit chat room ");
strcat(buf, chatrooms[room].name);
strcat(buf, “.\n”);
write(connfd[sfd], buf, strlen(buf) + 1);
}
}
/服务器推出/
void quit()
{
int i;
char msg[10];
while (1)
{
scanf("%s", msg); // scanf 不同于fgets, 它不会读入最后输入的换行符
if (strcmp(msg, “quit”) == 0)
{
save_users();
printf(“Byebye… \n”);
close(listenfd);
exit(0);
}
}
}
/用户输入无效命令/
void invalid_command(int sfd)
{
char buf[BUFFSIZE];
strcpy(buf, “Invalid command.\n”);
write(connfd[sfd], buf, strlen(buf) + 1);
}