linux網路程式設計之用select函式實現io複用(基於TCP)引發的思考
1、基本概念
IO多路複用是指核心一旦發現程序指定的一個或者多個IO條件準備讀取,它就通知該程序。IO多路複用適用如下場合:
(1)當客戶處理多個描述字時(一般是互動式輸入和網路套介面),必須使用I/O複用。
(2)當一個客戶同時處理多個套介面時,而這種情況是可能的,但很少出現。
(3)如果一個TCP伺服器既要處理監聽套介面,又要處理已連線套介面,一般也要用到I/O複用。
(4)如果一個伺服器即要處理TCP,又要處理UDP,一般要使用I/O複用。
(5)如果一個伺服器要處理多個服務或多個協議,一般要使用I/O複用。
與多程序和多執行緒技術相比,I/O多路複用技術的最大優勢是系統開銷小,系統不必建立程序/執行緒,也不必維護這些程序/執行緒,從而大大減小了系統的開銷。
2、select函式
該函式准許程序指示核心等待多個事件中的任何一個傳送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒。函式原型如下:
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就緒描述符的數目,超時返回0,出錯返回-1
函式引數介紹如下:
(1)第一個引數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(因此把該引數命名為maxfdp1),描述字0、1、2...maxfdp1-1均將被測試。
因為檔案描述符是從0開始的。
(2)中間的三個引數readset、writeset和exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指標。struct fd_set可以理解為一個集合,這個集合中存放的是檔案描述符,可通過以下四個巨集進行設定:
void FD_ZERO(fd_set *fdset); //清空集合 void FD_SET(int fd, fd_set *fdset); //將一個給定的檔案描述符加入集合之中 void FD_CLR(int fd, fd_set *fdset); //將一個給定的檔案描述符從集合中刪除 int FD_ISSET(int fd, fd_set *fdset); // 檢查集合中指定的檔案描述符是否可以讀寫
一定要注意:FD_ISSET函式,並不是判斷fd是不是在fdset集合裡面,理解如下
判斷描述符fd是否在給定的描述符集fdset中,通常配合select函式使用,由於select函式成功返回時會將未準備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函式返回後,某個描述符是否準備好,以便進行接下來的處理操作。 當描述符fd在描述符集fdset中返回非零值,否則,返回零。(3)timeout告知核心等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
struct timeval{ long tv_sec; //seconds long tv_usec; //microseconds };
這個引數有三種可能:
(1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。為此,把該引數設定為空指標NULL。
(2)等待一段固定時間:在有一個描述字準備好I/O時返回,但是不超過由該引數所指向的timeval結構中指定的秒數和微秒數。
(3)根本不等待:檢查描述字後立即返回,這稱為輪詢。為此,該引數必須指向一個timeval結構,而且其中的定時器值必須為0。
3、 原理圖:
4、服務端程式碼
實現服務端的併發,客戶端想服務端發訊息,然後服務端把收到的訊息再會給客戶端。
tcp_select.c檔案如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <assert.h>
#define IPADDR "127.0.0.1"
#define PORT 8888
#define MAXLINE 1024
#define LISTENQ 5
#define SIZE 100
typedef struct server_context_st
{
int cli_cnt;//客戶端個數
int clifds[SIZE];//客戶端的個數
fd_set allfds;//控制代碼集合
int maxfd;//控制代碼最大值
} server_context_st;
static server_context_st *s_srv_ctx = NULL;
//初始化服務端contxt
static int server_init()
{
s_srv_ctx = (struct server_context_st*)malloc(sizeof(server_context_st));
if (s_srv_ctx == NULL) {
return -1;
}
memset(s_srv_ctx, 0, sizeof(server_context_st));
for (int i = 0; i < SIZE; i++) {
s_srv_ctx->clifds[i] = -1;
}
return 0;
}
int create_server_proc(const char* ip, int port)
{
int fd;
struct sockaddr_in server_addr;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
fprintf(stderr, "create socket fail, errno:%d, reason:%s\n", errno, strerror(errno));
return -1;
}
//一個埠釋放後會等待2分鐘才能再次被使用,SO_REUSEADDR是讓埠釋放後立即可以被再次使用
int reuse = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
return -1;
}
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &server_addr.sin_addr);
server_addr.sin_port = htons(port);
if (bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind error:");
return -1;
}
listen(fd, LISTENQ);
return fd;
}
static int accept_client_proc(int srvfd)
{
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
cliaddrlen = sizeof(cliaddr);
int clifd = -1;
puts("accept client proc is call");
ACCEPT:
clifd = accept(srvfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
if (clifd == -1) {
if (errno == EINTR) {
goto ACCEPT;
} else {
fprintf(stderr, "accept fail error:%s\n", strerror(errno));
return -1;
}
}
fprintf(stdout, "accept a new client %s:%d\n", inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
//將新的連線描述符新增到陣列中
int i = 0;
for(i = 0; i < SIZE; i++) {
if (s_srv_ctx->clifds[i] < 0) {
s_srv_ctx->clifds[i] = clifd;
s_srv_ctx->cli_cnt++;
break;
}
}
if (i == SIZE) {
fprintf(stderr, "too many clients\n");
return -1;
}
}
static int handle_client_msg(int fd, char* buf)
{
assert(buf);
printf("recv buf is %s\n", buf);
write(fd, buf, strlen(buf) + 1);
return 0;
}
static void recv_client_msg(fd_set *readfds)
{
int i = 0, n = 0;
int clifd;
char buf[MAXLINE] = {0};
for (i = 0; i <= s_srv_ctx->cli_cnt; i++) {
clifd = s_srv_ctx->clifds[i];
if (clifd < 0) {
continue;
}
//判斷客戶端套接字是否有資料
if (FD_ISSET(clifd, readfds)) {
//接收客戶端訊息
n = read(clifd, buf, MAXLINE);
if (n <= 0) {
// n == 0表示讀取完才,客戶都關閉套接字
FD_CLR(clifd, &s_srv_ctx->allfds);
close(clifd);
s_srv_ctx->clifds[i] = -1;
continue;
}
handle_client_msg(clifd, buf);
}
}
}
//開始接收並且處理客戶端的請求
static void handle_client_proc(int srvfd)
{
int clifd = -1;
int retval = 0;
fd_set *readfds = &s_srv_ctx->allfds;
struct timeval tv;
int i = 0;
while (1) {
//每次呼叫select前都要重新設定檔案描述符和時間,因為事件發生之後,檔案描述符和時間都被核心修改
FD_ZERO(readfds);
FD_SET(srvfd, readfds);
s_srv_ctx->maxfd = srvfd;
tv.tv_sec = 30;
tv.tv_usec = 0;
//新增客戶端套接字
for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
clifd = s_srv_ctx->clifds[i];
//去除無效的客戶端控制代碼
if (clifd != -1) {
FD_SET(clifd, readfds);
}
s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
}
//開始輪詢接收處理服務端和客戶端套接字
retval = select(s_srv_ctx->maxfd + 1, readfds, NULL, NULL, &tv);
if (retval == -1) {
fprintf(stderr, "select error %s\n", strerror(errno));
return;
}
if (retval == 0) {
fprintf(stdout, "select is timeout\n");
continue;
}
if (FD_ISSET(srvfd, readfds))
{
//監聽客戶端請求
accept_client_proc(srvfd);
} else {
//接受處理客戶端的訊息
recv_client_msg(readfds);
}
}
}
static void server_uninit()
{
if (s_srv_ctx) {
free(s_srv_ctx);
}
s_srv_ctx = NULL;
}
int main()
{
int srvfd;
//初始化服務端contxt
if (server_init() < 0)
{
return -1;
}
//建立服務,開始監聽
srvfd = create_server_proc(IPADDR, PORT);
if (srvfd < 0)
{
fprintf(stderr, "socket create or bind failed\n");
goto error;
}
//開始接收並處理客戶端請求
handle_client_proc(srvfd);
server_uninit();
return 0;
error:
server_uninit();
return -1;
}
5、客戶端程式碼
tcp_select_client.c檔案如下
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#define MAXLINE 1024
#define IPADDRESS "127.0.0.1"
#define SERV_PORT 8888
#define max(a,b) (a > b) ? a : b
static void handle_recv_msg(int sockfd, char *buf)
{
printf("client recv msg is:%s\n", buf);
sleep(5);
write(sockfd, buf, strlen(buf) +1);
}
static void handle_connection(int sockfd)
{
char sendline[MAXLINE],recvline[MAXLINE];
int maxfdp,stdineof;
fd_set readfds;
int n;
struct timeval tv;
int retval = 0;
while (1) {
FD_ZERO(&readfds);
FD_SET(sockfd,&readfds);
maxfdp = sockfd;
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(maxfdp+1,&readfds,NULL,NULL,&tv);
if (retval == -1) {
return ;
}
if (retval == 0) {
printf("client timeout.\n");
continue;
}
if (FD_ISSET(sockfd, &readfds)) {
n = read(sockfd,recvline,MAXLINE);
if (n <= 0) {
fprintf(stderr,"client: server is closed.\n");
close(sockfd);
FD_CLR(sockfd,&readfds);
return;
}
handle_recv_msg(sockfd, recvline);
}
}
}
int main(int argc,char *argv[])
{
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
int retval = 0;
retval = connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
if (retval < 0) {
fprintf(stderr, "connect fail,error:%s\n", strerror(errno));
return -1;
}
printf("client send to server .\n");
write(sockfd, "hello server", 32);
handle_connection(sockfd);
return 0;
}
6、執行結果
7、FD_ISSET函式的迷惑和總結
while (1) {
//每次呼叫select前都要重新設定檔案描述符和時間,因為事件發生之後,檔案描述符和時間都被核心修改
FD_ZERO(readfds);
FD_SET(srvfd, readfds);
s_srv_ctx->maxfd = srvfd;
tv.tv_sec = 30;
tv.tv_usec = 0;
//新增客戶端套接字
for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
clifd = s_srv_ctx->clifds[i];
//去除無效的客戶端控制代碼
if (clifd != -1) {
FD_SET(clifd, readfds);
}
s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
}
//開始輪詢接收處理服務端和客戶端套接字
retval = select(s_srv_ctx->maxfd + 1, readfds, NULL, NULL, &tv);
if (retval == -1) {
fprintf(stderr, "select error %s\n", strerror(errno));
return;
}
if (retval == 0) {
fprintf(stdout, "select is timeout\n");
continue;
}
if (FD_ISSET(srvfd, readfds))
{
//監聽客戶端請求
accept_client_proc(srvfd);
} else {
//接受處理客戶端的訊息
recv_client_msg(readfds);
}
}
一開始,很明顯
FD_SET(srvfd, readfds);
然後select函式掛起,如果有事件發生就觸發了select函式,然後進行判斷
if (FD_ISSET(srvfd, readfds))
剛才才把srvfd加到readds裡面去,現在判斷是不是在裡面,肯定在呀,那程式怎麼進入else裡面呢?這不日了狗嗎?一直不理解,日了狗,後面再群裡面問了也不是太懂,可能自己計算機資質太差了,然後吃完飯問搞服務端的人,才搞明白。
我們先看下函式FD_ISSET函式解釋
一定要注意:FD_ISSET函式,並不是判斷fd是不是在fdset集合裡面,理解如下
判斷描述符fd是否在給定的描述符集fdset中,通常配合select函式使用,由於select函式成功返回時會將未準備好的描述符位清零。通常我們使用FD_ISSET是為了檢查在select函式返回後,某個描述符是否準備好,以便進行接下來的處理操作。 當描述符fd在描述符集fdset中返回非零值,否則,返回零。當第一個客戶端第一次執行Connect的函式的時候會呼叫select函式,然後這是時候srvfd,準備就緒,設定為1,所以FD_ISSET函式大於0,所以會執行accept_client_proc函式,這個時候服務端得到客戶端的fd,也就是clifd,因為執行TCP三次握手之後,客戶端再次發訊息,這個時候客戶端和服務端通訊也就是服務端新的clifd通訊,當客戶端發訊息的時候,srvfd基本上就被遺棄了,沒有準備好,會把srvfd清0,所以這個時候執行FD_ISSET函式的話,程式會跑到else裡面去,也就是執行recv_client_msg函式,好了,終於搞懂了,不知道理解有沒有錯,明天再學習poll epoll來實現io複用。
select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個優點。select的一 個缺點在於單個程序能夠監視的檔案描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改巨集定義甚至重新編譯核心的方式提升這一限制,但 是這樣也會造成效率的降低。