最近做了一个小项目,能够实现类似有道词典的注册,登录,查词,查询历史纪录等功能。通过这个项目,能够很好的联系Linux网络编程的知识。下面就来分享一下这个项目。

1.项目介绍

在线词典主要实现四个功能,分别是用户注册,用户登录,词汇查询以及用户查询历史的查询。项目整体架构为C/S架构。客户端启动后界面如下图所示:

客户端启动之后进入注册和登录界面。如上图所示,选择1为注册,选择2为登录,选择3为退出。当用户想要注册时,需要输入用户名和密码,需要注意的是,用户名不能与已有的用户名重复,否则无法注册,会提示用户已存在。

用户注册之后就可以进行登录,登录时同样需要输入用户名和密码。用户名和密码只要有一样不匹配,就不能进行登录操作,系统会提示错误信息,如下图所示:

用户登录成功以后,就会进入正式的软件界面。里面实现了查询词汇和查询历史纪录的功能。当用户选择查询词汇时,需要输入待查询的词汇,之后就会返回对该词汇的解释,如下图所示:

这里有一点需要注意,当用户在正式界面输入1选择查询词汇功能之后,用户可以不断地输入词汇,进行查询,软件并不会在查询一个单词之后返回正式界面,这也方便了用户的使用。如果用户不想再查询词汇了就输入"#"作为结束标志,如下图所示:

如上图所示,当用户选择2的时候,可以进行历史记录的查询。这时不需要用户输入额外的信息,只要再正式界面中选择2,就可以查到用户之前的历史记录,历史记录中包括查询的时间和对应的单词。这就是在线词典项目四个功能的介绍。

2.数据库设计

正如一般的C/S或者B/S系统一样,数据库是整个项目的核心和基石。客户端和服务器打交道,而服务器与数据库打交道,所以要根据项目需求合理设计数据库。本系统的数据库管理系统使用轻量级的软件sqlite3,数据库文件为dictionary.db。根据本项目需求,数据库中设置三个表:usr,record与dict。

2.1 usr

usr表记录用户信息,包括用户名name字段与密码pass字段,主要应用于注册和登录。为了防止重名现象,将用户名设置为主键。

当服务器处理客户端的注册请求时,会将客户端发来的用户名和密码作为一条记录插入到数据库中,由于有主键的限制,因此一旦客户端发来的用户名与数据库中已有的用户名相同,数据库就会报错,而服务器也籍此向客户端返回相应的应答。

当服务器处理客户端的登录请求时,会将客户端发来的用户名和密码作为查找字段去数据库中匹配,只要有一个字段不匹配,数据库就会返回空结果,服务器也能够由此向客户端返回相应的应答。

2.2 record

record表记录用户的查询历史,包括用户名name字段,时间date字段以及查询的单词word字段。当客户端发来查询历史的请求时,服务器可以根据客户端发来的用户名作为查找字段,查询相应的历史记录返回给客户端。

2.3 dict

dict表就是词库,包括单词word字段和相应的解释interpret字段。当客户端发来查询词汇的请求时,粉刷收起可以根据单词来查找相应的解释,并返回给客户端。不过一开始拿到的数据来源是以文件形式存在的,因此还需要导入到数据库中,具体内容后面会描述。

在开发之前按照刚才的描述建立数据表,相应的SQL语句如下:

CREATE TABLE usr (name text PRIMARY KEY, pass TEXT);
CREATE TABLE record (name TEXT, date TEXT, word TEXT);
CREATE TABLE dict (word TEXT, interpret TEXT);

3.网络数据包设计

由于本系统实现了注册,登录,查词和历史记录四个功能,因此有必要规范客户端与数据库之间数据传输,这也就是所谓的协议。出于方便起见,本系统将四个功能所需要传输的内容都封装到一个结构体中,在结构体的开头设置操作码来区分四个功能。其实这样设计不好,容易占用网络带宽,应该将其中某几个成员整合在一起,比如密码和查询的单词并不会同时出现在数据包中。不过出于省事,后续实现的时候就没再改动协议设计。代码如下所示:


//操作码协议规范:
// 1.表示注册
// 2.表示登录
// 3.表示退出
// 4.表示查询单词
// 5.表示查询历史记录
typedef struct {char msgcode;   //操作码char username[32]; //用户名char passwd[32]; //密码char word[32]; //要查询的单词char interpret[300]; //解释char searchtime[50]; //用户查询时间char reply[100]; //注册登录时的应答
} msg_t;​

