言之者无罪,闻之者足以戒。 - “诗序”
1、阻塞式I/O
下面看一下实现的逻辑:
2、非阻塞式I/O
下面看一下实现的逻辑:
3、I/O复用(select/epoll)
(1) int select (int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout)
第一个参数maxfdp :是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!
第二个参数readfds : 是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
第三个参数writefds :是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
第四个参数errorfds : 同上面两个参数的意图,用来监视文件是否发生错误异常。
第五个参数timeout : 是select的超时时间,这个参数至关重要。它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于永久阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来返回正值,超时返回0。
返回值:负值:select错误,正值:某些文件可读写或出错,0:等待超时,没有可读写或错误的文件。
(2)四个操作描述字的宏定义
FD_ZERO(&set); /* 将set清零 */
FD_SET(fd, &set); /* 将fd加入set */
FD_CLR(fd, &set); /* 将fd从set中清除 */
FD_ISSET(fd, &set); /* 如果fd在set中则真 */
关于FD_ISSET多说一句,select返回时会将没有准备就绪的文件描述符从set中清除,所以FD_ISSET(fd, &set)判断fd是否在set中,如果在说明他没有被清除,该描述符的状态发生了变化(可读、可写或者异常)。
下面看一下逻辑:
这里给出一张程序的逻辑图:
下面来看一下服务器程序的代码:
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 8888
#define MAX_LISTEN_QUE 5
#define MAX_BUFFER_SIZE 1024
#define RT_ERR (-1)
#define RT_OK 0
//创建套接字的函数
int Ipv4_tcp_create_socket(void)
{
int listenfd,sockfd,opt=1;
struct sockaddr_in server,client;
socklen_t len;
int temp;
int ret;
//创建套接字
listenfd = socket(AF_INET,SOCK_STREAM,0);//ipv4,全双工
if(listenfd < 0)
{
perror("Create socket fail\n");
return RT_ERR;
}
//设置地址重用
if((ret = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))) < 0)
{
perror("Error , set socket reuse addr failued\n");
return RT_ERR;
}
//初始化服务器端
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(SERV_PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);//连接所有的客户端
len = sizeof(struct sockaddr);
if(bind(listenfd,(struct sockaddr *)&server,len) < 0){
perror("bind error\n");
return RT_ERR;
}
listen(listenfd,MAX_LISTEN_QUE);
return listenfd;
}
int main(int argc,char *argv[])
{
int listenfd,sockfd;
struct sockaddr_in server,client;
socklen_t len;
int bytes = 0;
fd_set g_rdfs,cur_rdfs;
int maxfd;
int i;
char buf[MAX_BUFFER_SIZE];
len = sizeof(struct sockaddr_in);
//调用创建套接字函数
listenfd = Ipv4_tcp_create_socket();
//将c_rdfs清零
FD_ZERO(&g_rdfs);
//将listendfd添加进c_rdfs中
FD_SET(listenfd,&g_rdfs);
maxfd = listenfd;
while(1)
{
cur_rdfs = g_rdfs;
//监控套接字
if(select(maxfd + 1,&cur_rdfs,NULL,NULL,NULL) < 0){
perror("select error\n");
return RT_ERR;
}
for(i=0;i <= maxfd;i++){
//判断i是否在cur_rdfs中
if(FD_ISSET(i,&cur_rdfs)){
//判断是不是我们监听的套接字
if(listenfd == i){
//接收套接字(返回的是通信套接字)
if((sockfd = accept(listenfd,(struct sockaddr*)&client,(socklen_t*)&len)) < 0){
perror("accept error\n");
return RT_ERR;
}
printf("sockfd:%d\n",sockfd);
//清除我们添加进去的套接字,以防下次循环再次检测到
FD_CLR(i,&cur_rdfs);
//得到最大的套接字的个数
maxfd = maxfd > sockfd ? maxfd : sockfd;
//将通信套接字加入关注的套接字集
FD_SET(sockfd,&g_rdfs);
//如果不是监听套接字
}else{
printf("read socket :%d\n",i);
//如果不是监听套接字我就直接读取数据
bytes = recv(i,buf,MAX_BUFFER_SIZE,0);
if(bytes < 0){
perror("recv error\n");
return RT_ERR;
}
if(bytes == 0){
//客户端退出,从我关注的套接字集中把它清掉
FD_CLR(i,&g_rdfs);
//关闭套接字
close(i);
continue;
}
//打印读取到的内容
printf("buf:%s\n",buf);
//把客户端发送的数据,发送给客户端
send(i,buf,strlen(buf),0);
}
}
}
}
}
有的朋友可能会问怎么这两篇文章都没有客户端的程序,因为这两篇文章我是用windows下的命令提示符来完成和Linux编写的服务器程序通信的。所以没有给出客户端的代码。
直接找到windows下的命令提示符窗口,输入:telnet 192.168.177.128 8888回车就好了。(192.168.177.128是地址,8888是端口号)
注意:先运行服务器程序在连接。
上面程序的代码还是有很多不足的,有很多地方可以优化,优化代码是每一个程序员都应该思考的问题,我自己也想着优化了一下,下面直接粘贴出代码:
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 8888
#define MAX_LISTEN_QUE 5
#define MAX_BUFFER_SIZE 1024
#define RT_ERR (-1)
#define RT_OK 0
//创建套接字的函数
int Ipv4_tcp_create_socket(void)
{
int listenfd,sockfd,opt=1;
struct sockaddr_in server,client;
socklen_t len;
int temp;
int ret;
//创建套接字
listenfd = socket(AF_INET,SOCK_STREAM,0);//ipv4,全双工
if(listenfd < 0)
{
perror("Create socket fail\n");
return RT_ERR;
}
//设置地址重用
if((ret = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))) < 0)
{
perror("Error , set socket reuse addr failued\n");
return RT_ERR;
}
//初始化服务器端
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(SERV_PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);//连接所有的客户端
len = sizeof(struct sockaddr);
if(bind(listenfd,(struct sockaddr *)&server,len) < 0){
perror("bind error\n");
return RT_ERR;
}
listen(listenfd,MAX_LISTEN_QUE);
return listenfd;
}
int main(int argc,char *argv[])
{
int listenfd,sockfd;
struct sockaddr_in server,client;
socklen_t len;
int bytes = 0;
fd_set g_rdfs,cur_rdfs;
int maxfd;
int i;
char buf[MAX_BUFFER_SIZE];
//获得套接字的极限值
int client_fd[FD_SETSIZE];
printf("FD_SETSIZE:%d\n",FD_SETSIZE);
len = sizeof(struct sockaddr_in);
//调用创建套接字函数
listenfd = Ipv4_tcp_create_socket();
//将c_rdfs清零
FD_ZERO(&g_rdfs);
//将listendfd添加进c_rdfs中
FD_SET(listenfd,&g_rdfs);
maxfd = listenfd;
//赋初值
for(i = 0;i < FD_SETSIZE; i++)
{
client_fd[i] = -1;
}
while(1)
{
cur_rdfs = g_rdfs;
//监控套接字
if(select(maxfd + 1,&cur_rdfs,NULL,NULL,NULL) < 0){
perror("select error\n");
return RT_ERR;
}
//判断监听套接字是否在检测集中
if(FD_ISSET(listenfd,&cur_rdfs)){
//接收套接字(返回的是通信套接字)
if((sockfd = accept(listenfd,(struct sockaddr*)&client,(socklen_t*)&len)) < 0){
perror("accept error\n");
return RT_ERR;
}
printf("sockfd:%d\n",sockfd);
//清除我们添加进去的套接字,以防下次循环再次检测到
FD_CLR(listenfd,&cur_rdfs);
//得到最大的套接字的个数
maxfd = maxfd > sockfd ? maxfd : sockfd;
//将通信套接字加入关注的套接字集
FD_SET(sockfd,&g_rdfs);
//下面的for语句整体的作用是找到我们的通信套接字并将它的值赋值给存储套接字的数组
//(大家都应该知道前三个套接字都不是我们的通信套接字,第四个才是)
for(i = 0; i < maxfd; i++){
if(-1 == client_fd[i]){
client_fd[i] = sockfd;
break;
}
}
}
for(i=0;i <= maxfd;i++){
if(-1 == client_fd[i]){
continue;
}
//判断cur_rdfs是不是在我们关注的读写套接字中
if(FD_ISSET(client_fd[i],&cur_rdfs)){
printf("read socket :%d\n",client_fd[i]);
//如果不是监听套接字我就直接读取数据
bytes = recv(client_fd[i],buf,MAX_BUFFER_SIZE,0);
if(bytes < 0){
perror("recv error\n");
return RT_ERR;
}
if(bytes == 0){
//客户端退出,从我关注的套接字集中把它清掉
FD_CLR(client_fd[i],&g_rdfs);
//关闭套接字
close(client_fd[i]);
client_fd[i] = -1;
continue;
}
//打印读取到的内容
printf("buf:%s\n",buf);
//把客户端发送的数据,发送给客户端
send(client_fd[i],buf,strlen(buf),0);
}
}
}
}