我們知道用socket進行通信時,發送數據和接收數據所使用的recv/send函數會阻塞進程,只有收到或發送數據後才能返回值,導致是socket通信只能實現服務器和客戶端交替收發數據,而使用select可以很好地解決這個問題。
諸如connect、accept、recv或recvfrom這樣的阻塞程序(所謂阻塞方式block,顧名思義,就是進程或是線程執行到這些函數時必須等待某個事件的發生,如果事件沒有發生,進程或線程就被阻塞,函數不能立即返回)。可是使用Select就可以完成非阻塞(所謂非阻塞方式non-block,就是進程或線程執行此函數時不必非要等待事件的發生,一旦執行肯定返回,以返回值的不同來反映函數的執行情況,如果事件發生則與阻塞方式相同,若事件沒有發生則返回一個代碼來告知事件未發生,而進程或線程繼續執行,所以效率較高)方式工作的程序,它能夠監視我們需要監視的文件描述符的變化情況——讀寫或是異常。
select函數的原型:int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout); 關於select函數的用法和作用,大家看看這個帖子
這個帖子已經講的很詳細了,大家可以看一看,關於select如何解決socket的異步通信問題,個人的理解:select的作用在於,以輪詢的方式 同時對“自己有無發送數據” “對方有無發送數據”同時進行監控。而每次檢測的時間限制爲 struct timeval * timeout 中自己設置的時間,在檢測的這段時間內,進程/線程會阻塞。這樣就可以不斷地交替對“ 是否發送了數據”和“是否接收到數據”進行不斷交替檢測,這樣,進程就不會被recv或是send阻塞,從而實現了異步通信。
代碼如下:
服務器 server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <iostream>
#include <stdlib.h>
#include <netinet/in.h>
#define PORT 7000
#define QUEUE 20
int main() {
fd_set rfds;
struct timeval tv;
int retval, maxfd;
int listenfd= socket(AF_INET, SOCK_STREAM, 0);
//設置地址可重複方式
int opt = SO_REUSEADDR;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd, (struct sockaddr* ) &server_sockaddr, sizeof(server_sockaddr))==-1) {
perror("bind");
exit(1);
}
if(listen(listenfd, QUEUE) == -1) {
perror("listen");
exit(1);
}
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
int fd = accept(listenfd, (struct sockaddr*)&client_addr, &length);
if( fd < 0 ) {
perror("connect");
exit(1);
}
printf("You got connected from:%s\n",inet_ntoa(client_addr.sin_addr));
while(1) {
/*把可讀文件描述符的集合清空*/
FD_ZERO(&rfds);
/*把 標準輸入 的文件描述符加入到集合中*/
FD_SET(0, &rfds);
maxfd = 0;
/*把當前連接的文件描述符加入到集合中*/
FD_SET(fd, &rfds);
/*找出文件描述符集合中最大的文件描述符*/
if(maxfd < fd)
maxfd = fd;
/*設置超時時間*/
tv.tv_sec = 5;
tv.tv_usec = 0;
/*等待聊天*/
retval = select(maxfd+1, &rfds, NULL, NULL, &tv);
if(retval == -1){
printf("select出錯,客戶端程序退出\n");
break;
}else if(retval == 0){
continue;
}else{
/*客戶端發來了消息*/
if(FD_ISSET(fd,&rfds)){
char buffer[1024];
memset(buffer, 0 ,sizeof(buffer));
int len = recv(fd, buffer, sizeof(buffer), 0);
if(strcmp(buffer, "exit\n") == 0) exit(0);
printf("From client:%s", buffer);
}
/*用戶輸入信息了,開始處理信息併發送*/
if(FD_ISSET(0, &rfds)){
char buf[1024];
fgets(buf, sizeof(buf), stdin);
send(fd, buf, sizeof(buf), 0);
if(strcmp(buf, "exit\n") == 0) exit(0);
}
}
}
close(fd);
close(listenfd);
return 0;
}
客戶端 client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <stdlib.h>
#define PORT 7000
#define BUFFER_SIZE 1024
int main()
{
int fd;
fd_set rfds;
struct timeval tv;
int retval, maxfd;
fd = socket(AF_INET,SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("connect");
exit(1);
}
while(1){
/*把可讀文件描述符的集合清空*/
FD_ZERO(&rfds);
/*把標準輸入的文件描述符加入到集合中*/
FD_SET(0, &rfds);
maxfd = 0;
/*把當前連接的文件描述符加入到集合中*/
FD_SET(fd, &rfds);
/*找出文件描述符集合中最大的文件描述符*/
if(maxfd < fd)
maxfd = fd;
/*設置超時時間*/
tv.tv_sec = 5;
tv.tv_usec = 0;
/*等待聊天*/
retval = select(maxfd+1, &rfds, NULL, NULL, &tv);
if(retval == -1){
printf("select出錯,客戶端程序退出\n");
break;
}else if(retval == 0){
continue;
}else{
/*服務器發來了消息*/
if(FD_ISSET(fd,&rfds)){
char recvbuf[BUFFER_SIZE];
int len;
len = recv(fd, recvbuf, sizeof(recvbuf),0);
printf("From server:%s", recvbuf);
if(strcmp(recvbuf,"exit\n")==0) exit(0);
memset(recvbuf, 0, sizeof(recvbuf));
}
/*用戶輸入信息了,開始處理信息併發送*/
if(FD_ISSET(0, &rfds)){
char sendbuf[BUFFER_SIZE];
fgets(sendbuf, sizeof(sendbuf), stdin);
send(fd, sendbuf, strlen(sendbuf),0);
if(strcmp(sendbuf,"exit\n")==0) exit(0);
memset(sendbuf, 0, sizeof(sendbuf));
}
}
}
close(fd);
return 0;
}
個人理解:以客戶端爲例,在上述代碼中,若FD_ISSET(fd,&rfds)不爲0,則表示接收到了數據,這是因爲socket的數據傳輸是通過讀寫文件描述符來實現的,所以服務器發送數據時,select可以檢測到文件描述符已被讀寫,於是接收數據。而select中0 1 2分別表示標準輸入,標準輸出,標準錯誤。符若FD_ISSET(0,&rfds)不爲0,則表示用戶已經輸入了數據,於是發送數據。