结构体中每一个成员的内容在注释中都写得很明白,这里不再过多解释。只多说一点,由于之前考虑不甚周密,原本以为客户端退出时也会发送数据包,但实际开发时发现并不需要,因此操作码3也就是对应的退出操作就被废弃了,也就是说网络中并不会出现msgcode为3的数据包,只有1,2,4,5,对应的功能见代码中的注释。

4.数据导入

从这里开始,就进入正式的代码设计环节了。既然是C/S系统,那毫无疑问就分为客户端代码和服务器代码。但是本系统开发时拿到的词库是以.txt文本文件形式呈现的,故多增加一个数据库导入的代码。该代码只在开始的时候运行一次,用于将文件中的内容导入到数据库中,之后就不再运行。

词库在文件中的表现形式如下图所示:

每一个单词和它的解释再文本文件中占用一行,单词和解释之间以多个空格隔开。每一个单词和解释加起来不超过300字符。基于这些已知的信息,可以用标准I/O的方法,使用fgets函数读入一行,读入后将单词和解释分割开来,并分别作为数据库dict表中的word和interpret字段的值插入数据表中。

这里有一点要注意,单词和解释中会出现类似o'clock的形式,也就是中间有单引号,而众所周知,再sql语句中,对字符串的引用是用单引号或双引号的,因此文本中的单引号会造成sql语句的截断。这种问题有三种解决方法:

第一种是把单引号变成点符号,不过会造成词库内容的修改;

第二种是在词库中出现单引号的地方前面加入反斜线\,将单引号作为转义字符'\'',但是这样会造成大量数据移动,比较费时间;

第三种是修改sql语句,用双引号引用字符串,但是这需要确保词库中不再有双引号。

        在本系统中,使用第一种方法。代码实现不算困难,就是标准I/O和sqlite3 API的使用。就是注意字符串截断和刚才说的单引号问题即可。具体代码实现如下:

