使用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的部分程序:
|
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}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) writen(sock, sendbuf, strlen(sendbuf)); int ret = readline(sock, recvbuf, sizeof(recvbuf)); //按行读取 fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); } close(sock); |
客户端程序阻塞在了fgets 那里,即从标准输入读取数据,所以不能执行到下面的readline,也即不能返回0,不会退出循环,不会调用close关闭sock,所以出现上述的情况,即状态停滞,不能向前推进。具体的状态变化可以参见这里。
出现上述问题的根本原因在于客户端程序不能并发处理从标准输入读取数据和从套接字读取数据两个事件,我们可以使用前面讲过的select函数来完善客户端程序,如下所示:
|
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; char sendbuf[1024] = {0}; while (1) FD_SET(fd_stdin, &rset); if (nready == 0) if (FD_ISSET(sock, &rset)) int ret = readline(sock, recvbuf, sizeof(recvbuf)); //按行读取 fputs(recvbuf, stdout); if (FD_ISSET(fd_stdin, &rset)) if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) writen(sock, sendbuf, strlen(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函数,可以用它来改进服务器端程序,实现单进程并发服务。先看如下程序,再来解释:
|
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> #define ERR_EXIT(m) \ int main(void) struct sockaddr_in servaddr; if (bind(listenfd, (struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前 int nready; while (1) { if (nready == 0) if (FD_ISSET(listenfd, &rset)) { printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), FD_SET(conn, &allset); if (--nready <= 0) for (i = 0; i <= maxi; i++) { if (FD_ISSET(conn, &rset)) { } /* select所能承受的最大并发数受 |
程序有点长,但逻辑并不复杂,我们按照正常运行的状况走一下就清晰了。
前面调用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函数改进客户端/服务器端程序的更多相关文章
- UNIX网络编程——使用select函数编写客户端和服务器
首先看原先<UNIX网络编程--并发服务器(TCP)>的代码,服务器代码serv.c: #include<stdio.h> #include<sys/types.h> ...
- IO复用与select函数
socket select函数的详细讲解 select函数详细用法解析 http://blog.chinaunix.net/uid-21411227-id-1826874.html linu ...
- select模型(一 改进客户端)
一.改程序使用select来改进客户端对标准输入和套接字输入的处理,否则关闭服务器之后循环中的内容都要被gets阻塞.原程序中https://www.cnblogs.com/wsw-seu/p/841 ...
- UNIX网络编程——select函数的并发限制和 poll 函数应用举例
一.用select实现的并发服务器,能达到的并发数,受两方面限制 1.一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n来调整或者使用setrlimit函数设置, ...
- 并发服务器--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( ...
- 线程模型、pthread 系列函数 和 简单多线程服务器端程序
一.线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属于1:1模型. (一).N:1用户线程模型 “线程实现”建立在“进程控制”机制之上,由用 ...
- socket何时处于”读就绪状态“?---通过“应用程序大爷"和"内核孙子"对话再看重要的select函数的使用方法
前面. 我已经陆续介绍过select函数的一些零碎知识, 在本文中,我们来讨论这样一个问题:socket何时处于读就绪状态? 事实上主要讨论select函数, 毕竟socket的读就绪状态会导致sel ...
- posix 线程(一):线程模型、pthread 系列函数 和 简单多线程服务器端程序
posix 线程(一):线程模型.pthread 系列函数 和 简单多线程服务器端程序 一.线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属 ...
- select 函数1
Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect.accept.recv或recvfrom这样的阻塞程序( ...
随机推荐
- java根据经纬度获取地址
public class GetLocation { public static void main(String[] args) { // lat 39.97646 //log 116.3039 S ...
- 一个常见下拉菜单的样式:一体化小三角(纯css手写解决)
类似下拉菜单2个一体化小三角,习惯上用字体图标加jQuery处理,比较方便,但是下面纯css手写解决方式,效果也还不错,对CSS知识也是一个比较好的孔固. 小三角用了2种不同处理方式:1.利用bord ...
- WPF Converter 使用复杂参数的方法
Step 1在WPF的C#代码文件中给定义复杂类型的变量,并给其赋值:Sample code: List<User>lsUser=....Setp 2在 C#代码对应的XAML 中将此复杂 ...
- linux loop device介绍
在Linux中,有一种特殊的块设备叫loop device,这种loop device设备是通过影射操作系统上的正常的文件而形成的虚拟块设备.因为这种设备的存在,就为我们提供了一种创建一个存在于其他文 ...
- Kettle资源库采用SQLserver数据库需要注意的点
Kettle开源ETL工具有着自己的元数据存储方式,可以分为两种 1:File 2:DB 文件存储我这里就不多说了,下面说一下在用SQLserver2008 R2作为资源库在创建的过程中遇到的问题 K ...
- oracle最大连接数相关
1.连接数据库 sqlplus / as sysdba 2.查看当前数据库连接数 select count(*) fromv$process; 3.查看当前数据库允许的最大连接数 select val ...
- Cocos2d-x Touch事件处理机制
在Cocos2d-x中提供两种触摸事件处理机制:CCStandardTouchDelegate和CCTargetedTouchDelegate. 一.如何使用 0.默认情况下CCLayer都是没有启动 ...
- LESS详解之变量(@)
变量基本上是每个语言脚本上都会涉及的一个小小知识点,是学好语言脚本的必经之路.LESS中也可以设置变量,然后通过变量可以改变整个网站的设计风格.良好的掌握LESS中变量的用法,是LESS的基础. 变量 ...
- PHP高级教程-Session
PHP Session PHP session 变量用于存储关于用户会话(session)的信息,或者更改用户会话(session)的设置.Session 变量存储单一用户的信息,并且对于应用程序中的 ...
- C#.Net中操作XML方法一
我们知道XML是一种可标记性的语言,用来标记数据.定义数据类型,是一种执行用户对自己的标记语言进行定义的源语言.由于结构好.而且easy理解,就好比一棵树,层次关系分明,因此也经常把一些数据存储到XM ...