先创建一个名为 my.db 的数据库
然后运用SQL语句建立两张表
建表语句:
CREATE TABLE usr (name text PRIMARY KEY, pass TEXT);
CREATE TABLE record (name TEXT, date TEXT, word TEXT);
功能演示
1:注册用户选项
2:登录选项
3:退出选项
第一步先注册一个用户:
第二步使用刚注册用户名和密码登录:
执行完登录操作后界面会跳转到查询单词的界面
1:查询单词的选项
2:查询历史记录的选项
3:退出选项
选择1选项,输入想要搜索的单词
选择2选项,则会出现历史的记录
流程图:
客户端的流程图
客户端的流程图
代码实现:
客户端代码
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define N 16 // 用户名数组的大小
#define R 1 // 用户注册
#define L 2 // 用户登录
#define Q 3 // 查询单词
#define H 4 // 查询历史记录
#define DATABASE "my.db" //数据库名字
typedef struct
{
int type; //标志位(协议)
char name[N]; //用户名
char data[256]; // 密码或者单词
} MSG;
void do_register(int socketfd, MSG* msg); //用户注册函数声明
int do_login(int socketfd, MSG* msg); //用户登录函数声明
void do_query(int socketfd, MSG* msg); //查询单词函数声明
void do_history(int socketfd, MSG* msg); //查询历史记录函数声明
int main(int argc, char* argv[])
{
int socketfd; //定义套接字
struct sockaddr_in server_addr; //定义填充服务器网络信息的结构体
MSG msg; //定义结构体(数据包)
if (argc < 3) { //判断终端输入参数是否合理
printf("Usage : %s <serv_ip> <serv_port>\n", argv[0]); //打印错误信息
exit(-1);
}
//创建流式套接字
if (-1 == (socketfd = socket(PF_INET, SOCK_STREAM, 0))) {
perror("fail to socket"); //打印错误信息
exit(-1);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = PF_INET; //填充服务器网络信息结构体
server_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
server_addr.sin_port = htons(atoi(argv[2])); //端口号
//用connect和服务器创建连接
if (-1 == connect(socketfd, (struct sockaddr*)&server_addr, sizeof(server_addr))) {
perror("fail to connect"); //打印错误信息
exit(-1);
}
int choose; //定义选择变量
//在终端打印菜单
while (1) {
printf("************************************\n");
printf("* 1: register 2: login 3: quit *\n");
printf("************************************\n");
printf("please choose : ");
//用户输入选择
if (scanf("%d", &choose) <= 0) {
perror("scanf"); //打印错误信息
exit(-1);
}
//根据选择判断执行那些操作
switch (choose) {
case 1:
do_register(socketfd, &msg); //调用用户注册函数
break;
case 2:
if (do_login(socketfd, &msg) == 1) { //调用用户登录函数
goto NEXT; //登陆成功后跳转到查找单词的界面
}
break;
case 3:
close(socketfd); //关闭套接字
exit(0);
}
}
NEXT:
//查找单词的界面
while (1) {
printf("************************************\n");
printf("* 1: query 2: history 3: quit *\n");
printf("************************************\n");
printf("please choose : ");
//用户输入选择
if (scanf("%d", &choose) <= 0) {
perror("scanf");
exit(-1);
}
//根据选择判断执行那些操作
switch (choose) {
case 1:
do_query(socketfd, &msg); //调用查询单词函数
break;
case 2:
do_history(socketfd, &msg); //调用查询历史记录函数
break;
case 3:
close(socketfd); //关闭套接字
exit(0);
}
}
return 0;
}
//注册函数
void do_register(int socketfd, MSG* msg)
{
msg->type = R; //将数据包的类型定义为R(注册)
printf("input your name:");
scanf("%s", msg->name); //从终端输入用户名
printf("input your password:");
scanf("%s", msg->data); //在终端输入密码
//给服务器发送数据包
if (-1 == send(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
//接收服务器发来的数据包
if (-1 == recv(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
//打印提示信息
printf("register : %s\n", msg->data);
return;
}
//登录函数
int do_login(int socketfd, MSG* msg)
{
msg->type = L; //将数据包的类型定义为L(登录)
printf("input your name:");
scanf("%s", msg->name); //从终端输入用户名
printf("input your password:");
scanf("%s", msg->data); //在终端输入密码
//给服务器发送数据包
if (-1 == send(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return-1;
}
//接收服务器发来的数据包
if (-1 == recv(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return-1;
}
//比较发来的数据判断是否登陆成功
if (strncmp(msg->data, "OK", 3) == 0) {
printf("login : OK\n"); //打印msg中data的信息
return 1;
}
//打印msg中data的信息
printf("login : %s\n", msg->data);
return 0;
}
//查找单词的函数
void do_query(int socketfd, MSG* msg)
{
msg->type = Q; //将数据包的类型定义为Q(查找单词)
//循环查找
while (1) {
printf("input your (if # is end):");
scanf("%s", msg->data); //输入想要查找的单词
//如果不想查找了可用“#”退出
if (0 == strcmp(msg->data, "#")) {
break;
}
//给服务器发送数据包
if (-1 == send(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
//接收服务器发来的数据包
if (-1 == recv(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
//打印msg中data的信息
printf("query : %s\n", msg->data);
}
return;
}
//查询历史纪录的函数
void do_history(int socketfd, MSG* msg)
{
msg->type = H; //将数据包的类型定义为H(查询历史记录)
//给服务器发送数据包
if (-1 == send(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
while (1) {
//接收服务器发来的数据包
if (-1 == recv(socketfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
//判断 当接收到OVER时就表明服务器的数据已发完
if (!strcmp(msg->data, "OVER")) {
break;
}
//循环打印发送来的数据
printf("%s\n", msg->data);
}
return;
}
服务器代码
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define N 16 // 用户名数组的大小
#define R 1 // 用户注册
#define L 2 // 用户登录
#define Q 3 // 查询单词
#define H 4 // 查询历史记录
#define DATABASE "my.db" //数据库名字
typedef struct
{
int type; //标志位(协议)
char name[N]; //用户名
char data[256]; // 密码或者单词
} MSG;
void do_register(int connectfd, MSG* msg, sqlite3* db); //用户注册函数声明
void do_login(int connectfd, MSG* msg, sqlite3* db); //用户登录函数声明
void do_query(int connectfd, MSG* msg, sqlite3* db); //查询单词函数声明
void do_history(int connectfd, MSG* msg, sqlite3* db); //查询历史记录函数声明
void do_client(int connectfd, sqlite3* db); //解析协议函数声明
int do_searchword(int connectfd, MSG* msg); //在文件中查询单词的函数声明
void getdata(char data[]); //获取查询单词的时间的函数声明
int history_callback(void* arg, int f_num, char** f_value, char** f_name); //回调函数声明
void handler(int sig) //回收子进程资源函数
{
wait(NULL);
}
int main(int argc, char* argv[])
{
int listenfd, connectfd; //定义套接字
struct sockaddr_in server_addr; //定义填充服务器网络信息的结构体
pid_t pid; //用于创建父子进程
sqlite3* db; //数据库句柄
if (argc < 3) { //判断终端输入参数是否合理
printf("Usage : %s <ip> <port>\n", argv[0]);//打印错误信息
exit(-1);
}
if (sqlite3_open(DATABASE, &db) != SQLITE_OK) { //有就打开数据库,没有就创建
printf("error : %s\n", sqlite3_errmsg(db)); //打印错误信息
exit(-1);
}
if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { //创建流式套接字
perror("fail to socket"); //打印错误信息
exit(-1);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = PF_INET; //填充服务器网络信息结构体
server_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
server_addr.sin_port = htons(atoi(argv[2])); //端口号
int on = 1;
//设置允许端口复用
if (-1 == setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) {
perror("setsockopt error"); //打印错误信息
exit(-1);
}
//套接字和服务器网络信息结构体绑定
if (bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("fail to bind"); //打印错误信息
exit(-1);
}
if (listen(listenfd, 5) == -1) { //设置监听状态
perror("fail to listen"); //打印错误信息
exit(-1);
}
signal(SIGCHLD, handler); //处理僵尸进程
while (1) {
if ((connectfd = accept(listenfd, NULL, NULL)) < 0) { //创建对接客户的流式套接字
perror("fail to accept"); //打印错误信息
exit(-1);
}
if ((pid = fork()) < 0) {
perror("fail to fork"); //打印错误信息
exit(-1);
} else if (pid == 0) //子进程执行处理代码
{
do_client(connectfd, db); //解析协议函数的调用
} else //父进程负责连接
{
close(connectfd); //关闭对接客户的流式套接字
}
}
return 0;
}
void do_client(int connectfd, sqlite3* db) //解析协议函数
{
MSG msg; //定义结构体(数据包)
while (recv(connectfd, &msg, sizeof(MSG), 0) > 0) // receive request
{
printf("type = %d\n", msg.type); //在服务器上显示包的类型(可不要)
printf("type = %s\n", msg.data); //在服务器上显示输入的内容(可不要)
switch (msg.type) { //判断包的类型并决定下一步操作
case R:
do_register(connectfd, &msg, db); //调用用户注册函数
break;
case L:
do_login(connectfd, &msg, db); //调用用户登录函数
break;
case Q:
do_query(connectfd, &msg, db); //调用查询单词函数
break;
case H:
do_history(connectfd, &msg, db); //调用查询历史记录的函数
break;
}
}
printf("client quit\n"); //上面的类型都没有则打印client quit
exit(0);
return;
}
void do_register(int connectfd, MSG* msg, sqlite3* db)
{
char sqlstr[521] = { 0 }; //作为容器承载字符串(尽量大一点不然会出现警告)
//组装插入usr表的数据库的命令
sprintf(sqlstr, "INSERT INTO usr VALUES('%s','%s')", msg->name, msg->data);
//执行sql语句的函数
if (SQLITE_OK != sqlite3_exec(db, sqlstr, NULL, NULL, NULL)) {
//如果执行错误就先暂定为是用户已存在造成的错误
//将“用户已存在”写入msg的data中
strcpy(msg->data, "用户已存在");
//给客户端发送数据包
if (-1 == send(connectfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
} else {
//将“OK”写入msg的data中
strcpy(msg->data, "OK");
//给客户端发送数据包
if (-1 == send(connectfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return;
}
}
}
void do_login(int connectfd, MSG* msg, sqlite3* db)
{
char sqlstr[521] = { 0 }; //作为容器承载字符串(尽量大一点不然会出现警告)
char** result = NULL; //查询的结果
int row = 0; //成功返回的行数
int column = 0; //成功返回的列数
//组装查询usr表的数据库的命令
sprintf(sqlstr, "SELECT * FROM usr WHERE name='%s' AND pass='%s'", msg->name, msg->data);
//执行sql语句的函数并可以将结果打印到终端
if (SQLITE_OK != sqlite3_get_table(db, sqlstr, &result, &row, &column, NULL)) {
printf("error : %s\n", sqlite3_errmsg(db)); //打印错误信息
return;
}
//利用返回的行数判断是否有此用户并且密码正确
if (row != 0) {
strcpy(msg->data, "OK"); //如果有此用户并且密码正确则将“OK”写入msg的data中
} else {
//如果没有此用户或者密码不正确则将“name or password is wrony!!!”写入msg的data中
strcpy(msg->data, "name or password is wrony!!!");
}
//给客户端发送数据包
if (-1 == send(connectfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
}
return;
}
void do_query(int connectfd, MSG* msg, sqlite3* db)
{
char sqlstr[256] = { 0 }; //作为容器承载字符串(尽量大一点不然会出现警告)
char* errmsg = NULL; //定义一个指针指向错误信息
int found = 0; //用于接收do_searchword函数的返回值
char date[256] = { 0 }; //存放单词的解释
char word[128] = { 0 }; //保存单词
strcpy(word, msg->data); //将单词保存在word中,便于放入历史纪录的表中
found = do_searchword(connectfd, msg); //调用在文件中查询单词的函数
//执行成功,将保存历史记录
if (found == 1) {
getdata(date); //调用获取查询单词的时间的函数
//组装插入到record表的数据库的命令
sprintf(sqlstr, "INSERT INTO record VALUES('%s', '%s', '%s')", msg->name, date, word);
//执行sql语句的函数
if (sqlite3_exec(db, sqlstr, NULL, NULL, &errmsg) != SQLITE_OK) {
printf("error : %s\n", errmsg); //打印错误信息
}
}
//给客户端发送数据包
if (-1 == send(connectfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
}
//释放errmsg
sqlite3_free(errmsg);
return;
}
int do_searchword(int connectfd, MSG* msg)
{
FILE* fp; //创建标准IO文件描述符
char temp[300] = { 0 }; //容纳一行的单词及单词的解释
char* p = NULL; //定义一个一级指针用来截取单词的解释
int len; //用来保存单词的长度
int result; //用来接比较函数的返回值
len = strlen(msg->data); //计算并保存单词的长度
//打开保存单词及其解释的文件
if ((fp = fopen("dict.txt", "r")) == NULL) {
//将打开文件失败的信息写入到msg的data中
strcpy(msg->data, "dict can not open");
//给客户端发送数据包
if (-1 == send(connectfd, msg, sizeof(MSG), 0)) {
perror("send error"); //打印错误信息
return -1;
}
}
int flags = 0;
//每次读取一行内容
while (fgets(temp, 300, fp) != NULL) {
//比较len个长度看是否相等,相等返回0
result = strncmp(msg->data, temp, len);
//当result等于0并且单词的后一位为空格时,则说明匹配到了
if (result == 0 && temp[len] == ' ') {
//将p定位到单词后一位的空格
p = temp + len;
//让p循环++定位到单词解释的第一位上
while (*p == ' ') {
p++;
}
//将单词的解释写入到msg的data中
strcpy(msg->data, p);
fclose(fp); //关闭标准IO的文件描述符
return 1; //单词匹配成功返回1
}
}
//将"not found"写入到msg的data中
strcpy(msg->data, "not found");
fclose(fp); //关闭标准IO的文件描述符
return 0; //单词匹配失败返回
}
void getdata(char* data)
{
//定义获取时间的变量
time_t t;
//定义结构体指针变量
struct tm* tp;
//调用获取秒数的函数
time(&t);
//调用转换函数,将秒数转换为年月日时分秒
tp = localtime(&t);
//将获取的时间写入到data中
sprintf(data, "%4d-%02d-%02d %02d:%02d:%02d", 1900 + tp->tm_year, 1 + tp->tm_mon, tp->tm_mday,
tp->tm_hour, tp->tm_min, tp->tm_sec);
}
void do_history(int connectfd, MSG* msg, sqlite3* db)
{
//作为容器承载字符串(尽量大一点不然会出现警告)
char sqlstr[256] = { 0 };
//定义一个指针指向错误信息
char* errmsg;
//组装查询record表的数据库的命令
sprintf(sqlstr, "SELECT * FROM record WHERE name='%s'", msg->name);
//执行sql语句的函数
if (SQLITE_OK != sqlite3_exec(db, sqlstr, history_callback, &connectfd, &errmsg)) {
//打印错误信息
printf("error : %s\n", errmsg);
//释放errmsg
sqlite3_free(errmsg);
return;
}
//发送完成后将OVER写入到msg的data中,让客户端接收到发送完成的指令
strcpy(msg->data, "OVER");
//给客户端发送数据包
if (-1 == send(connectfd, msg, sizeof(MSG), 0)) {
//打印错误信息
perror("send error");
}
return;
}
int history_callback(void* arg, int ncolumn, char** f_value, char** f_name)
{
//将给回调函数传递的参数接一下并强转成int型
int connectfd = *(int*)arg;
//定义一个结构体变量(数据包)
MSG msg;
memset(&msg, 0, sizeof(MSG)); //清空结构体
//有几行历史记录就调用几次回调函数
//将时间和单词的字段写入到msg的data中
sprintf(msg.data, "%s:%s", f_value[1], f_value[2]);
//给客户端发送数据包
if (-1 == send(connectfd, &msg, sizeof(MSG), 0)) {
//打印错误信息
perror("send error");
return -1;
}
return 0;
}