int main(int argc, const char* argv[])
{FILE *fp = fopen("dict.txt","r");char txt[350] = {0};char word[30] = {0};char inter[300] = {0};sqlite3 *my_db;if(SQLITE_OK != sqlite3_open("dictionary.db",&my_db)){DB_ERRLOG;}int i = 0;char sql[400] = {0};while(NULL != fgets(txt,sizeof(txt),fp)){memset(word,0,sizeof(word));memset(inter,0,sizeof(inter));memset(sql,0,sizeof(sql));for(i = 0;txt[i] != ' ';i++); //找到第一个空格strncpy(word,txt,i);  //把单词拷贝出来for(;txt[i] == ' ';i++); //找到第一个不是空格的内容strcpy(inter,txt+i);int j = 0;snprintf(sql,sizeof(sql),"insert into dict values(\"%s\",\"%s\")",word,inter);if(SQLITE_OK != sqlite3_exec(my_db,sql,NULL,NULL,NULL)){printf("%s\n",sql);DB_ERRLOG;}}printf("装入数据库完成!\n");fclose(fp);sqlite3_close(my_db);return 0;
}

5.服务器框架

服务器与客户端的连接使用TCP协议,而TCP协议由于会在accept和recv函数阻塞,因此TCP服务器默认不能做到并发。要想并发,可以有三种手段:

       多线程:优点在于调度负担小,缺点在于如果涉及到临界资源的使用需要设计同步互斥机制。

       多进程:优点在于各进程资源独立,不用考虑临界资源使用的问题,缺点在于进程切换时上下文转换开销大。

        IO多路复用:能够有效地避免多线程和多进程的问题,本质上仍然是串行循环,只不过可以“按需循环”,哪个客户端准备好了数据就处理哪个客户端的请求。这是本系统实现TCP并发服务器的方法。

关于IO多路复用,网上有很多资料可以参考,我再简单地说一下它的思想。将感情绪的文件描述符编制为一张表,调用select/poll/epoll,由内核监视其行为,如果所有的文件描述符都未就绪,则在select/poll/epoll函数中阻塞,至少有一个文件描述符就绪,则从上述三个函数中返回,返回值为就绪的文件描述符的个数。在select函数中会将未就绪的文件描述符从表中擦除,保留就绪的文件描述符。程序可以通过遍历的方式寻找就绪的文件描述符,并对其做相应的处理。

本系统在处理就绪的文件描述符时,读取客户端发来的数据包,解析其中的msgcode成员,根据成员值的不同做相应的操作,分别是注册,登录,查询单词和查询历史记录。框架代码如下:

int main(int argc, const char* argv[])
{if (argc != 3) {printf("Usage : %s <ip> <port>\n", argv[0]);exit(-1);}sqlite3* my_db;char sql[256] = { 0 };if (SQLITE_OK != sqlite3_open(DATABASE, &my_db)) {DB_ERRLOG;}printf("数据库打开成功!\n");memset(sql, 0, sizeof(sql));sprintf(sql, "CREATE TABLE IF NOT EXISTS usr(name text PRIMARY KEY, pass TEXT)");if (SQLITE_OK != sqlite3_exec(my_db, sql, NULL, NULL, NULL)) {DB_ERRLOG;}printf("usr表打开成功!\n");memset(sql, 0, sizeof(sql));sprintf(sql, "CREATE TABLE IF NOT EXISTS record(name TEXT, date TEXT, word TEXT)");if (SQLITE_OK != sqlite3_exec(my_db, sql, NULL, NULL, NULL)) {DB_ERRLOG;}printf("record表打开成功!\n");memset(sql, 0, sizeof(sql));sprintf(sql, "CREATE TABLE IF NOT EXISTS dict(word TEXT, interpret TEXT)");if (SQLITE_OK != sqlite3_exec(my_db, sql, NULL, NULL, NULL)) {DB_ERRLOG;}printf("dict表打开成功!\n");int socketfd;if (-1 == (socketfd = socket(AF_INET, SOCK_STREAM, 0))) {ERRLOG("socket error");}struct sockaddr_in serveraddr;serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(argv[1]);serveraddr.sin_port = htons(atoi(argv[2]));socklen_t serveraddrlen = sizeof(serveraddr);if (-1 == bind(socketfd, (struct sockaddr*)&serveraddr, serveraddrlen)) {ERRLOG("bind error");}if (-1 == listen(socketfd, 10)) {ERRLOG("listen error");}fd_set fds;FD_ZERO(&fds);fd_set fds_temp;FD_ZERO(&fds_temp);int max_fd = 0;FD_SET(socketfd, &fds);max_fd = (max_fd > socketfd) ? max_fd : socketfd;struct sockaddr_in clientaddr;socklen_t clientaddrlen;int acceptfd;int ret;int nbytes;int i;msg_t msg;int opcode;printf("服务器启动成功!\n");while (1) {fds_temp = fds;if (-1 == (ret = select(max_fd + 1, &fds_temp, NULL, NULL, NULL))) {ERRLOG("select error");}for (i = 3; i <= max_fd && ret; i++) {if (FD_ISSET(i, &fds_temp)) {ret--;if (i == socketfd) {if (-1 == (acceptfd = accept(socketfd, (struct sockaddr*)&clientaddr, &clientaddrlen))) {ERRLOG("accept error");}printf("%s:%d发来了连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));FD_SET(acceptfd, &fds);max_fd = (max_fd > acceptfd) ? max_fd : acceptfd;} else {memset(&msg, 0, sizeof(msg));if (-1 == (nbytes = recv(i, &msg, sizeof(msg), 0))) {ERRLOG("recv error");}if (nbytes == 0) {//表示断开连接printf("客户端%d断开连接\n", i);FD_CLR(i, &fds);close(i);continue;}opcode = msg.msgcode;switch (opcode) {case 1:server_register(my_db, i, msg);break;case 2:server_login(my_db, i, msg);break;case 3:break;case 4:server_query(my_db, i, msg);break;case 5:server_history(my_db, i, msg);break;}}}}}close(socketfd);sqlite3_close(my_db);return 0;
}

如上图所示,服务器首先打开数据库,获取数据库句柄。之后创建usr,record,dict三张表格,由于服务器可能启动多次,但是表格不能创建多次,因此在sql语句中加入"IF NOT EXISTS"以确保数据表只会创建一次。

之后就是TCP服务器网络编程的过程了,基本上就是创建监听套接字->填写网络信息->绑定套接字于网络信息->将套接字设置为监听->等待客户端的连接请求->与客户端之间收发数据->关闭套接字。

在等待客户端请求之前,先创建感兴趣的文件描述符表,将监听套接字放在表中。一旦表中有文件描述符就绪,则遍历表做相应的处理。

如果是监听套接字准备就绪,说明有客户端发来连接请求,此时调用accept函数接收请求,创建交互套接字acceptfd,并将交互套接字放在文件描述符表中监听;

如果是交互套接字准备就绪,说明有客户端发来数据包,此时服务器解析数据包,根据包中的msgcode成员信息做相应的处理。

具体的网络编程和IO多路复用模型可以参考其他博文和资料,这里就不再赘述了,详细代码参考代码包中的server.c。

6.客户端框架

客户端的功能可分为两组。注册和登录一组,查词汇和历史记录是另一组。用户打开客户端后,首先看到第一组功能,也就是注册和登录。显示注册和登录的函数如下所示:

void print_login_interface()
{printf("************************************\n");printf("* 1: register   2: login   3: quit *\n");printf("************************************\n");printf("please choose : ");
}

用户可在此不断地进行注册操作,注册多个用户名。而对于登录操作,只要登录成功,就要进入客户端的第二组功能,也就是查词汇和历史记录,就不再注册和登录这里面循环了。显示查词汇和查历史记录的函数如下所示:

void print_formal_interface()
{printf("************************************\n");printf("* 1: query   2: history   3: quit  *\n");printf("************************************\n");printf("please choose : ");
}

客户端整体框架代码如下所示:

int login_OK = 0; //用来表示登陆操作是否完成,0表示未完成,1表示完成
char username[32] = { 0 }; //用于保存登录成功之后的用户名
int main(int argc, const char* argv[])
{if (argc != 3) {printf("Usage : %s <ip> <port>\n", argv[0]);exit(-1);}int socketfd;if (-1 == (socketfd = socket(AF_INET, SOCK_STREAM, 0))) {ERRLOG("socket error");}struct sockaddr_in serveraddr;socklen_t serveraddrlen = sizeof(serveraddr);serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(argv[1]);serveraddr.sin_port = htons(atoi(argv[2]));int choose;msg_t answer_msg;int retchoose;if (-1 == connect(socketfd, (struct sockaddr*)&serveraddr, serveraddrlen)) {ERRLOG("connect error");}while (1) {print_login_interface();if (1 != (retchoose = scanf("%d", &choose))) {printf("input error,please try again!\n");continue;}// printf("%d\n", choose);while (getchar() != 10);switch (choose) {case 1://注册client_register(socketfd, &answer_msg);break;case 2:client_login(socketfd, &answer_msg);break;case 3:close(socketfd);printf("Thank you for using this software! GoodBye!\n");exit(0);default:printf("input error,please try again!\n");continue;}if (login_OK) {break; //跳出当前循环,进入正式界面}}while (1) {print_formal_interface();if (1 != (retchoose = scanf("%d", &choose))) {printf("input error,please try again!\n");continue;}while (getchar() != 10);switch (choose) {case 1:client_query(socketfd, &answer_msg);break;case 2:client_history(socketfd,&answer_msg);break;case 3:close(socketfd);printf("Thank you for using this software! GoodBye!\n");exit(0);default:printf("input error,please try again!\n");continue;}}return 0;
}

大家从上面的代码可以看出,客户端一开始也是网络编程的步骤,包括获取套接字,填写服务器的网络信息,与服务器进行连接这三步操作,之后大家可以看到两组while(1)循环,分别对应了刚才说的两组功能。

需要注意的是,代码中设置了两个全局变量,login_OK和username,用来标记是否登录成功以及记录登录成功后的用户名信息。在客户端登录函数client_login中,如果服务器发来登录成功的应答,程序会将login_OK置为1,并保存当前的用户名。在第一组while循环的结尾,会读取login_OK的值,一旦为1,则结束第一组while(1)循环,进入第二组while(1)循环。

7.注册

在客户端,注册的操作就是封装一个msgcode为1的数据包,内附自己的用户名和密码,发送给服务器。之后等待服务器的应答消息,显示应答消息即可。

在服务器,注册的操作就是解析客户端发来的数据包中的用户和密码信息,并将其作为一条记录插入到数据库中。如果插入成功,则服务器向客户端发送插入成功的应答;如果插入失败,则原因基本上就是主键限制,此时服务器就会向客户端发送用户已存在的应答信息。

客户端注册代码实现如下:

int client_register(int socketfd, msg_t* ansmsg)
{msg_t msg;memset(&msg, 0, sizeof(msg));msg.msgcode = 1;printf("input your name:");fgets(msg.username, sizeof(msg.username), stdin);msg.username[strlen(msg.username) - 1] = 0;int nbytes;printf("input your password:");fgets(msg.passwd, sizeof(msg.passwd), stdin);msg.passwd[strlen(msg.passwd) - 1] = 0;//发送请求注册数据包if (-1 == send(socketfd, &msg, sizeof(msg), 0)) {ERRLOG("send error");}//等待服务器的注册应答if (-1 == (nbytes = recv(socketfd, ansmsg, sizeof(msg_t), 0))) {ERRLOG("send error");}if (nbytes == 0) {printf("与服务器断开连接!,程序自动退出\n");close(socketfd);exit(-1);}//服务器将注册应答封装在应答包的reply成员中,这里面只要打印即可printf("%s\n", ansmsg->reply);return 0;
}

服务器注册代码实现如下:

int server_register(sqlite3* my_db, int acceptfd, msg_t msg)
{msg_t ansmsg;memset(&ansmsg, 0, sizeof(ansmsg));char sql[256] = { 0 };char reply[100] = { 0 };printf("客户端%d发来注册请求\n", acceptfd);snprintf(sql, sizeof(sql), "insert into usr values('%s','%s')", msg.username, msg.passwd);int ret;if (SQLITE_OK != (ret = sqlite3_exec(my_db, sql, NULL, NULL, NULL))) {if (ret == 19) {// 19号错误码属于违反主键错误,需单独处理ansmsg.msgcode = 1;snprintf(reply, sizeof(reply), "register : user %s already exist!!!", msg.username);strcpy(ansmsg.reply, reply);} else {//其他错误的时候关闭连接close(acceptfd);DB_ERRLOG;}} else {ansmsg.msgcode = 1;snprintf(reply, sizeof(reply), "register : OK");strcpy(ansmsg.reply, reply);}if (-1 == send(acceptfd, &ansmsg, sizeof(ansmsg), 0)) {ERRLOG("send error!");}return 0;
}

有几个细节需要大家注意:

首先,客户端函数中的第二个参数是指向应答数据包的指针。换言之,我在客户端的主程序中定义了一个应答数据包ansmsg,客户端等待服务器的应答包时,就让服务器把应答数据写到客户端主程序中定义的ansmsg中。这个参数在后续的登录,查询,和历史记录中都会用到,就是专门用来接收服务器的应答包的。

第二,服务器调用sqlite3_exec函数执行sql语句,有关sqlite3 API的使用不是本文的重点,如有需要请查找相应的资料。我想说的是,执行结果不正常的时候会返回错误码,错误码有两种情况,一种情况是违反了主键规定,此时返回的错误码是19,这是sqlite3实现中规定的,大家可以上sqlite3官网手册中查询,这种情况下应该单独处理,发送用户已存在的应答信息,而对于其他错误,我们认为是真正的失败操作,此时应该结束进程,打印错误信息。当然,如果插入成功,返回成功的注册应答信息即可。

8.登录

在客户端,登录操作就是发送msgcode为2的数据包,内附用户名和密码。并等待服务器的应答信息并显示。需要注意的是,如果服务器发来了登录成功的消息,应该将login_OK标记置为1,此时客户端框架便会读取该标记的值,结束登录操作,跳转到客户端的第二组功能。如果登录不成功,则重新输入即可。

在服务器,登录操作就是接收客户端发来的数据包,解析其中的用户名和密码,并将其作为查询字段在数据库中查找信息。sqlite3中提供了sqlite3_get_table函数可用于获取查询结果,还能返回查询的行数和列数。对于登录操作,我们对查询结果实际上并不感兴趣,我们感兴趣的是行数,如果行数不为0,说明用户名和密码信息都是正确的,否则说明用户名或密码有误。服务器会籍此返回相应的应答。

客户端登陆代码实现如下:

int client_login(int socketfd, msg_t* ansmsg)
{msg_t msg;memset(&msg, 0, sizeof(msg));msg.msgcode = 2;printf("input your name:");fgets(msg.username, sizeof(msg.username), stdin);msg.username[strlen(msg.username) - 1] = 0;int nbytes;printf("input your password:");fgets(msg.passwd, sizeof(msg.passwd), stdin);msg.passwd[strlen(msg.passwd) - 1] = 0;//发送请求登录数据包if (-1 == send(socketfd, &msg, sizeof(msg), 0)) {ERRLOG("send error");}//等待服务器的登录应答if (-1 == (nbytes = recv(socketfd, ansmsg, sizeof(msg_t), 0))) {ERRLOG("send error");}if (nbytes == 0) {printf("与服务器断开连接!,程序自动退出\n");close(socketfd);exit(-1);}//服务器将登录应答封装在应答包的reply成员中,这里面只要打印即可//如果是login:OK则跳转到正式界面//并且还要记录正式的登录用户名if (strcmp(ansmsg->reply, "login : OK") == 0) {login_OK = 1;memset(username, 0, sizeof(username));strcpy(username, msg.username);}printf("%s\n", ansmsg->reply);
}

服务器登录代码实现如下:

int server_login(sqlite3* my_db, int acceptfd, msg_t msg)
{msg_t ansmsg;memset(&ansmsg, 0, sizeof(ansmsg));char sql[256] = { 0 };char reply[100] = { 0 };printf("客户端%d发来登录请求\n", acceptfd);int nRow;int nColumn;char** res;snprintf(sql, sizeof(sql), "select * from usr where name='%s' and pass='%s'", msg.username, msg.passwd);printf("loginsql:%s\n", sql);int ret;if (SQLITE_OK != (ret = sqlite3_get_table(my_db, sql, &res, &nRow, &nColumn, NULL)))                                                     {DB_ERRLOG;}if (nRow == 0) {//说明查无此人ansmsg.msgcode = 2;snprintf(reply, sizeof(reply), "login : name or password is wrony!!!");strcpy(ansmsg.reply, reply);} else {ansmsg.msgcode = 2;snprintf(reply, sizeof(reply), "login : OK");strcpy(ansmsg.reply, reply);}if (-1 == send(acceptfd, &ansmsg, sizeof(ansmsg), 0)) {ERRLOG("send error!");}sqlite3_free_table(res);return 0;
}

9.查词

对于客户端来说,查词和登录的操作没有太大的区别,只是操作码变成了4,内置的信息变成了单词和用户名而已。服务器根据单词查询解释,如果返回行数为零说明单词不存在,不为零则把解释发送给客户端作为应答,这里需要获取查询的结果,不能像登录操作一样只关心返回的行数。具体过程就不赘述了。

真正需要提的是服务器在查词之后的操作。由于后续还要实现历史记录查询的操作,因此在服务器查询单词之后要记录当前查询的时间,查询的单词和查询的用户。这也是我之前要在客户端记录登录的用户名的原因。C语言提供了获取系统时间的函数time和localtime。其中time返回一个很大的秒数,localtime则可以将秒数转化为年月日时分秒,具体细节可以参考其他资料。服务器端获取到时间信息之后,将其格式化为字符串,与单词和用户名一起作为一条记录插入到record表中。本系统设置无论客户端查询的单词是否存在都记录查询时间,当然根据需要也可以设置为只在单词查询到的时候记录时间。

客户端查词代码实现如下:

int client_query(int socketfd, msg_t* ansmsg)
{msg_t msg;memset(&msg, 0, sizeof(msg));msg.msgcode = 4;strcpy(msg.username, username);int nbytes;printf("---------\n");printf("input word : ");while (1) {fgets(msg.word, sizeof(msg.word), stdin);msg.word[strlen(msg.word) - 1] = 0;if (strcmp(msg.word, "#") == 0) {break;}//发送请求查询数据包if (-1 == send(socketfd, &msg, sizeof(msg), 0)) {ERRLOG("send error");}//等待服务器的查询应答if (-1 == (nbytes = recv(socketfd, ansmsg, sizeof(msg_t), 0))) {ERRLOG("send error");}if (nbytes == 0) {printf("与服务器断开连接!,程序自动退出\n");close(socketfd);exit(-1);}printf("\t");printf("%s\n", ansmsg->interpret);printf("input word : ");}return 0;
}

服务器查词代码实现如下:

int server_query(sqlite3* my_db, int acceptfd, msg_t msg)
{msg_t ansmsg;memset(&ansmsg, 0, sizeof(ansmsg));char sql[256] = { 0 };char interpret[300] = { 0 };printf("客户端%d发来查询单词请求\n", acceptfd);int nRow;int nColumn;char** res;snprintf(sql, sizeof(sql), "select interpret from dict where word='%s'", msg.word);printf("querysql:%s\n", sql);int ret;if (SQLITE_OK != (ret = sqlite3_get_table(my_db, sql, &res, &nRow, &nColumn, NULL))) {DB_ERRLOG;}if (nRow == 0) {//说明没有这个单词ansmsg.msgcode = 4;snprintf(interpret, sizeof(interpret), "not found");strcpy(ansmsg.interpret, interpret);} else {ansmsg.msgcode = 4;strcpy(ansmsg.interpret, res[1]);}sqlite3_free_table(res);if (-1 == send(acceptfd, &ansmsg, sizeof(ansmsg), 0)) {ERRLOG("send error!");}time_t tm = time(NULL);struct tm* lt;lt = localtime(&tm);char date[50] = { 0 };snprintf(date, sizeof(date), "%d-%02d-%02d %02d:%02d:%02d", lt->tm_year + 1900, lt->tm_mon + 1, lt->tm_mday, lt->tm_hour, lt->tm_min, lt->tm_sec);memset(sql, 0, sizeof(sql));snprintf(sql, sizeof(sql), "insert into record values('%s','%s','%s')", msg.username, date, msg.word);printf("%s\n", sql);if (SQLITE_OK != sqlite3_exec(my_db, sql, NULL, NULL, NULL)) {DB_ERRLOG;}return 0;
}

关于客户端,还有一个细节需要注意。那就是我把查词模块设置为一个while循环,这样用户在选择查词功能之后可以不断地查询,知道按下#结束,方便用户的使用。

10.查询历史纪录

对客户端来说,查询历史纪录只需要发送一个msgcode为5的数据包,并附上自己的用户名,之后等待答复即可。对服务器来说,查询历史纪录只需要根据客户端发来的用户名在record表中查询相应的历史记录,之后发回客户端即可。看起来很容易对吧,有没有什么细节呢?

如果你细心,你就会想到,无论是客户端还是服务器,在查询到之前,都不可能知道用户到底有几条历史记录。这不像查单词,单词肯定只有一条解释,所以一问一答就行了。但是历史记录的数量是未知的,能用一个数据包发给客户端吗?答案是不行,因为那样就不知道数据包设计成多大了。所以只能发送多个包,每个包包含一条历史记录信息。这就要求服务器用循环结构发送应答包,客户端用循环结构接收应答包。

那么问题又来了,客户端怎么知道该接受多少个应答包呢?这个问题有两种解决方案:

一种是服务器在发送完应答包之后发送一个结尾字符,当客户端收到结尾字符之后就明白历史记录接收完毕,因此结束循环,不再接受新的应答包;

另外一种是服务器先给客户端发送一个数字,注意这里面只发送一个数字,告知客户端有多少条历史记录,当客户端收到这个数字之后,就可以建立一个for循环,设置相应的循环次数接收历史记录数据包。这是本系统使用的方法。

        大家发现,这两种方法都体现出了浓厚的协议的思想。服务器和客户端要约定好一些事情,按照顺序发送和接收数据,这就是协议,只有双方都按照协议办事才能办成事。就像这里面规定了服务器先发送数字再设置相应的循环次数发送数据包,那么客户端就得先等待数字,然后设置相应的循环次数准备接收数据包并显示,双方的步调必须一致,否则对数据的解析就会出严重的问题。这里我们还是用sqlite3_get_table函数,因为他会告诉我们返回记录的个数,很方便。服务器就把这个记录的个数发送给客户端就行了。

客户端查询历史记录代码实现如下:

int client_history(int socketfd, msg_t* ansmsg)
{msg_t msg;memset(&msg, 0, sizeof(msg));msg.msgcode = 5;strcpy(msg.username, username);int nbytes;int numofrecords;//发送请求查询历史纪录数据包if (-1 == send(socketfd, &msg, sizeof(msg), 0)) {ERRLOG("send error");}//先收到记录的条数if (-1 == (nbytes = recv(socketfd, &numofrecords, sizeof(numofrecords), 0))) {ERRLOG("send error");}int i = 0;for (i = 0; i < numofrecords; i++) {if (-1 == (nbytes = recv(socketfd, ansmsg, sizeof(msg_t), 0))) {ERRLOG("send error");}printf("%s : %s\n",ansmsg->searchtime,ansmsg->word);}return 0;
}

服务器查询历史纪录的代码实现如下:

int server_history(sqlite3* my_db, int acceptfd, msg_t msg)
{msg_t ansmsg;memset(&ansmsg, 0, sizeof(ansmsg));char sql[256] = { 0 };printf("客户端%d发来查询历史记录请求\n",acceptfd);snprintf(sql, sizeof(sql), "select date,word from record where name = '%s'", msg.username);int nRow;int nColumn;char** res;int ret;if (SQLITE_OK != (ret = sqlite3_get_table(my_db, sql, &res, &nRow, &nColumn, NULL))) {DB_ERRLOG;}if (-1 == send(acceptfd, &nRow, sizeof(int), 0)) {ERRLOG("send error");}int i, j;for (i = 1; i <= nRow; i++) {memset(&ansmsg, 0, sizeof(ansmsg));ansmsg.msgcode = 5;//清空答复包for (j = 0; j < nColumn; j++) {if (j == 0) {//第一列是时间strcpy(ansmsg.searchtime, res[i * nColumn + j]);} else if (j == 1) {//第二列是单词strcpy(ansmsg.word, res[i * nColumn + j]);}}if (-1 == send(acceptfd, &ansmsg, sizeof(ansmsg), 0)) {ERRLOG("send error");}}sqlite3_free_table(res);
}

11.总结

至此,在线英英词典的基本功能就都实现了,项目总体不大,不过能够很好的串联起网络编程和数据库的使用,完整的代码我会打包上传到资源,大家也可以在此基础上做扩展。

在线英英词典项目实现相关推荐

  1. 基于linux的在线英汉词典(带源码)

    源码下载: 基于Linux的在线英汉词典-网络基础文档类资源-CSDN文库https://download.csdn.net/download/qq_63626307/86744311?spm=100 ...

  2. 【小型JavaFx项目】英汉词典

    英汉词典 简介 知识点简介 效果图 源代码 Word.java DictionaryFunction.java DictionaryFrame.java 简介 花了1个多小时帮数媒的同学用以前的项目改 ...

  3. python电子英汉词典显示_如何使用python为Linux打造一款命令行下的在线英汉词典...

    项目说明 : 在linux终端下有时候遇到一个想查询的英语单词 , 但是不想打开浏览器去谷歌或者百度去搜索 , 因此就写了这个基于爬虫的单词翻译工具 , 实现原理很简单 , 基本开发已经完成 ,总共有 ...

  4. python软件界面翻译_python英汉词典,在线翻译器,带GUI界面下载

    使用请求模块获得的职位信息有道词典网页,BeautifulSoup获得所需的内容,并生成gui界面通过tkinter模块.2. Python实现翻译工具git源代码地址代码如下:# !\/bin\/e ...

  5. 常用在线英汉、英英词典

    英汉词典站点: Dict.CN: http://dict.cn/        百度词典: http://dict.baidu.com/ 英英词典站点: TheFreeDictionary: http ...

  6. [Silverlight]使用MVVM模式打造英汉词典

    最近比较关注MVVM(Model-View_ViewModel)模式,该模式十分适合WPF/Silverlight的开发.出于练习的目的打算使用Silverlight做个英汉词典(可能是由于近来疯狂的 ...

  7. 2018-09-06 Java实现英汉词典API初版发布在Maven

    在打算批量代码汉化工具 · Issue #86 · program-in-chinese/overview时, 发现没有现成的Java库实现英汉查询功能. 于是开此项目. 源码库: program-i ...

  8. APP Designer 制作简易英汉词典的回调函数书写

    APP Designer 制作简易英汉词典的回调函数书写 在学习MATLAB 的App Designer 时看到的一个有意思的小项目,主要步骤参考了slandarer 的MATLAB 手把手带你制作第 ...

  9. PubMed(丁香)英汉词典爬取

    使用Scrapy爬去PubMed(丁香)英汉词典 1.使用Scrapy创建项目 scrapy startproject med 2.进入med文件夹创建HtmlFilter.py用于除去标签. 实现参 ...

  10. Linux网络编程之六 --在线英英字典的实现

    综合项目:在线英英字典 服务器端 head.h: ​ server.c do_client.c Makefile 客户端 head.h client.c Makefile 关注微信公众号获取更多资讯

最新文章

  1. java 技术 管理_java技术转型产品或者项目管理,请大神指导?
  2. 1000亿美元!英特尔要在美国建世界最大芯片厂,美520 亿美元芯片法案接近敲定...
  3. python训练模型函数参数_一步步亲手用python实现Logistic Regression
  4. window python输入路径问题--转义字符
  5. asp.net oracle优化,[转]ASP.NET性能优化
  6. 【POJ 1200】Crazy Search(将字符映射为数字,将NC进制hash成10进制)
  7. 【PAT甲级 StringBuilder的使用】1005 Spell It Right (20 分) Java版 7/7通过
  8. mysql aggregate_SQL语句之Aggregate函数
  9. 《C程序设计语言》-第2章-习题
  10. Sleep v.s. sleep
  11. ASA virtual telnet
  12. 目标规划运筹学例题doc_7.运筹学之目标规划(胡运权版).doc
  13. lcd1602c语言编程原理,lcd1602工作原理是什么?
  14. 精曲的竖曲线4800计算程序,可以计算直线与竖曲线通杀
  15. 【小工具】 - linux内存碎片检查和清理
  16. [转帖]2.0&TBC 术士常用宏+宏答疑
  17. 智能校对的技术原理和实践
  18. squid高性能代理缓存服务器
  19. 企业微信群机器人怎么用
  20. 中国私有云向头部玩家集中,如何走出差异化竞争路线?

热门文章

  1. 创建一个HashMap 集合,存储省份下的城市,key是为省份 value 是为该省份下的城市
  2. 南航计算机系复试内容,2010年南航计算机专业复试真题
  3. bpsk调制rician_fading信道的simulink仿真
  4. 18、HX1838红外遥控模块控制led
  5. 有哪些将英文文献翻译为中文的网站或软件?
  6. creo数控编程怎么样_creo3.0数控编程加工使用实例视频教程入门到精通 车床编程...
  7. 直方图均衡化与直方图规定化
  8. 计算机绘图 精品课程,机械制图与计算机绘图精品课程建设与成效.doc
  9. 新手如何在CSDN上写博客
  10. PCAN-Explorer5安装教程