一个输入操作通常包括两个不同的阶段:
1,等待数据准备好:
2,从内核中向进程复制数据:
对于一个套接字上的输入操作:
1,等待数据从网络中到达。当所有的等待分组到达时,它被复制到内核中的某个缓冲区。
2,数据从内核缓冲区复制到应用进程缓冲区。
套接口的默认状态是阻塞的。
这就意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待相应操作完成。
当然,也可以将其设置为非阻塞模式,调用一个函数
fcntl(fd,F_SETFL, flag | 0_NONBLOCK);
我们之前编写的(回射客户端)中就存在好多的函数,其中好多的都是跟阻塞相关的:
read,write,accept,connect,等等(客户端,服务端)
先来看看阻塞式的情况:
一个简单的recvfrom函数:(UNIX网络编程:page:123)
应用进程和内核之间的相互转化。。。。。。
这里选用recvfrom的原因在于:对于UDP而言,传送的是数据报,数据准备好读取的概念比较明确,数据报到达
或者数据报没有到达。但是对于TCP而言,以字节流为单位,来一个字节吗???理解不太容易。。。
还有,就是明确的:从应用进程切换到内核空间中,之后再切换回来的。。。
进程调用recvfrom函数,其系统调用直到数据报到达并且被复制到应用进程的缓冲区中或者发生错误才返回,
最常见的错误是:系统调用被信号中断。我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞
的,recvfrom成功返回后,应用进程开始处理数据报。。。。
1,输入操作,包括read,readv,recv,recvfrom,recvmsg共5个函数。如果某个进程对一个阻塞的TCP套接字
(默认设置)调用这些输入函数之一,而且该套接字的接收缓冲区中没有数据可读,该进程将被投入睡眠,直
到有一些数据可达。既然TCP是字节流协议,该进程的唤醒就是只要有一些数据到达,这些数据既可能是单个
字节,也可以是一个完整的TCP分节中的数据。如果想等到某个固定数目的数据可读为止,那么我们可以调用
我们的readn函数或者指定MSG_WAITALL标志。
对于UDP而言,是数据报协议,如果一个阻塞的UDP套接字的接收缓冲区为空,对它调用输入函数的进程将
被投入睡眠,直到有UDP数据报到达。
比如:对于read函数,从应用进程空间中运行切换到在内核空间中运行,一段时间后再切换会来。。。。。。
对于非阻塞的套接字,如果输入操作不能被满足(对于TCP套接字即至少有一个字节的数据可读,对于UDP
套接字即有一个完整的数据报可读),内核中没有给返回相应的数据,那么,调用将立即返回一个
EWOULDBLOCK(相当于-1)错误。需要数据的话,我们必须持续的调用这个函数(也就是所谓的循环接收
这种循环接收并不是阻塞,从而对CPU造成极大的浪费,这也就是:忙等待,想要等待一定的数据,但是
这些数据并没有到来,并且还占用着CPU,所以,不太经常使用这种I/O模型),
从而将内核空间的值拷贝到用户空间。一旦拷贝完成了,我们的read函数就可以返回了,返回的值也就不是-1了。
2,输出操作,包括write,writev,send, sendto 和 sendmsg共5个函数。对于一个TCP套接字而言,内核将从
应用进程的缓冲区到该套接字的发送缓冲区复制数据。对于阻塞的套接字,如果其发送缓冲区中没有空间,进程
将被投入到睡眠中,直到有空间为止。间接的相当于给了内核,再从内核中拿了出来。
对于一个非阻塞的TCP套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个
EWOULDBLOCK错误。如果其发送缓冲区中还有一些空间,返回值将是内核能够复制到该缓冲区中的字节数。
这个字节数也称为不足计数(short count)。
UDP套接字不存在真正的发送缓冲区。内核只是复制应用进程数据,并把它沿协议栈向下传送,渐次冠以UDP
首部和IP首部。因此,对一个阻塞的UDP套接字(默认设置),输出函数调用将不会因与TCP套接字一样的原
因而阻塞,不过有可能因其他原有而阻塞
3,接收外来连接,即accept函数。如果对一个阻塞的套接字调用accept函数,并且没有新的连接到来,那么进程将
同样的被投入到睡眠状态中。
如果对一个非阻塞的套接字调用accept函数,并且尚无新的连接到来,accept调用将立即返回一
个:EWOULDBLOCK错误。
4,发起外出连接,即用于TCP的connect函数(我们知道connect同样可以用于UDP,不过它不能使一个“真正”的连
接建立起来,它只是使内核保存对端的IP地址和端口号。),TCP连接的建立涉及到一个三路握手的过程,而且
connect函数一直要等到客户收到对于自己的SYN的ACK确认为止才返回。这意味着TCP的每个connect总会阻塞其
调用进程至少一个到服务的RTT时间。
如果对一个非阻塞的TCP套接字调用connect,并且连接不能立即建立,那么连接的建立能照样发起(比如:送出
TCP三路握手的第一个分组),不过会返回一个EINPROGRESS错误。注意这个错误不同于上述三个情形中返回的
错误
二:I/O复用模型
I/O复用:类似与我们之前编写的回射服务器,客户端有fgets函数,write函数,read函数(标准输入和TCP套接
字。我们遇到的就是:当服务器进程被杀死后,客户端不能立即关掉,阻塞于(标准输入上)fgets函数,服务
器TCP虽然正确的给客户TCP发送了一个FIN,但是既然客户进程正阻塞于从标准输入读入的过程,它将看不到
这个read返回的EOF,直到从套接字读时为止(可能已经好一段时间了))。这样的话,我们的进程就需要一种
预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程,这就是I/O复用。
是由(select和poll这两个函数支持的)
我们用:SELECT来管理多个文件描述符,一旦其中的一个或者多个文件描述符检测到了有数据到来,那么select
返回,那么这个时候在调用recv,read函数的时候就不会阻塞了,这时就可以从内核中拷贝数据到用户空间了,一
旦拷贝完成,那么就可以返回了,所以说:这里的阻塞的时间只是提前到啦select中,
二:信号驱动式I/O模型
主要就是:我们用信号,让内核在描述符就绪时发送SIGIO信号通知我们,这种就是信号驱动式I/O模型。
过程是这样的:首先,我们需要建立一个SIGIO的信号处理程序,这时,我们需要一个系统调用:sigaction
函数,安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞,当
数据报准备好读取时,内核就为该进程产生一个SIGIO的信号。
这个时候在调用recv,read函数的时候就不会阻塞了,这时就可以从内核中拷贝数据到用户空间了,
,这个时候,相当于是通过read函数将数据从内核中拉到用户空间的,是一种拉的机制,并不是内核
的主动推送会来,一
旦拷贝完成,那么就可以返回了
二:异步I/O
这种I/O的效率最高,是通过aio_read来实现的,这个函数会递交一个请求和一个
缓冲区(buf,应用层的),这个时候即使内核中没有数据到来,那么这个函数
也立刻返回,一旦返回之后,应用进程就可以处理其他的进程了,也就基本上
实现了异步处理。如果有数据到来,那么内核也会自动的将数据拷贝到应用层的
缓冲区,也就是拷贝到之前的buf中去,一旦复制完成,会通过一个信号(递交在
aio_read中)来通知应用进程中的。这个时候上层程序直接处理这些数据。
跟信号驱动式I/O的区别在:异步I/O中接收到信号后,数据已经赋值完成了,已
经赋值到了buf空间中了(内核已经默认操作了),已经返回给用户了,之后不需
要再次调用recv等,是内核直接推送到用户缓冲区中的,这是一种推的机制。效
率相对于之前拉的机制,效率应该能更高一些。。。。
三:select函数
用select函数来管理多个I/O,一旦其中的一个I/O或多个I/O检测到我们所感兴趣的事件发生时,那么select函数
返回,返回值为检测到的事件的个数。并且返回那些I/O发生了事件。遍历这些事件,进而处理它
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
中间三个参数:是三个集合,关于第二个参数(fd_set *readfds),读的集合,如果有数据可读,我们检测到
有数据可读的套接口,放到这个集合当中。一旦有数据可读,那么select返回。同理:第三个参数是可写的集合,
第四个参数是异常的集合。
第五个参数:指定超时时间,其中timeval结构用于指定这段时间的秒数和微秒数。如果为NULL,那么会函数出
错,或者持续一直的等待,反正就是不会产生一个0。
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
}
通常为NULL,也就是说:一定要检测到某个事件后才能返回。当然,我们也可以给一个超时时间,如果在时间到
来之前事件还没有产生,那么也会返回,这个时候返回的是0,
select返回失败为-1。
第一个参数:为存放到这些集合中最大描述符加1。(readset集合中存放(1,3,8事件),writest集合中存放
(4,9),exceptset中不存方),那么此刻maxfdp1就是10
关于select:我们经常会用到以下四个宏:
void FD_ZERO(fd_set *fdset); //clear all bits in fdset
void FD_SET(int fd, fd_set *faset); //turn on the bit for fd in fdset
void FD_CLR(int fd, fd_set *fdset); //turn off the bit for fd in fdset;
int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset?
我们通常将用select实现的服务器,称之为并发服务器。。。为什么说是并发呢??因为我们在检测到这些I/O操作
,之后,我们并不能并行的去处理这些问题,我们也是对这些I/O进行遍历,然后处理的。这些I/O是顺序执行的,所以
说是并发服务器,而不是并行服务器
1,对于读,写,异常事件发生的条件,我们来探究探究???
这些东西,主要都是针对套接口的,
可读事件(将一个套接口放到读集合中,那么这些情况产生,select会返回):
1,套接口缓冲区中有数据可读。也就是说,缓冲区中数据字节大于所要求(可以取出数据)的最低字节数。对这
样的套机字执行读操作不会阻塞并将返回一个大于0的值。我们可以使用SO_RCVLOWAT套接字选项设置该
套接字的最低标准,对于TCP,UDP套接字而言,其默认值是1
2,该连接的读半关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0,套接字返回
一个EOF。
3,如果是监听套接字的话,那么如果已完成的队列不为空的话,这样的话,套接字的accept函数通常不会阻塞
4,套接口上有一个错误事件待处理,对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时
把error设置成一个确切的错误条件。这些待处理(Pending error)也可以通过指定SO_ERROR套接字选项
调用getsockopt获取并处理
可读事件(将一个套接口放到读集合中,那么这些情况产生,select会返回):
1,该套接口发送缓冲区有空间容纳数据(可用空间字节数大于所要求(可以容纳数据)的最低空间数),并且
(TCP下),该套接字已经连接,(UDP下),该套接字不需要连接,这间接的意味着,我们把该套接字设
置成非阻塞的,写操作将不阻塞并返回一个正值(由传输层接受的字节数)。我们可以使用SO_SNDLOWAT
套接字选项来设置最低要求空间,对于TCP和UDP而言,默认值为2048
2,连接的写一半关闭,即收到RST段之后,继续调用write,套接口的写操作将产生SIGPIPE
3,同样的,套接口上有一个写错误待处理,这样套接口的写操作也不阻塞并返回-1,同时
把error设置成一个确切的错误条件。这些待处理(Pending error)也可以通过指定SO_ERROR套接字选项
调用getsockopt获取并处理
异常事件
1,套接字存在带外数据 。。。。
还记得,我们之前实现的多个客户端和服务端进行通信时,我们是通过fork来实现并发的,子进程来实现具体的通信
,而父进程来不断检测。。。。。
程序如下:
pid_t pid;
while(1)
{
if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0) //对等方的地址以及长度
ERR_EXIT("accept");
//conn为以连接套接字,是一个主动套接字
printf("ip = %s :port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
pid = fork();
if(pid == -1)
ERR_EXIT("fork");
if(pid == 0) //子进程所执行的就是一个简单的通信
{
close(listenfd);
echo_serv(conn);
close(conn);
exit(EXIT_SUCCESS);
}
else //父进程所执行就是一个不断的新的客户端接受连接的过程
{
close(conn);
}
}
而到了现在,我们可以使用select来处理并发,用select来检测是接收请求还是进行通信,一直这样,不断的伦寻。。
假如,我们将监听套接字放在了可读集合中,那么就会有多种返回情况,而select函数是这个样子的。。。
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
//其中rset是输入输出参数
返回-1:返回出错,或者被信号中断(那么就需要:continue,重新执行)
返回0: 对于当前的这个select是不会返回0的,因为没有给设置时间,所以会一直等待(响应),或者直接出错
当然,从程序的完整行来考虑,我们应该写下来
当前的已完成队列条目已经不为空了
、、
int i;
int client[FD_SETSIZE]; //防止accept的返回值conn产生重复的覆盖
for(i = 0; i< FD_SETSIZE; i++)
client[i] = -1; //-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");
}
if(nready == 0)
continue; //实际上,是不会发生这件事的,只是为了程序的完整性而已。。。。
//接下来,返回其他的,表示connect继续的三次握手已经完成,已完成队列不为空了,那么accept将不会进行阻塞了
if(FD_ISSET(listenfd, &rset))
{
peerlen = sizeof(peeraddr); //调用的时候一定要进行简单的初始化
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn == -1)
ERR_EXIT("accept");
for(i = 0; i< FD_SETSIZE; i++)
{
if(client[i] < 0) //找到空闲
{
client[i] = conn;
break;
}
}
if(i == FD_SETSIZE)
{
fprintf(stderr, "too many clients\n"); //说明连接的客户端已经达到上限
exit(EXIT_FAILURE);
}
printf("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< FD_SETSIZE; 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");
if(ret == 0)
{
printf("client close\n");
FD_CLR(conn, &allset);
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
if(--nready <= 0)
break;
}
}
}
return 0;
运行结果:
可以看到:我们这里利用但进程 实现了并发,,,,
关于select和poll
1,用select实现的并发服务器,能够达到的并发数受两方面的限制,,,
一个进程所能打开的最大文件描述符限制(可以调整内核参数)
select中fd_set集合容量的参数(需要重新编译内核)
接下来,我们看看一个进程所能打开的最大描述符:ulimit -n(命令)
可以看到这里,我们的最大描述符是1024(0-1023),我们可以通过命令来修改:ulimit -n 2048
由上面给出的警告可知,这里需要root用户,我们可以用命令:sudo bash
如上,我们已经合理的改变了最大文件描述符,可是我们在程序中如何处理呢????
1,首先我们应该调用getrlimit来获取资源的限制,并存储于相应的结构体中:rlim
<span style="color:#000000;"> #include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim); //rlim为相应的结构体
int setrlimit(int resource, const struct rlimit *rlim);
//rlim的结构体:
</span><pre name="code" class="cpp"> struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
而此刻我们要获取的资源是:RLIMIT_NOFILE
程序1:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
int main()
{
struct rlimit rl;
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
ERR_EXIT("getrlimit");
printf("%d\n", (int)rl.rlim_max);
return 0;
}
运行结果:
可以看到,此刻我们的最大描述符为4096(为当前进程),那么我们要通过程序来更改呢???
int main()
{
struct rlimit rl;
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
ERR_EXIT("getrlimit");
printf("%d\n", (int)rl.rlim_max);
//如果要改变的话,那么需要我们的重新设置,调用函数setrlimit
rl.rlim_cur = 2048;
rl.rlim_max = 2048;
if(setrlimit(RLIMIT_NOFILE, &rl) < 0)
ERR_EXIT("setrlimit");
//重新将修改后的结果打印出来
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
ERR_EXIT("getrlimit");
printf("%d\n", (int)rl.rlim_max);
return 0;
}
运行结果:
好啦,此刻我们已然修改成功。。。。。。。先是获取原来的值,后来再重新设置。。。。。。。
但是,但是,我们此刻更改的仅仅是当前进程的最大文件描述符的限制,并不影响其他进程的最大描述符,如:我们的
父进程的最大描述符为:
那么,我们为什么要改变文件描述符的限制呢??????
因为,我们在处理多个客户端和服务端进行通信的话,每创建一个客户端,这个socket都会占用一个文件描述符。。。
所以,我们要达到的并发数也就收到了相应的控制。。。
而关于第二点:select中fd_set集合容量的参数(需要重新编译内核),它的限制是:FD_SETSIZE(1024)是一个宏,、
在一个头文件中定义过了,,,如果我们要更改的话,那么我们应该找到这个头文件,并且对里面的宏进行更改。。。
并且要重新编译内核。。。。。。。
我们不能通过调整内核参数来修改fd_set集合的容量,通常这也是不被允许的。
我们可以做一个测试:将无穷多个客户端连接服务器
客户端程序:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <signal.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
int main()
{
int count = 0;
while(1)
{
int sock;
if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr; //IPv4地址socket结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; //协议族
servaddr.sin_port = htons(5188); //端口号,16位
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//上面为服务器端的地址
if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect");
//connect完成的话,那么就可以用于通信了,sock就是已连接套接字
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
ERR_EXIT("GETSOCKNAME");
printf("ip = %s port = %d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
printf("%d\n" , ++count);
}
return 0;
}
//通过while循环创建更多的客户端去连接服务端
服务端:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
struct packet
{
int len;
char buf[1024];
};
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;
while(nleft > 0)
{
if((nread = read(fd, bufp, nleft)) < 0)
{
if(errno == EINTR) //被信号中断了
continue;
return -1;
}
else if(nread == 0)
{
return count - nleft;
//对等方关闭了
break;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;
while(nleft > 0)
{
if((nwritten = write(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nwritten == 0)
continue; //对于write操作,也像什么都没有发生过一样
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while(1)
{
int ret = recv(sockfd, buf, len,MSG_PEEK);
if(ret == -1 && errno == EINTR)
continue;
return ret;
}
}
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = buf;
int nleft = maxline;
while(1)
{
ret = recv_peek(sockfd, bufp, nleft);
if(ret < 0)
return ret;
else if(ret == 0)
return ret;
nread = ret;
int i;
for(i = 0; i< nread; i++)
{
if(bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+1);
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
if(nread > nleft)
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if(ret != nread)
exit(EXIT_FAILURE);
bufp += nread;
}
return -1;
}
void echo_serv(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = readline(conn, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("READLine");
if(ret == 0)
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
void handle_sigchld(int sig)
{
// wait(NULL); //捕获子进程的退出状态
while(waitpid(-1, NULL, WNOHANG) > 0);
}
void handle_sigpipe(int sig)
{
printf("recv a sig = %d\n", sig);
}
int main()
{
// signal(SIGPIPE, SIG_IGN); //关于底下close,以及sleep产生的sigpipe信号
signal(SIGPIPE, handle_sigpipe);
// signal(SIGCHLD, SIG_IGN); //设置信号,处理僵尸进程,说明通过设置SIGCHLD,可以忽略僵尸进程
signal(SIGCHLD, handle_sigchld);
int listenfd;
if((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// if((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr; //IPv4地址socket结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; //协议族
servaddr.sin_port = htons(5188); //端口号,16位
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址,其实:INADDR_ANY都是全0的,所以不管如何转换都是不变的,所以说:htonl是可以省略的
/*
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");
//设置地址的重复利用
if(bind(listenfd, (struct sockaddr*)(&servaddr), sizeof(servaddr)) < 0) //bind函数调用成功则为0,若出错则为-1
ERR_EXIT("bind");
//如上所示,将socket和地址进行绑定
if(listen(listenfd, SOMAXCONN) < 0) //SOMAXCONN表示队列的最大长度,,,调用该函数之后,就将套接字变成了被动的,一般来说:默认为主动
{
ERR_EXIT("listen");
}
//如上所示:将其变成了被动的,不再处于监听的状态
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn;
/*
pid_t pid;
while(1)
{
if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0) //对等方的地址以及长度
ERR_EXIT("accept");
//conn为以连接套接字,是一个主动套接字
printf("ip = %s :port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
pid = fork();
if(pid == -1)
ERR_EXIT("fork");
if(pid == 0) //子进程所执行的就是一个简单的通信
{
close(listenfd);
echo_serv(conn);
close(conn);
exit(EXIT_SUCCESS);
}
else //父进程所执行就是一个不断的新的客户端接受连接的过程
{
close(conn);
}
}
close(conn);
close(listenfd);
*/
int i;
int client[FD_SETSIZE]; //防止accept的返回值conn产生重复的覆盖
for(i = 0; i< FD_SETSIZE; i++)
client[i] = -1; //-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");
}
if(nready == 0)
continue; //实际上,是不会发生这件事的,只是为了程序的完整性而已。。。。
//接下来,返回其他的,表示connect继续的三次握手已经完成,已完成队列不为空了,那么accept将不会进行阻塞了
if(FD_ISSET(listenfd, &rset))
{
peerlen = sizeof(peeraddr); //调用的时候一定要进行简单的初始化
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn == -1)
ERR_EXIT("accept");
for(i = 0; i< FD_SETSIZE; i++)
{
if(client[i] < 0) //找到空闲
{
client[i] = conn;
break;
}
}
if(i == FD_SETSIZE)
{
fprintf(stderr, "too many clients\n"); //说明连接的客户端已经达到上限
exit(EXIT_FAILURE);
}
printf("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< FD_SETSIZE; 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");
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;
}
运行结果:
可以看到:我们的客户端显示我们创建了1021个客户端。。。。。。。。。
那么,为什么是1021个呢????
我们创建1022个客户端的时候就出错了,原因只能是我们的客户端的个数达到了上限。。。。原本是1024个啊,为什么
到了这里我们的变成了1022就出错呢???是因为我们的0,1,2被占用着。。。
再次看看:我们向服务端也同样的加入变量,那么产生了多少个呢???
可见,服务端也产生了1022个,恩呢,这里产生的已经不准确了,0(标准输入),1(标准输出),2(错误输出)被
占用着,而3也被用作监听了,所以合理的话应该是1020个啊,,,,
显示1021个主要原因是:
客户端而言:会发起1021个连接,但是在创建第1022个套接字的时候失败了,对于服务器端而言,如果我们收到连接
那么,我们会在已完成的队列中维护1021个条目,,,此刻我们在队列中已经处理剩下了300个,此刻1022个套接字
失败,失败了之后那么会发送很多的FIN分节。。。而这个时候,服务端还不是所有的都建立连接,所以会收到FIN
,会打印client close,如上图。。。而已完成的这些队列就会被accept成功,所以我们可以看到accept打印和client打印
混在一起了,,,,,,,,,,,
当前的套接口关闭了,所以它能收到1021个连接,还应该注意的是:此刻的1021并不代表并发数,因为有好多的客户端
都已经关闭了。
那么这个问题应该如何避免呢????
1,我们可以加入延时,客户端调用close之前,如:
<span style="color:#000000;"> int sock;
if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
{
sleep(4);
ERR_EXIT("socket");
}</span>
运行结果:
嗯嗯,这样就好啦,也不会close和accept同时产生了,可以达到1020(服务端),1021(客户端)
1,继续讨论关于第二点:FD_SETSIZE......
printf("ip = %s, port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
printf("%d\n", ++count);
FD_SET(conn , &allset);
可以看到,我们每建立完成一次就将套接字加入到allset,当然这里的allset也是有限制的。我们添加的文件描述符的个数
不能超过集合的容量。。。。。
所以说:一个select实现高并发的时候受到两个体条件的限制:1,一个进程所能打开的最大文件描述符
2,一个是集合的限制(FD_SETSIZE)
那么为了避免如上的两个问题,我们有引入了poll函数,它没有FD_SETSIZE的限制。。。。。。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//fds是一个套接口或者一个事件,是一个指针,通常指向一个数组。。。。。
//nfds数组当中的个数,也就是我们要检测的I/O的个数。
//timeout超时时间
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
程序:
pollserv(poll函数实现的服务端)
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <poll.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
struct packet
{
int len;
char buf[1024];
};
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;
while(nleft > 0)
{
if((nread = read(fd, bufp, nleft)) < 0)
{
if(errno == EINTR) //被信号中断了
continue;
return -1;
}
else if(nread == 0)
{
return count - nleft;
//对等方关闭了
break;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;
while(nleft > 0)
{
if((nwritten = write(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nwritten == 0)
continue; //对于write操作,也像什么都没有发生过一样
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while(1)
{
int ret = recv(sockfd, buf, len,MSG_PEEK);
if(ret == -1 && errno == EINTR)
continue;
return ret;
}
}
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = buf;
int nleft = maxline;
while(1)
{
ret = recv_peek(sockfd, bufp, nleft);
if(ret < 0)
return ret;
else if(ret == 0)
return ret;
nread = ret;
int i;
for(i = 0; i< nread; i++)
{
if(bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+1);
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
if(nread > nleft)
{
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if(ret != nread)
exit(EXIT_FAILURE);
bufp += nread;
}
return -1;
}
void echo_serv(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = readline(conn, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("READLine");
if(ret == 0)
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
void handle_sigchld(int sig)
{
// wait(NULL); //捕获子进程的退出状态
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main()
{
// signal(SIGCHLD, SIG_IGN); //设置信号,处理僵尸进程,说明通过设置SIGCHLD,可以忽略僵尸进程
signal(SIGCHLD, handle_sigchld);
int listenfd;
if((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// if((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr; //IPv4地址socket结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; //协议族
servaddr.sin_port = htons(5188); //端口号,16位
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址,其实:INADDR_ANY都是全0的,所以不管如何转换都是不变的,所以说:htonl是可以省略的
/*
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");
//设置地址的重复利用
if(bind(listenfd, (struct sockaddr*)(&servaddr), sizeof(servaddr)) < 0) //bind函数调用成功则为0,若出错则为-1
ERR_EXIT("bind");
//如上所示,将socket和地址进行绑定
if(listen(listenfd, SOMAXCONN) < 0) //SOMAXCONN表示队列的最大长度,,,调用该函数之后,就将套接字变成了被动的,一般来说:默认为主动
{
ERR_EXIT("listen");
}
//如上所示:将其变成了被动的,不再处于监听的状态
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn;
/*
pid_t pid;
while(1)
{
if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0) //对等方的地址以及长度
ERR_EXIT("accept");
//conn为以连接套接字,是一个主动套接字
printf("ip = %s :port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
pid = fork();
if(pid == -1)
ERR_EXIT("fork");
if(pid == 0) //子进程所执行的就是一个简单的通信
{
close(listenfd);
echo_serv(conn);
close(conn);
exit(EXIT_SUCCESS);
}
else //父进程所执行就是一个不断的新的客户端接受连接的过程
{
close(conn);
}
}
close(conn);
close(listenfd);
*/
int i;
struct pollfd client[2048]; //防止accept的返回值conn产生重复的覆盖
for(i = 0; i< 2048; i++)
client[i].fd = -1; //-1表示空闲的,表示能够容纳新的客户端
int nready;
int maxi = 0;
client[0].fd = listenfd; //监听套接口
client[0].events = POLLIN;//表示对可读事件感兴趣。
int count = 0;
while(1)
{
nready = poll(client, maxi + 1, -1);//-1表示timeout永远等待
//也就是说:只有事件到啦的时候才会进行返回。。。
if(nready == -1)
{
if(errno == EINTR) //被信号中断,可以继续去循环
continue;
ERR_EXIT("select");
}
if(nready == 0)
continue; //实际上,是不会发生这件事的,只是为了程序的完整性而已。。。。
//接下来,返回其他的,表示connect继续的三次握手已经完成,已完成队列不为空了,那么accept将不会进行阻塞了
if(client[0].revents & POLLIN) //产生了可读事件,可以接受连接了
{
peerlen = sizeof(peeraddr); //调用的时候一定要进行简单的初始化
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn == -1)
ERR_EXIT("accept");
for(i = 0; i< 2048; i++)
{
if(client[i].fd < 0) //找到空闲
{
client[i].fd = conn;
break;
}
}
if(i == 2048)
{
fprintf(stderr, "too many clients\n"); //说明连接的客户端已经达到上限
exit(EXIT_FAILURE);
}
printf("ip = %s, port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
printf("%d\n", ++count;);
client[i].events = POLLIN;
if(--nready <= 0)
continue;
}
//这里是对已完成的套接字进行继续检测
for(i = 1; i< FD_SETSIZE; i++)
{
conn = client[i].fd;
if(conn == -1)
continue;
if(client[i].events & POLLIN)
{
char recvbuf[1024] = {0};
int ret = readline(conn, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("readline");
if(ret == 0)
{
printf("client close\n");
client[i].fd = -1;
close(conn);
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
if(--nready <= 0)
break;
}
}
}
return 0;
}
测试端contest(客户端):
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <signal.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
int main()
{
int count = 0;
while(1)
{
int sock;
if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
{
sleep(4);
ERR_EXIT("socket");
}
struct sockaddr_in servaddr; //IPv4地址socket结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; //协议族
servaddr.sin_port = htons(5188); //端口号,16位
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//上面为服务器端的地址
if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect");
//connect完成的话,那么就可以用于通信了,sock就是已连接套接字
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
ERR_EXIT("GETSOCKNAME");
printf("ip = %s port = %d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
printf("%d\n" , ++count);
}
return 0;
}
运行结果:
这时候,为了更多的客户进程连接过来,我们可以改改文件描述符的限制:
将两个进程的最大文件描述符都设置为2048:
继续运行,看我们的结果:
可以看到出来,此时,我们已经突破了1024的限制了。
而对于我们经常用到的来说:POLLIN(有文件描述符可读),POLLPRI(可读的紧急数据),POLLOUT(有文件描述
符可写)
本文大部分内容摘自:UNIX网络编程