socket编程之select()
select()函数是异步IO,等待有事件来临时会触发。相对于fork()的有点就是占用的资源较少。
服务端代码
//
// Tcp_server.cpp
// Cpp
//
// Created by JH on 2020/4/8.
// Copyright © 2020 JH. All rights reserved.
//#include <stdio.h>
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>#define PORT 8111
#define MESSAGE_LEN 1024
#define FD_SIZE 1024 // 最大的socket连接数int main(int argc,char * argv[]){int ret = -1;int socket_fd =-1,accept_fd=-1;int backlog = 10;//等待连接队列的最大长度。int flags =1 ;int max_fd = -1; // 集合中所有文件描述符的范围,int accept_fds[FD_SIZE] = {-1,};int maxposition=0; //最大位置int curposition=-1;//当前位置int events = 0;fd_set fd_sets;//用于存储接收的socket的fd ,fd_set是数组,每个元素与已打开的fd建立联系char in_buff[MESSAGE_LEN] ={0};struct sockaddr_in localaddr,remoteaddr;socket_fd = socket(AF_INET, SOCK_STREAM, 0);//创建服务端侦听的socketif (socket_fd == -1) {exit(-1);}ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));if (ret == -1) {std:: cout << "set option failed" << std::endl;exit(-1);}//设置socket的性质为非阻塞状态flags = fcntl(socket_fd, F_GETFL,0);fcntl(socket_fd, F_SETFL,flags | O_NONBLOCK);localaddr.sin_family = AF_INET;localaddr.sin_port = PORT;localaddr.sin_addr.s_addr = INADDR_ANY;bzero(&(localaddr.sin_zero), 8);ret = bind(socket_fd, (struct sockaddr *)&localaddr, sizeof(struct sockaddr));if (ret == -1) {std:: cout << "set bind failed" << std::endl;exit(-1);}ret = listen(socket_fd, backlog);if (ret == -1) {std:: cout << "set listen failed" << std::endl;exit(-1);}ssize_t size;max_fd = socket_fd; // 每次重新设置max_fdfor (int i = 0; i <FD_SIZE; i++) {accept_fds[i] = -1;}for (; ; ) {FD_ZERO(&fd_sets);//清空fd_sets都置为0FD_SET(socket_fd,&fd_sets); //侦听的socket设置进入fd_set,置为1std:: cout << "-----" << std::endl;for (int j=0; j < maxposition; j++) {if (accept_fds[j] != -1) {if (accept_fds[j] >max_fd) {max_fd = accept_fds[j];}FD_SET(accept_fds[j],&fd_sets);//将已连接的socket重新设置到fd_sets中}}//遍历所有的fd,有事件触发才会被会被调用,select执行系统会修改fd_sets中的内容events = select(max_fd + 1, &fd_sets, NULL, NULL, NULL);std::cout<<"events = "<<events<<std::endl;if (events < 0) {break;}else if (events == 0){continue;}else if (events){std::cout<<"events = "<<events <<" socket_fd: "<<socket_fd<<std::endl;if (FD_ISSET(socket_fd,&fd_sets)) { // 侦听的socket触发,表示这是新链接int a = 0;for (; a <FD_SIZE; a++) {if (accept_fds[a] == -1) {curposition = a;break;}}if (a == FD_SIZE) {std::cout << "链接已满"<<std::endl;continue;}socklen_t addr_len = sizeof(struct sockaddr);//接收请求accept_fd = accept(socket_fd, (struct sockaddr *)& remoteaddr, &addr_len);int flags = fcntl(accept_fd,F_GETFL, 0);//取出新链接的fd的相关选项fcntl(accept_fd, F_SETFL,flags |O_NONBLOCK);// 设置为非阻塞accept_fds[curposition] = accept_fd;if (curposition +1 >maxposition) {maxposition = curposition +1;}if (accept_fd >max_fd) {max_fd = accept_fd;}std::cout << "新链接的fd:"<<accept_fd << " curposition = " <<curposition<<std::endl;}for (int k = 0; k < maxposition ; k++) {if ((accept_fds[k] != -1) && FD_ISSET(accept_fds[k],&fd_sets)) { // 有事件时std:: cout <<"accpet event: " <<k << " accpetd:"<<accept_fds[k]<< std::endl;memset(in_buff, 0, MESSAGE_LEN);size = recv(accept_fds[k], &in_buff, MESSAGE_LEN, 0);if (size == 0) {close(accept_fds[k]);accept_fds[k] = -1;break;}std:: cout <<"recv : " <<in_buff << std::endl;send(accept_fds[k], (void *)in_buff, MESSAGE_LEN, 0);}}}}close(socket_fd);return 0;
}
select()函数要与fd_set、FD_ZERO、FD_SET、FD_ISSET配合使用:
fd_set:是存储文件描述符的数组,数组中的每个元素会已经打开的文件描述符建立联系。
FD_ZERO:用于清空fd_set,都置为0
FD_SET(fd,&fd_set):将fd设置到fd_set中某个位置1
FD_ISSET(d,&fd_set):判断socket_fd是否在fd_set中
select()函数执行后,系统会修改fd_set中的内容,因此应用层要重新设置fd_set中的内容
代码详解
- 服务端程序启动时,首先初始化监听的socket,随后监听的socket使用fcntl设置为非阻塞状态,并进行绑定。进入for循环前初始化accept_fds默认-1,它是用于存储客户端连接的socket文件描述符的数组。
//设置socket的性质为非阻塞状态flags = fcntl(socket_fd, F_GETFL,0);fcntl(socket_fd, F_SETFL,flags | O_NONBLOCK);
- 服务端随后进入for循环首先,清空fd_sets,将监听的sockt中设置到fd_sets中。然后maxposition用于找到最大的fd(select的一个参数需要一个最大的fd),并重新将accept_fds存到fd_sets中。(accept_fds是用于存储客户端连接的socket_id)。
FD_ZERO(&fd_sets);//清空fd_setsFD_SET(socket_fd,&fd_sets); //侦听的socket设置进入fd_setfor (int j=0; j < maxposition; j++) { //初始化的maxposition为0,有一个新链接进入+1if (accept_fds[j] != -1) { //获取最大的fdif (accept_fds[j] >max_fd) {max_fd = accept_fds[j];}FD_SET(accept_fds[j],&fd_sets); //将新加入链接的存入fd_sets}}
- 随后服务端程序执行到select()函数就被阻塞了。后面的程序不会执行,要等待事件来临触发。
events = select(max_fd + 1, &fd_sets, NULL, NULL, NULL);
- select()何时会被触发? 当客户端连接过来后,监听的socket此时侦听到有新的sockt过来,此时select()会被触发,并且返回值events大于0,表示有新的事件要处理。
- event大于0,使用FD_ISSET(socket_fd,&fd_sets)判断是不是新链接。由于侦听的scoket触发了,将select()函数唤醒,FD_ISSET()此时返回大于0;当select会
if (FD_ISSET(socket_fd,&fd_sets)) //判断是不是新连接
- 服务端使用accept()函数接收客户端的请求,返回一个新的文件描述符accept_fd,并将该accept_fd使用fcntl()设置为非阻塞状态。并存到accept_ids中。
//接收请求 accept_fd = accept(socket_fd, (struct sockaddr *)& remoteaddr, &addr_len); int flags = fcntl(accept_fd,F_GETFL, 0);//取出新链接的fd的相关选项 fcntl(accept_fd, F_SETFL,flags |O_NONBLOCK);// 设置为非阻塞
- 进入第二个for循环处理消息的接受和发送。由于已经有一个客户端连接了服务端maxpostion变为1,进入if中判断,但是新的请求还没有设置到fd_sets中,FD_ISSET()为0,跳出循环。
for (int k = 0; k < maxposition ; k++) {if ((accept_fds[k] != -1) && FD_ISSET(accept_fds[k],&fd_sets)) { // 有事件时...} }
- 随后程序回到最外层的for循环,重新设置fd_sets,并将accept_fds中的客户端请求的fd重新设置到fd_sets中。
- 程序第二次执行到select()时,由于没有新的请求,所以又被阻塞。
- 当第一个已经连接的客户端socket发送消息时,select()函数又会被触发,此时触发的不是侦听的socket_id,而是accept_ids[0]中请求的fd。并返回events大于0.
- 当再次进行FD_ISSET(socket_fd_&fd_sets)判断时,由于触发的事件并非侦听的scoket,返回的值为0。
- 进入第二个循环,开始处理消息的接收和发送。因为只有一个连接,此时maxpostion为1,在进行(accept_fds[k] != -1) && FD_ISSET(accept_fds[k],&fd_sets)判断时,accept_fds[0]为第一个请求的文件描述符(fd),第二个判断条件FD_ISSET()由于触发的是accept_fds[0]的请求,所以判断为真。
- recv接收该客户端的数据,send()发送给改连接数据。
- 跳出该循环,又回到最外层的for循环重新设置,当执行到select()函数时又被阻塞了。当有新的事件时又被唤醒,如此反复....