功能
有了套接字我们便可以在客户端和服务器之间传递信息,由send和recv函数来发送和接收文件,而我们不能提前知道客户端或服务器什么时候发送信息,如果客户端比较多,还要判断是哪一个客户端发来的信息。这让我们需要类似于监听套接字一样的功能(有连接就返回),select, poll, epoll都提供了这些功能,当有信息发送就返回有信息的套接字描述符,然后用recv接收信息。
select
select介绍
在客户端/服务器模型中,服务器端需要同时处理多个客户端的连接请求,此时就需要使用多路复用。实现多路复用最简单的方法是采用非阻塞方式套接字(阻塞与非阻塞),服务器端不断地查询各个套接字的状态,如果有数据到达则读出数据,如果没有数据则查看下-一个套接字。这种方法虽然简单,但在轮询过程中浪费了大量的CPU时间,效率非常低。另一种方法是服务器进程并不主动地询问套接字状态,而是向系统登记希望监视的套接字,然后阻塞。当套接字上有事件发生时(如有数据到达),系统通知服务器进程告知哪个套接字上发生了什么事件,服务器进程查询对应套接字并进行处理。在这种工作方式下,套接字上没有事件发生时,服务器进程不会去查询套接字的状态,从而不会浪费CPU时间,提高了效率。使用函数select可以实现第二二种多路复用,在Shell下输入“man select"可获得该函数的原型:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数n是需要监视的文件描述符数,要监视的文件描述符值为0~n-1.参数readfds指定需要监视的可读文件描述符集合,当这个集合中的一个描述符上有数据到达时,系统将通知调用select函数的程序。参数writefds指定需要监视的可写文件描述符集合,当这个集合中的某个描述符可以发送数据时,程序将收到通知。参数exceptfds指定需要监视的异常文件描述符集合,当该集合中的一个描述符发生异常时,程序将收到通知。参数timeout指定了阻塞的时间,如果在这段时间内监视的文件描述符上都没有事件发生,则函数select)将返回0
struct timeval{
long tv_sec;
long tv_usec;
}
成员tv_ sec指定秒数,tv_ usec指定微秒数。如果将timeout设为NULL,则函数select0)将一直被阻塞,直到某个文件描述符上发生了事件。如果将timeout设为0,则此时相当于非阻塞方式,函数select(查询完文件描述符集合的状态后立即返回。如果将timeout设成某-一时间值, 在这个时间内如果没有事件发生,函数select(将返回;如果在这段时间内有事件发生,程序将收到通知。
注意:这里的文件描述符既可以是普通文件的描述符,也可以是套接字描述符。
FD_CLR(int fd, fd_set *set); //将文件描述符fd从文件描述符集合set中删除
FD_ISSET(int fd, fd_set *set); //测试fd是否在set中
FD_SET(int fd, fd_set *set); //在文件描述符集合set中增加文件描述符fd
FD_ZERO(fd_set *set); //将文件描述符集合set清空
如果select()设定的要监视的文件描述符集合中有描述符发生了事件,则select将返回发生事件的文件描述符的个数。
select应用
服务器:server.c
客户端:client.c
server.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h> //atoi()
int get_max_socket_fd(int socket_arr[]);//获取最大的通信socket描述符。
int main(int argc, char *argv[])
{
//判断命令行参数是否满足
if(argc != 2)
{
printf("请传递一个端口号\n");
return -1;
}
//将接收端口号并转换为int
int port = atoi(argv[1]);
if( port<1025 || port>65535 )//0~1024一般给系统使用,一共可以分配到65535
{
printf("端口号范围应为1025~65535");
return -1;
}
//1 创建tcp通信socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(socket_fd == -1)
{
perror("创建tcp通信socket失败!\n");
return -1;
}
//2 绑定socket地址
struct sockaddr_in server_addr = {
0};//存放地址信息
server_addr.sin_family = AF_INET;//AF_INET->IPv4
server_addr.sin_port = htons(port);//端口号
server_addr.sin_addr.s_addr = INADDR_ANY;//让系统检测本地网卡,自动绑定本地IP
int mw_optval = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&mw_optval,sizeof(mw_optval));
int ret = bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr) );
if(ret == -1)
{
perror("bind failed!\n");
return -1;
}
//3 设置监听队列,设置为可以接受5个客户端连接
ret = listen(socket_fd, 5);
if(ret == -1)
{
perror("listen falied!\n");
}
printf("server is running!\n");
struct sockaddr_in client_addr = {
0};//用来保存客户端的地址信息
int len = sizeof(client_addr);
int socket_fd_arr[5] = {
-1, -1, -1, -1, -1};//用来保存5个客户端的通信socket
//定义一个文件描述符集合
fd_set fds;
//定义一个时间结构体
struct timeval time;
time.tv_sec = 3;//超时时间
time.tv_usec = 0;
//循环监视文件描述符集合
int new_socket_fd = -1;
int max_socket_fd = -1;//保存最大的socket描述符
int index = -1, i;
//循环监视文件描述符集合
while(1)
{
//清空文件描述符集合
FD_ZERO(&fds);
//把标准输入设备加入到集合中
FD_SET(0, &fds);
FD_SET(socket_fd, &fds);//将监听socket添加到集合中
//把网络通信文件描述符加入到集合中
for(i=0; i<=index; i++)
{
FD_SET(socket_fd_arr[i],&fds);
}
//获取最大的通信socket描述符。
max_socket_fd = get_max_socket_fd(socket_fd_arr);
if(max_socket_fd == -1)
{
max_socket_fd = socket_fd;
}
ret = select(max_socket_fd+1,&fds,NULL,NULL,0);//检查集合中是否有活跃的描述符
if(ret < 0)//错误
{
perror("select fail");
return -1;
}
else if(ret == 0) //超时
{
printf("timeout1\n");
}
else if(ret > 0) //有活跃的
{
if(FD_ISSET(socket_fd,&fds))//监听socket活跃,说明有客户端请求连接
{
new_socket_fd = accept( socket_fd, (struct sockaddr *)&client_addr, &len);
if(new_socket_fd == -1)
{
perror("accpet error!\n");
continue;
}
else
{
if(index>=5)
{
index = 4;
printf("index>=5\n");
continue;
}
socket_fd_arr[++index] = new_socket_fd;//将通信socket保存到数组中
printf("[ID:%d] IP:%s, PORT:%d [connected]\n", index, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
}
//判断是否 标准输入设备活跃 假设是则发送数据
if(FD_ISSET(0,&fds))
{
char buf[1024] = {
0};
int client = -1;
scanf("%d %s", &client, buf);
if(client>=0 && client<=index)
{
write(socket_fd_arr[client],buf,strlen(buf));
}
else //给所有的客户端发消息
{
for(i=0; i<=index; i++)
{
write(socket_fd_arr[i],buf,strlen(buf));
}
}
if(strcmp(buf, "exit") == 0)
{
break;
}
}
//判断是否有收到消息
for(i=0; i<=index; i++)
{
if(FD_ISSET(socket_fd_arr[i],&fds))//判断通信socket是否有活跃
{
char buf[1024] = {
0};
read(socket_fd_arr[i], buf, sizeof(buf)); //读出数据
if(strcmp(buf, "exit") == 0 || strcmp(buf, "") == 0)
{
break;
}
else if(strlen(buf)>0)
{
printf("receive msg form [ID:%d]:%s\n", i, buf); //打出数据
}
}
}
}
}
//5 关闭通信socket
close(new_socket_fd);
close(socket_fd);
return 0;
}
//获取最大的通信socket描述符。
int get_max_socket_fd(int socket_arr[])
{
int max = -1;
int i;
for(i=0; i<5; i++)
{
if(socket_arr[i]>max)
{
max = socket_arr[i];
}
}
return max;
}
client.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h> //atoi()
int main(int argc, char *argv[])
{
//检查命令行参数是否匹配
if(argc != 3)
{
printf("请传递要连接的服务器的ip和端口号");
return -1;
}
int port = atoi(argv[2]); //从命令行获取端口号
if( port<1025 || port>65535 ) //0~1024一般给系统使用,一共可以分配到65535
{
printf("端口号范围应为1025~65535");
return -1;
}
//1 创建tcp通信socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(socket_fd == -1)
{
perror("socket failed!\n");
}
//2 连接服务器
struct sockaddr_in server_addr = {
0};//服务器的地址信息
server_addr.sin_family = AF_INET;//IPv4协议
server_addr.sin_port = htons(port);//服务器端口号
server_addr.sin_addr.s_addr = inet_addr(argv[1]); //设置服务器IP
int ret = connect(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); //连接服务器
if(ret == -1)
{
perror("connect failed!\n");
}
else
{
printf("connect server successful!\n");
}
//要监视的描述符集合
fd_set fds;
//3 send msg
while(1)
{
FD_ZERO(&fds); //清空文件描述符集合
FD_SET(0,&fds); //把标准输入设备加入到集合中
FD_SET(socket_fd,&fds); //把网络通信文件描述符加入到集合中
ret = select(socket_fd+1,&fds,NULL,NULL,NULL);
if(ret < 0)//错误
{
perror("select fail:");
return -1;
}
else if(ret > 0) //有活跃的
{
//判断是否 标准输入设备活跃 假设是则发送数据
if(FD_ISSET(0,&fds))
{
char buf[1024] = {
0};
scanf("%s",buf);
write(socket_fd,buf,strlen(buf));
if(strcmp(buf, "exit") == 0)
{
break;
}
}
//判断是否有收到数据
if(FD_ISSET(socket_fd,&fds))
{
char buf[1024]={
0};
read(socket_fd,buf,sizeof(buf));
printf("receive msg:%s\n",buf);
if(strcmp(buf, "exit") == 0 || strcmp(buf, "") == 0)
{
break;
}
}
}
}
//4 关闭通信socket
close(socket_fd);
return 0;
}
服务器:
- gcc server.c
- ./a.out 9999
客户端: - gcc client.c
- ./a.out 127.0.0.1 9999
运行结果:
注意:因为输入信息使用的是scanf所以发送数据只能发送一个单词,遇到空格会出错。服务器发送信息格式 客户端编号 + 空格 + 一个单词
服务器:
server is running!
[ID:0] IP:127.0.0.1, PORT:59898 [connected] //可能不同
[ID:1] IP:127.0.0.1, PORT:59900 [connected] //可能不同
receive msg form [ID:0]:hello //接收来自客户端0的信息
0 hi //向客户端0发送信息
receive msg form [ID:1]:hi //接收来自客户端1的信息
1 hello //向客户端1发送信息
客户端0:
connect server successful!
hello //发送给服务器
receive msg:hi //接收到来自服务器的信息
客户端1:
connect server successful!
hi //发送给服务器
receive msg:hello //接收到来自服务器的信息
poll
因为select有套接字个数上有限制,要解决这个问题才出现了poll,我学习poll是看的别人的一篇博客,这里我就不多说了 POLL讲解 因为这篇博客给的代码中只有服务器(server)代码,客户端代码可以使用上面select中的client.c的代码。