這一節主要來說一下如何用select函數來改進我們前面的客戶端-服務器模型。
前面我們在處理多客戶端模型時,每當連接一個客戶端時,服務器端就需要開闢一個新的進程來處理新的客戶端,這樣就會耗費很大的內存資源。
而select函數允許進程指示內核等待多個事件中的任何一個發生,並只有在有一個或多個事件事件發生或經歷一段指定的時間後才喚醒它。或者說select具有管理多個I/O的能力,對於多個套接口,一旦某個套接口發生了我們所感興趣的事件,select函數返回,返回值爲監測到的事件個數。而且由於select函數的參數是“值—結果”型的,因此我們也知道哪些套接口發生了事件,然後遍歷這些套接口並處理相關事件。
關於select參數的具體用法這裏就不做過多說明,大家可以參考網上資料或者《UNIX網絡編程:卷一》上的相關內容。
下面給出改進後的回射客戶-服務器模型的代碼。
服務器端:echosrv
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
ssize_t readn(int fd, void* buf, size_t count)
{
//由於不能保證一次能夠讀取count個字節
//因此我們需要循環進行讀取
//直到讀取的字節數爲count
size_t nleft = count;
ssize_t nread;
char* bufp = (char*)buf;
while(nleft > 0)
{
if((nread = read(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)//如果信號中斷
continue;
return -1;
}
if(nread == 0)
//表示對等方關閉,這裏直接返回
return count-nleft;
nleft -= nread;//每次讀取後剩餘的字節數
bufp += nread;
}
return count;
}
ssize_t writen(int fd, void* buf, size_t count)
{
//我們每次希望寫入的字節數爲count
size_t nleft = count;
ssize_t nwritten;
char* bufp = (char*)buf;
while(nleft > 0)
{
if((nwritten = write(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
if(nwritten == 0)
//什麼都沒發生
continue;
nleft -= nwritten;//每次寫後剩餘要寫的字節數
bufp += nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void* buf, size_t len)
{
//該函數可以從套接口接收數據
//但是並不將數據從緩衝區中移除
while(1)
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
//讀到數據就返回,否則就返回
if(ret == -1 && errno == EINTR)
continue;
return ret;
}
}
ssize_t readline(int sockfd, void*buf, size_t maxline)
{
//讀取過程不一定要讀取maxline個字節
//只要遇到\n就可以返回
int ret;
int nread;
char* bufp = buf;
int nleft = maxline;
while(1)
{
ret = recv_peek(sockfd, bufp, nleft);
if(ret < 0)
return ret;
else if(ret == 0)
return ret;
nread = ret;
int i;
for(i = 0; i < nread; ++i)
{
if(bufp[i] == '\n')
{
//我們的recv_peek只是偷窺一下數據
//並沒有一走數據
//所以這裏用readn從緩衝區中移除已偷窺的數據
ret = readn(sockfd, bufp, i+1);
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//沒有遇到\n
if(nread > nleft)
exit(EXIT_FAILURE);
//把讀到的數據nread個字節從緩衝區中移走
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if(ret != nread)
exit(EXIT_FAILURE);
//繼續下一次的偷窺,需偏移
bufp += nread;
}
return -1;
}
void echo_srv(int conn)
{
char recvbuf[1024];
while(1)
{
memset(&recvbuf, 0, sizeof(recvbuf));
int ret = readline(conn, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("read failure");
if(ret == 0)
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
void handle_sigchld(int sig)
{
/* wait(NULL);*/
while(waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(void)
{
//避免殭屍進程
/* signal(SIGCHLD, SIG_IGN);//忽略
*/
signal(SIGCHLD, handle_sigchld);
//創建一個套接字
int listenfd;
if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前兩個參數已經可確定是TCP,所以第三個參數可以置0
// if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前兩個參數已經可確定是TCP,所以第三個參數可以置0
ERR_EXIT("socket_failure");
//初始化地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;//地址族
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示本機的任意地址,推薦使用(轉換成網絡字節序)
//也可以自己顯式指定
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//或者
// inet_aton("127.0.0.1", &servaddr.sin_addr);
//綁定之前開啓地址重複利用
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt_failure");
//接下來進行綁定,將該套接字與一個本地地址進行綁定
//需要將IPv4地址結構強制轉換爲通用地址結構
if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind_failure");//綁定失敗
//接下來是監聽,將socket從close狀態轉爲監聽狀態才能夠接受連接
if(listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen_failure");
//定義一個對方的地址
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn; //一個新的套接字,稱爲已連接套接字(主動套接字)
/*
pid_t pid;
while(1)
{
if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
ERR_EXIT("accept_failure");
//輸出客戶端的地址和端口
printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
//一旦獲得一個連接,就創建一個進程
//處理多個客戶端,達到併發的目的
pid = fork();
if(pid == -1)
ERR_EXIT("fork_failure");
if(pid == 0)
{
//讓子進程處理已有的通信過程
//不再需要監聽套接口
close(listenfd);
echo_srv(conn);
//一旦do_service函數返回,那麼該進程就沒有存在的價值了
exit(EXIT_SUCCESS);//此時,爲客戶端開闢的進程也銷燬了
}
else
//父進程進行accept
//不再需要連接套接口了,即conn(父子進程共享文件描述符)
close(conn);
}
//實現一個回射客戶/服務器模型
//即客戶端從標準輸入獲取數據,發送給服務器端,服務器端再回射過去
*/
//改爲用select實現
int client[FD_SETSIZE];//select最多能處理的事件個數
int i;
for(i = 0; i < FD_SETSIZE; ++i)
client[i] = -1;//初始化,-1表示空閒狀態
int maxi = 0; //最大不空閒位置
int nready;
int maxfd = listenfd;//3
fd_set rset;//定義一個讀集合
fd_set allset;
FD_ZERO(&rset);
FD_ZERO(&allset);
FD_SET(listenfd, &allset);//先把監聽套接口加進去
while(1)
{
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(nready == -1)
{
if(errno == EINTR)
continue;
ERR_EXIT("select");
}
if(nready == 0)
continue;
if(FD_ISSET(listenfd, &rset))
{
peerlen = sizeof(peeraddr);
//監聽套接口產生事件
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
if(conn == -1)
ERR_EXIT("accept error");
for(i = 0; i < FD_SETSIZE; ++i)
{
if(client[i] < 0)
{
//找一個空閒位置把該套接口放進去
client[i] = conn;
if(maxi < i)
maxi = i;
break;
}
}
if(i == FD_SETSIZE)
{
fprintf(stderr, "too many clients\n");
exit(EXIT_FAILURE);
}
printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
FD_SET(conn, &allset);
if(conn > maxfd)
maxfd = conn;
if(--nready <= 0)
continue;
}
//已連接套接口也可能產生事件
for(i = 0; i < FD_SETSIZE; ++i)
{
conn = client[i];
if(conn == -1)
continue;
if(FD_ISSET(conn, &rset))
{
//意味着產生了可讀事件
char recvbuf[1024] = {0};
int ret = readline(conn, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("readline error");
if(ret == 0)
{
//對等方關閉
printf("client close\n");
FD_CLR(conn, &allset);//將該套接口清除
client[i] = -1;
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
if(--nready <= 0)
break;
}
}
}
return 0;
}
客戶端:echocli.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
ssize_t readn(int fd, void* buf, size_t count)
{
size_t nleft = count; //剩餘要讀取的字節數
ssize_t nread; //每次讀取的字節數
char* bufp = (char*)buf;
while(nleft > 0)
{
if((nread = read(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
if(nread == 0)//對等方關閉
return count-nleft;
nleft -= nread;
bufp += nread;
}
return count;
}
ssize_t writen(int fd, void* buf, size_t count)
{
size_t nleft = count; //剩餘要讀取的字節數
ssize_t nwritten; //每次讀取的字節數
char* bufp = (char*)buf;
while(nleft > 0)
{
if((nwritten = write(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
if(nwritten == 0)
continue;
nleft -= nwritten;
bufp += nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd, void* buf, size_t len)
{
while(1)
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
if(ret == -1 && errno == EINTR)
continue;
//偷窺到數據就直接返回
return ret;
}
}
ssize_t readline(int sockfd, void*buf, size_t maxline)
{
char* bufp = buf;
int nleft = maxline;
int nread;
int ret;
while(1)
{
ret = recv_peek(sockfd, bufp, nleft);
if(ret < 0)
return ret;
if(ret == 0)
return ret;
nread = ret;
int i;
for(i = 0; i < nread; ++i)
{
if(bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+1);
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//沒有遇到\n
if(nread > nleft)
exit(EXIT_FAILURE);
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if(ret != nread)
exit(EXIT_FAILURE);
//繼續下一次偷窺
bufp += nread;
}
return -1;
}
void echo_cli(int sock)
{
/*
//如果連接成功,就可以進行通信
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
writen(sock, sendbuf, strlen(sendbuf));
int ret = readline(sock, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("read error");
else if(ret == 0)
{
printf("peer close\n");
break;
}
//顯示出來
fputs(recvbuf, stdout);
//這裏需要清空緩衝區
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
*/
//該爲select用法
fd_set rset;
FD_ZERO(&rset);
//檢測標準輸入是否產生了可讀事件
int nready;
int maxfd;
int fd_stdin = fileno(stdin);//標準輸入
char recvbuf[1024] = {0};
char sendbuf[1024] = {0};
//有兩個文件描述符,fd_stdin和sock
if(fd_stdin > sock)
maxfd = fd_stdin;
else
maxfd = sock;
while(1)
{
FD_SET(fd_stdin, &rset);
FD_SET(sock, &rset);
//這裏只有讀的集合
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if(nready == -1)
ERR_EXIT("select error");
if(nready == 0)
continue;
//如果檢測到了事件,那麼rset就會發生改變
//裏面會包含哪些套接口發生了事件
if(FD_ISSET(sock, &rset))
{
//套接口產生了可讀
//按行讀取
int ret = readline(sock, recvbuf, sizeof(recvbuf));
if(ret == -1)
ERR_EXIT("readline error");
else if(ret == 0)
{
printf("server close\n");
break;
}
//顯示出來
fputs(recvbuf, stdout);
memset(recvbuf, 0, sizeof(recvbuf));
}
if(FD_ISSET(fd_stdin, &rset))
{
//標準輸入產生事件,輸入緩衝區有內容,用fgets去清空
if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
break;
write(sock, sendbuf, strlen(sendbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
}
close(sock);
}
void handle_sigpipe(int sig)
{
printf("recv a sig=%d\n", sig);
}
int main(void)
{
signal(SIGPIPE, handle_sigpipe);
int sock;//創建一個套接字
if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前兩個參數已經可確定是TCP,所以第三個參數可以置0
// if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前兩個參數已經可確定是TCP,所以第三個參數可以置0
ERR_EXIT("socket_failure");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;//地址族
servaddr.sin_port = htons(5188);
//自己顯式指定服務器端地址
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//客戶端不需要綁定(bind),也不需要監聽(listen)
//直接連接過去就可以
if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect_failure");
//連接成功,查看本地的端口和地址
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
ERR_EXIT("getsockname error");
printf("IP=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
echo_cli(sock);
return 0;
}
說明:FD_SETSIZE是系統指定的select所能監聽的事件的最大值,一般爲1024。