题目要求
服务端
- 基于TCP套接字编写服务器端程序代码,可以绑定从终端输入的IP地址和端口。
- 服务器程序可以显示每一个进程的进程号和当前并发执行的进程数量。
- 服务器程序可以根据客户机要求的服务时间确定进程的生存时间,并在收到连接请求时,新建一个子进程处理该连接请求,并根据客户端请求的服务时间进行延时处理。
- 服务器要清除因并发服务而产生的僵尸进程。
客户端
- 基于TCP套接字编写客户端程序代码,可以从终端输入服务器的IP地址和端口。
- 客户端程序可以从终端输入对服务器的服务时间要求,并将该要求发送给服务器。
联调服务器和客户端,服务器每收到一个连接就新建一个子进程,在子进程中接收客户端的服务时间请求,根据所请求的时间进行延时,然后终止子进程。如:客户端请求服务10s,则服务器的子进程运行10s,然后结束。
程序设计思路
服务端设计思路
-
创建线程池结构体: 定义一个结构体
ThreadPool
用于管理线程池,包括线程数组、任务队列、队头、队尾、队列中任务数量以及互斥锁和条件变量。 -
任务处理函数: 编写一个任务处理函数
task_handler
,该函数在一个循环中等待任务队列中的任务,处理任务,然后继续等待。 -
主函数:
- 解析命令行参数:解析命令行输入的IP地址和端口号。
- 初始化线程池:初始化线程池的互斥锁和条件变量,以及其他相关变量。
- 创建线程:循环创建指定数量的线程,每个线程执行任务处理函数。
- 创建服务器套接字:使用
socket
函数创建一个TCP套接字。 - 绑定套接字:使用
bind
函数将套接字绑定到指定的IP地址和端口号。 - 监听连接:使用
listen
函数开始监听传入的连接请求。 - 循环接受连接:使用
accept
函数接受客户端的连接请求,并将客户端的套接字添加到任务队列中。 - 处理连接:当有新的连接到达时,将客户端的套接字添加到任务队列中,并通知等待的线程处理任务。
- 清理资源:关闭服务器套接字,释放线程池相关资源。
服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#define MAX_CLIENTS 10
#define THREAD_POOL_SIZE 5
// 定义线程池中的任务结构
typedef struct {
int client_socket;
int service_time;
} Task;
// 线程池结构体
typedef struct {
pthread_t threads[THREAD_POOL_SIZE]; // 线程数组
Task task_queue[MAX_CLIENTS]; // 任务队列
int head; // 队头索引
int tail; // 队尾索引
int count; // 队列中任务数量
pthread_mutex_t mutex; // 互斥锁
pthread_cond_t cond; // 条件变量
} ThreadPool; // 线程池结构体类型定义
// 线程池全局变量
ThreadPool thread_pool;
// 任务处理函数
void *task_handler(void *arg) {
while (1) {
pthread_mutex_lock(&thread_pool.mutex);
// 如果任务队列为空,则等待条件变量
while (thread_pool.count == 0) {
pthread_cond_wait(&thread_pool.cond, &thread_pool.mutex);
}
// 取出任务
Task task = thread_pool.task_queue[thread_pool.head];
thread_pool.head = (thread_pool.head + 1) % MAX_CLIENTS;
thread_pool.count--;
pthread_mutex_unlock(&thread_pool.mutex);
// 处理任务
printf("Thread %ld handling client\n", pthread_self());
sleep(task.service_time);
printf("Thread %ld done\n", pthread_self());
close(task.client_socket);
}
return NULL;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
char *ip = argv[1];
int port = atoi(argv[2]);
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// 创建线程池
pthread_mutex_init(&thread_pool.mutex, NULL);
pthread_cond_init(&thread_pool.cond, NULL);
thread_pool.head = 0;
thread_pool.tail = 0;
thread_pool.count = 0;
// 创建线程
for (int i = 0; i < THREAD_POOL_SIZE; i++) {
pthread_create(&thread_pool.threads[i], NULL, task_handler, NULL);
}
// 创建服务器套接字
/*socket(domain, type, protocol): 创建套接字,返回套接字描述符。
domain 参数指定地址家族(如 AF_INET 表示 IPv4),
type 参数指定套接字类型(如 SOCK_STREAM 表示 TCP 套接字),
protocol 参数指定协议(通常为 0,表示使用默认协议)。*/
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 绑定套接字
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(ip);
server_addr.sin_port = htons(port);
/*bind(sockfd, addr, addrlen): 将套接字绑定到地址。
sockfd 是套接字描述符,addr 是要绑定的地址信息结构体,addrlen 是地址结构体的长度。*/
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Error binding socket");
close(server_socket);
exit(EXIT_FAILURE);
}
// 监听连接
/*listen(sockfd, backlog): 开始监听传入的连接请求。
sockfd 是套接字描述符,backlog 是等待连接队列的最大长度。*/
if (listen(server_socket, MAX_CLIENTS) == -1) {
perror("Error listening for connections");
close(server_socket);
exit(EXIT_FAILURE);
}
printf("Server listening on %s:%d\n", ip, port);
while (1) {
// 接受连接
/*accept(sockfd, addr, addrlen): 接受传入的连接请求,并返回新的套接字描述符用于通信。
sockfd 是监听套接字的描述符,addr 是用于存储客户端地址的缓冲区,addrlen 是缓冲区长度。*/
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_len);
if (client_socket == -1) {
perror("Error accepting connection");
continue;
}
pthread_mutex_lock(&thread_pool.mutex);
// 如果任务队列已满,则等待条件变量
while (thread_pool.count == MAX_CLIENTS) {
pthread_cond_wait(&thread_pool.cond, &thread_pool.mutex);
}
// 将任务添加到队列
int service_time;
/*recv(sockfd, buf, len, flags): 接收数据。
sockfd 是套接字描述符,buf 是接收数据的缓冲区,len 是缓冲区长度,flags 是接收选项(通常为 0)。*/
recv(client_socket, &service_time, sizeof(service_time), 0);
thread_pool.task_queue[thread_pool.tail].client_socket = client_socket;
thread_pool.task_queue[thread_pool.tail].service_time = service_time;
thread_pool.tail = (thread_pool.tail + 1) % MAX_CLIENTS;
thread_pool.count++;
pthread_mutex_unlock(&thread_pool.mutex);
pthread_cond_signal(&thread_pool.cond); // 通知等待的线程
printf("Task added to thread pool\n");
}
close(server_socket);
return 0;
}
客户端设计思路
-
解析命令行参数: 解析命令行输入的服务器IP地址、端口号和服务时间。
-
创建套接字: 使用
socket
函数创建一个TCP套接字。 -
连接服务器: 使用
connect
函数连接到指定的服务器IP地址和端口号。 -
发送服务时间请求: 使用
send
函数将客户端请求的服务时间发送给服务器。 -
关闭套接字: 客户端发送完请求后,关闭套接字并退出程序。
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (argc != 4) {
printf("Usage: %s <IP> <port> <service_time>\n", argv[0]);
exit(EXIT_FAILURE);
}
char *ip = argv[1];
int port = atoi(argv[2]);
int service_time = atoi(argv[3]);
int client_socket;
struct sockaddr_in server_addr;
// Create socket
client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// Connect to server
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(ip);
server_addr.sin_port = htons(port);
if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Error connecting to server");
close(client_socket);
exit(EXIT_FAILURE);
}
// Send service time request to server
send(client_socket, &service_time, sizeof(service_time), 0);
printf("Service requested for %d seconds\n", service_time);
close(client_socket);
return 0;
}
编译运行方法
编译为可执行文件:
gcc server.c -o server
gcc client.c -o client
启动服务器:
./server <IP> <port>
启动客户端:
./client <IP> <port> <service_time>
其中,<IP>为服务器的IP地址,<port>为服务器的端口号,<service_time>为服务时间要求。
运行结果截图
可以看到服务端可以同时处理客户端的请求。