一、在前面讲过的回射客户/服务器程序中,服务器只能处理一个客户端的请求,如何同时服务多个客户端呢?在未讲到select/poll/epoll等高级IO之前,比较老土的办法是使用fork来实现。网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程,最简单的办法就是直接忽略SIGCHLD信号。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
 
/*************************************************************************
    > File Name: echoser.c
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Fri 01 Mar 2013 06:15:27 PM CST
 ************************************************************************/

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

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

void do_service(int);

int main(void)
{
    signal(SIGCHLD, SIG_IGN);
    int listenfd; //被动套接字(文件描述符),即只可以accept, 监听套接字
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        //  listenfd = socket(AF_INET, SOCK_STREAM, 0)
        ERR_EXIT("socket error");

struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
    /* inet_aton("127.0.0.1", &servaddr.sin_addr); */

int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt error");

if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind error");

if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
        ERR_EXIT("listen error");

struct sockaddr_in peeraddr; //传出参数
    socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值
    int conn; // 已连接套接字(变为主动套接字,即可以主动connect)

pid_t pid;

while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完成的序列
            ERR_EXIT("accept error");
        printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
               ntohs(peeraddr.sin_port));

pid = fork();
        if (pid == -1)
            ERR_EXIT("fork error");
        if (pid == 0)
        {
            // 子进程
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn); //父进程
    }

return 0;
}

void do_service(int conn)
{
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));
        if (ret == 0)   //客户端关闭了
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read error");
        fputs(recvbuf, stdout);
        write(conn, recvbuf, ret);
    }
}

上述程序利用了一点,就是父子进程共享打开的文件描述符,因为在子进程已经用不到监听描述符,故将其关闭,而连接描述符对父进程也没价值,将其关闭。当某个客户端关闭,则read
返回0,退出循环,子进程顺便exit,但如果没有设置对SIGCHLD信号的忽略,则因为父进程还没退出,故子进程会变成僵尸进程。

现在先运行server,再打开另外两个终端,运行client(直接用回射客户/服务器程序中的客户端程序),可以看到server输出如下:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_fork 
recv connect ip=127.0.0.1 port=46452
recv connect ip=127.0.0.1 port=46453

在另一个终端ps一下:

simba@ubuntu:~$ ps aux | grep echoser
simba     3300  0.0  0.0   2008   280 pts/0    S+   22:10   0:00 ./echoser_fork
simba     3303  0.0  0.0   2008    60 pts/0    S+   22:10   0:00 ./echoser_fork
simba     3305  0.0  0.0   2008    60 pts/0    S+   22:10   0:00 ./echoser_fork
simba     3313  0.0  0.0   4392   836 pts/3    S+   22:12   0:00 grep --color=auto echoser
simba@ubuntu:~$

发现共有3个进程,其中一个是父进程处于监听中,另外两个是子进程处于对客户端服务中,现在ctrl+c 掉其中一个client,由上面的分析可知对应服务的子进程也会退出,而因为我们设置了父进程对SIGCHLD信号进行忽略,故不会产生僵尸进程,输出如下:

simba@ubuntu:~$ ps aux | grep echoser
simba     3300  0.0  0.0   2008   280 pts/0    S+   22:10   0:00 ./echoser_fork
simba     3305  0.0  0.0   2008    60 pts/0    S+   22:10   0:00 ./echoser_fork
simba     3321  0.0  0.0   4392   836 pts/3    S+   22:13   0:00 grep --color=auto echoser

如果把29行代码注释掉,上述的情景输出可能为:

simba@ubuntu:~$ ps aux | grep echoser
simba     3125  0.0  0.0   2004   280 pts/0    S+   21:38   0:00 ./echoser_fork
simba     3128  0.0  0.0      0     0 pts/0    Z+   21:38   0:00
[echoser_fork] <defunct>
simba     3130  0.0  0.0   2004    60 pts/0    S+   21:38   0:00 ./echoser_fork
simba     3141  0.0  0.0   4392   832 pts/3    S+   21:40   0:00 grep --color=auto echoser

