一、整体工作流程
本代码实现的内网聊天系统采用经典的客户端 - 服务器(C/S)架构模型。系统运行过程中,服务器端与客户端分别遵循以下工作流程:
服务器端的初始化阶段,依次执行 socket 创建、端口绑定及监听状态设置等操作。随后进入循环监听模式,默认设置 5 秒超时机制等待客户端连接请求。当客户端成功连接后,服务器与客户端进入双向通信状态,双方可通过标准输入输出进行消息交互。客户端断开连接后,服务器将关闭当前连接通道,重新进入待连接状态,以处理后续客户端请求。
客户端在初始化时,通过创建 socket 实例并连接至服务器指定端口建立通信链路。连接成功后,客户端启动消息交互循环,实现用户输入消息的发送与服务器响应消息的接收功能。当满足预设退出条件时,客户端将主动断开与服务器的连接,并安全退出程序。
系统基于 TCP 协议通过 socket 接口建立连接,运用 select 函数实现 I/O 多路复用机制,同时监听标准输入及网络数据接收事件,确保通信过程的高效性与可靠性。各功能模块协同配合,共同完成消息收发、时间戳记录及会话管理等核心功能。
二、关键函数作用
2.1 printf_time()
该函数基于time.h
库实现系统时间获取与格式化输出功能。其核心逻辑为:通过time()函数获取当前系统时间戳,利用localtime()
函数将时间戳转换为本地时间结构体,按照 "[北京时间:% H:% M:% S]" 格式进行格式化输出。在消息接收场景下,该函数用于添加时间戳标识,为通信记录提供精确的时间维度信息,增强消息序列的可追溯性与可读性。
1 2 3 4 5 6
| //输出时间戳 void printf_time(){ time_t time_now=time(NULL); struct tm *now=localtime(&time_now); printf("[北京时间:%d:%d:%d]\t",now->tm_hour,now->tm_min,now->tm_sec); }
|
2.2 handle()
此函数作为 SIGINT 信号的注册处理函数,主要用于捕获用户通过 Ctrl+C 发起的中断请求。当接收到 SIGINT 信号时,函数将创建 "exit.txt" 文件并写入 "exit\n" 标识,该标识作为客户端退出的状态标记,为后续退出逻辑提供判断依据。
1 2 3 4 5 6 7 8
| //将^C信号转为下列函数 void handle(int sig){ int fd=open("exit.txt",O_RDWR|O_CREAT|O_TRUNC,0666); write(fd,"exit\n",5); printf("已退出\n"); close(fd); return; }
|
2.3 judge()
该函数承担客户端退出状态检测职责。通过尝试读取 "exit.txt" 文件内容,判断客户端是否触发退出操作。若文件读取成功且内容匹配 "exit\n",则向服务器发送退出指令,依次关闭文件描述符、socket 连接并终止程序运行。此函数在客户端消息输入与接收流程中均被调用,确保退出逻辑的实时性与有效性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| //判断是否退出 void judge(const int fd){ char buf[1024]={0}; int fp=open("exit.txt",O_RDWR); if(fp==-1){ return; }else{ read(fp,buf,5); if(strcmp(buf,"exit\n")==0){ send(fd,"exit\n",5,0); close(fp); close(fd); exit(1); } close(fp); } }
|
2.4 客户端的 stdin_set()
该函数负责处理客户端标准输入消息。执行流程包括:首先调用judge()函数检测退出状态;清空输入缓冲区后读取用户输入,将消息发送至服务器;若检测到 "exit\n" 输入,则执行程序退出操作;最后进行错误检查并清空已处理的输入缓冲区,确保输入处理流程的完整性与安全性。
1 2 3 4 5 6 7 8 9 10 11 12 13
| //输入信息 void stdin_set(int fd,char *buf){ judge(fd); bzero(buf,1024); int ret=read(STDIN_FILENO,buf,sizeof(buf)); send(fd,buf,ret,0); if(strcmp(buf,"exit\n")==0){ printf("退出\n"); exit(1); } ERROR_CHECK(ret,0,"你不中啊"); bzero(buf,ret); }
|
2.5 客户端的 recv_set()
该函数实现客户端接收服务器消息的功能。执行过程包括:调用judge()函数检测退出状态;接收服务器数据并进行错误处理;数据接收成功后,调用printf_time()
添加时间戳并输出消息内容;最后清空接收缓冲区,为后续消息接收做好准备。
1 2 3 4 5 6 7 8
| void recv_set(int fd,char *buf){ judge(fd); ssize_t ret_r=recv(fd,buf,1024,0); ERROR_CHECK(ret_r,-1,"recv fail"); printf_time(); printf("接到服务端信息:%s",buf); bzero(buf,ret_r); }
|
2.6 服务器端的 recv_set()
该函数用于处理服务器接收客户端消息的逻辑。消息接收后首先进行错误检查,若接收失败则返回错误标识;成功接收后添加时间戳,若检测到 "exit\n" 消息或接收长度为 0(客户端断开连接),则返回特定标识触发会话退出流程;否则输出接收到的消息内容并清空缓冲区,维持当前会话状态。
1 2 3 4 5 6 7 8 9 10 11 12 13
| //接收信息 int recv_set(int fd,char *buf){ ssize_t ret_r=recv(fd,buf,1024,0); ERROR_CHECK(ret_r,-1,"recv fail"); printf_time(); if(strcmp(buf,"exit\n")==0||ret_r==0){ printf("客户已经退出,等待新客户(5s内没有新接入则退出)\n"); return 1; } printf("收到客户信息:%s",buf); bzero(buf,ret_r); return 0; }
|
2.7 服务器端的 stdin_set()
该函数负责处理服务器端标准输入消息。通过读取用户输入并发送至客户端,完成消息转发功能。执行过程包含错误检查与缓冲区清理操作,确保消息发送的可靠性与输入处理的独立性。
1 2 3 4 5 6 7
| //发送信息 void stdin_set(int fd,char *buf){ int ret=read(STDIN_FILENO,buf,sizeof(buf)); ERROR_CHECK(ret,-1,"recv fail"); send(fd,buf,ret,0); bzero(buf,ret); }
|
三、socket 相关函数解析
socket()
:创建网络套接字,原型int socket(int domain, int type, int protocol)
。AF_INET
指定 IPv4,SOCK_STREAM
基于 TCP,0
采用默认协议。客户端用其获取的描述符建立连接,服务器用于监听。
connect()
:客户端专用,原型int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
,用于建立 TCP 连接,成功返 0,失败返 - 1。
bind()
:服务器将套接字与 IP、端口绑定,原型同connect()
,绑定后可监听客户端请求。
listen()
:使服务器套接字监听,原型int listen(int sockfd, int backlog)
,backlog
设连接队列最大长度。
accept()
:服务器接受连接,原型int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
,成功返回新描述符通信。
send()
:在连接套接字上发数据,原型ssize_t send(int sockfd, const void *buf, size_t len, int flags)
,flags
为 0 用默认方式,返回发送字节数。
recv()
:接收数据,原型类似send()
,数据存buf
,返回接收字节数,0 表示对方关闭,-1 表示失败。
close()
:关闭套接字并释放资源,用于客户端退出或连接终止。
四、select 函数的应用
select 函数在本系统中实现 I/O 多路复用功能,通过同时监听多个文件描述符的可读状态,实现高效的事件驱动处理机制。
在客户端主循环中,首先使用FD_ZERO()函数清空文件描述符集合,随后通过FD_SET()函数将客户端套接字描述符及标准输入描述符添加至待监听集合。调用select(10, &set, NULL, NULL, NULL)进行事件监听,其中第一个参数为待检查的最大文件描述符值加 1,第二参数指定监听的可读事件集合,后三个参数设置为 NULL 表示仅监听可读事件且不设置超时。
select 函数将阻塞当前线程,直至检测到可读事件发生。事件触发后,通过FD_ISSET()函数分别检查标准输入与套接字描述符的状态:若标准输入可读,则调用stdin_set()处理用户输入;若套接字可读,则调用recv_set()
处理服务器响应消息。
服务器端的 select 应用逻辑与客户端类似,在与客户端的会话循环中,同时监听与客户端通信的套接字描述符及标准输入描述符。当对应描述符可读时,分别执行消息接收与发送操作。这种设计模式有效避免了单一操作的阻塞问题,显著提升了系统的并发处理能力与运行效率。
五、信号处理与退出机制
5.1 信号处理
客户端通过signal(SIGINT, handle)
函数注册 SIGINT 信号处理函数,实现对用户中断请求的响应。当用户按下 Ctrl+C 触发 SIGINT 信号时,handle()函数将被调用,创建 "exit.txt" 文件并写入退出标识,为后续退出流程提供状态标记。
5.2 退出机制
输入 "exit" 退出:客户端stdin_set()
函数检测到用户输入 "exit\n" 时,立即执行程序退出操作。服务器端recv_set()
函数接收到 "exit\n" 消息后,将终止当前会话循环,关闭连接并重新进入监听状态。
信号处理退出:客户端在消息输入与接收流程中调用judge()函数,通过检查 "exit.txt" 文件内容判断是否触发退出请求。若检测到退出标识,则向服务器发送退出消息,依次关闭文件与套接字资源后退出程序。服务器端接收到退出消息后,按既定流程处理连接关闭操作。
服务器超时退出:服务器在等待客户端连接阶段,通过alarm(5)设置 5 秒超时机制。若超时时间内未收到连接请求,将触发 SIGALRM 信号,由于未注册该信号处理函数,系统将按默认行为终止进程,实现无连接情况下的自动退出。若在超时前接收到连接请求,则调用alarm(0)取消超时设置,继续进行会话处理。
六、完整代码
客户端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| #include <my_header.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/select.h> #include <signal.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> /* Usage:从内网进行聊天沟通 * 客户端使用函数SOCKET CONNECT SEND RECV CLOSE */
//输出时间戳 void printf_time(){ time_t time_now=time(NULL); struct tm *now=localtime(&time_now); printf("[北京时间:%d:%d:%d]\t",now->tm_hour,now->tm_min,now->tm_sec); }
//将^C信号转为下列函数 void handle(int sig){ int fd=open("exit.txt",O_RDWR|O_CREAT|O_TRUNC,0666); write(fd,"exit\n",5); printf("已退出\n"); close(fd); return; }
//判断是否退出 void judge(const int fd){ char buf[1024]={0}; int fp=open("exit.txt",O_RDWR); if(fp==-1){ return; }else{ read(fp,buf,5); if(strcmp(buf,"exit\n")==0){ send(fd,"exit\n",5,0); close(fp); close(fd); exit(1); } close(fp); } }
//输入信息 void stdin_set(int fd,char *buf){ judge(fd); bzero(buf,1024); int ret=read(STDIN_FILENO,buf,sizeof(buf)); send(fd,buf,ret,0); if(strcmp(buf,"exit\n")==0){ printf("退出\n"); exit(1); } ERROR_CHECK(ret,0,"你不中啊"); bzero(buf,ret); }
//读取服务端信息 void recv_set(int fd,char *buf){ judge(fd); ssize_t ret_r=recv(fd,buf,1024,0); ERROR_CHECK(ret_r,-1,"recv fail"); printf_time(); printf("接到服务端信息:%s",buf); bzero(buf,ret_r); } int main(int argc, char *argv[]){ //检查端口号 ARGS_CHECK(argc,2); //设置客户端socket int fd= socket(AF_INET,SOCK_STREAM,0); ERROR_CHECK(fd,-1,"error socket"); //获取网络地址 struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[1])); addr.sin_addr.s_addr=inet_addr("127.0.0.1"); //connect 链接对面 int ret=connect(fd,(struct sockaddr*)&addr,sizeof(addr)); ERROR_CHECK(ret,-1,"error connect"); printf("输入消息并按回车发送,输入'exit'退出\n"); char buf[1024]={0}; //设置监听 fd_set set; FD_ZERO(&set);
//移除exit.txt判定文件 remove("exit.txt");
signal(SIGINT,handle); while(1){ FD_SET(fd,&set); FD_SET(STDIN_FILENO,&set); select(10,&set,NULL,NULL,NULL); if(FD_ISSET(STDIN_FILENO,&set)){ stdin_set(fd,buf); } if(FD_ISSET(fd,&set)){ recv_set(fd,buf); } } close(fd); return 0; }
|
服务端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| #include <my_header.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <signal.h> /* Usage:服务端 输入socket bind listen accept recv send close */ //打印时间戳 void printf_time(){ time_t time_now=time(NULL); struct tm *now=localtime(&time_now); printf("[北京时间:%d:%d:%d]\t",now->tm_hour,now->tm_min,now->tm_sec); } //接收信息 int recv_set(int fd,char *buf){ ssize_t ret_r=recv(fd,buf,1024,0); ERROR_CHECK(ret_r,-1,"recv fail"); printf_time(); if(strcmp(buf,"exit\n")==0||ret_r==0){ printf("客户已经退出,等待新客户(5s内没有新接入则退出)\n"); return 1; } printf("收到客户信息:%s",buf); bzero(buf,ret_r); return 0; } //发送信息 void stdin_set(int fd,char *buf){ int ret=read(STDIN_FILENO,buf,sizeof(buf)); ERROR_CHECK(ret,-1,"recv fail"); send(fd,buf,ret,0); bzero(buf,ret); } int main(int argc, char *argv[]){ ARGS_CHECK(argc,2); //看端口是否正确
int fd=socket(AF_INET,SOCK_STREAM,0); //生成文件描述符 struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[1])); addr.sin_addr.s_addr=inet_addr("127.0.0.1"); //描述网络主机地址 bind(fd,(struct sockaddr*)&addr,sizeof(addr)); //固定服务端端口号 listen(fd,10); //监听 数值5~10 while(1){ //设置信号 struct sigaction sa; sigaction(SIGALRM,&sa,NULL); alarm(5); //设置新的socket接收对面的socket struct sockaddr_in client_a; socklen_t clientlen=sizeof(client_a); int new_fd=accept(fd,(struct sockaddr*)&client_a,&clientlen); ERROR_CHECK(new_fd,-1,"error accept"); printf("接收到客户信号,正在等待输入\n"); //看全连接队列,没有则等待,有则拿出一条连接 alarm(0); //如果连接到新的客户端,就不自动退出 char buf[1024]={0}; //设置select结构体,对具体情况进行监听 fd_set set; FD_ZERO(&set); while(1){ FD_SET(new_fd,&set); FD_SET(STDIN_FILENO,&set); select(10,&set,NULL,NULL,NULL); if(FD_ISSET(new_fd,&set)){ if(recv_set(new_fd,buf)){ break; } } if(FD_ISSET(STDIN_FILENO,&set)){ stdin_set(new_fd,buf); } } close(new_fd); } close(fd); return 0; }
|