0x04基础套接字-TCP时间服务器
TCP时间服务器
1. 流式套接字客户端/服务器编程
拟实现一个基本的流式套接字客户端/服务器通信程序。在该程序中,客户端和服务器将按照如下步骤交互:
- 客户端向服务器发出日期时间请求字符串,如:%D %Y %A %T 等。
- 服务器从网络接收到日期请求字符串后,根据字符串格式生成对应的日期时间值返回给客户端。
服务器端程序
/************************************************************************* > File Name: timeserver.c > Author: > Mail: > Created Time: Tue 10 Apr 2018 09:32:39 AM CST ************************************************************************/ /* * 服务器提供的服务: * 客户端向服务器发出日期时间请求字符串,格式:%D %Y %A %T等 * 服务器从网络接收到日期时间请求字符串后,根据字符串格式生成对应的日期时间值返回给客户端 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXBUF 512 int main(int argc, char **argv) { int listen_fd, conn_fd; /*定义监听套接字和连接套接字*/ struct sockaddr_in serv_addr, client_addr; /*定义服务器地址和客户端地址*/ socklen_t len; int port; time_t t; /*定义time_t类型的变量表示时间*/ struct tm stime; /*定义一个日历时间的结构体*/ char req[MAXBUF+1] = {0}; /*接收请求字符串*/ char send_time[MAXBUF+1] = {0}; /*发送时间字符串*/ int z; if(argc < 2) { printf("Usage: %s portnumber\n", argv[0]); exit(1); } if((port = atoi(argv[1])) < 0) { printf("portnumber: 0~65535\n"); exit(1); } /* * 创建套接字 */ if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket fail"); exit(1); } /* * 设置服务器地址 */ memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(port); /* * 绑定套接字 */ if(bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { perror("bind fail"); exit(1); } /* * 监听 */ if(listen(listen_fd, 128) == -1) { perror("listen fail"); exit(1); } while(1) { len = sizeof(client_addr); /*接收一个客户端的连接并创建连接套接字*/ if((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len)) == -1) { perror("accept fail"); exit(1); } fprintf(stdout, "Server get connection from %s: %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); /*处理客户端请求*/ while(1) { z = read(conn_fd, req, MAXBUF); if(z == -1) { perror("read fail"); exit(1); } if(z == 0) { /*z=0说明客户端关闭连接*/ fprintf(stdout, "client %s has closed the socket\n", inet_ntoa(client_addr.sin_addr)); close(conn_fd); break; } req[z] = '\0'; time(&t); stime = *localtime(&t); /* * 根据请求格式字符串生成相应时间字符串 */ strftime(send_time, MAXBUF, req, &stime); z = write(conn_fd, send_time, strlen(send_time)); if(z == -1) { perror("write fail"); exit(1); } } } close(listen_fd); return 0; }
客户端程序
/************************************************************************* > File Name: timeclient.c > Author: > Mail: > Created Time: Tue 10 Apr 2018 11:03:48 AM CST ************************************************************************/ /* * 客户端向服务器发送日期时间格式字符串 * 从服务器接收到时间对应的日期字符串后打印输出 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define MAXBUF 512 int main(int argc, char **argv) { int conn_fd; /*创建连接套接字*/ struct sockaddr_in serv_addr, client_addr; /*创建服务器地址和客户端地址*/ int port; char req[MAXBUF+1] = {0}; char recv[MAXBUF+1] = {0}; size_t z; if(argc < 3) { printf("Usage: %s address portnumber\n", argv[0]); exit(1); } if((port = atoi(argv[2])) < 0) { printf("portnumber: 0~65535\n"); exit(1); } if((conn_fd = socket(AF_INET, SOCK_STREAM,0)) == -1) { perror("sockt fail"); exit(1); } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(port); if(connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { perror("connect fail"); exit(1); } fprintf(stdout, "\tclient connects to %s: %s\n", argv[1], argv[2]); while(1) { printf("\nEnter format string('q'uit?):"); if(fgets(req, MAXBUF, stdin) == NULL || req[0] == 'q') { printf("\n"); break; } z = strlen(req); if(z>0 && req[--z] == '\n') { req[z] = '\0'; } if(z == 0) continue; /*客户端仅仅输入换行符*/ /*发送请求到服务器端*/ z = write(conn_fd, req, strlen(req)); if(z<0) { perror("write fail"); exit(1); } fprintf(stdout, "\tclient has sent '%s' to the server\n", req); /*从服务器端接收*/ if((z = read(conn_fd, recv, MAXBUF)) == -1) { perror("read fail"); exit(1); } if(z == 0) { fprintf(stdout, "server has closed the socket\n"); fprintf(stdout, "press any key to continue...\n"); getchar(); break; } recv[z] = '\0'; /*输出结果*/ printf("result from %s: %d \n\t'%s'\n", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port), recv); } close(conn_fd); return 0; }
运行实验
运行服务器端:
$ ./timeserver 9000 Server get connection from 127.0.0.1: 57628 client 127.0.0.1 has closed the socket
运行客户端
$ ./timeclient 127.0.0.1 9000 client connects to 127.0.0.1: 9000 Enter format string('q'uit?):%Y %D client has sent '%Y %D' to the server result from 127.0.0.1: 9000 '2018 04/10/18' Enter format string('q'uit?):q
2. 并发流式套接字客户端/服务器编程
上面的流式套接字服务器在对所接收到的一个客户端的连接请求进行处理时,不能再接收(执行accept函数)其他客户端的链接请求,只有当服务器完全结束了对这个客户端的所有请求处理后,才能对下一个客户端的请求进行处理。
可以利用Linux系统的多任务特性,通过创建子进程系统调用,让新产生的子进程对客户端请求进行后续处理,而主进程返回继续接收其他客户端发来的请求,这样就实现了同时对多个客户端请求的并行处理模式。
服务器程序
/************************************************************************* > File Name: multi_server.c > Author: > Mail: > Created Time: Tue 10 Apr 2018 01:03:19 PM CST ************************************************************************/ /* * 这是一个多进程服务器 * 服务器主进程负责监听 * 子进程负责处理连接请求 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #include <errno.h> #include <netdb.h> #include <signal.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/wait.h> #include <arpa/inet.h> #include <netinet/in.h> #define MAXBUF 512 static void sigchild_handler(int signo) { pid_t pid; int status; char msg[] = "SIGCHLD caught\n"; write(STDOUT_FILENO, msg, sizeof(msg)); do { pid = waitpid(-1, &status, WNOHANG); }while(pid>0); } int main(int argc, char **argv) { int listen_fd, conn_fd; /*定义监听套接字和连接套接字*/ pid_t pid; /*进程标识符*/ struct sockaddr_in serv_addr, client_addr; /*定义服务器地址和客户端地址*/ socklen_t len; int port; time_t t; struct tm stime; char req[MAXBUF+1] = {0}; char send_time[MAXBUF+1] = {0}; int z; struct sigaction child_action; memset(&child_action, 0, sizeof(child_action)); child_action.sa_flags |= SA_RESTART; child_action.sa_handler = sigchild_handler; if(sigaction(SIGCHLD, &child_action, NULL) == -1) { perror("Fail to ignore SIGCHLD"); } if(argc < 2) { fprintf(stdout, "Usage: %s portnumbr\n", argv[0]); exit(1); } if((port = atoi(argv[1])) < 0) { fprintf(stdout, "portnumber: 0~65535\n"); exit(1); } /* * 创建监听套接字 */ if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket fail"); exit(1); } /* * 设置服务器地址 */ memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(port); /* * 绑定 */ if(bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { perror("bind fail"); exit(1); } /* * 监听 */ if(listen(listen_fd, 128) == -1) { perror("listen fail"); exit(1); } while(1) { len = sizeof(client_addr); if((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &len)) == -1) { perror("accept fail"); exit(1); } fprintf(stdout, "Server get connection from %s: %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); /* * 创建子进程进行处理 */ pid = fork(); if(pid == -1) { perror("fork fail"); exit(1); } else if(pid == 0) { /* * 子进程处理 */ fprintf(stdout, "\tEntering the child: %d\n", getpid()); while(1) { z = read(conn_fd, req, MAXBUF); if(z == -1) { perror("read fail"); exit(1); } if(z == 0) { fprintf(stdout, "\tclient %s has close the socket\n", inet_ntoa(client_addr.sin_addr)); break; } req[z] = '\0'; time(&t); stime = *localtime(&t); strftime(send_time, sizeof(send_time), req, &stime); z = write(conn_fd, send_time, strlen(send_time)); if(z == -1) { perror("write fail"); exit(1); } } fprintf(stdout, "\tChild process: %d exits\n", getpid()); close(conn_fd); exit(0); } else { /* * 父进程 */ fprintf(stdout, "This is parent\n"); close(conn_fd); /*关闭重复的套接字*/ } } close(listen_fd); return 0; }
服务器主进程
- 函数
sigchild_handler
定义了子进程退出信号的处理程序,并且在之后注册了该信号处理程序。当负责处理客户端的子进程退出时,将由信号处理程序进行善后处理,避免出现僵死进程。 - 父进程在166行关闭连接套接字。这一步很重要,因为调用了fork函数后,父进程和子进程都打开了相连的套接字,但是父进程此时并不为此链接客户端的请求进行具体服务,而是继续进行监听,所以它必须关闭此连接套接字,而子进程继续使用此连接套接字为客户端提供服务。
服务器子进程
服务器子进程循环处理客户端发来的请求,当客户端请求完毕断开链接后,服务器子进程的read操作将遇到EOF,从而导致子进程退出处理循环并关闭服务器的此连接套接字。子进程执行exit(0)后,内核将向父进程发送SIGCHLD信号,同时子进程进入僵死状态。
服务器子进程终止
子进程终止时,父进程的相关善后处理:
- 内核提交SIGCHLD信号,说明子进程已经终止;
- sigchild_handler信号处理函数开始执行,通过do…while()循环,调用waitpid直到没有任何已退出的子进程。另外waitpid使用了WNOHANG参数,因为信号处理程序不能阻塞,否则无法为下一个可能到来的客户端连接服务。
实验
运行服务器:
$ ./multi_server 9000 Server get connection from 127.0.0.1: 57698 This is parent Entering the child: 3387 Server get connection from 127.0.0.1: 57700 This is parent Entering the child: 3389 client 127.0.0.1 has close the socket Child process: 3389 exits SIGCHLD caught client 127.0.0.1 has close the socket Child process: 3387 exits SIGCHLD caught
运行客户端1
$ ./timeclient 127.0.0.1 9000 client connects to 127.0.0.1: 9000 Enter format string('q'uit?):%c client has sent '%c' to the server result from 127.0.0.1: 9000 'Tue Apr 10 14:56:23 2018' Enter format string('q'uit?):q
运行客户端2
$ ./timeclient 127.0.0.1 9000 client connects to 127.0.0.1: 9000 Enter format string('q'uit?):q
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)