网络编程基础-简单的TCP回显程序(多进程和多线程版本)

13 篇文章 0 订阅
11 篇文章 0 订阅

个人博客传送门

这篇文章是对于上一篇文章的单进程版本的优化。不过这里采用的是回显方式,不是阻塞式聊天。客户端使用的同一个客户端代码。客户端代码

多进程服务器端

#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的轮转周期变长,导致服务效率变低。
多线程

优点:

  • 对比于单进程版本,可以处理多个服务请求。
  • 占用的资源比较于进程来说少得多。
  • 线程的调度周期比进程好得多

缺点:

  • 稳定性:线程中有一个线程崩溃就会导致整个进程结束,可靠性太低。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值