Linux版本:红帽企业版 5.5
1.概述:在之前的“Linux上使用套接字(socket)来发送信息”的文章中我们讲了套接字的相关相关的属性和相关的函数,但是我们在之前的文章中写的服务端的实例写的都是阻塞模式的套接字,而且我们之前编写的套接字的实例只能使用于单个套接字的收发,并不能适用于多个套接字的连接
2.什么是阻塞模式的套接字:要知道我们在创建一个服务端的套接字之后,我们要使用这个套接字来等待客户端的套接字的连接要使用到accept()函数,这个函数在没有客户端连接的时候会一直阻塞,阻塞的意思就是说在内存中运行的这个程序一直停滞在accept()函数上,直到有客户端的套接字连接过来才会让程序继续向下执行,而且会阻塞程序向下运行的不止是accept()函数,还有recv()函数,和send()函数同样会阻塞函数的运行,这些阻塞函数会使得系统不断的去扫描我们所建立的套接字是否有连接,或者是否有数据可以读写,这样让系统不断去扫描意味着你这个程序的进程会一直占用着CPU的资源,是的CPU不得不消耗大量的资源来运行这个由阻塞函数的进程相对的其他的进程占用的CPU资源就会变少,当然如果我们的系统中只有这样一个有着阻塞模式的程序还好,但是多了的话就对系统资源是一种巨大的消耗
3.什么是非阻塞模式的套接字:和上面的的阻塞模式的套接字刚好相反,非阻塞套接字就是使用相关函数设置套接字文件描述符的属性,从而达到非阻塞,非阻塞就是说当套接字没有连接或是没有数据可以读写的时候accept()函数,或是send()函数和recv()函数不会阻塞而让系统一直去扫描我们创建的套接字的文件描述符是否有状态的变化,而是是的函数直接返回,继续运行,这种设计的模式就会是的CPU消耗的资源相对较少
4套接字客户端的几种设计模式:
<1>多进程的阻塞/非阻塞套接字客户端:我们知道,在服务端建立套接字的时候,我们首先要建立一个本地的套接字,再使用listen()函数来创建监听队列,之后再使用accept()函数来接受客户端的连接,当有客户端连接的时候accept()函数会创建一个和本地套接字完全不一样的套接字,来表示客户端和本地服务端的套接字的连接成功之后的套接字,当我我们接受到这个新的套接字之后就会使用fork()函数来创建一个进程来处理连接好的套接字,这个就是多进程套接字,主进程负责等待客户端的连接而子进程负责处理连接好的套接字
<2>多线程的阻塞/非阻塞套接字客户端:和多进程类似,只不过我们接收到客户端的连接之后,accept()函数产生了新的套接字之后不是使用开新的进程的方式来处理,而是使用开一个线程来处理新的连接,当然开新的线程的代价要比开启一个子进程的代价小的多,总体而言就是,整个程序运行的时候只有一个进程,主线程负责接收客户端的连接,子线程负责处理连接完成的套接字,实现多线程有两种方式,一个是自己创建线程,一个就是使用线程池
<3>单进程单线程的阻塞/非阻塞的套接字客户端:顾名思义这个设计客户端的方式就是在整个客户端的程序中只有一个进程,而且我们也不会去开子线程来处理客户端的连接,这种设计方式一些相关的函数,但是你要注意因为整个程序只有一个进程我们在处理一个客户端的连接上不能花去太多的时间,以防影响到下一个客户端的的逻辑处理
5.相关函数和结构体介绍:
(1)#include
#include
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
函数功能:首先readfs,writefds,errorfds,是三个文件描述符集合,select函数的作用就是监控这三个文件描述符集合中的文件描述符的变化,如果说着三个集合中的文件描述符中有文件描述符的状态变为了可读,可写,出现错误,那么这个函数就会立即返回,在返回的时候select函数将会把有文件描述符变化的文件描述符集合中的其他没有变化的文件描述符清空
参数:1.nfds:这个参数是要监控的文件描述符的总数目,select函数要监控的文件描述符的范围是0~nfds-1
2.readfds:这是一个文件描述符的集合,在这个文件描述符集合中保存的都是我们认为可能发生状态变化为读的文件描述符,当然这些文件描述符要我们自己添加
3.writefds:这是一个文件描述符的集合,在这个文件描述符集合中保存的都是我们认为可能发生状态变化为写的文件描述符,当然这些文件描述符要我们自己添加
4.errorfds:这是一个文件描述符的集合,在这个文件描述符集合中保存的都是我们认为可能发生状态变化为错误的文件描述符,当然这些文件描述符要我们自己添加
5.timeout:这是一个时间结构体,select函数在timeout指定的时间后返回,如果timeout结构体为空,那么select函数将一直阻塞,直到有文件描述符发生变化促使select返回
返回值:1.返回值>0:表示已经有文件描述符准备好了
2.返回值=0:表示超时
3.返回值<0:失败的时候返回-1,并且设置errno
错误代码:
EBADF:无效的文件描述符
EINTR:因中断而返回
EINVAL:nfds或timeout取值错误
相关介绍:
1.fd_set结构体:
typedef struct fd_set
{
u_int fd_count;
socket fd_array[FD_SETSIZE];
}fd_set;
2.timeval结构体:
struct timeval
{
__time_t tv_sec; /*秒*/
__suseconds_t tv_usec; /*分秒*/
};
(2)#include
fcntl(int fd, int cmd, long arg)
函数功能:这个函数的作用就是根据文件描述符来改变文件的相关属性
参数:1.fd:文件描述符
2.cmd:我们对文件描述符操作的相关命令,这个命令相当的多我们这里只介绍一个:F_SETFL(这个命令表示我们要为文件描述符指向的文件设置属性)
3.arg:这个是我们设置的命令对应的相关的参数
返回值:1.失败:这个函数返回-1
2.成功:这个函数返回一个其他值
相关例子:(这个例子用来设置套接字文件描述符为非阻塞模式)
int flags = fcntl(socket, F_GETFL, 0); //0表示没有参数,F_GETFL表示得到文件描述符socket指向的文件的状态
fcntl(socket, F_SETFL, O_NONBLOCK|flags); //表示使用fcntl()函数,F_SETFL表示为文件描述符socket指向的文件设置新的属性,新的属性为O_NONBLOCK|flags(表示在原来的属性上添加类O_NONBLOCK非阻塞)设置了非阻塞之后send()或是recv()或是write()和read(),accept()来操作这个文件描述符指向的文件都将直接返回不阻塞
(3)#include
#include
下面将介绍一些宏函数,这些宏函数用来操作文件描述符集合
1.void FD_ZERO(fd_set* fdset); //将文件描述符集合fdset设置为空
2.void FD_CLR(int fd, fd_set* fdset); //这个函数的作用是用来清除文件描述符集合fdset中的fd文件描述符
3.void FD_SET(int fd, fd_set* fdset); //这个函数的作用是将fd这个文件描述符添加到fdset这个文件描述符集合中
4.int FD_ISSET(int fd, fd_set* fdset); //这个函数的作用是判断fd这文件描述符是否存在于fdset这个文件描述符集合中,存在返回非0值,不存在返回0
6.相关代码实例
在这里我们的要编写的就是一个单进程单线程的套接字客户端,而且要求是套接字的收发为非阻塞模式。
其实我们的设计思路十分简单,就是依靠select()函数,首先我们先将创建好的客户端的文件描述符添加到一个文件描述符集合中,在将这个集合放到select函数中
监控,当我们创建的套接字有了客户端的连接,那么select函数就会返回那个文件描述符,之后我们再将文件描述符放回到select中的readfd这个文件描述符中,因
为客户端一般都是接受客户端发过来数据,当我们放入的已经建立好连接的套接字要传输数据的时候,我们select函数又会返回它,我们到时候就再次处理就可以
/***************************************************************************
*文件名称:select_socket.c
*
*文件作用:这个文件的作用是用来编写一个单进程,单线程的多客户的套接字的
*客户端
****************************************************************************/
#include //引入stdio.h头文件,这个头文件中包含了Linux上相关的IO流操作函数
#include //引入stdlib.h头文件,这个头文件中包含了Linux下的一些标准的函数
#include //引入string,h头文件,这个头文件中包含了字符串操作的相关函数
#include //引入errno.h头文件,这个头文件中包含了错误处理的相关函数
#include //引入sys/目录下面的types.h头文件,这个头文件中包含了相关类型的定义
#include //引入sys/目录下面的socket.h头文件,这个头文件中包含了套接字相关的头文件
#include //引入sys/目录下面的time.h头文件,这个头文件中包含了时间操作的相关函数和相关的结构体
#include //引入netinet.h/目录下面的in.h头文件,这个头文件中包含了网络字节序列转换的相关函数,还有相关的网域的宏定义
int main(int argc, char* argv[])
{
//开始定义要使用的相关变量-------------------------
int ret; //定义一个int类型的数据,这个int类型的数据用来保存函数的运行结果
int server_socket; //定义一个int类型的数据用来保存本地建立的服务端的套接字
int conn_socket; //定义一个int类型的数据用来保存客户端和本地连接好的套接字
char RecvBuffer[1024]; //定义一个char类型的字符串,这个字符串用来保存从客户端接收到的数据
char SendBuffer[1024]; //定义一个char类型的字符串,这个字符串用来保存要向客户端发送的数据
struct sockaddr_in server_address; //定义一个sockaddr_in类型的结构体,这个结构体来保存你服务端的相关地址
struct sockaddr_in conn_address; //定义一个sockaddr_in类型的结构体,这个结构体用来保存连接服务端之后的客户端的地址信息
fd_set readfds, readcpyfds; //定义两个文件描述集合
//将相关的变量进行初始化---------------------------
memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
memset(SendBuffer, 0x00, sizeof(SendBuffer));
memset(&server_address, 0x00, sizeof(server_address));
memset(&conn_address, 0x00, sizeof(conn_address));
//开始命名套接字,并且建立监听队列---------------
server_socket = socket(AF_INET, SOCK_STREAM, 0); //创建一个因特网下的流套接字,协议默认定义
server_address.sin_family = AF_INET; //地址使用的协议簇为AF_INET
server_address.sin_addr.s_addr = htonl(INADDR_ANY); //定义这个服务端的套接字的可以接受的地址为任意地址,并且使用htonl将地址转化为网络字节序列
server_address.sin_port = htons(12580); //将端口转化为网络字节序列,监听9743端口
bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)); //使用bind函数为套接字来进行命名
listen(server_socket, 10); //为我们创建的本地套接字创建监听队列,这个队列长为10个连接数
//开始操作文件描述符集合---------------------------
FD_ZERO(&readfds); //使用FD_ZERO()函数将readfds文件描述符集合清空
FD_SET(server_socket, &readfds); //将server_socket这个文件描述符添加到readfds
//开始使用while()循环来接收连接--------------------
while(1)
{
readcpyfds = readfds; //将readfds进行一次备份
/*使用select()函数来监控文件描述符集合readcpyfds,如果这个集合中的任意一个文件描述符变为可写的状态select就返回,
*因为我们不需要可写和错误的文件描述符,就直接将2,3参数设置为0(0表示空,还进行了强制类型转换),同时我们将
*时间设置为空,表示select将阻塞*/
ret = select(FD_SETSIZE, &readcpyfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0);
if(ret < 1)
{
fprintf(stderr, "select error!\n"); //输出错误信息
exit(EXIT_FAILURE); //进程失败退出
}
int fd; //定义一个整形变量,这个整形变量用来控制循环
for(fd = 0; fd < FD_SETSIZE; fd++) //使用for循环来对文件描述符进行检索和操作,FD_SETSIZE为文件描述符集合的最大容量
{
if(FD_ISSET(fd, &readcpyfds)) //如果fd为testfds中设置的文件描述符
{
if(fd == server_socket) //如果fd == server_socket,表示是有客户端要连接服务端
{
int size = sizeof(conn_address); //得到结构体的大小
conn_socket = accept(server_socket, (struct sockaddr*)&conn_address, &size); //使用accept()函数来接受客户端传来的连接,将客户端连接的地址的信息保存到conn_address中
FD_SET(conn_socket, &readcpyfds); //将conn_sockfd这个已经连接好的套接字描述符保存到文件描述符集合中readcpyfds这个集合中
printf("We have recved a connection!\n"); //输出信息
}
else //如果不是server_socket那就表示是已经创建好连接的套接字字符,我们开始接受客户端传过来的数据,再将数据传回去
{
ret = recv(fd, RecvBuffer, sizeof(RecvBuffer), 0); //使用recv()函数将把从建立好的连接的数据保存到RecvBuffer中
if(ret < 0)
{
fprintf(stderr, "recv error!, The error is %s, errno is %d\n", strerror(errno), errno);
exit(EXIT_FAILURE); //进程异常退出
}
else if(ret == 0) //ret为0表示超时
{
printf("recv() time over!\n"); //输出超时
exit(EXIT_FAILURE); //进程异常退出
}
printf("The data we recve is %s\n", RecvBuffer); //输出我们接收到的数据
char* ptr; //定义一个char*指针,这个指针这个指针用来转化大小写
for(ptr = RecvBuffer; *ptr; ptr++) //将字符串中的数据转化为大写
{
*ptr = toupper(*ptr); //将小写转化为大写
}
strcpy(SendBuffer, RecvBuffer); //将RecvBuffer中的数据复制到SendBuffer中
send(fd, SendBuffer, strlen(SendBuffer), 0); //将SendBuffer中的数据发送回fd这个建立好连接的套接字中
FD_CLR(fd, &readfds); //将readfds中的fd这个文件描述符清除掉,以防在副本中出现多余的文件描述符
memset(RecvBuffer, 0x00, sizeof(RecvBuffer)); //将接受数据的字符串和发送数据的字符串清空
memset(SendBuffer, 0x00, sizeof(SendBuffer));
close(fd); //关闭套接字的连接
}
}
}
}
}
整个实例中只有select()是阻塞的,但是我们避免了accept()和recv()和send()的阻塞