前言:
最近在學程序間通信,是以做了一個小項目練習一下。主要用消息隊列和socket(UDP)實作這個系統,并資料庫存儲資料,對C語言操作資料庫不熟悉的可以參照我的這篇部落格:https://www.cnblogs.com/liudw-0215/p/9593414.html,所有代碼送出我的Github上,位址:https://github.com/ldw0215/Chat-System.git,可以自行下載下傳,然後make一下就可以了。
一、項目要求
- 要求實作使用者注冊、使用者登入功能,密碼需加密顯示
- 要求實作聊天功能,雙方能互發消息
- 資料要求資料庫存儲
二、架構解析
主要流程圖如下:

主要有用戶端(使用者)和服務端,用戶端發送注冊、登入請求,服務端回應請求,并且雙方可以模拟聊天(互相發送消息),主要用戶端請求界面見下圖:
注冊、登入使用消息隊列進行通信的,聊天是通過socket(UDP)實作的!資料存在資料庫中,需要一張資料表,建表資料語句如下:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL DEFAULT '',
`password` varchar(64) NOT NULL DEFAULT '',
`check` varchar(64) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8;
三、用戶端實作
client.c建立不同的消息隊列的鍵,根據不同的消息類型的進行發送,并等待服務端響應,client.c代碼如下:
#include "my.h"
Msg m;
Msg_stoc msg_stoc;
static int msgid_ctos;
static int msgid_stoc;
void showmenu()
{
puts("-------CHAT----------");
puts("| 1:發送 2:接收 |");
puts("| 3:退出 |");
puts("--------------------");
}
void show()
{
puts("-------CHAT----------");
puts("| 1:注冊 2:登入 |");
puts("| 0:退出 |");
puts("--------------------");
}
void send1()
{
printf("%s","send");
char buf[16] = {'\0'};
char str[200] = {'\0'};
struct sockaddr_in dui,zj;
int n;
short x;
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
zj.sin_family = AF_INET;
zj.sin_port = htons(5555);
zj.sin_addr.s_addr = htonl(INADDR_ANY);
n = bind(sockfd,(struct sockaddr *)&zj,sizeof(zj));
if(n < 0)
{
close(sockfd);
perror("bind");
exit(-1);
}
//puts("請輸入對方号碼 端口 IP ");
//scanf("%hd%s",&x,buf);
getchar();
dui.sin_addr.s_addr = inet_addr("10.10.3.129");
dui.sin_port = htons(8888);
dui.sin_family = AF_INET;
puts("請輸入想要發送的内容:");
//gets(str);
fgets(str,200,stdin);
n = sendto(sockfd,str,sizeof(str),0,(struct sockaddr *)&dui,sizeof(dui));
if(n <= 0)
{
close(sockfd);
perror("sendto");
exit(-1);
}
close(sockfd);
return;
}
#if 1
void asend(int sockfd,struct sockaddr_in dui)
{
char buf[200] = {'\0'};
int n;
puts("請輸入要回複的内容:");
fgets(buf,200,stdin);
//gets(buf);
n = sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&dui,sizeof(dui));
if(n <= 0)
{
perror("sendto");
close(sockfd);
exit(-1);
}
close(sockfd);
return ;
}
void choose1(char ch ,int sockfd,struct sockaddr_in dui)
{
switch(ch)
{
case 'a':
asend(sockfd,dui);
break;
case 'n':
break;
default:
puts("input error!");
break;
}
}
#endif
void recv1()
{
struct sockaddr_in dui,zj;
socklen_t len = sizeof(dui);
int n;
char buf[200] = {'\0'};
char ch;
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
zj.sin_family = AF_INET;
zj.sin_port = htons(5555);
zj.sin_addr.s_addr = htonl(INADDR_ANY);
n = bind(sockfd,(struct sockaddr *)&zj,sizeof(zj));
if(n < 0)
{
close(sockfd);
perror("bind");
exit(-1);
}
n = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&dui,&len);
if(n <= 0)
{
close(sockfd);
perror("recvfrom");
exit(-1);
}
puts(buf);
puts("是否要回複: 回複-》a,不回複-》n");
ch = getchar();
getchar();
choose1(ch,sockfd,dui);
}
void choose(int ch)
{
printf("%d",ch);
switch(ch)
{
case 1:
m.type = 5;
msgsnd(msgid_ctos,&m,sizeof(m)-sizeof(m.type),0);
send1();
break;
case 2:
recv1();
break;
case 3:
exit(0);
break;
default:
puts("input error!");
break;
}
}
void regis()
{
printf("請輸入姓名:");
scanf("%s",m.name);
printf("請輸入密碼:");
scanf("%s",m.passwd);
m.type=1;
}
void login(void)
{
printf("請輸入使用者名:");
scanf("%s",&m.name);
printf("請輸入密碼:");
scanf("%s",&m.passwd);
m.type=3;
}
int main(int argc ,char *argv[])
{
msgid_ctos=get_ctos_msg();
msgid_stoc=get_stoc_msg();
int temptype;
while(1)
{
show();
int a = 0;
scanf("%d",&a);
getchar();
switch(a)
{
case 0:return 0;
case 1:regis();temptype=2;break; //注冊
case 2:login();temptype=4;break; //登入
}
int ret;
long type=0;
printf("name:%s,passwd:%s\n", m.name,m.passwd);
msgsnd(msgid_ctos,&m,sizeof(m)-sizeof(m.type),0);
sleep(2);
msgrcv(msgid_stoc,&msg_stoc,sizeof(msg_stoc),temptype,0);
if(0 == strcmp(msg_stoc.check,"yes"))
{
printf("%s",msg_stoc.info);
break;
}
}
while(1)
{
showmenu();
int ch;
puts("請輸入功能");
scanf("%d",&ch);
getchar();
printf("%d",ch);
choose(ch);
}
return ;
}
View Code
注意,不同的通信,要用建立不同消息隊列的鍵,并且消息類型也要不同!
四、服務端實作
服務端主要接送并響應用戶端,主要建立不同的子程序,然後調用exec族函數,調用二進制檔案,并通過消息隊列接收阻塞執行,并建立信号,檢測Ctrl+c信号,是程序退出,服務端響應截圖如下:
其實作代碼如下:
#include"my.h"
static pid_t sub_pid[9];
static int msgid_ctos;
static int msgid_stoc;
void sigint(int signum)
{
int i;
for(i=0;i<3;i++)
{
kill(sub_pid[i],SIGKILL);
}
}
int main(int argc,char*argv[])
{
signal(SIGINT,sigint);
msgid_ctos=get_ctos_msg();
msgid_stoc=get_stoc_msg();
sub_pid[0]=vfork();
if(0==sub_pid[0])
{
execl("register","register",NULL);
}
sub_pid[1]=vfork();
if(0==sub_pid[1])
{
execl("login","login",NULL);
}
sub_pid[2]=vfork();
if(0==sub_pid[2])
{
execl("chat","chat",NULL);
}
wait(NULL);
return 0;
}
四、各子產品及資料庫解析
資料庫是通過資料庫函數實作的,需要頭檔案<mysql.h>,并鍊上動态庫-I/usr/include/mysql -L/usr/lib64/mysql -lmysqlclient,資料量比較小,之後還要考慮優化的問題;
注冊、登入、聊天都是不同的.c檔案生成二進制實作的:
注冊通過消息隊列接收使用者名和密碼存入資料庫,代碼如下:
#include"my.h"
Msg per;
Msg_stoc msg_stoc;
static int msgid_ctos;
static int msgid_stoc;
void open_cli()
{
MYSQL conn;
int res;
//MYSQL_RES * result;
//MYSQL_ROW row;
mysql_init(&conn);
//第三、四和五個參數,需要自己修改一下
if (mysql_real_connect(&conn, "localhost", "root", "p@s#0fSPV", "pvault", 0, NULL, 0)) {
printf("coneect mysql successful\n");
char insert_query[80];
//insert
memset(insert_query, 0, sizeof(insert_query));
strcat(insert_query, "insert into user(name,password) values('");
strcat(insert_query, per.name);
strcat(insert_query, "','");
strcat(insert_query, per.passwd);
strcat(insert_query, "')");
printf("SQL語句: %s\n", insert_query);
res = mysql_query(&conn, insert_query);
if (!res) {
printf("insert %lu rows\n", (unsigned long)mysql_affected_rows(&conn));
sprintf(msg_stoc.check,"%s","no");
}
else {
printf("insert error\n");
}
}
}
int main()
{
msgid_ctos = get_ctos_msg();
msgid_stoc = get_stoc_msg();
//int sockfd = socket_rcv();
while(1)
{
int n;
long type;
//per.type = 1;
msgrcv(msgid_ctos,&per,sizeof(per)-sizeof(type),1,0);
printf("name:%s,passwd:%s\n", per.name,per.passwd);
sleep(1);
open_cli();
//Msg m;
msg_stoc.type = 2;
sprintf(msg_stoc.check,"%s","no");
printf("check:%s", msg_stoc.check);
msgsnd(msgid_stoc,&msg_stoc,sizeof(msg_stoc)-sizeof(type),0);
}
}
登入也是通過消息隊列接收使用者名和密碼,并從查詢出資料,進行對比,是否可以登入,代碼如下:
#include"my.h"
Msg per;
Msg_stoc msg_stoc;
static int msgid_ctos;
static int msgid_stoc;
void login(void)
{
MYSQL conn;
int res;
MYSQL_RES * result;
MYSQL_ROW row;
mysql_init(&conn);
//第三、四和五個參數,需要自己修改一下
if (mysql_real_connect(&conn, "localhost", "root", "p@s#0fSPV", "pvault", 0, NULL, 0)) {
printf("coneect mysql successful\n");
char select_query[64] = {0};
snprintf(select_query,64,"select name,password from user where name='%s'",per.name);
printf("SQL語句: %s\n", select_query);
if (mysql_query(&conn, select_query) != 0) {
fprintf(stderr, "查詢失敗\n");
exit(1);
}
else {
if ((result = mysql_store_result(&conn)) == NULL) {
fprintf(stderr, "儲存結果集失敗\n");
exit(1);
}
else {
while ((row = mysql_fetch_row(result)) != NULL) {
printf("name is %s , ", row[0]);
printf("age is %s\n", row[1]);
if((0 == strcmp(row[0],per.name)) && (0 == strcmp(row[1],per.passwd)))
strcpy(per.info,"login success!");
}
}
}
}
}
int main()
{
msgid_ctos = get_ctos_msg();
msgid_stoc = get_stoc_msg();
//int sockfd = socket_rcv();
while(1)
{
int n;
long type;
//per.type = 3;
msgrcv(msgid_ctos,&per,sizeof(per)-sizeof(type),3,0);
//printf("name:%s,passwd:%s\n", per.name,per.passwd);
sleep(1);
login();
//Msg m;
msg_stoc.type = 4;
sprintf(msg_stoc.check,"%s","yes");
printf("check:%s", per.info);
msgsnd(msgid_stoc,&msg_stoc,sizeof(msg_stoc)-sizeof(type),0);
}
}
聊天是通過socket(UDP)實作的。
總結:通過做這種小項目學到了很多,也發現許多不足,最重要的就是架構能力,之前都是做一小塊,沒有大局觀,雖然項目小,但五張俱全,很鍛煉人,繼續找項目做!
作者:
柳德維出處:
https://www.cnblogs.com/liudw-0215/-------------------------------------------