linux网络编程之socket(九):使用select函数改进客户端/服务器端程序

一、当我们使用单进程单连接且使用readline修改后的客户端程序,去连接使用readline修改后的服务器端程序,会出现一个有趣的现象,先来看输出:

先运行服务器端,再运行客户端,

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_recv_peek  recv connect ip=127.0.0.1 port=54005

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_recv_peek  local ip=127.0.0.1 port=54005

可以先查看一下网络状态,

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188 tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      tcp        0      0 127.0.0.1:54005         127.0.0.1:5188          ESTABLISHED tcp        0      0 127.0.0.1:5188          127.0.0.1:54005         ESTABLISHED

可以看出建立了连接,服务器端有两个进程,一个父进程处于监听状态,另一子进程正在对客户端进行服务。

再ps 出服务器端的子进程,并kill掉它,

simba@ubuntu:~$ ps -ef | grep echoser

simba     4549  3593  0 15:57 pts/0    00:00:00 ./echoser_recv_peek

simba     4551  4549  0 15:57 pts/0    00:00:00 ./echoser_recv_peek

simba     4558  4418  0 15:57 pts/6    00:00:00 grep --color=auto echoser

simba@ubuntu:~$ kill -9 4551

这时再查看一下网络状态,

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188 tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      tcp        1      0 127.0.0.1:54005         127.0.0.1:5188          CLOSE_WAIT  tcp        0      0 127.0.0.1:5188          127.0.0.1:54005         FIN_WAIT2 

来分析一下,我们将server子进程  kill掉,则其终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN 段给 server子进程,因此server 子进程的TCP连接处于FIN_WAIT2状态。

为什么会出现这种情况呢,来看client的部分程序:

void do_echocli(int sock)
{

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};

    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {


        writen(sock, sendbuf, strlen(sendbuf));

        int ret = readline(sock, recvbuf, sizeof(recvbuf)); //按行读取
        if (ret == -1)
            ERR_EXIT("readline error");
        else if (ret  == 0)   //服务器关闭
        {
            printf("server close\n");
            break;
        }

        fputs(recvbuf, stdout);

        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));

    }

    close(sock);
}

客户端程序阻塞在了fgets 那里,即从标准输入读取数据,所以不能执行到下面的readline,也即不能返回0,不会退出循环,不会调用close关闭sock,所以出现上述的情况,即状态停滞,不能向前推进。具体的状态变化可以参见这里

出现上述问题的根本原因在于客户端程序不能并发处理从标准输入读取数据和从套接字读取数据两个事件,我们可以使用前面讲过的select函数来完善客户端程序,如下所示:

