Linux 并发服务器编程(多进程)

说明

在Linux中通过流式套接字编程(TCP),实现一个并发服务器的访问回显,适合刚学完Linux套接字编程的朋友进行巩固训练
具体功能:

  • 服务器能够同时连接、处理多个客户端的信息
  • 客户端向服务器发送数据之后,服务器收到数据,然后反手发送给客户端
  • 服务器能够对客户端的退出做出反应,并在客户端退出连接的时候给出提示
  • 服务器能够识别每个客户端发送的信息,在显示的时候加上客户端的IP地址
  • 服务器中能够对已经退出的服务进程作回收处理
  • 客户端能够对服务器的退出作出反应,检测到服务器退出后客户端也退出

注意事项

  • 多进程并发服务器编程中,每次建立一个套接字连接,都会fork一个进程来处理
  • accept是自带阻塞的,所以fork返回父进程之后,父进程就会阻塞等待下一个已连接套接字
  • 客户端的关闭通过ctrl-c发出的信号(SIGINT)来终止客户端
  • 当客户端终止之后,服务器上对应的服务进程通过exit结束,此时由于服务器的主进程还阻塞在accept中,所以无法及时回收子服务进程,所以通过注册一个信号SIGCHLD处理函数,在信号处理的时候回收僵尸子进程。SIGCHLD是子进程结束的时候发送给父进程的信号,默认忽略。
  • 服务器进程如何检测客户端退出呢?通过recv()函数,当返回值为0的时候,表示客户端已经关闭套接字,即客户端退出。
  • 当服务线程主动关闭的时候,客户端也会通过recv()收到服务器关闭的信息,然后客户端主动退出
  • 关于套接字描述符,因为描述符也算是进程的资源,当套接字描述符的引用值为0的时候,才会关闭套接字,或者是进程退出的时候释放套接字描述符资源
  • 每次fork的时候,都会产生一个对于已经打开的套接字描述符的引用,所以要在进入子服务进程后关闭监听套接字描述符、在主服务进程中关闭已连接套接字描述符、在子服务进程退出的时候关闭已连接套接字描述符、在退出主服务器进程的时候关闭监听套接字描述符,这样才做到了有始有终(fork之后已连接套接字描述符的引用就有两份)在这里插入图片描述

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>

#define SERVER_ADDR "172.17.44.154"
#define BUFSIZE 100

void sigchld_handler(int arg);

int main(int argc, const char *argv[])
{
    int socket_fd, new_fd;
    struct sockaddr_in server_addr, cli_addr;
    char buf[BUFSIZE];
    int pid;
    struct sigaction sig;

    /* 注册中断信号处理函数 */
    sig.sa_handler = sigchld_handler;
    sigaction(SIGCHLD, &sig, NULL);


    /* 创建套接字,并获取套接字描述符 */
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == socket_fd) {
        perror("socket");
        exit(-1);
    }

    /* 绑定地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(5001);
    inet_pton(AF_INET, SERVER_ADDR, (void*)&server_addr.sin_addr.s_addr);   //地址转换
    if (-1 == bind(socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))) {
        perror("bind");
        exit(-1);
    }

    /* 转换为被动连接套接字 */
    if (-1 == listen(socket_fd, 5)) {
        perror("listen");
        exit(-1);
    }

#if 0  //单进程服务器
    /* 获取已连接套接字 */
    socklen_t len = 0;
    new_fd = accept(socket_fd, (struct sockaddr*)&cli_addr, (socklen_t *)&len);
    if (-1 == new_fd) {
        perror("accept");
        exit(-1);
    }
    printf("accept socket!\nclient ip :%s  port:%d\n", inet_ntoa(cli_addr.sin_addr), cli_addr.sin_port);

    while (1) {
        memset(buf, 0, BUFSIZE);
        if (0 == recv(new_fd, buf, BUFSIZE, 0)) {  //接受数据
            printf("the client is closed\n");
            break;
        }
        printf("read:%s\n", buf);
        send(new_fd, buf, BUFSIZE, 0);           //回应客户端
    }
    close(new_fd);

#else   //多进程并发服务器
    while (1) {

        socklen_t len = 0;
        new_fd = accept(socket_fd, (struct sockaddr*)&cli_addr, (socklen_t *)&len);   
        if (-1 == new_fd) {
            if (errno == EINTR) continue;   //accept可能会被信号中断
            perror("accept");
            exit(-1);
        }

        /* 并发服务器:子进程中进行TCP通信 */
        pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(0);
        } else if (pid == 0) {
            close(socket_fd);         //关闭监听套接字

            while (1) {
                memset(buf, 0, BUFSIZE);
                if (0 == recv(new_fd, buf, BUFSIZE, 0)) {  //接受数据,只有当客户端主动关闭的时候,才退出线程,还要对关闭之后的子进程
                    printf("the client socket %s is closed\n", inet_ntoa((struct in_addr)cli_addr.sin_addr));
                    close(new_fd);                         //退出之前记得关闭 已连接套接字
                    exit(0);    //通过信号处理函数进行回收                             
                    
                }
                getsockname(new_fd, (struct sockaddr*)&cli_addr, (socklen_t *)&len);  //获取连接套接字信息
                printf("recv client IP:%s data:%s\n", inet_ntoa((struct in_addr)cli_addr.sin_addr), buf);
                send(new_fd, buf, BUFSIZE, 0);           //回应客户端
            }

        } else {
            close(new_fd);   //父进程关闭 已连接套接字
        }

    }
    
#endif

    close(socket_fd);
    return 0;
}

void sigchld_handler(int arg)
{
    int child_pid;
    if (SIGCHLD == arg) {
        if ((child_pid = waitpid(-1, NULL,  WNOHANG)) == -1) {
            perror("sigchld");
        }
        printf("a client %d is end\n", child_pid);
    }

}

client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#define SERVER_ADDR "172.17.44.154"
#define BUFSIZE 100



int main(int argc, const char *argv[])
{
    int new_fd;
    struct sockaddr_in server_addr;
    char buf[BUFSIZE];

    /* 创建套接字,并获取套接字描述符 */
    new_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == new_fd) {
        perror("socket");
        exit(-1);
    }


    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(5001);
    inet_pton(AF_INET, SERVER_ADDR, (void*)&server_addr.sin_addr.s_addr);
    if (-1 == connect(new_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))) {
        perror("connect");
        exit(-1);
    }

    while (1) {
        printf("input:");
        fgets(buf, BUFSIZE, stdin);                 //获取数据

        if (-1 == send(new_fd, buf, BUFSIZE, 0)) {  //发送数据
            perror("send");
            close(new_fd);
            exit(-1);
        }
        if (0 == recv(new_fd, buf, BUFSIZE, 0)) {   //收到数据
            printf("server closed\n");
            break;
        }
        printf("recv:%s\n", buf);
    }
    close(new_fd);

    return 0;
}

运行截图

PS:这里是在同一主机下做实验的,所以各个客户端的IP地址都是一样的
正常运行的状态如下:
在这里插入图片描述

当有一个客户端退出时,服务器会显示信息,但是对其他客户端的服务正常进行:
在这里插入图片描述
当服务器主动关闭之后,所有客户端都会收到服务器关闭的信息,并且主动退出:
在这里插入图片描述

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值