即子进程退出后变成了僵尸进程。

如果不想忽略SIGCHLD信号,则必须在信号处理函数中调用wait处理,但这里需要注意的是wait只能等待第一个退出的子进程,所以这里需要使用

waitpid。若当前只有一个子进程退出,则waitpid一次之后因为其他子进程状态尚未改变,故返回0退出循环;若几个连接同时断开,信号因为不能排队

而只接收到一个SIGCHLD信号,waitpid多次之后已经不存在子进程了,返回-1退出循环。如下所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
 
signal(SIGCHLD, handler);
.....................

void handler(int sig)
{
    /*  wait(NULL); //只能等待第一个退出的子进程 */
 
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

二、在最基本的回射客户/服务器程序中,服务器只能被动接收客户端的信息,而不能主动发送信息给客户端,如果我们想实现对等通信,即P2P,可以

在服务器程序用使用两个进程,一个进程接收用户的输入并发送给客户端,另一个进程被动接收客户端的消息并打印出来,此进程当read 返回0 时得知

客户端已经关闭需要退出进程,此时尚有另一个进程未退出,可以通过在退出前发送消息给它,在消息处理函数中退出。当然客户端也必须使用双进

程,原理与服务器程序相同。

下面贴出服务器端程序:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
 
/*************************************************************************
    > File Name: echoser.c
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Fri 01 Mar 2013 06:15:27 PM CST
 ************************************************************************/

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

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int listenfd; //被动套接字(文件描述符),即只可以accept
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        //  listenfd = socket(AF_INET, SOCK_STREAM, 0)
        ERR_EXIT("socket error");

struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
    /* inet_aton("127.0.0.1", &servaddr.sin_addr); */

int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt error");

if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind error");

if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
        ERR_EXIT("listen error");

struct sockaddr_in peeraddr; //传出参数
    socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值
    int conn; // 已连接套接字(变为主动套接字,即可以主动connect)
    if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept error");
    printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
           ntohs(peeraddr.sin_port));

pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork error");
    if (pid == 0)
    {

signal(SIGUSR1, handler);

char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        exit(EXIT_SUCCESS);
    }
    else
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read error");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            fputs(recvbuf, stdout);
        }
        kill(pid, SIGUSR1); //父进程退出时发送信号给子进程
        exit(EXIT_SUCCESS);
    }

}

客户端程序与服务器端程序的改变差不多,就不贴了,这里是使用父子进程来完成对等通信,即双方都可以发送信息给对方,也可以接收对方的信息,

上面使用了kill 函数来发现自定义信号,如果子进程发送信号给父进程,可以使用getppid 函数得到父进程的id。输出如下:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./p2pser
recv connect ip=127.0.0.1 port=56404
fsafd           //<< server 输入
fds

^C

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./p2pcli
fsafd
fds            //<< client 输入

peer close
recv a sig=10

注意,ctrl+c 会将前台进程组中的两个进程都终止掉,即父子进程都打开了conn,只有两个进程都close(conn),将file 的引用计数减为0,才会真正关

闭sockfs 文件,这样client 其中一个进程才能read 返回0打印peer close,并发信号给另一个进程,在信号处理函数中退出。

