一、当我们使用单进程单连接且使用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的部分程序:

 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
 
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函数来完善客户端程序,如下所示:

 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
 
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函数,可以用它来改进服务器端程序,实现单进程并发服务。先看如下程序,再来解释:

 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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
 
/*************************************************************************
    > 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》

使用select函数改进客户端/服务器端程序的更多相关文章

  1. UNIX网络编程——使用select函数编写客户端和服务器

    首先看原先<UNIX网络编程--并发服务器(TCP)>的代码,服务器代码serv.c: #include<stdio.h> #include<sys/types.h> ...

  2. IO复用与select函数

    socket select函数的详细讲解 select函数详细用法解析      http://blog.chinaunix.net/uid-21411227-id-1826874.html linu ...

  3. select模型(一 改进客户端)

    一.改程序使用select来改进客户端对标准输入和套接字输入的处理,否则关闭服务器之后循环中的内容都要被gets阻塞.原程序中https://www.cnblogs.com/wsw-seu/p/841 ...

  4. UNIX网络编程——select函数的并发限制和 poll 函数应用举例

    一.用select实现的并发服务器,能达到的并发数,受两方面限制 1.一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n来调整或者使用setrlimit函数设置,  ...

  5. 并发服务器--02(基于I/O复用——运用Select函数)

    I/O模型 Unix/Linux下有5中可用的I/O模型: 阻塞式I/O 非阻塞式I/O I/O复用(select.poll.epoll和pselect) 信号驱动式I/O(SIGIO) 异步I/O( ...

  6. 线程模型、pthread 系列函数 和 简单多线程服务器端程序

    一.线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属于1:1模型. (一).N:1用户线程模型 “线程实现”建立在“进程控制”机制之上,由用 ...

  7. socket何时处于”读就绪状态“?---通过“应用程序大爷&quot;和&quot;内核孙子&quot;对话再看重要的select函数的使用方法

    前面. 我已经陆续介绍过select函数的一些零碎知识, 在本文中,我们来讨论这样一个问题:socket何时处于读就绪状态? 事实上主要讨论select函数, 毕竟socket的读就绪状态会导致sel ...

  8. posix 线程(一):线程模型、pthread 系列函数 和 简单多线程服务器端程序

    posix 线程(一):线程模型.pthread 系列函数 和 简单多线程服务器端程序 一.线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属 ...

  9. select 函数1

    Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect.accept.recv或recvfrom这样的阻塞程序( ...

随机推荐

  1. IDEA是如何导入项目的,及启动导入项目遇到的问题:无法加载主类的一连串问题

    1.启动报错误: 找不到或无法加载主类 org.spring.springboot.Application 可能在工程下面有多个module,然后,module里面的iml配置文件不止一个,删除留主的 ...

  2. IOS之UITabBarController

    在学习IOS开发过程中,针对于UITabBarController的使用也不少出现,UITabBarController和UINavigationController类似,UITabBarContro ...

  3. js 获取读取cookie

    // --- 设置cookie function setCookie(sName, sValue, expireHours) {     var cookieString = sName + &quo ...

  4. CSDN问答频道“华章杯”11月排行榜活动开始,丰厚奖品等你拿

    CSDN问答频道月度排行榜,是CSDN问答频道从3月开始举办的活动,旨在鼓励更多用户参与提问和解答,创造一个良好的互帮互助氛围,使参与者在问和答的过程中得到技术水平的提升,也希望大家能在技术交流中结交 ...

  5. (转)Overview : Writing Scripts in C# 使用C#书写脚本

    Apart from syntax, there are some differences when writing scripts in C# or Boo. Most notable are: 除 ...

  6. 单页WEB应用(三),Chat聊天模块

    Chat 聊天模块 这个模块应该就是该书全篇的唯一一个模块吧,后面差点儿全部的篇章都环绕这个模块去实现的,只是就通过这一个模块的实现和上线,也能体现单页应用开发到公布上线的整个过程,毕竟后面的数据.通 ...

  7. Grails开发环境的高速搭建

    Grails开发环境的高速搭建 1 JAVA环境变量的设置和Grails设置环境变量 个人參考 JAVA_HOME =E:\kaifa\Java\jdk7_32 GRAILS_HOME =E:\kai ...

  8. Canvas简述

    HTML Canvas API有两方面优势可以弥补:首先,不需要将所绘制图像中的每个图元当做对象存储,因此执行性能非常好:其次,在其他编程语言现有的优秀二维绘图API的基础上实现Canvas API相 ...

  9. maven 配置 Java Servlet API

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --><dependency> ...

  10. html页面禁止选择复制剪切

    在body加入 onselectstart="return false" oncopy="return false;" oncut="return f ...