这篇文章是对于上一篇文章的单进程版本的优化。不过这里采用的是回显方式,不是阻塞式聊天。客户端使用的同一个客户端代码。客户端代码
多进程服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
//定义服务器IP地址
#define SERVER_IP INADDR_ANY
//定义listen等待队列
#define WAIT_QUEUE 5
//套接字创建和绑定函数
int StartUp(char* port){
//创建服务器套接字
int server_sock = socket(AF_INET, SOCK_STREAM, 0);
if(server_sock < 0){ printf("create server_sock failed, errno : %d, error_string: %s\n", errno, strerror(errno));
return 2;
}
//初始化
struct sockaddr_in service_socket;
bzero(&service_socket, sizeof(service_socket));
service_socket.sin_family = AF_INET;
service_socket.sin_port = htons(atoi(port));
service_socket.sin_addr.s_addr = htonl(SERVER_IP);
socklen_t service_socket_len = sizeof(service_socket);
//绑定端口号和IP地址
if((bind(server_sock, (struct sockaddr*)(&service_socket), service_socket_len)) < 0){
printf("bind failed, errno : %d, error_string: %s\n", errno, strerror(errno));
close(server_sock);
return 3;
}
//设置为监听状态
if((listen(server_sock, WAIT_QUEUE)) < 0){
printf("listen failed, errno : %d, error_string: %s\n", errno, strerror(errno));
close(server_sock);
return 4;
}
printf("listen success, wait accept...\n");
return server_sock;
}
//进行数据通信函数
void Processer(int sock, struct sockaddr_in* socket){
while(1){
char buf[1024];
bzero(buf, sizeof(buf));
//等待客户端的数据
int num = recv(sock, buf, sizeof(buf), 0);
if(num == 0){
printf("client %s quit\n", inet_ntoa(socket->sin_addr));
close(sock);
break;
}
if(num > 0){
printf("client %s show: %s\n", inet_ntoa(socket->sin_addr), buf);
buf[strlen(buf)] = '\0';
//发送数据给客户端
int num1 = send(sock, buf, strlen(buf)+1, 0);
if(num1 < 0){
printf("send failed, errno : %d, error_string: %s\n", errno, strerror(errno));
continue;
}
}
else{
printf("recv failed, errno : %d, error_string: %s\n", errno, strerror(errno));
continue;
}
}
}
//创建孙子进程用于执行服务。
void CreateWorker(int sock, struct sockaddr_in* socket){
pid_t pid = fork();
if(pid > 0){//parent
close(sock);
waitpid(pid, NULL, 0);
}
else if(pid == 0){//child
pid_t pid2 = fork();
if(pid2 == 0){//grand child
Processer(sock, socket);
exit(0);
}
else if(pid2 < 0){//wrong
printf("fork2 failed, errno : %d, error_string: %s\n", errno, strerror(errno));
exit(1);
}
//child
exit(0);
}
else{//wrong
printf("fork1 failed, errno : %d, error_string: %s\n", errno, strerror(errno));
exit(1);
}
}
//等待连接函数
int WaitAccept(int sock, struct sockaddr_in* client_socket){
socklen_t client_socket_len = sizeof(&client_socket);
//随时等待接入的客户端
int client_sock = accept(sock, (struct sockaddr*)(client_socket), &client_socket_len);
if(client_sock < 0){
printf("accept failed, errno : %d, error_string: %s\n", errno, strerror(errno));
printf("wait next accept\n");
sleep(2);
return -1;
}
//获取接入客户端的IP地址和端口号并输出
/*INET_ADDRSTRLEN 16*/
char client_ip_buf[INET_ADDRSTRLEN];
const char* ptr = inet_ntop(AF_INET, &(client_socket->sin_addr), client_ip_buf, sizeof(client_ip_buf));
if(ptr == NULL){
printf("inet_ntop failed, errno : %d, error_string: %s\n", errno, strerror(errno));
return -1;
}
printf("a new accept, client ip: %s, client port: %d\n", client_ip_buf, ntohs(client_socket->sin_port));
printf("waiting message from client\n");
return client_sock;
}
// ./service port
int main(int argc, char* argv[]){
if(argc != 2){
printf("Usage: %s port\n", argv[0]);
return 1;
}
int listen_sock = StartUp(argv[1]);
//让服务器主动断开连接的时候,不需要等待2*MML时间。实现可以直接重连
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//进入循环等待链接
while(1){
struct sockaddr_in client_socket;
int client_sock = WaitAccept(listen_sock,&client_socket);
if(client_sock == -1){
continue;
}
CreateWorker(client_sock,& client_socket);
}
return 0;
}
多线程版本的创建孙子进程解析
这个函数里面有一些细节,解析一下。
- 首先为什么要创建孙子进程来执行服务代码呢?其实考虑到了僵尸进程资源回收和阻塞的问题。我们的父进程是用来进行监听的,这个是他的唯一一个任务,所以父进程不能处于阻塞状态。但是我们还是得等待子进程结束之后在进行监听下一个的操作。此时为了实现不阻塞父进程,我们用子进程创建了一个孙子进程。让孙子进程执行服务,子进程fork之后立刻退出,这样父进程就不会阻塞,并且通过waitpid函数进行了子进程资源的回收。同时我们的孙子进程变成了孤儿进程,那么会被1号进程默认收养,孙子进程处理完数据退出后的僵尸进程由1号进程负责。
- 为什么父进程要关闭sock。父进程的作用只有一个,就是监听等待接入。他是不需要对数据进行任何的写入、读入的。为了安全考虑,我们将sock在父进程这里关闭。
//创建孙子进程用于执行服务。
void CreateWorker(int sock, struct sockaddr_in* socket){
pid_t pid = fork();
if(pid > 0){//parent
close(sock);
waitpid(pid, NULL, 0);
}
else if(pid == 0){//child
pid_t pid2 = fork();
if(pid2 == 0){//grand child
Processer(sock, socket);
exit(0);
}
else if(pid2 < 0){//wrong
printf("fork2 failed, errno : %d, error_string: %s\n", errno, strerror(errno));
exit(1);
}
//child
exit(0);
}
else{//wrong
printf("fork1 failed, errno : %d, error_string: %s\n", errno, strerror(errno));
exit(1);
}
}
多线程版本
多线程版本的改变并不多,基本是基于多进程版本修改。以下为添加修改的内容,其余内容跟多进程版本一样。
//添加头文件
#include <pthread.h>
//添加结构体Arg,用于pthread_create传参
typedef struct Arg{
int fd;
struct sockaddr_in addr;
}Arg;
//修改CreateWorker函数
void* CreateWorker(void *ptr){
Arg* arg = (Arg*)ptr;
Processer(arg->fd, &arg->addr);
free(arg);
return NULL;
}
//修改main函数中线程的创建
// ./service port
int main(int argc, char* argv[]){
if(argc != 2){
printf("Usage: %s port\n", argv[0]);
return 1;
}
int listen_sock = StartUp(argv[1]);
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//进入循环等待链接
while(1){
struct sockaddr_in client_socket;
int client_sock = WaitAccept(listen_sock,&client_socket);
if(client_sock == -1){
continue;
}
pthread_t tid = 0;
Arg* arg = (Arg*)malloc(sizeof(Arg));
arg->fd = client_sock;
arg->addr = client_socket;
pthread_create(&tid, NULL, CreateWorker, (void*)arg);
pthread_detach(tid);
}
return 0;
}
Makefile
.phony:all
all:service client
//多线程版本需要添加pthread库参数,多进程不需要
flags=-Wall -lpthread
service:service.c
gcc -o $@ $^ $(flags)
client:client.c
gcc -o $@ $^ $(flags)
.phony:clean
clean:
rm -f service client
实验截图
两个版本的实验截图都是一样的,因为两者的差别不大,使用的同一份代码进行改编的。
多进程和多线程版本比较
两个版本的程序都是可以处理多个客户端接入的情况。两者之间的优缺点主要如下:
多进程
优点:
- 对比于单进程版本可以处理多个服务请求。
- 稳定性:多进程版本较为稳定,因为每个进程之间互相独立,其中一个进程崩溃不会影响其他进程的使用。
缺点:
- 效率较低:因为服务是在孙子进程内进行的,创建子进程需要时间,在子进程创建之前不能提供服务。
- 资源浪费:进程占用的资源较多,针对于我们这种简单的回显服务来说浪费资源了。
- 服务客户端数量有限:系统中可以创建的进程是有限的。
- 服务周期:当服务的数量变多了,CPU的调用进程变多,这样CPU的轮转周期变长,导致服务效率变低。
多线程
优点:
- 对比于单进程版本,可以处理多个服务请求。
- 占用的资源比较于进程来说少得多。
- 线程的调度周期比进程好得多
缺点:
- 稳定性:线程中有一个线程崩溃就会导致整个进程结束,可靠性太低。