实验内容
-
实验内容:代码阅读与分析
alg.11-0-BBS-8.h
alg.11-3.socket-input-pad-8-1.c
alg.11-4-socket-connector-BBS-8-1.c
alg.11-5-socket-server-BBS-nickname-8-1.c
-
分析并画出系统的总体功能结构图、程序流程图和数据流图
-
描述代码实现的进程并发行为
-
描述代码实现的IPC行为
-
实验环境
-
架构:Intel x86_64 (虚拟机)
-
操作系统: Ubuntu 18.04
-
汇编器: gas (GNU Assembler) in AT&T mode
-
编译器: gcc
技术日志
代码阅读与分析
-
分析代码alg.11-0-BBS-8.h
MAX_QUE_CONN_NM:建立的队列的长度
MAX_CONN_NUM:最大连接进程数
CODE_LEN:4位输入发送台编码
ERR_EXIT(m):错误处理并exit()
ERR_RETURN(m):错误处理并return
SN_SIZE:连接的客户数
UID_SIZE:uid数
NICKNAME_LEN:昵称长度
SEND_SIZE:传送的信息长度
sn_attri:用户结构体,封装了uid,上一个状态,当前状态,昵称,ip地址和端口号
send_buf:发送缓冲区,封装了用于发送信息的用户sn,uid,昵称和信息msg_buf
stat_buf:状态结构体,封装了用户编号sn,uid,状态stat,ip地址和端口号
-
分析代码alg.11-3.socket-input-pad-8-1.c
函数分析:
-
char* s_gets(char* stdin_buf, int n):从标准输入stdin中读取一个长度为n-1的字符串,存储在stdin_buf中,并清空输入缓冲区
-
void heart_beating(int sec):不断从命名管道pipe_hbr读取信息以监听写入端是否缺失
-
int init_FIFO(int a_m):a_m为1时自定义连接的命名管道编号,否则函数自动连接并打开一个广播管道,从广播管道中获取可连接的命名管道编号。函数根据用户自定义或程序自动换取的命名管道编号连接一个用于通信的命名管道和一个用于监听的命名管道
-
void help_print(void):打印客户端和控制台的帮助信息
代码分析:
程序首先打印"Enter pad mode (1 - manual, other - auto)\n"提示用户选择连接命名管道的方式,1为自定义连接,其他为自动连接,然后调用s_gets()读取用户输入,并根据输入调用init_FIFO创建命名管道。
接着创建一个子进程,子进程调用heart_beating(1)监听写入端是否缺失,当写入端缺失时,从heart_beating(1)函数返回,接着子进程关闭打开的命名管道,然后调用kill()杀死父进程。
与此同时,父进程处于一个循环,循环中打印"Enter some text (--help): \n"提示用户输入信息,并将信息读取到msg_buf中,若用户输入“--help”,则调用help_print()打印帮助信息,否则调用write(pipe_padw, msg_buf, MSG_SIZE)将用户输入的信息写入打开的命名管道。若写入失败,则打印"\tConnector terminated ...\n"并退出循环;若用户输入为#9,则调用kill(htbtpid, SIGKILL)杀死子进程并退出循环,退出循环后调用close()关闭打开的管道,并正常退出。
分析代码alg.11-4-socket-connector-BBS-8-1.c
函数分析:
-
char* s_gets(char* stdin_buf, int n):从标准输入stdin中读取一个长度为n-1的字符串,存储在stdin_buf中,并清空输入缓冲区
-
int random_code(void):产生一个4位的随机码,用于自动创建命名管道
-
void heart_beating(int sec):不断向命名管道pipe_hbw写入信息然后休眠sec秒,表示connector存在
-
int init_input_pad(void):自动创建并打开一个用于通信的命名管道pipe_padr、一个用于监听的命名管道pipe_hbw和一个用于广播发送台号码的命名管道pipe_brdc,并将用于通信的命名管道号码写入pipe_brdc进行广播
-
int init_socket(void):根据用户输入的ip地址和端口号创建一个连接到服务器的套接字,并打印服务器主机的名称和地址、创建的套接字的地址和端口号
代码分析:
程序首先调用init_socket()根据用户输入的ip地址和端口号创建一个连接到服务器的套接字,如果返回值ret为EXIT_FAILURE,说明连接失败,调用exit(EXIT_FAILURE)退出程序,否则继续向下,调用init_input_pad()自动创建并打开一个用于通信的命名管道pipe_padr、一个用于监听的命名管道pipe_hbw和一个用于广播通信管道号码的命名管道pipe_brdc,并广播通信管道号码。
接着创建一个子进程,若创建失败,则关闭与发送台连接的命名管道和与服务器连接的套接字,删除init_input_pad()中创建的并用于通信的命名管道和用于监听的命名管道。否则成功创建子进程,子进程调用heart_beating(5)每5秒向用于监听的命名管道写入,表示connector存在。
于此同时,父进程创建另一个子进程,若创建失败,则关闭命名管道和套接字,删除命名管道同时杀死用于heart_beating的子进程,并调用ERR_EXIT("fork()")表示创建子进程异常退出,否则成功创建子进程,子进程从套接字中接收信息并打印昵称和接收的信息到终端。其中父进程用于向套接字发送信息,子进程用于从套接字读取信息。
父进程进入一个循环,采用阻塞方式从发送台命名管道中读取信息到msg_buf中,再采用阻塞方式向套接字发送msg_buf中的信息。若信息为“#9”,则退出循环,杀死两个子进程并正常退出。新创建的子进程进入一个循环,采用阻塞方式从套接字中读取并打印信息,当接收到的信息为“#9”时,打印"I am terminated by Console!\n"表示控制台终止该进程,退出循环并关闭打开的命名管道和套接字,删除相关的命名管道,并杀死另一个子进程和父进程并正常退出。
分析代码alg.11-5-socket-server-BBS-nickname-8-1.c
函数分析:
-
void init_stat_des(void):初始化状态数组
-
void sleep_ms(long int timegap_ms):休眠相应的毫秒数
-
char* s_gets(char* stdin_buf, int n):从标准输入stdin中读取一个长度为n-1的字符串,存储在stdin_buf中,并清空输入缓冲区
-
int random_code(void):产生一个CODE_LEN-1位的随机码,用于自动创建命名管道
-
void heart_beating(int sec):每隔sec秒向命名管道pipe_hbw写入信息,表示服务器存在
-
int getipv4addr(char *ip_addr):保存服务器的ip地址到ip_addr中
-
int init_input_pad(void):自动创建并打开一个用于通信的命名管道pipe_padr(设置为非阻塞读写方式)、一个用于监听的命名管道pipe_hbw和一个用于广播发送台号码的命名管道pipe_brdc,并将用于通信的命名管道号码写入pipe_brdc进行广播
-
int init_pipe(void):创建若干匿名管道并设置读写方式
-
int init_socket(void):创建一个套接字并打印服务器套接字的名称和地址,然后绑定到创建的套接字,打印服务器套接字的端口号并开始监听
-
void cli_center(void):根据main()传送来的指令或者从pipe_updatesn[0]读取状态信息并更新相应客户sn的状态,更新现有客户的状态并传送未使用的用户信息到main();或者从date_center()读取客户信息并更新。
-
void data_center(void):从main()读取信息用于添加新客户,从socket_trans()读取客户从服务器读到的信息并保存在msg_buf中,根据msg_buf执行相应操作,从输入发送台读取信息用于服务器控制,写入信息到cli_center()用于删除客户
-
void socket_trans(int sn):通过socketfd_cli[sn]从服务器读取信息并向data_center()写入读取到的信息,通过pipe_d2s[sn][0]读取来自data_center()的信息并向socketfd_cli[sn]写入读取到的信息,传到服务器中
代码分析:
程序定义了六种状态:NULL,ACCEPTED,ACTIVE,BANNED,UNDISTURBED和RESERVED,创建普通管道pipe_newsn用于将信息从main()传送到data_center(),创建普通管道pipe_req_nullsn用于将信息从main()传送到cli_center(),创建普通管搭pipe_resp_nullsn用于将信息从cli_center() 传送到main(),创建普通管道pipe_updatesn用于将信息从data_center()传送到cli_center(),创建普通管道pipe_s2d用于将信息从socket_trans()传送到data_center(),创建普通管道pipe_d2s用于将信息从data_center()传送到socket_trans(),创建命名管道pipe_padr用于将信息从server input-pad传送到data_center()。
首先调用init_stat_des()初始化状态数组,接着调用init_input_pad()自动创建并打开一个用于通信的命名管道(设置为非阻塞读写方式)、一个用于监听的命名管道和一个用于广播发送台号码的命名管道pipe_brdc,并将用于通信的命名管道号码写入pipe_brdc进行广播,若函数返回值为EXIT_FAILURE,说明执行失败,则退出程序。否则继续向下,调用init_pipe()创建各个匿名管道,若函数返回值为EXIT_FAILURE,说明执行失败,则退出程序。否则继续向下,调用init_socket()创建与绑定服务器套接字并开始监听,若函数返回值为EXIT_FAILURE,说明执行失败,则退出程序。否则继续向下,通过一个for循环初始化客户sn_attri数组。
接着创建一个子进程,若创建失败,则关闭创建的套接字并调用ERR_EXIT("\tfork()")异常退出。否则子进程调用data_center()作为信息管理进程,从main()读取信息用于添加新客户,从socket_trans()读取客户从服务器读到的信息并保存在msg_buf中,根据msg_buf执行相应操作,从输入发送台读取信息用于服务器控制,写入信息到cli_center()用于删除客户。
接着再创建一个子进程,若创建失败,则关闭创建的套接字并调用ERR_EXIT("\tfork()")异常退出。否则创建的子进程调用cli_center()作为用户管理进程,从pipe_updatesn[0]读取状态信息并更新相应客户sn的状态,从pipe_req_nullsn[0]读取信息,更新现有客户的状态并传送状态信息到main()
于此同时,父进程进入一个循环,向pipe_req_nullsn[1]写入信息,如果写入失败则打印错误信息,否则继续向下,以阻塞方式从pipe_resp_nullsn[0]读取信息到stat_buf中,获取相应的用户sn,接受用户的套接字连接请求,若连接失败,而调用perror("\taccept()")输出错误信息。否则继续向下,打印建立连接的用户sn,用户id,用户ip地址和与用户连接的新套接字的端口号。接着,父进程再创建一个子进程,若创建失败,打印fork error, connection discarded和当前用户编号及其uid,并调用输出错误信息perror("\tfork()"),继续循环。若创建成功,则子进程调用socket_trans(curr_sn),从socketfd_cli[sn]读取信息并向data_center()写入读取到的信息,从pipe_d2s [sn][0]读取来自data_center()的信息并向socketfd_cli[sn]写入读取到的信息。同时父进程将当前用户信息写入pipe_newsn[1]中,接着初始化send_buf的相关信息,并将"Console", "Initiate you nickname by command [#2 nickname] ..."写入pipe_d2s[curr_sn][1],若写入失败,则调用perror("\tpipe_d2s write()")打印错误信息。
跳出循环后,父进程调用wait(0)等待所有子进程结束,关闭与客户连接的套接字,接着关闭相应的匿名管道和命名管道,最后正常退出。
分析并画出系统的总体功能结构图、程序流程图和数据流图
总体功能结构图
程序流程图
数据流图
(其中用于获取命名管道号码的input_pad.fifo-brdc未画出)
描述代码实现的进程并发行为
socket-input-pad:
父进程根据终端输入创建命名管道,将用户输入通过命名管道发送到connector,子进程监听连接的connector,父子进程并发执行。
socket-connector:
父进程根据终端输入创建连接到服务器的套接字,通过套接字向服务器发送信息,创建并广播命名管道用于与input-pad连接,通过命名管道接收发送台的信息;一个子进程每5秒向用于监听的命名管道写入,表示connector存在,另一个子进程通过套接字从服务器接收信息并打印到终端,父子进程并发执行。
socket-server:
父进程创建套接字并监听、接收套接字连接请求,创建命名管道与发送台连接;一个子进程作为信息处理中心,另一个子进程作为客户管理中心,每个客户都有单独的子进程调用socket_trans(curr_sn)作为客户与服务器通信中介;进程间都是并发执行的。
同时input-pad、connector、server分别在不同终端运行,相互间也是并发执行的。
描述代码实现的IPC行为
server创建了一个用于通信的命名管道、一个用于监听的命名管道和一个用于广播发送台号码的命名管道pipe_brdc,通过命名管道与一个终端发送台进行通信。
server创建了一个使用IPV4和TCP协议的套接字server_fd,设置服务端的IP地址和端口号后,使用bind()函数将服务器的地址绑定给套接字,接着使用listen()函数监听是否有来自客户端的连接请求,有请求后,使用accept()函数接收客户端的连接请求并建立连接,建立连接后,server与client通过套接字进行通信。
server创建了若干个匿名管道pipe_req_nullsn、pipe_resp_nullsn、pipe_updatesn、pipe_s2d、pipe_d2s,用于在各个进程main()、cli_center()、data_center()、socket_trans()之间进行通信。
connector创建了一个用于通信的命名管道、一个用于监听的命名管道和一个用于广播发送台号码的命名管道pipe_brdc,通过命名管道与一个终端发送台进行通信。
connector与服务器通过套接字连接,通过套接字与服务器进行通信。
执行结果截图
首先打开两个终端,在终端1运行 server,初始化后提示 pad 连接码和本机 ip4 地址、端口号,进入监听状态。在终端2运行 pad,按提示选择自动连接 server 或手动输入 server 提示的4个数字的连接码,连接成功后进入等待输入状态,在 pad 上输入 --help 可以得到命令提示。
可以看到,服务器正确创建套接字和命名管道,并打印相关信息,pad正确与服务器通过命名管道连接。
打开另外两个终端,在终端3运行运行 client(connector),按提示输入 server 的 ip4 地址和端口号,连接成功后 client 的默认名字是 Anonymous,这时处于单接收状态,按提示输入 #2 nickname 修改名字后才能输入其它命令。在终端4运行 pad 建立和 connector 的连接,进入等待输入状态,在 pad 上输入 #2 chen将用户名修改为chen。
可以看到,输入 server 的 ip4 地址和端口号后,connector与server通过套接字建立连接,pad正确与connector通过命名管道连接,用户名正确得到修改。
通过与connector连接的pad向服务器发送信息,可以看到信息正确传送到服务器
创建另一个用户,与服务器连接并改名为xin
在与connector(chen)连接的pad通过@xin hello向用户xin发送信息hello,可以看到,信息正确发送给指定用户
-