使用fork并发处理多个client的请求和对等通信p2p的更多相关文章

  1. [WEB API] CLIENT 指定请求及回应格式(XML/JSON)

    [Web API] Client 指定请求及响应格式(xml/json) Web API 支持的格式请参考 http://www.asp.net/web-api/overview/formats-an ...

  2. Elasticsearch High Level Rest Client 发起请求的过程分析

    本文讨论的是JAVA High Level Rest Client向ElasticSearch6.3.2发送请求(index操作.update.delete--)的一个详细过程的理解,主要涉及到Res ...

  3. python3 http.client 网络请求

    python3 http.client 网络请求 一:get 请求 ''' Created on 2014年4月21日 @author: dev.keke@gmail.com ''' import h ...

  4. 基于Netty和SpringBoot实现一个轻量级RPC框架-Client端请求响应同步化处理

    前提 前置文章: <基于Netty和SpringBoot实现一个轻量级RPC框架-协议篇> <基于Netty和SpringBoot实现一个轻量级RPC框架-Server篇> & ...

  5. 多个client与一个server端通信的问题

    多个client与一个server端通信的问题 上篇博文主要是讲的关于client与server端的通信问题.在上篇博文中当我们仅仅有一个client訪问我们的server时是能够正常执行的,可是当我 ...

  6. Nginx与Tomcat、Client之间请求的长连接配置不一致问题解决[转]

    http://bert82503.iteye.com/blog/2152613 前些天,线上出现“服务端长连接与客户端短连接引起Nginx的Writing.Active连接数过高问题”,这个是由于“服 ...

  7. Spark技术内幕:Client,Master和Worker 通信源代码解析

    Spark的Cluster Manager能够有几种部署模式: Standlone Mesos YARN EC2 Local 在向集群提交计算任务后,系统的运算模型就是Driver Program定义 ...

  8. 【Android】使用Gson和Post请求和服务器通信

    一.需求文档如下: URL:http://108.188.129.56:8080/example/cal 请求格式: {"para1":10,"para2":2 ...

  9. IOS-网络(GET请求和POST请求、HTTP通信过程、请求超时、URL转码)

    // // ViewController.m // IOS_0129_HTTP请求 // // Created by ma c on 16/1/29. // Copyright © 2016年 博文科 ...

随机推荐

  1. Binary Tree Inorder Traversal leetcode java

    题目: Given a binary tree, return the inorder traversal of its nodes' values. For example: Given binar ...

  2. 用C#代码编写的SN快速输入工具

    一般软件都要输入序列号(SN),而大家平时用的最多的恐怕是盗版软件,通常盗版软件的序列号(SN)都保存成:XXXXX-XXXXX-XXXX-XXXX的形式. 而软件输入序列号的地方通常都是几个文本框( ...

  3. Windows下创建文件的权限问题

    在Windows下如果在某个目录下建立一个文件,那么新建立的文件会默认继承该目录的所有权限(父子关系) 如果将一个文件从一个目录移动到到另一个目录下,那么该文件的权限并不会继承自新目录的权限而是还保留 ...

  4. php实现支付宝授权登录

    第一步: 登录到蚂蚁金服开放平台https://open.alipay.com/platform/home.htm,前提是有商户号.创建应用之后,然后到开发者中心开通对应功能.如图: 第二步: 到应用 ...

  5. [Node.js]30. Level 6: Listen 'Question' from client, and then Answer the Question

    Clients can also answer each other questions, so let's build that feature by first listening for the ...

  6. Java程序调用带参数的shell脚本返回值

    Java程序调用带参数的shell脚本返回值 首先来看看linux中shell变量(\(#,\)@,$0,$1,\(2)的含义解释 变量说明: -  \)$  Shell本身的PID(ProcessI ...

  7. MySQL的IFNULL函数

    MySQL函数里有一个很有用的函数IFNULL,它的形式是IFNULL(fieldA,fieldB),意义是当字段fieldA是NULL时取fieldB,不是NULL时取fieldA的值. 这个函数与 ...

  8. ArcGIS Pro体验02——启动、创建工程

    所有的猜测都是眼睛看到的,自己想到的,可能不一定正确哈. 任务界面十分简洁,左上是创建新工程,右上是账户名称,左上是关于. 可以直接创建一个工程,Blank应该是无类型,最后保存再选择:Global ...

  9. JavaScript原始基础

    一.算法 + 数据结构 = 程序 程序=数据结构+算法是由N.Wirth(沃斯)提出来的. 程序是计算机指令的某种组合,控制计算机的工作流程,完成一定的逻辑功能,以实现某种任务: 数据结构指的是数据与 ...

  10. Java从零开始学二十七(NumberFormat类)

    一.NumberFormat表示数字的格式化类 NumberFormat表示数字的格式化类,即:可以按照本地的风格习惯进行数字的显示. No. 方法 类型 描述 1 public static Loc ...