1. 引言
随着科技的不断发展以及互联网的进步,许多在线下才能完成的事情,已可以通过互联网的帮助,在手机或者电脑上完成,而为人们节省下许多时间。早期购买火车票时,都需要到各地的火车站排队,但是在网上购票系统出现后,人们便能通过轻松地点击手机或鼠标,就将自己所需的车票购买下来,如此提高了购票效率,为生活带来很大的便利。
由于Linux系统具有免费和开源等特性,以及拥有多任务和丰富的网络接口(如TCP和UDP)和更加安全的用户及文件权限策略,再加上Android是如今主流的手机系统,所以使用Linux和Android studio进行开发与编程,可以提供良好和稳定的服务端,也能实现一个操作便捷、随时使用的GUI (图形操作界面)的客户端,以便用户能随时随地进行操作。
本设计基于Linux系统下来实现一个具有图形使用者界面的火车售票系统,其附有:用户买票/退票/改签、用户注册/登录、管理员管理用户/车票信息、通过python爬虫的方式将12306中的车票载入本地、操作简捷的安卓APP等功能。根据以上的需求,拟定整体的设计思路与开发流程之后,确定整个设计分为服务端和客户端两个部分:服务端负责接收和处理客户端传递过来的信息,车票、用户信息的存储,以及大部分功能的实现;客户端主要负责信息的显示,以及获取用户输入的信息,通过socket通信将信息传递给服务端进行处理。整体设计框架如下:
1) Linux服务端有多个线程,分别是主线程负责接收客户端的连接,然后将连接的客户端标志写入连接状态表中;分线程1负责监听连接状态表中的客户端是否有发送消息过来,如果有就将消息写入数据缓冲区;还有n个分线程2负责监听数据缓冲区是否具有数据,如果有数据就将数据进行对应的处理。
2) 安卓客户端大体是分为不同的活动,以及一个为了长时间连接客户端,以及服务端能检测到客户端保持在线,而持续发送的心跳包的服务。服务是在运行APP初就开始持续执行的,活动的运行则是在主活动运行之后,根据用户不同的操作,切换到不同的活动中。
2. 程序开发环境介绍
本设计进行开发所使用的程序,简单的概述如下:
1) GCC (GNU编译器套件)
GCC (GNU Compiler Collection,GNU编译器套件)整体流程分为:预处理、编译、汇编、链接四个步骤,首先调用cpp进行预处理,对.c文件中的包含和宏定义进行分析,生成.i文件。第二步调用cc1进行编译,生成.s的汇编文件。第三步调用as进行汇编,生成.o文件。最后调用ld将.o文件进行链接,生成可执行文件 [1]。
2) Makefile编译
使用Makefile进行自动化编译,写好Makefile的文件,输入一个make,进行自动编译。Makefile执行多条GCC编译语句,通过不同的语句进行复杂的一些操作,同时还可以加入shell脚本,执行一些操作系统的命令 [2]。
3) GDB调试工具
Linux工具在GCC编译时加入-g这个编译选项,通过GDB调试,接着在Linux终端输入gdb,进入GDB的工作界面,通过一些基础的命令来调试程序。
4) tcpdump调试工具
tcpdump根据使用者的需求对网络上的数据包进行截获的工具,是Linux系统中的一个调试工具。
5) pycharm
pycharm使用python语言进行高效开发的工具,具有调试、代码跳转、智能提示、版本控制等功能。本设计创建好project和文件,将爬虫的代码写入文件,编译器自动地进行编译。
6) Android studio
Android studio前端布局的实时预览、自带的安卓模拟器、第三方的sdk和开源项目、gradle构建系统,可以通过拖动控件就将其加入布局中等功能 [3]。
3. 程序技术要点原理
本设计使用的程序技术概述如下:
1) socket网络通信编程
本设计在Linux部分使用了TCP协议 [4],主要依序调用socket()、bind()、listen()、int accept()等函数,连接上客户端之后,调用sendto()、sendmsg()、send()之类的函数就能发送数据到客户端,也可以通过调用recvfrom()、recvmsg()、recv()这类的函数接收客户端的数据。最后,在不需要传输数据后,通过调用int close(int sockfd)关闭socket对象。
2) 多线程的使用
使用多线程技术,就可以给accept()函数另外创建一条线程,能够一直保持等待接收客户端的连接请求,不会影响到与已连接的客户端进行通信和对数据进行处理 [5]。
3) 多线程并发处理与多线程同步
使用复杂点的多线程难免会遇到并发的问题,需要对线程进行同步的处理。
4) IO多路复用服务端模型
图1展示了IO多路复用服务端模型。使用IO多路复用服务端模型的好处在于相比于单进程或者是单线程的服务端,实现多个客户端连接和收发数据;可以一个线程负责接收客户端请求连接,一个线程负责接收数据,将两个不同的功能分开,减小耦合 [6]。
5) 活动(Activity)
安卓的活动内容有活动的创建、在AndroidManifest.xml中注册活动、活动之间的跳转、以及活动的生命周期,图2展示了活动的生命周期 [7]。
![](//html.hanspub.org/file/6-1542270x7_hanspub.png?20211015093738037)
Figure 1. IO multiplexing server model
图1. IO多路复用服务端模型图
6) 线性布局(LinearLayout)和相对布局(RelativeLayout)
通过android:orientation属性来指定是水平排列还是竖直排列。当其属性设置为android:orientation=horizontal则为水平排列,若android:orientation=vertical则为竖直排列。相对布局用顶部、底部、中心、右边、左边或者父布局或者其他控件等等为基准来设置位置 [8]。
7) 服务
Android的服务用于后台用户切换到另一个APP,APP不被系统杀死,服务不会中断 [8]。服务和活动需要在AndroidManifest.xml文件中注册之后才能使用。
8) 心跳包
安卓APP在后台被直接杀死,无法正常地断开socket的连接,为了与Linux服务端保持长连接,同时也让Linux服务端能知道安卓APP是否还在保持连接状态,需要加入心跳包的功能。安卓客户端需要定时发送一个数据包给Linux服务端,Linux服务端接收到心跳包就判定安卓客户端还在保持连接。如果超过一定时间Linux服务端没有收到心跳包的话,判定安卓客户端已经断开了,将socket的连接给断开。
9) python爬虫
requests模块是python中原生的一款基于网络请求的模块,用于模拟浏览器发请求,可将请求后得到的数据进行进一步分析来得到想要的数据 [9]。UA (User-Agent)是用户代理,不使用UA伪装的话,默认的UA很容易会被网站监测到是爬虫,然后请求不到数据,所以需要将爬虫通过UA伪装,伪装成浏览器,就可以请求到网站的数据了 [10]。
10) GTK+
GTK+是图形界面库,创建窗口、显示控件、信号注册函数等功能 [11]。
4. 程序设计
本设计使用的程序开发环境与技术要点之后,接着分成服务端与客户端两部分来说明本设计的程序设计。
4.1. Linux服务端程序设计
服务端主要分为GTK线程、销毁线程、接收客户端连接、接收客户端数据和处理客户端数据并响应等五个线程,其总体框架如图3所示。在服务端初始化时,先读取本地文件,将所需信息进行读取;创建GTK线程,以便将信息显示到GTK的图形界面上;创建销毁线程,等待销毁信号,将服务端运行时申请的内存释放,以及保存所需数据;创建接收数据线程,接收客户端的数据;创建处理线程,处理来自客户端的数据;然后在运行函数部分使用主进程对客户端的连接请求进行监听,将申请连接的客户端接入。这些线程简单说明如下:
1) GTK线程
初始化GTK的数据后,调用gtk_main()函数等待有关GTK的信号,当其他线程调用有关GTK的函数后,会在图形界面上显示信息。
2) 销毁线程
等待Ctrl + c的信号,当按下Ctrl + c后,或者操作者按下退出后,销毁线程开始执行,对之前运行时申请的内存进行释放,并将相关数据保存到文件中。
3) accept()接收客户端连接的线程
等待连接部分的主体使用accept()函数对预设好的ip地址和端口进行监测,当有客户端进行连接时,就可以得到客户端的套接字标识。以及当每个客户端连接时,给每个客户端设定一个初始的时间,以便后面对客户端发送心跳包进行计时。最后将接收到的客户端的套接字标识写入环形缓冲区1。
4) select()接收客户端数据的线程
接收客户端数据部分使用select()函数对已连接的客户端进行监听,监听是否有数据包或者心跳包传输给服务端。客户端超过预定的时间都还没有传输心跳包给服务端,该客户端断开socket连接,将其移除监听列表。当收到客户端的数据时,先分析数据包的结构,是否符合自己设定好的通信协议,对数据进行接收,并写入环形缓冲区2;若是不符合,则回传一条消息给客户端,告诉客户端数据错误。
5) 处理客户端数据并响应的线程
对来自客户端的不同的数据进行处理,然后将处理结果的数据再发送回客户端。大致分为处理用户数据部分和处理车票数据部分两大类。
在处理用户数据部分,主要是对用户的数据进行增加、删除、修改等操作。用户数据结构体如下:
struct user {
char user_id[USER_ID_SIZE];
char passwd[USER_PASSWD_SIZE];
char name[NAME_SIZE];
char citizen_id[CITIZEN_ID_SIZE];
char phone_num[PHONE_NUM_SIZE];
struct list_head list;
};
分别为用户的ID、密码、姓名、身份证号以及手机号,最后一个list负责将每个结构体变量串联进一个链表中。用户数据相关应用函数如下:
用户信息链表初始化函数:
int user_init(struct list_head *user_head) ;
用户信息链表销毁函数:
int user_destroy(struct list_head *user_head) ;
用户注册登录:
int user_login(struct data_packet data, int clientfd, struct interaction *interaction) ;
用户注册函数:
int user_register(struct data_packet data, int clientfd, struct interaction *interaction) ;
用户信息修改函数:
void change_user(struct data_packet data, int clientfd, struct list_head *user_head) ;
用户信息删除函数:
void delete_user(struct data_packet data, int clientfd, struct list_head *user_head) ;
用户信息添加函数:
void add_user(struct data_packet data, int clientfd, struct list_head *user_head) ;
将用户信息保存到本地文件函数:
int write_user_file(struct list_head *user_head) ;
将用户信息从本地读取函数:
int read_user_file(struct list_head *user_head) ;
在处理车票数据部分,主要是对车票的数据进行管理,分为用户已经购买的车票数据和还未购买的车票数据,数据结构如下:
struct ticket {
char origin_place[ORIGIN_PLACE_SIZE];
char destination[DESTINATION_SIZE];
char train_number[TRAIN_NUM_SIZE];
char departure_time[DEPARTURE_TIME_SIZE];
char arrival_time[ARRIVAL_TIME_SIZE];
char type1_price[PRICE_SIZE];
char type2_price[PRICE_SIZE];
char type3_price[PRICE_SIZE];
char type4_price[PRICE_SIZE];
char date[TO_DATE_SIZE];
char ticket_type1_num[TICKET_NUM_SIZE];
char ticket_type2_num[TICKET_NUM_SIZE];
char ticket_type3_num[TICKET_NUM_SIZE];
char ticket_type4_num[TICKET_NUM_SIZE];
struct list_head list;
};
struct user_ticket {
char id[USER_TICKET_ID_SIZE];
char name[NAME_SIZE];
char citizen_id[CITIZEN_ID_SIZE];
char phone_num[PHONE_NUM_SIZE];
char origin_place[ORIGIN_PLACE_SIZE];
char destination[DESTINATION_SIZE];
char train_number[TRAIN_NUM_SIZE];
char departure_time[DEPARTURE_TIME_SIZE];
char arrival_time[ARRIVAL_TIME_SIZE];
char price[PRICE_SIZE];
char date[TO_DATE_SIZE];
char ticket_type[TICKET_TYPE_SIZE];
struct list_head list;
};
部分相关应用函数如下:
车票链表初始化:
int ticket_init(struct list_head *ticket_head, struct list_head *user_ticket_head) ;
更新车票显示:
int flash_ticket(int clientfd, struct interaction *interaction) ;
获取12306的车票:
int get_ticket_from_py(struct data_packet data, int clientfd, char *ticket_buffer) ;
用户购买车票:
int buy_ticket(struct data_packet data, int clientfd, struct list_head *user_ticket_head,
struct list_head *user_head) ;
用户查询自己的订单信息:
int find_order(struct data_packet data, int clientfd, struct list_head *user_ticket_head,
struct list_head *temporary_head) ;
用户退票:
int return_ticket(struct data_packet data, int clientfd, struct list_head *user_ticket_head) ;
查看所有车票信息:
int build_all_ticket_buffer(char *ticket_buffer, struct list_head *ticket_head) ;
查看所有用户信息:
int build_all_user_buffer(char *user_buffer, struct list_head *user_head) ;
管理员添加车票:
void add_ticket(struct data_packet data, int clientfd, struct list_head *ticket_head)
管理员删除车票:
void delete_ticket(struct data_packet data, int clientfd, struct list_head *ticket_head,
struct list_head *user_ticket_head)
管理员删除用户:
void delete_user(struct data_packet data, int clientfd, struct list_head *user_head)
管理员修改车票:
void change_ticket(struct data_packet data, int clientfd, struct list_head *ticket_head,
struct list_head *user_ticket_head)
管理员修改用户:
void change_user(struct data_packet data, int clientfd, struct list_head *user_head)
用户查找本地已保存的车票:
int user_find_ticket(struct data_packet data, int clientfd, struct list_head *ticket_head,
char *ticket_buffer)
需要注意的是,并非一个函数对应一个功能,有些功能是通过几个函数集成在一起实现的,比如说用户改签车票的功能,就是通过搜索本地已保存车票的函数加退票函数加买票函数来实现的。
4.2. 安卓客户端设计
安卓客户端程序逻辑如图4所示,安卓客户端主要由多个功能界面的活动以及一个发送心跳包的服务组成。在安卓客户端界面设计上,选择深绿色和浅蓝色为APP的主题色,字体大多采用白色、黑色、红色等相对显眼的颜色,控件的布局方式采用相对布局和线性布局相结合,进行说明:
1) 图5为用户登录、注册的界面,左边为用户登录界面,采用线性布局的方式,将最上方的图片到最下面的管理员登录Button和账户注册Button依次排列。其中穿插了部分相对布局的方式,例如:输入ID的EditText和左边的小人物的图片其实是采用了相对布局。右边为用户注册界面,仅采用相对布局的方式,以第一个输入账号的EditText为基准,将其他EditText和Button放在其下方。
2) 图6为查询车票界面和查询结果,左边的查询车票界面采用线性布局和相对布局,以线性布局为主体,将每个部分从上到下依次排列,上方的图片,往下接着的是出发城市和抵达城市等等。每个部分中是采用相对布局的方式。右边的查询结果页面只有两个控件,采用相对布局,一上一下进行分布。下方的ListView是用来添加不同车票信息的,一种车票占据一个item。
3) 提交订单界面采用相对布局,整体分为三个部分:上方的图片及其中间的控件、中间的选取车票类型部分、下方的提交订单Button。再分别采用相对布局,将每个部分的控件进行布局。
![](//html.hanspub.org/file/6-1542270x11_hanspub.png?20211015093738037)
Figure 5. Display of login and registration
图5. 用户登录、注册界面
![](//html.hanspub.org/file/6-1542270x12_hanspub.png?20211015093738037)
Figure 6. Display of searching tickets and the results
图6. 用户查询车票界面、查询结果界面
4) 图7为用户查看订单界面和改签退票界面,左边的用户查看页面与上面的查询结果页面的布局和控件是一样的,只是ListView中的item不一样。右边的改签退票界面是采用相对布局的方式,以上方的图片为基准,将改签和退票的Button放下其下方。图片中的控件也是采用相对布局的方式,以正中间的车次为基准进行布局。
5) 改签选取车票界面,该界面是在用户查看自己的订单界面的基础上多加了一行TextView。每个item与查询结果的item相同。
6) 管理员登录界面,该界面和用户登录界面是类似的。
7) 图8为管理车票信息和用户信息界面各自的fragment管理车票信息的fragment是采用相对布局的方式,是以整体父布局作为基准进行布局的。上方有刷新、载入、添加三个ImageButton。中间是输入车次的EditText和搜索的Button。下方是展示车票的ListView。管理车票信息fragment中使用的item和查询结果的item相同。管理用户信息fragment和管理车票信息fragment类似,只是去掉了载入的ImageButton,以及下方的ListView中的item不一样。
8) 图9为管理员搜索12306车票以及将车票载入本地界面,管理员搜索12306车票的界面和查询车票界面类似,只是删除了右下角的Button。在运行过程中上下两个界面之间还有个将搜索的车票展示出来的界面,是和查询结果页面一样。车票载入本地界面和改签退票界面类似,更换了一个Button中的文字,和将箭头下方的控件更换为由线性布局组成的车票座位类型信息的控件。
9) 图10为管理员添加车票和用户界面,管理员添加车票界面采用相对布局,以最上方输入车次的EditText为基准向下进行布局,同时最下方加入TextView进行提示。管理员添加用户界面采用相对布局,以最上方输入ID的EditText为基准向下进行布局。
![](//html.hanspub.org/file/6-1542270x13_hanspub.png?20211015093738037)
Figure 7. Display of order list and change/refund tickets
图7. 用户查看自己的订单、改签退票界面
![](//html.hanspub.org/file/6-1542270x14_hanspub.png?20211015093738037)
Figure 8. Fragments of administrators and users
图8. 管理车票信息和用户信息界面各自的fragment
![](//html.hanspub.org/file/6-1542270x15_hanspub.png?20211015093738037)
Figure 9. Display of searching and loading tickets’ information from 12306 by administrators
图9. 管理员搜索12306车票以及将车票载入本地界面
![](//html.hanspub.org/file/6-1542270x16_hanspub.png?20211015093738037)
Figure 10. Display of adding tickets and users by administrators
图10. 管理员添加车票和用户界面
4.3. 安卓客户端与Linux服务端之间的通信协议
图11为安卓客户端与Linux服务端之间的通信协议数据格式,由数据头、数据长度、命令类型、命令吗、返回码、数据域和校验位组成。
数据头让程序知道数据的起始位置。数据长度让程序读取数据在什么位置截止,可以用数据尾来代替数据长度。命令类型是用来区分不同功能所需要的请求码。参数是不同命令需要携带的数据,例如若命令是登录的话,参数携带的就是账号和密码等信息。校验位是判断整段数据有没在传输过程中发生丢失或改变。
![](//html.hanspub.org/file/6-1542270x17_hanspub.png?20211015093738037)
Figure 11. Data format between server and user in communication protocol
图11. 安卓客户端与Linux服务端之间的通信协议数据格式
5. 系统测试及运行结果
本设计的操作显示画面很多,因此仅截取部分页面展示说明,对于本设计的系统测试及运行结果分别如下所述:
1) 服务端初始化测试:
在终端上输入./train_system运行Linux服务端后,点击运行,Linux服务端初始化成功,其画面显示如图12所示。
2) 客户端连接测试:
将手机联网,启动Android客户端,成功接入Linux服务端,Android客户端开始发送心跳包,打开APP的画面显示如图13所示。
3) 用户登录测试:
在安卓APP上输入已注册的账号和密码后,点击登录。服务器收到请求登录的数据。Linux服务端匹配账号和密码,正确后发送登录成功的数据给安卓客户端,安卓客户端进入搜索车票页面。
4) 用户搜索车票测试:
在搜索车票页面填写出发城市、抵达城市,选择出发时间后,点击搜索,Linux服务端接收到查看车票信息,输入车辆信息的手机画面显示如图14所示。搜索之后,Linux服务端将与搜索信息相对应的车票数据发送给安卓客户端,手机画面显示如图15所示。
![](//html.hanspub.org/file/6-1542270x18_hanspub.png?20211015093738037)
Figure 12. Display of initializing successfully in server
图12. Linux服务端初始化成功示意图
![](//html.hanspub.org/file/6-1542270x19_hanspub.png?20211015093738037)
Figure 13. Display of executing APP on the mobile phone
图13. 打开APP的手机画面显示图
![](//html.hanspub.org/file/6-1542270x20_hanspub.png?20211015093738037)
Figure 14. Display of searching trains’ information on the mobile phone
图14. 输入车辆信息的手机画面显示图
![](//html.hanspub.org/file/6-1542270x21_hanspub.png?20211015093738037)
Figure 15. Display of tickets’ information on the mobile phone
图15. 用户请求搜索车票,成功后显示车票信息的手机画面显示图
5) 用户购买车票测试:
搜索到的任意一张车票,进入提交订单的界面,选择想要购买的座位类型,点击提交订单后,Linux服务端接收到请求购买车票的数据。
6) 用户退票测试:
进入改签和退票的界面,点击退票按钮,可以在订单页面看到自己的车票已经退掉了。
7) 用户改签测试:
在自己的订单界面选择想要改签的车票,点击提交订单后,改签成功。
8) 添加用户功能测试:
输入用户的信息,请求添加用户。
9) 添加车票:
输入车票的信息,请求添加用户界面。
10) 从12306载入车票信息功能:
填写出发城市、抵达城市、出发时间,点击搜索。选择想要载入的车票,点击载入本地,手机画面显示如图16所示。载入成功,手机画面显示如图17所示。
11) 删除车票功能:
在管理用户信息界面选择想要删除的车票,点击删除车票。
12) 删除用户:
选择想要删除的用户,点击删除,选择想要删除的用户信息,则手机画面显示如图18所示。
13) 修改车票信息:
选择想要修改的车票,填写想要修改的信息,选择想要修改的车票,成功后如图19所示。
14) 修改用户信息:
选择想要修改的用户,填写想要修改的信息。
![](//html.hanspub.org/file/6-1542270x22_hanspub.png?20211015093738037)
Figure 16. Displays of loading tickets’ information from 12306 on the mobile phone
图16. 从12306载入车票信息功能的三张手机画面显示图
![](//html.hanspub.org/file/6-1542270x23_hanspub.png?20211015093738037)
Figure 17. Display of loading tickets’ information successfully from 12306 on the mobile phone
图17. 从12306载入车票信息成功的手机画面显示图
![](//html.hanspub.org/file/6-1542270x24_hanspub.png?20211015093738037)
Figure 18. Display of choices in eliminating users’ information on the mobile phone
图18. 选择想要删除的用户信息的手机画面显示图
![](//html.hanspub.org/file/6-1542270x25_hanspub.png?20211015093738037)
Figure 19. Display of choices in changing tickets on the mobile phone
图19. 选择想要修改的车票的手机画面显示图
本设计完成的火车售票系统测试正常、运行正确,具实用性;需要说明的是,由于法规与金流的限制,从购买到付款的阶段,目前尚无法实现,但已具备潜力,满足法规要求便能成为一个真正可用的火车售票系统了。
6. 总结与展望
本设计基于Linux和安卓系统实现了一个简单的火车售票系统,于Linux开发和安卓开发,python爬虫的知识,同时结合Linux开发和安卓开发的现状,对火车售票系统进行了可用性分析,制定了整体的设计方案。对火车售票系统进行代码的编写,并对整体的功能进行测试,保证其稳定性和可用性。
通过这次对火车售票系统的开发,未来有以下几点可以再做精进与改善:
1) 在上服务端,不能灵活管理处理数据的线程。主要体现在,需要事先开好几个线程,有数据需要处理时,就用上其中的线程,这样就导致有些空闲的线程在占用资源。
2) 不能灵活申请部分内存,主要体现在,对需要传输到APP的数据,没法根据数据的大小进行申请内存,只能初始设置一个较大的内存,然后进行使用。