void do_echocli(int sock)
{
    fd_set rset;
    FD_ZERO(&rset);

    int nready;
    int maxfd;
    int fd_stdin = fileno(stdin); //
    if (fd_stdin > sock)
        maxfd = fd_stdin;
    else
        maxfd = sock;

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};

    while (1)
    {

        FD_SET(fd_stdin, &rset);
        FD_SET(sock, &rset);
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //select返回表示检测到可读事件
        if (nready == -1)
            ERR_EXIT("select error");

        if (nready == 0)
            continue;

        if (FD_ISSET(sock, &rset))
        {

            int ret = readline(sock, recvbuf, sizeof(recvbuf)); //按行读取
            if (ret == -1)
                ERR_EXIT("read error");
            else if (ret  == 0)   //服务器关闭
            {
                printf("server close\n");
                break;
            }

            fputs(recvbuf, stdout);
            memset(recvbuf, 0, sizeof(recvbuf));
        }

        if (FD_ISSET(fd_stdin, &rset))
        {

            if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
                break;

            writen(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
    }

    close(sock);
}

即将两个事件都添加进可读事件集合,在while循环中,如果select返回说明有事件发生,依次判断是哪些事件发生,如果是标准输入有数据可读,则读取后再次回到循环开头select阻塞等待事件发生,如果是套接口有数据可读,且返回为0则说明对方已经关闭连接,退出循环并调用close关闭sock。

重复前面的实验过程,把客户端换成使用select函数修改后的程序,可以看到最后的输出:

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188 tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      tcp        0      0 127.0.0.1:5188          127.0.0.1:54007         TIME_WAIT  

即 client 关闭socket描述符,server 子进程的TCP连接收到client发的FIN段后处于TIME_WAIT状态,此时会再发生一个ACK段给client,client接收到之后就处于CLOSED状态,这个状态存在时间很短,所以看不到客户端的输出条目,TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,需要有MSL 时间的主要原因是在这段时间内如果最后一个ack段没有发送给对方,则可以重新发送。

过一小会再次查看网络状态,

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188 tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN 

可以发现只剩下服务器端父进程的监听状态了,由TIME_WAIT状态转入CLOSED状态,也很快会消失。

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

二、前面我们实现的能够并发服务的服务器端程序是使用fork出多个子进程来实现的,现在学习了select函数,可以用它来改进服务器端程序,实现单进程并发服务。先看如下程序,再来解释:

/*************************************************************************
    > 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>
#include<sys/wait.h>
#include "read_write.h"

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


int main(void)
{
    
    signal(SIGPIPE, 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)
    int i;
    int client[FD_SETSIZE];
    int maxi = 0; // client数组中最大不空闲位置的下标
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;

    int nready;
    int maxfd = listenfd;
    fd_set rset;
    fd_set allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    while (1) {
        rset = allset;
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (nready == -1) {
            if (errno == EINTR)
                continue;
            ERR_EXIT("select error");
        }

        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset)) {
        
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);  //accept不再阻塞
            if (conn == -1)
                ERR_EXIT("accept error");
            
            for (i = 0; i < FD_SETSIZE; i++) {
                if (client[i] < 0) {
                    client[i] = conn;
                    if (i > maxi)
                        maxi = i;
                    break;
                } 
            }
            
            if (i == FD_SETSIZE) {
                fprintf(stderr, "too many clients\n");
                exit(EXIT_FAILURE);
            }

            printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
                ntohs(peeraddr.sin_port));

            FD_SET(conn, &allset);
            if (conn > maxfd)
                maxfd = conn;

            if (--nready <= 0)
                continue;
        }

        for (i = 0; i <= maxi; i++) {
            conn = client[i];
            if (conn == -1)
                continue;

            if (FD_ISSET(conn, &rset)) {
                
                char recvbuf[1024] = {0};
                int ret = readline(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("readline error");
                else if (ret  == 0) { //客户端关闭 
                    printf("client close \n");
                    FD_CLR(conn, &allset);
                    client[i] = -1;
                    close(conn);
                }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
                
                if (--nready <= 0)
                    break; 
            }
        }


    }
        
    return 0;
}

/* select所能承受的最大并发数受
 * 1.一个进程所能打开的最大文件描述符数,可以通过ulimit -n来调整
 *   但一个系统所能打开的最大数也是有限的,跟内存有关,可以通过cat /proc/sys/fs/file-max 查看
 * 2.FD_SETSIZE(fd_set)的限制,这个需要重新编译内核                                                                          
 */

程序有点长,但逻辑并不复杂,我们按照正常运行的状况走一下就清晰了。

前面调用socket,listen,bind等函数等初始化工作就不说了。程序第一次进入while 循环,只把监听套接字加入关心的事件,select返回说明监听套接字有可读事件,即已完成连接队列不为空,这时调用accept不会阻塞,返回一个已连接套接字,将这个套接字加入allset,因为第一次运行则nready = 1,直接continue跳回到while 循环开头,再次调用select,这次会关心监听套接字和一个已连接套接字的可读事件,如果继续有客户端连接上来则继续将其加入allset,这次nready = 2,继续执行下面的for 循环,然后对客户端进行服务。服务完毕再次回到while 开头调用select 阻塞时,就关心一个监听套接字和2个已连接套接字的可读事件了,一直循环下去。

程序大概逻辑就这样,一些细节就大家自己想想了,比如client数组是用来保存已连接套接字的,为了避免每次都得遍历到FD_SETSIZE-1,保存一个最大不空闲下标maxi,每次遍历到maxi就可以了。每次得到一个conn,要判断一下conn与maxfd的大小。

当得知某个客户端关闭,则需要将conn在allset中清除掉。之所以要有allset 和 rset 两个变量是因为rset是传入传出参数,在select返回时rset可能被改变,故需要每次在回到while 循环开头时需要将allset 重新赋予rset 。

参考:

《Linux C 编程一站式学习》

《TCP/IP详解 卷一》

《UNP》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏散尽浮华

Linux下的Mongodb部署应用梳理

一、Mongodb简介  官网地址:http://www.mongodb.org/ MongoDB是一个高性能,开源,无模式的文档型数据库,是当前NoSql数...

2058
来自专栏杨建荣的学习笔记

一条关于swap争用的报警邮件分析(一)(r7笔记第28天)

最近这些天有一台服务器总是会收到剩余swap过低的告警。 邮件内容大体如下: ############ ZABBIX-监控系统: --------------...

3424
来自专栏开源优测

[快学Python3]PyMySQL库

概述 本文主要讲解如何使用pymysql库进行MySQL的管理操作。 主要讲解如何使用pymysql实现增删改查动作,并附上对应的示例。 安装pymysql p...

3479
来自专栏IMWeb前端团队

在NodeJS中利用bookshelf.js进行事务(transaction)管理

本文作者:IMWeb link 原文出处:IMWeb社区 未经同意,禁止转载 术语事务指的是构成单一逻辑工作单元的操作的集合。比如:将钱从一个账户转...

2077
来自专栏JMCui

Netty 系列八(基于 WebSocket 的简单聊天室).

    之前写过一篇 Spring 集成 WebSocket 协议的文章 —— Spring消息之WebSocket ,所以对于 WebSocket 协议的介绍...

1295
来自专栏SpringBoot 核心技术

第三十九章:基于SpringBoot & Quartz完成定时任务分布式单节点持久化

43610
来自专栏互联网高可用架构

初识分库分表框架DBSPLIT

1304
来自专栏数据和云

Oracle写错误与文件离线

当Oracle写数据文件遇到错误时,该如何应对呢?是离线文件还是崩溃实例?这个简单问题的技术变化跨度超过了20年。 自Oracle 11.2.0.2版本开始...

2533
来自专栏码匠的流水账

nginx lua重置请求参数及常量备忘

1081
来自专栏小怪聊职场

MySQL(一)|性能分析方法、SQL性能优化和MySQL内部配置优化

4048

扫码关注云+社区